Sharding-JDBC 實戰(史上最全)


文章很長,而且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 為您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經典圖書:《Java高並發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:尼恩Java面試寶典 最新版 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


Sharding-JDBC 實戰(史上最全)

在開始 Sharding-JDBC分庫分表具體實戰之前,

必要先了解分庫分表的一些核心概念。

分庫分表的背景:

傳統的將數據集中存儲⾄單⼀數據節點的解決⽅案,在性能、可⽤性和運維成本這三⽅⾯已經難於滿⾜互聯⽹的海量數據場景。

隨着業務數據量的增加,原來所有的數據都是在一個數據庫上的,網絡IO及文件IO都集中在一個數據庫上的,因此CPU、內存、文件IO、網絡IO都可能會成為系統瓶頸。

當業務系統的數據容量接近或超過單台服務器的容量、QPS/TPS接近或超過單個數據庫實例的處理極限等,

此時,往往是采用垂直和水平結合的數據拆分方法,把數據服務和數據存儲分布到多台數據庫服務器上。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

容量瓶頸:

從性能⽅⾯來說,由於關系型數據庫⼤多采⽤ B+ 樹類型的索引,

數據量超過一定大小,B+Tree 索引的高度就會增加,而每增加一層高度,整個索引掃描就會多一次 IO 。

在數據量超過閾值的情況下,索引深度的增加也將使得磁盤訪問的 IO 次數增加,進而導致查詢性能的下降;

一般的存儲容量是多少呢? 請參見 3 高架構秒殺部分內容。

吞吐量瓶頸:

同時,⾼並發訪問請求也使得集中式數據庫成為系統的最⼤瓶頸。

一般的吞吐量是多少呢? 請參見 3 高架構秒殺部分內容。

在傳統的關系型數據庫⽆法滿⾜互聯⽹場景需要的情況下,將數據存儲⾄原⽣⽀持分布式的 NoSQL 的嘗試越來越多。

但 NoSQL 並不能包治百病,而關系型數據庫的地位卻依然不可撼動。

如果進行sql、nosql數據庫的選型呢? 請參見 推送中台架構部分的內容。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

分治模式在存儲領域的落地

分治模式在存儲領域的使用:數據分⽚

數據分⽚指按照某個維度將存放在單⼀數據庫中的數據, 分散地存放⾄多個數據庫或表中以達到提升性能瓶頸以及可⽤性的效果。

數據分⽚的有效⼿段是對關系型數據庫進⾏分庫和分表。

分庫能夠⽤於有效的分散對數據庫單點的訪問量;

分庫的合理的時機, 請參見 3 高架構秒殺部分內容。

分表能夠⽤於有效的數據量超過可承受閾值而產⽣的查詢瓶頸, 解決MySQL 單表性能問題

分表的合理的時機, 請參見 3 高架構秒殺部分內容。

使⽤多主多從的分⽚⽅式,可以有效的避免數據單點,從而提升數據架構的可⽤性。

通過分庫和分表進⾏數據的拆分來使得各個表的數據量保持在閾值以下,以及對流量進⾏疏導應對⾼訪問量,是應對⾼並發和海量數據系統的有效⼿段。

數據分⽚的拆分⽅式⼜分為垂直分⽚和⽔平分⽚。

分庫分表的問題

分庫導致的事務問題

不過,由於目前采用柔性事務居多,實際上,分庫的事務性能也是很高的,有關柔性事務,請參見瘋狂創客圈的專題博文:

分布式事務面試題 (史上最全、持續更新、吐血推薦)

Sharding-JDBC簡介

Sharding-JDBC 是當當網開源的適用於微服務的分布式數據訪問基礎類庫,完整的實現了分庫分表,讀寫分離和分布式主鍵功能,並初步實現了柔性事務。

從 2016 年開源至今,在經歷了整體架構的數次精煉以及穩定性打磨后,如今它已積累了足夠的底蘊。

官方的網址如下:

http://shardingsphere.apache.org/index_zh.html

ShardingSphere是一套開源的分布式數據庫中間件解決方案組成的生態圈,它由Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar這3款相互獨立的產品組成。

他們均提供標准化的數據分片、分布式事務 和 數據庫治理功能,可適用於如Java同構、異構語言、雲原生等各種多樣化的應用場景。

Apache ShardingSphere 是一套開源的分布式數據庫中間件解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規划中)這 3 款相互獨立,卻又能夠混合部署配合使用的產品組成。 它們均提供標准化的數據分片、分布式事務和數據庫治理功能,可適用於如 Java 同構、異構語言、雲原生等各種多樣化的應用場景。

Apache ShardingSphere 定位為關系型數據庫中間件,旨在充分合理地在分布式的場景下利用關系型數據庫的計算和存儲能力,而並非實現一個全新的關系型數據庫。 它通過關注不變,進而抓住事物本質。關系型數據庫當今依然占有巨大市場,是各個公司核心業務的基石,未來也難於撼動,我們目前階段更加關注在原有基礎上的增量,而非顛覆。

Apache ShardingSphere 5.x 版本開始致力於可插拔架構,項目的功能組件能夠靈活的以可插拔的方式進行擴展。 目前,數據分片、讀寫分離、數據加密、影子庫壓測等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 與協議的支持,均通過插件的方式織入項目。 開發者能夠像使用積木一樣定制屬於自己的獨特系統。Apache ShardingSphere 目前已提供數十個 SPI 作為系統的擴展點,仍在不斷增加中。

ShardingSphere 已於2020年4月16日成為 Apache 軟件基金會的頂級項目。

Sharding-JDBC的優勢

Sharding-JDBC直接封裝JDBC API,可以理解為增強版的JDBC驅動,舊代碼遷移成本幾乎為零:

  • 可適用於任何基於Java的ORM框架,如JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC。
  • 可基於任何第三方的數據庫連接池,如DBCP、C3P0、 BoneCP、Druid等。
  • 理論上可支持任意實現JDBC規范的數據庫。雖然目前僅支持MySQL,但已有支持Oracle、SQLServer等數據庫的計划。

Sharding-JDBC定位為輕量Java框架,使用客戶端直連數據庫,以jar包形式提供服務,無proxy代理層,無需額外部署,無其他依賴,DBA也無需改變原有的運維方式。

Sharding-JDBC分片策略靈活,可支持等號、between、in等多維度分片,也可支持多分片鍵。

SQL解析功能完善,支持聚合、分組、排序、limit、or等查詢,並支持Binding Table以及笛卡爾積表查詢。

與常見開源產品對比

下表僅列出在數據庫分片領域非常有影響力的幾個項目:

img

通過以上表格可以看出,Cobar(MyCat)屬於中間層方案,在應用程序和MySQL之間搭建一層Proxy。

中間層介於應用程序與數據庫間,需要做一次轉發,而基於JDBC協議並無額外轉發,直接由應用程序連接數據庫,性能上有些許優勢。這里並非說明中間層一定不如客戶端直連,除了性能,需要考慮的因素還有很多,中間層更便於實現監控、數據遷移、連接管理等功能。

Cobar-Client、TDDL和Sharding-JDBC均屬於客戶端直連方案。

此方案的優勢在於輕便、兼容性、性能以及對DBA影響小。其中Cobar-Client的實現方式基於ORM(Mybatis)框架,其兼容性與擴展性不如基於JDBC協議的后兩者。

img

目前常用的就是Cobar(MyCat)與Sharding-JDBC兩種方案

MyCAT

MyCAT是社區愛好者在阿里cobar基礎上進行二次開發,解決了cobar當時存 在的一些問題,並且加入了許多新的功能在其中。目前MyCAT社區活 躍度很高,

目前已經有一些公司在使用MyCAT。

總體來說支持度比 較高,也會一直維護下去,發展到目前的版本,已經不是一個單純的MySQL代理了,

它的后端可以支持MySQL, SQL Server, Oracle, DB2, PostgreSQL等主流數據庫,也支持MongoDB這種新型NoSQL方式的存儲,未來還會支持更多類型的存儲。

MyCAT是一個強大的數據庫中間件,不僅僅可以用作讀寫分離,以及分表分庫、容災管理,而且可以用於多租戶應用開發、雲平台基礎設施,讓你的架構具備很強的適應性和靈活性,

借助於即將發布的MyCAT只能優化模塊,系統的數據訪問瓶頸和熱點一目了然,

根據這些統計分析數據,你可以自動或手工調整后端存儲,將不同的表隱射到不同存儲引擎上,而整個應用的代碼一行也不用改變。

MyCAT是在Cobar基礎上發展的版本,兩個顯著提高:

  • 后端由BIO改為NIO,並發量有大幅提高;

  • 增加了對Order By, Group By, Limit等聚合功能

(雖然Cobar也可以支持Order By, Group By, Limit語法,但是結果沒有進行聚合,只是簡單返回給前端,聚合功能還是需要業務系統自己完成, 適用於有專門團隊維護的大型企業、或者大團隊。)

Sharding-JDBC

Sharding-JDBC定位為輕量Java框架,使用客戶端直連數據庫,以jar包形式提供服務,無proxy代理層,無需額外部署,無其他依賴,DBA也無需改變原有的運維方式。

所以 ,適用於中小企業、或者中小團隊

Sharding-JDBC分片策略靈活,可支持等號、between、in等多維度分片,也可支持多分片鍵。

SQL解析功能完善,支持聚合、分組、排序、limit、or等查詢,並支持Binding Table以及笛卡爾積表查詢。

Sharding-JDBC 功能列表

  • 分庫 & 分表
  • 讀寫分離
  • 分布式主鍵

高並發數據分片的兩大工作

一般情況下,開發維度的數據分片,大多是以水平切分模式(水平分庫、分表)為基礎來說的,

垂直分片主要在於 運維維度,或者 或者做存儲的深級改造的時候。

數據分片的工作

簡單來說,數據分片的工作分為兩大工作 :

第一大工作:分片的拆分

es 的數據分片的背后原理

參見視頻

rediscluster的數據分片的背后原理

表的拆分:

將一張大表 t_order ,拆分生成數個表結構完全一致的小表 t_order_0、t_order_1、···、t_order_n,

每張小表,只存儲大表中的一部分數據,

第二大工作:分片的路由

當執行一條SQL時,會通過 路由策略 , 將數據route(路由)到不同的分片內。

面臨的問題:

  • 分片建的選擇

  • 分片策略的選擇

  • 分片算法的選擇

什么是數據分片?

按照分片規則把數據分到若干個shard、partition當中

在這里插入圖片描述

主要的分片算法

range 分片

一種是按照 range 來分,就是每個片,一段連續的數據,這個一般是按比如時間范圍/數據范圍來的,但是這種一般較少用,因為很容易發生數據傾斜,大量的流量都打在最新的數據上了。

比如,安裝數據范圍分片,把1到100個數字,要保存在3個節點上

按照順序分片,把數據平均分配三個節點上

  • 1號到33號數據保存到節點1上
  • 34號到66號數據保存到節點2上
  • 67號到100號數據保存到節點3上

在這里插入圖片描述

ID取模分片

此種分片規則將數據分成n份(通常dn節點也為n),從而將數據均勻的分布於各個表中,或者各節點上。

擴容方便。

ID取模分片常用在關系型數據庫的設計

具體請參見 秒殺視頻的 億級庫表架構設計

hash 哈希分布

使用hash 算法,獲取key的哈希結果,再按照規則進行分片,這樣可以保證數據被打散,同時保證數據分布的比較均勻

哈希分布方式分為三個分片方式:

  • 哈希取余分片
  • 一致性哈希分片
  • 虛擬槽分片

哈希取余模分片

例如1到100個數字,對每個數字進行哈希運算,然后對每個數的哈希結果除以節點數進行取余,余數為1則保存在第1個節點上,余數為2則保存在第2個節點上,余數為0則保存在第3個節點,這樣可以保證數據被打散,同時保證數據分布的比較均勻

比如有100個數據,對每個數據進行hash運算之后,與節點數進行取余運算,根據余數不同保存在不同的節點上

在這里插入圖片描述

哈希取余分片是非常簡單的一種分片方式

哈希取模分片有一個問題

即當增加或減少節點時,原來節點中的80%的數據會進行遷移操作,對所有數據重新進行分布

哈希取余分片,建議使用多倍擴容的方式,例如以前用3個節點保存數據,擴容為比以前多一倍的節點即6個節點來保存數據,這樣只需要適移50%的數據。

數據遷移之后,第一次無法從緩存中讀取數據,必須先從數據庫中讀取數據,然后回寫到緩存中,然后才能從緩存中讀取遷移之后的數據

img

哈希取余分片優點:

  • 配置簡單:對數據進行哈希,然后取余

哈希取余分片缺點:

  • 數據節點伸縮時,導致數據遷移
  • 遷移數量和添加節點數據有關,建議翻倍擴容

一致性哈希分片

一致性哈希原理:

將所有的數據當做一個token環,

token環中的數據范圍是0到2的32次方。

然后為每一個數據節點分配一個token范圍值,這個節點就負責保存這個范圍內的數據。

img

對每一個key進行hash運算,被哈希后的結果在哪個token的范圍內,則按順時針去找最近的節點,這個key將會被保存在這個節點上。

img

一致性哈希分片的節點擴容

在下面的圖中:

  • 有4個key被hash之后的值在在n1節點和n2節點之間,按照順時針規則,這4個key都會被保存在n2節點上

  • 如果在n1節點和n2節點之間添加n5節點,當下次有key被hash之后的值在n1節點和n5節點之間,這些key就會被保存在n5節點上面了

下圖的例子里,添加n5節點之后:

  • 數據遷移會在n1節點和n2節點之間進行
  • n3節點和n4節點不受影響
  • 數據遷移范圍被縮小很多

同理,如果有1000個節點,此時添加一個節點,受影響的節點范圍最多只有千分之2。所以,一致性哈希一般用在節點比較多的時候,節點越多,擴容時受影響的節點范圍越少

img

分片方式:哈希 + 順時針(優化取余)

一致性哈希分片優點:

  • 一致性哈希算法解決了分布式下數據分布問題。比如在緩存系統中,通過一致性哈希算法把緩存鍵映射到不同的節點上,由於算法中虛擬節點的存在,哈希結果一般情況下比較均勻。
  • 節點伸縮時,只影響鄰近節點,但是還是有數據遷移

“但沒有一種解決方案是銀彈,能適用於任何場景。所以實踐中一致性哈希算法有哪些缺陷,或者有哪些場景不適用呢?”

一致性哈希分片缺點:

一致性哈希在大批量的數據場景下負載更加均衡,但是在數據規模小的場景下,會出現單位時間內某個節點完全空閑的情況出現。

虛擬槽分片 (范圍分片的變種)

Redis Cluster在設計中沒有使用一致性哈希(Consistency Hashing),而是使用數據分片引入哈希槽(hash slot)來實現;

虛擬槽分片是Redis Cluster采用的分片方式.

虛擬槽分片 ,可以理解為范圍分片的變種, hash取模分片+范圍分片, 把hash值取余數分為n段,一個段給一個節點負責

在這里插入圖片描述

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

es的數據分片兩大工作

Shards

代表索引分片,es可以把一個完整的索引分成多個分片,這樣的好處是可以把一個大的索引拆分成多個,分布到不同的節點上。構成分布式搜索。

分片的數量只能在索引創建前指定,並且索引創建后不能更改。(why,大家可以獨立思考一下!)

分片配置建議:

每個分片大小不要超過30G,硬盤條件好的話,不建議超過100G.

(官方推薦,每個shard的數據量應該在20GB - 50GB)。

總而言之,每個分片都是一個Lucene實例,當查詢請求打到ES后,ES會把請求轉發到每個shard上分別進行查詢,最終進行匯總。

這時候,shard越少,產生的額外開銷越少

路由機制

一條數據是如何落地到對應的shard上的?

當索引一個文檔的時候,文檔會被存儲到一個主分片中。

Elasticsearch 如何知道一個文檔應該存放到哪個分片中呢?

img

es的路由過程是根據下面這個算法決定的:

shard_num = hash(_routing) % num_primary_shards


其中 _routing是一個可變值,默認是文檔的 _id 的值  ,也可以設置成一個自定義的值。Elasticsearch文檔的ID(類似於關系數據庫中的自增ID),

_routing 通過 hash 函數生成一個數字,然后這個數字再除以  num_of_primary_shards (主分片的數量)后得到余數 。

這個分布在 0 到  number_of_primary_shards-1 之間的余數,就是我們所尋求的文檔所在分片的位置。


這就解釋了為什么我們要在創建索引的時候就確定好主分片的數量 並且永遠不會改變這個數量

因為如果數量變化了,那么所有之前路由的值都會無效,文檔也再也找不到了。

假設你有一個100個分片的索引。當一個請求在集群上執行時會發生什么呢?

1. 這個搜索的請求會被發送到一個節點
2. 接收到這個請求的節點,將這個查詢廣播到這個索引的每個分片上(可能是主分片,也可能是復本分片)
3. 每個分片執行這個搜索查詢並返回結果
4. 結果在通道節點上合並、排序並返回給用戶

img

rediscluster的數據分片兩大工作

虛擬槽分片 ( hash取模分片+范圍分片的混血)

Redis Cluster在設計中沒有使用一致性哈希(Consistency Hashing),而是使用數據分片引入哈希槽(hash slot)來實現;

虛擬槽分片是Redis Cluster采用的分片方式.

在該分片方式中:

  • 首先 預設虛擬槽,每個槽為一個hash值,每個node負責一定槽范圍。
  • 每一個值都是key的hash值取余,每個槽映射一個數據子集,一般比節點數大

Redis Cluster中預設虛擬槽的范圍為0到16383

在這里插入圖片描述

3個節點的Redis集群虛擬槽分片結果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

虛擬槽分片的路由機制:

1.把16384槽按照節點數量進行平均分配,由節點進行管理
2.對每個key按照CRC16規則進行hash運算
3.把hash結果對16383進行取模
4.把余數發送給Redis節點
5.節點接收到數據,驗證是否在自己管理的槽編號的范圍

  • 如果在自己管理的槽編號范圍內,則把數據保存到數據槽中,然后返回執行結果
  • 如果在自己管理的槽編號范圍外,則會把數據發送給正確的節點,由正確的節點來把數據保存在對應的槽中

需要注意的是:Redis Cluster的節點之間會共享消息,每個節點都會知道是哪個節點負責哪個范圍內的數據槽

虛擬槽分布方式中,由於每個節點管理一部分數據槽,數據保存到數據槽中。

當節點擴容或者縮容時,對數據槽進行重新分配遷移即可,數據不會丟失。

shardingjdbc的數據分片兩大工作

第一大工作:分片的拆分

表的拆分:

將一張大表 t_order ,拆分生成數個表結構完全一致的小表 t_order_0、t_order_1、···、t_order_n,

每張小表,只存儲大表中的一部分數據,

例子:user表的數據分片

在這里插入圖片描述

例子:order表的數據分片

在這里插入圖片描述

第二大工作:分片的路由

當執行一條SQL時,會通過 路由策略 , 將數據route(路由)到不同的分片內。

  • 數據源的路由

  • 表的路由

面臨的問題:

  • 分片key的選擇
  • 分片策略的選擇
  • 分片算法的選擇

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

核心概念

分片鍵

⽤於分⽚的字段,是將數據庫(表)⽔平拆分的關鍵字段。

在對表中的數據進行分片時,首先要選出一個分片鍵(Shard Key),即用戶可以通過這個字段進行數據的水平拆分。

例:

將訂單表中的訂單主鍵的尾數取模分⽚,則訂單主鍵為分⽚字段。

執行表的選擇

我們將 t_order 表分片以后,當執行一條 SQL 時,通過對字段 order_id 取模的方式來決定要執行的表, 這條數據該在哪個數據庫中的哪個表中執行,此時 order_id 字段就是分片健。

在這里插入圖片描述

執行庫的選擇(數據源的選擇)

這樣以來同一個訂單的相關數據就會存在同一個數據庫表中,大幅提升數據檢索的性能,

說明

  • 除了使用單個字段作為分片件, sharding-jdbc 還支持根據多個字段作為分片健進行分片。

  • SQL 中如果⽆分⽚字段,將執⾏全路由,性能較差。

數據節點

數據節點是分庫分表中一個不可再分的最小數據單元(表),它由數據源名稱和數據表組成,

例如上圖中 ds1.t_user_0 就表示一個數據節點。

邏輯表

邏輯表是指一組具有相同邏輯和數據結構表的總稱。

比如我們將訂單表 t_order 拆分成 t_order_0 ··· t_order_9 等 10 張表。

此時我們會發現分庫分表以后數據庫中已不在有 t_order 這張表,取而代之的是 t_order_n,但我們在代碼中寫 SQL 依然按 t_order 來寫。

此時 t_order 就是這些拆分表的邏輯表。

例如上圖中 t_user 就表示一個數據節點。

真實表(物理表)

真實表也就是上邊提到的 t_order_n 數據庫中真實存在的物理表。

例如上圖中 t_user _0就表示一個真實表。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

分片策略

分片策略是一種抽象的概念,實際分片操作的是由分片算法和分片健來完成的。

真正可⽤於分⽚操作的是分⽚鍵 + 分⽚算法,也就是分⽚策略。

在這里插入圖片描述

為什么要這么設計,是出於分⽚算法的獨⽴性,將其獨⽴抽離。

ShardingSphere-JDBC考慮更多的靈活性,把分片算法單獨抽象出來,方便開發者擴展;

標准分片策略

標准分片策略適用於單分片鍵,此策略支持 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 兩個分片算法。

其中 PreciseShardingAlgorithm 是必選的,用於處理 = 和 IN 的分片。

RangeShardingAlgorithm 用於處理BETWEEN AND, >, <,>=,<= 條件分片,

RangeShardingAlgorithm 是可選的, 如果不配置RangeShardingAlgorithm,SQL中的條件等將按照全庫路由處理。

在這里插入圖片描述

復合分片策略

復合分片策略對應 ComplexShardingStrategy。

同樣支持對 SQL語句中的 =,>, <, >=, <=,IN和 BETWEEN AND 的分片操作。

不同的是它支持多分片鍵,具體分配片細節完全由應用開發者實現。

