上一篇《ShardingSphere入门》中有提到,要想利用ShardingSphere设计一套满足项目需求的分库分表解决方案,分片策略是关键,其中分片策略又由分片键和分片算法组成,所以在设计分库分表解决方案的时候,我着重于分片键以及分片算法的设计。
分片键选择
shardingsphere分库分表目的首先是解决数据量过大,数据可能存储不下的问题,其次就是性能问题。数据量过大进行分片就可以解决了,那性能问题该如何处理呢?
在shardingsphere jdbc中,假设我们使用order_id作为分片键,那么如果使用user_id进行数据查询的时候,就会走全路由,也就是会查询所有分片。对于大型商城的订单系统,他们订单表可能会分成百上千个分片,那么如果走全路由,这对性能来说是非常大的损耗,所以我们不能只考虑订单id作为分片键,需要根据业务考量,再结合合适的分片算法,提升sql执行性能。
针对订单表,考虑到我方系统主要是新增和查询交易,其中查询又分为根据用户id查询以及根据订单id查询,所以我们可以将userid以及orderid设置为分片键。这时候有些人可能会有疑惑,还会有根据时间进行范围查询的呀,怎么不考虑进去?可以结合传统的商城购买场景,凡是范围查询,userid都是必传的,所以这种查询也是包含分片键的,也就是说并不会去查询全分片。
分片算法选择
为保证使用多个分片键的同时,查询效率还比较高,我这边使用Type为Complex自定义的“求模取余”算法,那么该自定义算法如何设计呢?
首先我们必须要明确,执行订单表的insert语句时,根据shardingsphere提供的ComplexKeysShardingAlgorithm接口得到的分片结果,其必须是唯一值,也就是说除了计算得到的分片数不能为零之外,根据多个分片键(shardingKeys)以及对应分片值(shardingValues)计算得到的分片数不能为多个值,这一点很重要。区别于订单表的update、delete、select语句,这三个sql语句是可以产生多个分片数的。所以在设计分片算法时候,需要考虑到上述两种不同情况。
下面说一下我多个分片键的复合分片算法设计:
总体逻辑就是一句话:保证在orderid和userid中就能识别出具体是哪个数据库以及哪张表,同时为了避免通过orderid、userid计算分片信息时只有一个值,我们也得保证orderid和userid中代表数据库和表信息的下标值是一致的。
例如:orderid为:11000001012411211632509625400000,userid为:10000001012411211634589095400001,我们将字符串下标为2-4记为数据库的索引,也就是“00”;将字符串下标为5-9记为表的索引,也就是0001。可以看到userid和orderid的这两个值都是相同的,于是他们生成的分片也只会有一个。所以我们需要关注的就只是如何保证同一订单的userid和orderid字符串下标的2-9位所代表的值是一致的?
在分布式系统中,分布式orderid都是由服务端生成的,客户端需要请求分布式id服务接口获取不同的订单id。在我所处的系统中,门店id(记为storeid)是在所有交易中都会由客户端传入的,所以我生成分布式订单id以及userid都和storeid关联,通过storeid来保证同一订单的userid和orderid字符串下标的2-9位所代表的值是一致的,也就是保证同一订单的orderid和userid所代表的分片是一致的。
所以难点就在于如何通过storeid生成上述规则的分布式订单id以及userid?
根据门店id生成分布式订单id以及userid
计算分片
我们配置的数据库数量是4,表数量也是8,在“2”中说明了,我们使用的是求模取余算法,所以我们可以根据门店id就计算出它属于哪个分片。
例如:
假设一个门店是017,我们配置的数据库数量是4(ds0,ds1,ds2,ds3),表数量是8(t_order_0000...t_order_0007),那么这个门店所在的数据库就是017/8=2.......1,也就是在下标为2(ds2)代表的数据库(第三个数据库)。下标为1的表(第二个表,表数量从0开始记的,余数为0才是第一个)
综上所属就是ds2.t_order_0001
设计分布式id
package ddd.application.base.sequence;
import common.toolkit.DateUtils;
import ddd.application.base.StringUtil;
import ddd.application.constants.ShardingConstant;
import ddd.application.enum_.DbAndTableEnum;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.shardingsphere.sharding.algorithm.keygen.SnowflakeKeyGenerateAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author chenjian
* @version 1.0
* @date 2024/11/05 10:17
* @className KeyGenerator
* @desc 自定义分布式主键生成器
*/
@Component
public class KeyGenerator {
@Autowired
private SequenceGenerator sequenceGenerator;
/**默认集群机器总数*/
private static final int DEFAULT_HOST_NUM = 64;
/** 通过门店id获取系统内部所有分片相关的id,包括:订单id、用户id。这就导致同一门店获取到的userid或者orderid所在的数据库index、表index都是相同的,
* 如是就避免了根据userid和orderid insert、update、delete数据时,会出现计算出多个分片的情况。
*
* 同时业务方可以根据门店就预估出该门店所属哪个分片。
* 例如:
* 假设一个门店是017,我们配置的数据库数量是4(ds0,ds1,ds2,ds3),表数量是8(t_order_0000...t_order_0007),那么这个门店所在的数据库就是 017/8 = 2 ....... 1,也就是在下标为2(ds2)代表的数据库(第三个数据库)。下标为1的表(第二个表,表数量从0开始记的,余数为0才是第一个)
* 综上所属就是ds2.t_order_0001
*
* @param targetEnum 待生成主键的目标表规则配置
* @param storeId 门店id
* @return
*/
public String generateKey(DbAndTableEnum targetEnum, String storeId) {
if (StringUtils.isBlank(storeId)) {
throw new IllegalArgumentException("路由id参数为空");
}
StringBuilder key = new StringBuilder();
/** 1.id业务前缀*/
String idPrefix = targetEnum.getCharsPrefix();
/** 2.id数据库索引位*/
String dbIndex = getDbIndexAndTbIndexMap(targetEnum, storeId).get("dbIndex");
/** 3.id表索引位*/
String tbIndex = getDbIndexAndTbIndexMap(targetEnum, storeId).get("tbIndex");
/** 4.id规则版本位*/
String idVersion = targetEnum.getIdVersion();
/** 5.id时间戳位*/
String timeString = DateUtils.formatTime(new Date());
/** 6.id分布式机器位 2位*/
String distributedIndex = getDistributedId(2);
/** 7.随机数位*/
String sequenceId = sequenceGenerator.getNextVal(targetEnum, Integer.parseInt(dbIndex), Integer.parseInt(tbIndex));
/** 库表索引靠前*/
return key.append(idPrefix)
.append(dbIndex)
.append(tbIndex)
.append(idVersion)
.append(timeString)
.append(distributedIndex)
.append(sequenceId).toString();
}
/**
* 根据已知路由id取出库表索引,外部id和内部id均 进行ASCII转换后再对库表数量取模
* @param targetEnum 待生成主键的目标表规则配置
* @param relatedRouteId 路由id
* 取模求表 取商求库
* @return
*/
private Map<String, String> getDbIndexAndTbIndexMap(DbAndTableEnum targetEnum,String relatedRouteId) {
Map<String, String> map = new HashMap<>();
/** 获取库索引*/
String preDbIndex = String.valueOf(StringUtil.getDbIndexByMod(relatedRouteId,targetEnum.getDbCount(),targetEnum.getTbCount()));
String dbIndex = StringUtil.fillZero(preDbIndex, ShardingConstant.DB_SUFFIX_LENGTH);
/** 获取表索引*/
String preTbIndex = String
.valueOf(StringUtil.getTbIndexByMod(relatedRouteId,targetEnum.getDbCount(),targetEnum.getTbCount()));
String tbIndex = StringUtil
.fillZero(preTbIndex,ShardingConstant.TABLE_SUFFIX_LENGTH);
map.put("dbIndex", dbIndex);
map.put("tbIndex", tbIndex);
return map;
}
/**
* 生成id分布式机器位
* @return 分布式机器id
* length与hostCount位数相同
*/
private String getDistributedId(int length, int hostCount) {
return StringUtil
.fillZero(String.valueOf(getIdFromHostName() % hostCount), length);
}
/**
* 生成id分布式机器位
* @return 支持最多64个分布式机器并存
*/
private String getDistributedId(int length) {
return getDistributedId(length, DEFAULT_HOST_NUM);
}
/**
* 适配分布式环境,根据主机名生成id
* 分布式环境下,如:Kubernates云环境下,集群内docker容器名是唯一的
* 通过 @See org.apache.commons.lang3.SystemUtils.getHostName()获取主机名
* @return
*/
private Long getIdFromHostName(){
//unicode code point
int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
int sums = 0;
for (int i: ints) {
sums += i;
}
return (long)(sums);
}
/**
* ShardingSphere 默认雪花算法 {@link org.apache.shardingsphere.sharding.algorithm.keygen.SnowflakeKeyGenerateAlgorithm}
* 生成18位随机id,并提供静态方法设置workId
* @return
*/
public Long getSnowFlakeId() {
return new SnowflakeKeyGenerateAlgorithm().generateKey();
}
/**
* 使用ShardingSphere 内置主键生成器 生成分布式主键id
* 共16位:其中4位减号,12位数字+小写字母
* 形如:3e9c3ac9-d7a1-43de-9db3-dec502e9ab3e
*/
public String getUUID() {
return UUID.randomUUID().toString();
}
}
如上所示,分布式id最终实现就在如下实现了
/** 7.随机数位*/
String sequenceId = sequenceGenerator.getNextVal(targetEnum, Integer.parseInt(dbIndex), Integer.parseInt(tbIndex));
生成序列化数值
在这里,我们可以进行拓展,抽象SequenceGenerator接口
package ddd.application.base.sequence;
import ddd.application.enum_.DbAndTableEnum;
public interface SequenceGenerator {
/**
* 查redis方式 key前缀(形如:tableName_dbIndex_tbIndex_)
* @param targetEnum
* @param dbIndex
* @param tbIndex
* @return
*/
String getNextVal(DbAndTableEnum targetEnum, int dbIndex, int tbIndex);
}
在此是实现了两种序列数生成方式:
借用redis生成分布式序列数:
package ddd.application.base.sequence.redis;
import ddd.application.base.StringUtil;
import ddd.application.base.sequence.SequenceGenerator;
import ddd.application.constants.ShardingConstant;
import ddd.application.enum_.DbAndTableEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* @author chenjian
* @version 1.0
* @date 2024/11/05 11:09
* @className RedisSequenceGenerator
* @desc 序列生成器Redis实现
*/
public class RedisSequenceGenerator implements SequenceGenerator {
/**序列生成器key前缀*/
public static String LOGIC_TABLE_NAME = "sequence:redis:";
public static int SEQUENCE_LENGTH = 5;
public static int sequence_max = 90000;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* redis序列获取实现方法
* @param targetEnum
* @param dbIndex
* @param tbIndex
* @return
*/
@Override
public String getNextVal(DbAndTableEnum targetEnum, int dbIndex, int tbIndex) {
//拼接key前缀
String redisKeySuffix = new StringBuilder(targetEnum.getTableName())
.append("_")
.append("dbIndex")
.append(StringUtil.fillZero(String.valueOf(dbIndex), ShardingConstant.DB_SUFFIX_LENGTH))
.append("_tbIndex")
.append(StringUtil.fillZero(String.valueOf(tbIndex), ShardingConstant.TABLE_SUFFIX_LENGTH))
.append("_")
.append(targetEnum.getShardingKey()).toString();
String increKey = new StringBuilder(LOGIC_TABLE_NAME).append(redisKeySuffix).toString();
long sequenceId = stringRedisTemplate.opsForValue().increment(increKey);
//达到指定值重置序列号,预留后10000个id以便并发时缓冲
if (sequenceId == sequence_max) {
stringRedisTemplate.delete(increKey);
}
// 返回序列值,位数不够前补零
return StringUtil.fillZero(String.valueOf(sequenceId), SEQUENCE_LENGTH);
}
public static void main(String[] args) {
}
}
使用jvm内存生成序列数:
package ddd.application.base.sequence.memory;
import ddd.application.base.StringUtil;
import ddd.application.base.sequence.SequenceGenerator;
import ddd.application.enum_.DbAndTableEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author chenjian
* @version 1.0
* @date 2024/11/05 11:09
* @className RedisSequenceGenerator
* @desc 序列生成器Redis实现
*/
public class MemorySequenceGenerator implements SequenceGenerator {
public static int SEQUENCE_LENGTH = 5;
private static AtomicLong seq = new AtomicLong(-1);
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* redis序列获取实现方法
* @param targetEnum
* @param dbIndex
* @param tbIndex
* @return
*/
@Override
public String getNextVal(DbAndTableEnum targetEnum, int dbIndex, int tbIndex) {
int maxSeq = 100000;
long num = seq.incrementAndGet();
num = num%maxSeq;
String formatSeq = String.format("%05d",num);
// 返回序列值,位数不够前补零
return StringUtil.fillZero(String.valueOf(formatSeq), SEQUENCE_LENGTH);
}
public static void main(String[] args) {
System.out.println();
}
}
利用spring设置序列数生成优先级,如果存在redis就使用redis的规则,如果没有就基于内存生成序列值。
package ddd.application.base.sequence.config;
import ddd.application.base.sequence.SequenceGenerator;
import ddd.application.base.sequence.memory.MemorySequenceGenerator;
import ddd.application.base.sequence.redis.RedisSequenceGenerator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class SequenceGeneratorConfig {
/** 优先使用redis生成序列号,没有则使用java 内存生成
*
* @return
*/
@Bean
@Primary
@ConditionalOnBean(name = {"redisTemplate","stringRedisTemplate"})
public SequenceGenerator sequenceRedisBean() {
return new RedisSequenceGenerator();
}
@Bean
@ConditionalOnMissingBean(name = {"redisSequenceGenerator"})
public SequenceGenerator sequenceDataSourceBean() {
return new MemorySequenceGenerator();
}
}
以上就生成了符合“保证同一订单的userid和orderid字符串下标的2-9位所代表的值是一致的”条件的分布式id,接下来就是进行shardingsphere相关的编码了。
ShardingSphere分片算法
application.yml文件:
spring:
shardingsphere:
props:
sql-show: true
datasource:
names: ds0,ds1,ds2,ds3
ds0:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/jpacqdb0?useUnicode=true&autoReconnect=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ******
pool-name: HikariPool-0
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 500000
max-lifetime: 540000
connection-timeout: 60000
connection-test-query: SELECT 1
ds1:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/jpacqdb1?useUnicode=true&autoReconnect=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ******
pool-name: HikariPool-1
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 500000
max-lifetime: 540000
connection-timeout: 60000
connection-test-query: SELECT 1
ds2:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/jpacqdb2?useUnicode=true&autoReconnect=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ******
pool-name: HikariPool-2
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 500000
max-lifetime: 540000
connection-timeout: 60000
connection-test-query: SELECT 1
ds3:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://localhost:3306/jpacqdb3?useUnicode=true&autoReconnect=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ******
pool-name: HikariPool-3
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 500000
max-lifetime: 540000
connection-timeout: 60000
connection-test-query: SELECT 1
rules:
sharding:
tables:
#自定义标准分片策略
t_order:
actual-data-nodes: ds$->{0..3}.t_order_000$->{0..3}
database-strategy:
complex:
sharding-columns: user_id,order_id
sharding-algorithm-name: algorithm_db
table-strategy:
complex:
sharding-columns: user_id,order_id
sharding-algorithm-name: algorithm_table
sharding-algorithms:
algorithm_db:
type: CLASS_BASED
props:
strategy: COMPLEX
algorithmClassName: ddd.infrastructure.sharding.ComplexShardingDB
algorithm_table:
type: CLASS_BASED
props:
strategy: COMPLEX
algorithmClassName: ddd.infrastructure.sharding.ComplexShardingTB
数据库分片算法
package ddd.infrastructure.sharding;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Range;
import ddd.application.base.StringUtil;
import ddd.application.constants.ShardingConstant;
import ddd.application.enum_.DbAndTableEnum;
import org.apache.commons.lang.StringUtils;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.util.*;
import static ddd.application.base.StringUtil.getDbIndexBySubString;
/**
* @author chenjian
* @version 1.0
* @date 2024/11/20 10:55
* @desc 自定义复合分片规则--数据源分片规则
*/
public class ComplexShardingDB implements ComplexKeysShardingAlgorithm<BigInteger> {
private static final Logger log = LoggerFactory.getLogger(ComplexShardingDB.class);
private Properties props;
/**
* 根据分片键计算分片节点
* @param logicTableName
* @param columnName
* @param shardingValue
* @return
*/
public String getIndex(String logicTableName, String columnName, String shardingValue) {
String index = "";
if (StringUtils.isBlank(shardingValue)) {
throw new IllegalArgumentException("分片键值为空");
}
//截取分片键值-下标循环主键规则枚举类,匹配主键列名得到规则
for (DbAndTableEnum targetEnum : DbAndTableEnum.values()) {
/**目标表路由
* 如果逻辑表命中,判断路由键是否与列名相同
*/
if (targetEnum.getTableName().equals(logicTableName)) {
//目标表的目标主键路由-例如:根据订单id查询订单信息
index = getDbIndexBySubString(targetEnum, shardingValue);
break;
}
}
if (StringUtils.isBlank(index)) {
String msg = "从分片键值中解析数据库索引异常:logicTableName=" + logicTableName + "|columnName=" + columnName + "|shardingValue=" + shardingValue;
throw new IllegalArgumentException(msg);
}
return index;
}
/**
* 内部用户id使用取模方式对目标表库表数量取模获取分片节点
* @param shardingValue
* @return
*/
public String getDbIndexByMod(DbAndTableEnum targetEnum,String shardingValue) {
String index = String.valueOf(StringUtil.getDbIndexByMod(shardingValue,targetEnum.getDbCount(),targetEnum.getTbCount()));
return index;
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<BigInteger> shardingValue) {
log.info("availableTargetNames:" + JSON.toJSONString(availableTargetNames) + ",shardingValues:" + JSON.toJSONString(shardingValue));
//进入通用复杂分片算法-抽象类:availableTargetNames=["ds0","ds1","ds2","ds3"],
// shardingValue包含三个变量
// logicTableName逻辑库的名称,对应t_order
// columnNameAndShardingValuesMap,列名与该列的分片值的映射关系,分片值可以是多个.
// columnNameAndRangeValuesMap,列名与该列的分片范围的映射关系,Range 类型通常用于表示一个区间.
Set<String> shardingResults = new HashSet<>();
String logicTableName = shardingValue.getLogicTableName();
Map<String, Collection<BigInteger>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
Map<String, Range<BigInteger>> columnNameAndRangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap();
for (String var: columnNameAndShardingValuesMap.keySet()) {
Collection<BigInteger> listShardingValue = columnNameAndShardingValuesMap.get(var);
for (Object id : listShardingValue) {
//根据列名获取索引规则,得到索引值
String index = getIndex(logicTableName,
var, id + "");
//循环匹配数据源
for (String name : availableTargetNames) {
//获取逻辑数据源索引后缀
String nameSuffix = name.substring(ShardingConstant.LOGIC_DB_PREFIX_LENGTH);
if (nameSuffix.equals(index)) {
shardingResults.add(name);
break;
}
}
}
}
return shardingResults;
}
@Override
public Properties getProps() {
return props;
}
@Override
public void init(Properties props) {
this.props = props;
}
}
表分片算法:
package ddd.infrastructure.sharding;
import com.alibaba.fastjson.JSON;
import ddd.application.base.StringUtil;
import ddd.application.constants.ShardingConstant;
import ddd.application.enum_.DbAndTableEnum;
import org.apache.commons.lang3.StringUtils;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.*;
import static ddd.application.base.StringUtil.getTbIndexBySubString;
/**
* @author chenjian
* @version 1.0
* @date 2024/11/20 14:55
* @desc 通用复杂分片算法-表路由
*/
public class ComplexShardingTB implements ComplexKeysShardingAlgorithm<BigInteger> {
private static final Logger log = LoggerFactory.getLogger(ComplexShardingTB.class);
private Properties props;
@Override
public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
log.info("availableTargetNames:" + JSON.toJSONString(availableTargetNames) + ",shardingValues:" + JSON.toJSONString(shardingValue));
//进入通用复杂分片算法-抽象类:availableTargetNames=["t_order_0000","t_order_0001","t_order_0002","t_order_0003"],
// shardingValue包含三个变量
// logicTableName逻辑表的名称,对应t_order
// columnNameAndShardingValuesMap,列名与该列的分片值的映射关系,分片值可以是多个.
// columnNameAndRangeValuesMap,列名与该列的分片范围的映射关系,Range 类型通常用于表示一个区间.
Set<String> shardingResults = new HashSet<>();
String logicTableName = shardingValue.getLogicTableName();
Map<String, Collection<BigInteger>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
Map<String, Collection<BigInteger>> columnNameAndRangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap();
// 不同key的处理
for (String var: columnNameAndShardingValuesMap.keySet()) {
Collection<BigInteger> listShardingValue = columnNameAndShardingValuesMap.get(var);
log.info("shardingValue:" + JSON.toJSONString(shardingValue));
// 同一key有多个value的处理
for (Object id : listShardingValue) {
//根据列名获取索引规则,得到索引值
String index = getIndex(logicTableName,
var, id + "");
//循环匹配表信息
for (Object availableTargetName : availableTargetNames) {
if (availableTargetName.toString().endsWith("_" + index)) {
shardingResults.add(availableTargetName.toString());
break;
}
}
}
}
return shardingResults;
}
@Override
public Properties getProps() {
return props;
}
@Override
public void init(Properties props) {
this.props = props;
}
/**
* 根据分片键计算分片节点
* @param logicTableName
* @param columnName
* @param shardingValue
* @return
*/
public String getIndex(String logicTableName,String columnName,String shardingValue) {
String index = "";
if (StringUtils.isBlank(shardingValue)) {
throw new IllegalArgumentException("分片键值为空");
}
//截取分片键值-下标循环主键规则枚举类,匹配主键列名得到规则
for (DbAndTableEnum targetEnum : DbAndTableEnum.values()) {
//目标表路由
if (targetEnum.getTableName().equals(logicTableName)) {
//目标表的目标主键路由-例如:根据订单id查询订单信息
index = getTbIndexBySubString(targetEnum, shardingValue);
break;
}
}
if (StringUtils.isBlank(index)) {
String msg = "从分片键值中解析表索引异常:logicTableName=" + logicTableName + "|columnName=" + columnName + "|shardingValue=" + shardingValue;
throw new IllegalArgumentException(msg);
}
return index;
}
/**
* 内部用户id使用取模方式对订单表库表数量取模获取分片节点
* @param shardingValue
* @return
*/
public String getTbIndexByMod(DbAndTableEnum targetEnum, String shardingValue) {
String index = StringUtil.fillZero(String.valueOf(StringUtil.getTbIndexByMod(shardingValue,targetEnum.getDbCount(),targetEnum.getTbCount())), ShardingConstant.TABLE_SUFFIX_LENGTH);
return index;
}
}
总结
纵观上述分片逻辑,可以发现是根据门店id来决定所属分片的,在实际业务场景中,如果一个门店订单量特别大,所分的数据片也无法保存这些大量数据,那怎么办?
针对此场景,我们可以引入商户概念,一个商户对应多个门店。如果一个门店订单特别大,我们就给它多分配几个门店,这些门店同属于一个商户,计算总收益从商户角度结算就。当然这只是一个参考,各位完全可以由不同的实现思路。
以上就是根据userid和orderid生成的复合分片策略,优点是查询效率比普通的单字段(只通过orderid)inline分片策略要高很多。但是缺点是历史数据的userid或者orderid的生成策略很可能不是按照上述分布式id生成规则,所以在shardingsphere的历史数据迁移上会比传统的数据迁移更有挑战,历史数据可能需要单独的分片算法来进行分片,具体场景具体分析。