氦元素 – CUBA 應用程序新樣式主題

    CUBA 框架一直以來定位的目標是業務系統的開發。業務系統的界面通常是給後台員工使用的,看重的是功能實現。多年來,界面外觀和樣式並不是後台管理系統的主要關注點,界面中的控件也更緊湊,唯一的原因就是:在單一屏幕中擺放盡可能多的控件,以提供足夠多的功能。

    但是在最近十年裡,人們使用了越來越多的小尺寸觸控屏設備,遊戲規則因此而改變。要有易於觸控點擊的控件,它們之間需要有足夠的間距,防止被誤點擊。應用程序的配色也變得偏於使用雜色和對比色。所以,近些年的現代應用程序在設計上更注重樣式,其中非功能性需求都佔了很大的一部分。

    順應趨勢,我們很高興推出新可視化主題 – 氦元素(Helium)!與以前的樣式主題相比,氦元素主題更簡潔、明亮,視覺噪音也更少。另外很重要的一點:能夠以最少的工作量為最終用戶和 CUBA 開發人員提供樣式定製。

功能

    首先要提到的是,新主題是動態的,意味着您可以隨時在線更改應用程序樣式!以前,CUBA 使用 SCSS 變量定義顏色,這樣每次修改過後都需要重新編譯。而新主題依賴 CSS 定製屬性 ,即使沒有頁面刷新或重新登錄,這些屬性也可以在運行時立即起效。

預設配色

    開箱即用支持兩種預設的配色方案:淺色(light)和深色(dark)。

    終端用戶可選擇的主題需要通過下列屬性配置(主題安裝流程請參考後續章節):

cuba.theme.modes = light|dark
cuba.theme.defaultMode = light

    另外,也可以通過在線主題編輯器創建自定義的配色方案(點擊下方的在線示例和編輯器章節名稱了解更多細節)。

預設大小

    主題自帶三種內置的控件大小配置:小(small),中(medium)和大(large)。

    跟配色方案類似,也可以通過主題屬性修改:

cuba.theme.sizes = small|medium|large
cuba.theme.defaultSize = medium

設置界面

    設置界面可以通過主菜單的 Help -> Theme Settings 打開。這裏可以讓最終用戶自定義他們喜歡的配色和控件大小。該界面帶有幾乎所有的主要控件,所以用戶可以預覽他調整之後的界面大概什麼樣。

在線示例和編輯器

    關於新主題的另一個重要部分就是其在線交互式編輯器。這裏可以試試調整樣式變量,馬上就能看到調整結果 – 一旦樣式滿足您的需求,只需要點擊下載按鈕並按照提供的說明將其安裝到您的 CUBA 應用程序中即可。

    其實,在線編輯器最好的一點是:不只是開發人員可以使用。將這個鏈接發送給您的追求極致的客戶,這樣他們可以根據自己的偏好自定義配色,然後下載併發送結果給開發人員,只需要幾分鐘便可以應用新的樣式。同時,編輯器本身也允許導入顏色變量,這樣可以基於已有的配色做修改。

    如需使用自定義配色,需要基於氦元素創建主題擴展。

安裝

    該主題是通過 擴展插件 的形式發布。該擴展兼容 7.1.5 以上的 CUBA 應用程序(注意,我們推薦您跟蹤 CUBA 框架的 bugfix 版本並更新您的應用程序至最新版)。可以參考 Studio 的 相關章節 了解如何安裝擴展插件。

    擴展安裝完成后,可以通過下面的方式啟用新的主題:
    啟動應用程序,打開主菜單下的設置界面:Help -> Settings。在可視化主題選項中選擇 Helium 即可。

    如需將氦元素主題設為默認主題,可以在 web-app.properties 中添加:

cuba.web.theme = helium

    如果之前自定義設置過 cuba.themeConfig 屬性,別忘了也需要添加氦元素屬性:

cuba.themeConfig = +/com/haulmont/addon/helium/web/helium-theme.properties

    如需自定義主題屬性,可以在 web 模塊的主包路徑創建 helium-theme.properties,內容如下:

@include=com/haulmont/addon/helium/web/helium-theme.properties

cuba.theme.modes = light|dark
cuba.theme.defaultMode = light

cuba.theme.sizes = small|medium|large
cuba.theme.defaultSize = medium\

    然後在 cuba.themeConfig 屬性中註冊一下該文件:

cuba.themeConfig = +/com/example/project/helium-theme.properties

    如需深度定製該主題,包括應用自定義配色,則需要創建主題擴展。按照該說明操作。

路線圖

    我們計劃繼續開發氦元素主題並添加新功能,包括:

  • 深色模式改進
  • 在主題編輯器中提供更有用的模板
  • 在淺色和深色模式之間自動切換
  • 提供可自定義的 border radius 變量,同樣也基於 CSS 變量。
  • 提供 Figma 的 CUBA 套件。Figma 是 UI/UХ 設計師最流行的工具之一,他們可以用來創建界面模型。

結論

    我們很樂意看到新主題和在線編輯器能用在您的項目中,希望新主題能讓您的應用煥然一新。如果您有任何問題,可以在我們的論壇創建主題討論。項目源碼可以在 Github 找到: 主題 / 編輯器。期待您的反饋!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

Vue中key的作用

Vue中key的作用

key的特殊attribute主要用在Vue的虛擬DOM算法,在新舊Nodes對比時辨識VNodes。如果不使用keyVue會使用一種最大限度減少動態元素並且盡可能的嘗試就地修改、復用相同類型元素的算法,而使用key時,它會基於key的變化重新排列元素順序,並且會移除key不存在的元素。此外有相同父元素的子元素必須有獨特的key,重複的key會造成渲染錯誤。

描述

首先是官方文檔的描述,當Vue正在更新使用v-for渲染的元素列表時,它默認使用就地更新的策略,如果數據項的順序被改變,Vue將不會移動DOM元素來匹配數據項的順序,而是就地更新每個元素,並且確保它們在每個索引位置正確渲染。這個默認的模式是高效的,但是只適用於不依賴子組件狀態或臨時DOM狀態的列表渲染輸出,例如表單輸入值。為了給Vue一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key attribute,建議盡可能在使用v-for時提供key attribute,除非遍歷輸出的DOM內容非常簡單,或者是刻意依賴默認行為以獲取性能上的提升。
簡單來說,當在列表循環中使用key時,需要使用key來給每個節點做一個唯一標識,diff算法就可以正確的識別此節點,找到正確的位置直接操作節點,盡可能地進行重用元素,key的作用主要是為了高效的更新虛擬DOM。此外,使用index作為key是並不推薦的做法,其只能保證Vue在數據變化時強制更新組件,以避免原地復用帶來的副作用,但不能保證最大限度的元素重用,且使用index作為key在數據更新方面和不使用key的效果基本相同。

示例

首先定義一個Vue實例,渲染四個列表,分別為簡單列表與複雜列表,以及其分別攜帶key與不攜帶key時對比其更新渲染時的速度,本次測試使用的是Chrome 81.0,每次在Console執行代碼時首先會進行刷新重新加載界面,避免瀏覽器以及Vue自身優化帶來的影響。

<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>
</head>
<body>
    <div id="app">
        <ul>
            <li v-for="item in simpleListWithoutKey" >{{item}}</li>
        </ul>

        <ul>
            <li v-for="item in simpleListWithKey" :key="item" >{{item}}</li>
        </ul>

        <ul>
            <li v-for="item in complexListWithoutKey">
                <span v-for="value in item.list" v-if="value > 5">{{value}}</span>
            </li>
        </ul>

        <ul>
            <li v-for="item in complexListWithKey" :key="item.id">
                <span v-for="value in item.list" :key="value" v-if="value > 5">{{value}}</span>
            </li>
        </ul>

    </div>
</body>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {
            simpleListWithoutKey: [1, 2, 3, 4, 5, 6],
            simpleListWithKey: [1, 2, 3, 4, 5, 6],
            complexListWithoutKey:[
                {id: 1, list: [1, 2, 3]},
                {id: 2, list: [4, 5, 6]},
                {id: 3, list: [7, 8, 9]}
            ],
            complexListWithKey:[
                {id: 1, list: [1, 2, 3]},
                {id: 2, list: [4, 5, 6]},
                {id: 3, list: [7, 8, 9]}
            ],
        }
    })
</script>
</html>

簡單列表

在簡單列表的情況下,不使用key可能會比使用key的情況下在更新時的渲染速度更快,這也就是官方文檔中提到的,除非遍歷輸出的DOM內容非常簡單,或者是刻意依賴默認行為以獲取性能上的提升。在下面的例子中可以看到沒有key的情況下列表更新時渲染速度會快,當不存在key的情況下,這個列表直接進行原地復用,原有的節點的位置不變,原地復用元素,將內容更新為5678910,並添加了1112兩個節點,而存在key的情況下,原有的1234節點被刪除,56節點保留,添加了789101112六個節點,由於在DOM的增刪操作上比較耗時,所以表現為不帶key的情況下速度更快一些。

// 沒有key的情況下
console.time();
vm.simpleListWithoutKey = [5, 6, 7, 8, 9, 10, 11, 12];
vm.$nextTick(() => console.timeEnd());
// default: 2.193056640625ms
// 存在key的情況下
console.time();
vm.simpleListWithKey = [5, 6, 7, 8, 9, 10, 11, 12];
vm.$nextTick(() => console.timeEnd());
// default: 3.2138671875ms

