SpringBoot集成ShardingSphere分表中间件

ShardingSphere简介

ShardingSphere 由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的基于数据库作为存储节点的增量功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。

关系型数据库当今依然占有巨大市场份额,是企业核心系统的基石,未来也难于撼动,我们更加注重在原有基础上提供增量,而非颠覆。

ShardingSphere-JDBC

定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。

  • 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC

  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, HikariCP 等;

  • 支持任意实现 JDBC 规范的数据库,目前支持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使用 JDBC 访问的数据库。

总结 兼容性好 , 目前主流的ORM框架均能够支持、而且只需要在pom文件中引入依赖即可、使用起来非常方便。

查看更多关于分库分表、读写分离:https://mp.weixin.qq.com/s/aFXZ8rT9g4oj3ZnioC2vkg

添加依赖

<!-- 关系型数据库中间件sharding -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.1</version>
</dependency>

配置数据源

@Configuration
public class DruidConfig{

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        setDataSource(targetDataSources, DataSourceType.SHARDING.name(), "shardingDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
}
/**
 * 数据源
 */
public enum DataSourceType
{
    /**
     * 主库
     */
    MASTER,
    /**
     * 从库
     */
    SLAVE,
    /**
     * 分表
     */
    SHARDING
}
/**
 * 在Mapper类中配置分表数据源
 */
@DataSource(DataSourceType.SHARDING)
public interface TestMapper {
    // 针对分表的所有增删改查操作,都要重新写对应的SQL语句,否则直接mybatis接口不会生效
}

添加yml配置

spring:
    main:
        # 一个实体类对应多张表,必须设置这个
        allow-bean-definition-overriding: true
    shardingsphere:
        props:
            sql:
                # 打印SQL
                show: false
        datasource:
            names: master
            master:
                type: com.alibaba.druid.pool.DruidDataSource
                driver-class-name: com.mysql.cj.jdbc.Driver
                url: jdbc:mysql://localhost:3306/times_tool?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: 123456
        sharding:
            tables:
                ts_dir_oss:
                    # 配置表规则
                    actual-data-nodes: master.ts_dir_oss_$->{2013..2023}_$->{(1..12).collect{t ->t.toString().padLeft(2,'0')}}_$->{(1..2).collect{t ->t.toString().padLeft(2,'0')}}
                    # 配置主键id生成策略,指定雪花算法
                    key-generator:
                        column: id
                        type: SNOWFLAKE
                    table-strategy:
                        standard:
                            # 分片字段
                            sharding-column: point_time
                            # 标准策略 + 精确分片算法 SQL就是(=或in)
                            precise-algorithm-class-name: com.cn.framework.datasource.PointTimePreciseShardingAlgorithm
                            # 标准策略 + 范围分片算法  主要是(between A and B)
                            range-algorithm-class-name: com.cn.framework.datasource.PointTimeRangeShardingAlgorithm
                        # 复合分片策略  提供对SQL语句中的(=,in,beteewn A and B)的分片操作支持
#                        complex:
#                            sharding-columns: point_time
#                            algorithm-class-name: com.cn.framework.datasource.ComplexShardingAlgorithm

代码实现:一张表存储半月数据

复合分片策略

/**
 * 复合分片策略  提供对SQL语句中的(=,in,beteewn A and B)的分片操作支持.
 */
@Slf4j
public class ComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm {

    @Override
    public Collection<String> doSharding(Collection collection, ComplexKeysShardingValue complexKeysShardingValue) {
        Set<String> tables = new HashSet<>();
        Map<String, Range<String>> rangeMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
        if(CollUtil.isNotEmpty(rangeMap)){
            for(String s : rangeMap.keySet()){
                Range<String> valueRange = rangeMap.get(s);
                if(valueRange!=null){
                    Date begin = DateUtil.parse(valueRange.lowerEndpoint(), DateUtils.YYYY_MM);
                    Date end = DateUtil.parse(valueRange.upperEndpoint(),DateUtils.YYYY_MM);
                    log.info("lowerSuffix:{},upperSuffix:{}",begin,end);
                    for(Object tab:collection){
                        String tableName = (String)tab;
                        String dateStr = StrUtil.subAfter(tableName, "ts_dir_oss_", true).replace("_", "-");
                        Date date = DateUtil.parse(dateStr,DateUtils.YYYY_MM);
                        if((date.after(begin) || date.equals(begin)) && (date.before(end) || date.equals(end) )){
                            tables.add(tableName);
                        }
                    }
                }
            }
        }
        Map<String, List<String>> map = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        if(CollUtil.isNotEmpty(map)){
            for(String s : map.keySet()){
                List<String> list = map.get(s);
                if(list.size()>0){
                    String v = list.get(0);
                    Date date = DateUtil.parseDate(v);
                    String suffix = getSuffixByDate(date);
                    for(Object tab: collection){
                        String tableName = (String)tab;
                        if(tableName.endsWith(suffix)){
                            tables.add(tableName);
                        }
                    }
                }
            }
        }
        log.info("match tableNames:{}", tables.toString());
        return tables;
    }

    public String getSuffixByDate(Date date){
        String dateStr = DateUtil.format(date,"yyyy_MM");
        int day = DateUtil.date(date).dayOfMonth();
        if(day<=15) {
            return dateStr + "_01";
        }else{
            return dateStr + "_02";
        }
    }
}

标准策略+精确分片算法

/**
 * 标准策略 + 精确分片算法 SQL就是(=或in)
 */
@Slf4j
public class PointTimePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

    @Override
    public String doSharding(Collection<String> availableTargetName, PreciseShardingValue<String> preciseShardingValue) {
        Date date = DateUtil.parseDate(preciseShardingValue.getValue());
        String suffix = getSuffixByDate(date);
        for(String tabName: availableTargetName){
            if(tabName.endsWith(suffix)){
                log.info("match tableName:{}", tabName);
                return tabName;
            }
        }
        throw new IllegalArgumentException("未找到匹配的数据表");
    }
    public String getSuffixByDate(Date date){
        String dateStr = DateUtil.format(date,"yyyy_MM");
        int day = DateUtil.date(date).dayOfMonth();
        if(day<=15) {
            return dateStr + "_01";
        }else{
            return dateStr + "_02";
        }
    }
}

标准策略+范围分片算法

/**
 * 标准策略 + 范围分片算法  主要是(between A and B)
 */
@Slf4j
public class PointTimeRangeShardingAlgorithm implements RangeShardingAlgorithm<String> {

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetName, RangeShardingValue<String> rangeShardingValue) {
        Range<String> valueRange = rangeShardingValue.getValueRange();
        Date begin = DateUtil.parse(valueRange.lowerEndpoint(), DateUtils.YYYY_MM);
        Date end = DateUtil.parse(valueRange.upperEndpoint(),DateUtils.YYYY_MM);
        log.info("lowerSuffix:{},upperSuffix:{}",begin,end);
        Set<String> tables = new HashSet<>();
        for(String tableName:availableTargetName){
            String dateStr = StrUtil.subAfter(tableName, "ts_dir_oss_", true).replace("_", "-");
            Date date = DateUtil.parse(dateStr,DateUtils.YYYY_MM);
            if((date.after(begin) || date.equals(begin)) && (date.before(end) || date.equals(end) )){
                tables.add(tableName);
            }
        }
        log.info("match tableNames:{}", tables.toString());
        return tables;
    }
}

动态新增数据表脚本

CREATE TABLE IF NOT EXISTS ts_dir_oss_${tableSuffix} (
  `id` BIGINT(20) AUTO_INCREMENT NOT NULL,
  `nodes_id` bigint(20) DEFAULT NULL COMMENT '目录节点id',
  `point_id` varchar(48) DEFAULT NULL COMMENT '测点名称',
  `point_time` varchar(18) DEFAULT NULL COMMENT '测点时间',
  `path` varchar(225) DEFAULT NULL COMMENT '地址',
  `size` bigint(11) DEFAULT NULL COMMENT '文件大小',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_point_time` (`point_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

动态更新分片策略

@Component
public class ShardingTableRuleActualTablesRefresh {

    private Logger logger = LoggerFactory.getLogger(ShardingTableRuleActualTablesRefresh.class);

    @Resource(name = "shardingDataSource")
    private DataSource dataSource;

    @PostConstruct
    public void initData(){
        try {
            List<String> sdList = new ArrayList<>();
            // 启动项目时初始化分片规则,查出分片配置表信息
            actualTablesRefresh(sdList);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 动态刷新变量表的分片策略, 核心通过反射更新tableRule
     * @param idList
     */
    public void actualTablesRefresh(List<String> sdList) {
        try {
            logger.info("-----开始刷新表节点-----");
            if(CollUtil.isEmpty(idList)){
                logger.error("传入idList参数为空");
                return;
            }
            ShardingDataSource shardingDataSource = (ShardingDataSource)dataSource;
            //运行时获取分片规则
            ShardingRule rule = shardingDataSource.getRuntimeContext().getRule();

            //获取分表策略集合
            Collection<TableRule> tableRules = rule.getTableRules();

            for (TableRule tableRule : tableRules) {
                //获取真实节点
                List<DataNode> actualDataNodes = tableRule.getActualDataNodes();

                Field actualDataNodesField = TableRule.class.getDeclaredField("actualDataNodes");
                Field modifiersField = Field.class.getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(actualDataNodesField, actualDataNodesField.getModifiers() & ~Modifier.FINAL);

                //数据源名
                String dataSourceName = actualDataNodes.get(0).getDataSourceName();
                //逻辑表名
                String logicTableName = tableRule.getLogicTable();
                //根据真实业务,新增节点的逻辑
                for (String sd : sdList) {
                    actualDataNodes.add(new DataNode(dataSourceName+"."+logicTableName+"_" + sd));
                }
                actualDataNodesField.setAccessible(true);
                actualDataNodesField.set(tableRule, actualDataNodes);

                Set<String> actualTables = Sets.newHashSet();
                Map<DataNode, Integer> dataNodeIntegerMap = Maps.newHashMap();
                //更新actualTables、dataNodeIntegerMap
                AtomicInteger a = new AtomicInteger(0);
                actualDataNodes.forEach((dataNode -> {
                    actualTables.add(dataNode.getTableName());
                    if (a.intValue() == 0){
                        a.incrementAndGet();
                        dataNodeIntegerMap.put(dataNode, 0);
                    }else {
                        dataNodeIntegerMap.put(dataNode, a.intValue());
                        a.incrementAndGet();
                    }
                }));

                //动态刷新:actualTables
                Field actualTablesField = TableRule.class.getDeclaredField("actualTables");
                actualTablesField.setAccessible(true);
                actualTablesField.set(tableRule, actualTables);
                //动态刷新:dataNodeIndexMap
                Field dataNodeIndexMapField = TableRule.class.getDeclaredField("dataNodeIndexMap");
                dataNodeIndexMapField.setAccessible(true);
                dataNodeIndexMapField.set(tableRule, dataNodeIntegerMap);
                //动态刷新:datasourceToTablesMap
                Map<String, Collection<String>> datasourceToTablesMap = Maps.newHashMap();
                datasourceToTablesMap.put(dataSourceName, actualTables);
                Field datasourceToTablesMapField = TableRule.class.getDeclaredField("datasourceToTablesMap");
                datasourceToTablesMapField.setAccessible(true);
                datasourceToTablesMapField.set(tableRule, datasourceToTablesMap);
                logger.info("-----------------end----------------");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意事项

  • sharding不支持部分复杂的聚合查询,如果要有统计数据需求,可以用一个统计表实时或定时实现数据同步。
  • 数据表需要根据切片规则提前创建好,否则程序会报错提示找不到指定表。
posted @ 2022-09-26 16:03  空还是空  阅读(126)  评论(0编辑  收藏  举报