#Sharding-JDBC之单库分表
##应用场景
最近的应用场景中,遇到了单表数据量太大,影响效率,分表的情况。所以就出现了“单库分表”这个需求。一开始我是自己写的工具类,但是这样业务代码就不简洁,每次CRUD操作之前都要自己计算表名。更严重的问题是我没有考虑到在关联表中主键重复这个问题,会导致业务异常 公司里有用过sharding-jdbc的,反映还不错,所以我就开始了踩坑之路。
##sharding-jdbc
sharding-jdbc可以保证全局主键唯一。
sharding-jdbc表的分片策略有四种,我的情况是根据两个字段分表,也就是多分片键,所以要使用complex模式,这个比标准的分片场景稍微复杂一点,要自己写分片策略。
###1.引入依赖
io.shardingsphere
sharding-jdbc-spring-boot-starter
3.1.2
###2.数据库、分表配置
···
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;
}
###4.问题
第一、多分片键要使用complex模式,我一开始没注意,被inline表达式折磨了很久
第二、如果配置的不对,它找不到真正的表,它就会执行所有表
第三、大小写敏感,如果分片键,表中字段是小写,配置文件中是大写,恭喜你,它没办法找到真正的表
第四、相同的逻辑表的真实表,必须结构相同。启动应用的时候,加载配置文件,它会检查配置的分表表结构是否一致,比如你的数据库中只有2019年的表,而你配置的是2019-2022年的表,很好,工程会起不来,抛异常表结构不一致。
5.批量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>
###6.select和update语句,必须把分片键写入到where条件中,否则就操作所有的表。这里就要注意SQL的效率,分片键最好建索引,否则很影响查询效率。
之前看过一个大佬说的话,觉得很有道理,“能不分表就不分表,能用分区表就不要用物理分表”。sharding-jdbc还有很多限制,因为我的应用场景比较简单,所以目前还没有遇到,仅供参考。