简介

分库分表是一种将数据拆分成数据库或表中的策略,例如按照时间的月份创建新的表,将数据隔离开,用于提高查询效率,我们可以使用 Apache 提供的服务 ShardingSphere,用于简化数据库分片、读写分离、分布式事务等复杂场景的管理。

使用步骤一:引入相关依赖

Apache ShardingSphere分为两种模块一种是JDBC一种是proxy,应用于不同的场景和语言,在Java中,我们选择JDBC版本即可,他仅支持Java

<!-- 分库分表 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.2.0</version>
</dependency>

步骤二:进行相关配置

我们在yaml中可以对shardingSphere进行相对应的配置

spring:
  # 使用 shardingSphere 空间图片分表
  shardingsphere:
    datasource:
      names: zzx_picture  # 数据库名称
      zzx_picture: # 数据源名称
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://${picture.mysql.host}:${picture.mysql.port}/${picture.mysql.database}
        username: ${picture.mysql.username}
        password: ${picture.mysql.password}
    rules:
      sharding:
        tables:
          picture: # 配置 picture 表的分片规则
            actual-data-nodes: zzx_picture.picture  # 动态分表
            table-strategy:
              standard:
                sharding-column: spaceId # 分表字段
                sharding-algorithm-name: picture_sharding_algorithm  # 使用自定义分片算法
        sharding-algorithms:
          picture_sharding_algorithm:
            type: CLASS_BASED
            props:
              strategy: standard # 标准分片
              algorithmClassName: com.zzx.zzxpicturebackend.manager.sharding.PictureShardingAlgorithm # 自定义分片算法
    props:
      sql-show: true # 显示分片后的实际 SQL 语句,用于调试

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false    # MyBatis 下划线转驼峰
    # 仅在开发环境开启日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    # 启用 SQL 运行器
    enable-sql-runner: true
    db-config:
      logic-delete-field: is_delete  # 全局逻辑删除
      logic-delete-value: 1    # 逻辑删除值(默认为1)
      logic-not-delete-value: 0  # 逻辑未删除值(默认为0)
  mapper-locations: classpath:/mapper/**.xml

我们在分片规则 actual-data-nodes配置中只写了基本数据表,因为我们业务需求是创建动态表,所以这里以动态分表为例

这里的配置需要改成自己的数据库的相关配置和MybatisPlus的相关配置

这里的配置需要注意的是:

actual-data-nodes 一般指定的你分表的范围,比如 ds0.table_${0..9999},也可以直接写成ds0.table1,ds0.table2

我们还需要指定分表字段,比如spaceId

需要配置指定一下分片算法类,这个分片算法类主要是需要实现StandardShardingAlgorithm接口,编写doSharding方法,泛型为你分表字段的类型,比如日期就是Data

步骤三:书写相关类

我们需要写两个类,一个类为自己实现分表算法类,一个为动态分表管理相关类

分表算法类:

package com.zzx.zzxpicturebackend.manager.sharding;

import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Properties;

/**
 * 图片分片算法
 */
public class PictureShardingAlgorithm implements StandardShardingAlgorithm<Long> {