ComplexShardingStrategy ⽀持多分⽚鍵,由於多分⽚鍵之間的關系復雜,因此並未進⾏過多的封裝,而是直接將分⽚鍵值組合以及分⽚操作符透傳⾄分⽚算法,完全由應⽤開發者實現,提供最⼤的靈活度。

表達式分片策略(inline內聯分片策略)

行表達式分片策略,支持對 SQL語句中的 = 和 IN 的分片操作,但只支持單分片鍵。

這種策略通常用於簡單的分片,不需要自定義分片算法,可以直接在配置文件中接着寫規則。

t_order_$->{t_order_id % 4} 代表 t_order 對其字段 t_order_id取模,拆分成4張表,而表名分別是t_order_0 到 t_order_3。

強制分片策略(Hint 暗示分片策略)

Hint 分片策略,通過指定分片健而非從 SQL 中提取分片健的方式進行分片的策略。

對於分⽚值⾮ SQL 決定,不是來自於分片建,甚至連分片建都沒有 ,而由其他外置條件決定的場景,可使⽤Hint 分片策略 。

前面的分片策略都是解析 SQL 語句, 提取分片建和分片值,並根據設置的分片算法進行分片。

Hint 分片算法 指定分⽚值而⾮從 SQL 中提取,而是手工設置的⽅式,進⾏分⽚的策略。

例:內部系統,按照員⼯登錄主鍵分庫,而數據庫中並⽆此字段。

不分⽚策略

對應 NoneShardingStrategy。不分⽚的策略。

這種嚴格來說不算是一種分片策略了。

只是ShardingSphere也提供了這么一個配置。

分片算法

上邊我們提到可以用分片健取模的規則分片,但這只是比較簡單的一種,

在實際開發中我們還希望用 >=、<=、>、<、BETWEEN 和 IN 等條件作為分片規則,自定義分片邏輯,這時就需要用到分片策略與分片算法。

從執行 SQL 的角度來看,分庫分表可以看作是一種路由機制,把 SQL 語句路由到我們期望的數據庫或數據表中並獲取數據,分片算法可以理解成一種路由規則。

咱們先捋一下它們之間的關系,分片策略只是抽象出的概念,它是由分片算法和分片健組合而成,分片算法做具體的數據分片邏輯。

分庫、分表的分片策略配置是相對獨立的,可以各自使用不同的策略與算法,每種策略中可以是多個分片算法的組合,每個分片算法可以對多個分片健做邏輯判斷。

img

sharding-jdbc 提供了多種分片算法:

提供了抽象分片算法類:ShardingAlgorithm,根據類型又分為:精確分片算法、區間分片算法、復合分片算法以及Hint分片算法;

  • 精確分片算法:對應PreciseShardingAlgorithm類,主要用於處理 =IN的分片;
  • 區間分片算法:對應RangeShardingAlgorithm類,主要用於處理 BETWEEN AND, >, <, >=, <= 分片;
  • 復合分片算法:對應ComplexKeysShardingAlgorithm類,用於處理使用多鍵作為分片鍵進行分片的場景;
  • Hint分片算法:對應HintShardingAlgorithm類,用於處理使用 Hint 行分片的場景;

精確分片算法 PreciseShardingAlgorithm

精確分片算法(PreciseShardingAlgorithm)用於單個字段作為分片鍵,SQL中有 = 與 IN 等條件的分片,

需要配合 StandardShardingStrategy 使⽤。

范圍分片算法 RangeShardingAlgorithm

范圍分片算法(RangeShardingAlgorithm)用於單個字段作為分片鍵,SQL中有 BETWEEN AND、>、<、>=、<= 等條件的分片,需要需要配合 StandardShardingStrategy 使⽤。

復合分片算法 ComplexKeysShardingAlgorithm

對應 ComplexKeysShardingAlgorithm,⽤於處理使⽤ 多鍵作為分⽚鍵 進⾏分⽚的場景,

(多個字段作為分片鍵),同時獲取到多個分片健的值,根據多個字段處理業務邏輯。

包含多個分⽚鍵的邏輯較復雜,需要應⽤開發者⾃⾏處理其中的復雜度。

需要配合 ComplexShardingStrategy 使⽤。

需要在復合分片策略(ComplexShardingStrategy )下使用。

Hint 分片算法 HintShardingAlgorithm

Hint 分片算法(HintShardingAlgorithm)稍有不同

前面的算法(如StandardShardingAlgorithm)都是解析 SQL 語句, 提取分片值,並根據設置的分片算法進行分片。

Hint 分片算法 指定分⽚值而⾮從 SQL 中提取,而是手工設置的⽅式,進⾏分⽚的策略。

對於分⽚值⾮ SQL 決定,不是來自於分片建,甚至連分片建都沒有 ,而由其他外置條件決定的場景,可使⽤Hint 分片算法 。

就需要通過 Java API 等方式 指定 分片值,這也叫強制路由、或者說 暗示路由

例: 內部系統,按照員⼯登錄主鍵分庫,而數據庫中並⽆此字段。

SQL Hint ⽀持通過 Java API 和 SQL 注釋(待實現)兩種⽅式使⽤。

ShardingJDBC的分片策略

整個ShardingJDBC 分庫分表的核心就是在於**配置 分片策略+分片算法 **。

我們的這些實戰都是使用的inline分片算法,即提供一個分片鍵和一個分片表達式來制定分片算法。

這種方式配置簡單,功能靈活,是分庫分表最佳的配置方式,並且對於絕大多數的分庫分片場景來說,都已經非常好用了。

但是,如果針對一些更為復雜的分片策略,例如多分片鍵、按范圍分片等場景,inline分片算法就有點力不從心了。

所以,我們還需要學習下ShardingSphere提供的其他幾種分片策略。

ShardingSphere目前提供了一共五種分片策略:

  • NoneShardingStrategy 不分片

  • InlineShardingStrategy

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

InlineShardingStrategy

最常用的分片方式

實現方式:

按照分片表達式來進行分片。

實戰:JavaAPI使用InlineShardingStrategy 實戰

Inline內聯分片策略

分片策略基本和上面的分片算法對應,包括:標准分片策略、復合分片策略、Hint分片策略、內聯分片策略、不分片策略;\

  • 內聯分片策略:

對應InlineShardingStrategy類,沒有提供分片算法,路由規則通過表達式來實現;

Inline內聯分片配置類

在使用中我們並沒有直接使用上面的分片策略類,ShardingSphere-JDBC分別提供了對應策略的配置類包括:

  • InlineShardingStrategyConfiguration

Inline內聯分片實戰

有了以上相關基礎概念,接下來針對每種分片策略做一個簡單的實戰,

在實戰前首先准備好庫和表;

具體請參見視頻,和配套源碼

准備真實數據源

分別准備兩個庫:ds0ds1;然后每個庫分別包含4個表

CREATE TABLE `t_user_0` (`user_id` bigInt NOT NULL, `name` VARCHAR(45) NULL, PRIMARY KEY (`user_id`));
CREATE TABLE `t_user_1` (`user_id` bigInt NOT NULL, `name` VARCHAR(45) NULL, PRIMARY KEY (`user_id`));
CREATE TABLE `t_user_2` (`user_id` bigInt NOT NULL, `name` VARCHAR(45) NULL, PRIMARY KEY (`user_id`));
CREATE TABLE `t_user_3` (`user_id` bigInt NOT NULL, `name` VARCHAR(45) NULL, PRIMARY KEY (`user_id`));


我們這里有兩個數據源,這里都使用java代碼的方式來配置:

  @Before

    public void buildShardingDataSource() throws SQLException {
        /*
         * 1. 數據源集合:dataSourceMap
         * 2. 分片規則:shardingRuleConfig
         *
         */

        DataSource druidDs1 = buildDruidDataSource(
                "jdbc:mysql://cdh1:3306/sharding_db1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC",
                "root", "123456");

        DataSource druidDs2 = buildDruidDataSource(
                "jdbc:mysql://cdh1:3306/sharding_db2?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC",
                "root", "123456");
        // 配置真實數據源
        Map<String, DataSource> dataSourceMap = new HashMap<String, DataSource>();
        // 添加數據源.
        // 兩個數據源ds_0和ds_1
        dataSourceMap.put("ds0",druidDs1);
        dataSourceMap.put("ds1", druidDs2);

        /**
         * 需要構建表規則
         * 1. 指定邏輯表.
         * 2. 配置實際節點》
         * 3. 指定主鍵字段.
         * 4. 分庫和分表的規則》
         *
         */
        // 配置分片規則
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        //消息表分片規則
        TableRuleConfiguration userShardingRuleConfig = userShardingRuleConfig();
        shardingRuleConfig.getTableRuleConfigs().add(userShardingRuleConfig);
        // 多數據源一定要指定默認數據源
        // 只有一個數據源就不需要
        shardingRuleConfig.setDefaultDataSourceName("ds0");

        Properties p = new Properties();
        //打印sql語句,生產環境關閉
        p.setProperty("sql.show", Boolean.TRUE.toString());

        dataSource= ShardingDataSourceFactory.createDataSource(
                dataSourceMap, shardingRuleConfig, p);

    }

這里配置的兩個數據源都是普通的數據源,最后會把dataSourceMap交給ShardingDataSourceFactory管理;

表規則配置

表規則配置類TableRuleConfiguration,包含了五個要素:

邏輯表、真實數據節點、數據庫分片策略、數據表分片策略、分布式主鍵生成策略;

  /**
     * 消息表的分片規則
     */
    protected TableRuleConfiguration userShardingRuleConfig() {
        String logicTable = USER_LOGIC_TB;

        //獲取實際的 ActualDataNodes
        String actualDataNodes = "ds$->{0..1}.t_user_$->{0..1}";

        TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration(logicTable, actualDataNodes);

        //設置分表策略
        // inline 模式
        ShardingStrategyConfiguration tableShardingStrategy =
                new InlineShardingStrategyConfiguration("user_id", "t_user_$->{user_id % 2}");
                //自定義模式
//        TableShardingAlgorithm tableShardingAlgorithm = new TableShardingAlgorithm();
//        ShardingStrategyConfiguration tableShardingStrategy = new StandardShardingStrategyConfiguration("user_id", tableShardingAlgorithm);

        tableRuleConfig.setTableShardingStrategyConfig(tableShardingStrategy);

        // 配置分庫策略(Groovy表達式配置db規則)
        // inline 模式
        ShardingStrategyConfiguration dsShardingStrategy = new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}");
        //自定義模式
//        DsShardingAlgorithm dsShardingAlgorithm = new DsShardingAlgorithm();
//        ShardingStrategyConfiguration dsShardingStrategy = new StandardShardingStrategyConfiguration("user_id", dsShardingAlgorithm);
        tableRuleConfig.setDatabaseShardingStrategyConfig(dsShardingStrategy);
        tableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "user_id"));
        return tableRuleConfig;
    }

  • 邏輯表:這里配置的邏輯表就是t_user,對應的物理表有t_user_0,t_user_1;

  • 真實數據節點:這里使用行表達式進行配置的,簡化了配置;上面的配置就相當於配置了:

    db0
      ├── t_user_0 
      └── t_user_1 
    db1
      ├── t_user_0 
      └── t_user_1
    
    
  • 數據庫分片策略:

    這里的庫分片策略就是上面介紹的五種類型,

    這里使用的InlineShardingStrategy,需要設置 內聯表達式,groovy表達式;

    
            //設置分表策略
            // inline 模式
            ShardingStrategyConfiguration tableShardingStrategy =
                    new InlineShardingStrategyConfiguration("user_id", "t_user_$->{user_id % 2}");
                    //自定義模式
    //        TableShardingAlgorithm tableShardingAlgorithm = new TableShardingAlgorithm();
    //        ShardingStrategyConfiguration tableShardingStrategy = new StandardShardingStrategyConfiguration("user_id", tableShardingAlgorithm);
    
            tableRuleConfig.setTableShardingStrategyConfig(tableShardingStrategy);
    

    這里的shardingValue就是user_id對應的真實值,每次和2取余;availableTargetNames可選擇就是{ds0,ds1};看余數和哪個庫能匹配上就表示路由到哪個庫;

  • 數據表分片策略:指定的分片鍵(order_id)和分庫策略不一致,其他都一樣;

  • 分布式主鍵生成策略:ShardingSphere-JDBC提供了多種分布式主鍵生成策略,后面詳細介紹,這里使用雪花算法;

groovy語法說明

行表達式的使⽤⾮常直觀,只需要在配置中使⽤ ${ expression } 或 $->{ expression } 標識 行表達式即可。

⽬前⽀持數據節點和分⽚算法這兩個部分的配置。

行表達式的內容使⽤的是 Groovy 的語法,Groovy 能夠⽀持的所有操作, 行表達式均能夠⽀持。例如:
${begin..end} 表⽰范圍區間
${[unit1, unit2, unit_x]} 表⽰枚舉值

行表達式中如果出現連續多個 ${ expression } 或 $->{ expression } 表達式,整個表達式最終的結果將會根據每個表達式的結果進笛卡爾組合。
例如,以下⾏表達式: ${['online', 'offline']}_table${1..3}
最終會解析為:
online_table1, online_table2, online_table3, offline_table1, offline_table2,offline_table3

配置數據節點時對於均勻分布的數據節點,如果數據結構如下:

db0
├── t_order0
└── t_order1
db1
├── t_order0
└── t_order1

用行表達式可以簡化為:
db${0..1}.t_order${0..1}
或者
db$->{0..1}.t_order$->{0..1}
對於⾃定義的數據節點,如果數據結構如下:

db0
├── t_order0
└── t_order1
db1
├── t_order2
├── t_order3
└── t_order4

用行表達式可以簡化為:
db0.t_order${0..1},db1.t_order${2..4}
或者
db0.t_order$->{0..1},db1.t_order$->{2..4}

配置分片規則

配置分片規則ShardingRuleConfiguration,包括多種配置規則:

表規則配置、綁定表配置、廣播表配置、默認數據源名稱、默認數據庫分片策略、默認表分片策略、默認主鍵生成策略、主從規則配置、加密規則配置;

  • 表規則配置 tableRuleConfigs:也就是上面配置的庫分片策略和表分片策略,也是最常用的配置;
  • 綁定表配置 bindingTableGroups:指分⽚規則⼀致的主表和⼦表;綁定表之間的多表關聯查詢不會出現笛卡爾積關聯,關聯查詢效率將⼤⼤提升;
  • 廣播表配置 broadcastTables:所有的分⽚數據源中都存在的表,表結構和表中的數據在每個數據庫中均完全⼀致。適⽤於數據量不⼤且需要與海量數據的表進⾏關聯查詢的場景;
  • 默認數據源名稱 defaultDataSourceName:未配置分片的表將通過默認數據源定位;
  • 默認數據庫分片策略 defaultDatabaseShardingStrategyConfig:表規則配置可以設置數據庫分片策略,如果沒有配置可以在這里面配置默認的;
  • 默認表分片策略 defaultTableShardingStrategyConfig:表規則配置可以設置表分片策略,如果沒有配置可以在這里面配置默認的;
  • 默認主鍵生成策略 defaultKeyGeneratorConfig:表規則配置可以設置主鍵生成策略,如果沒有配置可以在這里面配置默認的;內置UUID、SNOWFLAKE生成器;
  • 主從規則配置 masterSlaveRuleConfigs:用來實現讀寫分離的,可配置一個主表多個從表,讀面對多個從庫可以配置負載均衡策略;
  • 加密規則配置 encryptRuleConfig:提供了對某些敏感數據進行加密的功能,提供了⼀套完整、安全、透明化、低改造成本的數據加密整合解決⽅案;

實戰:數據插入

以上准備好,就可以操作數據庫了,這里執行插入操作:


    /**
     * 新增測試.
     *
     */
    @Test
    public  void testInsertUser() throws SQLException {

        /*
         * 1. 需要到DataSource
         * 2. 通過DataSource獲取Connection
         * 3. 定義一條SQL語句.
         * 4. 通過Connection獲取到PreparedStament.
         *  5. 執行SQL語句.
         *  6. 關閉連接.
         */


        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "insert into t_user(name) values('name-0001')";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        preparedStatement.execute();

         sql = "insert into t_user(name) values('name-0002')";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.execute();

        // * 6. 關閉連接.
        preparedStatement.close();
        connection.close();
    }

通過以上配置的真實數據源、分片規則以及屬性文件創建分片數據源ShardingDataSource

接下來就可以像使用單庫單表一樣操作分庫分表了,sql中可以直接使用邏輯表,分片算法會根據具體的值就行路由處理;

經過路由最終:奇數入ds1.t_user_1,偶數入ds0.t_user_0;

實戰:數據查詢

以上准備好,就可以操作數據庫了,這里執行查詢操作:

  /**
     * 新增測試.
     *
     */
    @Test
    public  void testSelectUser() throws SQLException {

        /*
         * 1. 需要到DataSource
         * 2. 通過DataSource獲取Connection
         * 3. 定義一條SQL語句.
         * 4. 通過Connection獲取到PreparedStament.
         *  5. 執行SQL語句.
         *  6. 關閉連接.
         */


        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "select * from  t_user where user_id=10000";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        ResultSet resultSet= preparedStatement.executeQuery();


        // * 6. 關閉連接.
        preparedStatement.close();
        connection.close();
    }

實戰:Properties配置InlineShardingStrategy 實戰

通過 Properties 配置來使用 InlineShardingStrategy

配置參數:

inline.shardingColumn 分片鍵;

inline.algorithmExpression 分片表達式

配置實例

spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds0.filters=com.alibaba.druid.filter.stat.StatFilter,com.alibaba.druid.wall.WallFilter,com.alibaba.druid.filter.logging.Log4j2Filter
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://cdh1:3306/sharding_db1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.maxActive=20
spring.shardingsphere.datasource.ds0.initialSize=1
spring.shardingsphere.datasource.ds0.maxWait=60000
spring.shardingsphere.datasource.ds0.minIdle=1
spring.shardingsphere.datasource.ds0.timeBetweenEvictionRunsMillis=60000
spring.shardingsphere.datasource.ds0.minEvictableIdleTimeMillis=300000
spring.shardingsphere.datasource.ds0.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.ds0.testWhileIdle=true
spring.shardingsphere.datasource.ds0.testOnBorrow=false
spring.shardingsphere.datasource.ds0.testOnReturn=false
spring.shardingsphere.datasource.ds0.poolPreparedStatements=true
spring.shardingsphere.datasource.ds0.maxOpenPreparedStatements=20
spring.shardingsphere.datasource.ds0.connection-properties=druid.stat.merggSql=ture;druid.stat.slowSqlMillis=5000
spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds1.filters=com.alibaba.druid.filter.stat.StatFilter,com.alibaba.druid.wall.WallFilter,com.alibaba.druid.filter.logging.Log4j2Filter
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://cdh1:3306/sharding_db2?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds1.password=123456
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.maxActive=20
spring.shardingsphere.datasource.ds1.initialSize=1
spring.shardingsphere.datasource.ds1.maxWait=60000
spring.shardingsphere.datasource.ds1.minIdle=1
spring.shardingsphere.datasource.ds1.timeBetweenEvictionRunsMillis=60000
spring.shardingsphere.datasource.ds1.minEvictableIdleTimeMillis=300000
spring.shardingsphere.datasource.ds1.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.ds1.testWhileIdle=true
spring.shardingsphere.datasource.ds1.testOnBorrow=false
spring.shardingsphere.datasource.ds1.testOnReturn=false
spring.shardingsphere.datasource.ds1.poolPreparedStatements=true
spring.shardingsphere.datasource.ds1.maxOpenPreparedStatements=20
spring.shardingsphere.datasource.ds1.connection-properties=druid.stat.merggSql=ture;druid.stat.slowSqlMillis=5000




spring.shardingsphere.sharding.tables.t_user.actual-data-nodes=ds$->{0..1}.t_user_$->{0..1}
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_user.table-strategy.inline.algorithm-expression=t_user_$->{user_id % 2}
spring.shardingsphere.sharding.tables.t_user.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_user.database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
spring.shardingsphere.sharding.tables.t_user.key-generator.column=user_id
spring.shardingsphere.sharding.tables.t_user.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.t_user.key-generator.props.worker.id=123

spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1}
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2}
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123


spring.shardingsphere.sharding.binding-tables[0]=t_order,t_user


# 配置公共表
spring.shardingsphere.sharding.broadcast-tables=t_config
spring.shardingsphere.sharding.tables.t_config.key-generator.column=id
spring.shardingsphere.sharding.tables.t_config.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.t_config.key-generator.props.worker.id=123

行表達式分片策略的測試用例


    @Test
    public void testAddSomeUser() {

        for (int i = 0; i < 10; i++) {
            User dto = new User();

            dto.setName("user_" + i);

            //增加用戶
            entityService.addUser(dto);
        }


    }

    @Test
    public void testSelectAllUser() {
        //增加用戶
        List<User> all = entityService.selectAllUser();
        System.out.println(all);

    }


    @Test
    public void testSelectAll() {
        entityService.selectAll();
    }

行表達式分片策略的問題

行表達式分片策略(InlineShardingStrategy),在配置中使用 Groovy 表達式,提供對 SQL語句中的 =IN 的分片操作支持,它只支持單分片健。

行表達式分片策略適用於做簡單的分片算法,無需自定義分片算法,省去了繁瑣的代碼開發,是幾種分片策略中最為簡單的。

它的配置相當簡潔,這種分片策略利用inline.algorithm-expression書寫表達式。

比如:ds-$->{order_id % 2} 表示對 order_id 做取模計算,$ 是個通配符用來承接取模結果,最終計算出分庫ds-0 ··· ds-n,整體來說比較簡單。

spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1}
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2}

優勢:

相當簡潔

行表達式分片策略的問題:

不能支持 范圍分片

范圍分片 用於處理含有 BETWEEN AND>>=, <=<的分片處理。

具體演示,請參見視頻

實戰: JavaAPI使用StandardShardingStrategy

標准分片策略的使用場景

使用場景:SQL 語句中有>>=, <=<=INBETWEEN AND 操作符,都可以應用此分片策略。

標准分片策略(StandardShardingStrategy),它只支持對單個分片健(字段)為依據的分庫分表,

並提供了兩種分片算法 PreciseShardingAlgorithm(精准分片)和 RangeShardingAlgorithm(范圍分片)。

其中,精准分片算法是必須實現的算法,用於 SQL 含有 =IN 的分片處理;

范圍分片算法是非必選的,用於處理含有 BETWEEN AND>>=, <=<的分片處理。

一旦我們沒配置范圍分片算法,而 SQL 中又用到 BETWEEN AND 或者 like等,那么 SQL 將按全庫、表路由的方式逐一執行,查詢性能會很差需要特別注意。

實戰准備

有了以上相關基礎概念,接下來針對每種分片策略做一個簡單的實戰,

在實戰前首先准備好庫和表;

具體請參見視頻,和配套源碼

精准分片用於處理含有= 、in的分片處理。

范圍分片 用於處理含有 BETWEEN AND>>=, <=<的分片處理。

表規則配置

表規則配置類TableRuleConfiguration,包含了五個要素:

邏輯表、真實數據節點、數據庫分片策略、數據表分片策略、分布式主鍵生成策略;


  
    /**
     * 表的分片規則
     */
    protected TableRuleConfiguration userShardingRuleConfig() {
        String logicTable = USER_LOGIC_TB;

        //獲取實際的 ActualDataNodes
        String actualDataNodes = "ds$->{0..1}.t_user_$->{0..1}";

        // 兩個表達式的 笛卡爾積
//ds0.t_user_0
//ds1.t_user_0
//ds0.t_user_1
//ds1.t_user_1

        TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration(logicTable, actualDataNodes);

        //設置分表策略
        // inline 模式
//        ShardingStrategyConfiguration tableShardingStrategy =
//                new InlineShardingStrategyConfiguration("user_id", "t_user_$->{user_id % 2}");
        //自定義模式
        TablePreciseShardingAlgorithm tablePreciseShardingAlgorithm =
                new TablePreciseShardingAlgorithm();

     /*   RouteInfinityRangeShardingAlgorithm tableRangeShardingAlg =
                new RouteInfinityRangeShardingAlgorithm();
                */
        RangeOrderShardingAlgorithm tableRangeShardingAlg =
                new RangeOrderShardingAlgorithm();
        PreciseOrderShardingAlgorithm preciseOrderShardingAlgorithm =
                new PreciseOrderShardingAlgorithm();

        ShardingStrategyConfiguration tableShardingStrategy =
                new StandardShardingStrategyConfiguration("user_id",
                        preciseOrderShardingAlgorithm);
        tableRuleConfig.setTableShardingStrategyConfig(tableShardingStrategy);

        // 配置分庫策略(Groovy表達式配置db規則)
        // inline 模式
//        ShardingStrategyConfiguration dsShardingStrategy = new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}");
        //自定義模式
        DsPreciseShardingAlgorithm dsPreciseShardingAlgorithm = new DsPreciseShardingAlgorithm();
        RangeOrderShardingAlgorithm dsRangeShardingAlg =
                new RangeOrderShardingAlgorithm();

        ShardingStrategyConfiguration dsShardingStrategy =
                new StandardShardingStrategyConfiguration("user_id",
                        preciseOrderShardingAlgorithm);

        tableRuleConfig.setDatabaseShardingStrategyConfig(dsShardingStrategy);

        tableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "user_id"));
        return tableRuleConfig;
    }


數據庫分片策略 StandardShardingStrategyConfiguration

        ShardingStrategyConfiguration dsShardingStrategy =
                new StandardShardingStrategyConfiguration("user_id",
                        dsPreciseShardingAlgorithm);

這里的shardingValue就是user_id對應的真實值,每次和2取余;availableTargetNames可選擇就是{ds0,ds1};看余數和哪個庫能匹配上就表示路由到哪個庫;

  • 數據表分片策略:指定的分片鍵(order_id)和分庫策略不一致,其他都一樣;

  • 分布式主鍵生成策略:ShardingSphere-JDBC提供了多種分布式主鍵生成策略,后面詳細介紹,這里使用雪花算法;

測試用例

以上准備好,就可以操作數據庫了,這里執行插入操作:

  @Test
    public void testSelectUserIn() throws SQLException {

        /*
         * 1. 需要到DataSource
         * 2. 通過DataSource獲取Connection
         * 3. 定義一條SQL語句.
         * 4. 通過Connection獲取到PreparedStament.
         *  5. 執行SQL語句.
         *  6. 關閉連接.
         */


        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "select * from  t_user where user_id in (10,11,23)";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        ResultSet resultSet = preparedStatement.executeQuery();


        // * 6. 關閉連接.
        preparedStatement.close();
        connection.close();
    }


通過以上配置的真實數據源、分片規則以及屬性文件創建分片數據源ShardingDataSource

接下來就可以像使用單庫單表一樣操作分庫分表了,sql中可以直接使用邏輯表,分片算法會根據具體的值就行路由處理;

以上使用了最常見的精確分片算法,下面繼續看一下其他幾種分片算法;

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

實戰: JavaAPI使用RangeShardingAlgorithm實戰1

分片算法與分片值

四大分片算法

  • 精確分片算法 PreciseShardingAlgorithm

精確分片算法(PreciseShardingAlgorithm)用於單個字段作為分片鍵,SQL中有 = 與 IN 等條件的分片,

需要配合 StandardShardingStrategy 使⽤。

  • 范圍分片算法 RangeShardingAlgorithm

范圍分片算法(RangeShardingAlgorithm)用於單個字段作為分片鍵,SQL中有 BETWEEN AND、>、<、>=、<= 等條件的分片,需要需要配合 StandardShardingStrategy 使⽤。

  • 復合分片算法 ComplexKeysShardingAlgorithm

對應 ComplexKeysShardingAlgorithm,⽤於處理使⽤ 多鍵作為分⽚鍵 進⾏分⽚的場景,

(多個字段作為分片鍵),同時獲取到多個分片健的值,根據多個字段處理業務邏輯。

包含多個分⽚鍵的邏輯較復雜,需要應⽤開發者⾃⾏處理其中的復雜度。

需要配合 ComplexShardingStrategy 使⽤。

需要在復合分片策略(ComplexShardingStrategy )下使用。

  • Hint 分片算法 HintShardingAlgorithm

Hint 分片算法(HintShardingAlgorithm)稍有不同

前面的算法(如StandardShardingAlgorithm)都是解析 SQL 語句, 提取分片值,並根據設置的分片算法進行分片。

Hint 分片算法 指定分⽚值而⾮從 SQL 中提取,而是手工設置的⽅式,進⾏分⽚的策略。

對於分⽚值⾮ SQL 決定,不是來自於分片建,甚至連分片建都沒有 ,而由其他外置條件決定的場景,可使⽤Hint 分片算法 。

就需要通過 Java API 等方式 指定 分片值,這也叫強制路由、或者說 暗示路由

例: 內部系統,按照員⼯登錄主鍵分庫,而數據庫中並⽆此字段。

四大分片值

SQL Hint ⽀持通過 Java API 和 SQL 注釋(待實現)兩種⽅式使⽤。

ShardingSphere-JDBC針對每種分片算法都提供了相應的ShardingValue,具體包括:

  • PreciseShardingValue
  • RangeShardingValue
  • ComplexKeysShardingValue
  • HintShardingValue

范圍分片算法

用在區間查詢/范圍查詢的時候,比如下面的查詢SQL:

select * from  t_user where user_id between 10 and 20

以上兩個區間值10、20會直接保存到RangeShardingValue中,做庫路由時,所以會訪問兩個庫;

參考的代碼如下(以下代碼,視頻中有詳細介紹):

public final class RangeOrderShardingAlgorithm implements RangeShardingAlgorithm<Integer> {

    @Override
    public Collection<String> doSharding(final Collection<String> availableTargetNames, final RangeShardingValue<Integer> shardingValue) {
        Collection<String> result = new HashSet<>(2);
        for (int i = shardingValue.getValueRange().lowerEndpoint(); i <= shardingValue.getValueRange().upperEndpoint(); i++) {

            for (String each : availableTargetNames) {
                System.out.println("shardingValue = " + shardingValue.getValueRange() + " target = " + each + "  shardingValue.getValue() % 2) = " + i % 2);
                if (each.endsWith(String.valueOf(i % 2))) {
                    result.add(each);
                }
            }
        }
        return result;
    }
}

測試用例:

  @Test
    public void testSelectUserBetween() throws SQLException {

        /*
         * 1. 需要到DataSource
         * 2. 通過DataSource獲取Connection
         * 3. 定義一條SQL語句.
         * 4. 通過Connection獲取到PreparedStament.
         *  5. 執行SQL語句.
         *  6. 關閉連接.
         */


        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "select * from  t_user where user_id between 10 and 20 ";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        ResultSet resultSet = preparedStatement.executeQuery();


        // * 6. 關閉連接.
        preparedStatement.close();
        connection.close();
    }

實戰: JavaAPI使用RangeShardingAlgorithm實戰2

異常:range unbounded on this side

用上面的算法,執行下面的測試用例,會拋出 異常:range unbounded on this side

可以執行下面的用例,看看異常的效果


    @Test
    public void testSelectUserBigThan() throws SQLException {

        /*
         * 1. 需要到DataSource
         * 2. 通過DataSource獲取Connection
         * 3. 定義一條SQL語句.
         * 4. 通過Connection獲取到PreparedStament.
         *  5. 執行SQL語句.
         *  6. 關閉連接.
         */


        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "select * from  t_user where user_id > 10000";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        ResultSet resultSet = preparedStatement.executeQuery();


        // * 6. 關閉連接.
        preparedStatement.close();
        connection.close();
    }

異常的原因

以上兩個區間值是沒有邊界的,執行獲取上邊界時,RangeShardingValue會拋出異常

既然沒有邊界,直接做全路由

對沒有邊界的范圍分片進行路由

用在區間查詢/范圍查詢的時候,比如下面的查詢SQL:

select * from  t_user where user_id > 10000

參考的代碼如下(以下代碼,視頻中有詳細介紹):

public final class RouteInfinityRangeShardingAlgorithm implements RangeShardingAlgorithm<Integer> {

    @Override
    public Collection<String> doSharding(final Collection<String> availableTargetNames, final RangeShardingValue<Integer> shardingValue) {

        Collection<String> result = new HashSet<>();

        result.addAll(availableTargetNames);

        return result;
    }
}

Properties配置StandardShardingStrategy 實戰

通過 Properties 配置來使用 StandardShardingStrategy

配置參數:

  • standard.sharding-column 分片鍵;

  • standard.precise-algorithm-class-name 精確分片算法類名;

  • standard.range-algorithm-class-name 范圍分片算法類名

參數standard.precise-algorithm-class-name 說明:

standard.precise-algorithm-class-name 指向一個實現了PreciseShardingAlgorithm接口的java實現類,

 io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm

此java實現類提供按照 = 或者 IN 邏輯的精確分片

參數standard.range-algorithm-class-name 說明:

指向一個實現了 io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm接口的java類名,

此java實現類提供按照 Between 條件進行的范圍分片。

示例: com.crazymaker.springcloud.message.core.PreciseShardingAlgorithm

參數補充說明:

StandardShardingStrategy的兩大內嵌算法中:精確分片算法是必須提供的,而范圍分片算法則是可選的。

配置實例


spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..1}
#spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=user_id
#spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{user_id % 2}
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.crazymaker.springcloud.sharding.jdbc.demo.core.TablePreciseShardingAlgorithmDemo
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.crazymaker.springcloud.sharding.jdbc.demo.core.DsPreciseShardingAlgorithmDemo
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id
#spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
#spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2}

就寫這么多,更加詳細的內容,請參見視頻

ComplexShardingStrategy復合分片策略實戰

內聯分片、標准分片 策略的不足:

只有一個分片建

問題: 多個分片鍵參與分片路由,咋整?

ComplexSharding分片策略

分片策略基本和上面的分片算法對應,包括:標准分片策略、復合分片策略、Hint分片策略、內聯分片策略、不分片策略;

  • 標准分片策略:對應StandardShardingStrategy類,提供PreciseShardingAlgorithmRangeShardingAlgorithm兩個分片算法,PreciseShardingAlgorithm是必須的,RangeShardingAlgorithm可選的;

    public final class StandardShardingStrategy implements ShardingStrategy {
        private final String shardingColumn;
        private final PreciseShardingAlgorithm preciseShardingAlgorithm;
        private final RangeShardingAlgorithm rangeShardingAlgorithm;
    }
    
    
  • 復合分片策略:對應ComplexShardingStrategy類,提供ComplexKeysShardingAlgorithm分片算法;

    public final class ComplexShardingStrategy implements ShardingStrategy {
        @Getter
        private final Collection<String> shardingColumns;
        private final ComplexKeysShardingAlgorithm shardingAlgorithm;
    }
    
    

    可以發現支持多個分片鍵;

  • Hint分片策略:對應HintShardingStrategy類,通過 Hint 指定分片值而非從 SQL 中提取分片值的方式進行分片的策略;提供HintShardingAlgorithm分片算法;

    public final class HintShardingStrategy implements ShardingStrategy {
        @Getter
        private final Collection<String> shardingColumns;
        private final HintShardingAlgorithm shardingAlgorithm;
    }
    
    
  • 內聯分片策略:對應InlineShardingStrategy類,沒有提供分片算法,路由規則通過表達式來實現;

  • 不分片策略:對應NoneShardingStrategy類,不分片策略;

ComplexSharding分片策略配置類

在使用中我們並沒有直接使用上面的分片策略類,ShardingSphere-JDBC分別提供了對應策略的配置類包括:

  • StandardShardingStrategyConfiguration
  • ComplexShardingStrategyConfiguration
  • HintShardingStrategyConfiguration
  • InlineShardingStrategyConfiguration
  • NoneShardingStrategyConfiguration
/**
 * Complex sharding strategy configuration.
 */
@Getter
public final class ComplexShardingStrategyConfiguration implements ShardingStrategyConfiguration {
    
    private final String shardingColumns;
    
    private final ComplexKeysShardingAlgorithm shardingAlgorithm;
    
    public ComplexShardingStrategyConfiguration(
    
    final String shardingColumns, 
    final ComplexKeysShardingAlgorithm shardingAlgorithm) {
    
        Preconditions.checkArgument(!Strings.isNullOrEmpty(shardingColumns), "ShardingColumns is required.");
        Preconditions.checkNotNull(shardingAlgorithm, "ShardingAlgorithm is required.");
        this.shardingColumns = shardingColumns;
        this.shardingAlgorithm = shardingAlgorithm;
    }
}

ComplexSharding分片算法

提供了抽象分片算法類:ShardingAlgorithm,根據類型又分為:精確分片算法、區間分片算法、復合分片算法以及Hint分片算法;

  • 精確分片算法:對應PreciseShardingAlgorithm類,主要用於處理 =IN的分片;
  • 區間分片算法:對應RangeShardingAlgorithm類,主要用於處理 BETWEEN AND, >, <, >=, <= 分片;
  • 復合分片算法:對應ComplexKeysShardingAlgorithm類,用於處理使用多鍵作為分片鍵進行分片的場景;
  • Hint分片算法:對應HintShardingAlgorithm類,用於處理使用 Hint 行分片的場景;

以上所有的算法類都是接口類,具體實現交給開發者自己;

自定義ComplexSharding分片算法

問題: 多個分片鍵參與分片路由,咋整?

user_id,和oder_id 參與分片

分片算法如下:


public class SimpleComplexKeySharding implements ComplexKeysShardingAlgorithm<Long> {

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                         ComplexKeysShardingValue<Long> shardingValue) {
        Map<String, Collection<Long>> map = shardingValue.getColumnNameAndShardingValuesMap();

        Collection<Long> userIds = map.get("user_id");
        Collection<Long> orderIds = map.get("order_id");

        List<String> result = new ArrayList<>();
        // user_id,order_id分片鍵進行分表
        for (Long userId : userIds) {
            for (Long orderId : orderIds) {

                Long innerShardingValue = userId + orderId;
                Long suffix = innerShardingValue % 2;


                for (String each : availableTargetNames) {
                    System.out.println("innerShardingValue = " + innerShardingValue + " target = " + each + " innerShardingValue % 2 = " + suffix);
                    if (each.endsWith(suffix + "")) {
                        result.add(each);
                    }
                }
            }
        }
        return result;
    }
}

通過代碼使用ComplexSharding復合分片算法

可以同時使用多個分片鍵,比如可以同時使用user_id和order_id作為分片鍵;

orderTableRuleConfig.setDatabaseShardingStrategyConfig(
		new ComplexShardingStrategyConfiguration("order_id,user_id", new SimpleComplexKeySharding()));
orderTableRuleConfig.setTableShardingStrategyConfig(
		new ComplexShardingStrategyConfiguration("order_id,user_id", new SimpleComplexKeySharding()));

如上在配置分庫分表策略時,指定了兩個分片鍵,用逗號隔開;

使用屬性進行配置

支持多分片鍵的復雜分片策略。

配置參數:

complex.sharding-columns 分片鍵(多個);

complex.algorithm-class-name 分片算法實現類。

配置參數:

shardingColumn指定多個分片列。

algorithmClassName指向一個實現了org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm接口的java類名。提供按照多個分片列進行綜合分片的算法。

具體的介紹,請參見視頻

測試用例與執行

參見視頻

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

HintShardingStrategy強制(暗示)分片策略實戰

問題: 在一些應用場景中,分片值並不存在於 SQL,而存在於外部業務邏輯,咋整?

問題2:根據外部值分片,咋整?

eg:

我要根據 月份分片,或者根據 小時分片

我要根據 心情 分片

簡單來理解

這個分片策略,簡單來理解就是說,他的分片鍵不再跟SQL語句相關聯,而是用程序另行指定。

對於一些復雜的情況,例如select count(*) from (select userid from t_user where userid in (1,3,5,7,9)) 這樣的SQL語句,就沒法通過SQL語句來指定一個分片鍵。

暗示策略與前面的策略之不同:

  • 前面的策略提取分片鍵列與值並進行分片是 Apache ShardingSphere 對 SQL 零侵入的實現方式。

若 SQL 語句中沒有分片條件,則無法進行分片,需要全路由。

在一些應用場景中,分片條件並不存在於 SQL,而存在於外部業務邏輯。

  • 暗示策略需要提供一種通過外部指定分片值的方式,在 Apache ShardingSphere 中叫做 Hint。

暗示分片值算法如下:

可以通過編程的方式向 HintManager 中添加分片值,該分片值僅在當前線程內生效;然后通過 hint暗示策略+hint暗示算法分片

分片策略算法

ShardingSphere-JDBC在分片策略上分別引入了分片算法分片策略兩個概念,

當然在分片的過程中分片鍵也是一個核心的概念;在此可以簡單的理解分片策略 = 分片算法 + 分片鍵

至於為什么要這么設計,應該是ShardingSphere-JDBC考慮更多的靈活性,把分片算法單獨抽象出來,方便開發者擴展;

分片算法

提供了抽象分片算法類:ShardingAlgorithm,根據類型又分為:精確分片算法、區間分片算法、復合分片算法以及Hint分片算法;

  • 精確分片算法:對應PreciseShardingAlgorithm類,主要用於處理 =IN的分片;
  • 區間分片算法:對應RangeShardingAlgorithm類,主要用於處理 BETWEEN AND, >, <, >=, <= 分片;
  • 復合分片算法:對應ComplexKeysShardingAlgorithm類,用於處理使用多鍵作為分片鍵進行分片的場景;
  • Hint分片算法:對應HintShardingAlgorithm類,用於處理使用外部值分片的場景;

以上所有的算法類都是接口類,具體實現交給開發者自己;

分片策略

分片策略基本和上面的分片算法對應,包括:標准分片策略、復合分片策略、Hint分片策略、內聯分片策略、不分片策略;

  • Hint分片策略:對應HintShardingStrategy類,通過 Hint 指定分片值而非從 SQL 中提取分片值的方式進行分片的策略;提供HintShardingAlgorithm分片算法;

    public final class HintShardingStrategy implements ShardingStrategy {
        @Getter
        private final Collection<String> shardingColumns;
        private final HintShardingAlgorithm shardingAlgorithm;
    }
    
    
  • 內聯分片策略:對應InlineShardingStrategy類,沒有提供分片算法,路由規則通過表達式來實現;

  • 不分片策略:對應NoneShardingStrategy類,不分片策略;

分片策略配置類

在使用中我們並沒有直接使用上面的分片策略類,ShardingSphere-JDBC分別提供了對應策略的配置類包括:

  • StandardShardingStrategyConfiguration
  • ComplexShardingStrategyConfiguration
  • HintShardingStrategyConfiguration 外部值分片
  • InlineShardingStrategyConfiguration
  • NoneShardingStrategyConfiguration

自定義HintShardingAlgorithm分片算法

問題:根據外部值分片,咋整?

我要根據 月份分片,或者根據 小時分片

我要根據 心情 分片

分片算法如下:


public class SimpleHintShardingAlgorithmDemo implements HintShardingAlgorithm<Integer> {

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                         HintShardingValue<Integer> hintShardingValue) {

        Collection<String> result = new HashSet<>(2);
        Collection<Integer> values = hintShardingValue.getValues();


        for (String each : availableTargetNames) {

            for (int shardingValue : values) {


                System.out.println("shardingValue = " + shardingValue + " target = " + each + " shardingValue % 2 = " + shardingValue % 2);
                if (each.endsWith(String.valueOf(shardingValue % 2))) {
                    result.add(each);
                }

            }
        }
        return result;
    }

	
}

使用代碼進行配置

// 設置庫表分片策略
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new HintShardingStrategyConfiguration(new 		SimpleHintShardingAlgorithmDemo()));
orderTableRuleConfig.setTableShardingStrategyConfig(new HintShardingStrategyConfiguration(new SimpleHintShardingAlgorithmDemo()));

使用屬性進行配置

  • 配置參數:hint.algorithm-class-name 分片算法實現類。

  • 實現方式:

    algorithmClassName指向一個實現了org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm接口的java類名。 示例:com.roy.shardingDemo.algorithm.MyHintShardingAlgorithm

    在這個算法類中,同樣是需要分片鍵的。而分片鍵的指定是通過HintManager.addDatabaseShardingValue方法(分庫)和HintManager.addTableShardingValue(分表)來指定。

    使用時要注意,這個分片鍵是線程隔離的,只在當前線程有效,所以通常建議使用之后立即關閉,或者用try資源方式打開。