原地復用可能會帶來一些副作用,文檔中提到原地復用這個默認的模式是高效的,但是只適用於不依賴子組件狀態或臨時DOM狀態的列表渲染輸出,例如表單輸入值。在不設置key的情況下,元素中沒有與數據data綁定的部分,Vue會默認使用已經渲染的DOM,而綁定了數據data的部分會進行跟隨數據渲染,假如操作了元素位置,則元素中未綁定data的部分會停留在原地,而綁定了data的部分會跟隨操作進行移動,在下面的例子中首先需要將兩個A之後的輸入框添加數據信息,這樣就製作了一個臨時狀態,如果此時點擊下移按鈕,那麼不使用key的組中的輸入框將不會跟隨下移,且B到了頂端並成為了紅色,而使用key的組中會將輸入框進行下移,且A依舊是紅色跟隨下移。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>就地復用</title>
</head>
<body>

  <div id="app">
    <h3>採用就地復用策略(vuejs默認情況)</h3>
    <div  v-for='(p, i) in persons'>
      <span>{{p.name}}<span>  
      <input type="text"/>  
      <button @click='down(i)' v-if='i != persons.length - 1'>下移</button>
    </div> 

    <h3>不採用就地復用策略(設置key)</h3>
    <div  v-for='(p, i) in persons' :key='p.id'>
      <span>{{p.name}}<span> 
      <input type="text"/>  
      <button @click='down(i)' v-if='i != persons.length - 1'>下移</button>
    </div>

  </div>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        persons: [
            { id: 1, name: 'A' },
            { id: 2, name: 'B' },
            { id: 3, name: 'C' }
        ]
      },
      mounted: function(){
        // 此DOM操作將兩個A的顏色設置為紅色 主要是為了演示原地復用
        document.querySelectorAll("h3 + div > span:first-child").forEach( v => v.style.color="red");
      },
      methods: {
        down: function(i) {
            if (i == this.persons.length - 1) return;
          var listClone = this.persons.slice();
          var one = listClone[i];
          listClone[i] = listClone[i + 1];
          listClone[i + 1] = one;
          this.persons = listClone;
        }
      }
    });
  </script>
</body>
</html>
<!-- 源於 https://www.zhihu.com/question/61078310 @霸都丶傲天 有修改-->

複雜列表

使用key不僅能夠避免上述的原地復用的副作用,且在一些操作上可能能夠提高渲染的效率,主要體現在重新排序的情況,包括在中間插入和刪除節點的操作,在下面的例子中沒有key的情況下重新排序會原地復用元素,但是由於v-if綁定了data所以會一併進行操作,在這個DOM操作上比較消耗時間,而使用key得情況則直接復用元素,v-if控制的元素在初次渲染就已經決定,在本例中沒有對其進行更新,所以不涉及v-ifDOM操作,所以在效率上會高一些。

console.time();
vm.complexListWithoutKey = [
        {id: 3, list: [7, 8, 9]},
        {id: 2, list: [4, 5, 6]},
        {id: 1, list: [1, 2, 3]},
    ];
vm.$nextTick(() => console.timeEnd());
vm.$nextTick(() => console.timeEnd());
// default: 4.100244140625ms
console.time();
vm.complexListWithKey = [
        {id: 3, list: [7, 8, 9]},
        {id: 2, list: [4, 5, 6]},
        {id: 1, list: [1, 2, 3]},
    ];
vm.$nextTick(() => console.timeEnd());
// default: 3.016064453125ms

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://cn.vuejs.org/v2/api/#key
https://www.jianshu.com/p/4bdd2690859c
https://www.zhihu.com/question/61078310
https://segmentfault.com/a/1190000012861862
https://www.cnblogs.com/zhumingzhenhao/p/7688336.html
https://blog.csdn.net/hl18730262380/article/details/89306500
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/1
https://cn.vuejs.org/v2/guide/list.html#%E7%BB%B4%E6%8A%A4%E7%8A%B6%E6%80%81

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

.NET 技術棧 思維導圖

1{icon} {views}
    技能棧
  • Web front-end
    o 框架技術

    ▣ Vue

    ▣ Bootstrap

    ▣ LayUI

    ▣ EasyUI

    ▣ Node.js

    ▣ ReactJS

    ▣ Angular

    ▣ Webpack

    o 開發工具

    ▨ 編碼工具
    ▣ HBuilder X

    ▨ 設計工具
    ▣ PxCook — 像素大廚
    ▣ 藍湖
    ▣ Visio
    ▣ 迅捷流程圖軟件

  • Website
    o 框架技術

    ▨ 雲服務模型
    ▣ Saas — 軟件即服務
    ▣ IaaS — 基礎設施即服務
    ▣ PaaS — 平台即服務

    ▨ 緩存技術
    ▣ Redis
    ▣ MemoryCache
    ▣ Session

    ▨ 消息隊列
    ▣ Windows Message Queue
    ▣ RabbitMQ
    ▣ Kafka

    ▨ 數據操作
    ▤ ORM — 對象關係映射(Object Relational Mapping)
    ▣ Entity framework
    〼 Code First
    〼 Database First
    〼 Model First
    ▣ NHibernate
    ▣ Dapper

    ▨ 框架概念
    ▤ OOD — 面向對象設計(Object-Oriented Design)
    ▤ DDD — 領域驅動設計(Domain-DrivenDesign)
    ▤ AOP — 面向切面編程(Aspect Oriented Programming)
    ▤ IOC — 控制反轉(Inversion of Control)
    ▣ 概念
    〼 依賴注入DI(Dependency Injection)
    〼 依賴查找(Dependency Lookup)
    ▣ 框架
    〼 Autofac
    〼 Spring.NET
    〼 MEF(Managed Extensibility Framework)
    〼 Unity
    〼 PostSharp

  • Windows app
    o 框架技術

    ▣ Winform

    ▣ WPF

    ▤ 通訊技術
    〼 即時通訊
    ▣ SignalR
    ▣ Socket
    〼 通訊協議
    ▣ TCP/IP
    ▣ UDP
    ▣ SSH
    ▣ PCI/PCIE
    ▣ Canbus
    ▣ Modbus
    〼 串口通訊

    ▤ 異步編程
    ▣ 多線程
    ▣ 隊列

    o 控件倉庫

    ▣ DevExpress

    ▣ Metroframework UI

    o 打包加密

    ▤ 打包
    ▣ InnoSetup

    ▤ 混淆/加密
    ▣ .NET Reactor

    ▤ 簽名

  • Web api
    o 框架技術

    ▤ 接口規範
    ▣ Resultful
    ▣ OpenAPI

    ▤ 接口管理
    ▣ Yapi
    ▣ Swagger

  • Windows services
    o WCF
    o Windows services
  • Devops
    o CI — 持續集成(Continuous integration)

    ▤ 代碼倉庫
    ▣ Git
    ▣ Svn

    ▤ 構建工具
    〼 Maven
    〼 Jenkins
    ▣ SonarQube — 自動化測試工具
    〼 Daily build
    〼 Puppet
    ▣ 供應(Provisioning)
    ▣ 配置(Configuration)
    ▣ 聯動(Orchestration)
    ▣ 報告(Reporting)

    ▤ 測試工具
    ▣ Selenium
    ▣ QTP
    ▣ Loadrunner
    ▣ Robot Framework
    ▣ Postman
    ▣ Soapui

    ▤ 配置管理
    ▣ Zookeeper

    ▤ 日誌監控
    〼 ELK
    ▣ Elasticsearch — 實時搜索
    ▣ Logstash — 中央數據流引擎
    ▣ Kibana — 實時分析

    ▤ 文件存儲
    ▣ TFS — 淘寶分佈式文件存儲(Taobao File System)
    ▣ NAS網絡存儲
    ▣ 阿里雲OSS對象存儲

    ▤ 高可用性
    〼 容器技術 — Docker + kubernetes
    〼 讀寫分離
    ▣ 數據庫複製和訂閱
    ▣ 集群服務
    〼 分庫分表
    ▣ 水平拆分
    ▣ 垂直拆分
    〼 邏輯分區

    o CD

    ▤ 持續交付(Continuous Delivery)
    ▣ 預發布環境
    ▣ 灰度環境

    ▤ 持續部署(Continuous Deployment)
    ▣ 自動發布到生產環境

    o 雲

    ▤ 雲平台
    ▣ 阿里雲
    ▣ 華為雲
    ▣ 騰訊雲
    ▣ Microsoft Azure

    ▤ 雲概念
    ▣ 公有雲
    ▣ 私有雲

    o SRE

    ▤ 網站可靠性工程師

    o 開發語言

    ▤ Python — 人工智能、系統運維

    ▤ Go語言 — 服務器編程、分佈式系統、網絡編程、雲平台

    思維導圖

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

[源碼解析] Flink的groupBy和reduce究竟做了什麼

2{icon} {views}

[源碼解析] Flink的groupBy和reduce究竟做了什麼

目錄

  • [源碼解析] Flink的groupBy和reduce究竟做了什麼
    • 0x00 摘要
    • 0x01 問題和概括
      • 1.1 問題
      • 1.2 概括
    • 0x02 背景概念
      • 2.1 MapReduce細分
      • 2.2 MapReduce細分
      • 2.3 Combine
      • 2.4 Partition
      • 2.5 Shuffle
      • 2.6 Reducer
    • 0x03 代碼
    • 0x04 從Flink JAVA API入手挖掘
      • 4.1 GroupBy是個輔助概念
        • 4.1.1 Grouping
        • 4.1.2 UnsortedGrouping
      • 4.2 reduce才是算子
    • 0x05 批處理執行計劃(Plan)
    • 0x06 批處理優化計劃(Optimized Plan)
    • 0x07 JobGraph
    • 0x08 Runtime
      • 8.1 FlatMap
        • 8.1.1 Combine
        • 8.1.2 Partition
      • 8.2 UnilateralSortMerger
        • 8.2.1 三種線程
        • 8.2.2 MutableObjectIterator
      • 8.3 ReduceDriver
    • 0x09 參考

