SpringBoot中使用ShardingJdbc切分資料庫表

本文主要將業界知名的開源分庫分表中間件—ShardingJdbc集成至SpringBoot工程中,利用ShardingJdbc的數據庫切分能力來實現庫表水平切分和擴展的目標,提高分佈式系統整體的併發量,解決數據庫中的單表因數據量過大而帶來得各種瓶頸和影響(本文所述的ShardingJdbc中間件以其1.X版本為參考,2.X版本和1.X版本有較大區別,在後面的文章中會有介紹)。

分庫分表中間件ShardingJdbc介紹

ShardingJdbc是噹噹開源的數據庫水平切分的中間件,其代表了客戶端類的分庫分表技術框架(這一點與MyCat不同,MyCat本質上是一種數據庫代理)。ShardingJdbc定位為輕量級數據庫驅動,由客戶端直連數據庫,以jar包形式提供服務,未使用中間層,無需額外部署,無其他依賴,業務系統開發人員與數據庫運維人員無需改變原有的開發與運維方式。因此ShardingJdbc即為增強版的JDBC驅動,可以實現舊代碼遷移零成本的目標。

下面的是ShardingJdbc的架構圖:

SpringBoot中使用ShardingJdbc切分數據庫表

從上面的架構圖中,可以看出ShardingJdbc與業務工程集成起來十分方便與快捷,同時其提供的分片規則配置、SQL解析、SQL改寫、SQL路由、SQL執行以及結果歸併等強大的功能,使得業務開發人員無需在這些方面花費較大多的精力,而可以更加專注於業務流程的開發。其主要的特點如下:

(1)分片規則配置

ShardingJdbc的分片邏輯非常靈活,支持分片策略自定義、複數分片鍵、多運算符分片等功能。

(2)JDBC規範重寫

ShardingJdbc中間件對標準JDBC規範的重寫思路是針對DataSource、Connection、Statement、PreparedStatement和ResultSet五個核心接口封裝,將多個JDBC實現類集合(如:MySQL JDBC實現/DBCP JDBC實現等)納入ShardingJdbc實現類管理。

(3)SQL解析

SQL解析作為分庫分表類中間件框架的核心之一,其性能和兼容性是最重要的衡量指標。目前常見的SQL解析器主要有fdb/jsqlparser和Druid。在ShardingJdbc1.5.0之前的版本是採用解析速度相對最快的Druid作為SQL解析器。ShardingJdbc在1.5.0.M1 正式發佈時,將SQL解析引擎從Druid替換成了自研的。新引擎僅解析分片上下文,對於 SQL 採用“半理解”理念,進一步提升性能和兼容性,同時降低了代碼複雜度。

(4)SQL改寫

這裡一部分是將分表的邏輯表名稱替換為實際真實的分表名稱。另一部分是根據SQL解析結果替換一些在分片環境中不正確的功能。其中,1.5.0之前的版本,SQL改寫是在SQL路由之前完成的,而在1.5.x中調整為SQL路由之後,因為SQL改寫可以根據路由至單庫表還是多庫表而進行進一步優化。

(5)SQL路由

SQL路由是指根據分片規則配置,將待執行的SQL定位至真正的DB數據源。

(6)SQL執行

這裡指的是路由至真實的DB數據源後,ShardingJdbc將採用多線程併發執行SQL,並完成對addBatch等批量方法的處理。

(7)結果歸併

ShardingJdbc支持通遍歷類、排序類、聚合類和分組類四種結果並歸方式。普通遍歷類最為簡單,只需按順序遍歷ResultSet的集合即可。排序類結果則將結果先排序再輸出,因為各分庫的分片結果均按照各自條件完成排序,所以採用歸併排序算法整合最終結果。聚合類分為3種類型,比較型、累加型和平均值型。分組類相對最為複雜,需要將所有的ResultSet結果放入內存,使用Map-Reduce算法分組,最後根據排序和聚合條件做相關處理。最為消耗內存,損失性能的地方就是這裡了,可以考慮使用limit合理的限制分組數據大小。