在代碼使用HintManager進行暗示

在一些應用場景中,分片條件並不存在於 SQL,而存在於外部業務邏輯;

問題:根據外部值分片,咋整?

我要根據 月份分片,或者根據 小時分片

我要根據 心情 分片

可以通過編程的方式向 HintManager 中添加分片值,該分片值僅在當前線程內生效;


    @Test
    public void testAddSomeOrderByMonth() {

        for (int month = 1; month <= 12; month++) {
            final int index = month;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("當前月份 = " + index);
                    HintManager hintManager = HintManager.getInstance();
                    hintManager.addTableShardingValue("t_order", index);
                    hintManager.addDatabaseShardingValue("t_order", index);

                    Order dto = new Order();
                    dto.setUserId(704733680467685377L);

                    //增加訂單
                    entityService.addOrder(dto);


                }
            }).start();
        }
    }

測試用例與執行

參見視頻

Hint實現機制

Apache ShardingSphere 使用 ThreadLocal 管理分片鍵值。 可以通過編程的方式向 HintManager 中添加分片條件,該分片條件僅在當前線程內生效。

除了通過編程的方式使用強制分片路由,Apache ShardingSphere 還可以通過 SQL 中的特殊注釋的方式引用 Hint,使開發者可以采用更加透明的方式使用該功能。

指定了強制分片路由的 SQL 將會無視原有的分片邏輯,直接路由至指定的真實數據節點。

切記:

涉及到ThreadLocal 線程局部變量的,執行完后用完記得清理哦。免得污染后面的執行,尤其在線程池的場景中。

Session的使用,也是類似的。

Hint分片策略的優勢和劣勢

場景優勢:

可以程序指定分片值

性能優勢:

Hint分片策略並沒有完全按照SQL解析樹來構建分片策略,是繞開了SQL解析的,

所有對某些比較復雜的語句,Hint分片策略性能有可能會比較好,僅僅是可能,還需要是分析源碼

使用限制

Hint路由在使用時有非常多的限制:

-- 不支持UNION
SELECT * FROM t_order1 UNION SELECT * FROM t_order2
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ?

-- 不支持多層子查詢
SELECT COUNT(*) FROM (SELECT * FROM t_order o WHERE o.id IN (SELECT id FROM t_order WHERE status = ?))

-- 不支持函數計算。ShardingSphere只能通過SQL字面提取用於分片的值
SELECT * FROM t_order WHERE to_date(create_time, 'yyyy-mm-dd') = '2019-01-01';

從這里也能看出,即便有了ShardingSphere框架,分庫分表后對於SQL語句的支持依然是非常脆弱的。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

NoneShardingStrategyConfiguration不分片策略實戰

不分片,怎么配置呢

分片策略

分片策略基本和上面的分片算法對應,包括:標准分片策略、復合分片策略、Hint分片策略、內聯分片策略、不分片策略;

  • 不分片策略:對應NoneShardingStrategy類,不分片策略;

分片策略配置類

在使用中我們並沒有直接使用上面的分片策略類,ShardingSphere-JDBC分別提供了對應策略的配置類包括:

  • NoneShardingStrategyConfiguration

使用代碼進行配置

配置NoneShardingStrategyConfiguration即可:

orderTableRuleConfig.setDatabaseShardingStrategyConfig(new NoneShardingStrategyConfiguration());
orderTableRuleConfig.setTableShardingStrategyConfig(new NoneShardingStrategyConfiguration());

使用屬性進行配置

參見視頻

這樣數據會插入每個庫每張表,可以理解為廣播表

實戰:廣播表原理與實操

什么是廣播表:

存在於所有的數據源中的表,表結構和表中的數據在每個數據庫中均完全一致。

一般是為字典表或者配置表 t_config,

某個表一旦被配置為廣播表,只要修改某個數據庫的廣播表,所有數據源中廣播表的數據都會跟着同步。

存在這樣的情況:表結構和表中的數據在每個數據庫中完全一致,如字典表,那么這時候應該怎么辦?廣播表這時候就應運而生了。

定義:指所有的分片數據源中都存在的表,表結構和表中的數據在每個數據庫中完全一致。
適用:數據量不大且需要與海量數據的表進行關聯查詢的場景,例如:字典表。

廣播表需要滿足如下:
(1)在每個數據庫表都存在該表以及表結構都一樣。
(2)當保存的時候,每個數據庫都會插入相同的數據。

使用代碼進行配置

配置NoneShardingStrategyConfiguration即可:


        //廣播表配置如下;
//        shardingRuleConfig.getBroadcastTables().add("t_config");


使用屬性進行配置

spring.shardingsphere.sharding.broadcast-tables=t_config

具體的演示,請參見視頻

廣播表的效果

運行結果如下:

添加記錄時,在ds0和ds1都會保存1條相同的數據。
當查詢的時候,會隨機的選擇一個數據源進行查詢。

添加


[main] INFO  ShardingSphere-SQL - Logic SQL: insert into t_config (status, id) values (?, ?)
[main] INFO  ShardingSphere-SQL - SQLStatement: InsertStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement@61be6051, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@13c18bba), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@13c18bba, columnNames=[status, id], insertValueContexts=[InsertValueContext(parametersCount=2, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=42, stopIndex=42, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=45, stopIndex=45, parameterMarkerIndex=1)], parameters=[UN_KNOWN, 1])], generatedKeyContext=Optional.empty)
[main] INFO  ShardingSphere-SQL - Actual SQL: ds0 ::: insert into t_config (status, id) values (?, ?) ::: [UN_KNOWN, 1]
[main] INFO  ShardingSphere-SQL - Actual SQL: ds1 ::: insert into t_config (status, id) values (?, ?) ::: [UN_KNOWN, 1]

查詢

[main] INFO  o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory
[main] INFO  ShardingSphere-SQL - Logic SQL: select configenti0_.id as id1_0_, configenti0_.status as status2_0_ from t_config configenti0_ limit ?
[main] INFO  ShardingSphere-SQL - SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@784212, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@5ac646b3), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@5ac646b3, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=66, distinctRow=false, projections=[ColumnProjection(owner=configenti0_, name=id, alias=Optional[id1_0_]), ColumnProjection(owner=configenti0_, name=status, alias=Optional[status2_0_])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@24b38e8f, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@5cf072ea, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@1edac3b4, containsSubquery=false)
[main] INFO  ShardingSphere-SQL - Actual SQL: ds1 ::: select configenti0_.id as id1_0_, configenti0_.status as status2_0_ from t_config configenti0_ limit ? ::: [3]
[ConfigBean(id=1, status=UN_KNOWN), ConfigBean(id=704836248892059648, status=UN_KNOWN0), ConfigBean(id=704836250150350849, status=UN_KNOWN1)]


實戰:綁定表

綁定表:那些分片規則一致的主表和子表。

比如:t_order 訂單表和 t_order_item 訂單服務項目表,都是按 order_id 字段分片,因此兩張表互為綁定表關系。

那綁定表存在的意義是啥呢?

通常在我們的業務中都會使用 t_order 和 t_order_item 等表進行多表聯合查詢,但由於分庫分表以后這些表被拆分成N多個子表。

如果不配置綁定表關系,會出現笛卡爾積關聯查詢,將產生如下四條 SQL。

沒有綁定表的效果


[main] INFO  ShardingSphere-SQL - Logic SQL: SELECT a.* FROM `t_order` a left join `t_user` b on a.user_id=b.user_id  where  a.user_id=?
....
[main] INFO  ShardingSphere-SQL - Actual SQL: ds1 ::: SELECT a.* FROM `t_order_1` a left join `t_user_1` b on a.user_id=b.user_id  where  a.user_id=? ::: [704733680467685377]
[main] INFO  ShardingSphere-SQL - Actual SQL: ds1 ::: SELECT a.* FROM `t_order_1` a left join `t_user_0` b on a.user_id=b.user_id  where  a.user_id=? ::: [704733680467685377]
[order_id: 704786564605521921, user_id: 704733680467685377, status: NotPayed, order_id: 704786564697796609, ....]

有綁定表的效果

[main] INFO  ShardingSphere-SQL - Logic SQL: SELECT a.* FROM `t_order` a left join `t_user` b on a.user_id=b.user_id  where  a.user_id=?
[main] INFO  ShardingSphere-SQL - SQLStatement: SelectStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement@4247093b, tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7074da1d), tablesContext=org.apache.shardingsphere.sql.parser.binder.segment.table.TablesContext@7074da1d, projectionsContext=ProjectionsContext(startIndex=7, stopIndex=9, distinctRow=false, projections=[ShorthandProjection(owner=Optional[a], actualColumns=[ColumnProjection(owner=a, name=order_id, alias=Optional.empty), ColumnProjection(owner=a, name=user_id, alias=Optional.empty), ColumnProjection(owner=a, name=status, alias=Optional.empty)])]), groupByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.groupby.GroupByContext@5bdb6ea8, orderByContext=org.apache.shardingsphere.sql.parser.binder.segment.select.orderby.OrderByContext@3e55eeb9, paginationContext=org.apache.shardingsphere.sql.parser.binder.segment.select.pagination.PaginationContext@44a13699, containsSubquery=false)
[main] INFO  ShardingSphere-SQL - Actual SQL: ds1 ::: SELECT a.* FROM `t_order_1` a left join `t_user_1` b on a.user_id=b.user_id  where  a.user_id=? ::: [704733680467685377]
[order_id: 704786564605521921, user_id: 704733680467685377, status: NotPayed, order_id: 704786564697796609, user_id: 704733680467685377, status: NotPayed, order_id: 704786564790071297, user_id: 704733680467685377, .....]

shardingjdbc 的sql執行流程

shardingjdbc 對原有的 DataSourceConnection 等接口擴展成 ShardingDataSourceShardingConnection

而對外暴露的分片操作接口與 JDBC 規范中所提供的接口完全一致,只要你熟悉 JDBC 就可以輕松應用 Sharding-JDBC 來實現分庫分表。

img

一張表經過分庫分表后被拆分成多個子表,並分散到不同的數據庫中,

在不修改原業務 SQL 的前提下,Sharding-JDBC 就必須對 SQL進行一些改造才能正常執行。

大致的執行流程:SQL 解析 -> 查詢優化 -> SQL 路由 -> SQL 改寫 -> SQL 執⾏ -> 結果歸並 六步組成,一起瞅瞅每個步驟做了點什么。

img

SQL解析

接着語法解析會將拆分后的SQL轉換為抽象語法樹,通過對抽象語法樹遍歷,提煉出分片所需的上下文,

上下文包含查詢字段信息(Field)、表信息(Table)、查詢條件(Condition)、排序信息(Order By)、分組信息(Group By)以及分頁信息(Limit)等,並標記出 SQL中有可能需要改寫的位置。

例如,以下 SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18

SQL 解析引擎

相對於其他編程語⾔,SQL 是⽐較簡單的。不過,它依然是⼀⻔完善的編程語⾔,因此對 SQL 的語法進⾏解析,與解析其他編程語⾔(如:Java 語⾔、C 語⾔、Go 語⾔等)並⽆本質區別。

功能點

• 提供獨立的 SQL 解析功能

• 可以非常方便的對語法規則進行擴充和修改 (使用了 ANTLR)

• 支持多種方言的 SQL 解析

數據庫 支持狀態
MySQL 支持,完善
PostgreSQL 支持,完善
SQLServer 支持
Oracle 支持
SQL92 支持
  • 歷史

SQL 解析作為分庫分表類產品的核心,其性能和兼容性是最重要的衡量指標。ShardingSphere 的 SQL 解 析器經歷了 3 代產品的更新迭代。

第一代 SQL 解析器為了追求性能與快速實現,在 1.4.x 之前的版本使用 Druid 作為 SQL 解析器。經實際 測試,它的性能遠超其它解析器。

第二代 SQL 解析器從 1.5.x 版本開始,ShardingSphere 采用完全自研的 SQL 解析引擎。由於目的不同, ShardingSphere 並不需要將 SQL 轉為一顆完全的抽象語法樹,也無需通過訪問器模式進行二次遍歷。它 采用對 SQL 半理解的方式,僅提煉數據分片需要關注的上下文,因此 SQL 解析的性能和兼容性得到了進 一步的提高。

第三代 SQL 解析器從 3.0.x 版本開始,嘗試使用 ANTLR 作為 SQL 解析引擎的生成器,並采用 Visit 的方 式從 AST 中獲取 SQL Statement。從 5.0.x 版本開始,解析引擎的架構已完成重構調整,同時通過將第一 次解析的得到的 AST 放入緩存,方便下次直接獲取相同 SQL 的解析結果,來提高解析效率。

因此官方建 議用戶采用 PreparedStatement 這種 SQL 預編譯的方式來提升性能。

抽象語法樹

解析過程分為詞法解析和語法解析。詞法解析器⽤於將 SQL 拆解為不可再分的原⼦符號,稱為 Token。並根據不同數據庫⽅⾔所提供的字典,將其歸類為關鍵字,表達式,字⾯量和操作符。再使⽤語法解析器將 SQL 轉換為抽象語法樹。

SQL 改寫

將基於邏輯表開發的SQL改寫成可以在真實數據庫中可以正確執行的語句。比如查詢 t_order 訂單表,我們實際開發中 SQL是按邏輯表 t_order 寫的。

SELECT * FROM t_order

但分庫分表以后真實數據庫中 t_order 表就不存在了,而是被拆分成多個子表 t_order_n 分散在不同的數據庫內,還按原SQL執行顯然是行不通的,這時需要將分表配置中的邏輯表名稱改寫為路由之后所獲取的真實表名稱。

SELECT * FROM t_order_n

SQL執行

將路由和改寫后的真實 SQL 安全且高效發送到底層數據源執行。但這個過程並不是簡單的將 SQL 通過JDBC 直接發送至數據源執行,而是平衡數據源連接創建以及內存占用所產生的消耗,它會自動化的平衡資源控制與執行效率。

結果歸並

將從各個數據節點獲取的多數據結果集,合並成一個大的結果集並正確的返回至請求客戶端,稱為結果歸並。

而我們SQL中的排序、分組、分頁和聚合等語法,均是在歸並后的結果集上進行操作的。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

shardingjdbc 的SQL 路由原理

SQL 路由通過解析分片上下文,匹配到用戶配置的分片策略,並生成路由路徑。

簡單點理解就是可以根據我們配置的分片策略計算出 SQL該在哪個庫的哪個表中執行,

而SQL路由又根據有無分片健區分出 分片路由廣播路由

img

有分⽚鍵的路由叫分片路由,細分為直接路由、標准路由和笛卡爾積路由這3種類型。

DQL、DML、DDL、DCL、TCL、DAL

一、DDL (Data Definition Language) 數據庫定義語言

  用於創建、改變、刪除對象的SQL語句統稱:DDL。

    1. Create

    create命令用於創建對象如:表、索引、存儲過程、觸發器、函數等。  

create table tblEmployee(
    Id int primary key identity(1,1) not null,
    Name nvarchar(50) ,
    Gender nvarchar(50) ,
    Salary int ,
    DepartmentId int ,
);

    2. Alter

    Alter命令用於創建數據庫和對象。

    3. Drop

    Drop命令用於從數據庫中刪除對象。

    4. Truncate

    Truncate表命令用戶移除表中所有的記錄,包括所分配的空間(不可恢復)

    5. Rename

    Rename用於重命名對象

    6. Comment

    // -> 單行 Comments, /* --多行 Comments-- */ 用戶注釋SQL

二、DML(Data Manipulation Language) 數據庫操作語言

   用於操作數據庫(insert、modify、delete)的SQL命令,統稱:DML

    1. Insert

    2. Modify、update

    3. Delete

三、DQL (Data Query Language) 數據庫查詢語言

  用於從數據庫檢索數據的SQL命令,統稱:DQL, 所以,所有的select語句都屬於DQL

​ select

四、DCL(Data Control Language) 數據庫控制語言

 用於在數據庫訪問中控制訪問限制的SQL命令統稱:DCL

  1. Grant

  授權

  2. Revoke

  取消授權

五、TCL(Transaction Control Language) 事務控制語言

   用於控制數據庫沖突的SQL 統稱為TCL。 如:

  1. Commit

  提交事務,並使已對數據庫進行的所有修改稱為永久性。

  2. Rollback

  回滾用戶的事務,並撤銷正在進行的所有未提交的事務。

  3. Save Point

  保存回滾點。

  4. Set Transaction

  INNODB存儲引擎提供的事務隔離級別READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLLE.

六 dal

DESCRIBE

直接路由(暗示路由)

直接路由是通過使用 HintAPI 直接將 SQL路由到指定⾄庫表的一種分⽚方式,而且直接路由可以⽤於分⽚鍵不在SQL中的場景,還可以執⾏包括⼦查詢、⾃定義函數等復雜情況的任意SQL。

比如根據 t_order_id 字段為條件查詢訂單,此時希望在不修改SQL的前提下,加上 user_id作為分片條件就可以使用直接路由。

直接路由需要通過 Hint(使用 HintAPI 直接指定路由至庫表)方式指定分片值,

不需要提取分片鍵值,並且 是只分庫不分表的前提下,則可以避免 SQL 解析。

因此它的兼容性最好,可以執行包 括子查詢、自定義函數等復雜情況的任意 SQL。直接路由還可以用於分片鍵不在 SQL 中的場景。例如,設 置用於數據庫分片的值為 3

hintManager.setDatabaseShardingValue(3);

假如路由算法為 value % 2,當一個邏輯庫 t_order 對應 2 個真實庫 t_order_0 和 t_order_1 時, 路由后 SQL 將在 t_order_1 上執行。

標准路由

標准路由是最推薦也是最為常⽤的分⽚⽅式,它的適⽤范圍是不包含關聯查詢或僅包含綁定表之間關聯查詢的SQL。

  • 當 SQL分片健的運算符為 = 時,路由結果將落⼊單庫(表),路由策略返回的是單個的目標。

  • 當分⽚運算符是BETWEENIN 等范圍時,路由結果則不⼀定落⼊唯⼀的庫(表),因此⼀條邏輯SQL最終可能被拆分為多條⽤於執⾏的真實SQL。

如果按照 order_id 的奇數和偶數進行數據分片,一個單表查詢的 SQL 如下:

SELECT * FROM t_order  where t_order_id in (1,2)

SQL路由處理后

SELECT * FROM t_order_0  where t_order_id in (1,2)
SELECT * FROM t_order_1  where t_order_id in (1,2)

綁定表的關聯查詢與單表查詢復雜度和性能相當。

舉例說明,如果一個包含綁定表的關聯查詢的 SQL 如 下:

 SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_ id IN (1, 2);

那么路由的結果應為:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

可以看到,SQL 拆分的數目與單表是一致的。

笛卡爾積路由

笛卡爾路由是由非綁定表之間的關聯查詢產生的,查詢性能較低盡量避免走此路由模式。

笛卡爾路由是最復雜的情況,它無法根據綁定表的關系定位分片規則,因此非綁定表之間的關聯查詢需 要拆解為笛卡爾積組合執行。

如果上個示例中的 SQL 並未配置綁定表關系,那么路由的結果應為:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

笛卡爾路由查詢性能較低,需謹慎使用。

廣播路由

無分⽚鍵的路由又叫做廣播路由,可以划分為全庫表路由、全庫路由、 全實例路由、單播路由和阻斷路由這 5種類型。

全庫路由

全庫路由⽤於處理對數據庫的操作,包括⽤於庫設置的 SET 類型的數據庫管理命令,以及 TCL 這樣的事務控制語句。

在這種情況下,會根據邏輯庫的名字遍歷所有符合名字匹配的真實庫,並在真實庫中執⾏該命令,例如:

SET autocommit=0;

在 t_order 中執⾏,t_order 有 2 個真實庫。

則實際會在 t_order_0 和 t_order_1 上都執⾏這個命令。

全庫表路由

全庫表路由⽤於處理對數據庫中與其邏輯表相關的所有真實表的操作,主要包括不帶分⽚鍵的 DQL 和DML,以及 DDL 等。例如:

SELECT * FROM t_order WHERE good_prority IN (1, 10);

則會遍歷所有數據庫中的所有表,逐⼀匹配邏輯表和真實表名,能夠匹配得上則執⾏。路由后成為

SELECT * FROM t_order_0 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_1 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_2 WHERE good_prority IN (1, 10);
SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);

全實例路由

全實例路由是針對數據庫實例的 DCL 操作(設置或更改數據庫用戶或角色權限),

比如:創建一個用戶 order ,這個命令將在所有的真實庫實例中執行,以此確保 order 用戶可以正常訪問每一個數據庫實例。

CREATE USER order@127.0.0.1 identified BY '程序員內點事';

⽆論⼀個實例中包含多少個 Schema,每個數據庫的實例只執⾏⼀次。例如:

CREATE USER customer@127.0.0.1 identified BY '123';

這個命令將在所有的真實數據庫實例中執⾏,以確保 customer ⽤⼾可以訪問每⼀個實例。

單播路由

單播路由用來獲取某一真實表信息,比如獲得表的描述信息:

DESCRIBE t_order; 

t_order 的真實表是 t_order_0 ···· t_order_n,他們的描述結構相完全同,我們只需在任意的真實表執行一次就可以。

阻斷路由

⽤來屏蔽SQL對數據庫的操作,例如:

USE order_db;

這個命令不會在真實數據庫中執⾏,因為 ShardingSphere 采⽤的是邏輯 Schema(數據庫的組織和結構) ⽅式,所以無需將切換數據庫的命令發送⾄真實數據庫中。

問題:分庫分表時,分片鍵怎么選擇

在這里插入圖片描述

這個是小伙伴剛提出來的內容

我從減少傾斜和提高性能的角度,用3個案例,說明了一下,具體請參見視頻。

問題:分庫的join怎么解決

在這里插入圖片描述

首先看是那種 join。

JOIN的含大致分為左連接,右連接,內連接,外連接,自然連接。

img

笛卡爾積

JOIN首先要理解笛卡爾積。

笛卡爾積就是將A表的每一條記錄與B表的每一條記錄強行拼在一起。所以,如果A表有n條記錄,B表有m條記錄,笛卡爾積產生的結果就會產生n*m條記錄。