0x00 摘要

Groupby和reduce是大數據領域常見的算子,但是很多同學應該對其背後機制不甚了解。本文將從源碼入手,為大家解析Flink中Groupby和reduce的原理,看看他們在背後做了什麼。

0x01 問題和概括

1.1 問題

探究的原因是想到了幾個問題 :

  • groupby的算子會對數據進行排序嘛。
  • groupby和reduce過程中究竟有幾次排序。
  • 如果有多個groupby task,什麼機制保證所有這些grouby task的輸出中,同樣的key都分配給同一個reducer。
  • groupby和reduce時候,有沒有Rebalance 重新分配。
  • reduce算子會不會重新劃分task。
  • reduce算子有沒有可能和前後的其他算子組成Operator Chain。

1.2 概括

為了便於大家理解,我們先總結下,對於一個Groupby + Reduce的操作,Flink做了如下處理:

  • Group其實沒有真實對應的算子,它只是在在reduce過程之前的一个中間步驟或者輔助步驟。
  • 在Flink生成批處理執行計劃后,有意義的結果是Reduce算子。
  • 為了更好的reduce,Flink在reduce之前大量使用了Combine操作。Combine可以理解為是在map端的reduce的操作,對單個map任務的輸出結果數據進行合併的操作。
  • 在Flink生成批處理優化計劃(Optimized Plan)之後,會把reduce分割成兩段,一段是SORTED_PARTIAL_REDUCE,一段是SORTED_REDUCE。
  • SORTED_PARTIAL_REDUCE就是Combine。
  • Flink生成JobGraph之後,Flink形成了一個Operator Chain:Reduce(SORTED_PARTIAL_REDUCE)和其上游合併在一起。
  • Flink用Partitioner來保證多個 grouby task 的輸出中同樣的key都分配給同一個reducer。
  • groupby和reduce過程中至少有三次排序:
    • combine
    • sort + merge
    • reduce

這樣之前的疑問就基本得到了解釋。

0x02 背景概念

2.1 MapReduce細分

MapReduce是一種編程模型,用於大規模數據集的并行運算。概念 “Map(映射)”和”Reduce(歸約)” 是它們的主要思想,其是從函數式編程語言,矢量編程語言里借來的特性。

我們目前使用的Flink,Spark都出自於MapReduce,所以我們有必有追根溯源,看看MapReduce是如何區分各個階段的。

2.2 MapReduce細分

如果把MapReduce細分,可以分為一下幾大過程:

  • Input-Split(輸入分片):此過程是將從HDFS上讀取的文件分片,然後送給Map端。有多少分片就有多少Mapper,一般分片的大小和HDFS中的塊大小一致。
  • Shuffle-Spill(溢寫):每個Map任務都有一個環形緩衝區。一旦緩衝區達到閾值80%,一個後台線程便開始把內容“溢寫”-“spill”到磁盤。在溢寫過程中,map將繼續輸出到剩餘的20%空間中,互不影響,如果緩衝區被填滿map會被堵塞直到寫磁盤完成。
  • Shuffle-Partition(分區):由於每個Map可能處理的數據量不同,所以到達reduce有可能會導致數據傾斜。分區可以幫助我們解決這一問題,在shuffle過程中會按照默認key的哈希碼對分區數量取余,reduce便根據分區號來拉取對應的數據,達到數據均衡。分區數量對應Reduce個數。
  • Shuffle-Sort(排序):在分區后,會對此分區的數據進行內排序,排序過程會穿插在整個MapReduce中,在很多地方都存在。
  • Shuffle-Group(分組):分組過程會把key相同的value分配到一個組中,wordcount程序就利用了分組這一過程。
  • Shuffle-Combiner(組合):這一過程我們可以理解為一個小的Reduce階段,當數據量大的時候可以在map過程中執行一次combine,這樣就相當於在map階段執行了一次reduce。由於reduce和map在不同的節點上運行,所以reduce需要遠程拉取數據,combine就可以有效降低reduce拉取數據的量,減少網絡負荷(這一過程默認是不開啟的,在如求平均值的mapreduce程序中不要使用combine,因為會影響結果)。
  • Compress(壓縮):在緩衝區溢寫磁盤的時候,可以對數據進行壓縮,節約磁盤空間,同樣減少給reducer傳遞的數據量。
  • Reduce-Merge(合併):reduce端會拉取各個map輸出結果對應的分區文件,這樣reduce端就會有很多文件,所以在此階段,reduce再次將它們合併/排序再送入reduce執行。
  • Output(輸出):在reduce階段,對已排序輸出中的每個鍵調用reduce函數。此階段的輸出直接寫到輸出文件系統,一般為HDFS。

2.3 Combine

Combine是我們需要特殊注意的。在mapreduce中,map多,reduce少。在reduce中由於數據量比較多,所以我們乾脆在map階段中先把自己map裏面的數據歸類,這樣到了reduce的時候就減輕了壓力。

Combine可以理解為是在map端的reduce的操作,對單個map任務的輸出結果數據進行合併的操作。combine是對一個map的,而reduce合併的對象是對於多個map

map函數操作所產生的鍵值對會作為combine函數的輸入,經combine函數處理后再送到reduce函數進行處理,減少了寫入磁盤的數據量,同時也減少了網絡中鍵值對的傳輸量。在Map端,用戶自定義實現的Combine優化機制類Combiner在執行Map端任務的節點本身運行,相當於對map函數的輸出做了一次reduce。

集群上的可用帶寬往往是有限的,產生的中間臨時數據量很大時就會出現性能瓶頸,因此應該盡量避免Map端任務和Reduce端任務之間大量的數據傳輸。使用Combine機制的意義就在於使Map端輸出更緊湊,使得寫到本地磁盤和傳給Reduce端的數據更少。

2.4 Partition

Partition是分割map每個節點的結果,按照key分別映射給不同的reduce,mapreduce使用哈希HashPartitioner幫我們歸類了。這個我們也可以自定義。

這裏其實可以理解歸類。我們對於錯綜複雜的數據歸類。比如在動物園裡有牛羊雞鴨鵝,他們都是混在一起的,但是到了晚上他們就各自牛回牛棚,羊回羊圈,雞回雞窩。partition的作用就是把這些數據歸類。只不過是在寫程序的時候,

在經過mapper的運行后,我們得知mapper的輸出是這樣一個key/value對: key是“aaa”, value是數值1。因為當前map端只做加1的操作,在reduce task里才去合併結果集。假如我們知道這個job有3個reduce task,到底當前的“aaa”應該交由哪個reduce task去做呢,是需要立刻決定的。

MapReduce提供Partitioner接口,它的作用就是根據key或value及reduce task的數量來決定當前的這對輸出數據最終應該交由哪個reduce task處理。默認對key hash后再以reduce task數量取模。默認的取模方式只是為了平均reduce的處理能力,如果用戶自己對Partitioner有需求,可以訂製並設置到job上。

在我們的例子中,假定 “aaa”經過Partitioner后返回0,也就是這對值應當交由第一個reducer來處理。

2.5 Shuffle

shuffle就是map和reduce之間的過程,包含了兩端的combine和partition。它比較難以理解,因為我們摸不着,看不到它。它屬於mapreduce的框架,編程的時候,我們用不到它。

Shuffle的大致範圍就是:怎樣把map task的輸出結果有效地傳送到reduce端。也可以這樣理解, Shuffle描述着數據從map task輸出到reduce task輸入的這段過程。

2.6 Reducer

簡單地說,reduce task在執行之前的工作就是不斷地拉取當前job里每個map task的最終結果,然後對從不同地方拉取過來的數據不斷地做merge,最終形成一個文件作為reduce task的輸入文件。

0x03 代碼

我們以Flink的KMeans算法作為樣例,具體摘要如下:

public class WordCountExampleReduce {

    DataStream ds;

    public static void main(String[] args) throws Exception {
        //構建環境
        final ExecutionEnvironment env =
                ExecutionEnvironment.getExecutionEnvironment();
        //通過字符串構建數據集
        DataSet<String> text = env.fromElements(
                "Who‘s there?",
                "I think I hear them. Stand, ho! Who‘s there?");
        //分割字符串、按照key進行分組、統計相同的key個數
        DataSet<Tuple2<String, Integer>> wordCounts = text
                .flatMap(new LineSplitter())
                .groupBy(0)
                .reduce(new ReduceFunction<Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1,
                                          Tuple2<String, Integer> value2) throws Exception {
                        return new Tuple2(value1.f0, value1.f1 + value2.f1);
                    }
                });
        //打印
        wordCounts.print();
    }
    //分割字符串的方法
    public static class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String line, Collector<Tuple2<String, Integer>> out) {
            for (String word : line.split(" ")) {
                out.collect(new Tuple2<String, Integer>(word, 1));
            }
        }
    }
}

輸出是:

(hear,1)
(ho!,1)
(them.,1)
(I,2)
(Stand,,1)
(Who‘s,2)
(there?,2)
(think,1)

0x04 從Flink JAVA API入手挖掘

首先,我們從Flink基本JAVA API來入手開始挖掘。

4.1 GroupBy是個輔助概念

4.1.1 Grouping

我們需要留意的是:GroupBy並沒有對應的Operator。GroupBy只是生成DataSet轉換的一个中間步驟或者輔助步驟

