【团队空间|第十一天】基础功能实现,RBAC权限控制,ShardingSphere详解

目录

团队共享空间

方案设计

空间成员权限控制

[RBAC 权限控制](#RBAC 权限控制)

方案设计

后端实现

空间数据管理

方案设计

静态分表

动态分表

后端实现


本部分内容主要来源于鱼皮智能协图云图库部分,并在笔者个人项目学习的基础上进行扩展衍生。由于项目开发文档已经足够详细,因此这里只记录要点。

团队共享空间

该部分内容与创建私人空间类似,基础的增删改查操作较多,复用代码较多。

方案设计

目的:开发新模块------团队共享空间,实现基本增删改查等操作。

创建过程:

1.库表设计------用空间表的spaceType字段区分出团队空间。为空间成员单独创建表。

2.数据模型------dto与entity补充spaceType字段,定义空间枚举类(用于空间类别判断)

3.空间业务开发------对原有空间创建方法复用,增添校验

4.空间成员业务开发------增删改查业务开发

这里难点不多,主要掌握业务流程与校验的细节。

空间成员权限控制

RBAC 权限控制

RBAC 权限控制模型(基于角色的访问控制,Role-Based Access Control),核心概念包括 用户、角色、权限。某接口需要A权限,因此先根据用户判断角色,才从角色中抽取拥有的权限进行判断。

方案设计

其实我觉得在前面普通用户---管理员的实现已经包含了RBAC权限验证,只是具体权限没有单独写出来(读权限,改权限等等),而是以某接口管理员可访问的方式体现出来的。

在这里可以有多种设计思路:

1.注解+aop切面鉴权,以权限为最细粒度进行判读(前面的实现是以角色为粒度)

2.拦截器/过滤器实现,在拦截器/过滤器中写具体业务判断逻辑

3.第三方校验框架(包装了查询当前角色权限再进行权限比较)

标准的 RBAC 实现需要 5 张表:用户表、角色表、权限表、用户角色关联表、角色权限关联表。这里为了简化,直接将具体的权限,角色类别定义在resource目录下的.json文件中。

具体实现流程:

1.引入依赖

2.写角色权限的.json文件

3.新建数据模型,接收配置文件

4.加载配置文件到对象

5.kit模式实现多账号体系认证

6.权限校验逻辑(获取当前用户的权限列表/角色列表)

7.注释/编程式方式使用sa-token

sa-token的本质还是包装,权限验证是包装获取权限列表和权限列表与注解权限(注解标记需要哪个权限)比较的过程。角色登录是包装生成token,封装用户信息到session的过程。这里获取权限列表是核心代码,怎么获取是主要思考点。

后端实现

1.JSON 配置文件来定义角色、权限、角色和权限之间的关系

bash 复制代码
json代码{
  "permissions": [
    {
      "key": "spaceUser:manage",
      "name": "成员管理",
      "description": "管理空间成员,添加或移除成员"
    },
    {
      "key": "picture:view",
      "name": "查看图片",
      "description": "查看空间中的图片内容"
    },
    {
      "key": "picture:upload",
      "name": "上传图片",
      "description": "上传图片到空间中"
    },
    {
      "key": "picture:edit",
      "name": "修改图片",
      "description": "编辑已上传的图片信息"
    },
    {
      "key": "picture:delete",
      "name": "删除图片",
      "description": "删除空间中的图片"
    }
  ],
  "roles": [
    {
      "key": "viewer",
      "name": "浏览者",
      "permissions": [
        "picture:view"
      ],
      "description": "查看图片"
    },
    {
      "key": "editor",
      "name": "编辑者",
      "permissions": [
        "picture:view",
        "picture:upload",
        "picture:edit",
        "picture:delete"
      ],
      "description": "查看图片、上传图片、修改图片、删除图片"
    },
    {
      "key": "admin",
      "name": "管理员",
      "permissions": [
        "spaceUser:manage",
        "picture:view",
        "picture:upload",
        "picture:edit",
        "picture:delete"
      ],
      "description": "成员管理、查看图片、上传图片、修改图片、删除图片"
    }
  ]
}

存放在 resources/biz 目录下,biz为business缩写,业务,商务。

在resources目录下在maven构建项目时会被自动打包,运行时可通过classpath便捷读取,不需要读取绝对路径。

2.SpaceUserAuthManager可加载配置文件到对象

java 复制代码
@Component
public class SpaceUserAuthManager {

    @Resource
    private SpaceUserService spaceUserService;

    @Resource
    private UserService userService;

    public static final SpaceUserAuthConfig SPACE_USER_AUTH_CONFIG;

    static {
        String json = ResourceUtil.readUtf8Str("biz/spaceUserAuthConfig.json");
        SPACE_USER_AUTH_CONFIG = JSONUtil.toBean(json, SpaceUserAuthConfig.class);
    }

    /**
     * 根据角色获取权限列表
     */
    public List<String> getPermissionsByRole(String spaceUserRole) {
        if (StrUtil.isBlank(spaceUserRole)) {
            return new ArrayList<>();
        }
        // 找到匹配的角色
        SpaceUserRole role = SPACE_USER_AUTH_CONFIG.getRoles().stream()
                .filter(r -> spaceUserRole.equals(r.getKey()))
                .findFirst()
                .orElse(null);
        if (role == null) {
            return new ArrayList<>();
        }
        return role.getPermissions();
    }
}

要点:

1.静态代码实现只执行一次

2.Hutool工具包读取资源文件ResourceUtil.readUtf8Str()

3.Hutool工具包实现类转换JsonUtil.toBean()

3.定义空间账号体系-实现登录逻辑

java 复制代码
/**
 * StpLogic 门面类,管理项目中所有的 StpLogic 账号体系
 * 添加 @Component 注解的目的是确保静态属性 DEFAULT 和 SPACE 被初始化
 */
@Component
public class StpKit {

    public static final String SPACE_TYPE = "space";

    /**
     * 默认原生会话对象,项目中目前没使用到
     */
    public static final StpLogic DEFAULT = StpUtil.stpLogic;

    /**
     * Space 会话对象,管理 Space 表所有账号的登录、权限认证
     */
    public static final StpLogic SPACE = new StpLogic(SPACE_TYPE);
}

使用:

java 复制代码
// 在当前会话进行 Space 账号登录
StpKit.SPACE.login(10001);

// 检测当前会话是否以 Space 账号登录,并具有 picture:edit 权限
StpKit.SPACE.checkPermission("picture:edit");

// 获取当前 Space 会话的 Session 对象,并进行写值操作 
StpKit.SPACE.getSession().set("user", "程序员鱼皮");

要点:

1.@Component实现静态属性初始化,Spring 启动时会主动扫描并加载该类则静态属性初始化

2.new StpLogic()创建实例时,sa-token底层会自动完成体系注册

3.StpKit实现了门面模式,统一入口

4.获取上下文对象

java 复制代码
@Value("${server.servlet.context-path}")
private String contextPath;

/**
 * 从请求中获取上下文对象
 */
private SpaceUserAuthContext getAuthContextByRequest() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    String contentType = request.getHeader(Header.CONTENT_TYPE.getValue());
    SpaceUserAuthContext authRequest;
    // 兼容 get 和 post 操作
    if (ContentType.JSON.getValue().equals(contentType)) {
        String body = ServletUtil.getBody(request);
        authRequest = JSONUtil.toBean(body, SpaceUserAuthContext.class);
    } else {
        Map<String, String> paramMap = ServletUtil.getParamMap(request);
        authRequest = BeanUtil.toBean(paramMap, SpaceUserAuthContext.class);
    }
    // 根据请求路径区分 id 字段的含义
    Long id = authRequest.getId();
    if (ObjUtil.isNotNull(id)) {
        String requestUri = request.getRequestURI();
        String partUri = requestUri.replace(contextPath + "/", "");
        String moduleName = StrUtil.subBefore(partUri, "/", false);
        switch (moduleName) {
            case "picture":
                authRequest.setPictureId(id);
                break;
            case "spaceUser":
                authRequest.setSpaceUserId(id);
                break;
            case "space":
                authRequest.setSpaceId(id);
                break;
            default:
        }
    }
    return authRequest;
}