內連接:INNER JOIN

內連接INNER JOIN是最常用的連接操作。從數學的角度講就是求兩個表的交集,從笛卡爾積的角度講就是從笛卡爾積中挑出ON子句條件成立的記錄。

左連接:LEFT JOIN

左連接LEFT JOIN的含義就是求兩個表的交集外加左表剩下的數據。

從笛卡爾積的角度講,就是先從笛卡爾積中挑出ON子句條件成立的記錄,然后加上左表中剩余的記錄

右連接:RIGHT JOIN

同理右連接RIGHT JOIN就是求兩個表的交集外加右表剩下的數據。

從笛卡爾積的角度描述,右連接就是從笛卡爾積中挑出ON子句條件成立的記錄,然后加上右表中剩余的記錄

常用的是左外連接

   /**
     * 根據用戶查詢 order
     *
     * @return
     */
    @Query(nativeQuery = true,
            value = "SELECT a.* FROM `t_order` a left join `t_user` b on a.user_id=b.user_id  where  a.user_id=?1")
    List<OrderEntity> selectOrderOfUserId(long userId);
    /**
     * 根據用戶查詢 order
     *
     * @return
     */
    @Query(nativeQuery = true,
            value = "SELECT a.* FROM `t_order` a left join `t_user` b on a.user_id=b.user_id ")
    List<OrderEntity> selectOrderOfUser();

回答:分庫的join怎么解決:

  • 就是一般用左外連接,

  • 兩個表用相同的分片建,

  • 並且進行表綁定,防止產生數據源實例內的笛卡爾積路由。

  • 使得join的時候,一個分片內部的數據,在分片內部完成 join操作,再由shardingjdbc完成 結果的歸並。

  • 從而得到最終的結果。

連環問:分庫分表后,模糊條件查詢怎么處理?

上面提到的都是條件中有sharding column的SQL執行。

但是,總有一些查詢條件是不包含sharding column的,同時,我們也不可能為了這些請求量並不高的查詢,無限制的冗余分庫分表。

那么這些查詢條件中沒有sharding column的SQL怎么處理?

而在移動互聯網時代,海量的用戶每天產生海量的數量,這些海量數據遠不是一張表能Hold住的。

比如

  • 用戶表:支付寶8億,微信10億。CITIC對公140萬,對私8700萬。

  • 訂單表:美團每天幾千萬,淘寶歷史訂單百億、千億。

目前絕大部分公司的核心數據都是:以RDBMS存儲為主,NoSQL/NewSQL存儲為輔!

  • RDBMS互聯網公司又以MySQL為主

  • NoSQL比較具有代表性的是MongoDB,es

  • NewSQL比較具有代表性的是TiDB。

但是,MySQL單表可以存儲10億級數據,具體的原因,前面視頻已經具體分析

但是,行業認可的,MySQL單表容量在1KW以下, 所以必然要分庫分表

回顧一下,sharding 核心的步驟是:

SQL解析,重寫,路由,執行,結果歸並。

以sharding-jdbc為例,有多少個分庫分表,就要並發路由到多少個分庫分表中執行,然后對結果進行合並。

更有甚者,尤其是有些模糊條件查詢,或者上十個條件篩選。

這種條件查詢相對於有sharding column的條件查詢性能很明顯會下降很多。

多sharding column最好不要使用,建議采用 單sharding column + es + HBase的索引與存儲隔離的架構。

索引與存儲隔離的架構

例如有sharding column的查詢走分庫分表,一些模糊查詢,或者多個不固定條件篩選則走es,海量存儲則交給HBase。

在這里插入圖片描述

HBase特點:

所有字段的全量數據保存到HBase中,Hadoop體系下的HBase存儲能力是海量的,

rowkey查詢速度快,快如閃電(可以優化到50Wqps甚至更高)。

es特點:

es的多條件檢索能力非常強大。可能參與條件檢索的字段索引到ES中。

這個方案把es和HBase的優點發揮的淋漓盡致,同時又規避了它們的缺點,可以說是一個揚長避免的最佳實踐。

這就是經典的ES+HBase組合方案,即索引與數據存儲隔離的方案。

它們之間的交互大概是這樣的:

  • 先根據用戶輸入的條件去es查詢獲取符合過濾條件的rowkey值,

  • 然后用rowkey值去HBase查詢

交互圖如下所示:

在這里插入圖片描述

對於海量數據,且有一定的並發量的分庫分表,絕不是引入某一個分庫分表中間件就能解決問題,而是一項系統的工程。

需要分析整個表相關的業務,讓合適的中間件做它最擅長的事情。

例如有sharding column的查詢走分庫分表,

一些模糊查詢,或者多個不固定條件篩選則走es,海量存儲則交給HBase。

biglog同步保障數據一致性的架構

在很多業務情況下,我們都會在系統中加入redis緩存做查詢優化, 使用es 做全文檢索。

如果數據庫數據發生更新,這時候就需要在業務代碼中寫一段同步更新redis的代碼。

這種數據同步的代碼跟業務代碼糅合在一起會不太優雅,能不能把這些數據同步的代碼抽出來形成一個獨立的模塊呢,答案是可以的。

在這里插入圖片描述

數據的冷熱分離

做了這么多事情后,后面還會有很多的工作要做,比如數據同步的一致性問題,

還有運行一段時間后,某些表的數據量慢慢達到單表瓶頸,這時候還需要做冷數據遷移。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

問題:廣播表是不是公共表

在這里插入圖片描述

可以這么理解。

廣播表,就是更新操作,覆蓋所有分片, 查詢操作,查一個分片即可。

分布式主鍵

數據分⽚后,不同數據節點⽣成全局唯⼀主鍵是非常棘⼿的問題,

同⼀個邏輯表(t_order)內的不同真實表(t_order_n)之間的⾃增鍵由於無法互相感知而產⽣重復主鍵。

盡管可通過設置⾃增主鍵 初始值 和 步長 的⽅式避免 ID 碰撞,但這樣會使維護成本加大,乏完整性和可擴展性。

如果后去需要增加分片表的數量,要逐一修改分片表的步長,運維成本非常高,所以不建議這種方式。

為了讓上手更加簡單,ApacheShardingSphere 內置了 UUID、SNOWFLAKE 兩種分布式主鍵⽣成器,

默認使⽤雪花算法(snowflake)⽣成 64bit 的⻓整型數據。

不僅如此它還抽離出分布式主鍵⽣成器的接口,⽅便我們實現⾃定義的⾃增主鍵⽣成算法。

實現動機

傳統數據庫軟件開發中,主鍵⾃動⽣成技術是基本需求。而各個數據庫對於該需求也提供了相應的⽀持,⽐如 MySQL 的⾃增鍵,Oracle 的⾃增序列等。

數據分⽚后,不同數據節點⽣成全局唯⼀主鍵是⾮常棘⼿的問題。

同⼀個邏輯表內的不同實際表之間的⾃增鍵由於⽆法互相感知而產⽣重復主鍵

雖然可通過約束⾃增主鍵初始值和步⻓的⽅式避免碰撞,但需引⼊額外的運維規則,使解決⽅案缺乏完整性和可擴展性。

⽬前有許多第三⽅解決⽅案可以完美解決這個問題,如 UUID 等依靠特定算法⾃⽣成不重復鍵,或者通過引⼊主鍵⽣成服務等。

為了⽅⽤⼾使⽤、滿⾜不同⽤⼾不同使⽤場景的需求,Apache ShardingSphere不僅提供了內置的分布式主鍵⽣成器,

例如 UUID 、SNOWFLAKE,

還抽離出分布式主鍵⽣成器的接口,⽅便⽤⼾⾃⾏實現⾃定義的⾃增主鍵⽣成器。

內置的主鍵⽣成器

UUID

采⽤ UUID.randomUUID() 的⽅式產⽣分布式主鍵。

SNOWFLAKE

在分⽚規則配置模塊可配置每個表的主鍵⽣成策略,默認使⽤雪花算法(snowfl ake )⽣成 64bit 的⻓整型數據。
雪花算法是由 Twitter 公布的分布式主鍵⽣成算法,它能夠保證不同進程主鍵的不重復性,以及相同進程主鍵的有序性。

實現原理

在同⼀個進程中,它⾸先是通過時間位保證不重復,如果時間相同則是通過序列位保證。同時由於時間位是單調遞增的,且各個服務器如果⼤體做了時間同步,那么⽣成的主鍵在分布式環境可以認為是總體有序的,這就保證了對索引字段的插⼊的⾼效性。

例如 MySQL 的 Innodb 存儲引擎的主鍵。
使⽤雪花算法⽣成的主鍵,⼆進制表⽰形式包含 4 部分,從⾼位到低位分表為:

  • 1bit 符號位、
  • 41bit 時間戳位、
  • 10bit ⼯作進程位以及
  • 12bit 序列號位。

符號位(1bit)

預留的符號位,恆為零。

時間戳位(41bit)

41 位的時間戳可以容納的毫秒數是 2 的 41 次冪,⼀年所使⽤的毫秒數是:365 * 24 * 60 * 60 *1000。

通過計算可知:
Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
結果約等於 69.73 年。

Apache ShardingSphere 的雪花算法的時間紀元從 2016 年 11 ⽉ 1 ⽇零點開始,可以使⽤到 2086 年,

相信能滿⾜絕⼤部分系統的要求。

⼯作進程位(10bit)

該標志在 Java 進程內是唯⼀的,如果是分布式應⽤部署應保證每個⼯作進程的 id 是不同的。該值默認為0,可通過屬性設置。

序列號位(12bit)

該序列是⽤來在同⼀個毫秒內⽣成不同的 ID。

如果在這個毫秒內⽣成的數量超過 4096 (2 的 12 次冪),那么⽣成器會等待到下個毫秒繼續⽣成。
雪花算法主鍵的詳細結構⻅下圖。

在這里插入圖片描述

時鍾回撥

服務器時鍾回撥會導致產⽣重復序列,因此默認分布式主鍵⽣成器提供了⼀個最⼤容忍的時鍾回撥毫秒數。

如果時鍾回撥的時間超過最⼤容忍的毫秒數閾值,則程序報錯;

如果在可容忍的范圍內,默認分布式主鍵⽣成器會等待時鍾同步到最后⼀次主鍵⽣成的時間后再繼續⼯作。

最⼤容忍的時鍾回撥毫秒數的默認值為 0,可通過屬性設置。

步長不均衡

導致數據傾斜

基因id算法

基本原理

業務:查詢用戶的所有訂單、 查詢訂單詳情。

字段:用戶ID、訂單ID。

普通水平切分:

  根據訂單ID切分則無法一次查詢用戶的所有訂單;
  根據用戶ID切分則需要先查訂單所屬用戶;

什么是分庫基因?

通過uid分庫,假設分為16個庫,采用uid%16的方式來進行數據庫路由,這里的uid%16,

其本質是uid的最后4個bit決定這行數據落在哪個庫上,這4個bit,就是分庫基因。

什么是基因法分庫?
  在這里插入圖片描述

  

如上圖所示,uid=666的用戶發布了一條訂單(666的二進制表示為:1010011010):
  使用uid%16分庫,決定這行數據要插入到哪個庫中
  分庫基因是uid的最后4個bit,即1010

  在生成tid時,先使用一種分布式ID生成算法生成前60bit(上圖中綠色部分)

  將分庫基因加入到tid的最后4個bit(上圖中粉色部分)

  拼裝成最終的64bit帖子tid(上圖中藍色部分)

  這般,保證了同一個用戶發布的所有訂單的tid,都落在同一個庫上,tid的最后4個bit都相同,

於是:
    通過uid%16能夠定位到庫
    通過tid%16也能定位到庫

 

 全局唯一ID也可以通過表自增位置和自增范圍實現,比如用戶ID哈希為 1 的表,自增位置為1,自增范圍為 10。

代碼實現

請參見視頻配套源碼

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

Shardingjdbc SPI與自定義主鍵

Java SPI是什么

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啟用框架擴展和替換組件。

系統設計的各個抽象,往往有很多不同的實現方案,

在面向的對象的設計里,一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。

一旦代碼里涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。

為了實現在模塊裝配的時候能不在程序里動態指明,這就需要一種服務發現機制。

Java SPI就是提供這樣的一個機制:為某個接口尋找服務實現的機制。

有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。

整體機制圖如下:

在這里插入圖片描述

這里寫圖片描述

Java SPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。所以SPI的核心思想就是解耦

Java SPI的約定

Java SPI使用場景

概括地說,適用於:調用者根據實際使用需要,啟用、擴展、或者替換框架的實現策略

比較常見的例子:

  • 數據庫驅動加載接口實現類的加載
    JDBC加載不同類型數據庫的驅動
  • 日志門面接口實現類加載
    SLF4J加載不同提供商的日志實現類
  • Spring
    Spring中大量使用了SPI,比如:對servlet3.0規范對ServletContainerInitializer的實現、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo
    Dubbo中也大量使用SPI的方式實現框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實現Filter接口

Java SPI使用約定

要使用Java SPI,需要遵循如下約定:

  • 1、當服務提供者提供了接口的一種具體實現后,在jar包的META-INF/services目錄下創建一個以“接口全限定名”為命名的文件,內容為實現類的全限定名;
  • 2、接口實現類所在的jar包放在主程序的classpath中;
  • 3、主程序通過java.util.ServiceLoder動態裝載實現模塊,它通過掃描META-INF/services目錄下的配置文件找到實現類的全限定名,把類加載到JVM;
  • 4、SPI的實現類必須攜帶一個不帶參數的構造方法;

JavaSPI實戰

首先,我們需要定義一個接口,SPI Service 接口

package com.crazymaker.springcloud.sharding.jdbc.demo.generator;

public interface IdGenerator
{

    /**
     * Next id long.
     *
     * @return the nextId
     */
    Long nextId();

}

然后,定義兩個實現類,也可以定義兩個實現類

// 單機版 AtomicLong 類型的ID生成器
@Data
public class AtomicLongShardingKeyGeneratorSPIDemo implements IdGenerator {

    private AtomicLong atomicLong = new AtomicLong(0);

    @Override
    public Long nextId() {
        return atomicLong.incrementAndGet();
    }
}

最后,要在ClassPath路徑下配置添加一個文件:

  • 文件名字是接口的全限定類名
  • 內容是實現類的全限定類名
  • 多個實現類用換行符分隔。

SPI配置文件位置,文件路徑如下:

在這里插入圖片描述

內容就是實現類的全限定類名:

 com.crazymaker.springcloud.sharding.jdbc.demo.generator.AtomicLongShardingKeyGeneratorSPIDemo

測試

然后我們就可以通過ServiceLoader.load或者Service.providers方法拿到實現類的實例。

  • Service.providers包位於sun.misc.Service
  • ServiceLoader.load包位於java.util.ServiceLoader

    @Test
    public void testGenIdByProvider() {
        Iterator<IdGenerator> providers = Service.providers(IdGenerator.class);

        while (providers.hasNext()) {
            IdGenerator generator = providers.next();

            for (int i = 0; i < 100; i++) {

                Long id = generator.nextId();

                System.out.println("id = " + id);

            }
        }

    }

    @Test
    public void testGenIdByServiceLoader() {
        ServiceLoader<IdGenerator> serviceLoaders = ServiceLoader.load(IdGenerator.class);


        Iterator<IdGenerator> iterator = serviceLoaders.iterator();
        while (iterator.hasNext()) {
            IdGenerator generator = iterator.next();

            for (int i = 0; i < 100; i++) {

                Long id = generator.nextId();

                System.out.println("id = " + id);

            }
        }
    }

兩種方式的輸出結果是一致的:

可插拔架構

背景

在 Apache ShardingSphere 中,很多功能實現類的加載⽅式是通過 SPI(Service Provider Interface)注的⽅式完成的。SPI 是⼀種為了被第三⽅實現或擴展的 API,它可以⽤於實現框架擴展或組件替換。

挑戰

可插拔架構對程序架構設計的要求⾮常⾼,需要將各個模塊相互獨⽴,互不感知,並且通過⼀個可插拔內核,以疊加的⽅式將各種功能組合使⽤。設計⼀套將功能開發完全隔離的架構體系,既可以最⼤限度的將開源社區的活⼒激發出來,也能夠保障項⽬的質量。
Apache ShardingSphere 5.x 版本開始致⼒於可插拔架構,項⽬的功能組件能夠靈活的以可插拔的⽅式進⾏擴展。⽬前,數據分⽚、讀寫分離、數據加密、影⼦庫壓測等功能,以及對 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 與協議的⽀持,均通過插件的⽅式織⼊項⽬。Apache ShardingSphere ⽬前已提供數⼗個 SPI 作為系統的擴展點,而且仍在不斷增加中。

⽬標

讓開發者能夠像使⽤積木⼀樣定制屬於⾃⼰的獨特系統,是 Apache ShardingSphere 可插拔架構的設計⽬標。

Apache ShardingSphere 可插拔架構提供了數⼗個基於 SPI 的擴展點。對於開發者來說,可以⼗分⽅便的對功能進⾏定制化擴展。
本章節將 Apache ShardingSphere 的 SPI 擴展點悉數列出。如⽆特殊需求,⽤⼾可以使⽤ Apache Shard-ingSphere 提供的內置實現;⾼級⽤⼾則可以參考各個功能模塊的接口進⾏⾃定義實現。

基於類型的SPI機制

package org.apache.shardingsphere.spi;

import java.util.Properties;

/**
 * Base algorithm SPI.
 */
public interface TypeBasedSPI {
    
    /**
     * Get algorithm type.
     * 
     * @return type
     */
    String getType();
    
    /**
     * Get properties.
     * 
     * @return properties of algorithm
     */
    Properties getProperties();
    
    /**
     * Set properties.
     * 
     * @param properties properties of algorithm
     */
    void setProperties(Properties properties);
}

分布式主鍵擴展點

在這里插入圖片描述

自定義主鍵實戰

package com.crazymaker.springcloud.sharding.jdbc.demo.generator;

import lombok.Data;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;

import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;

// 單機版 AtomicLong 類型的ID生成器
@Data
public class AtomicLongShardingKeyGenerator implements ShardingKeyGenerator {

    private AtomicLong atomicLong = new AtomicLong(0);
    private Properties properties = new Properties();

    @Override
    public Comparable<?> generateKey() {
        return atomicLong.incrementAndGet();
    }

    @Override
    public String getType() {

        //聲明類型
        return "DemoAtomicLongID";
    }
}

使用實例



    @Test
    public void testGenIdByShardingServiceLoader() {
        ShardingKeyGeneratorServiceLoader serviceLoader = new ShardingKeyGeneratorServiceLoader();
        ShardingKeyGenerator keyGenerator= serviceLoader.newService("DemoAtomicLongID" ,new Properties());

        for (int i = 0; i < 100; i++) {

            Long id = (Long) keyGenerator.generateKey();

            System.out.println("id = " + id);

        }
    }


演示和源碼介紹:

請參見視頻

ShardingSphere的SQL使用限制

參見官網文檔:

https://shardingsphere.apache.org/document/current/cn/features/sharding/use-norms/sql/

文檔中

詳細列出了非常多ShardingSphere目前版本支持和不支持的SQL類型。

這些需要關注。

支持的SQL

SQL 必要條件
SELECT * FROM tbl_name
SELECT * FROM tbl_name WHERE (col1 = ? or col2 = ?) and col3 = ?
SELECT * FROM tbl_name WHERE col1 = ? ORDER BY col2 DESC LIMIT ?
SELECT COUNT(*), SUM(col1), MIN(col1), MAX(col1), AVG(col1) FROM tbl_name WHERE col1 = ?
SELECT COUNT(col1) FROM tbl_name WHERE col2 = ? GROUP BY col1 ORDER BY col3 DESC LIMIT ?, ?
INSERT INTO tbl_name (col1, col2,…) VALUES (?, ?, ….)
INSERT INTO tbl_name VALUES (?, ?,….)
INSERT INTO tbl_name (col1, col2, …) VALUES (?, ?, ….), (?, ?, ….)
INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? INSERT表和SELECT表必須為相同表或綁定表
REPLACE INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? REPLACE表和SELECT表必須為相同表或綁定表
UPDATE tbl_name SET col1 = ? WHERE col2 = ?
DELETE FROM tbl_name WHERE col1 = ?
CREATE TABLE tbl_name (col1 int, …)
ALTER TABLE tbl_name ADD col1 varchar(10)
DROP TABLE tbl_name
TRUNCATE TABLE tbl_name
CREATE INDEX idx_name ON tbl_name
DROP INDEX idx_name ON tbl_name
DROP INDEX idx_name
SELECT DISTINCT * FROM tbl_name WHERE col1 = ?
SELECT COUNT(DISTINCT col1) FROM tbl_name
SELECT subquery_alias.col1 FROM (select tbl_name.col1 from tbl_name where tbl_name.col2=?) subquery_alias

不支持的SQL

SQL 不支持原因
INSERT INTO tbl_name (col1, col2, …) VALUES(1+2, ?, …) VALUES語句不支持運算表達式
INSERT INTO tbl_name (col1, col2, …) SELECT * FROM tbl_name WHERE col3 = ? SELECT子句暫不支持使用*號簡寫及內置的分布式主鍵生成器
REPLACE INTO tbl_name (col1, col2, …) SELECT * FROM tbl_name WHERE col3 = ? SELECT子句暫不支持使用*號簡寫及內置的分布式主鍵生成器
SELECT * FROM tbl_name1 UNION SELECT * FROM tbl_name2 UNION
SELECT * FROM tbl_name1 UNION ALL SELECT * FROM tbl_name2 UNION ALL
SELECT SUM(DISTINCT col1), SUM(col1) FROM tbl_name 詳見DISTINCT支持情況詳細說明
SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ? 會導致全路由
(SELECT * FROM tbl_name) 暫不支持加括號的查詢
SELECT MAX(tbl_name.col1) FROM tbl_name 查詢列是函數表達式時,查詢列前不能使用表名;若查詢表存在別名,則可使用表的別名

DISTINCT支持情況詳細說明

支持的SQL

