一、背景
最近的应用场景中,遇到了单表数据量太大,影响效率,分表的情况。所以就出现了“单库分表”这个需求。一开始我是自己写的工具类,但是这样业务代码就不简洁,每次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单库分表示例 (第二篇)
sharding-jdbc的SpringBoot版配置项说明