要点:

1.编写逻辑:这里涉及注解式与编程式的底层逻辑。我们最终都会通过getPermissionList方法获取当前用户的权限列表,sa-token再进行比较完成鉴权。但由于注解式触发时机是在接口方法之前,因此需要我们手动完成request的解析,封装为java对象。编程式是在业务内灵活调用,此时接口的入参已经被spring解析封装为Java对象了。

注解式:前端发送请求Sa-Token 拦截器拦截请求执行注解对应的权限校验校验通过 → 进入接口方法体执行业务逻辑校验失败 → 直接返回无权限响应

编程式:前端发送请求进入接口方法体执行业务逻辑→ 可将用户信息存入ThreadlocalSa-Token 拦截器拦截请求执行注解对应的权限校验校验通过 → 继续执行校验失败 → 直接返回无权限响应

java 复制代码
public List<String> getPermissionList(Object loginId, String loginType) {
    // 只能自己解析request获取更多信息
    
      SpaceUserAuthContext authContext = getAuthContextByRequest();
    // 后续权限校验逻辑...
}
java 复制代码
public List<String> getPermissionList(Object loginId, String loginType) {
    // 直接从 ThreadLocal 中获取 spaceId,无需依赖 Servlet 请求对象
    Long spaceId = RequestContextHolder.getSpaceId();
    
    // 后续权限校验逻辑...
}