SQL
SELECT DISTINCT * FROM tbl_name WHERE col1 = ?
SELECT DISTINCT col1 FROM tbl_name
SELECT DISTINCT col1, col2, col3 FROM tbl_name
SELECT DISTINCT col1 FROM tbl_name ORDER BY col1
SELECT DISTINCT col1 FROM tbl_name ORDER BY col2
SELECT DISTINCT(col1) FROM tbl_name
SELECT AVG(DISTINCT col1) FROM tbl_name
SELECT SUM(DISTINCT col1) FROM tbl_name
SELECT COUNT(DISTINCT col1) FROM tbl_name
SELECT COUNT(DISTINCT col1) FROM tbl_name GROUP BY col1
SELECT COUNT(DISTINCT col1 + col2) FROM tbl_name
SELECT COUNT(DISTINCT col1), SUM(DISTINCT col1) FROM tbl_name
SELECT COUNT(DISTINCT col1), col1 FROM tbl_name GROUP BY col1
SELECT col1, COUNT(DISTINCT col1) FROM tbl_name GROUP BY col1

不支持的SQL

SQL 不支持原因
SELECT SUM(DISTINCT tbl_name.col1), SUM(tbl_name.col1) FROM tbl_name 查詢列是函數表達式時,查詢列前不能使用表名;若查詢表存在別名,則可使用表的別名

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

ShardingJdbc數據分片開發總結

作為一個開發者,ShardingJdbc可以幫我們屏蔽底層的細節,

讓我們在面對分庫分表的場景下,可以像使用單庫單表一樣簡單;

分片策略算法

ShardingSphere-JDBC在分片策略上分別引入了分片算法分片策略兩個概念,

當然在分片的過程中分片鍵也是一個核心的概念;在此可以簡單的理解分片策略 = 分片算法 + 分片鍵

至於為什么要這么設計,應該是ShardingSphere-JDBC考慮更多的靈活性,把分片算法單獨抽象出來,方便開發者擴展;

分片算法

提供了抽象分片算法類:ShardingAlgorithm,根據類型又分為:精確分片算法、區間分片算法、復合分片算法以及Hint分片算法;

  • 精確分片算法:對應PreciseShardingAlgorithm類,主要用於處理 =IN的分片;
  • 區間分片算法:對應RangeShardingAlgorithm類,主要用於處理 BETWEEN AND, >, <, >=, <= 分片;
  • 復合分片算法:對應ComplexKeysShardingAlgorithm類,用於處理使用多鍵作為分片鍵進行分片的場景;
  • Hint分片算法:對應HintShardingAlgorithm類,用於處理使用 Hint 行分片的場景;

以上所有的算法類都是接口類,具體實現交給開發者自己;

分片策略

分片策略基本和上面的分片算法對應,包括:標准分片策略、復合分片策略、Hint分片策略、內聯分片策略、不分片策略;

  • 標准分片策略:對應StandardShardingStrategy類,提供PreciseShardingAlgorithmRangeShardingAlgorithm兩個分片算法,PreciseShardingAlgorithm是必須的,RangeShardingAlgorithm可選的;

    public final class StandardShardingStrategy implements ShardingStrategy {
        private final String shardingColumn;
        private final PreciseShardingAlgorithm preciseShardingAlgorithm;
        private final RangeShardingAlgorithm rangeShardingAlgorithm;
    }
    
    
  • 復合分片策略:對應ComplexShardingStrategy類,提供ComplexKeysShardingAlgorithm分片算法;

    public final class ComplexShardingStrategy implements ShardingStrategy {
        @Getter
        private final Collection<String> shardingColumns;
        private final ComplexKeysShardingAlgorithm shardingAlgorithm;
    }
    
    

    可以發現支持多個分片鍵;

  • Hint分片策略:對應HintShardingStrategy類,通過 Hint 指定分片值而非從 SQL 中提取分片值的方式進行分片的策略;提供HintShardingAlgorithm分片算法;

    public final class HintShardingStrategy implements ShardingStrategy {
        @Getter
        private final Collection<String> shardingColumns;
        private final HintShardingAlgorithm shardingAlgorithm;
    }
    
    
  • 內聯分片策略:對應InlineShardingStrategy類,沒有提供分片算法,路由規則通過表達式來實現;

  • 不分片策略:對應NoneShardingStrategy類,不分片策略;

分片策略配置類

在使用中我們並沒有直接使用上面的分片策略類,ShardingSphere-JDBC分別提供了對應策略的配置類包括:

  • StandardShardingStrategyConfiguration
  • ComplexShardingStrategyConfiguration
  • HintShardingStrategyConfiguration
  • InlineShardingStrategyConfiguration
  • NoneShardingStrategyConfiguration

實戰步驟總結

有了以上相關基礎概念,接下來針對每種分片策略做一個簡單的實戰,在實戰前首先准備好庫和表;

准備

分別准備兩個庫:ds0ds1;然后每個庫分別包含兩個表:t_order0t_order1

