一、背景
最近的應用場景中,遇到了單表數據量太大,影響效率,分表的情況。所以就出現了“單庫分表”這個需求。一開始我是自己寫的工具類,但是這樣業務代碼就不簡潔,每次CRUD操作之前都要自己計算表名。更嚴重的問題是我沒有考慮到在關聯表中主鍵重複這個問題,會導致業務異常。第一考慮的是每張表的主鍵分段,但是這樣就要經常關注數據庫的情況,萬一預估的數據量不準,又回出現主鍵重複。身邊的同學有用過sharding-jdbc的,反映還不錯,所以我就開始了踩坑之路。
二、理論
sharding-jdbc可以保證全局主鍵唯一。
sharding-jdbc表的分片策略有四種,我的情況是根據兩個字段分表,也就是多分片鍵,所以要使用complex模式,這個比標準的分片場景稍微複雜一點,要自己寫分片策略。
更多的理論知識請查看參考文檔,因爲我是一個坑貨,沒辦法和您解釋。
三、實踐
第一,引入依賴
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
第二,數據庫、分表配置
# application-test.yml 測試環境DB配置
sharding:
jdbc:
datasource: #配置數據源
user:
url:
username:
password:
config:
sharding:
props:
sql:
show: true #打印SQL
# application.yml 公共配置
sharding:
jdbc:
datasource:
names: user #數據源
user:#數據庫配置
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 100
max-wait: 10000
validation-query: SELECT 1 FROM DUAL
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 30000
min-evictable-idle-time-millis: 30000
config:
sharding:
tables:#表的配置
user_base_info:#邏輯表名
actual-data-nodes: user.user_base_info$->{2019..2022}_0$->{0..7}#真實的表名
table-strategy.complex.sharding-columns: finance_year,hash_code#分表字段
table-strategy.complex.algorithm-class-name: #自定義分表策略實現類,要實現ComplexKeysShardingAlgorithm接口
key-generator-column-name: id#主鍵
default-data-source-name: user #默認數據庫
第三,分表策略實現
import io.shardingsphere.api.algorithm.sharding.ListShardingValue;
import io.shardingsphere.api.algorithm.sharding.ShardingValue;
import io.shardingsphere.api.algorithm.sharding.complex.ComplexKeysShardingAlgorithm;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class WalletComplexKeysShardingAlgorithmImpl implements ComplexKeysShardingAlgorithm {
private static String logicTableName = "";
/**
* 自定義分片策略
* @param collection 實際表名集合
* @param shardingValues 分片鍵集合
* @return
*/
@Override
public Collection<String> doSharding(Collection<String> collection, Collection<ShardingValue> shardingValues) {
Collection<Integer> financeYearValues = getShardingValue(shardingValues, FINANCE_YEAR);
Collection<Integer> hashCodeValues = getShardingValue(shardingValues, HASH_CODE);
List<String> shardingSuffix = new ArrayList<>();
for (Integer financeYear : financeYearValues) {
for (Integer hashCode : hashCodeValues) {
long numSuffix = 0L;
if (logicTableName.equals(TableTypeEnum.WALLET.getCode())) {
numSuffix = Math.abs(hashCode) % 8;
}
if (logicTableName.equals(TableTypeEnum.WALLET_DETAIL.getCode())
|| logicTableName.equals(TableTypeEnum.WALLET_ACTION.getCode())
|| logicTableName.equals(TableTypeEnum.WALLET_ACTION_REL.getCode())
|| logicTableName.equals(TableTypeEnum.WALLET_DETAIL_BIZ_REL.getCode())) {
numSuffix = Math.abs(hashCode) % 16;
}
String tableSuffix = "";
if (numSuffix < 10) {
tableSuffix = financeYear + "_0" + numSuffix;
} else {
tableSuffix = financeYear + "_" + numSuffix;
}
for (String tableName : collection) {
if (tableName.endsWith(tableSuffix)) {
shardingSuffix.add(tableName);
}
}
}
}
return shardingSuffix;
}
/**
* 獲得分片鍵的值
* @param shardingValues 分片鍵集合
* @param splitKey 分片鍵
* @return
*/
private Collection<Integer> getShardingValue(Collection<ShardingValue> shardingValues, final String splitKey) {
Collection<Integer> valueSet = new ArrayList<>();
for (ShardingValue shardingValue : shardingValues) {
if (shardingValue instanceof ListShardingValue) {
ListShardingValue listShardingValue = (ListShardingValue) shardingValue;
if (listShardingValue.getColumnName().equals(splitKey)) {
logicTableName = listShardingValue.getLogicTableName();
return listShardingValue.getValues();
}
}
}
return valueSet;
}
}
第四,踩坑
聊一聊血淚史,踩坑踩了兩天!
- 第一、多分片鍵要使用complex模式,我一開始沒注意,被inline表達式折磨了很久
- 第二、如果配置的不對,它找不到真正的表,它就會執行所有表
- 第三、大小寫敏感,如果分片鍵,表中字段是小寫,配置文件中是大寫,恭喜你,它沒辦法找到真正的表
- 第四、相同的邏輯表的真實表,必須結構相同。啓動應用的時候,加載配置文件,它會檢查配置的分表表結構是否一致,比如你的數據庫中只有2019年的表,而你配置的是2019-2022年的表,很好,工程會起不來,拋異常表結構不一致。
-
第五、批量insert,不能foreach insert語句,只能foreach values。這樣就會有一個問題,必須指明字段。如果我沒有說清楚,請看代碼
# 不支持的寫法
<insert id="batchInsertSelective" parameterType="java.util.Map">
<foreach collection="userList" index="index" item="userDO" separator=";">
insert into user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userDO.id != null">
id,
</if>
<if test="userDO.createDatetime != null">
create_datetime,
</if>
<if test="userDO.updateDatetime != null">
update_datetime,
</if>
<if test="userDO.createUser != null">
create_user,
</if>
<if test="userDO.updateUser != null">
update_user,
</if>
<if test="userDO.financeYear != null">
finance_year,
</if>
<if test="userDO.hashCode != null">
hash_code,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userDO.id != null">
#{userDO.id,jdbcType=BIGINT},
</if>
<if test="userDO.createDatetime != null">
#{userDO.createDatetime,jdbcType=TIMESTAMP},
</if>
<if test="userDO.updateDatetime != null">
#{userDO.updateDatetime,jdbcType=TIMESTAMP},
</if>
<if test="userDO.createUser != null">
#{userDO.createUser,jdbcType=VARCHAR},
</if>
<if test="userDO.updateUser != null">
#{userDO.updateUser,jdbcType=VARCHAR},
</if>
<if test="userDO.financeYear != null">
#{userDO.financeYear,jdbcType=INTEGER},
</if>
<if test="userDO.hashCode != null">
#{userDO.hashCode,jdbcType=INTEGER},
</if>
</trim>
</foreach>
</insert>
# 支持的寫法
<insert id="batchInsertSelective" parameterType="java.util.List">
insert into wallet_action_rel (
create_user, update_user,finance_year,hash_code
) values
<foreach collection="list" index="index" item="userDO" separator=",">
<trim prefix="(" suffix=")" >
#{userDO.createUser,jdbcType=VARCHAR},
#{userDO.updateUser,jdbcType=VARCHAR},
#{userDO.financeYear,jdbcType=INTEGER},
#{userDO.hashCode,jdbcType=INTEGER}
</trim>
</foreach>
</insert>
-
第六、select和update語句,必須把分片鍵寫入到where條件中,否則就操作所有的表。這裏就要注意SQL的效率,分片鍵最好建索引,否則很影響查詢效率。
-
更多配置,請移步官網,我用的是SpringBoot配置
四、總結
之前看過一個大佬說的話,覺得很有道理,"能不分表就不分表,能用分區表就不要用物理分表"。sharding-jdbc還有很多限制,因爲我的應用場景比較簡單,所以目前還沒有遇到,僅供參考。
膜拜各路大佬:
Spring Boot中整合Sharding-JDBC單庫分表示例 (第二篇)