ShardingSphere复合分片

  上一篇《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,我们配置的数据库数量是4ds0,ds1,ds2,ds3),表数量是8(t_order_0000...t_order_0007),那么这个门店所在的数据库就是017/8=2.......1,也就是在下标为2ds2)代表的数据库(第三个数据库)。下标为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();
	    }
	}

以上就生成了符合“保证同一订单的useridorderid字符串下标的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的历史数据迁移上会比传统的数据迁移更有挑战,历史数据可能需要单独的分片算法来进行分片,具体场景具体分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值