CREATE TABLE `t_order0` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `order_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

准備真實數據源

我們這里有兩個數據源,這里都使用java代碼的方式來配置:

  /**
     * 通過ShardingDataSourceFactory 構建分片數據源
     *
     * @return
     * @throws SQLException
     */
    @Before

    public void buildShardingDataSource() throws SQLException {
        /*
         * 1. 數據源集合:dataSourceMap
         * 2. 分片規則:shardingRuleConfig
         * 3. 屬性:properties
         *
         */

        DataSource druidDs1 = buildDruidDataSource(
                "jdbc:mysql://cdh1:3306/sharding_db1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC",
                "root", "123456");

        DataSource druidDs2 = buildDruidDataSource(
                "jdbc:mysql://cdh1:3306/sharding_db2?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC",
                "root", "123456");
        // 配置真實數據源
        Map<String, DataSource> dataSourceMap = new HashMap<String, DataSource>();
        // 添加數據源.
        // 兩個數據源ds_0和ds_1
        dataSourceMap.put("ds0",druidDs1);
        dataSourceMap.put("ds1", druidDs2);

        /**
         * 需要構建表規則
         * 1. 指定邏輯表.
         * 2. 配置實際節點》
         * 3. 指定主鍵字段.
         * 4. 分庫和分表的規則》
         *
         */
        // 配置分片規則
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();


        //step2:分片規則
        TableRuleConfiguration userShardingRuleConfig = userShardingRuleConfig();
        shardingRuleConfig.getTableRuleConfigs().add(userShardingRuleConfig);


        // 多數據源一定要指定默認數據源
        // 只有一個數據源就不需要
        shardingRuleConfig.setDefaultDataSourceName("ds0");

        Properties properties = new Properties();
        //打印sql語句,生產環境關閉
        properties.setProperty("sql.show", Boolean.TRUE.toString());

        dataSource= ShardingDataSourceFactory.createDataSource(
                dataSourceMap, shardingRuleConfig, properties);

    }

這里配置的兩個數據源都是普通的數據源,最后會把dataSourceMap交給ShardingDataSourceFactory管理;

表規則配置

表規則配置類TableRuleConfiguration,包含了五個要素:邏輯表、真實數據節點、數據庫分片策略、數據表分片策略、分布式主鍵生成策略;

    /**
     * 表的分片規則
     */
    protected TableRuleConfiguration userShardingRuleConfig() {
        String logicTable = USER_LOGIC_TB;

        //獲取實際的 ActualDataNodes
        String actualDataNodes = "ds$->{0..1}.t_user_$->{0..1}";

        // 兩個表達式的 笛卡爾積
//ds0.t_user_0
//ds1.t_user_0
//ds0.t_user_1
//ds1.t_user_1

        TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration(logicTable, actualDataNodes);

        //設置分表策略
        // inline 模式
//        ShardingStrategyConfiguration tableShardingStrategy =
//                new InlineShardingStrategyConfiguration("user_id", "t_user_$->{user_id % 2}");
        //自定義模式
        TablePreciseShardingAlgorithm tablePreciseShardingAlgorithm =
                new TablePreciseShardingAlgorithm();

        RouteInfinityRangeShardingAlgorithm routeInfinityRangeShardingAlgorithm =
                new RouteInfinityRangeShardingAlgorithm();

        RangeOrderShardingAlgorithm tableRangeShardingAlg =
                new RangeOrderShardingAlgorithm();

        PreciseOrderShardingAlgorithm preciseOrderShardingAlgorithm =
                new PreciseOrderShardingAlgorithm();

        ShardingStrategyConfiguration tableShardingStrategy =
                new StandardShardingStrategyConfiguration("user_id",
                        preciseOrderShardingAlgorithm,
                        routeInfinityRangeShardingAlgorithm);

        tableRuleConfig.setTableShardingStrategyConfig(tableShardingStrategy);

        // 配置分庫策略(Groovy表達式配置db規則)
        // inline 模式
//        ShardingStrategyConfiguration dsShardingStrategy = new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}");
        //自定義模式
        DsPreciseShardingAlgorithm dsPreciseShardingAlgorithm = new DsPreciseShardingAlgorithm();
        RangeOrderShardingAlgorithm dsRangeShardingAlg =
                new RangeOrderShardingAlgorithm();

        ShardingStrategyConfiguration dsShardingStrategy =
                new StandardShardingStrategyConfiguration("user_id",
                        preciseOrderShardingAlgorithm,
                        routeInfinityRangeShardingAlgorithm);

        tableRuleConfig.setDatabaseShardingStrategyConfig(dsShardingStrategy);

        tableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "user_id"));
        return tableRuleConfig;
    }

  • 邏輯表:這里配置的邏輯表就是t_user,對應的物理表有t_user_0,t_user_1;

  • 真實數據節點:這里使用行表達式進行配置的,簡化了配置;上面的配置就相當於配置了:

    db0
      ├── t_user_0 
      └── t_user_1 
    db1
      ├── t_user_0 
      └── t_user_1
    
    
  • 數據庫分片策略:這里的庫分片策略就是上面介紹的五種類型,這里使用的StandardShardingStrategyConfiguration,需要指定分片鍵分片算法,這里使用的是精確分片算法

    
    public final class PreciseOrderShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
    
        @Override
        public String doSharding(final Collection<String> availableTargetNames,
                                 final PreciseShardingValue<Long> shardingValue) {
            for (String each : availableTargetNames) {
                System.out.println("shardingValue = " + shardingValue.getValue()+ " target = " + each + "  shardingValue.getValue() % 2) = " + shardingValue.getValue() % 2L);
                if (each.endsWith(String.valueOf(shardingValue.getValue() % 2L))) {
                    return each;
                }
            }
            return null;
        }
    }
    
    
    

    這里的shardingValue就是user_id對應的真實值,每次和2取余;availableTargetNames可選擇就是{ds0,ds1};看余數和哪個庫能匹配上就表示路由到哪個庫;

  • 數據表分片策略:指定的分片鍵(order_id)和分庫策略不一致,其他都一樣;

  • 分布式主鍵生成策略:ShardingSphere-JDBC提供了多種分布式主鍵生成策略,后面詳細介紹,這里使用雪花算法;

配置分片規則

配置分片規則ShardingRuleConfiguration,包括多種配置規則:表規則配置、綁定表配置、廣播表配置、默認數據源名稱、默認數據庫分片策略、默認表分片策略、默認主鍵生成策略、主從規則配置、加密規則配置;

  • 表規則配置 tableRuleConfigs:也就是上面配置的庫分片策略和表分片策略,也是最常用的配置;
  • 綁定表配置 bindingTableGroups:指分⽚規則⼀致的主表和⼦表;綁定表之間的多表關聯查詢不會出現笛卡爾積關聯,關聯查詢效率將⼤⼤提升;
  • 廣播表配置 broadcastTables:所有的分⽚數據源中都存在的表,表結構和表中的數據在每個數據庫中均完全⼀致。適⽤於數據量不⼤且需要與海量數據的表進⾏關聯查詢的場景;
  • 默認數據源名稱 defaultDataSourceName:未配置分片的表將通過默認數據源定位;
  • 默認數據庫分片策略 defaultDatabaseShardingStrategyConfig:表規則配置可以設置數據庫分片策略,如果沒有配置可以在這里面配置默認的;
  • 默認表分片策略 defaultTableShardingStrategyConfig:表規則配置可以設置表分片策略,如果沒有配置可以在這里面配置默認的;
  • 默認主鍵生成策略 defaultKeyGeneratorConfig:表規則配置可以設置主鍵生成策略,如果沒有配置可以在這里面配置默認的;內置UUID、SNOWFLAKE生成器;
  • 主從規則配置 masterSlaveRuleConfigs:用來實現讀寫分離的,可配置一個主表多個從表,讀面對多個從庫可以配置負載均衡策略;
  • 加密規則配置 encryptRuleConfig:提供了對某些敏感數據進行加密的功能,提供了⼀套完整、安全、透明化、低改造成本的數據加密整合解決⽅案;

數據插入

以上准備好,就可以操作數據庫了,這里執行插入操作:

 /**
     * 新增測試.
     *
     */
    @Test
    public  void testInsertUser() throws SQLException {

        /*
         * 1. 需要到DataSource
         * 2. 通過DataSource獲取Connection
         * 3. 定義一條SQL語句.
         * 4. 通過Connection獲取到PreparedStament.
         *  5. 執行SQL語句.
         *  6. 關閉連接.
         */


        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "insert into t_user(name) values('name-0001')";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        preparedStatement.execute();

         sql = "insert into t_user(name) values('name-0002')";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.execute();

        // * 6. 關閉連接.
        preparedStatement.close();
        connection.close();
    }

通過以上配置的真實數據源、分片規則以及屬性文件創建分片數據源ShardingDataSource

接下來就可以像使用單庫單表一樣操作分庫分表了,sql中可以直接使用邏輯表,分片算法會根據具體的值就行路由處理;

經過路由最終:奇數入ds1.t_user_1,偶數入ds0.t_user_0;

分片算法

上面的介紹的精確分片算法中,通過PreciseShardingValue來獲取當前分片鍵值,ShardingSphere-JDBC針對每種分片算法都提供了相應的ShardingValue,具體包括:

  • PreciseShardingValue
  • RangeShardingValue
  • ComplexKeysShardingValue
  • HintShardingValue

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

主從復制&讀寫分離

對於同一時刻有大量並發讀操作和較少寫操作類型的應用系統來說,將數據庫拆分為主庫和從庫,主庫負責處理事務性的增刪改操作,從庫負責處理查詢操作,能夠有效的避免由數據更新導致的行鎖,使得整個系統的查詢性能得到極大的改善。

搭建的Mysql主從集群

設置前注意下面幾點:
1)要保證同步服務期間之間的網絡聯通。即能相互ping通,能使用對方授權信息連接到對方數據庫(防火牆開放3306端口)。
2)關閉selinux。
3)同步前,雙方數據庫中需要同步的數據要保持一致。這樣,同步環境實現后,再次更新的數據就會如期同步了。如果主庫是新庫,忽略此步。

創建目錄

把尼恩提供的 配置文件、編排文件整體復制到 cdn2 虛擬機的 目錄下:

docker-compose.yml文件如下

version: '3.5'
services:
  mysql-master:
    container_name: mysql-master 
    image: mysql:5.7.31
    restart: always
    ports:
      - 3340:3306 
    privileged: true
    volumes:
      - ./mysql-master/log:/var/log/mysql  
      - ./mysql-master/conf/my.cnf:/etc/mysql/my.cnf
      - ./mysql-master/data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: "123456"
    command: [
        '--character-set-server=utf8mb4',
        '--collation-server=utf8mb4_general_ci',
        '--max_connections=3000'
    ]

    networks:
      mysql-canal-network:
        aliases:
          - mysql-master
      
  mysql-slave:
    container_name: mysql-slave 
    image: mysql:5.7.31
    restart: always
    ports:
      - 3341:3306 
    privileged: true
    volumes:
      - ./mysql-slave/log:/var/log/mysql  
      - ./mysql-slave/conf/my.cnf:/etc/mysql/my.cnf
      - ./mysql-slave/data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: "123456"
    command: [
        '--character-set-server=utf8mb4',
        '--collation-server=utf8mb4_general_ci',
        '--max_connections=3000'
    ]
    networks:
      mysql-canal-network:
        aliases:
          - mysql-slave
 
 
networks:
 mysql-canal-network:
  name: mysql-canal-network
  driver: bridge

1. 主master配置文件my.cnf

vim msql-master/volumes/conf/my.cnf
[mysqld]
# [必須]服務器唯一ID,默認是1,一般取IP最后一段
server-id=1

# [必須]啟用二進制日志
log-bin=mysql-bin 

# 復制過濾:也就是指定哪個數據庫不用同步(mysql庫一般不同步)
binlog-ignore-db=mysql

# 設置需要同步的數據庫 binlog_do_db = 數據庫名; 
# 如果是多個同步庫,就以此格式另寫幾行即可。
# 如果不指明對某個具體庫同步,表示同步所有庫。除了binlog-ignore-db設置的忽略的庫
# binlog_do_db = test #需要同步test數據庫。

# 確保binlog日志寫入后與硬盤同步
sync_binlog = 1

# 跳過所有的錯誤,繼續執行復制操作
slave-skip-errors = all       
溫馨提示:在主服務器上最重要的二進制日志設置是sync_binlog,這使得mysql在每次提交事務的時候把二進制日志的內容同步到磁盤上,即使服務器崩潰也會把事件寫入日志中。
sync_binlog這個參數是對於MySQL系統來說是至關重要的,他不僅影響到Binlog對MySQL所帶來的性能損耗,而且還影響到MySQL中數據的完整性。對於``"sync_binlog"``參數的各種設置的說明如下:
sync_binlog=0,當事務提交之后,MySQL不做fsync之類的磁盤同步指令刷新binlog_cache中的信息到磁盤,而讓Filesystem自行決定什么時候來做同步,或者cache滿了之后才同步到磁盤。
sync_binlog=n,當每進行n次事務提交之后,MySQL將進行一次fsync之類的磁盤同步指令來將binlog_cache中的數據強制寫入磁盤。
  
在MySQL中系統默認的設置是sync_binlog=0,也就是不做任何強制性的磁盤刷新指令,這時候的性能是最好的,但是風險也是最大的。因為一旦系統Crash,在binlog_cache中的所有binlog信息都會被丟失。而當設置為“1”的時候,是最安全但是性能損耗最大的設置。因為當設置為1的時候,即使系統Crash,也最多丟失binlog_cache中未完成的一個事務,對實際數據沒有任何實質性影響。
  
從以往經驗和相關測試來看,對於高並發事務的系統來說,“sync_binlog”設置為0和設置為1的系統寫入性能差距可能高達5倍甚至更多。

2. 從slave配置文件my.cnf

每個slave都需要設置server_id,且一個集群中所有的server_id不能夠被重復。

每個slave只能有一個Master,但每個Master可以有多個Slave(MySQL 5.7開始出現多源復制,就是允許Slave有多個Master)。


vim msql-slave/volumes/conf/my.cnf
[mysqld]
# [必須]服務器唯一ID,默認是1,一般取IP最后一段  
server-id=2

# 如果想實現 主-從(主)-從 這樣的鏈條式結構,需要設置:
# log-slave-updates      只有加上它,從前一台機器上同步過來的數據才能同步到下一台機器。

# 設置需要同步的數據庫,主服務器上不限定數據庫,在從服務器上限定replicate-do-db = 數據庫名;
# 如果不指明同步哪些庫,就去掉這行,表示所有庫的同步(除了ignore忽略的庫)。
# replicate-do-db = test;

# 不同步test數據庫 可以寫多個例如 binlog-ignore-db = mysql,information_schema 
replicate-ignore-db=mysql  

## 開啟二進制日志功能,以備Slave作為其它Slave的Master時使用
log-bin=mysql-bin
log-bin-index=mysql-bin.index

## relay_log配置中繼日志
#relay_log=edu-mysql-relay-bin  

## 還可以設置一個log保存周期:
#expire_logs_days=14

# 跳過所有的錯誤,繼續執行復制操作
slave-skip-errors = all   

啟動服務mysql主從集群


rm -rf  /home/docker-compose/mysqlmasterslave
cp -rf /vagrant/3G-middleware/mysqlmasterslave  /home/docker-compose/


cd /home/docker-compose/

chmod 777 -R  /home/docker-compose/mysqlmasterslave

chmod 644 /home/docker-compose/mysqlmasterslave/master/my.cnf
chmod 644 /home/docker-compose/mysqlmasterslave/slave/my.cnf


 chown -R mysql:mysql /home/docker-compose/mysqlmasterslave/slave/bin-log
 chown -R mysql:mysql /home/docker-compose/mysqlmasterslave/master/bin-log

cd mysqlmasterslave
docker-compose  up


docker-compose --compatibility  up  -d



docker-compose  logs -f


docker-compose down

tail -30  /home/docker-compose/mysqlmasterslave/master/log/mysql/error.log

tail -f  /home/docker-compose/mysqlmasterslave/master/log/mysql/error.log

有關docker環境的學習,請參考瘋狂創客圈 總目錄

在這里插入圖片描述

查詢服務ip地址

從上面的信息里獲取服務創建的網絡名稱mysql-canal-network

docker network inspect mysql-canal-network


      "Containers": {
            "4714af786aa835a8b6eca35e123e9f4d7f4cdd7099f329d17ba5e513e8cbd59b": {
                "Name": "mysql-master",
                "EndpointID": "b14745f5abc126aef1b534e43f3eb99d0afec6ab8ba92c7edc162f52eb5d0662",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            },
            "8573f0faeea2259995db260fa7d4cba4d5fd363752ef24aef825bec237a0df7f": {
                "Name": "mysql-slave",
                "EndpointID": "369bca8b3d30c19b2fae71e648b6ff5c73583dcb2aa73f0380cc3410dd018114",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },


查到結果

mysql-master ip為 172.18.0.3
mysql-slave ip為 172.18.0.2

進入主mysql服務

docker exec -it mysql-master bash
mysql -uroot -p123456




#查看server_id是否生效
mysql> show variables like '%server_id%';
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| server_id      | 3306  |
| server_id_bits | 32    |
+----------------+-------+
2 rows in set (0.00 sec)






#看master信息 File 和 Position, 從服務上要用

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000004 |      154 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)


給root開 遠程權限
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456';  
grant replication slave,replication client on *.* to 'slave'@'%' identified by "123456";
flush privileges;
#開權限
grant replication slave,replication client on *.* to 'slave'@'%' identified by "123456";
flush privileges;


進入從slave服務

docker exec -it mysql-slave bash

mysql -uroot -p123456

show variables like '%server_id%';

#查看server_id是否生效
mysql> show variables like '%server_id%';
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| server_id      | 2     |
| server_id_bits | 32    |
+----------------+-------+


建立主從關系


 change master to master_host='mysql-master',master_user='slave',master_password='123456',master_port=3306,master_log_file='bin-log.000003', master_log_pos=1346,master_connect_retry=30;

mysql> change master to master_host='mysql-master',master_user='slave',master_password='123456',master_port=3306,master_log_file='bin-log.000003', master_log_pos=154,master_connect_retry=30;
Query OK, 0 rows affected, 2 warnings (0.24 sec)



在從庫上,啟動 slave線程

mysql> start slave;

mysql>  show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: mysql-master
                  Master_User: slave
                  Master_Port: 3306
                Connect_Retry: 30
              Master_Log_File: mysql-bin.000004
          Read_Master_Log_Pos: 610
               Relay_Log_File: ed93d4a41e0f-relay-bin.000002
                Relay_Log_Pos: 776
        Relay_Master_Log_File: mysql-bin.000004
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 610
              Relay_Log_Space: 990
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 3306
                  Master_UUID: 3c1efb5d-a1e9-11ec-ad17-0242ac120002
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
1 row in set (0.00 sec)

連接主mysql參數說明:

**master_port**:Master的端口號,指的是容器的端口號

**master_user**:用於數據同步的用戶

**master_password**:用於同步的用戶的密碼

**master_log_file**:指定 Slave 從哪個日志文件開始復制數據,即上文中提到的 File 字段的值

**master_log_pos**:從哪個 Position 開始讀,即上文中提到的 Position 字段的值

**master_connect_retry**:如果連接失敗,重試的時間間隔,單位是秒,默認是60秒

上面看到,有兩個Yes,說明已經成功了

        Relay_Master_Log_File: mysql-bin.000004
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

設置從服務器slave為只讀模式

在從服務器slave上操作

SHOW VARIABLES LIKE '%read_only%'; #查看只讀狀態

SET GLOBAL super_read_only=1;   #super權限的用戶只讀狀態 1.只讀 0:可寫
SET GLOBAL read_only=1;     #普通權限用戶讀狀態 1.只讀 0:可寫

到此已經設置成功了,下面就可以測試一下,已經可以主從同步了

從服務器上的常用操作

docker exec -it mysql-slave bash

mysql -uroot -p123456


stop slave;
start slave;
show slave status;

show slave status \G;
  
  
reset slave;

drop user canal;

drop database ...;

tail -30 /home/docker-compose/mysqlmasterslave/master/log/mysql/error.log

數據源准備

在cdh1節點的主庫創建表

在這里插入圖片描述

腳本如下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user_0
-- ----------------------------
DROP TABLE IF EXISTS `t_user_0`;
CREATE TABLE `t_user_0`  (
  `id` bigint(20) NULL DEFAULT NULL,
  `name` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `t_user_1`;
CREATE TABLE `t_user_1`  (
  `id` bigint(20) NULL DEFAULT NULL,
  `name` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

確保cdh2節點的從庫也有以上兩個表:

在這里插入圖片描述

注意:主庫創建的表,會自動復制到從庫

binlog(歸檔日志)

名稱:歸檔日志(二進制日志)

作用:用於復制,在主從復制中,從庫利用主庫上的binlog進行重播,實現主從同步。

​ 用於的基於時間點的數據還原,主要是用於增量數據還原。

內容:邏輯格式的日志,可以簡單認為就是執行過的事務中的sql語句。

binlog記錄了對MySQL數據庫執行更改的所有操作,不包括SELECT和SHOW這類操作,主要作用是用於數據庫的主從復制及數據的增量恢復

使用mysqldump備份時,只是對一段時間的數據進行全備,但是如果備份后突然發現數據庫服務器故障,這個時候就要用到binlog的日志了

mysqldump 全量備份 + mysqlbinlog 二進制日志增量備份

存在一個完全備份,生產環境一般凌晨某個時刻進行全備

mysqldump -uroot -p --default-character-set=gbk --single-transaction -F -B school |gzip > /backup/school_$(date +%F).sql.gz



恢復增量備份
mysqlbinlog -d school mysql-bin.000004 > db-bin.sql
mysql -uroot -p111111 db < db-bin.sql



要開啟 mysql log-bin 日志功能,
[mysqld]
datadir=/var/lib/mysql/data
log-bin=mysql-bin
server-id=1

binlog格式有三種:STATEMENT,ROW,MIXED

  • STATEMENT模式:binlog里面記錄的就是SQL語句的原文。優點是並不需要記錄每一行的數據變化,減少了binlog日志量,節約IO,提高性能。缺點是在某些情況下會導致master-slave中的數據不一致
  • ROW模式:不記錄每條SQL語句的上下文信息,僅需記錄哪條數據被修改了,修改成什么樣了,解決了STATEMENT模式下出現master-slave中的數據不一致。缺點是會產生大量的日志,尤其是alter table的時候會讓日志暴漲
  • MIXED模式:以上兩種模式的混合使用,一般的復制使用STATEMENT模式保存binlog,對於STATEMENT模式無法復制的操作使用ROW模式保存binlog,MySQL會根據執行的SQL語句選擇日志保存方式

binlog 持久化策略

在進行事務的過程中,首先會把binlog 寫入到binlog cache中(因為寫入到cache中會比較快,一個事務通常會有多個操作,避免每個操作都直接寫磁盤導致性能降低),事務最終提交的時候再吧binlog 寫入到磁盤中。當然事務在最終commit的時候binlog是否馬上寫入到磁盤中是由參數 sync_binlog 配置來決定的。

1、sync_binlog=0 的時候,表示每次提交事務binlog不會馬上寫入到磁盤,而是先寫到page cache,相對於磁盤寫入來說寫page cache要快得多,不過在Mysql 崩潰的時候會有丟失日志的風險。

2、sync_binlog=1 的時候,表示每次提交事務都會執行 fsync 寫入到磁盤 ;

3、sync_binlog的值大於1 的時候,表示每次提交事務都 先寫到page cach,只有等到積累了N個事務之后才fsync 寫入到磁盤,同樣在此設置下Mysql 崩潰的時候會有丟失N個事務日志的風險。

很顯然三種模式下,sync_binlog=1 是強一致的選擇,選擇0或者N的情況下在極端情況下就會有丟失日志的風險,具體選擇什么模式還是得看系統對於一致性的要求。

redo log(重做日志)

名稱: 重做日志

作用:確保事務的持久性。防止在發生故障的時間點,尚有臟頁未寫入磁盤,在重啟mysql服務的時候,根據redo log進行重做,從而達到事務的持久性這一特性。

內容:物理格式的日志,記錄的是物理數據頁面的修改的信息,其redo log是順序寫入redo log file的物理文件中去的。

MySQL整體來看就有兩塊:

  • 一塊是Server層,主要做的是MySQL功能層面的事情;比如 binlog是 Server層也有自己的日志

  • 還有一塊是引擎層,負責存儲相關的具體事宜。比如,redo log是InnoDB引擎特有的日志,

MyISAM是MySQL的默認數據庫引擎(5.5版之前),由早期的ISAM所改良。雖然性能極佳,但卻有一個缺點:不支持事務處理(transaction)。

redo log的來源

redo log 是屬於引擎層(innodb)的日志,它的設計目標是支持innodb的“事務”的特性,事務ACID特性分別是原子性、一致性、隔離性、持久性, 一致性是事務的最終追求的目標,隔離性、原子性、持久性是達成一致性目標的手段,隔離性是通過鎖機制來實現的。

而事務的原子性和持久性則是通過redo log 和undo log來保障的。

redo log 能保證對於已經COMMIT的事務產生的數據變更,即使是系統宕機崩潰也可以通過它來進行數據重做,達到數據的一致性,這也就是事務持久性的特征,一旦事務成功提交后,只要修改的數據都會進行持久化,不會因為異常、宕機而造成數據錯誤或丟失,所以解決異常、宕機而可能造成數據錯誤或丟是redo log的核心職責。

什么是 WAL

MySQL里常說的WAL技術,全稱是Write Ahead Log,即當事務提交時,先寫redo log,再修改頁。

WAL(Write Ahead Log)預寫日志,是數據庫系統中常見的一種手段,用於保證數據操做的原子性和持久性。

在計算機科學中,預寫式日志(Write-ahead logging,縮寫 WAL)是關系數據庫系統中用於提供原子性和持久性(ACID 屬性中的兩個)的一系列技術。在使用 WAL 的系統中,全部的修改在提交以前都要先寫入 log 文件中。

log 文件中一般包括 redo 和 undo 信息。

這樣作的目的能夠經過一個例子來講明。

假設一個程序在執行某些操做的過程當中機器掉電了。在重新啟動時,若是使用了 WAL,程序就能夠檢查 log 文件,並對忽然掉電時計划執行的操做內容跟實際上執行的操做內容進行比較。在這個比較的基礎上,程序就能夠決定是撤銷已作的操做仍是繼續完成已作的操做,或者是保持原樣。

預寫式日志(Write-ahead logging,縮寫 WAL),當有一條記錄需要更新的時候,InnoDB會先把記錄寫到redo log里面,並更新Buffer Pool的page,這個時候更新操作就算完成了

Buffer Pool是物理頁的緩存,對InnoDB的任何修改操作都會首先在Buffer Pool的page上進行,然后這樣的頁將被標記為臟頁並被放到專門的Flush List上,后續將由專門的刷臟線程階段性的將這些頁面寫入磁盤

InnoDB的redo log是固定大小的,比如可以配置為一組4個文件,每個文件的大小是1GB,循環使用,從頭開始寫,寫到末尾就又回到開頭循環寫(順序寫,節省了隨機寫磁盤的IO消耗)

7224acb71a77c3f5d97f316dcf60b59d.png

Write Pos是當前記錄的位置,一邊寫一邊后移,寫到第3號文件末尾后就回到0號文件開頭。

Check Point是當前要擦除的位置,也是往后推移並且循環的,擦除記錄前要把記錄更新到數據文件

Write Pos和Check Point之間空着的部分,可以用來記錄新的操作。如果Write Pos追上Check Point,這時候不能再執行新的更新,需要停下來擦掉一些記錄,把Check Point推進一下

當數據庫發生宕機時,數據庫不需要重做所有的日志,因為Check Point之前的頁都已經刷新回磁盤,只需對Check Point后的redo log進行恢復,從而縮短了恢復的時間

當緩沖池不夠用時,根據LRU算法會溢出最近最少使用的頁,若此頁為臟頁,那么需要強制執行Check Point,將臟頁刷新回磁盤。

InnoDB首先將redo log放入到redo log buffer,然后按一定頻率將其刷新到redo log file

下列三種情況下會將redo log buffer刷新到redo log file:

  • Master Thread每一秒將redo log buffer刷新到redo log file

  • 每個事務提交時會將redo log buffer刷新到redo log file

  • 當redo log緩沖池剩余空間小於1/2時,會將redo log buffer刷新到redo log file

兩階段提交

將redo log的寫入拆成了兩個步驟:prepare和commit,這就是兩階段提交

update T set c=c+1 where ID=2;

執行器和InnoDB引擎在執行這個update語句時的內部流程:

  • 執行器先找到引擎取ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果ID=2這一行所在的數據也本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然后再返回
  • 執行器拿到引擎給的行數據,把這個值加上1,得到新的一行數據,再調用引擎接口寫入這行新數據
  • 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log里面,此時redo log處於prepare狀態。然后告知執行器執行完成了,隨時可以提交事務
  • 執行器生成這個操作的binlog,並把binlog寫入磁盤
  • 執行器調用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交狀態,更新完成

04c58afebecb9f83ffc8a6982e55bd5f.png

兩階段提交的原因

兩階段提交,是為了binlog和redolog兩分日志之間的邏輯一致。

redo log 和 binlog 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。

如果不用兩階段提交,那么有兩種可能:

  • 要么就是先寫完 redo log 再寫 binlog,
  • 或者采用反過來的順序。

可能造成的問題:

 update 語句來做例子。

假設當前 ID=2 的行,字段 c 的值是 0,再假設執行 update 語句過程中在寫完第一個日志后,第二個日志還沒有寫完期間發生了 crash崩潰,會出現什么情況呢?

  1. 先寫 redo log 后寫 binlog。

  假設在 redo log 寫完,binlog 還沒有寫完的時候,MySQL 進程異常重啟。

由於,redo log 寫完之后,系統即使崩潰,仍然能夠把數據恢復回來,所以恢復后這一行 c 的值是 1。

但是由於 binlog 沒寫完就 crash 了,這時候 binlog 里面就沒有記錄這個語句。

因此,之后備份日志的時候,存起來的 binlog 里面就沒有這條語句。

然后你會發現,如果需要用這個 binlog 來恢復臨時庫的話,由於這個語句的 binlog 丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行 c 的值就是 0,與原庫的值不同。

  2. 先寫 binlog 后寫 redo log。

  如果在 binlog 寫完之后 crash,由於 redo log 還沒寫,崩潰恢復以后這個事務無效,所以這一行 c 的值是 0。

但是 binlog 里面已經記錄了“把 c 從 0 改成 1”這個日志。

所以,在之后用 binlog 來恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不同。

  如果不使用“兩階段提交”,那么數據庫的狀態就有可能和用它的日志恢復出來的庫的狀態不一致。

 

redolog 寫入策略

redo 占用的空間是一定的,並不會無線增大(可以通過參數設置),寫入的時候是進順序寫的,所以寫入的性能比較高。

當redo log空間滿了之后又會從頭開始以循環的方式進行覆蓋式的寫入。

在寫入redo log的時候也有一個redo log buffer,日志什么時候會刷到磁盤是通過innodb_flush_log_at_trx_commit 參數決定。

innodb_flush_log_at_trx_commit=0 ,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;

innodb_flush_log_at_trx_commit=1,表示每次事務提交時都將 redo log 直接持久化到磁盤;

innodb_flush_log_at_trx_commit=2,表示每次事務提交時都只是把 redo log 寫到 page cache。

除了上面幾種機制外,還有其它兩種情況會把redo log buffer中的日志刷到磁盤。

1、定時處理:有線程會定時(每隔 1 秒)把redo log buffer中的數據刷盤。

2、根據空間處理:redo log buffer 占用到了一定程度( innodb_log_buffer_size 設置的值一半)占,這個時候也會把redo log buffer中的數據刷盤。

redo log&Write-Ahead 的本質

一個事務要修改多張表的多條記錄,多條記錄分布在不同的Page里面,

對應到磁盤的不同位置。如果每個事務都直接寫磁盤,一次事務提交就要多次磁盤的隨機I/O,性能達不到要求。

怎么辦呢?

不寫磁盤,在內存中進行事務提交。然后再通過后台線程,異步地把內存中的數據寫入到磁盤中。

但有個問題:

機器宕機,內存中的數據還沒來得及刷盤,數據就丟失了。

為此,就有了Write-ahead Log的思路:

先在內存中提交事務,然后寫日志(所謂的Write-ahead Log),然后后台任務把內存中的數據異步刷到磁盤。

是順序地在尾部Append,從而也就避免了一個事務發生多次磁盤隨機 I/O 的問題。

明明是先在內存中提交事務,后寫的日志,為什么叫作Write-Ahead呢?

這里的Ahead,其實是指相對於真正的數據刷到磁盤,因為是先寫的日志,后把內存數據刷到磁盤,所以叫Write-Ahead Log。

redo log和binlog日志的不同

binlog是邏輯記錄,格式為row模式

比如update tb_user set age =18 where name ='趙白' ,如果這條語句修改了三條記錄的話,那么binlog記錄就是

 UPDATE `db_test`.`tb_user` WHERE @1=5 @2='趙白' @3=91 @4='1543571201' SET  @1=5 @2='趙白' @3=18 @4='1543571201'
 UPDATE `db_test`.`tb_user` WHERE @1=6 @2='趙白' @3=91 @4='1543571201' SET  @1=5 @2='趙白' @3=18 @4='1543571201'
 UPDATE `db_test`.`tb_user` WHERE @1=7 @2='趙白' @3=91 @4='1543571201' SET  @1=5 @2='趙白' @3=18 @4='1543571201'

redo log是物理記錄

上面的修改語句,在redo log里面記錄得可能就是下面的形式。

 把表空間10、頁號5、偏移量為10處的值更新為18。
 把表空間11、頁號1、偏移量為2處的值更新為18。
 把表空間12、頁號2、偏移量為9處的值更新為18。
  • redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用
  • redo log是物理日志,記錄的是在某個數據上做了什么修改;binlog是邏輯日志,記錄的是這個語句的原始邏輯,比如給ID=2這一行的c字段加1
  • redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的,binlog文件寫到一定大小后會切換到下一個,並不會覆蓋以前的日志

undo log

名稱:回滾日志

作用:保存了事務發生之前的數據的一個版本,可以用於回滾,同時可以提供多版本並發控制下的讀(MVCC),也即非鎖定讀

內容:邏輯格式的日志,根據每行記錄進行記錄。

在執行undo的時候,僅僅是將數據從邏輯上恢復至事務之前的狀態,而不是從物理頁面上操作實現的,這一點是不同於redo log的。

undo log與redo log的不同

undo日志用於記錄事務開始前的狀態,用於事務失敗時的回滾操作;

redo日志記錄事務執行后的狀態,用來恢復未寫入data file的已成功事務更新的數據。

例如:

某一事務的事務序號為T1,其對數據c進行修改,設c的原值是0,修改后的值為1,

那么Undo日志為<T1, c, 0>,

Redo日志為<T1, c, 1>。

redo log 是也屬於引擎層(innodb)的日志,從上面的redo log介紹中我們就已經知道了,

redo log 和undo log的核心是為了保證innodb事務機制中的持久性和原子性,事務提交成功由redo log保證數據持久性,

而事務可以進行回滾從而保證事務操作原子性則是通過undo log 來保證的。

要對事務數據回滾到歷史的數據狀態,所以我們也能猜到undo log是保存的是數據的歷史版本,通過歷史版本讓數據在任何時候都可以回滾到某一個事務開始之前的狀態。

undo log除了進行事務回滾的日志外還有一個作用,就是為數據庫提供MVCC多版本數據讀的功能。

undo log物理文件:

MySQL5.6之前,位於數據文件目錄中,undo表空間位於共享表空間的回滾段中,共享表空間的默認的名稱是ibdata,位於數據文件目錄中。

MySQL5.6之后,undo表空間可以配置成獨立的文件,但是提前需要在配置文件中配置,完成數據庫初始化后生效且不可改變undo log文件的個數

redo、undo、binlog三大log的生成流程與crash崩潰恢復

當我們執行 update T set c=c+1 where ID=1; 的時候大致流程如下:

1、從磁盤讀取到id=1的記錄,放到內存。

2、修改內存中的記錄。

3、記錄undo log 日志。

4、記錄redo log (預提交狀態)

5、記錄binlog

6、提交事務,寫入redo log (commit狀態)

我們根據上面的流程來看,如果在上面的某一個階段數據庫崩潰,如何恢復數據。

1、在第一步、第二步、第三步執行時據庫崩潰:因為這個時候數據還沒有發生任何變化,所以沒有任何影響,不需要做任何操作。

2、在第四步修改內存中的記錄時數據庫崩潰:因為此時事務沒有commit,所以這里要進行數據回滾,所以這里會通過undo log進行數據回滾。

3、第五步寫入binlog時數據庫崩潰:這里和第四步一樣的邏輯,此時事務沒有commit,所以這里要進行數據回滾,會通過undo log進行數據回滾。

binlog不存在事務記錄,那么這種情況事務還未提交成功,redo log 也沒有commit標記,所以會對數據進行回滾。

4、執行第六步事務提交時數據庫崩潰:如果數據庫在這個階段崩潰,那其實事務還是沒有提交成功,但是這里並不能像之前一樣對數據進行回滾,因為在提交事務前,binlog可能成功寫入磁盤了,所以這里要根據兩種情況來做決定。

一種情況,binlog不存在事務記錄,那么這種情況事務還未提交成功,redo log 也沒有commit標記,所以會對數據進行回滾。

一種情況, 是binlog存在數據記錄,而binlog寫入后,那么依賴於binlog的其它擴展業務(比如:從庫已經同步了日志進行數據的變更)數據就已經產生了,如果這里進行數據回滾,那么勢必就會造成主從數據的不一致。此時不能回滾,怎么辦呢。

如果把 innodb_flush_log_at_trx_commit 設置成 1,那么 redo log 在 prepare 階段就要持久化一次,崩潰恢復邏輯是要依賴於 prepare 的 redo log,再加上 binlog 來恢復的。

結合binlog的狀態,進行redo。

如果binlog存在事務記錄:那么就"認為"事務已經提交了,這里可以根據binlog對數據進行重做。其實這個階段發生崩潰了,最終的事務是沒提交成功的,這里應該對數據進行回滾。

binlog主從復制原理

f752c82faa35276f40ebc9e882f10aeb.png

從庫B和主庫A之間維持了一個長連接。主庫A內部有一個線程,專門用於服務從庫B的這個長連接。

一個事務日志同步的完整過程如下:

  • 在從庫B上通過change master命令,設置主庫A的IP、端口、用戶名、密碼,以及要從哪個位置開始請求binlog,這個位置包含文件名和日志偏移量
  • 在從庫B上執行start slave命令,這時從庫會啟動兩個線程,就是圖中的I/O線程和SQL線程。其中I/O線程負責與主庫建立連接
  • 主庫A校驗完用戶名、密碼后,開始按照從庫B傳過來的位置,從本地讀取binlog,發給B
  • 從庫B拿到binlog后,寫到本地文件,稱為中繼日志 relay log
  • SQL線程讀取中繼日志 relay log,解析出日志里的命令,並執行

由於多線程復制方案的引入,SQL線程演化成了多個線程

主從復制不是完全實時地進行同步,而是異步實時。

主從服務之間的延時較大

這中間存在主從服務之間的執行延時,如果主服務器的壓力很大,則可能導致主從服務器延時較大。

應該怎么優化,縮短回放時間?

MySQL使用單線程回放RelayLog。

多線程並行回放RelayLog可以縮短時間。

slave_parallel_type='LOGICAL_CLOCK'

slave_parallel_workers=8

Sharding-JDBC實現讀寫分離

sharding-jdbc實現讀寫分離技術的思路比較簡潔,不支持類似主庫雙寫或多寫這樣的特性,但目前來看,已經可以滿足一般的業務需求了。

提供了一主多從的讀寫分離配置,可獨立使用,也可配合分庫分表使用。

使用Sharding-JDBC配置讀寫分離,優點在於數據源完全有Sharding托管,寫操作自動執行master庫,讀操作自動執行slave庫。不需要程序員在程序中關注這個實現了。

spring.main.allow-bean-definition-overriding=true
spring.shardingsphere.datasource.names=master,slave
spring.shardingsphere.datasource.master.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.master.url=jdbc:mysql://localhost:3306/db_master?characterEncoding=utf-8
spring.shardingsphere.datasource.master.username=
spring.shardingsphere.datasource.master.password=
spring.shardingsphere.datasource.slave.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.slave.url=jdbc:mysql://localhost:3306/db_slave?characterEncoding=utf-8
spring.shardingsphere.datasource.slave.username=
spring.shardingsphere.datasource.slave.password=
spring.shardingsphere.masterslave.load-balance-algorithm-type=round_robin
spring.shardingsphere.masterslave.name=dataSource
spring.shardingsphere.masterslave.master-data-source-name=master
spring.shardingsphere.masterslave.slave-data-source-names=slave
spring.shardingsphere.props.sql.show=true

參數解讀:

load-balance-algorithm-type 用於配置從庫負載均衡算法類型,可選值:ROUND_ROBIN(輪詢),RANDOM(隨機)

props.sql.show=true 在執行SQL時,會打印SQL,並顯示執行庫的名稱

Java API主從配置

分別給ds0和ds1准備從庫:ds01和ds11,分別配置主從同步;讀寫分離配置如下:

		
List<String> slaveDataSourceNames1 = new ArrayList<String>();
slaveDataSourceNames1.add("ds11");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration1 = new MasterSlaveRuleConfiguration("ds1", "ds1",
				slaveDataSourceNames1);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration1);

這樣在執行查詢操作的時候會自動路由到從庫,實現讀寫分離;

MasterSlaveRuleConfiguration

在上面章節介紹分片規則的時候,其中有說到主從規則配置,其目的就是用來實現讀寫分離的,核心配置類:MasterSlaveRuleConfiguration

public final class MasterSlaveRuleConfiguration implements RuleConfiguration {
    private final String name;
    private final String masterDataSourceName;
    private final List<String> slaveDataSourceNames;
    private final LoadBalanceStrategyConfiguration loadBalanceStrategyConfiguration;
}

  • name:配置名稱,當前使用的4.1.0版本,這里必須是主庫的名稱;
  • masterDataSourceName:主庫數據源名稱;
  • slaveDataSourceNames:從庫數據源列表,可以配置一主多從;
  • LoadBalanceStrategyConfiguration:面對多個從庫,讀取的時候會通過負載算法進行選擇;

主從負載算法類:MasterSlaveLoadBalanceAlgorithm,實現類包括:隨機和循環;

  • ROUND_ROBIN:實現類RoundRobinMasterSlaveLoadBalanceAlgorithm
  • RANDOM:實現類RandomMasterSlaveLoadBalanceAlgorithm

問題: 讀寫分離架構中經常出現,那就是讀延遲的問題如何解決?

  剛插入一條數據,然后馬上就要去讀取,這個時候有可能會讀取不到?歸根到底是因為主節點寫入完之后數據是要復制給從節點的,讀不到的原因是復制的時間比較長,也就是說數據還沒復制到從節點,你就已經去從節點讀取了,肯定讀不到。

mysql5.7 的主從復制是多線程了,意味着速度會變快,但是不一定能保證百分百馬上讀取到,這個問題我們可以有兩種方式解決:

  (1)業務層面妥協,是否操作完之后馬上要進行讀取

  (2)對於操作完馬上要讀出來的,且業務上不能妥協的,我們可以對於這類的讀取直接走主庫,當然Sharding-JDBC也是考慮到這個問題的存在,所以給我們提供了一個功能,可以讓用戶在使用的時候指定要不要走主庫進行讀取。

在讀取前使用下面的方式進行設置就可以了:

    public List<UserInfo> getList() {
    
    //默認的查詢
    
        // * 2. 通過DataSource獲取Connection
        Connection connection = dataSource.getConnection();
        // * 3. 定義一條SQL語句.
        // 注意:******* sql語句中 使用的表是 上面代碼中定義的邏輯表 *******
        String sql = "select * from  t_user where user_id between 10 and 20 ";

        // * 4. 通過Connection獲取到PreparedStament.
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // * 5. 執行SQL語句.
        ResultSet resultSet = preparedStatement.executeQuery();

//如果結果集為空,做一次兜底的方案
    
        // 強制路由主庫
        HintManager.getInstance().setMasterRouteOnly();
        
            // * 5. 執行SQL語句.
        ResultSet resultSet = preparedStatement.executeQuery();

        
        return this.list();
    }

通過canal接收二進制日志,通過rocketmq同步到redis+es+hbase

canal [kə'næl],譯意為水道/管道/溝渠.

canal 主要用途是基於 MySQL 數據庫增量日志解析,提供增量數據 訂閱 和 消費。

canal 應該是阿里雲DTS(Data Transfer Service)的開源版本,開源地址:

https://github.com/alibaba/canal。

偽裝的mysqlslave, dump協議

回放線程不一樣, 而是 進行轉發, socket,也可以發送rocketmq

canal的工作原理

  • canal模擬mysql slave的交互協議,偽裝自己為mysql slave,向mysql master發送dump請求;

  • mysql master收到dump請求,開始推送binary log給slave(也就是canal);

  • canal解析binary log對象(原始為byte流);

  • canal將解析后的對象,根據業務場景,分發到比如 MySQL 、RocketMQ 或者 ES 中。

canal使用場景

在很多業務情況下,我們都會在系統中加入redis緩存做查詢優化, 使用es 做全文檢索 ,hbase /mongdb做海量存儲。

在這里插入圖片描述

canal rocketmq redis

如果數據庫數據發生更新,這時候就需要在業務代碼中寫一段同步更新redis的代碼。

這種數據同步的代碼跟業務代碼糅合在一起會不太優雅,能不能把這些數據同步的代碼抽出來形成一個獨立的模塊呢,答案是可以的。

canal即可作為MySQL binlog增量訂閱消費組件+MQ消息隊列將增量數據更新到redis

Redis緩存和MySQL數據一致性解決方案

延時雙刪策略
異步更新緩存(基於訂閱binlog的延遲更新機制)

canal rocketmq es+hbase /mongdb

canal即可作為MySQL binlog增量訂閱消費組件+MQ消息隊列將增量數據更新到es

基於canal的數據一致性的環境搭建

master 為 canal 創建專用賬號,並且授權

登錄mysql輸入以下命令,canal的原理是模擬自己為mysql slave,所以這里一定需要做為mysql slave的相關權限


docker exec -it mysql-master bash

mysql -uroot -p123456


CREATE USER canal IDENTIFIED BY 'canal';  


grant select, replication slave,replication client on *.* to 'canal'@'%' identified by "canal";

flush privileges;



創建數據庫



USE `canal_manager`;

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for canal_adapter_config
-- ----------------------------
DROP TABLE IF EXISTS `canal_adapter_config`;
CREATE TABLE `canal_adapter_config` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `category` varchar(45) NOT NULL,
  `name` varchar(45) NOT NULL,
  `status` varchar(45) DEFAULT NULL,
  `content` text NOT NULL,
  `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