2.非Controller层解析HttpServletRequest,涉及许多servlet的知识,改日再专门写篇文章攻克一下。

3.兼容 get 和 post 操作-- application/json格式与application/x-www-form-urlencoded。ServletUtil.getBody(),ServletUtil.getParamMap()。

4.HttpServletRequest 的 body 值是流,只支持读取一次,因此还需要额外的处理。

5.编写返回权限列表的逻辑

java 复制代码
public List<String> getPermissionList(Object loginId, String loginType) {
    // 判断 loginType,仅对类型为 "space" 进行权限校验
    if (!StpKit.SPACE_TYPE.equals(loginType)) {
        return new ArrayList<>();
    }
    // 管理员权限,表示权限校验通过
    List<String> ADMIN_PERMISSIONS = spaceUserAuthManager.getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue());
    // 获取上下文对象
    SpaceUserAuthContext authContext = getAuthContextByRequest();
    // 如果所有字段都为空,表示查询公共图库,可以通过
    if (isAllFieldsNull(authContext)) {
        return ADMIN_PERMISSIONS;
    }
    // 获取 userId
    User loginUser = (User) StpKit.SPACE.getSessionByLoginId(loginId).get(USER_LOGIN_STATE);
    if (loginUser == null) {
        throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "用户未登录");
    }
    Long userId = loginUser.getId();
    // 优先从上下文中获取 SpaceUser 对象
    SpaceUser spaceUser = authContext.getSpaceUser();
    if (spaceUser != null) {
        return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
    }
    // 如果有 spaceUserId,必然是团队空间,通过数据库查询 SpaceUser 对象
    Long spaceUserId = authContext.getSpaceUserId();
    if (spaceUserId != null) {
        spaceUser = spaceUserService.getById(spaceUserId);
        if (spaceUser == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间用户信息");
        }
        // 取出当前登录用户对应的 spaceUser
        SpaceUser loginSpaceUser = spaceUserService.lambdaQuery()
                .eq(SpaceUser::getSpaceId, spaceUser.getSpaceId())
                .eq(SpaceUser::getUserId, userId)
                .one();
        if (loginSpaceUser == null) {
            return new ArrayList<>();
        }
        // 这里会导致管理员在私有空间没有权限,可以再查一次库处理
        return spaceUserAuthManager.getPermissionsByRole(loginSpaceUser.getSpaceRole());
    }
    // 如果没有 spaceUserId,尝试通过 spaceId 或 pictureId 获取 Space 对象并处理
    Long spaceId = authContext.getSpaceId();
    if (spaceId == null) {
        // 如果没有 spaceId,通过 pictureId 获取 Picture 对象和 Space 对象
        Long pictureId = authContext.getPictureId();
        // 图片 id 也没有,则默认通过权限校验
        if (pictureId == null) {
            return ADMIN_PERMISSIONS;
        }
        Picture picture = pictureService.lambdaQuery()
                .eq(Picture::getId, pictureId)
                .select(Picture::getId, Picture::getSpaceId, Picture::getUserId)
                .one();
        if (picture == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到图片信息");
        }
        spaceId = picture.getSpaceId();
        // 公共图库,仅本人或管理员可操作
        if (spaceId == null) {
            if (picture.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
                return ADMIN_PERMISSIONS;
            } else {
                // 不是自己的图片,仅可查看
                return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW);
            }
        }
    }
    // 获取 Space 对象
    Space space = spaceService.getById(spaceId);
    if (space == null) {
        throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间信息");
    }
    // 根据 Space 类型判断权限
    if (space.getSpaceType() == SpaceTypeEnum.PRIVATE.getValue()) {
        // 私有空间,仅本人或管理员有权限
        if (space.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
            return ADMIN_PERMISSIONS;
        } else {
            return new ArrayList<>();
        }
    } else {
        // 团队空间,查询 SpaceUser 并获取角色和权限
        spaceUser = spaceUserService.lambdaQuery()
                .eq(SpaceUser::getSpaceId, spaceId)
                .eq(SpaceUser::getUserId, userId)
                .one();
        if (spaceUser == null) {
            return new ArrayList<>();
        }
        return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
    }
}

