Sharding-JDBC最早是當當網內部使用的一款分庫分表框架,到2017年的時候才開始對外開源,這幾年在大量社區貢獻者的不斷迭代下,功能也逐漸完善,現已更名為ShardingSphere,2020年4?16日正式成為 Apache 軟件基金會的頂級項目。
ShardingSphere-Jdbc定位為輕量級Java框架,在Java的Jdbc層提供的額外服務。它使用客戶端直連數據庫,以jar包形式提供服務,可理解為增強版的Jdbc驅動,完全兼容Jdbc和各種ORM框架。
隨著版本的不斷更迭 ShardingSphere 的核心功能也變得多元化起來。
從最開始 Sharding-JDBC 1.0 版本只有數據分片,到 Sharding-JDBC 2.0 版本開始支持數據庫治理(注冊中心、配置中心等等),再到 Sharding-JDBC 3.0版本又加分布式事務 (支持 Atomikos、Narayana、Bitronix、Seata),如今已經迭代到了 Sharding-JDBC 4.0 版本。
現在的 ShardingSphere 不單單是指某個框架而是一個生態圈,這個生態圈Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar這三款開源的分布式數據庫中間件解決方案所構成。
ShardingSphere 的前身就是 Sharding-JDBC,所以它是整個框架中最為經典、成熟的組件,先從 Sharding-JDBC 框架入手學習分庫分表。
1核心概念
分庫分表
分庫,顯而易見,就是一個數據庫分成多個數據庫,部署到不同機器。
分表,就是一個數據庫表分成多個表。
分片
一般在提到分庫分表的時候,大多是以水平切分模式(水平分庫、分表)為基礎來說的,數據分片將原本一張數據量較大的表例如 t_order 拆分生成數個表結構完全一致的小數據量表 t_order_0、t_order_1、···、t_order_n,每張表只存儲原大表中的一部分數據,當執行一條SQL時會通過分庫策略、分片策略將數據分散到不同的數據庫、表內。
數據節點
數據節點是分庫分表中一個不可再分的最小數據單元(表),它由數據源名稱和數據表組成,例如上圖中 order_db_1.t_order_0、order_db_2.t_order_1 就表示一個數據節點。
邏輯表
邏輯表是指一組具有相同邏輯和數據結構表的總稱。
比如將訂單表 t_order 拆分成 t_order_0 ··· t_order_9 等 10張表。
此時會發現分庫分表以后數據庫中已不在有 t_order 這張表,取而代之的是 t_order_n,但在代碼中寫 SQL 依然按 t_order 來寫。此時 t_order 就是這些拆分表的邏輯表。
真實表
真實表也就是上邊提到的 t_order_n 數據庫中真實存在的物理表。
分片鍵
用于分片的數據庫字段。將 t_order 表分片以后,當執行一條SQL時,通過對字段 order_id 取模的方式來決定,這條數據該在哪個數據庫中的哪個表中執行,此時 order_id 字段就是 t_order 表的分片健。
這樣以來同一個訂單的相關數據就會存在同一個數據庫表中,大幅提升數據檢索的性能,不僅如此 sharding-jdbc 還支持根據多個字段作為分片健進行分片。
分片算法
上邊提到可以用分片健取模的規則分片,但這只是比較簡單的一種,在實際開發中還希望用 >=、<=、>、<、BETWEEN 和 IN 等條件作為分片規則,自定義分片邏輯,這時就需要用到分片策略與分片算法。
從執行 SQL 的角度來看,分庫分表可以看作是一種路由機制,把 SQL 語句路由到期望的數據庫或數據表中并獲取數據,分片算法可以理解成一種路由規則。
咱們先捋一下它們之間的關系,分片策略只是抽象出的概念,它是由分片算法和分片健組合而成,分片算法做具體的數據分片邏輯。
分庫、分表的分片策略配置是相對獨立的,可以各自使用不同的策略與算法,每種策略中可以是多個分片算法的組合,每個分片算法可以對多個分片健做邏輯判斷。
分片算法和分片策略的關系
注意:sharding-jdbc 并沒有直接提供分片算法的實現,需要開發者根據業務自行實現。
sharding-jdbc 提供了4種分片算法。
1、精確分片算法
精確分片算法(PreciseShardingAlgorithm)用于單個字段作為分片鍵,SQL中有 = 與 IN 等條件的分片,需要在標準分片策略(StandardShardingStrategy )下使用。
2、范圍分片算法
范圍分片算法(RangeShardingAlgorithm)用于單個字段作為分片鍵,SQL中有 BETWEEN AND、>、<、>=、<= ?等條件的分片,需要在標準分片策略(StandardShardingStrategy )下使用。
3、復合分片算法
復合分片算法(ComplexKeysShardingAlgorithm)用于多個字段作為分片鍵的分片操作,同時獲取到多個分片健的值,根據多個字段處理業務邏輯。需要在復合分片策略(ComplexShardingStrategy )下使用。
4、Hint分片算法
Hint分片算法(HintShardingAlgorithm)稍有不同,上邊的算法中都是解析SQL 語句提取分片鍵,并設置分片策略進行分片。但有些時候并沒有使用任何的分片鍵和分片策略,可還想將 SQL 路由到目標數據庫和表,就需要通過手動干預指定SQL的目標數據庫和表信息,這也叫強制路由。
分片策略
上邊講分片算法的時候已經說過,分片策略是一種抽象的概念,實際分片操作的是由分片算法和分片健來完成的。
1、標準分片策略
標準分片策略適用于單分片鍵,此策略支持 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 兩個分片算法。
其中 PreciseShardingAlgorithm 是必選的,用于處理 = 和 IN 的分片。RangeShardingAlgorithm 是可選的,用于處理BETWEEN AND, >, <,>=,<= 條件分片,如果不配置RangeShardingAlgorithm,SQL中的條件等將按照全庫路由處理。
2、復合分片策略
復合分片策略,同樣支持對 SQL語句中的 =,>, <, >=, <=,IN和 BETWEEN AND 的分片操作。不同的是它支持多分片鍵,具體分配片細節完全由應用開發者實現。
3、行表達式分片策略
行表達式分片策略,支持對 SQL語句中的 = 和 IN 的分片操作,但只支持單分片鍵。這種策略通常用于簡單的分片,不需要自定義分片算法,可以直接在配置文件中接著寫規則。
t_order_$->{t_order_id % 4} 代表 t_order 對其字段 t_order_id取模,拆分成4張表,而表名分別是t_order_0 到 t_order_3。
4、Hint分片策略
Hint分片策略,對應上邊的Hint分片算法,通過指定分片健而非從 SQL中提取分片健的方式進行分片的策略。
分布式主鍵
數據分?后,不同數據節點?成全局唯?主鍵是?常棘?的問題,同?個邏輯表(t_order)內的不同真實表(t_order_n)之間的?增鍵由于?法互相感知而產?重復主鍵。
盡管可通過設置?增主鍵 初始值 和 步? 的?式避免ID碰撞,但這樣會使維護成本加大,乏完整性和可擴展性。如果后去需要增加分片表的數量,要逐一修改分片表的步長,運維成本非常高,所以不建議這種方式。
為了讓上手更加簡單,ApacheShardingSphere 內置了UUID、SNOWFLAKE 兩種分布式主鍵?成器,默認使用雪花算法(snowflake)?成64bit的?整型數據。不僅如此它還抽離出分布式主鍵?成器的接口,方便實現自定義的自增主鍵生成算法。
廣播表
廣播表:存在于所有的分片數據源中的表,表結構和表中的數據在每個數據庫中均完全一致。一般是為字典表或者配置表 t_config,某個表一旦被配置為廣播表,只要修改某個數據庫的廣播表,所有數據源中廣播表的數據都會跟著同步。
綁定表
綁定表:那些分片規則一致的主表和子表。比如:t_order 訂單表和 t_order_item 訂單服務項目表,都是按 order_id 字段分片,因此兩張表互為綁定表關系。
那綁定表存在的意義是啥呢?
通常在業務中都會使用 t_order 和 t_order_item 等表進行多表聯合查詢,但由于分庫分表以后這些表被拆分成N多個子表。如果不配置綁定表關系,會出現笛卡爾積關聯查詢,將產生如下四條SQL。
SELECT*FROMt_order_0oJOINt_order_item_0iONo.order_id=i.order_id SELECT*FROMt_order_0oJOINt_order_item_1iONo.order_id=i.order_id SELECT*FROMt_order_1oJOINt_order_item_0iONo.order_id=i.order_id SELECT*FROMt_order_1oJOINt_order_item_1iONo.order_id=i.order_id
而配置綁定表關系后再進行關聯查詢時,只要對應表分片規則一致產生的數據就會落到同一個庫中,那么只需 t_order_0 和 t_order_item_0 表關聯即可。
SELECT*FROMt_order_0oJOINt_order_item_0iONo.order_id=i.order_id SELECT*FROMt_order_1oJOINt_order_item_1iONo.order_id=i.order_id
注意:在關聯查詢時 t_order 它作為整個聯合查詢的主表。所有相關的路由計算都只使用主表的策略,t_order_item 表的分片相關的計算也會使用 t_order 的條件,所以要保證綁定表之間的分片鍵要完全相同。
2MySQL主從復制
docker配置mysql主從復制
創建主服務器所需目錄
mkdir-p/usr/local/mysqlData/master/cnf mkdir-p/usr/local/mysqlData/master/data
定義主服務器配置文件
vim /usr/local/mysqlData/master/cnf/mysql.cnf [mysqld] ## 設置server_id,注意要唯一 server-id=1 ## 開啟binlog log-bin=mysql-bin ## binlog緩存 binlog_cache_size=1M ## binlog格式(mixed、statement、row,默認格式是statement) binlog_format=mixed
創建并啟動mysql主服務
dockerrun-itd-p3306:3306--namemaster-v/usr/local/mysqlData/master/cnf:/etc/mysql/conf.d-v/usr/local/mysqlData/master/data:/var/lib/mysql-eMYSQL_ROOT_PASSWORD=123456mysql:5.7
添加復制master數據的用戶reader,供從服務器使用
[root@aliyun/]#dockerps CONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES 6af1df686fffmysql:5.7"docker-entrypoint..."5secondsagoUp4seconds0.0.0.0:3306->3306/tcp,33060/tcpmaster [root@aliyun/]#dockerexec-itmaster/bin/bash root@41d795785db1:/#mysql-uroot-p123456 mysql>GRANTREPLICATIONSLAVEON*.*to'reader'@'%'identifiedby'reader'; QueryOK,0rowsaffected,1warning(0.00sec) mysql>FLUSHPRIVILEGES; QueryOK,0rowsaffected(0.00sec)
創建從服務器所需目錄,編輯配置文件
mkdir /usr/local/mysqlData/slave/cnf -p mkdir /usr/local/mysqlData/slave/cnf -p vim /usr/local/mysqlData/slave/cnf/mysql.cnf [mysqld] ## 設置server_id,注意要唯一 server-id=2 ## 開啟binlog,以備Slave作為其它Slave的Master時使用 log-bin=mysql-slave-bin ## relay_log配置中繼日志 relay_log=edu-mysql-relay-bin ## 如果需要同步函數或者存儲過程 log_bin_trust_function_creators=true ## binlog緩存 binlog_cache_size=1M ## binlog格式(mixed、statement、row,默認格式是statement) binlog_format=mixed ## 跳過主從復制中遇到的所有錯誤或指定類型的錯誤,避免slave端復制中斷 ## 如:1062錯誤是指一些主鍵重復,1032錯誤是因為主從數據庫數據不一致 slave_skip_errors=1062
創建并運行mysql從服務器
dockerrun-itd-p3307:3306--nameslaver-v/usr/local/mysqlData/slave/cnf:/etc/mysql/conf.d-v/usr/local/mysqlData/slave/data:/var/lib/mysql-eMYSQL_ROOT_PASSWORD=123456mysql:5.7
在從服務器上配置連接主服務器的信息
首先主服務器上查看master_log_file、master_log_pos兩個參數,然后切換到從服務器上進行主服務器的連接信息的設置
主服務上執行:
root@6af1df686fff:/#mysql-uroot-p123456 mysql>showmasterstatus; +------------------+----------+--------------+------------------+-------------------+ |File|Position|Binlog_Do_DB|Binlog_Ignore_DB|Executed_Gtid_Set| +------------------+----------+--------------+------------------+-------------------+ |mysql-bin.000003|591|||| +------------------+----------+--------------+------------------+-------------------+ 1rowinset(0.00sec)
docker查看主服務器容器的ip地址
[root@aliyun/]#dockerinspect--format='{{.NetworkSettings.IPAddress}}'master 172.17.0.2
從服務器上執行:
[root@aliyun/]#dockerexec-itslaver/bin/bash root@fe8b6fc2f1ca:/#mysql-uroot-p123456 mysql>changemastertomaster_host='172.17.0.2',master_user='reader',master_password='reader',master_log_file='mysql-bin.000003',master_log_pos=591;
從服務器啟動I/O 線程和SQL線程
mysql> start slave; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> show slave statusG *************************** 1. row *************************** Slave_IO_State: Waiting for master to send event Master_Host: 172.17.0.2 Master_User: reader Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000003 Read_Master_Log_Pos: 591 Relay_Log_File: edu-mysql-relay-bin.000002 Relay_Log_Pos: 320 Relay_Master_Log_File: mysql-bin.000003 Slave_IO_Running: Yes Slave_SQL_Running: Yes
Slave_IO_Running: Yes,Slave_SQL_Running: Yes 即表示啟動成功
binlog和redo log回顧
redo log(重做日志)
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
MySQL里常說的WAL技術,全稱是Write Ahead Log,即當事務提交時,先寫redo log,再修改頁。也就是說,當有一條記錄需要更新的時候,InnoDB會先把記錄寫到redo log里面,并更新Buffer Pool的page,這個時候更新操作就算完成了
Buffer Pool是物理頁的緩存,對InnoDB的任何修改操作都會首先在Buffer Pool的page上進行,然后這樣的頁將被標記為臟頁并被放到專門的Flush List上,后續將由專門的刷臟線程階段性的將這些頁面寫入磁盤
InnoDB的redo log是固定大小的,比如可以配置為一組4個文件,每個文件的大小是1GB,循環使用,從頭開始寫,寫到末尾就又回到開頭循環寫(順序寫,節省了隨機寫磁盤的IO消耗)
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,將臟頁刷新回磁盤
binlog(歸檔日志)
MySQL整體來看就有兩塊:一塊是Server層,主要做的是MySQL功能層面的事情;還有一塊是引擎層,負責存儲相關的具體事宜。redo log是InnoDB引擎特有的日志,而Server層也有自己的日志,稱為binlog
binlog記錄了對MySQL數據庫執行更改的所有操作,不包括SELECT和SHOW這類操作,主要作用是用于數據庫的主從復制及數據的增量恢復
使用mysqldump備份時,只是對一段時間的數據進行全備,但是如果備份后突然發現數據庫服務器故障,這個時候就要用到binlog的日志了
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語句選擇日志保存方式
redo log和binlog日志的不同
redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用
redo log是物理日志,記錄的是在某個數據也上做了什么修改;binlog是邏輯日志,記錄的是這個語句的原始邏輯,比如給ID=2這一行的c字段加1
redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的,binlog文件寫到一定大小后會切換到下一個,并不會覆蓋以前的日志
兩階段提交
createtableT(IDintprimarykey,cint); updateTsetc=c+1whereID=2;
執行器和InnoDB引擎在執行這個update語句時的內部流程:
執行器先找到引擎取ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果ID=2這一行所在的數據也本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然后再返回
執行器拿到引擎給的行數據,把這個值加上1,得到新的一行數據,再調用引擎接口寫入這行新數據
引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log里面,此時redo log處于prepare狀態。然后告知執行器執行完成了,隨時可以提交事務
執行器生成這個操作的binlog,并把binlog寫入磁盤
執行器調用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交狀態,更新完成
update語句的執行流程圖如下,圖中淺色框表示在InnoDB內部執行的,深色框表示是在執行器中執行的
將redo log的寫入拆成了兩個步驟:prepare和commit,這就是兩階段提交
MySQL主從復制原理
從庫B和主庫A之間維持了一個長連接。主庫A內部有一個線程,專門用于服務從庫B的這個長連接。一個事務日志同步的完整過程如下:
在從庫B上通過change master命令,設置主庫A的IP、端口、用戶名、密碼,以及要從哪個位置開始請求binlog,這個位置包含文件名和日志偏移量
在從庫B上執行start slave命令,這時從庫會啟動兩個線程,就是圖中的I/O線程和SQL線程。其中I/O線程負責與主庫建立連接
主庫A校驗完用戶名、密碼后,開始按照從庫B傳過來的位置,從本地讀取binlog,發給B
從庫B拿到binlog后,寫到本地文件,稱為中繼日志
SQL線程讀取中繼日志,解析出日志里的命令,并執行
由于多線程復制方案的引入,SQL線程演化成了多個線程。
主從復制不是完全實時地進行同步,而是異步實時。這中間存在主從服務之間的執行延時,如果主服務器的壓力很大,則可能導致主從服務器延時較大。
3為什么需要分庫分表?
首先要明確一個問題,單一的數據庫是否能夠滿足公司目前的線上業務需求,比如用戶表,可能有幾千萬,甚至上億的數據,只是說可能,如果有這么多用戶,那必然是大公司了,那么這個時候,如果不分表也不分庫的話,那么數據了上來的時候,稍微一個不注意,MySQL單機磁盤容量會撐爆,但是如果拆成多個數據庫,磁盤使用率大大降低。
這樣就把磁盤使用率降低,這是通過硬件的形式解決問題,如果數據量是巨大的,這時候,SQL 如果沒有命中索引,那么就會導致一個情況,查這個表的SQL語句直接把數據庫給干崩了。
即使SQL命中了索引,如果表的數據量 超過一千萬的話, 查詢也是會明顯變慢的。這是因為索引一般是B+樹結構,數據千萬級別的話,B+樹的高度會增高,查詢自然就變慢了,當然,這是題外話了。
4MySQL分庫分表原理
分庫分表
水平拆分:同一個表的數據拆到不同的庫不同的表中。可以根據時間、地區或某個業務鍵維度,也可以通過hash進行拆分,最后通過路由訪問到具體的數據。拆分后的每個表結構保持一致
垂直拆分:就是把一個有很多字段的表給拆分成多個表,或者是多個庫上去。每個庫表的結構都不一樣,每個庫表都包含部分字段。一般來說,可以根據業務維度進行拆分,如訂單表可以拆分為訂單、訂單支持、訂單地址、訂單商品、訂單擴展等表;也可以,根據數據冷熱程度拆分,20%的熱點字段拆到一個表,80%的冷字段拆到另外一個表
不停機分庫分表數據遷移
一般數據庫的拆分也是有一個過程的,一開始是單表,后面慢慢拆成多表。那么就看下如何平滑的從MySQL單表過度到MySQL的分庫分表架構
利用MySQL+Canal做增量數據同步,利用分庫分表中間件,將數據路由到對應的新表中
利用分庫分表中間件,全量數據導入到對應的新表中
通過單表數據和分庫分表數據兩兩比較,更新不匹配的數據到新表中
數據穩定后,將單表的配置切換到分庫分表配置上
5分庫分表方案
分庫分表方案,不外乎就兩種,一種是垂直切分,一種是水平切分。
但是總有做開發的小伙伴不知道這垂直切分和水平切分到底是什么樣的,為什么垂直切分,為什么水平切分,什么時候應該選擇垂直切分,什么時候應該選擇水平切分。
有人是這么說的,垂直切分是根據業務來拆分數據庫,同一類業務的數據表拆分到一個獨立的數據庫,另一類的數據表拆分到其他數據庫。
有些人不理解這個,實際上垂直切分也是有劃分的,上面描述的是垂直切分數據庫,可能容易讓很多人不太理解,但是如果是垂直切分表,那么肯定百分之90的人都能理解。
垂直切分
有一張Order表,表中有諸多記錄,比如設計這么一張簡單的表。
id | order_id | order_date | order_type | order_state |
---|---|---|---|---|
1 | cd96cff0356e483caae6b2ff4e878fd6 | 2022-06-18 1311 | 支付寶 | 1 |
2 | e2496f9e22ce4391806b18480440526a | 2022-06-18 1433 | 微信 | 2 |
3 | 9e7ab5a1915c4570a9eaaaa3c01f79c1 | 2022-06-18 1544 | 現金 | 2 |
以上是簡化版Order表,如果想要垂直切分,那么應該怎么處理?
直接拆分成2個表,這時候就直接就一分為2 ,咔的一下拆分成兩個表
Order1
id | order_id | order_date |
---|---|---|
1 | cd96cff0356e483caae6b2ff4e878fd6 | 2022-06-18 1311 |
2 | e2496f9e22ce4391806b18480440526a | 2022-06-18 1433 |
3 | 9e7ab5a1915c4570a9eaaaa3c01f79c1 | 2022-06-18 1544 |
Order2
id | order_type | order_state |
---|---|---|
1 | 支付寶 | 1 |
2 | 微信 | 2 |
3 | 現金 | 2 |
這時候主鍵ID保持的時一致的,而這個操作,就是垂直拆分,分表的操作。
既然說了垂直拆分,那么必然就有水平拆分,
什么是水平拆分呢?
實際上水平拆分的話,那真的是只有一句話。
水平切分
按照數據來拆分
水平拆分數據庫:將一張表的數據 ( 按照數據行) 分到多個不同的數據庫。每個庫的表結構相同,每個庫都只有這張表的部分數據,當單表的數據量過大,如果繼續使用水平分庫,那么數據庫的實例 就會不斷增加,不利于系統的運維。這時候就要采用水平分表。
水平拆分表:將一張表的數據 ( 按照數據行) ,分配到同一個數據庫的多張表中,每個表都只有一部分數據。
來看看Order表進行水平拆分的話,是什么樣子的。
Order1
id | order_id | order_date | order_type | order_state |
---|---|---|---|---|
1 | cd96cff0356e483caae6b2ff4e878fd6 | 2022-06-18 1311 | 支付寶 | 1 |
2 | e2496f9e22ce4391806b18480440526a | 2022-06-18 1433 | 微信 | 2 |
Order2
id | order_id | order_date | order_type | order_state |
---|---|---|---|---|
3 | 9e7ab5a1915c4570a9eaaaa3c01f79c1 | 2022-06-18 1544 | 現金 | 2 |
實際上就是水平的把表數據給分成了2份,這么看起來是不是就很好理解了。
6分庫分表帶來的問題
事務問題
首先,分庫分表最大的隱患就是,事務的一致性, 當需要更新的內容同時分布在不同的庫時,不可避免的會產生跨庫的事務問題。
原來在一個數據庫操 作,本地事務就可以進行控制,分庫之后 一個請求可能要訪問多個數據庫,如何保證事務的一致性,目前還沒有簡單的解決方案。
無法聯表的問題
還有一個就是,沒有辦法進行聯表查詢了,因為,原來在一個庫中的一些表,被分散到多個庫,并且這些數據庫可能還不在一臺服務器,無法關聯查詢,所以相對應的業務代碼可能就比較多了。
分頁問題
分庫并行查詢時,如果用到了分頁,每個庫返回的結果集本身是無序的,只有將多個庫中的數據先查出來,然后再根據排序字段在內存中進行排序,如果查詢結果過大也是十分消耗資源的。
分庫分表的技術
目前比較流行的就兩種,一種是MyCat,另外一種則是Sharding-Jdbc,都是可以進行分庫的,
MyCat是一個數據庫中間件,Sharding-Jdbc是以 jar 包提供服務的jdbc框架。
Mycat和Sharding-jdbc 實現原理也是不同:
Mycat的原理中最重要的一個動詞是“攔截”,它攔截了用戶發送過來的SQL語句,首先對SQL語句做了一些特定的分析:如分庫分表分析、路由分析、讀寫分離分析、緩存分析等,然后將此SQL發往后端的真實數據庫,并將返回的結果做適當的處理,最終再返回給用戶。
而Sharding-JDBC的原理是接受到一條SQL語句時,會陸續執行SQL解析 => 查詢優化 => SQL路由 => SQL改寫 => SQL執行 => 結果歸并 ,最終返回執行結果。
小結
垂直分表:將一張寬表(字段很多的表),按照字段的訪問頻次進行拆分,就是按照表單結構進行 拆。
垂直分庫:根據不同的業務,將表進行分類,拆分到不同的數據庫。這些庫可以部署在不同的服 務器,分攤訪問壓力。
水平分庫:將一張表的數據 ( 按照數據行) 分到多個不同的數據庫。每個庫的表結構相同
水平分表:將一張表的數據 ( 按照數據行) ,分配到同一個數據庫的多張表中,每個表都只有一部 分數據。
7Sharding-Jdbc實現讀寫分離
搭建mysql主從服務
根據上面docker配置mysql主從復制部分搭建。
主服務創建庫表
CREATEDATABASEsharding-jdbc-db; CREATETABLE`t_user`( `id`int(11)NOTNULLAUTO_INCREMENT, `nickname`varchar(100)DEFAULTNULL, `password`varchar(100)DEFAULTNULL, `sex`int(11)DEFAULTNULL, `birthday`varchar(50)DEFAULTNULL, PRIMARYKEY(`id`) )ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
創建SpringBoot工程,引入依賴
4.0.0 org.springframework.boot spring-boot-starter-parent 2.6.8 com.itjing springboot-sharding-jdbc 0.0.1-SNAPSHOT springboot-sharding-jdbc springboot-sharding-jdbc 1.8 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2 mysql mysql-connector-java 5.1.47 com.alibaba druid-spring-boot-starter 1.1.17 org.apache.shardingsphere sharding-jdbc-spring-boot-starter 4.0.0-RC1 org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
配置文件
spring: main: allow-bean-definition-overriding:true shardingsphere: datasource: ds: maxPoolSize:100 #master-ds1數據庫連接信息 ds1: driver-class-name:com.mysql.jdbc.Driver maxPoolSize:100 minPoolSize:5 password:123456 type:com.alibaba.druid.pool.DruidDataSource url:jdbc//192.168.56.111:3306/sharding-jdbc-db?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai username:root #slave-ds2數據庫連接信息 ds2: driver-class-name:com.mysql.jdbc.Driver maxPoolSize:100 minPoolSize:5 password:123456 type:com.alibaba.druid.pool.DruidDataSource url:jdbc//192.168.56.111:3307/sharding-jdbc-db?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai username:root #slave-ds3數據庫連接信息 ds3: driver-class-name:com.mysql.jdbc.Driver minPoolSize:5 password:123456 type:com.alibaba.druid.pool.DruidDataSource url:jdbc//192.168.56.111:3307/sharding-jdbc-db?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai username:root #配置數據源 names:ds1,ds2,ds3 masterslave: #配置slave節點的負載均衡均衡策略,采用輪詢機制 load-balance-algorithm-type:round_robin #配置主庫master,負責數據的寫入 master-data-source-name:ds1 #配置主從名稱 name:ms #配置從庫slave節點 slave-data-source-names:ds2,ds3 #顯示sql props: sql: show:true #配置默認數據源ds1默認數據源,主要用于寫 sharding: default-data-source-name:ds1 #整合mybatis的配置 mybatis: type-aliases-package:com.itjing.sharding.entity
定義Controller、Mapper、Entity
packagecom.itjing.sharding.entity; importlombok.Data; /** *@authorlijing *@date2022年06月19日10:45 *@description */ @Data publicclassUser{ privateIntegerid; privateStringnickname; privateStringpassword; privateIntegersex; privateStringbirthday; } packagecom.itjing.sharding.controller; importcom.itjing.sharding.entity.User; importcom.itjing.sharding.mapper.UserMapper; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.web.bind.annotation.GetMapping; importorg.springframework.web.bind.annotation.PostMapping; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RestController; importjava.util.List; importjava.util.Random; /** *@authorlijing *@date2022年06月19日10:45 *@description */ @RestController @RequestMapping("/api/user") publicclassUserController{ @Autowired privateUserMapperuserMapper; @PostMapping("/save") publicStringaddUser(){ Useruser=newUser(); user.setNickname("zhangsan"+newRandom().nextInt()); user.setPassword("123456"); user.setSex(1); user.setBirthday("1997-12-03"); userMapper.addUser(user); return"success"; } @GetMapping("/findUsers") publicListfindUsers(){ returnuserMapper.findUsers(); } } packagecom.itjing.sharding.mapper; importcom.itjing.sharding.entity.User; importorg.apache.ibatis.annotations.Insert; importorg.apache.ibatis.annotations.Mapper; importorg.apache.ibatis.annotations.Select; importjava.util.List; /** *@authorlijing *@date2022年06月19日10:46 *@description */ @Mapper publicinterfaceUserMapper{ @Insert("insertintot_user(nickname,password,sex,birthday)values(#{nickname},#{password},#{sex},#{birthday})") voidaddUser(Useruser); @Select("select*fromt_user") List findUsers(); }
啟動項目驗證
啟動日志中三個數據源初始化成功:
調用http://localhost:8080/api/user/save一直進入到ds1主節點
調用http://localhost:8080/api/user/findUsers一直進入到ds2、ds3節點,并且輪詢進入
8Sharding-Jdbc實現分庫分表
分表
創建庫表
創建數據庫及其對應的相同的兩張表結構的表
先在MySQL上創建數據庫,直接起名叫做order
然后分別創建兩個表,分別是order_1和order_2。
這兩張表是訂單表拆分后的表,通過Sharding-Jdbc向訂單表插入數據,按照一定的分片規則,主鍵為偶數的落入order_1表 ,為奇數的落入order_2表,再通過Sharding-Jdbc 進行查詢。
DROPTABLEIFEXISTSorder_1; CREATETABLEorder_1( order_idBIGINT(20)PRIMARYKEYAUTO_INCREMENT, user_idINT(11), product_nameVARCHAR(128), COUNTINT(11) ); DROPTABLEIFEXISTSorder_2; CREATETABLEorder_2( order_idBIGINT(20)PRIMARYKEYAUTO_INCREMENT, user_idINT(11), product_nameVARCHAR(128), COUNTINT(11) );
創建SpringBoot的項目
略
依賴
參照讀寫分離的依賴
配置文件
比較重要的一步,那就是配置分片規則,因為這里的分表是直接把數據進行水平拆分成到2個表中,所以屬于水平切分數據表的操作,配置如下:
#讀寫分離 server: servlet: encoding: enabled:true charset:UTF-8 force:true spring: application: name:sharding-jdbc-simple main: allow-bean-definition-overriding:true shardingsphere: datasource: names:db1 db1: type:com.alibaba.druid.pool.DruidDataSource driver-class-name:com.mysql.jdbc.Driver url:jdbc//127.0.0.1:3306/order?characterEncoding=UTF-8&useSSL=false username:root password:root sharding: tables: order: actual-data-nodes:db1.order_$->{1..2} key-generator: column:order_id type:SNOWFLAKE #分表策略 table-strategy: inline: sharding-column:order_id algorithm-expression:order_$->{order_id%2+1} props: sql: show:true mybatis: configuration: map-underscore-to-camel-case:true
測試
packagecom.itjing.sharding.mapper; importorg.apache.ibatis.annotations.Insert; importorg.apache.ibatis.annotations.Mapper; importorg.apache.ibatis.annotations.Param; /** *@authorlijing *@date2022年06月19日11:18 *@description */ @Mapper publicinterfaceOrderMapper{ /** *新增訂單 */ @Insert("INSERTINTOorder(user_id,product_name,COUNT)VALUES(#{user_id},#{product_name},#{count})") intinsertOrder(@Param("user_id")intuser_id,@Param("product_name")Stringproduct_name,@Param("count")intcount); } packagecom.itjing.sharding.controller; importcom.itjing.sharding.mapper.OrderMapper; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.web.bind.annotation.PostMapping; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.RestController; /** *@authorlijing *@date2022年06月19日11:20 *@description */ @RestController @RequestMapping("/api/order") publicclassOrderController{ @Autowired privateOrderMapperorderMapper; @PostMapping("/save") publicStringtestInsertOrder(){ for(inti=0;i10;?i++)?{ ????????????orderMapper.insertOrder(100?+?i,?"空調"?+?i,?10); ????????} ????????return?"success"; ????} }
當執行完畢的時候,可以看下日志:
再看下數據庫:
偶數訂單在表1中,奇數訂單在表2中。
接下來就是直接執行查詢,然后去查詢對應表中的數據。
給定1表和2表中的一個order_id 來進行 In 查詢,看是否能正確返回想要的數據:
/** *查詢訂單 */ @Select({""}) List
很成功,使用Sharding-JDBC 進行單庫水平切分表的操作已經完成了。
分庫
把同一個表的數據按一定規則拆到不同的數據庫中,每個庫可以放在不同的服務器上,在上面裝好數據庫之后,就可以開始進行操作了。
建立庫表
建立數據庫 order1 和 order2,然后創建相同表結構的表。
DROPTABLEIFEXISTSorder_info; CREATETABLEorder_info( order_idBIGINT(20)PRIMARYKEYAUTO_INCREMENT, user_idINT(11), product_nameVARCHAR(128), COUNTINT(11) );
配置
#讀寫分離 server: servlet: encoding: enabled:true charset:UTF-8 force:true spring: application: name:sharding-jdbc-simple main: allow-bean-definition-overriding:true shardingsphere: datasource: names:db1,db2 db1: type:com.alibaba.druid.pool.DruidDataSource driver-class-name:com.mysql.jdbc.Driver url:jdbc//localhost:3306/order1?characterEncoding=UTF-8&useSSL=false username:root password:root db2: type:com.alibaba.druid.pool.DruidDataSource driver-class-name:com.mysql.jdbc.Driver url:jdbc//localhost:3306/order2?characterEncoding=UTF-8&useSSL=false username:root password:root ##分庫策略,以user_id為分片鍵,分片策略為user_id % 2 + 1,user_id為偶數操作db1數據源,否則操作db2。 sharding: tables: order_info: actual-data-nodes:db$->{1..2}.order_info key-generator: column:order_id type:SNOWFLAKE #分庫策略 database-strategy: inline: sharding-column:user_id algorithm-expression:db$->{user_id%2+1} props: sql: show:true mybatis: configuration: map-underscore-to-camel-case:true
配置文件,在這里是通過配置對數據庫的分片策略,來指定數據庫進行操作。
分庫策略,以user_id為分片鍵,分片策略為user_id % 2 + 1,user_id為偶數操作db1數據源,否則操作db2。
這樣的分庫策略,直接通過 user_id 的奇偶性,來判斷到底是用哪個數據源,用哪個數據庫和表數據的。
測試
@Insert("INSERTINTOorder_info(user_id,product_name,COUNT)VALUES(#{user_id},#{product_name},#{count})") intinsertOrderFk(@Param("user_id")intuser_id,@Param("product_name")Stringproduct_name,@Param("count")intcount); @PostMapping("/saveFk") publicStringsaveFk(){ for(inti=0;i10;?i++)?{ ????????orderMapper.insertOrderFk(i,?"空調"?+?i,?1); ????} ????return?"success"; }
看日志的話,看樣子是成功了,看一下數據庫:
這么看下來,保存的數據是沒問題的,從水平切分來看,把數據分別保存了order1和order2庫中的 order_info 里面,也就是說數據算是水平切分到了不同的數據庫對應的表中。
分庫分表后的查詢
@Select({""}) ListfindOrderByIdsFk(@Param("orderIds")List orderIds); @GetMapping("findFk") publicvoidtestFindOrderByIdsFk(){ List ids=newArrayList<>(); ids.add(745252093001990145L); ids.add(745252094096703488L); List list=orderMapper.findOrderByIdsFk(ids); System.out.println(list); }
9相關配置
在說配置之前,得先了解一下關于Sharding-JDBC的執行流程,不然也不知道這些配置都是干嘛用的。
當把SQL發送給 Sharding 之后,Sharding 會經過五個步驟,然后返回接口,這五個步驟分別是:
SQL解析
SQL路由
SQL改寫
SQL執行
結果歸并
SQL解析:編寫SQL查詢的是邏輯表,執行時 ShardingJDBC 要解析SQL,解析的目的是為了找到需要改寫的位置。
SQL路由:SQL的路由是指將對邏輯表的操作,映射到對應的數據節點的過程. ShardingJDBC會獲取分片鍵判斷是否正確,正確 就執行分片策略(算法) 來找到真實的表。
SQL改寫:程序員面向的是邏輯表編寫SQL,并不能直接在真實的數據庫中執行,SQL改寫用于將邏輯 SQL改為在真實的數據庫中可以正確執行的SQL。
SQL執行:通過配置規則order_$->{order_id % 2 + 1},可以知道當 order_id 為偶數時,應該向 order_1表中插入數據,為奇數時向 order_2表插入數據。
結果歸并:將所有真正執行sql的結果進行匯總合并,然后返回。
都知道,要是用Sharding分庫分表,那么自然就會有相對應的配置,而這些配置才是比較重要的地方,而其中比較經典的就是分片策略了。
分片策略
分片策略分為分表策略和分庫策略,它們實現分片算法的方式基本相同,沒有太大的區別,無非一個是針對庫,一個是針對表。
而一般分片策略主要是分為如下的幾種:
standard:標準分片策略
complex:復合分片策略
inline:行表達式分片策略,使用Groovy的表達式.
hint:Hint分片策略,對應HintShardingStrategy。
none:不分片策略,對應NoneShardingStrategy。
標準分片策略StandardShardingStrategy
使用場景:SQL 語句中有>,>=, <=,<,=,IN 和BETWEEN AND操作符,都可以應用此分片策略。
也就是說,SQL 語句中頻繁的出現這些符號的時候,而且這個時候還想要進行分庫分表的時候,就可以采用這個策略了。
但是這個時候要謹記一些內容,那就是標準分片策略(StandardShardingStrategy),它只支持對單個分片鍵(字段)為依據的分庫分表,并提供了兩種分片算法PreciseShardingAlgorithm(精準分片)和RangeShardingAlgorithm(范圍分片)。
在使用標準分片策略時,精準分片算法是必須實現的算法,用于 SQL 含有 = 和 IN 的分片處理;范圍分片算法是非必選的,用于處理含有 BETWEEN AND 的分片處理。
復合分片策略
使用場景:SQL 語句中有>,>=,<=,<,=,IN 和BETWEEN AND等操作符,不同的是復合分片策略支持對多個分片鍵操作。
這里要注意的就是多個分片鍵,也就是說,如果分片的話需要使用兩個字段作為分片鍵,自定義復合分片策略。
行表達式分片策略
它的配置相當簡潔,這種分片策略利用inline.algorithm-expression書寫表達式。
這里就是使用的這個,來完成的分片,而且行表達式分片策略適用于做簡單的分片算法,無需自定義分片算法,省去了繁瑣的代碼開發,是幾種分片策略中最為簡單的。
但是要注意,行表達式分片策略,它只支持單分片鍵。
Hint分片策略
Hint分片策略(HintShardingStrategy)和其他的分片策略都不一樣了,這種分片策略無需配置分片鍵,分片鍵值也不再從 SQL中解析,而是由外部指定分片信息,讓 SQL在指定的分庫、分表中執行。
不分片策略
不分片策略這個沒啥可說的,不分片的話,用Sharing-JDBC的話,可能就沒啥意思了。畢竟玩的就是分片。
官方文檔:https://shardingsphere.apache.org/document/current/cn/overview/
-
MySQL
+關注
關注
1文章
829瀏覽量
26743 -
JDBC
+關注
關注
0文章
25瀏覽量
13431 -
sharding
+關注
關注
0文章
5瀏覽量
7928 -
SpringBoot
+關注
關注
0文章
174瀏覽量
201
原文標題:SpringBoot + Sharding JDBC,一文搞定分庫分表、讀寫分離
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論