shardingsphere之sharding-proxy分庫分表學習筆記
引言
隨着業務數據量的變大,單庫單表已經不能滿足需求了。當單表數據量超過五百萬行,查詢性能急劇下降。分庫分表迫在眉睫,尋找一個簡單實用的解決方案相信是很多小夥伴的想法。
我在看了好多的博客之後遇到了開源數據庫中間件mycat和shardingsphere(前身是sharding-jdbc),經過一番比較之後,我選了京東開源的shardingsphere作爲我的解決方案。
寫這篇文章的目的有兩個,一來是幫助剛入門學習shardingsphere的童鞋快速上手,減少時間成本,先看下怎麼用再去看官方文檔可以達到事半功倍的效果;二來是記錄自己在學習過程中遇到的問題,方便以後在項目中的使用。
重要提示
(1)版本信息:
- 操作系統:windows 7
- JDK1.8
- mybatis-plus-generator 3.1.0
- mybatis-plus-boot-starter 3.1.0
- druid-spring-boot-starter 1.1.18
- MYSQL 5.7.10
(2)用於演示的代碼和重要的參考鏈接已經放到文章的末尾,有需要的童鞋可直接下載查看。其中
sharding_proxy文件夾存放代碼,sharding-proxy-server存放proxy服務端文件。
sharding-proxy簡介
概念
太多的理論知識我就不贅述了,麻煩自己到官網去看。
sharding-jdbc 和 sharding-proxy對比
特點
配置好之後可作爲獨立的數據源使用,一個邏輯數據庫代理着幾個真實數據庫,可以用客戶端軟件比如Navicat Premium 直接去連接和操作。很好的幫助我們處理分庫分表的問題,基本不需要對現有的業務代碼修改,減少時間成本。
使用情況
從零開始整合sharding-proxy
(1)其實大部分的工作都是在編寫conf目錄下的yml文件,配置好之後應用還是跟以前的方式一樣連接和操作,基本不需要對代碼進行修改。
(2)邏輯數據源可以配置多個,每個yml文件代表一個邏輯數據源,可手動創建yml文件和編寫規則實現多個邏輯數據源的使用。
(3)本文演示的是單個邏輯數據源的配置和使用。
整合前的思考
首先你要對業務需要用到的表有一個清晰的認識。哪些表不需要拆分,哪些表需要拆分,表跟表之間是否存在關聯。通過閱讀官網和我的理解,我覺得主要分爲這幾種表:
- 單庫單表
這種表數據量不大,小於十萬這樣,而且跟其他表沒有關聯。這樣的表不需要拆分,放在一個默認庫中即可。比如:配置表,地區編碼表。
- 廣播表
這種表數據量不大,沒有必要拆分;但是跟其他表有關聯關係。在每個庫都保存一個完整表,當讀取數據的時候隨機路由到任一庫,當寫入數據時每個庫下的表都寫入。
- 邏輯表
數據量較大需要拆分的表。比如說訂單數據根據主鍵尾數拆分爲10張表,分別是t_order_0到t_order_9,他們的邏輯表名爲t_order。
- 綁定表
按我的理解就是父子表,常見的就是訂單表和訂單詳情表,通過訂單id關聯。這種類型的表數據量大也是需要拆分的。
場景模擬
爲了加深對sharding-proxy的理解,我在這裏模擬了一個場景,基本涵蓋了常見的情況,順便把實現步驟和使用過程的問題也提一提。
搭建項目
1. 建庫建表
按照前面表的關係圖,我們可以劃分一個默認庫(存放單庫單表和廣播表)和三個庫(存放邏輯表和廣播表);如下所示。sql文件放在git地址的sql目錄下。
data_source
--area
--config
--factory
--warehouse
data_source0
--code_relate0
--code_relate1
--customer0
--customer1
--factory
--indent_detail0
--indent_detail1
--indent0
--indent1
--task_upload0
--task_upload1
--task0
--task1
--warehouse
data_source1
--code_relate0
--code_relate1
--customer0
--customer1
--factory
--indent_detail0
--indent_detail1
--indent0
--indent1
--task_upload0
--task_upload1
--task0
--task1
--warehouse
data_source2
--code_relate0
--code_relate1
--customer0
--customer1
--factory
--indent_detail0
--indent_detail1
--indent0
--indent1
--task_upload0
--task_upload1
--task0
--task1
--warehouse
2.下載sharding-proxy二進制壓縮包
3、解壓zip包
注意事項:
(1)解壓工具最好選擇winrar,不然解壓出來的lib文件夾下的jar包由於命名太長被截斷,後面將會導致文件找不到,系統啓動失敗異常。
(2)解壓後的文件的路徑下最好不要有中文,否則也會導致啓動失敗。
4、下載mysql驅動包
MySQL Connector/J,下載之後放到lib文件夾下。
5、編寫yml文件
(1)首先要編寫的是server.yaml,配置全局信息
##治理中心
# orchestration:
# name: orchestration_ds
# overwrite: true
# registry:
# type: zookeeper
# serverLists: localhost:2181
# namespace: orchestration
#權限配置
authentication:
users:
root: #用戶名
password: root #密碼
sharding:
password: sharding
authorizedSchemas: sharding_db #只能訪問的邏輯數據庫
#Proxy屬性
props:
max.connections.size.per.query: 1
acceptor.size: 16 #用於設置接收客戶端請求的工作線程個數,默認爲CPU核數*2
executor.size: 16 # Infinite by default.
proxy.frontend.flush.threshold: 128 # The default value is 128.
# LOCAL: Proxy will run with LOCAL transaction.
# XA: Proxy will run with XA transaction.
# BASE: Proxy will run with B.A.S.E transaction.
proxy.transaction.type: LOCAL #默認爲LOCAL事務
proxy.opentracing.enabled: false #是否開啓鏈路追蹤功能,默認爲不開啓。
query.with.cipher.column: true
sql.show: true #SQL打印
check.table.metadata.enabled: true #是否在啓動時檢查分表元數據一致性,默認值: false
# proxy.frontend.flush.threshold: # 對於單個大查詢,每多少個網絡包返回一次
注意事項:
(1)當你把註釋的示例內容複製粘貼,然後用快捷鍵去掉‘#’註釋符的時候,注意父子層的距離不要改變,比如下方父子層之間是兩個空格。
props:
max.connections.size.per.query: 1
(2) 在編寫的時候不要用‘Tab’鍵拉開距離,應該使用空格鍵,否則啓動的時候會報錯。
小插曲
當你編寫yml文件的時候,裏面都會有這句話,一開始我以爲還要在哪裏引用這些要用的文件,後面才發現直接去掉註釋編寫就好,文件都是自動引用的。
If you want to configure orchestration, authorization and proxy properties, please refer to this file.
6.編寫config-sharding.yaml配置文件
其實編寫這個文件和sharding-jdbc的yml配置文件基本相同,區別就是對於組合單詞jdbc用的是駝峯,proxy用的是“-”,如果之前玩過sharding-jdbc的話可以直接把配置文件複製過來,對應修改即可。
schemaName: sharding_db #邏輯數據庫名配置
dataSources: #真實數據源配置
db:
url: jdbc:mysql://127.0.0.1:3306/data_source?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
db0:
url: jdbc:mysql://127.0.0.1:3306/data_source0?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
db1:
url: jdbc:mysql://127.0.0.1:3306/data_source1?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
db2:
url: jdbc:mysql://127.0.0.1:3306/data_source2?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
username: root
password: root
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
shardingRule: ##分庫分表規則
defaultDataSourceName: db #默認數據源,放置不需要分片的表和廣播表
broadcastTables:
- factory
- warehouse
bindingTables:
- indent,indent_detail
- task_upload,code_relate ##綁定表配置
defaultDatabaseStrategy: #默認的分庫規則,如果邏輯表沒單獨配置則使用這個
inline:
shardingColumn: customer_id #默認按照customer_id分庫
algorithmExpression: db$->{customer_id % 3}
tables: #邏輯表配置
config: ###單庫單表,使用UUID作爲主鍵
actualDataNodes: db.config
keyGenerator:
column: code
type: UUID
customer:
actualDataNodes: db$->{0..2}.customer$->{0..1} #具體的數據節點
tableStrategy: ##分表策略
inline:
shardingColumn: customer_name #根據hash值取模確定落在哪張表
algorithmExpression: customer$->{Math.abs(customer_name.hashCode() % 2)}
keyGenerator: #配置主鍵生成策略,默認使用SNOWFLAKE
column: customer_id
type: SNOWFLAKE
props:
worker:
id: 20200422
indent:
actualDataNodes: db$->{0..2}.indent$->{0..1}
tableStrategy:
inline:
shardingColumn: indent_id
algorithmExpression: indent$->{indent_id % 2}
keyGenerator:
column: indent_id
type: SNOWFLAKE
indent_detail:
actualDataNodes: db$->{0..2}.indent_detail$->{0..1}
tableStrategy:
inline:
shardingColumn: indent_id
algorithmExpression: indent_detail$->{indent_id % 2}
keyGenerator:
column: detail_id
type: SNOWFLAKE
task:
actualDataNodes: db$->{0..2}.task$->{0..1} #具體的數據節點
databaseStrategy: #分庫規則
inline:
shardingColumn: task_id
algorithmExpression: db$->{task_id % 3}
tableStrategy:
inline:
shardingColumn: task_id
algorithmExpression: task$->{task_id % 2}
task_upload:
actualDataNodes: db$->{0..2}.task_upload$->{0..1} #具體的數據節點
databaseStrategy: #分庫規則
inline:
shardingColumn: task_id
algorithmExpression: db$->{task_id % 3}
tableStrategy:
inline:
shardingColumn: stack_code
algorithmExpression: task_upload$->{Math.abs(stack_code.hashCode() % 2)}
keyGenerator:
column: upload_id
type: SNOWFLAKE
code_relate:
actualDataNodes: db$->{0..2}.code_relate$->{0..1} #具體的數據節點
databaseStrategy: #分庫規則
inline:
shardingColumn: task_id
algorithmExpression: db$->{task_id % 3}
tableStrategy:
inline:
shardingColumn: stack_code
algorithmExpression: code_relate$->{Math.abs(stack_code.hashCode() % 2)}
keyGenerator:
column: relate_id
type: SNOWFLAKE
注意事項:
(1)數組的寫法
- 如果是廣播表應該這麼寫
broadcastTables:
- factory
- warehouse
- 如果是綁定表應該這麼寫
bindingTables:
- indent,indent_detail
- task_upload,code_relate ##綁定表配置
(2)分片鍵分爲分庫鍵和分表鍵。
(3)主鍵生成默認使用SNOWFLAKE算法,使用UUID主鍵的話需要配置。
(4)如果分片鍵的值爲long型,分片規則爲分片字段取模即可;如果是String型,分片規則爲分片字段的哈希值取模再求絕對值,因爲哈希值取模之後也許會出現負數。
(5)邏輯表和綁定表配置建議,儘可能的讓同一類型的數據落在同一個庫中。比如用戶的信息和他產生的訂單以及訂單詳情,可以通過consumer_id作爲分庫鍵,indent_id作爲分表鍵存放,這樣如果查詢命中分片鍵的話可以提高查詢效率(少查了不必要的表)。
(6)綁定表建表的時候,子表最好增加分庫鍵字段便於新增數據時確定落到哪個庫中。比如用戶表、訂單表和訂單詳情表,consumer_id作爲分庫鍵,訂單表需要有這個字段,訂單詳情表也需要這個字段,否則訂單詳情新增數據的時候會在每個庫都新增數據,很明顯是不合理的情況。
7.編寫logback.xml日誌記錄文件
日誌路徑默認在啓動方式start.bat下創建個logs文件夾,所以只要填寫相對路徑就好。下面設置的是按級別按天輸出日誌,基本符合常規需求。
<?xml version="1.0"?>
<configuration>
<!-- 控制檯輸出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日誌消息,%n是換行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %highlight([%-5level] %logger{50} - %msg%n)</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 系統正常日誌文件 -->
<appender name="SYSTEM_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 過濾器,只打印INFO級別的日誌 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日誌文件輸出的文件名-->
<fileNamePattern>../logs/%d{yyyy-MM-dd}/info.%i.log</fileNamePattern>
<!--日誌文件保留天數-->
<!-- <MaxHistory>15</MaxHistory> -->
<!-- 除按日誌記錄之外,還配置了日誌文件不能超過2M,若超過2M,日誌文件會以索引0開始,命名日誌文件,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式記錄日誌 -->
<append>true</append>
<!-- 日誌文件的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日誌消息,%n是換行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 系統警告日誌文件,記錄用戶輸入不符合規則的數據 -->
<appender name="SYSTEM_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 過濾器,只打印warn級別的日誌 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日誌文件輸出的文件名-->
<fileNamePattern>../logs/%d{yyyy-MM-dd}/warn.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式記錄日誌 -->
<append>true</append>
<!-- 日誌文件的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日誌消息,%n是換行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 系統錯誤日誌文件 -->
<appender name="SYSTEM_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 過濾器,只打印ERROR級別的日誌 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日誌文件輸出的文件名-->
<fileNamePattern>../logs/%d{yyyy-MM-dd}/error.%i.log</fileNamePattern>
<!-- 除按日誌記錄之外,還配置了日誌文件不能超過2M,若超過2M,日誌文件會以索引0開始,命名日誌文件,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<MaxFileSize>10MB</MaxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- 追加方式記錄日誌 -->
<append>true</append>
<!-- 日誌文件的格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日誌消息,%n是換行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="system_error" additivity="false">
<appender-ref ref="SYSTEM_ERROR"/>
</logger>
<!-- 默認紀錄級別 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="SYSTEM_INFO" />
<appender-ref ref="SYSTEM_WARN" />
<appender-ref ref="SYSTEM_ERROR" />
</root>
</configuration>
8.啓動sharding-proxy
在cmd命令窗口中cd到bin目錄下,然後直接運行start.bat,默認是3307端口;當然你也可以指定端口,在後面加上參數即可,比如start.bat 3309即可指定連接端口爲3309。然後觀察cmd窗口,如果沒報出異常的話表示啓動成功。
9.客戶端連接
這裏推薦使用Navicat Premium 11 去連接,使用Navicat Premium 12的話你會發現很多奇怪的問題,下面是官網的建議和成功連接的圖。至此,proxy服務端搭建好了,接下來是編寫代碼。
10.基礎CRUD代碼生成
通過示例代碼的工具類連接generator庫可以快速生成基礎的CRUD代碼
src/test/java/com/project/generator/MybatisGenerator.java
注意事項
(1)實體主鍵類型的選擇
- 如果主鍵是long型的話,可以這麼配置,個人建議選擇type = IdType.ID_WORKER這樣更直白明瞭。否則插入數據時會報錯。
/**
* id
*/
@TableId(value = "id", type = IdType.ID_WORKER)
private Long id;
或者
/**
* id
*/
@TableId(value = "id", type = IdType.NONE)
private Long id;
使用type= ID.AUTO的異常信息
Caused by: java.sql.SQLException: Field ‘id’ doesn’t have a default value
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:965)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3978)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903)
at com.mysql.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2124)
at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java:1801)
… 89 common frames omitted
- 如果主鍵是String型的話,可以這麼配置。因爲默認的是SNOWFLAKE生成,否則會插入一個long型的主鍵值導致報錯。
/**
* 編號
*/
@TableId(value = "code", type = IdType.AUTO)
private String code;
或者
/**
* 編號
*/
@TableId(value = "code", type = IdType.UUID)
private String code;
項目運行
測試代碼已經寫到裏面了,通過發起請求和觀察cmd窗口或者日誌文件你會發現邏輯SQL和真實SQL,從而發現他的查詢規則:
1、如果表沒配置規則,那麼直接到默認庫去訪問
2、如果訪問的是廣播表,那麼讀的時候是隨機路由到一個庫,寫的時候是全部庫都寫數據。
3、邏輯表查詢,查詢字段命中了分庫鍵,那麼路由到指定庫下的所有表查詢;命中了分表鍵,到所有庫下指定表查詢。如果都沒命中,那麼將發生笛卡爾積,進行全路由所有的庫和表都查詢一遍,效率不高。所以合理的配置分片規則是很重要的。
這裏插入個未解之謎:
如果主鍵設置的是snowflake生成,當連續發起兩個請求插入一樣內容數據的時候會報出主鍵重複的異常,比如連續訪問下面的請求將會報錯,原因不得而知,如果解決了這個問題的童鞋麻煩私信原因,謝謝。
/**
* 模擬插入數據
*/
List<Area> list = Lists.newArrayList();
@PostConstruct
private void getData() {
list.add(new Area("110000", "北京市"));
list.add(new Area("110101", "東城區"));
list.add(new Area("110102", "西城區"));
list.add(new Area("110106", "豐臺區"));
}
@PostMapping("/save")
public ResponseData<?> save() {
areaService.saveBatch(list);
return ResponseData.out(CodeEnum.SUCCESS, null);
}
分佈式事務
由於這裏只配置了一個邏輯數據源(包含多個真實數據源),不大清楚算不算分佈式事務。不過寫法跟原來是一樣的,加上註解即可。運行下面的代碼發生異常,數據將不會寫進數據庫,表示事務控制成功。
/**
* 測試事務管理
*
* @date 2020年5月3日
* @author huangjg
*/
@Transactional(rollbackFor = Exception.class)
@PostMapping("/save")
public ResponseData<?> save() {
customerService.saveBatch(list);
int i = 5 / 0;
return ResponseData.out(CodeEnum.SUCCESS, null);
}
彈性伸縮
這個還沒開始研究,估計在4.1.x版本會有這個功能。
配置zookeeper
目前我將zookeeper跑起來的時候不懂如何跟項目對接起來,如果有成功的同學麻煩將方法告知下。
結語
官網的文檔比較詳細和社區都是很活躍的,這些可以減少我們的學習成本,快速用於項目。如果在學習的過程中遇到問題可以多看看官方文檔或者直接到github上面提issues,官方人員會很快給予答覆的。