在Spring Boot中實踐ShardingJdbc

本節將主要詳細介紹在SpringBoot工程中如何集成ShardingJdbc這款切分庫表的中間件,並使用其完成對庫表的切分/路由,以及在業務開發中的使用。

版本環境

Spring Boot 1.4.1.RELEASE

Druid 1.0.12

JDK 1.8

添加ShardingJdbc的pom依賴

因為噹噹開源了ShardingJdbc的源碼,我們可以通過maven倉庫來獲得jar包依賴。當訪問http://mvnrepository.com/artifact/com.dangdang/sharding-jdbc-core(該鏈接為1.X版本的)選擇自己項目需要的版本(在本次集成中選擇的版本為1.4.2),點擊進入後複製maven內容到pom.xml內即可。同時,這裡需要注意的是在ShardingJdbc 1.5.0.M1正式發佈以前,SQL解析引擎仍然是採用阿里的Druid數據源連接池,還需要在Pom中引入Druid的依賴(版本為1.0.12),此外需要保證ShardingJdbc與Druid版本相對應,否則在執行SQL解析時候會導致ShardingJdbc的運行時異常。Pom文件的引入如下圖所示:

SpringBoot中使用ShardingJdbc切分數據庫表

yaml文件配置

在MySql數據庫中創建相應的分庫和分表後,需要在SpringBoot工程中的yaml配置文件(或property配置文件)中完成對分庫數據源的配置,具體如下:

testShardingDB0:

type: com.alibaba.druid.pool.DruidDataSource

driverClassName: com.mysql.jdbc.Driver

url: jdbc:mysql://{ip}:{port} /res_test_cloud_bill_sharding0?useUnicode=true&characterEncoding=utf-8

username: xxxxxx

password: xxxxxx

max-idle: 500

max-active: 100

min-idle: 50

initial-size: 50

validation-query: SELECT 1

max-wait: -1

test-on-borrow: true

test-on-return: true

test-while-idle: true

num-tests-per-eviction-run: 3

time-between-eviction-runs-millis: 30000

min-evictable-idle-time-millis: 18000000

removeAbandoned: true

removeAbandonedTimeout: 180

# 打開PSCache,並且指定每個連接上PSCache的大小

poolPreparedStatements: false

maxPoolPreparedStatementPerConnectionSize: -1

# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆

filters: stat,wall,log4j

# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄

connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

# 合併多個DruidDataSource的監控數據

useGlobalDataSourceStat: true

###分庫1配置,具體的ip/host:port和數據庫用戶名和密碼根據實際填寫

testShardingDB1:

type: com.alibaba.druid.pool.DruidDataSource

driverClassName: com.mysql.jdbc.Driver

url: jdbc:mysql:// {ip}:{port}/res_test_cloud_bill_sharding1?useUnicode=true&characterEncoding=utf-8

username: xxxxxx

password: xxxxxx

max-idle: 500

max-active: 100

min-idle: 50

initial-size: 50

validation-query: SELECT 1

max-wait: -1

test-on-borrow: true

test-on-return: true

test-while-idle: true

num-tests-per-eviction-run: 3

time-between-eviction-runs-millis: 30000

min-evictable-idle-time-millis: 18000000

removeAbandoned: true

removeAbandonedTimeout: 180

# 打開PSCache,並且指定每個連接上PSCache的大小

poolPreparedStatements: false

maxPoolPreparedStatementPerConnectionSize: -1

# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆

filters: stat,wall,log4j

# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄

connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

# 合併多個DruidDataSource的監控數據

useGlobalDataSourceStat: true

ShardingJdbc分庫分表的Bean配置

引入了ShardingJdbc中間件後,在SpringBoot工程中可以通過配置Bean來初始化分庫分表的數據源,其中需要設置邏輯表與實際分表的映射關係和分庫分表的ID路由策略。

@Configuration

@MapperScan(basePackages = "com.chinamobile.bcop.test.sharding.xxxxxxxx", sqlSessionTemplateRef = "shardingSqlSessionTemplate")

public class MybatisTestShardingConfig {

@Bean(name = "testShardingDB0")

@ConfigurationProperties(prefix = "spring.datasource.tesetShardingDB0")

public DataSource testShardingDataSource0(){

return new DruidDataSource();

}

@Bean(name = "testShardingDB1")

@ConfigurationProperties(prefix = "spring.datasource.testShardingDB1")

public DataSource testShardingDataSource1(){

return new DruidDataSource();

}

@Bean(name = "dataSourceRule")

public DataSourceRule dataSourceRule(@Qualifier("testShardingDB0") DataSource ds0, @Qualifier("testShardingDB1") DataSource ds1){

Map dataSourceMap = new HashMap<>();

dataSourceMap.put("shardingDataSource0", ds0);

dataSourceMap.put("shardingDataSource1", ds1);

return new DataSourceRule(dataSourceMap, "shardingDataSource0");

}

@Bean(name = "shardingRule")

public ShardingRule shardingRule(@Qualifier("dataSourceRule")DataSourceRule dataSourceRule){

//表策略

//設置分表映射,將test_msg_queue_bill_record_0~test_msg_queue_bill_record_4

// 幾個實際的表映射到test_msg_queue_bill_record邏輯表

//0~4幾個表是真實的表,test_msg_queue_bill_record是個虛擬不存在的表,只是供使用

TableRule orderTableRule = TableRule.builder("test_msg_queue_bill_record")

.actualTables(Arrays.asList(

"test_msg_queue_bill_record_0",

"test_msg_queue_bill_record_1",

"test_msg_queue_bill_record_2",

"test_msg_queue_bill_record_3",

"test_msg_queue_bill_record_4"))

.dataSourceRule(dataSourceRule)

.build();

//綁定表策略,在查詢時會使用主表策略計算路由的數據源,因此需要約定綁定表策略的表的規則需要一致,可以一定程度提高效率

List bindingTableRules = new ArrayList();

bindingTableRules.add(new BindingTableRule(Arrays.asList(orderTableRule)));

return ShardingRule.builder()

.dataSourceRule(dataSourceRule)

.tableRules(Arrays.asList(orderTableRule))

.bindingTableRules(bindingTableRules)

.databaseShardingStrategy(new DatabaseShardingStrategy("CUSTOMER_ID", new CustomizedDbShardingStrategy()))

.tableShardingStrategy(new TableShardingStrategy("USER_ID", new CustomizedTableShardingStrategy()))

.build();

}

@Bean(name = "testShardingDataSource")

public DataSource shardingDataSource(@Qualifier("shardingRule")ShardingRule shardingRule){

return ShardingDataSourceFactory.createDataSource(shardingRule);

}

@Bean(name = "testShardingTransactionManager")

public DataSourceTransactionManager transactitonManager(@Qualifier("testShardingDataSource") DataSource dataSource){

return new DataSourceTransactionManager(dataSource);

}

//MyBatis SqlSessionFactory/SqlSessionTemplate/MybatisDAO的Bean初始化省略

//……..

在本文的示例中,按照業務要求先對原來的單庫單表進行拆分,由原來的單庫單表—testmsgqueuebillrecord,分成兩個庫的多表(每個庫中均有testmsgqueuebillrecord0~testmsgqueuebillrecord4五個分表,這裡只是示例,在真實的業務場景下需要根據業務數據的總體容量來設定分庫分表的規模,究竟是分5個庫每個庫5表,還是分10個庫每個庫10表來滿足業務要求)。由上面的分庫分表配置Bean的代碼可見,使用Druid連接池初始化兩個分庫的數據源後設置ShardingJdbc數據源的分片規則。其中,testmsgqueuebillrecord表為邏輯表,分別與testmsgqueuebillrecord0~testmsgqueuebillrecord4的實際分表名建立映射。如果執行的SQL為“select * from testmsgqueuebillrecord” 就能遍歷查完hwmsgqueuebillrecord0~4五個分表。最後,在設置切分規則時候,分別以customerId和userId作為切分數據庫和切分數據表的路由規則。根據上面的配置可以得到下面的分庫分表邏輯視圖:

SpringBoot中使用ShardingJdbc切分數據庫表

具體的庫表分片路由策略

ShardingJdbc對分片路由策略完全採用開放式的,將這一部分的控制權交給業務側開發人員手中。業務開發人員可以根據具體業務的需求,設置靈活的分庫分表路由策略。在上一節的示例代碼中,可以看到在設置分庫分表規則的時候有兩個比較顯著的類——CustomizedDbShardingStrategy和CustomizedTableShardingStrategy。正是這兩個類完成了自定義的庫表的自定義分片路由策略,CustomizedDbShardingStrategy的示例代碼如下:

public class CustomizedDbShardingStrategy implements SingleKeyDatabaseShardingAlgorithm {

@Override

public String doEqualSharding(Collection databaseNames, ShardingValue shardingValue) {

//在這裡設置具體的條件為“=”的分庫策略

//code here

}

@Override

public Collection doInSharding(Collection databaseNames, ShardingValue shardingValue) {

//在這裡設置具體SQL語句條件為“In”的分庫策略

//code here

}

@Override

public Collection doBetweenSharding(Collection databaseNames, ShardingValue shardingValue) {

//在這裡設置具體的SQL語句條件為“between”的分庫策略

//code here

}

CustomizedTableShardingStrategy的示例代碼如下:

public class CustomizedTableShardingStrategy implements SingleKeyTableShardingAlgorithm {

@Override

public String doEqualSharding(Collection tableNames, ShardingValue shardingValue) {

//在這裡設置具體的條件為“=”的分表策略

//code here

}

@Override

public Collection doInSharding(Collection tableNames, ShardingValue shardingValue) {

//在這裡設置具體SQL語句條件為“In”的分表策略

//code here

}

@Override

public Collection doBetweenSharding(Collection tableNames, ShardingValue shardingValue) {

//在這裡設置具體的SQL語句條件為“between”的分表策略

//code here

}

}

先看分庫表路由策略中的doEqualSharding方法,比如對於SQL語句—“Insert into testmsgqueuebillrecord(CUSTOMERID,USERID,xxxx1,xxxx2……)values(‘testcustid111111’,’testuserid22222’,’xxxx1’,’xxxx2’……)”,doEqualSharding方法中的形參databaseNames為分庫的名稱(比如shardingDataSource0數據源對應的實際分庫名稱為restestcloudbillsharding0),shardingValue為分庫的鍵值(在前一節分庫分表Bean配置中設定),也就是對應該SQL語句中的“testcustid111111”,我們可以在doEqualSharding方法中設置適合業務需求的鍵值分片路由規則,比如以“testcustid111111”的哈希值作為分片路由規則來選擇該插入SQL究竟應該路由至實際的哪個分庫的哪個分表來執行。而另外的兩個方法,doInSharding和doBetweenSharding則是作用在SQL條件為“Between”和“In”的語句上。

分佈式主鍵生成

對於傳統的數據表設計而言,主鍵自增都是基本需求。對MySQL而言,分庫分表之後,不同表生成全局唯一主鍵是較為頭疼的問題。因為同一個邏輯表所對應的不同實際分表之間的自增鍵是無法感知,這樣會造成有部分分表的主鍵值重複。我們當然可以通過分表的約束規則來達到數據不重複,但是這需要引入額外的方法來解決重複性問題。

目前,有許多第三方技術方案可以解決該問題,比如flickr的全局主鍵生成方案和uuid的全局主鍵生成方案。ShardingJdbc在設計之初也有考慮過這個問題,提供了自己的實現方案,其分佈式全局ID自動生成器可以根據時間偏移量、工作進程id和同一毫秒的自增量來生成一個64位的Long型數值以保證其全局唯一性。

在工程中需要在pom中添加依賴如下:

SpringBoot中使用ShardingJdbc切分數據庫表

在配置中需要完成對其Bean對象的初始化,具體代碼如下:

@Bean(name = "shardingIdGenerator")

public IdGenerator getIdGenerator() {

return new CommonSelfIdGenerator();

}

使用ShardingJdbc編寫自定義的Dao

經過上面幾節內容後,這一節就可以使用配置後的ShardingJdbc所對應的MybatisDao來完成一些業務的應用了。

public abstract class AbstractResBillShardingDao {

@Qualifier(value = "hwShardingMyBatisDao")

@Autowired

protected IMybatisDAO mybatisShardingDao;

@Qualifier(value = "shardingIdGenerator")

@Autowired

private IdGenerator idGenerator;

public int shardingInsertRows (List resBillRecordList, String resType) {

int resultRowNum = 0;

try {

String shardingInsertSqlMapper = MyBatisMapperMapping.ResBatchInsertMapper.getBatchInsertMapperByTypeCode(resType);

if(StringUtils.isEmpty(shardingInsertSqlMapper)) {

log.info("無法匹配到對應的sqlmapper的statement,直接返回,無法匹配的資源類型為:{}",resType);

return resultRowNum;

}

for(BillRecord msgQueueBillRecord:resBillRecordList) {

//設置分佈式id主鍵msgQueueBillRecord.setId(idGenerator.generateId().longValue());

resultRowNum+=mybatisShardingDao.insert(shardingInsertSqlMapper, msgQueueBillRecord);

}

} catch (Exception ex) {

log.error("批量插入數據出現異常", ex);

}

return resultRowNum;

}

}

可以看到在這個Dao的類中,批量插入方法—shardingInsertBills,通過自動注入的mybatisShardingDao和idGenerator來完成對數據記錄的切分插入,這裡需要注意ShardingJdbc目前尚未支持批量插入的SQL語句,因此需要在代碼中自己完成遍歷數據集合後的單行插入,這裡idGenerator用於生成每個待插入記錄的全局唯一健值。工程運行的截圖日誌如下:

SpringBoot中使用ShardingJdbc切分數據庫表

使用ShardingJdbc的限制

ShardingJdbc中間件並非是萬能,它還是有一些SQL和JDCB接口的使用限制,其最大的使用限制在於如下幾點:

(1)對於DataSource接口不支持超時相關的操作。

(2)對於Connection接口不支持存儲過程、遊標、函數、savePoint、自定義類型映射等。

(3)對於Statement和PreparedStatement接口,不支持返回多結果集的語句和國際化操作。

(4)對於ResultSet接口,不支持對於結果集指針位置判斷;不支持通過非next方法改變結果指針位置;不支持修改結果集內容。

(5)SQL語句限制:不支持HAVING;不支持OR,UNION 和 UNION ALL;不支持特殊INSERT,尤其是是批量插入的SQL語句;不支持DISTINCT聚合;不支持dual虛擬表;不支持SELECT LASTINSERTID();不支持CASE WHEN。

總結

本文主要介紹瞭如何在Spring Boot工程中完成數據庫切分中間件—ShardingJdbc集成以及如何進行分庫分表配置、庫表分片路由策略設置和分佈式主鍵生成關鍵點的闡述。限於筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。在這裡在給大家推薦一個架構交流群:617434785,裡面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源。相信對於已經工作和遇到技術瓶頸的碼友,在這個群裡會有你需要的內容。

鏈接:https://www.jianshu.com/p/ec91d62621c0

來源:簡書


分享到:


相關文章: