DIY 三種分庫分表分片算法,自己寫的輪子才吊!
前言
本文是《ShardingSphere5.x分庫分表原理與實(shí)戰(zhàn)》系列的第六篇,書接上文實(shí)現(xiàn)三種自定義分片算法。通過自定義算法,可以根據(jù)特定業(yè)務(wù)需求定制分片策略,以滿足不同場景下的性能、擴(kuò)展性或數(shù)據(jù)處理需求。同時(shí),可以優(yōu)化分片算法以提升系統(tǒng)性能,規(guī)避數(shù)據(jù)傾斜等問題。
在這里,自定義分片算法的類型(Type)統(tǒng)一為CLASS_BASED,包含兩個(gè)屬性:strategy 表示分片策略類型,目前支持三種:STANDARD、COMPLEX、HINT;algorithmClassName 表示自定義分片算法的實(shí)現(xiàn)類路徑。此外,還可以向算法類內(nèi)傳入自定義屬性。
圖片
自定義 STANDARD 算法
要實(shí)現(xiàn)自定義 STANDARD 標(biāo)準(zhǔn)算法,需要實(shí)現(xiàn)StandardShardingAlgorithm<T>接口( T 代表接收的分片健值類型),并重寫接口中的四個(gè)方法。其中,有兩個(gè) doSharding() 方法為處理分片的核心邏輯;getProps() 方法用于獲取分片算法的配置信息;init() 方法則用于初始化分片算法的配置信息,支持動(dòng)態(tài)修改。
5.X 以后的版本,實(shí)現(xiàn)自定義標(biāo)準(zhǔn)算法的精準(zhǔn)分片和范圍分片,不在需要實(shí)現(xiàn)多個(gè)接口。只用實(shí)現(xiàn) StandardShardingAlgorithm 標(biāo)準(zhǔn)算法接口,重寫兩個(gè) doSharding() 方法。doSharding(availableTargetNames,rangeShardingValue) 處理含有 >、<、between and 等操作符的 SQL,doSharding(availableTargetNames,preciseShardingValue) 處理含有 = 、in 等操作符的 SQL。
精準(zhǔn)分片
精準(zhǔn)分片用于SQL中包含 in、= 等操作符的場景,支持單一分片健。
重寫方法 doSharding(Collection availableTargetNames, PreciseShardingValue preciseShardingValue),該方法返回單一的分片數(shù)據(jù)源或分片表數(shù)據(jù)。有兩個(gè)參數(shù):一個(gè)是可用目標(biāo)分庫、分表的集合,另一個(gè)是精準(zhǔn)分片屬性對象。
圖片
PreciseShardingValue 對象屬性數(shù)據(jù)格式如下:
{
"columnName": "order_id", // 分片健
"dataNodeInfo": {
"paddingChar": "0",
"prefix": "db", // 數(shù)據(jù)節(jié)點(diǎn)信息前綴,例如:分庫時(shí)為db,分表時(shí)為分片表t_order_
"suffixMinLength": 1
},
"logicTableName": "t_order", // 邏輯表
"value": 1 // 分片健值
}
范圍分片
范圍分片用于 SQL中包含 >、< 等范圍操作符的場景,支持單一分片健。
重寫方法 doSharding(Collection availableTargetNames, RangeShardingValue rangeShardingValue),該方法可以返回多個(gè)分片數(shù)據(jù)源或分片表數(shù)據(jù)。有兩個(gè)參數(shù):一個(gè)是可用目標(biāo)分庫、分表的集合,另一個(gè)是精準(zhǔn)分片屬性對象。
圖片
RangeShardingValue 對象屬性數(shù)據(jù)格式如下:
{
"columnName": "order_id", // 分片健
"dataNodeInfo": {
"paddingChar": "0",
"prefix": "db", // 數(shù)據(jù)節(jié)點(diǎn)前綴,分庫時(shí)為數(shù)據(jù)源,分表時(shí)為分片表t_order_
"suffixMinLength": 1
},
"logicTableName": "t_order", // 邏輯表
"valueRange": [0,∞] // 分片健值的范圍數(shù)據(jù)
}
精準(zhǔn)分片算法的 doSharding() 執(zhí)行流程:從PreciseShardingValue.getValue()中獲取分片鍵值,然后經(jīng)過計(jì)算得出相應(yīng)編號,最終在availableTargetNames可用目標(biāo)分庫、分片表集合中選擇以一個(gè)符合的返回。
范圍分片算法的 doSharding() 執(zhí)行流程:從RangeShardingValue.getValueRange()方法獲取分片鍵的數(shù)值范圍,然后經(jīng)過計(jì)算得出相應(yīng)編號,最終在availableTargetNames可用目標(biāo)分庫、分片表集合中選擇多個(gè)符合的返回。
下面是具體實(shí)現(xiàn)分片的邏輯:
/**
* 自定義標(biāo)準(zhǔn)分片算法
*
* @author 公眾號:程序員小富
* @date 2024/03/22 11:02
*/
@Slf4j
public class OrderStandardCustomAlgorithm implements StandardShardingAlgorithm<Long> {
/**
* 精準(zhǔn)分片進(jìn)入 sql中有 = 和 in 等操作符會執(zhí)行
*
* @param availableTargetNames 所有分片表的集合
* @param shardingValue 分片健的值,SQL中解析出來的分片值
*/
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
/**
* 分庫策略使用時(shí):availableTargetNames 參數(shù)數(shù)據(jù)為分片庫的集合 ["db0","db1"]
* 分表策略使用時(shí):availableTargetNames 參數(shù)數(shù)據(jù)為分片庫的集合 ["t_order_0","t_order_1","t_order_2"]
*/
log.info("進(jìn)入精準(zhǔn)分片 precise availableTargetNames:{}", JSON.toJSONString(availableTargetNames));
/**
* 分庫策略使用時(shí):shardingValue 參數(shù)數(shù)據(jù):{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"db","suffixMinLength":1},"logicTableName":"t_order","value":1}
* 分表策略使用時(shí):shardingValue 參數(shù)數(shù)據(jù):{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"t_order_","suffixMinLength":1},"logicTableName":"t_order","value":1}
*/
log.info("進(jìn)入精準(zhǔn)分片 preciseShardingValue:{}", JSON.toJSONString(shardingValue));
int tableSize = availableTargetNames.size();
// 真實(shí)表的前綴
String tablePrefix = shardingValue.getDataNodeInfo().getPrefix();
// 分片健的值
long orderId = shardingValue.getValue();
// 對分片健取模后確定位置
long mod = orderId % tableSize;
return tablePrefix + mod;
}
/**
* 范圍分片進(jìn)入 sql中有 between 和 < > 等操作符會執(zhí)行
*
* @param availableTargetNames 所有分片表的集合
* @param shardingValue 分片健的值,SQL中解析出來的分片值
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
/**
* 分庫策略使用時(shí):availableTargetNames 參數(shù)數(shù)據(jù)為分片庫的集合 ["db0","db1"]
* 分表策略使用時(shí):availableTargetNames 參數(shù)數(shù)據(jù)為分片庫的集合 ["t_order_0","t_order_1","t_order_2"]
*/
log.info("進(jìn)入范圍分片:range availableTargetNames:{}", JSON.toJSONString(availableTargetNames));
/**
* 分庫策略使用時(shí) shardingValue 參數(shù)數(shù)據(jù):{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"db","suffixMinLength":1},"logicTableName":"t_order","valueRange":{"empty":false}}
* 分表策略使用時(shí) shardingValue 參數(shù)數(shù)據(jù):{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"t_order_","suffixMinLength":1},"logicTableName":"t_order","valueRange":{"empty":false}}
*/
log.info("進(jìn)入范圍分片:rangeShardingValue:{}", JSON.toJSONString(shardingValue));
// 分片健值的下邊界
Range<Long> valueRange = shardingValue.getValueRange();
Long lower = valueRange.lowerEndpoint();
// 分片健值的上邊界
Long upper = valueRange.upperEndpoint();
// 真實(shí)表的前綴
String tablePrefix = shardingValue.getDataNodeInfo().getPrefix();
if (lower != null && upper != null) {
// 分片健的值
long orderId = upper - lower;
// 對分片健取模后確定位置
long mod = orderId % availableTargetNames.size();
return Arrays.asList(tablePrefix + mod);
}
//
return Collections.singletonList("t_order_0");
}
@Override
public Properties getProps() {
return null;
}
/**
* 初始化配置
*
* @param properties
*/
@Override
public void init(Properties properties) {
Object prop = properties.get("prop");
log.info("配置信息:{}", JSON.toJSONString(prop));
}
}
配置算法
在實(shí)現(xiàn)了自定義分片算法的兩個(gè) doSharding() 核心邏輯之后,接著配置并使用定義的算法。配置屬性包括strategy分片策略類型設(shè)置成standard,algorithmClassName自定義標(biāo)準(zhǔn)算法的實(shí)現(xiàn)類全路徑。需要注意的是:策略和算法類型必須保持一致,否則會導(dǎo)致錯(cuò)誤。
spring:
shardingsphere:
rules:
sharding:
# 分片算法定義
sharding-algorithms:
t_order_database_mod:
type: MOD
props:
sharding-count: 2 # 指定分片數(shù)量
# 12、自定義 STANDARD 標(biāo)準(zhǔn)算法
t_order_standard_custom_algorithm:
type: CLASS_BASED
props:
# 分片策略
strategy: standard
# 分片算法類
algorithmClassName: com.shardingsphere_101.algorithm.OrderStandardCustomAlgorithm
# 自定義屬性
prop:
aaaaaa: 123456
bbbbbb: 654321
tables:
# 邏輯表名稱
t_order:
# 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫.分片表
actual-data-nodes: db$->{0..1}.t_order_${0..2}
# 分庫策略
database-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_database_mod
# 分表策略
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_standard_custom_algorithm
測試算法
在插入測試數(shù)據(jù)時(shí),默認(rèn)會自動(dòng)進(jìn)入精確分片的 doSharding() 方法內(nèi),看到該方法會獲取分片鍵的數(shù)值,根據(jù)我們的計(jì)算規(guī)則確定返回一個(gè)目標(biāo)分片表用于路由。
圖片
接著執(zhí)行一個(gè)范圍查詢的 SQL,此時(shí)將進(jìn)入范圍分片的 doSharding() 方法。通過觀察 shardingValue.getValueRange() 方法中分片鍵的數(shù)值范圍,可以發(fā)現(xiàn)這些數(shù)值范圍是從SQL查詢中解析得到的。
select * from t_order where order_id > 1 and order_id < 10
圖片
自定義 COMPLEX 算法
復(fù)合分片算法支持包含 >,>=, <=,<,=,IN 和 BETWEEN AND 等操作符的SQL,支持多分片健。
自定義COMPLEX復(fù)合分片算法,需要我們實(shí)現(xiàn) ComplexKeysShardingAlgorithm<T> 接口(其中 T 代表接收的分片鍵值類型),并重寫該接口內(nèi)部的 3 個(gè)方法。其中,主要關(guān)注用于處理核心分片邏輯的 doSharding()方法,可以返回多個(gè)分片數(shù)據(jù)源或分片表數(shù)據(jù);其他兩個(gè)配置方法與上述類似,這里不再贅述。
重寫復(fù)合分片方法 doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValues) 實(shí)現(xiàn)定制的多分片健邏輯,該方法有兩個(gè)參數(shù):一個(gè)是可用目標(biāo)分庫、分表的集合;另一個(gè)是多分片健屬性對象。
圖片
logicTableName為邏輯表名,columnNameAndShardingValuesMap用于存儲多個(gè)分片鍵和對應(yīng)的鍵值,columnNameAndRangeValuesMap用于存儲多個(gè)分片鍵和對應(yīng)的鍵值范圍。
ComplexKeysShardingValue數(shù)據(jù)結(jié)構(gòu)如下:
public final class ComplexKeysShardingValue<T extends Comparable<?>> implements ShardingValue {
// 邏輯表
private final String logicTableName;
// 多分片健及其數(shù)值
private final Map<String, Collection<T>> columnNameAndShardingValuesMap;
// 多分片健及其范圍數(shù)值
private final Map<String, Range<T>> columnNameAndRangeValuesMap;
}
核心流程:通過循環(huán) Map 得到多個(gè)分片健值進(jìn)行計(jì)算,從 availableTargetNames 可用目標(biāo)分庫、分片表集合中選擇多個(gè)符合條件的返回。
/**
* 自定義復(fù)合分片算法
*
* @author 公眾號:程序員小富
* @date 2024/03/22 11:02
*/
@Slf4j
public class OrderComplexCustomAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
/**
* 復(fù)合分片算法進(jìn)入,支持>,>=, <=,<,=,IN 和 BETWEEN AND 等操作符
*
* @param availableTargetNames 所有分片表的集合
* @param complexKeysShardingValue 多個(gè)分片健的值,并SQL中解析出來的分片值
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Long> complexKeysShardingValue) {
/**
* 分庫策略使用時(shí):availableTargetNames 參數(shù)數(shù)據(jù)為分片庫的集合 ["db0","db1"]
* 分表策略使用時(shí):availableTargetNames 參數(shù)數(shù)據(jù)為分片庫的集合 ["t_order_0","t_order_1","t_order_2"]
*/
log.info("進(jìn)入復(fù)合分片:complex availableTargetNames:{}", JSON.toJSONString(availableTargetNames));
// 多分片健和其對應(yīng)的分片健范圍值
Map<String, Range<Long>> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
log.info("進(jìn)入復(fù)合分片:columnNameAndRangeValuesMap:{}", JSON.toJSONString(columnNameAndRangeValuesMap));
columnNameAndRangeValuesMap.forEach((columnName, range) -> {
// 分片健
log.info("進(jìn)入復(fù)合分片:columnName:{}", columnName);
// 分片健范圍值
log.info("進(jìn)入復(fù)合分片:range:{}", JSON.toJSONString(range));
});
// 多分片健和其對應(yīng)的分片健值
Map<String, Collection<Long>> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
log.info("進(jìn)入復(fù)合分片:columnNameAndShardingValuesMap:{}", JSON.toJSONString(columnNameAndShardingValuesMap));
columnNameAndShardingValuesMap.forEach((columnName, shardingValues) -> {
// 分片健
log.info("進(jìn)入復(fù)合分片:columnName:{}", columnName);
// 分片健值
log.info("進(jìn)入復(fù)合分片:shardingValues:{}", JSON.toJSONString(shardingValues));
});
return null;
}
}
配置算法
處理完復(fù)合分片算法的doSharding()核心邏輯,接著配置使用定義的算法,配置屬性包括strategy分片策略類型設(shè)置成complex,algorithmClassName自定義算法的實(shí)現(xiàn)類全路徑。
需要注意:配置分片鍵時(shí),一定要使用 sharding-columns 表示復(fù)數(shù)形式,很容易出錯(cuò)。
spring:
shardingsphere:
rules:
sharding:
sharding-algorithms:
t_order_database_mod:
type: MOD
props:
sharding-count: 2 # 指定分片數(shù)量
# 13、自定義 complex 標(biāo)準(zhǔn)算法
t_order_complex_custom_algorithm:
type: CLASS_BASED
props:
# 分片策略
strategy: complex
# 分片算法類
algorithmClassName: com.shardingsphere_101.algorithm.OrderComplexCustomAlgorithm
# 自定義屬性
aaaaaa: aaaaaa
tables:
# 邏輯表名稱
t_order:
# 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫.分片表
actual-data-nodes: db$->{0..1}.t_order_${0..2}
# 分庫策略
database-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_database_mod
# 分表策略
table-strategy:
complex:
sharding-columns: order_id , user_id
sharding-algorithm-name: t_order_complex_custom_algorithm
測試算法
插入測試數(shù)據(jù),debug 進(jìn)入 doSharding() 方法,看到columnNameAndShardingValuesMap內(nèi)獲取到了 user_id 、order_id 兩個(gè)分片鍵及健值。
圖片
當(dāng)執(zhí)行范圍查詢的SQL,columnNameAndRangeValuesMap屬性內(nèi)獲取到了 user_id、order_id 兩個(gè)分片鍵及健值范圍,通過range.upperEndpoint()、lowerEndpoint()得到上下界值。
select * from t_order where order_id > 1 and user_id > 1;
圖片
自定義 HINT 算法
要實(shí)現(xiàn)自定義HINT強(qiáng)制路由分片算法,需要實(shí)現(xiàn) HintShardingAlgorithm<T> 接口( T 代表接收的分片鍵值類型)。在實(shí)現(xiàn)過程中,需要重寫接口中的3個(gè)方法。其中,核心的分片邏輯在 doSharding() 方法中處理,可以支持返回多個(gè)分片數(shù)據(jù)源或分片表數(shù)據(jù)。另外,其他兩個(gè)prop配置方法的使用方式與上述相同,這里不贅述。
重寫 HINT 核心分片方法 doSharding(Collection availableTargetNames, HintShardingValue shardingValue),以實(shí)現(xiàn)我們的定制邏輯。該方法接受兩個(gè)參數(shù):一個(gè)是可用目標(biāo)分庫、分表的集合,另一個(gè)是 Hint 分片屬性對象。
圖片
方法內(nèi)執(zhí)行流程:我們首先獲取 HintManager API 設(shè)置的分庫或分表的分片值,經(jīng)過計(jì)算后得到合適的分片數(shù)據(jù)源或分片表集合,然后直接路由到目標(biāo)位置,無需再關(guān)注SQL本身的條件信息。
/**
* 自定義強(qiáng)制路由分片算法
*
* @author 公眾號:程序員小富
* @date 2024/03/22 11:02
*/
@Slf4j
public class OrderHintCustomAlgorithm implements HintShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Long> hintShardingValue) {
/**
* 獲取到設(shè)置的分表或者分庫的分片值
* 指定分表時(shí)的分片值 hintManager.addTableShardingValue("t_order",2L);
* 指定分庫時(shí)的分片值 hintManager.addDatabaseShardingValue("t_order", 100L);
*/
Collection<Long> values = hintShardingValue.getValues();
Collection<String> result = new ArrayList<>();
// 從所有分片表中得到合適的分片表
for (String each : availableTargetNames) {
for (Long value : values) {
Long mod = value % availableTargetNames.size();
if (each.endsWith(String.valueOf(mod))) {
result.add(each);
}
}
}
return result;
}
}
配置算法
配置自定義Hint算法,配置屬性包括strategy分片策略類型設(shè)置成hint,algorithmClassName自定義Hint算法的實(shí)現(xiàn)類全路徑。使用該算法時(shí)無需指定分片健!
spring:
shardingsphere:
# 具體規(guī)則配置
rules:
sharding:
# 分片算法定義
sharding-algorithms:
t_order_database_mod:
type: MOD
props:
sharding-count: 2 # 指定分片數(shù)量
# 14、自定義 hint 標(biāo)準(zhǔn)算法
t_order_hint_custom_algorithm:
type: CLASS_BASED
props:
# 分片策略
strategy: hint
# 分片算法類
algorithmClassName: com.shardingsphere_101.algorithm.OrderHintCustomAlgorithm
# 自定義屬性
bbbbbb: bbbbbb
tables:
# 邏輯表名稱
t_order:
# 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫.分片表
actual-data-nodes: db$->{0..1}.t_order_${0..2}
# 分庫策略
database-strategy:
hint:
sharding-algorithm-name: t_order_database_mod
# 分表策略
table-strategy:
hint:
sharding-algorithm-name: t_order_hint_custom_algorithm
測試算法
在執(zhí)行SQL操作之前,使用 HintManager API 的 addDatabaseShardingValue和 addTableShardingValue方法來指定分庫或分表的分片值,這樣算法內(nèi)通過 HintShardingValue 可以獲取到分片值。注意:如果在執(zhí)行 SQL 時(shí)沒有使用 HintManager 指定分片值,那么執(zhí)行SQL將會執(zhí)行全庫表路由。
@DisplayName("Hint 自動(dòng)義分片算法-范圍查詢")
@Test
public void queryHintTableTest() {
HintManager hintManager = HintManager.getInstance();
// 指定分表時(shí)的分片值
hintManager.addTableShardingValue("t_order",2L);
// 指定分庫時(shí)的分片值
hintManager.addDatabaseShardingValue("t_order", 100L);
QueryWrapper<OrderPo> queryWrapper = new QueryWrapper<OrderPo>()
.eq("user_id", 20).eq("order_id", 10);
List<OrderPo> orderPos = orderMapper.selectList(queryWrapper);
log.info("查詢結(jié)果:{}", JSON.toJSONString(orderPos));
}
圖片
到這關(guān)于 shardingsphere-jdbc 的 3種自定義分片算法實(shí)現(xiàn)就全部結(jié)束了。
總結(jié)
本文介紹了 STANDARD、COMPLEX 和 HINT 三種自定義分片算法的實(shí)現(xiàn),和使用過程中一些要注意的事項(xiàng)。ShardingSphere 內(nèi)置的十幾種算法,其實(shí)已經(jīng)可以滿足我們絕大部分的業(yè)務(wù)場景,不過,如果考慮到后續(xù)的性能優(yōu)化和擴(kuò)展性,定制分片算法是個(gè)不錯(cuò)的選擇。