省略.....

啟動cannel服務


rm -rf  /home/docker-compose/canal
cp -rf /vagrant/3G-middleware/canal  /home/docker-compose/


cd /home/docker-compose/

chmod 777 -R  canal

cd canal

docker-compose --compatibility  up  -d



docker-compose  logs -f


docker-compose down

mysql/5.7.31

docker save mysql:5.7.31  -o /vagrant/3G-middleware/mysql.5.7.31.tar

docker save registry.cn-hangzhou.aliyuncs.com/zhengqing/canal-server:v1.1.5        -o /vagrant/3G-middleware/canal-server.v1.1.5.tar

docker save registry.cn-hangzhou.aliyuncs.com/zhengqing/canal-admin:v1.1.5        -o /vagrant/3G-middleware/canal-admin.v1.1.5.tar


訪問canal admin 並且配置實例 / Instance

在瀏覽器上面輸入 hostip:9089 即可進入到管理頁面,如果使用的默認的配置信息,用戶名入”admin”,密碼輸入”123456”即可訪問首頁。

canal  amin 123456

http://cdh2:18089


rocketmq

http://cdh2:19001

通過配置文件對canal進行配置

#################################################
######### 		common argument		#############
#################################################
# tcp bind ip
canal.ip =
# register ip to zookeeper
canal.register.ip =
canal.port = 11111
canal.metrics.pull.port = 11112
# canal instance user/passwd
# canal.user = canal
# canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458

# canal admin config
#canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
# admin auto register
#canal.admin.register.auto = true
#canal.admin.register.cluster =
#canal.admin.register.name =

canal.zkServers =
# flush data to zk
canal.zookeeper.flush.period = 1000
canal.withoutNetty = false
# tcp, kafka, rocketMQ, rabbitMQ
canal.serverMode = tcp

# tcp, kafka, rocketMQ, rabbitMQ
canal.serverMode = rocketMQ
# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000
## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384
## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024 
## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE
canal.instance.memory.rawEntry = true

## detecing config
canal.instance.detecting.enable = false
#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false

# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size =  1024
# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60

# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30

# binlog filter config
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.filter.transaction.entry = false
canal.instance.filter.dml.insert = false
canal.instance.filter.dml.update = false
canal.instance.filter.dml.delete = false

# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED 
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB

# binlog ddl isolation
canal.instance.get.ddl.isolation = false

# parallel parser config
canal.instance.parser.parallel = true
## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
#canal.instance.parser.parallelThreadSize = 16
## disruptor ringbuffer size, must be power of 2
canal.instance.parser.parallelBufferSize = 256

# table meta tsdb info
canal.instance.tsdb.enable = true
canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.dbUsername = canal
canal.instance.tsdb.dbPassword = canal
# dump snapshot interval, default 24 hour
canal.instance.tsdb.snapshot.interval = 24
# purge snapshot expire , default 360 hour(15 days)
canal.instance.tsdb.snapshot.expire = 360

#################################################
######### 		destinations		#############
#################################################
canal.destinations = 
# conf root dir
canal.conf.dir = ../conf
# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5
# set this value to 'true' means that when binlog pos not found, skip to latest.
# WARN: pls keep 'false' in production env, or if you know what you want.
canal.auto.reset.latest.pos.mode = false

canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml

canal.instance.global.mode = manager
canal.instance.global.lazy = false
canal.instance.global.manager.address = ${canal.admin.manager}
#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
canal.instance.global.spring.xml = classpath:spring/file-instance.xml
#canal.instance.global.spring.xml = classpath:spring/default-instance.xml

##################################################
######### 	      MQ Properties      #############
##################################################
# aliyun ak/sk , support rds/mq
canal.aliyun.accessKey =
canal.aliyun.secretKey =
canal.aliyun.uid=

canal.mq.flatMessage = true
canal.mq.canalBatchSize = 50
canal.mq.canalGetTimeout = 100
# Set this value to "cloud", if you want open message trace feature in aliyun.
canal.mq.accessChannel = local

canal.mq.database.hash = true
canal.mq.send.thread.size = 30
canal.mq.build.thread.size = 8

##################################################
######### 		     Kafka 		     #############
##################################################
kafka.bootstrap.servers = 127.0.0.1:6667
kafka.acks = all
kafka.compression.type = none
kafka.batch.size = 16384
kafka.linger.ms = 1
kafka.max.request.size = 1048576
kafka.buffer.memory = 33554432
kafka.max.in.flight.requests.per.connection = 1
kafka.retries = 0

kafka.kerberos.enable = false
kafka.kerberos.krb5.file = "../conf/kerberos/krb5.conf"
kafka.kerberos.jaas.file = "../conf/kerberos/jaas.conf"

##################################################
######### 		    RocketMQ	     #############
##################################################
rocketmq.producer.group = canal_producer
rocketmq.enable.message.trace = false
rocketmq.customized.trace.topic =
rocketmq.namespace =
rocketmq.namesrv.addr = rmqnamesrv:9876
rocketmq.retry.times.when.send.failed = 0
rocketmq.vip.channel.enabled = false
rocketmq.tag = 

##################################################
######### 		    RabbitMQ	     #############
##################################################
rabbitmq.host =
rabbitmq.virtual.host =
rabbitmq.exchange =
rabbitmq.username =
rabbitmq.password =
rabbitmq.deliveryMode =

訪問canal admin 並且配置實例 / Instance

在瀏覽器上面輸入 hostip:9089 即可進入到管理頁面,如果使用的默認的配置信息,用戶名入”admin”,密碼輸入”123456”即可訪問首頁。


canal  amin 123456

http://cdh2:18089


rocketmq

http://cdh2:19001

訪問canal-admin,可以看到自動出現了一個Server,可在此頁面進行Server的配置、修改、啟動、查看log等操作

#################################################
## mysql serverId , v1.0.26+ will autoGen
# canal.instance.mysql.slaveId=0

# enable gtid use true/false
canal.instance.gtidon=false

# position info
canal.instance.master.address=mysql-master:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==

# table regex
canal.instance.filter.regex=.*\\..*
# table black regex
canal.instance.filter.black.regex=
# table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch
# table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch

# mq config
canal.mq.topic=canal_log
# dynamic topic route by schema or table regex
# canal.mq.dynamicTopic=test.user,student\\..*,.*\\..*
canal.mq.partition=0
# hash partition config
canal.mq.partitionsNum=3
canal.mq.partitionHash=test.users:uid,.*\\..*

##################################################
######### 		     MQ 		     #############
##################################################
canal.mq.servers = 192.168.56.122:9876
#canal.mq.servers = rmqnamesrv:9876
canal.mq.retries = 0
canal.mq.batchSize = 16384
canal.mq.maxRequestSize = 1048576
canal.mq.lingerMs = 100
canal.mq.bufferMemory = 33554432
canal.mq.canalBatchSize = 50
canal.mq.canalGetTimeout = 100
canal.mq.flatMessage = true
canal.mq.compressionType = none
canal.mq.acks = all
#canal.mq.properties. =
canal.mq.producerGroup = canal_producer
# Set this value to "cloud", if you want open message trace feature in aliyun.
canal.mq.accessChannel = local
# aliyun mq namespace
#canal.mq.namespace =

去Instance列表新增Instance,可選擇載入模版進行修改,可參考上文中的canal相關配置文件修改

點擊側邊欄的Instance管理,選擇新建 Instance,選擇那個唯一的主機,再點擊載入模板,修改下面的一些參數:

實例名稱隨便填一個就行。

#################################################
######### 		common argument		#############
#################################################
# tcp bind ip
canal.ip =
# register ip to zookeeper
canal.register.ip =
canal.port = 11111
canal.metrics.pull.port = 11112
# canal instance user/passwd
# canal.user = canal
# canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458

# canal admin config
#canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
# admin auto register
#canal.admin.register.auto = true
#canal.admin.register.cluster =
#canal.admin.register.name =

canal.zkServers =
# flush data to zk
canal.zookeeper.flush.period = 1000
canal.withoutNetty = false
# tcp, kafka, rocketMQ, rabbitMQ
# canal.serverMode = tcp
canal.serverMode = RocketMQ
# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000
## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384
## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024 
## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE
canal.instance.memory.rawEntry = true

## detecing config
canal.instance.detecting.enable = false
#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false

# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size =  1024
# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60

# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30

# binlog filter config
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.filter.transaction.entry = false
canal.instance.filter.dml.insert = false
canal.instance.filter.dml.update = false
canal.instance.filter.dml.delete = false

# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED 
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB

# binlog ddl isolation
canal.instance.get.ddl.isolation = false

# parallel parser config
canal.instance.parser.parallel = true
## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
#canal.instance.parser.parallelThreadSize = 16
## disruptor ringbuffer size, must be power of 2
canal.instance.parser.parallelBufferSize = 256

# table meta tsdb info
canal.instance.tsdb.enable = true
canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.dbUsername = canal
canal.instance.tsdb.dbPassword = canal
# dump snapshot interval, default 24 hour
canal.instance.tsdb.snapshot.interval = 24
# purge snapshot expire , default 360 hour(15 days)
canal.instance.tsdb.snapshot.expire = 360


rocketmq.producer.group = canal_producers
rocketmq.enable.message.trace = false
rocketmq.customized.trace.topic =
rocketmq.namespace =
#rocketmq.namesrv.addr = 127.0.0.1:9876

#rocketmq.namesrv.addr = 192.168.56.122:9876
rocketmq.namesrv.addr = rmqnamesrv:9876

rocketmq.retry.times.when.send.failed = 0
rocketmq.vip.channel.enabled = false
rocketmq.tag = 



創建好的新實例默認是停止狀態,將其啟動。

從master 查看從節點

select * from information_schema.processlist as p where p.command = 'Binlog Dump';


mysql> select * from information_schema.processlist as p where p.command = 'Binlog Dump';
+----+-------+----------------------------------------+------+-------------+-------+---------------------------------------------------------------+------+
| ID | USER  | HOST                                   | DB   | COMMAND     | TIME  | STATE                                                         | INFO |
+----+-------+----------------------------------------+------+-------------+-------+---------------------------------------------------------------+------+
| 80 | canal | canal-server.mysql-canal-network:44699 | NULL | Binlog Dump |   315 | Master has sent all binlog to slave; waiting for more updates | NULL |
|  3 | slave | mysql-slave.mysql-canal-network:43428  | NULL | Binlog Dump | 18133 | Master has sent all binlog to slave; waiting for more updates | NULL |
+----+-------+----------------------------------------+------+-------------+-------+---------------------------------------------------------------+------+
2 rows in set (0.01 sec)

基於canal+rocketmq的數據一致性的實操

SnowFlake時鍾回撥問題

SnowFlake很好,分布式、去中心化、無第三方依賴。

但它並不是完美的,由於SnowFlake強依賴時間戳,所以時間的變動會造成SnowFlake的算法產生錯誤。

時鍾回撥:最常見的問題就是時鍾回撥導致的ID重復問題,在SnowFlake算法中並沒有什么有效的解法,僅是拋出異常。時鍾回撥涉及兩種情況①實例停機→時鍾回撥→實例重啟→計算ID ②實例運行中→時鍾回撥→計算ID

手動配置:另一個就是workerId(機器ID)是需要部署時手動配置,而workerId又不能重復。幾台實例還好,一旦實例達到一定量級,管理workerId將是一個復雜的操作。

ntp導致的時鍾回撥

我們的服務器時間校准一般是通過ntp進程去校准的。但由於校准這個動作,會導致時鍾跳躍變化的現象。
而這種情況里面,往往回撥最能引起我們的困擾,回撥如下所示:

img

時鍾回撥改進避免

ID生成器一旦不可用,可能造成所有數據庫相關新增業務都不可用,影響太大。所以時鍾回撥的問題必須解決。

造成時鍾回撥的原因多種多樣,可能是閏秒回撥,可能是NTP同步,還可能是服務器時間手動調整。總之就是時間回到了過去。針對回退時間的多少可以進行不同的策略改進。

一般有以下幾種方案:

  1. 少量服務器部署ID生成器實例,關閉NTP服務器,嚴格管理服務器。這種方案不需要從代碼層面解決,完全人治。
  2. 針對回退時間斷的情況,如閏秒回撥僅回撥了1s,可以在代碼層面通過判斷暫停一定時間內的ID生成器使用。雖然少了幾秒鍾可用時間,但時鍾正常后,業務即可恢復正常。
if (refusedSeconds <= 5) {
    try {
    //時間偏差大小小於5ms,則等待兩倍時間
		wait(refusedSeconds << 1);//wait
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
    currentSecond = getCurrentSecond();
}else {//時鍾回撥較大
    //用其他策略修復時鍾問題
}

實例啟動后,改用內存生成時間。

該方案為baidu開源的UidGenerator使用的方案,由於實例啟動后,時間不再從服務器獲取,所以不管服務器時鍾如何回撥,都影響不了SnowFlake的執行。

如下代碼中lastSecond變量是一個AtomicLong類型,用以代替系統時間

 List<Long> uidList = uidProvider.provide(lastSecond.incrementAndGet());

以上2和3都是解決時鍾實例運行中→時鍾回撥→計算ID的情況。

實例停機→時鍾回撥→實例重啟→計算ID的情況,可以通過實例啟動的時候,采用未使用過的workerId來完成。

只要workerId和此前生成ID的workerId不一致,即便時間戳有誤,所生成的ID也不會重復。

UidGenerator采取的就是這種方案,但這種方案又必須依賴一個存儲中心,不管是redis、mysql、zookeeper都可以,但必須存儲着此前使用過的workerId,不能重復。

尤其是在分布式部署Id生成器的情況下,更要注意用一個存儲中心解決此問題。

UidGenerator代碼可上Githubhttps://github.com/zer0Black/uid-generator查看

本文持續優化中

有問題,大家可以直接找尼恩, 聯系方式請參見 瘋狂創客圈 總目錄。

說明:本文會以pdf格式持續更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼雲

參考文檔:

https://www.jianshu.com/p/d3c1ee5237e5
https://www.cnblogs.com/zer0black/p/12323541.html?ivk_sa=1024320u
https://shardingsphere.apache.org/document/current/cn/features/sharding/use-norms/sql/
https://blog.csdn.net/free_ant/article/details/111461606
https://www.jianshu.com/p/46b42f7f593c
https://blog.csdn.net/yangguosb/article/details/78772730
https://blog.csdn.net/youanyyou/article/details/121005680
https://www.cnblogs.com/huanshilang/p/12055296.html
https://www.cnblogs.com/haima/p/14341903.html
https://www.cnblogs.com/wilburxu/p/7229642.html
https://zhuanlan.zhihu.com/p/213770128


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM