基于 sharding-jdbc 拓展點實現(xiàn)復(fù)雜分庫分表算法
我們之前介紹一款輕量級的分庫分表中間件sharding-jdbc,默認(rèn)情況下該框架的分表算法都是采用內(nèi)聯(lián)表達(dá)式進(jìn)行配置,對于某些比較靈活的需求無法實現(xiàn),所以本文就以一個基于電話號碼號頭的案例介紹一下如何通過基于sharding-jdbc拓展點實現(xiàn)復(fù)雜分庫分表算法。
一、詳解自定義分表邏輯開發(fā)
1. 基于復(fù)雜發(fā)表算法的案例說明
我們的案例是為了采集不同地區(qū)的電話號碼用戶的信息,希望相同號頭的電話號碼會落到同一張分表上,例如我們現(xiàn)在有分表3張,有一個電話號碼10658888,我們必須截取到1065和分表數(shù)進(jìn)行取模運算得到分表名user_0:
2. 基于源碼了解拓展點
我們直接定位到框架進(jìn)行分庫分表計算的代碼段StandardRoutingEngine的routeTables,可以看到該方法會通過程序初始化加載好的shardingRule定位到當(dāng)前分表策略:
private Collection<DataNode> routeTables(final TableRule tableRule, final String routedDataSource, final List<RouteValue> tableShardingValues) {
//獲取分表的前綴,以本文為例就是user
Collection<String> availableTargetTables = tableRule.getActualTableNames(routedDataSource);
//基于shardingRule調(diào)用getTableShardingStrategy獲取分表策略
Collection<String> routedTables = new LinkedHashSet<>(tableShardingValues.isEmpty() ? availableTargetTables
: shardingRule.getTableShardingStrategy(tableRule).doSharding(availableTargetTables, tableShardingValues));
Preconditions.checkState(!routedTables.isEmpty(), "no table route info");
Collection<DataNode> result = new LinkedList<>();
for (String each : routedTables) {
result.add(new DataNode(routedDataSource, each));
}
return result;
}
查看getTableShardingStrategy方法可以看到,如果tableRule沒有實現(xiàn)默認(rèn)分表策略,則采用默認(rèn)也就是我們配置的內(nèi)斂策略defaultTableShardingStrategy ,反之返回我們自定義實現(xiàn)的分表策略:
public ShardingStrategy getTableShardingStrategy(final TableRule tableRule) {
//若getTableShardingStrategy返回空說明我們沒有自定義實現(xiàn)類,返回defaultTableShardingStrategy 通過內(nèi)聯(lián)表達(dá)式進(jìn)行分表運算,反之返回我們的自定義實現(xiàn)類
return null == tableRule.getTableShardingStrategy() ? defaultTableShardingStrategy : tableRule.getTableShardingStrategy();
}
此時我們可以推測tableShardingStrategy的配置就決定了我們是走內(nèi)聯(lián)表達(dá)式還是自定義類分表算法,通過源碼的定位筆者發(fā)現(xiàn)tableShardingStrategy關(guān)于分表的配置來源于配置,在程序啟動時tableShardingStrategy會根據(jù)yml的配置得到分表前綴如果是.table-strategy.standard.則說明當(dāng)前程序采用的是自定義分表算法,就會基于這段配置定位到Java類生分表引擎:
對應(yīng)的我們給出分表算法初始化的入口:
public TableRule(final TableRuleConfiguration tableRuleConfig, final ShardingDataSourceNames shardingDataSourceNames, final String defaultGenerateKeyColumn) {
//......
//基于配置的值決定分表算法如何創(chuàng)建
tableShardingStrategy = null == tableRuleConfig.getTableShardingStrategyConfig() ? null : ShardingStrategyFactory.newInstance(tableRuleConfig.getTableShardingStrategyConfig());
//......
}
繼續(xù)步進(jìn)我們就可以直接定位到對應(yīng)分表配置加載邏輯:
@Override
public TableRuleConfiguration swap(final YamlTableRuleConfiguration yamlConfiguration) {
//......
//基于yml配置得到分表算法采用哪種方式
result.setTableShardingStrategyConfig(shardingStrategyConfigurationYamlSwapper.swap(yamlConfiguration.getTableStrategy()));
//......
return result;
}
最終查看配置加載的swap就可以看到自定分表配置加載的邏輯可以看到,只要我們配置的是StandardShardingStrategyConfiguration前綴的配置,這段配置就會為我們生成自定義算法:
@Override
public YamlShardingStrategyConfiguration swap(final ShardingStrategyConfiguration data) {
//基于yml中給定的字段、分表類生成標(biāo)準(zhǔn)的分表策略配置
StandardShardingStrategyConfiguration得到自定義分表算法
YamlShardingStrategyConfiguration result = new YamlShardingStrategyConfiguration();
if (data instanceof StandardShardingStrategyConfiguration) {
result.setStandard(createYamlStandardShardingStrategyConfiguration((StandardShardingStrategyConfiguration) data));
}
//......
return result;
}
3. 配置與分表算法實現(xiàn)
基于上述源碼,筆者得到對應(yīng)配置前綴,我們開始進(jìn)行自定義分庫分表算法的配置步驟,首先自然是完成數(shù)據(jù)源的配置,如下所示,筆者自定義分表數(shù)據(jù)源名稱為ds0,后續(xù)數(shù)據(jù)源信息配置的datasource后面都要拼上這個自定義的數(shù)據(jù)源名稱ds0:
# 數(shù)據(jù)源名稱
spring.shardingsphere.datasource.names=ds0
# 數(shù)據(jù)源基本鏈接、賬號、密碼信息
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost:3306/db?characterEncoding=utf-8
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
完成數(shù)據(jù)源基本配置后就到最重要分表核心配置了,對應(yīng)選項含義分別是:
- actual-data-nodes:配置分庫分表的庫表區(qū)間,以筆者為例,配置為單庫多表,對應(yīng)的配置為ds0.user_$->{0..2},即ds0這個數(shù)據(jù)源下的user_0、user_1、user_2。
- precise-algorithm-class-name:指定分庫分表的策略的實現(xiàn)類的全路徑,以筆者為例包的全路徑為com.sharkChili.algorithm.TableShardingAlgorithm。
- sharding-column:配置分片鍵,本文采用的是用戶表的電話號碼也就是phone字段。
- key-generator.column:該表的主鍵id為id。
- key-generator.type:id算法采用雪花算法。
# 配置分表區(qū)間
spring.shardingsphere.sharding.tables.user.actual-data-nodes=ds0.user_$->{0..2}
# 指定自定義分表類的包全路徑
spring.shardingsphere.sharding.tables.user.table-strategy.standard.precise-algorithm-class-name=com.sharkChili.algorithm.TableShardingAlgorithm
# 配置分表分片鍵
spring.shardingsphere.sharding.tables.user.table-strategy.standard.sharding-column=phone
# 配置主鍵生成策略,指定數(shù)據(jù)表的主鍵為id字典,id算法采用雪花算法
spring.shardingsphere.sharding.tables.user.key-generator.column=id
# id使用雪花算法,因為雪花算法生成的id具有全球唯一性,并且又有自增特性,適合mysql的innodb引擎
spring.shardingsphere.sharding.tables.user.key-generator.type=SNOWFLAKE
如果我們希望打印分庫分表執(zhí)行SQL日志可以加上這條配置:
# 打開sql輸出日志
spring.shardingsphere.props.sql.show=true
最后我們給出分表實現(xiàn)類,直接繼承PreciseShardingAlgorithm并執(zhí)行泛型為phone字典的類型String,通過截取電話號碼頭4位通過取模算法返回分表名稱:
@Slf4j
public class TableShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<String> phoneNum) {
log.info("分表信息:{}", JSONUtil.toJsonStr(collection));
//號頭小于4位,放到默認(rèn)表
if (StrUtil.isEmpty(phoneNum.getValue()) || phoneNum.getValue().length() < 4) {
return "user";
}
//獲取號頭進(jìn)行取模獲取表號
String phonePrex = phoneNum.getValue().substring(0, 4);
int tableNo = (Integer.valueOf(phonePrex)) % collection.size();
log.info("preciseShardingValue:{} table no:{}", phoneNum, tableNo);
//返回分表名
return "user_" + tableNo;
}
}
4. 自定義分表算法演示
最后我們給出插入的測試代碼:
@Test
void insert() {
User user = new User();
user.setId((long) RandomUtil.randomInt());
user.setName("user" + 7879879843L);
user.setPhone("10658888");
userMapper.insert(user);
}
從日志可以看出,我們的插入數(shù)據(jù)定位到了分表0,與預(yù)期一致:
INFO 11940 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: insert into user_0 (id, name, phone) VALUES (?, ?, ?) ::: [-1687644961, user7879879843, 10658888]
二、詳解Sharding-JDBC幾種分片策略
1. Sharding-JDBC分片策略概覽
上述代碼我們已經(jīng)通過inline關(guān)鍵字指明基于表達(dá)式user_$->{id % 3}所實現(xiàn)的內(nèi)聯(lián)策略,通過該配置與之關(guān)聯(lián)的關(guān)系類YamlShardingStrategyConfiguration我們可以看出,Sharding-JDBC總共一共了如下幾種分片策略:
- standard:精確分片策略,即基于用戶給定的單個分片鍵定位對應(yīng)的庫表。
- complex:復(fù)雜分片策略,即基于用戶傳入的多個字段定位對應(yīng)的庫表。
- hint:強(qiáng)制路由策略,比較少用,該策略用基于用戶傳參并結(jié)合路由策略實現(xiàn)類定位庫表。
- inline:內(nèi)聯(lián)表達(dá)式,基于用戶給定的分片鍵值和表達(dá)式獲取對應(yīng)的庫表。
- none:無分片策略。
對應(yīng)的我們給出YamlShardingStrategyConfiguration 的配置類印證這種說法:
@Getter
@Setter
public class YamlShardingStrategyConfiguration implements YamlConfiguration {
private YamlStandardShardingStrategyConfiguration standard;
private YamlComplexShardingStrategyConfiguration complex;
private YamlHintShardingStrategyConfiguration hint;
private YamlInlineShardingStrategyConfiguration inline;
private YamlNoneShardingStrategyConfiguration none;
}
2. 標(biāo)準(zhǔn)分片策略(范圍分片)
我們先來說說標(biāo)準(zhǔn)分片策略,也就是我們上文所實現(xiàn)的自定義分片算法,這里我們介紹另一種基于范圍分片的策略實現(xiàn),如下所示,可以看到我們分片鍵為user表的id之后,指明range-algorithm-class-name即范圍分片查詢算法的實現(xiàn)類為TableRangeShardingAlgorithm:
spring.shardingsphere.sharding.tables.user.table-strategy.standard.sharding-column=id
# 指明user表的范圍分片算法類為TableRangeShardingAlgorithm
spring.shardingsphere.sharding.tables.user.table-strategy.standard.range-algorithm-class-name=com.sharkChili.algorithm.TableRangeShardingAlgorithm
對應(yīng)我們也給出范圍分片實現(xiàn)的算法實現(xiàn)類TableRangeShardingAlgorithm :
@Slf4j
public class TableRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
//記錄分片鍵對應(yīng)的分表
Set<String> resTbSet = new ConcurrentHashSet<>();
//獲取id起始值
long begin = rangeShardingValue.getValueRange().lowerEndpoint();
//獲取id結(jié)束值
long end = rangeShardingValue.getValueRange().upperEndpoint();
//基于這個id的范圍取模定位分表名稱寫入set中
LongStream.rangeClosed(begin, end).forEach(i -> resTbSet.add("user_" + i % 3));
log.info("res tb set:{}", resTbSet);
return resTbSet;
}
}
這里我們給出測試代碼:
UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
UserExample userExample = new UserExample();
//指明id為2、3對應(yīng)算法%3后的分表為0、1
userExample.createCriteria()
.andIdBetween(2L, 3L);
List<User> userList = userMapper.selectByExample(userExample);
log.info("user list:{}", userList);
輸出結(jié)果如下,可以看到定位到了0和3兩個分表,與預(yù)期的邏輯一致:
需要補充的是范圍分片和標(biāo)準(zhǔn)精準(zhǔn)匹配的分片策略是兼容的,所以我們在標(biāo)準(zhǔn)分片的配置情況下可以同時實現(xiàn)兩套算法針對不同維度的查詢:
# 標(biāo)準(zhǔn)分片策略
spring.shardingsphere.sharding.tables.user.table-strategy.standard.sharding-column=id
# 標(biāo)準(zhǔn)分片的精準(zhǔn)定位算法
spring.shardingsphere.sharding.tables.user.table-strategy.standard.precise-algorithm-class-name=com.sharkChili.algorithm.TableShardingAlgorithm
# 指明user表的范圍分片算法類為TableRangeShardingAlgorithm
spring.shardingsphere.sharding.tables.user.table-strategy.standard.range-algorithm-class-name=com.sharkChili.algorithm.TableRangeShardingAlgorithm
3. 復(fù)雜分片策略
涉及多字段條件的查詢,sharding-jdbc同樣提供了復(fù)雜分片策略配置,例如我們的分表查詢算法的是基于id和age兩個字段,那么我們就可以指明complex聲明分片鍵為id和age,通過ComplexTableShardingAlgorithm實現(xiàn)分表邏輯:
# 指名復(fù)雜分片算法鍵為id和age
spring.shardingsphere.sharding.tables.user.table-strategy.complex.sharding-columns=id,age
# 復(fù)合分片算法
spring.shardingsphere.sharding.tables.user.table-strategy.complex.algorithm-class-name=com.sharkChili.algorithm.ComplexTableShardingAlgorithm
這里為了簡單演示復(fù)雜分片算法的實現(xiàn)和使用,筆者簡單的取出多值查詢中id和age各一個,然后定位到分表集合,對應(yīng)邏輯如下,讀者可以參考注釋了解一下邏輯,對應(yīng)的我們查詢時只需傳入id和age后就會走到該算法,這里就不多做結(jié)果演示了:
public class ComplexTableShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
Map<String, Collection<Long>> map = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
Collection<Long> idList = map.get("id");
Collection<Long> ageList = map.get("age");
Set<String> tbSet = new HashSet<>();
//定位到age字段值
long age = Long.valueOf(String.valueOf(ageList.stream().findFirst().get()));
//定位到id字段值
long id = idList.stream().findFirst().get();
//如果年齡大于100則說明是無效數(shù)據(jù),到user表查
if (age > 100) {
tbSet.add("user");
} else {//反之基于id進(jìn)行取模運算定位分表
tbSet.add("user_" + id % 3);
}
return tbSet;
}
}
4. 強(qiáng)制路由策略
強(qiáng)制路由算是比較少用的分片策略,它的分表算法由用戶自行實現(xiàn)且定位分表的邏輯與SQL語句沒有任何關(guān)系,常用于系統(tǒng)維度的分表算法,所以配置時只需給出分表實現(xiàn)的策略類即可:
spring.shardingsphere.sharding.tables.user.table-strategy.hint.algorithm-class-name=com.sharkChili.algorithm.TableHintShardingAlgorithm
可以看到分表實現(xiàn)策略如下:
- 通過用戶入?yún)⒅蝎@取邏輯分表
- 從入?yún)⒊霁@取邏輯分表對應(yīng)的value
基于上述兩個值組裝成分表:
public class TableHintShardingAlgorithm implements HintShardingAlgorithm<String> {
@Override
public Collection<String> doSharding(Collection<String> tableNames, HintShardingValue<String> hintShardingValue) {
//定位傳入的邏輯分表
String logicTableName = hintShardingValue.getLogicTableName();
String logicTableValue = hintShardingValue.getValues().stream().findFirst().get();
//基于values的第一個值定位分表號碼,并于邏輯分配構(gòu)成分表名稱
String tbName = logicTableName+"_"+logicTableValue;
return Arrays.asList(tbName);
}
}
hint算法是通過外部指定分片信息讓分片策略決定路由最終指向,所以我們都是通過HintManager實例傳入組裝當(dāng)前線程的邏輯表名和值從而定位到分表:
HintManager hintManager = HintManager.getInstance();
try {
//邏輯分表傳入user,value傳入0,讓分表算法組成user_0
hintManager.addTableShardingValue("user", 0L);
List<User> userList = SpringUtil.getBean(UserMapper.class).selectByExample(null);
log.info(JSONUtil.toJsonStr(userList));
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//因為hintManager是基于threadLocal進(jìn)行傳值,所以用完后注意手動clear清除線程數(shù)據(jù)
hintManager.clear();
}
從輸出結(jié)果就可以看出,我們通過傳參實現(xiàn)參數(shù)驅(qū)動式的分片算法是成功的:
5. 行表達(dá)式分片算法
最后還有一種行表達(dá)式的分片策略算法,只需給定id并在配置給定分片算法即可,使用于簡單的分表算法的實現(xiàn):
## 行表達(dá)式 使用哪一列用作計算分表策略,我們就使用id
spring.shardingsphere.sharding.tables.user.table-strategy.inline.sharding-column=id
##具體的分表路由策略,我們有3個user表,使用主鍵id取余3,余數(shù)0/1/2分表對應(yīng)表user_0,user_2,user_2
spring.shardingsphere.sharding.tables.user.table-strategy.inline.algorithm-expressinotallow=user_$->{id % 3}
三、小結(jié)
本文結(jié)合源碼實現(xiàn)了解到sharding-jdbc自定義分表算法實現(xiàn)的拓展點,并基于該拓展點完成我們的的號頭分表邏輯,希望對你有所幫助。