    /**
     *
     * @param collection           所有分表名 是一个固定值,由yaml中 actual-data-nodes 指定
     * @param preciseShardingValue 分片键值
     * @return 分表名
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        Long spaceId = preciseShardingValue.getValue();
        String logicTableName = preciseShardingValue.getLogicTableName();
        // spaceId 为 null 表示查询所有图片
        if (spaceId == null || spaceId == 0) {
            return logicTableName;
        }
        // 根据 spaceId 动态生成分表名
        String realTableName = "picture_" + spaceId;
        if (collection.contains(realTableName)) {
            // 找到匹配的目标表
            return realTableName;
        }else{
            // 未找到匹配的目标表,返回逻辑表
            return logicTableName;
        }
    }

    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
        return new ArrayList<>();
    }

    @Override
    public Properties getProps() {
        return null;
    }

    @Override
    public void init(Properties properties) {

    }
}

如果只实现分表算法类,还不能完成动态分表,

因为 doSharding方法中的 collection 字段是在yaml中 actual-data-nodes直接确定的,所以我们需要自己维护分表列表,并且更新在到 ShardingSphere 中 actual-data-nodes配置中

我们可以写一个动态分表管理类,用于首次加载项目是需要更新分表配置,当项目读取完yaml配置后,然后再把读取到的配置给修改了

最后在这个管理类中创建动态分表的方法,通过拼接SQL 的方式创建出和 picture 表结构一样的分表

因为框架不支持动态维护分表,所以我们需要自己写一个分表管理器,自己维护分表,并且在项目加载完配置文件后动态更新到加载后的配置中

package com.zzx.zzxpicturebackend.manager.sharding;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
import com.zzx.zzxpicturebackend.constant.ShardingSphereConstant;
import com.zzx.zzxpicturebackend.exception.BusinessException;
import com.zzx.zzxpicturebackend.exception.ErrorCode;
import com.zzx.zzxpicturebackend.model.enums.SpaceLevelEnum;
import com.zzx.zzxpicturebackend.model.enums.SpaceTypeEnum;
import com.zzx.zzxpicturebackend.model.po.Space;
import com.zzx.zzxpicturebackend.service.SpaceService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.driver.jdbc.core.connection.ShardingSphereConnection;
import org.apache.shardingsphere.infra.config.rule.RuleConfiguration;
import org.apache.shardingsphere.infra.metadata.database.rule.ShardingSphereRuleMetaData;
import org.apache.shardingsphere.mode.manager.ContextManager;
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
import org.apache.shardingsphere.sharding.route.strategy.ShardingStrategy;
import org.apache.shardingsphere.sharding.rule.ShardingRule;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 动态分表管理
 * 首次加载项目时需要更新分表配置
 */
@Component
@Slf4j
public class DynamicShardingManager {

    @Resource
    private DataSource dataSource;

    @Resource
    private SpaceService spaceService;


    /**
     * 初始化动态分表
     */
    @PostConstruct // 当这个bean 创建成功之后,会执行这个方法
    public void initialize() {
        log.info("初始化动态分表配置");
        updateShardingTableNodes();
    }

    /**
     * 更新 ShardingSphere 的 actual-data-nodes 动态表名配置
     */
    private void updateShardingTableNodes() {
        // 现获取所有的表名
        Set<String> tableNames = fetchAllTableNames();
        // zzx_picture.picture_53613631563,zzx_picture.picture_436434847934,zzx_picture.picture_48348934873934,
        String newActualDataNodes = tableNames.stream()
                .map(tableName -> ShardingSphereConstant.MY_DATABASES_NAME + "." + tableName) // 将表名转换为逻辑表名
                .collect(Collectors.joining(","));
        log.info("动态分表 actual-data-nodes 配置为:{}", newActualDataNodes);
        ContextManager contextManager = getContextManager();
        // 获取 ShardingSphere 的规则元数据
        ShardingSphereRuleMetaData ruleMetaData = contextManager.getMetaDataContexts()
                .getMetaData()
                .getDatabases()
                .get(ShardingSphereConstant.DATABASES_NAME)
                .getRuleMetaData();
        Optional<ShardingRule> shardingRule = ruleMetaData.findSingleRule(ShardingRule.class);
        // 如果存在,则更新
        if (shardingRule.isPresent()) {
            ShardingRuleConfiguration ruleConfig = (ShardingRuleConfiguration) shardingRule.get().getConfiguration();
            List<ShardingTableRuleConfiguration> updateRules = ruleConfig.getTables()
                    .stream()
                    .map(oldTableRule -> {
                        if (ShardingSphereConstant.LOGIC_TABLE_NAME.equals(oldTableRule.getLogicTable())) {
                            // 设置新的表配置
                            ShardingTableRuleConfiguration newTableRuleConfig = new ShardingTableRuleConfiguration(ShardingSphereConstant.LOGIC_TABLE_NAME, newActualDataNodes);
                            newTableRuleConfig.setDatabaseShardingStrategy(oldTableRule.getDatabaseShardingStrategy());
                            newTableRuleConfig.setTableShardingStrategy(oldTableRule.getTableShardingStrategy());
                            newTableRuleConfig.setKeyGenerateStrategy(oldTableRule.getKeyGenerateStrategy());
                            newTableRuleConfig.setAuditStrategy(oldTableRule.getAuditStrategy());
                            return newTableRuleConfig;
                        }
                        return oldTableRule;
                    })
                    .collect(Collectors.toList());
            // 更新配置
            ruleConfig.setTables(updateRules);
            // 更新规则
            contextManager.alterRuleConfiguration(ShardingSphereConstant.DATABASES_NAME, Collections.singleton(ruleConfig));
            // 重新加载数据库
            contextManager.reloadDatabase(ShardingSphereConstant.DATABASES_NAME);
            log.info("动态分表配置更新成功");
        } else {
            log.error("未找到 ShardingSphere 的分片规则配置,动态分表更新失败");
        }
    }

    /**
     * 动态创建一张分表
     *
     * @param space 团队空间
     */
    public void createSpacePictureTable(Space space) {
        // 仅为旗舰版团队空间创建分表
        if (space.getSpaceType().equals(SpaceTypeEnum.TEAM.getValue()) && space.getSpaceLevel() == SpaceLevelEnum.FLAGSHIP.getValue()) {
            Long spaceId = space.getId();
            String tableName = ShardingSphereConstant.LOGIC_TABLE_NAME + "_" + spaceId;
            // 创建新表
            String createTableSql = "CREATE TABLE " + tableName + " LIKE " + ShardingSphereConstant.LOGIC_TABLE_NAME;
            try {
                SqlRunner.db().update(createTableSql);
                // 更新分表
                updateShardingTableNodes();
            } catch (Exception e) {
                log.error("创建分表失败,空间 id = {}", spaceId);
                e.printStackTrace();
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "创建失败,空间 id = " + spaceId);
            }
        }
    }

    /**
     * 获取 ShardingSphere 上下文管理器
     */
    private ContextManager getContextManager() {
        try (ShardingSphereConnection connection = dataSource.getConnection().unwrap(ShardingSphereConnection.class)) {
            return connection.getContextManager();
        } catch (SQLException e) {
            throw new RuntimeException("获取 ShardingSphere 上下文管理器失败", e);
        }
    }

    /**
     * 获取所有的表名,包括初始表 picture 和 分表 picture_{spaceId}
     *
     * @return 所有的表名
     */
    private Set<String> fetchAllTableNames() {
        // 查询对旗舰版的团队空间分表
        LambdaQueryWrapper<Space> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Space::getSpaceType, SpaceTypeEnum.TEAM.getValue());
        // 为了方便测试,目前只对团队空间进行动态分表,实际上线可取消注释
        queryWrapper.eq(Space::getSpaceLevel, SpaceLevelEnum.FLAGSHIP.getValue());
        Set<Long> spaceIds = spaceService.list(queryWrapper).stream()
                .map(Space::getId)
                .collect(Collectors.toSet());
        // 获取所有的表名
        Set<String> tableNames = spaceIds.stream()
                .map(spaceId -> ShardingSphereConstant.LOGIC_TABLE_NAME + "_" + spaceId)
                .collect(Collectors.toSet());
        // 添加初始表
        tableNames.add(ShardingSphereConstant.LOGIC_TABLE_NAME);
        return tableNames;
    }


}

为了我们的业务便利性,可以在这个管理类中编写创建动态分表的方法,通过拼接SQL 的方式创建出和 picture 表结构一样的分表,在相关的业务代码中执行该方法

至此我们的业务就完成了,在重新梳理一下,我们在yaml进行了相关的配置,然后自己管理创建分表的方法,然后在项目启动时或者执行相关业务通过更新配置文件,实现了动态分表的操作

相关的参考文档:

鱼皮中云图库项目

https://blog.csdn.net/weixin_60523038/article/details/145650882

https://developer.aliyun.com/article/1575676

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