GroupBy功能的基類是Grouping,其只是DataSet轉換的一个中間步驟。其幾個主要成員是:

  • 對應的輸入數據DataSet
  • 分組所基於的keys
  • 用戶自定義的Partitioner
// Grouping is an intermediate step for a transformation on a grouped DataSet.
public abstract class Grouping<T> {
   protected final DataSet<T> inputDataSet;
   protected final Keys<T> keys;
   protected Partitioner<?> customPartitioner;
}

Grouping並沒有任何業務相關的API,具體API都是在其派生類中,比如UnsortedGrouping。

4.1.2 UnsortedGrouping

我們代碼中對應的就是UnsortedGrouping類。我們看到它提供了很多業務API,比如:sum,max,min,reduce,aggregate,reduceGroup,combineGroup…..

回到我們的示例,groupBy做了如下操作

  • 首先,groupBy返回的就是一個UnsortedGrouping,這個UnsortedGrouping是用來轉換DataSet。
  • 其次,.groupBy(0).reduce(new CentroidAccumulator()) 返回的是ReduceOperator。這就對應了前面我們提到的,groupBy只是中間步驟,reduce才能返回一個Operator
public class UnsortedGrouping<T> extends Grouping<T> {
  
    // groupBy返回一個UnsortedGrouping
    public UnsortedGrouping<T> groupBy(int... fields) {
       return new UnsortedGrouping<>(this, new Keys.ExpressionKeys<>(fields, getType()));
    }
  
    // reduce返回一個ReduceOperator
 		public ReduceOperator<T> reduce(ReduceFunction<T> reducer) {
      return new ReduceOperator<T>(this, inputDataSet.clean(reducer), Utils.getCallLocationName());
    } 
}

4.2 reduce才是算子

對於業務來說,reduce才是真正有意義的邏輯算子。

從前文的函數調用和ReduceOperator定義可以看出,.groupBy(0).reduce() 的調用結果是生成一個ReduceOperator,而 UnsortedGrouping 被設置為 ReduceOperator 的 grouper 成員變量,作為輔助操作

public class ReduceOperator<IN> extends SingleInputUdfOperator<IN, IN, ReduceOperator<IN>> {
  
	private final ReduceFunction<IN> function;
	private final Grouping<IN> grouper; // UnsortedGrouping被設置在這裏,後續reduce操作中會用到。

	public ReduceOperator(Grouping<IN> input, ReduceFunction<IN> function, 
                        String defaultName) {
		this.function = function;
		this.grouper = input; // UnsortedGrouping被設置在這裏,後續reduce操作中會用到。
    this.hint = CombineHint.OPTIMIZER_CHOOSES; // 優化時候會用到。
	}
}

讓我們順着Flink程序執行階段繼續看看系統都做了些什麼。

0x05 批處理執行計劃(Plan)

程序執行的第一步是:當程序運行時候,首先會根據java API的結果來生成執行plan。

public JobClient executeAsync(String jobName) throws Exception {
   final Plan plan = createProgramPlan(jobName);
} 

其中重要的函數是translateToDataFlow,因為在translateToDataFlow方法中,會從批處理Java API模塊中operators包往核心模塊中operators包的轉換

對於我們的示例程序,在生成 Graph時,translateToDataFlow會生成一個 SingleInputOperator,為後續runtime使用。下面是代碼縮減版。

protected org.apache.flink.api.common.operators.SingleInputOperator<?, IN, ?> translateToDataFlow(Operator<IN> input) {
    
    ......
      
    // UnsortedGrouping中的keys被取出,  
		else if (grouper.getKeys() instanceof Keys.ExpressionKeys) {

			// reduce with field positions
			ReduceOperatorBase<IN, ReduceFunction<IN>> po =
					new ReduceOperatorBase<>(function, operatorInfo, logicalKeyPositions, name);

			po.setCustomPartitioner(grouper.getCustomPartitioner());
			po.setInput(input);
			po.setParallelism(getParallelism()); // 沒有并行度的變化

			return po;//translateToDataFlow會生成一個 SingleInputOperator,為後續runtime使用
		}	    
  }  
}

我們代碼最終生成的執行計劃如下,我們可以看出來,執行計劃基本符合我們的估計:簡單的從輸入到輸出。中間有意義的算子其實只有Reduce

GenericDataSourceBase ——> FlatMapOperatorBase ——> ReduceOperatorBase ——> GenericDataSinkBase

具體在代碼中體現如下是:

plan = {Plan@1296} 
 sinks = {ArrayList@1309}  size = 1
  0 = {GenericDataSinkBase@1313} "collect()"
   formatWrapper = {UserCodeObjectWrapper@1315} 
   input = {ReduceOperatorBase@1316} "ReduceOperatorBase - Reduce at main(WordCountExampleReduceCsv.java:25)"
    hint = {ReduceOperatorBase$CombineHint@1325} "OPTIMIZER_CHOOSES"
    customPartitioner = null
    input = {FlatMapOperatorBase@1326} "FlatMapOperatorBase - FlatMap at main(WordCountExampleReduceCsv.java:23)"
     input = {GenericDataSourceBase@1339} "at main(WordCountExampleReduceCsv.java:20) (org.apache.flink.api.java.io.TextInputFormat)"

0x06 批處理優化計劃(Optimized Plan)

程序執行的第二步是:Flink對於Plan會繼續優化,生成Optimized Plan。其核心代碼位於PlanTranslator.compilePlan 函數,這裏得到了Optimized Plan。

這個編譯的過程不作任何決策與假設,也就是說作業最終如何被執行早已被優化器確定,而編譯也是在此基礎上做確定性的映射。所以我們將集中精力看如何優化plan。

private JobGraph compilePlan(Plan plan, Configuration optimizerConfiguration) {
   Optimizer optimizer = new Optimizer(new DataStatistics(), optimizerConfiguration);
   OptimizedPlan optimizedPlan = optimizer.compile(plan);

   JobGraphGenerator jobGraphGenerator = new JobGraphGenerator(optimizerConfiguration);
   return jobGraphGenerator.compileJobGraph(optimizedPlan, plan.getJobId());
}

在內部調用plan的accept方法遍歷它。accept會挨個在每個sink上調用accept。對於每個sink會先preVisit,然後 postVisit。

這裏優化時候有幾個注意點:

  1. 在 GraphCreatingVisitor.preVisit 中,當發現Operator是 ReduceOperatorBase 類型的時候,會建立ReduceNode。

    else if (c instanceof ReduceOperatorBase) {
       n = new ReduceNode((ReduceOperatorBase<?, ?>) c);
    }
    
  2. ReduceNode是Reducer Operator的Optimizer表示。

    public class ReduceNode extends SingleInputNode {
    	private final List<OperatorDescriptorSingle> possibleProperties;	
    	private ReduceNode preReduceUtilityNode;
    }
    
  3. 生成ReduceNode時候,會根據之前提到的 hint 來決定 combinerStrategy = DriverStrategy.SORTED_PARTIAL_REDUCE;

    public ReduceNode(ReduceOperatorBase<?, ?> operator) {
    			DriverStrategy combinerStrategy;
    			switch(operator.getCombineHint()) {
    				case OPTIMIZER_CHOOSES:
    					combinerStrategy = DriverStrategy.SORTED_PARTIAL_REDUCE;
    					break;
          }  
    }
    

生成的優化執行計劃如下,我們可以看到,這時候設置了并行度,也把reduce分割成兩段,一段是SORTED_PARTIAL_REDUCE,一段是SORTED_REDUCE

Data Source  ——> FlatMap ——> Reduce(SORTED_PARTIAL_REDUCE)   ——> Reduce(SORTED_REDUCE)  ——> Data Sink

具體在代碼中體現如下是:

optimizedPlan = {OptimizedPlan@1506} 
 
 allNodes = {HashSet@1510}  size = 5
   
  0 = {SourcePlanNode@1512} "Data Source "at main(WordCountExampleReduceCsv.java:20) (org.apache.flink.api.java.io.TextInputFormat)" : NONE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  1 = {SingleInputPlanNode@1513} "FlatMap "FlatMap at main(WordCountExampleReduceCsv.java:23)" : FLAT_MAP [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  2 = {SingleInputPlanNode@1514} "Reduce "Reduce at main(WordCountExampleReduceCsv.java:25)" : SORTED_REDUCE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  3 = {SinkPlanNode@1515} "Data Sink "collect()" : NONE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

  4 = {SingleInputPlanNode@1516} "Reduce "Reduce at main(WordCountExampleReduceCsv.java:25)" : SORTED_PARTIAL_REDUCE [[ GlobalProperties [partitioning=RANDOM_PARTITIONED] ]] [[ LocalProperties [ordering=null, grouped=null, unique=null] ]]"
   parallelism = 4

0x07 JobGraph

程序執行的第三步是:建立JobGraph。LocalExecutor.execute中會生成JobGraph。Optimized Plan 經過優化後生成了 JobGraph, JobGraph是提交給 JobManager 的數據結構。

主要的優化為,將多個符合條件的節點 chain 在一起作為一個節點,這樣可以減少數據在節點之間流動所需要的序列化/反序列化/傳輸消耗。

JobGraph是唯一被Flink的數據流引擎所識別的表述作業的數據結構,也正是這一共同的抽象體現了流處理和批處理在運行時的統一

public CompletableFuture<JobClient> execute(Pipeline pipeline, Configuration configuration) throws Exception {
   final JobGraph jobGraph = getJobGraph(pipeline, configuration);
}

我們可以看出來,這一步形成了一個Operator Chain:

CHAIN DataSource -> FlatMap -> Combine (Reduce) 

於是我們看到,Reduce(SORTED_PARTIAL_REDUCE)和其上游合併在一起

具體在程序中打印出來:

jobGraph = {JobGraph@1739} "JobGraph(jobId: 30421d78d7eedee6be2c5de39d416eb7)"
 taskVertices = {LinkedHashMap@1742}  size = 3
  
  {JobVertexID@1762} "e2c43ec0df647ea6735b2421fb7330fb" -> {InputOutputFormatVertex@1763} "CHAIN DataSource (at main(WordCountExampleReduceCsv.java:20) (org.apache.flink.api.java.io.TextInputFormat)) -> FlatMap (FlatMap at main(WordCountExampleReduceCsv.java:23)) -> Combine (Reduce at main(WordCountExampleReduceCsv.java:25)) (org.apache.flink.runtime.operators.DataSourceTask)"
  
  {JobVertexID@1764} "2de11f497e827e48dda1d63b458dead7" -> {JobVertex@1765} "Reduce (Reduce at main(WordCountExampleReduceCsv.java:25)) (org.apache.flink.runtime.operators.BatchTask)"
  
  {JobVertexID@1766} "2bee17f2c86aa1e9439e3dedea58007b" -> {InputOutputFormatVertex@1767} "DataSink (collect()) (org.apache.flink.runtime.operators.DataSinkTask)"

0x08 Runtime

Job提交之後,就是程序正式運行了。這裏實際上涉及到了三次排序,

  • 一次是在FlatMap發送時候調用到了ChainedReduceCombineDriver.sortAndCombine。這部分對應了我們之前提到的MapReduce中的Combine和Partition。
  • 一次是在 ReduceDriver 所在的 BatchTask中,由UnilateralSortMerger完成了sort & merge操作。
  • 一次是在ReduceDriver,這裏做了最後的reducer排序。

8.1 FlatMap

這裡是第一次排序

當一批數據處理完成之後,在ChainedFlatMapDriver中調用到close函數進行發送數據給下游。

public void close() {
   this.outputCollector.close();
}

Operator Chain會調用到ChainedReduceCombineDriver.close

public void close() {
   // send the final batch
   try {
      switch (strategy) {
         case SORTED_PARTIAL_REDUCE:
            sortAndCombine(); // 我們是在這裏
            break;
         case HASHED_PARTIAL_REDUCE:
            reduceFacade.emit();
            break;
      }
   } catch (Exception ex2) {
      throw new ExceptionInChainedStubException(taskName, ex2);
   }

   outputCollector.close();
   dispose(false);
}

8.1.1 Combine

sortAndCombine中先排序,然後做combine,最後會不斷髮送數據

private void sortAndCombine() throws Exception {
   final InMemorySorter<T> sorter = this.sorter;

   if (!sorter.isEmpty()) {
      sortAlgo.sort(sorter); // 這裡會先排序

      final TypeSerializer<T> serializer = this.serializer;
      final TypeComparator<T> comparator = this.comparator;
      final ReduceFunction<T> function = this.reducer;
      final Collector<T> output = this.outputCollector;
      final MutableObjectIterator<T> input = sorter.getIterator();

      if (objectReuseEnabled) {
        ......
      } else {
         T value = input.next();

         // 這裏就是combine
         // iterate over key groups
         while (running && value != null) {
            comparator.setReference(value);
            T res = value;

            // iterate within a key group
            while ((value = input.next()) != null) {
               if (comparator.equalToReference(value)) {
                  // same group, reduce
                  res = function.reduce(res, value);
               } else {
                  // new key group
                  break;
               }
            }

            output.collect(res); //發送數據
         }
      }
   }
}

8.1.2 Partition

最後發送給哪個下游,是由OutputEmitter.selectChannel決定的。有如下幾種決定方式:

hash-partitioning, broadcasting, round-robin, custom partition functions。這裏採用的是PARTITION_HASH。

每個task都會把同樣字符串統計結果發送給同樣的下游ReduceDriver。這就保證了下游Reducer一定不會出現統計出錯。

public final int selectChannel(SerializationDelegate<T> record) {
   switch (strategy) {
   ...
   case PARTITION_HASH:
      return hashPartitionDefault(record.getInstance(), numberOfChannels);
   ...
   }
}

private int hashPartitionDefault(T record, int numberOfChannels) {
	int hash = this.comparator.hash(record);
	return MathUtils.murmurHash(hash) % numberOfChannels;
}

具體調用棧:

hash:50, TupleComparator (org.apache.flink.api.java.typeutils.runtime)
hash:30, TupleComparator (org.apache.flink.api.java.typeutils.runtime)
hashPartitionDefault:187, OutputEmitter (org.apache.flink.runtime.operators.shipping)
selectChannel:147, OutputEmitter (org.apache.flink.runtime.operators.shipping)
selectChannel:36, OutputEmitter (org.apache.flink.runtime.operators.shipping)
emit:60, ChannelSelectorRecordWriter (org.apache.flink.runtime.io.network.api.writer)
collect:65, OutputCollector (org.apache.flink.runtime.operators.shipping)
collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
sortAndCombine:254, ChainedReduceCombineDriver (org.apache.flink.runtime.operators.chaining)
close:266, ChainedReduceCombineDriver (org.apache.flink.runtime.operators.chaining)
close:40, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
close:88, ChainedFlatMapDriver (org.apache.flink.runtime.operators.chaining)
invoke:215, DataSourceTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

8.2 UnilateralSortMerger

這裡是第二次排序

在 BatchTask中,會先Sort, Merge輸入,然後才會交由Reduce來具體完成過。sort & merge操作具體是在UnilateralSortMerger類中完成的。

getIterator:646, UnilateralSortMerger (org.apache.flink.runtime.operators.sort)
getInput:1110, BatchTask (org.apache.flink.runtime.operators)
prepare:95, ReduceDriver (org.apache.flink.runtime.operators)
run:474, BatchTask (org.apache.flink.runtime.operators)
invoke:369, BatchTask (org.apache.flink.runtime.operators)
doRun:707, Task (org.apache.flink.runtime.taskmanager)
run:532, Task (org.apache.flink.runtime.taskmanager)
run:748, Thread (java.lang)

UnilateralSortMerger是一個full fledged sorter,它實現了一個多路merge sort。其內部的邏輯被劃分到三個線程上(read, sort, spill),這三個線程彼此之間通過一系列blocking queues來構成了一個閉環。

其內存通過MemoryManager分配,所以這個組件不會超過給其分配的內存。

該類主要變量摘錄如下:

public class UnilateralSortMerger<E> implements Sorter<E> {
	// ------------------------------------------------------------------------
	//                                  Threads
	// ------------------------------------------------------------------------

	/** The thread that reads the input channels into buffers and passes them on to the merger. */
	private final ThreadBase<E> readThread;

	/** The thread that merges the buffer handed from the reading thread. */
	private final ThreadBase<E> sortThread;

	/** The thread that handles spilling to secondary storage. */
	private final ThreadBase<E> spillThread;
	
	// ------------------------------------------------------------------------
	//                                   Memory
	// ------------------------------------------------------------------------
	
	/** The memory segments used first for sorting and later for reading/pre-fetching
	 * during the external merge. */
	protected final List<MemorySegment> sortReadMemory;
	
	/** The memory segments used to stage data to be written. */
	protected final List<MemorySegment> writeMemory;
	
	/** The memory manager through which memory is allocated and released. */
	protected final MemoryManager memoryManager;
	
	// ------------------------------------------------------------------------
	//                            Miscellaneous Fields
	// ------------------------------------------------------------------------
	/**
	 * Collection of all currently open channels, to be closed and deleted during cleanup.
	 */
	private final HashSet<FileIOChannel> openChannels;
	
	/**
	 * The monitor which guards the iterator field.
	 */
	protected final Object iteratorLock = new Object();
	
	/**
	 * The iterator to be returned by the sort-merger. This variable is null, while receiving and merging is still in
	 * progress and it will be set once we have &lt; merge factor sorted sub-streams that will then be streamed sorted.
	 */
	protected volatile MutableObjectIterator<E> iterator; 	// 如果大家經常調試,就會發現driver中的input都是這個兄弟。

	private final Collection<InMemorySorter<?>> inMemorySorters;
}

8.2.1 三種線程

ReadingThread:這種線程持續讀取輸入,然後把數據放入到一個待排序的buffer中。The thread that consumes the input data and puts it into a buffer that will be sorted.

SortingThread : 這種線程對於上游填充好的buffer進行排序。The thread that sorts filled buffers.

SpillingThread:這種線程進行歸併操作。The thread that handles the spilling of intermediate results and sets up the merging. It also merges the channels until sufficiently few channels remain to perform the final streamed merge.

8.2.2 MutableObjectIterator

UnilateralSortMerger有一個特殊變量:

protected volatile MutableObjectIterator<E> iterator;

這個變量就是最終sort-merger的輸出。如果大家調試過算子,就會發現這個變量就是具體算子的輸入input類型。最終算子的輸入就是來自於此。

8.3 ReduceDriver

這裡是第三次排序,我們可以看出來reduce是怎麼和groupby一起運作的。

  1. 針對 .groupBy(0),ReduceDriver就是單純獲取輸入的第一個數值 T value = input.next();
  2. 後續代碼中有嵌套的兩個while,分別是 :遍歷各種key,以及某一key中reduce。
  3. 遍歷 group keys的時候,把value賦於比較算子comparator(這個算子概念不是Flink算子,就是為了說明邏輯概念) comparator.setReference(value); 因為groubBy只是指定按照第一個位置比較,沒有指定具體key數值,所以這個value就是key了。此處記為while (1) ,代碼中有註解。
  4. 從輸入中讀取後續的數值value,如果下一個數值是同一個key,就reduce;如果下一個數值不是同一個key,就跳出循環。放棄比較,把reduce結果輸出。此處記為 while (2)
  5. 跳出 while (2) 之後,代碼依然在 while (1) ,此時value是新值,所以繼續在 while (1)中運行 。把value繼續賦於比較算子 comparator.setReference(value);,於是進行新的key比較
public class ReduceDriver<T> implements Driver<ReduceFunction<T>, T> {
	@Override
	public void run() throws Exception {

		final Counter numRecordsIn = this.taskContext.getMetricGroup().getIOMetricGroup().getNumRecordsInCounter();
		final Counter numRecordsOut = this.taskContext.getMetricGroup().getIOMetricGroup().getNumRecordsOutCounter();

		// cache references on the stack
		final MutableObjectIterator<T> input = this.input;
		final TypeSerializer<T> serializer = this.serializer;
		final TypeComparator<T> comparator = this.comparator;		
		final ReduceFunction<T> function = this.taskContext.getStub();		
		final Collector<T> output = new CountingCollector<>(this.taskContext.getOutputCollector(), numRecordsOut);

		if (objectReuseEnabled) {
      ......
		} else {
      // 針對 `.groupBy(0)`,ReduceDriver就是單純獲取輸入的第一個數值 `T value = input.next();`
			T value = input.next();

      // while (1)
			// iterate over key groups
			while (this.running && value != null) {
				numRecordsIn.inc();
        // 把value賦於比較算子,這個value就是key了。
				comparator.setReference(value);
				T res = value;

        // while (2)
				// iterate within a key group,循環比較這個key
				while ((value = input.next()) != null) {
					numRecordsIn.inc();
					if (comparator.equalToReference(value)) {
						// same group, reduce,如果下一個數值是同一個key,就reduce
						res = function.reduce(res, value);
					} else {
						// new key group,如果下一個數值不是同一個key,就跳出循環,放棄比較。
						break;
					}
				}
        // 把reduce結果輸出
				output.collect(res);
			}
		}
	}  
}

0x09 參考

mapreduce里的shuffle 里的 sort merge 和combine

實戰錄 | Hadoop Mapreduce shuffle之Combine探討

Hadoop中MapReduce中combine、partition、shuffle的作用是什麼?在程序中怎麼運用?

Flink運行時之生成作業圖

mapreduce過程

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

從10萬的SUV到70萬的豪華轎車,分析銷量最好的熱門車!

3{icon} {views}

緊湊型車中大型車緊湊型SUV總結:金無赤足,人無完人。汽車是個綜合體商品,在一輛車定型之前,工程師要對外觀設計、空間造型、材質用料以及功能配置等綜合起來進行考量,以取得消費者體驗的最佳平衡點,這一切還須建立在產品成本不超標的前提下。

發現十有八九的朋友都是買車以後才開始去了解車,其中對選購的車感到後悔的不知凡幾,你是否其中一員呢?

這些朋友普遍對汽車市場一知半解,不多加體驗,就單憑一款車的外觀、汽車銷量榜和別人片面的評價就為它買賬,一意孤行、不聽勸阻,也甚是無奈。

今年1-10月的汽車總銷量榜單出爐,銷量能否成為購車的參考?告訴你,銷量最好的車未必是產品力最強的車,但一定是最迎合消費者口味的車,對這些公司在營銷方面的造詣深感佩服。下面為你簡單點評幾款熱度相當高的車型。

緊湊型車

中大型車

緊湊型SUV

總結:金無赤足,人無完人。汽車是個綜合體商品,在一輛車定型之前,工程師要對外觀設計、空間造型、材質用料以及功能配置等綜合起來進行考量,以取得消費者體驗的最佳平衡點,這一切還須建立在產品成本不超標的前提下。所以沒有最好的車,只有最適合自己的車。明確個人用車需求、預算,平時多看的推文,總能找到最適合你的車。對了,別吝嗇你的贊!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

都說發飄的車不是好車,為什麼現在的車還越造越輕?

3{icon} {views}

一般來說升力:Y= Cy1/2 ρv2sρ—空氣密度s—機翼面積Cy—升力係數v—氣流速度Cy = Y/(qS)在公式中Cy:升力係數Y :升力(升力垂直於氣流速度方向,向上為正)q :動壓,q=ρv*v/2 (ρ為空氣密度,v為氣流相對於物體的流速)S :參考面積(飛機一般選取機翼面積為參考面積)所以機翼的形狀決定空氣流速產生壓強差從而產生升力。

老一輩的人買衣服,除了看面料,還要看裡子,老一輩的司機除了看車質量好不好,還要聽關門聲音厚不厚重,關門聲音好聽,恩~~~好車。

中國的汽車發展說快也不快,但是說慢也不慢了。真正的發展就在近20年。如風起雲涌。以前的車少,能開上車的不是土豪官宦就是司機。所以關於車的一切都是老司機說了算,因為車開得多,說什麼都對,你TM連車都沒碰過居然敢質疑我的觀點?

那麼這些年老司機說過哪些經典段子做過哪些經典事情呢?

1.XX國的車太輕,開起來發飄。

輕飄飄輕飄飄,輕所以飄,聽起來沒毛病啊!但是為什麼這麼簡單的道理這些國際大廠就是不懂呢?

反而各家的鋁製車身越來越多,把車子越造越輕。難道他們都不知道“車子輕了會發飄”?你看開卡車就從不會發飄,就是因為重。

為什麼飛機那麼重照樣可以上天呢?最基本的原理我們都知道,物理課吹紙的實驗誰都做過。空氣流速快的地方壓強小。

一般來說升力:Y= Cy1/2 ρv2s

ρ—空氣密度

s—機翼面積

Cy—升力係數

v—氣流速度

Cy = Y/(qS)

在公式中

Cy:升力係數

Y :升力(升力垂直於氣流速度方向,向上為正)

q :動壓,q=ρv*v/2 (ρ為空氣密度,v為氣流相對於物體的流速)

S :參考面積(飛機一般選取機翼面積為參考面積)

所以機翼的形狀決定空氣流速產生壓強差從而產生升力。

而我們看到汽車的側面和機翼也一樣,會產生上下不一的氣流。到達一定車速的時候也會產生升力。

這就是我們廣義上說的發飄。競速摩托車之所以不需要尾翼增加下壓力就是因為不會產生這兩股相對氣流。

在上述公式當中V和S是變量,而我們說車飄的時候也多是跑高速。所以最後就只有S這一個變量了。怎麼搞?把車頂做平,底盤做凸出來?很顯然不可能。這個時候就有尾翼的出現了。

尾翼的效果就是反過來裝的機翼,簡單粗暴,原理就是在車屁股產生一個下壓力,所以就算跑車很輕只要尾翼能夠產生足夠的下壓力,車也不會發飄。所以就算車再輕只要有相對的下壓力,都不會發飄。但是這樣一來問題又來了。

你想下要是你走路的時候有個人拚命按着你的頭你會走得輕鬆嗎?這個時候下壓力大又影響油耗,所以,怎樣在下壓力和油耗之間達到一個平衡才是汽車廠商考慮的。

和車身重量並沒有直接關係,而車的體重減下來,腰不酸了,油耗低了,腿不疼了,操控好了,一箱油能跑更遠了,油價上漲也不怕了。但是覺得鋁製車身修起來太貴的贊一個。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

2016“鴻情•粵意”捷豹路虎媒體品鑒會在廣州舉行

1{icon} {views}

得益於捷豹家族引以為傲的智能全鋁車身架構,全新捷豹XFL雖然擁有同級領先的修長車身,但整體表現依然輕盈靈活,表現出良好的動態性能。首次引入的後排開門警示系統,在監測到後方有車輛靠近時,會提醒後排乘客暫勿開啟車門,保護車內外人員安全,首次應用於捷豹車型上的nanoe™車內空氣凈化技術為乘客奉上純凈空氣。

12月18日,廣州鴻粵銳虎捷豹路虎4S中心聯手廣東鴻粵銳虎捷豹路虎4S中心舉辦了2016“鴻情•粵意”捷豹路虎媒體品鑒會。品鑒會上,來自廣州地區的30位主流媒體再度體驗了捷豹路虎車型的非凡魅力、進一步加深了對捷豹“性能美學”品牌DNA和路虎品牌始終如一的探索和發現精神的了解。

廣州鴻粵銳虎汽車銷售服務有限公司是集銷售、維修、零配件供應、信息反饋四位一體的4S標準捷豹路虎經銷店。公司在成立一年之內,先後榮獲“南區最佳售後經銷商”、“季度最佳區域售後服務獎”、“路虎中國誠信經銷商”、“年度最佳售後服務團隊”等獎項。這些殊榮,是鴻粵銳虎實力的見證,也是尊貴路虎車主們卓越服務的保證。

廣東鴻粵銳虎汽車銷售服務有限公司,是華南區最大的捷豹路虎旗艦4S店,也是廣州唯一一家擁有路虎越野體驗中心旗艦店。它致力於為廣東客戶帶來頂級豪華座駕體驗,把具有純正英倫皇室的血統和文化呈現給高端消費群體。

此次媒體品鑒會在雅韻軒精素茶館舉行,來自廣州地區網站、電台、自媒體等20家主流媒體共同體驗品鑒了包括捷豹XFL、路虎發現神行、路虎攬勝極光等捷豹路虎車型的卓越性能。

為中國市場專屬打造的全新捷豹XFL,表現出新格調運動商務座駕的獨特風範,就像一位身着套裝,舉止優雅的英倫紳士。在軸距增加140毫米后,其軸距達到3100毫米,帶來寬綽舒適的後排乘坐空間。得益於捷豹家族引以為傲的智能全鋁車身架構,全新捷豹XFL雖然擁有同級領先的修長車身,但整體表現依然輕盈靈活,表現出良好的動態性能。首次引入的後排開門警示系統,在監測到後方有車輛靠近時,會提醒後排乘客暫勿開啟車門,保護車內外人員安全,首次應用於捷豹車型上的nanoe™車內空氣凈化技術為乘客奉上純凈空氣。

路虎品牌旗下的全能全地形豪華SUV發現神行以優秀的全地形能力輕鬆征服各種複雜地形,在城市道路上,發現神行又表現出優異的駕乘舒適性和燃油經濟性。超凡多功能設計讓全家出遊非常實用。靈活的座椅布局營造超強的儲物能力,容納所有行李,42處儲物空間讓手機、平板電腦、玩具,保溫杯都有各自的安身之處。最多4個12V電源接口和4個USB接口,滿足多個电子設備同時充電的需求。攬勝極光又是另外一種風格,其動感時尚的設計超越了人們對SUV車型的傳統認知,引領時尚潮流,滿足年輕消費者對個性的追求,其內飾在簡潔精緻的基礎上,更凸顯了對豪華的追求,是追求時尚的不二之選。

通過現場品鑒體驗,廣州地區媒體記者親身領略了捷豹車型誘人設計、卓越性能、精湛工藝以及路虎旗下車型豪華理念和強大的全地形能力、增進了對捷豹路虎的品牌的了解,對於捷豹和路虎品牌在廣州地區的知名度提升起到重要作用。

同時,歷時一個多月的捷豹路虎系列區域活動也隨着這場品鑒會進入尾聲。北至呼和浩特、吉林,西至西安、甘肅,南至廣州、深圳,東至青島,捷豹和路虎已為全國20餘座城市的消費者帶去獨一無二的專屬體驗,一次次踐行了品牌深耕全國區域市場的承諾。未來,捷豹路虎也將持續在區域市場發力,不斷將更多更好的產品、更尊貴的體驗,帶給地區媒體與消費者。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

一部有內涵的車,這些功能必不可少

2{icon} {views}

另外,相信有了感應式尾門的MpV,也沒有像全新GL8這麼人性化,電動尾門能夠在任意角度停止,方便了不同身高的用戶使用。除了感應式尾門,全新GL8幾乎將人性化配置做到極致,還配備了電動滑移門,其具備仿夾功能,在滑移門關閉過程中,如遇到一定阻力,滑移們會自動停止,避免傷害用戶。

不知道從什麼時候開始,很多車一上來就要用 “大氣”、“亮麗”等外在的詞去定義自身,殊不知隨着生活品質的不斷提高,用戶對車的 “內涵”也有更多的追求。而什麼樣的車才能算得上有“內涵”?這個應該是被定義,而非自定義。它需要經歷時間的積累以及用戶的考驗。

時光荏苒,從1999年上市至今,17年來別克GL8在受眾心中都享有極高口碑,前後經歷4次換代,從最初的“大轎子“大空間,到追求舒適與”陸上公務艙“,再到升級的陸尊、GL8商務版,科技配置一直在升級,品質不斷在提高,”內涵“可以說滲透到了全新GL8骨子里。全新GL8也一直身體力行的在詮釋真正的“內涵”不是簡單的堆砌,而是從“心”出發。

領先科技——安吉星4G LTE/WiFi、HUD抬頭显示、Carplay

在這個幾乎每個人都離不開網絡的時代,無論到哪裡,WiFi都顯得無比重要。全新一代GL8,自帶4G Wi-Fi熱點,速度高達100M/s,穩定可靠的4G LTE最多可支持7台移動設備同時接入,幾乎覆蓋了整車人的WiFi。讓全新GL8在點火的狀態下,成為了一個大的移動熱點,可隨時隨地暢享車內信息娛樂,大大豐富車生活,給用戶帶來了真切的需求。

第10代Onstar還可全時在線助理為用戶提供包括碰撞自動求助、緊急救援協助、安全保障、導航、車況監測、全聲控免提電話、手機應用等7大類26項服務,全新GL8簡直可以說應有盡有,讓移動辦公暢通無阻。

導航不必低頭的HUD抬頭显示配置,更是貼心的領先科技配備。HUD抬頭显示可以在很大程度上避免分散駕駛員的注意力,從而提高行車安全性。

而Carplay配置目前在很多高端車身上都使用了,如:法拉利、奧迪Q7等等,但在MpV車型裏面,全新GL8無疑是做了領頭羊,這也展現GL8不斷追求科技、提升“內涵”的表現。其可以利用智能手機的絕大部分常用App,比如:QQ音樂、導航等,這點相信大家都知道手機的App永遠比車載的要更新更方便。

再則, Carplay不僅可以給手機充電,同時能夠通過數據線將手機的基本功能,比如:撥打電話、切換音樂等,通過車輛的多功能方向盤直接控制,這科技,大大提高了行車安全性。

人性化配置——感應式尾門、電動滑移門、車輛自動上鎖

全新GL8的感應式尾門配置,給用戶帶來的將是很大的便捷服務。據說這個配置在多數MpV裏面是沒有的,如夏朗這款車型,便經常有車主抱怨雙手搬着行李箱的時候,沒有感應式尾門很不方便。另外,相信有了感應式尾門的MpV,也沒有像全新GL8這麼人性化,電動尾門能夠在任意角度停止,方便了不同身高的用戶使用。

除了感應式尾門,全新GL8幾乎將人性化配置做到極致,還配備了電動滑移門,其具備仿夾功能,在滑移門關閉過程中,如遇到一定阻力,滑移們會自動停止,避免傷害用戶;在滑移門開啟過程中,如果車窗處於打開狀態,滑移門開啟時將停止在安全位置,防止夾傷用戶手或頭部。

像GL8這樣,才是真正從每個用戶的角度考慮,如:車輛自動上鎖功能,當車輛熄火后,用戶帶着鑰匙,關車門離開車輛后,即使忘記上鎖,當用戶走出3米的距離車輛會自動上鎖,並鳴號提示。以及其音響和空調系統會在用戶撥打藍牙電話時自動降低風速和調低音量,並在掛電話后自動恢復。

周全防護——智慧安全系統

看到這裏,可能有人會說全新GL8配置在服務這方面確實是很人性化,但這麼大的車,駕駛方面有沒有保障。這方面無需擔心,GL8在安全配置方面一直以來都是“自帶光環”,全新GL8更是採用了智慧安全系統,擁有ACC自適應巡航、FCA 前方碰撞預警、CMB 碰撞自動剎車、LKA 車道保持、SBZA 側盲區預警、ApA自動泊車系統等。

當在城市中行駛時需要頻繁的停車和起步,如果你開的是GL8,ACC自適應巡航可以幫你實現“停車/起步”功能,以及“自動跟車”。

另外,FCA 前方碰撞預警則可以幫你綜合評估,前方慢速行駛或者靜止的車輛進行碰撞危險指數,並採取主動躲避動作,從而提高安全保護。

如果前方有障礙物,沒來得及採取操作,CMB 碰撞自動剎車系統不單可避免了車子不必要的損失,重要的是保護了乘員的安全。

再則,你是否曾有這樣的顧慮,自己的車是否有壓線,對方車輛是否會在毫不知情的情況下變線等等,開GL8有LKA 車道保持系統,從此,這些顧慮再也不會有。

當你在轉彎或車速比較高時,側盲區預警系統將時刻幫用戶注意着左右車輛;如果你是新手,發現停車位有點窄,那就用自動泊車。

全新GL8以上的大部分配置幾乎可以說是被很多同級別MpV忽視的,如今市面上很多車型,為了迎合大眾年輕時尚化的需求,紛紛在外觀造型等元素上堆砌、下“功夫”,但這些做法可能只會是曇花一現。唯有像全新GL8真正從用戶的角度出發考慮,讓用戶享受到最佳體驗,把每個細節、“內涵”做到極致,必不可少的功能一個都不少,才能贏得用戶持久的支持。

事實也在證明,在全新一代GL8還沒上市前,上汽通用別克GL8今年1-10月份在華累計銷量61516萬輛,比奧德賽和艾力紳銷量加起來還高出不少。相信這個不斷提升“內涵”、擁有高科技的全新GL8,會俘獲更多用戶的心。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

企鵝出沒澳南島嶼 不時迷路跑到本土

3{icon} {views}

摘錄自2020年3月10日公視報導

南半球的澳洲靠近南極洲附近的島嶼,經常會有野生企鵝出沒,有些企鵝迷路,就會不小心跑到澳洲本土的海灘上,吸引民眾圍觀。野生動物專家呼籲,千萬不要餵食企鵝,否則可能有害健康,也會妨礙牠們回歸大自然。

澳洲南部靠近南極洲的島嶼,總共有八種企鵝出沒,專家近年來發現經常有迷路的企鵝,跑到不該出現的澳洲本土岸邊。在伯斯附近的瑪格麗特河小鎮,就有一處企鵝保育基地,專門收容這些需要幫助的企鵝。

專家說,一般民眾如果發現落單或迷路的企鵝,可以趕快通知野生動物管理人員,但最好不要亂餵食。莫爾解釋,「在牠們獲得照料前,你不需要給牠們水或食物,只有照護人員知道什麼是適當的食物。」

生態保育
國際新聞
澳洲
企鵝

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

MySQL InnoDB MVCC

2{icon} {views}

MySQL 原理篇

MVCC

MVCC 的定義

MVCC(Multiversion concurrency control):多版本併發控制,併發訪問(讀或寫)數據庫時,對正在事務內處理的數據做多版本的管理。以達到用來避免寫操作的堵塞,從而引發讀操作的併發問題。

MVCC 邏輯流程

插入

MySQL 在每一行數據中都會默認添加一些隱藏列 DB_TRX_IDDB_ROLL_PT。

上面圖中的執行步驟如下:

  1. 手動開啟事務,從 InnoDB 引擎中獲取一個全局事務ID(1)
  2. 然後往 teacher 表中插入兩條數據,同時設置數據行的版本號為當前事務ID,刪除版本號為 NULL

思考:如果事務是自動提交的(SET AUTOCOMMIT = NO),且未手動開啟事務,執行如下兩條 SQL,插入的數據會是什麼樣子的?

INSERT INTO teacher (NAME, age) VALUE ('seven', 18) ;

INSERT INTO teacher (NAME, age) VALUE ('qingshan', 19) ;

因為事務是自動提交的,所以兩條插入語句會分別獲取事務ID,所以這裏插入的數據行的版本號是1和2。

刪除

上面圖中的執行步驟如下:

  1. 手動開啟事務,從 InnoDB 引擎中獲取一個全局事務ID(22)
  2. 然後執行一條刪除語句,InnoDB 會找到這條記錄,把它的刪除版本號設置為當前事務ID

修改

上面圖中的執行步驟如下:

  1. 手動開啟事務,從 InnoDB 引擎中獲取一個全局事務ID(33)
  2. 然後執行一條修改語句,InnoDB 會找到這條記錄,copy 一份原數據插入到表中,將新行數據的數據行的版本號的值設置為當前事務ID,將原行數據的刪除版本號的值設置為當前事務ID

查詢

上面圖中的執行步驟如下:

  1. 手動開啟事務,從 InnoDB 引擎中獲取一個全局事務ID(44)
  2. 根據數據查詢規則的描述
    1. 查找數據行版本早於當前事務版本的數據行,發現表中三行數據都滿足條件
    2. 查找刪除版本號要麼為 NULL,要麼大於當前事務版本號的記錄,發現只有最後一條數據滿足條件(1, seven, 19)

案例分析

數據準備:

CREATE TABLE `teacher` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `age` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

INSERT  INTO teacher(id,NAME,age) VALUES (1,'seven',18);
INSERT  INTO teacher(id,NAME,age) VALUES (2,'qingshan',20);

案例一

-- 事務A執行
BEGIN;                                     -- 1
SELECT * FROM teacher;                       -- 2
COMMIT;

--事務B執行
BEGIN;                                     -- 3
UPDATE teacher SET age =28 WHERE id=1;     -- 4
COMMIT;

案例一的執行步驟是:1,2,3,4,2,執行效果如下圖所示:

雖然在執行 3,4 步驟的時候更新 id=1 的數據,但是根據 MVCC 的查詢邏輯流程,再次執行2,獲取到的數據依然和第一次一樣。

案例二

-- 事務A執行
BEGIN;                                     -- 1
SELECT * FROM teacher;                       -- 2
COMMIT;

--事務B執行
BEGIN;                                     -- 3
UPDATE teacher SET age =28 WHERE id=1;     -- 4
COMMIT;

案例二的執行步驟是:3,4,1,2,執行效果如下圖所示:

根據 MVCC 的查詢邏輯流程,執行1,2,獲取到的數據是事務B未提交的數據,這個是有問題的。

分析了案例一和案例二,發現 MVCC 不能解決案例二的問題,InnoDB 會使用 Undo log 解決案例二的問題。

Undo Log

Undo Log 的定義

Undo:意為取消,以撤銷操作為目的,返回指定某個狀態的操作。

Undo Log:數據庫事務提交之前,會將事務修改數據的鏡像(即修改前的舊版本)存放到 undo 日誌里,當事務回滾時,或者數據庫奔潰時,可以利用 undo 日誌,即舊版本數據,撤銷未提交事務對數據庫產生的影響。。

  • 對於 insert 操作,undo 日誌記錄新數據的 PK(ROW_ID),回滾時直接刪除;
  • 對於 delete/update 操作,undo 日誌記錄舊數據 row,回滾時直接恢復;
  • 他們分別存放在不同的buffer里。

Undo Log 是為了實現事務的原子性而出現的產物。

 

Undo Log 實現事務原子性:事務處理過程中,如果出現了錯誤或者用戶執行了 ROLLBACK 語句,MySQL 可以利用 Undo Log 中的備份將數據恢復到事務開始之前的狀態。

InnoDB 發現可以基於 Undo Log 來實現多版本併發控制。

Undo Log 在 MySQL InnoDB 存儲引擎中用來實現多版本併發控制。

 

Undo Log 實現多版本併發控制:事務未提交之前,Undo Log 保存了未提交之前的版本數據,Undo Log 中的數據可作為數據舊版本快照供其他併發事務進行快照讀。

分析下圖中 SQL 的執行過程。

  • 事務A手動開啟事務,執行更新操作,首先會把更新命中的數據拷貝到 Undo Buffer 中
  • 事務B手動開啟事務,執行查詢操作,會讀取 Undo Log 中數據返回,進行快照度

當前讀和快照讀

快照讀

SQL 讀取的數據是快照版本,也就是歷史版本,普通的 SELECT 就是快照讀。

InnoDB 快照讀,數據的讀取將由 cache(原本數據)+ Undo Log(事務修改過的數據)兩部分組成。

當前讀

SQL 讀取的數據是最新版本,通過鎖機制來保證讀取的數據無法通過其他事務進行修改。

UPDATE 、DELETE 、INSERT 、SELECT … LOCK IN SHARE MODE 、SELECT … FOR UPDATE 都是當前讀,這些操作在《MySQL InnoDB 鎖》這篇文章中有過演示,事務A執行這些 SQL,會阻塞事務B的 SQL 執行。

在 InnoDB 引擎裏面,快照讀通過 MVCC 解決幻讀的問題,當前讀通過 Next-Key Locks 解決幻讀的問題。

Redo Log

Redo Log 的定義

Redo:顧名思義就是重做。以恢復操作為目的,重現操作。

Redo Log:指事務中操作的任何數據,將最新的數據備份到一個地方(Redo Log)。

Redo Log 的持久化:不是隨着事務的提交才寫入的,而是在事務的執行過程中,便開始寫入 Redo Log 中,具體的落盤策略可以進行配置。

Redo Log 是為了實現事務的持久性而出現的產物。

Redo Log 實現事務持久性:防止在發生故障的時間點,尚有臟頁未寫入表的 IBD 文件中,在重啟 MySQL 服務的時候,根據 Redo Log 進行重做,從而達到事務的未入磁盤數據進行持久化這一特性。

根據下圖分析 Redo Log 的執行流程

InnoDB 不是每一次提交事務都把數據從緩存區持久化到硬盤的,因為每次提交事務都把數據持久化到硬盤,效率很低,每一次持久化都需要執行 IO 操作。

InnoDB 會把每次數據變化會先進入 Redo Buffer 中,事務提交了,會根據策略把新的數據寫入 Redo Log 中,InnoDB 就會認為這次事務提交成功了,數據並不一定馬上就進入表的 IBD 文件中。

疑問:持久化到 Redo Log 中和持久化到表的 IBD 文件一樣都是 IO 操作,為什麼要設計 Redo Log 呢?

其實是因為持久化到 Redo Log 中是順序 IO 的操作,而持久化到表的 IBD 文件中是一個隨機 IO 的操作,比如我們需要更新 id=1 和 id=8 的數據,如果是 Redo Log,就只需要把更新的數據順序存入 Redo Log 中;但如果是表的 IBD 文件,就需要先找到 id=1 和 id=8 的兩個不連續的磁盤文件地址,再做持久化操作,影響數據庫服務的併發性能。

Redo Log 的持久化配置

指定 Redo Log 記錄在 {datadir}/ib_logfile1 和 ib_logfile2 兩個文件中,可以通過 innodb_log_group_home_dir配置指定目錄存儲。

一旦事務成功提交且數據持久化到表的 IBD 文件中之後,此時 Redo Log 中的對應事務數據記錄就失去了意義,所 以 Redo Log 的寫入是日誌文件循環寫入的過程,也就是覆蓋寫的過程。

  • 指定 Redo Log 日誌文件組中的數量 innodb_log_files_in_group 默認為2
  • 指定 Redo Log 每一個日誌文件最大存儲量 innodb_log_file_size 默認48M
  • 指定 Redo Log 在 cache/buffer 中的 buffer 池大小 innodb_log_buffer_size 默認16M

Redo Buffer 持久化到 Redo Log 的策略,通過設置 Innodb_flush_log_at_trx_commit 的值:

  • 取值0:每秒提交 Redo buffer -> Redo Log OS cache -> flush cache to disk,可能丟失一秒內的事務數據。
  • 取值1(默認值):每次事務提交執行 Redo Buffer -> Redo Log OS cache -> flush cache to disk,最安全,性能最差的方式
  • 取值2:每次事務提交執行 Redo Buffer -> Redo log OS cache 再每一秒執行 -> flush cache to disk 操作

一般建議選擇取值2,因為 MySQL 掛了最多損失一次事務提交的數據,整個服務期掛了才會損失一秒的事務提交數據。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

大陸寄台灣空運注意事項

大陸海運台灣交貨時間多久?

※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!