这里比较复杂,但主要是为了兼容公共图库、私有空间和团队空间共同校验。

6.权限校验注解简化

java 复制代码
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

    // 注册 Sa-Token 拦截器,打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }

    @PostConstruct
    public void rewriteSaStrategy() {
        // 重写Sa-Token的注解处理器,增加注解合并功能 
        SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
            return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
        };
    }
}
java 复制代码
/**
 * 空间权限认证:必须具有指定权限才能进入该方法
 * <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上)
 */
@SaCheckPermission(type = StpKit.SPACE_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SaSpaceCheckPermission {

    /**
     * 需要校验的权限码
     *
     * @return 需要校验的权限码
     */
    @AliasFor(annotation = SaCheckPermission.class)
    String[] value() default {};

    /**
     * 验证模式:AND | OR,默认AND
     *
     * @return 验证模式
     */
    @AliasFor(annotation = SaCheckPermission.class)
    SaMode mode() default SaMode.AND;

    /**
     * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验
     *
     * <p>
     * 例1:@SaCheckPermission(value="user-add", orRole="admin"),
     * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。
     * </p>
     *
     * <p>
     * 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。 <br>
     * 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。
     * </p>
     *
     * @return /
     */
    @AliasFor(annotation = SaCheckPermission.class)
    String[] orRole() default {};

}

要点:

1.注册 Sa-Token 拦截器,开启注解式鉴权。

2.@PostConstruct 标记该方法在当前bean初始化后立刻执行且只执行一次

3.重写注解策略,开启「注解合并」功能。即可继承Sa-Token原生注解@SaCheckPermission所以鉴权功能

4.自定义注解,提前指定type类型,简化注解使用

5.元注解@Retention表明注解生命周期,@Target表明注解范围,@AliasFor表明属性映射,映射到@SaCheckPermission

6.implements WebMvcConfigurer:实现 Spring MVC 的配置扩展接口,用于自定义 Spring MVC 的功能(这里主要是注册拦截器),这是 Spring MVC 扩展的标准方式。配置跨域请求,消息转换器等都需要实现这个接口。

接下来在接口上加上注解即可。

空间数据管理

由于数据庞大以及复杂性上升,需要进行分库分表。

方案设计

使用ShardingSphere 分库分表实现。开发者操作逻辑表,ShardingSphere自动化分表路由物理表。

2 大核心模块 ShardingSphere-JDBC 和 ShardingSphere-Proxy

维度 ShardingSphere JDBC ShardingSphere Proxy
运行方式 嵌入式运行在应用内部 独立代理,运行在应用与数据库之间
性能 低网络开销,性能较高 引入网络开销,性能略低
支持语言 仅支持 Java 支持多语言(Java、Python、Go 等)
配置管理 分布式配置较复杂 支持集中配置和动态管理
扩展性 随着应用扩展,需单独调整配置 代理服务集中化管理,扩展性强
适用场景 单体或小型系统,对性能要求高的场景 多语言、大型分布式系统或需要统一管理的场景
静态分表

在设计阶段,分表的数量和规则固定,不会根据业务增长动态调整。分片规则简单(取模,哈希等)。

基本格式

XML 复制代码
rules:
  sharding:
    tables:
      picture:
        actualDataNodes: ds0.picture_${0..2} # 3张分表:picture_0, picture_1, picture_2
        tableStrategy:
          standard:
            shardingColumn: pictureId       # 按 pictureId 分片
            shardingAlgorithmName: pictureIdMod
    shardingAlgorithms:
      pictureIdMod:
        type: INLINE
        props:
          algorithm-expression: picture_${pictureId % 3} # 分片表达式
动态分表

需要自定义分表算法,编写创建表的逻辑。

后端实现

1.动态分表配置

XML 复制代码
spring:
  # 空间图片分表
  shardingsphere:
    datasource:
      names: yu_picture
      yu_picture:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/yu_picture
        username: root
        password: 123456
    rules:
      sharding:
        tables:
          picture:
            actual-data-nodes: yu_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.yupi.yupicturebackend.manager.sharding.PictureShardingAlgorithm
    props:
      sql-show: true

要点:

1.actual-data-nodes一般指定逻辑表名与分表范围,查询时会在这个范围内查询验证合法性。然而由于通过spaceid分表,且spaceid为长整型,范围太大。因此这里直接设置为逻辑表,放弃物理表预校验。

2.sharding-column指明分表字段

3.sharding-algorithm-name表明分表算法

4.配置分表算法,指明类路径等

2.新建分表算法类

java 复制代码
public class PictureShardingAlgorithm implements StandardShardingAlgorithm<Long> {

    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> preciseShardingValue) {
        Long spaceId = preciseShardingValue.getValue();
        String logicTableName = preciseShardingValue.getLogicTableName();
        // spaceId 为 null 表示查询所有图片
        if (spaceId == null) {
            return logicTableName;
        }
        // 根据 spaceId 动态生成分表名
        String realTableName = "picture_" + spaceId;
        if (availableTargetNames.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) {

    }
}

要点:

1.实现路由操作

2.配置采用逻辑分表的表明,这里的判断可用分表无法实现。因此手写分表管理器。

java 复制代码
@Component
@Slf4j
public class DynamicShardingManager {

    @Resource
    private DataSource dataSource;

    @Resource
    private SpaceService spaceService;

    private static final String LOGIC_TABLE_NAME = "picture";

    private static final String DATABASE_NAME = "logic_db"; // 配置文件中的数据库名称

    @PostConstruct
    public void initialize() {
        log.info("初始化动态分表配置...");
        updateShardingTableNodes();
    }

    /**
     * 获取所有动态表名,包括初始表 picture 和分表 picture_{spaceId}
     */
    private Set<String> fetchAllPictureTableNames() {
        // 为了测试方便,直接对所有团队空间分表(实际上线改为仅对旗舰版生效)
        Set<Long> spaceIds = spaceService.lambdaQuery()
                .eq(Space::getSpaceType, SpaceTypeEnum.TEAM.getValue())
                .list()
                .stream()
                .map(Space::getId)
                .collect(Collectors.toSet());
        Set<String> tableNames = spaceIds.stream()
                .map(spaceId -> LOGIC_TABLE_NAME + "_" + spaceId)
                .collect(Collectors.toSet());
        tableNames.add(LOGIC_TABLE_NAME); // 添加初始逻辑表
        return tableNames;
    }

    /**
     * 更新 ShardingSphere 的 actual-data-nodes 动态表名配置
     */
    private void updateShardingTableNodes() {
        Set<String> tableNames = fetchAllPictureTableNames();
        String newActualDataNodes = tableNames.stream()
                .map(tableName -> "yu_picture." + tableName) // 确保前缀合法
                .collect(Collectors.joining(","));
        log.info("动态分表 actual-data-nodes 配置: {}", newActualDataNodes);

        ContextManager contextManager = getContextManager();
        ShardingSphereRuleMetaData ruleMetaData = contextManager.getMetaDataContexts()
                .getMetaData()
                .getDatabases()
                .get(DATABASE_NAME)
                .getRuleMetaData();

        Optional<ShardingRule> shardingRule = ruleMetaData.findSingleRule(ShardingRule.class);
        if (shardingRule.isPresent()) {
            ShardingRuleConfiguration ruleConfig = (ShardingRuleConfiguration) shardingRule.get().getConfiguration();
            List<ShardingTableRuleConfiguration> updatedRules = ruleConfig.getTables()
                    .stream()
                    .map(oldTableRule -> {
                        if (LOGIC_TABLE_NAME.equals(oldTableRule.getLogicTable())) {
                            ShardingTableRuleConfiguration newTableRuleConfig = new ShardingTableRuleConfiguration(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(updatedRules);
            contextManager.alterRuleConfiguration(DATABASE_NAME, Collections.singleton(ruleConfig));
            contextManager.reloadDatabase(DATABASE_NAME);
            log.info("动态分表规则更新成功!");
        } else {
            log.error("未找到 ShardingSphere 的分片规则配置,动态分表更新失败。");
        }
    }

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

生成完整的物理表列表并更新到配置actual-data-nodes中

3.动态创建分表

分表管理器中新增方法

java 复制代码
public void createSpacePictureTable(Space space) {
    // 动态创建分表
    // 仅为旗舰版团队空间创建分表
    if (space.getSpaceType() == SpaceTypeEnum.TEAM.getValue() && space.getSpaceLevel() == SpaceLevelEnum.FLAGSHIP.getValue()) {
        Long spaceId = space.getId();
        String tableName = "picture_" + spaceId;
        // 创建新表
        String createTableSql = "CREATE TABLE " + tableName + " LIKE picture";
        try {
            SqlRunner.db().update(createTableSql);
            // 更新分表
            updateShardingTableNodes();
        } catch (Exception e) {
            log.error("创建图片空间分表失败,空间 id = {}", space.getId());
        }
    }
}

要点:

1.开启MyBatis Plus 的 SqlRunner配置

2.使用SqlRunner.db().update(createTableSql);动态创建表

ShardingSphere 还提供了 hint 强制分表路由机制 来实现动态分表,允许在代码中强制指定具体的物理表,从而解决动态分表问题。但缺点是需要在每次查询或者操作数据时都显式设置表名,会给代码增加很多额外逻辑,不够优雅。所以不采用。

整个流程:静态路由配置(yml + 分片算法)→ 项目启动初始化(分表管理器更新actual-data-nodes)→ 运行时动态扩展(动态建表 + 更新配置),三者闭环,实现完整的动态分表。

相关推荐
fengxin_rou2 小时前
从 String 到 Zset:Redis 核心数据结构全解析及排行榜应用
java·开发语言·redis·多线程
世界尽头与你2 小时前
CVE-2025-55752_ Apache Tomcat 安全漏洞
java·安全·网络安全·渗透测试·tomcat·apache
Re.不晚2 小时前
Java进阶之路--线程最最详细讲解
java·开发语言
梵刹古音2 小时前
【C语言】 数组基础与地址运算
c语言·开发语言·算法
long3162 小时前
KMP模式搜索算法
数据库·算法
wuguan_2 小时前
C#/VP联合编程之绘制图像与保存
开发语言·c#
Howrun7772 小时前
C++_错误处理
开发语言·c++
有味道的男人2 小时前
接入MIC(中国制造)接口的帮助
网络·数据库·制造
Jacob程序员2 小时前
达梦数据库私有服务配置指南
linux·服务器·数据库