使用ShardingSphere实现若依框架用户表分库分表

若依框架中实现用户表的分库分表可以通过Apache ShardingSphere来完成。下面介绍具体实现步骤:

1. 添加依赖

首先在pom.xml中添加ShardingSphere-JDBC依赖:

<!-- ShardingSphere-JDBC -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>

2. 配置分库分表规则

application.yml中配置ShardingSphere的分库分表规则:

spring:
  shardingsphere:
    mode:
      type: Memory
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/ry_user_0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/ry_user_1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
    rules:
      sharding:
        tables:
          sys_user:
            actual-data-nodes: ds$->{0..1}.sys_user_$->{0..1}
            database-strategy:
              standard:
                sharding-column: dept_id
                sharding-algorithm-name: database-inline
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-inline
        sharding-algorithms:
          database-inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{dept_id % 2}
          table-inline:
            type: INLINE
            props:
              algorithm-expression: sys_user_$->{user_id % 2}
    props:
      sql-show: true

这个配置实现了:

  • 两个数据库:ds0ds1
  • 每个数据库中有两张用户表:sys_user_0sys_user_1
  • 分库规则:根据dept_id取模,决定数据存放在哪个数据库
  • 分表规则:根据user_id取模,决定数据存放在哪张表

3. 创建分库分表

在MySQL中创建两个数据库和对应的用户表:

-- 创建第一个数据库
CREATE DATABASE ry_user_0 DEFAULT CHARACTER SET utf8mb4;

-- 创建第二个数据库
CREATE DATABASE ry_user_1 DEFAULT CHARACTER SET utf8mb4;

-- 在每个数据库中创建用户表
CREATE TABLE `sys_user_0` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
  `user_name` varchar(30) NOT NULL COMMENT '用户账号',
  `nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
  `user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户)',
  `email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
  `phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',
  `sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
  `avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
  `password` varchar(100) DEFAULT '' COMMENT '密码',
  `status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
  `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
  `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`user_id`),
  KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

CREATE TABLE `sys_user_1` LIKE `sys_user_0`;

4. 修改实体类

修改SysUser实体类,确保字段与分表结构一致:

package com.ruoyi.system.domain;

import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class SysUser extends BaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 用户ID */
    private Long userId;
    
    /** 部门ID */
    private Long deptId;
    
    /** 用户账号 */
    private String userName;
    
    /** 用户昵称 */
    private String nickName;
    
    /** 用户类型(00系统用户) */
    private String userType;
    
    /** 用户邮箱 */
    private String email;
    
    /** 手机号码 */
    private String phonenumber;
    
    /** 用户性别(0男 1女 2未知) */
    private String sex;
    
    /** 头像地址 */
    private String avatar;
    
    /** 密码 */
    private String password;
    
    /** 帐号状态(0正常 1停用) */
    private String status;
    
    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;
    
    /** 最后登录IP */
    private String loginIp;
    
    /** 最后登录时间 */
    private Date loginDate;
    
    // 省略getter和setter方法
}

5. 修改Mapper接口

修改SysUserMapper接口,确保SQL语句正确处理分表情况:

package com.ruoyi.system.mapper;

import com.ruoyi.system.domain.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface SysUserMapper {
    /**
     * 查询用户列表
     */
    List<SysUser> selectUserList(SysUser user);
    
    /**
     * 根据用户ID查询用户
     */
    SysUser selectUserById(Long userId);
    
    /**
     * 根据用户名查询用户
     */
    SysUser selectUserByUserName(String userName);
    
    /**
     * 新增用户
     */
    int insertUser(SysUser user);
    
    /**
     * 修改用户
     */
    int updateUser(SysUser user);
    
    /**
     * 删除用户
     */
    int deleteUserById(Long userId);
    
    /**
     * 批量删除用户
     */
    int deleteUserByIds(Long[] userIds);
    
    /**
     * 重置用户密码
     */
    int resetUserPwd(@Param("userName") String userName, @Param("password") String password);
}

6. 修改Service层

确保Service层正确处理分表后的业务逻辑:

package com.ruoyi.system.service;

import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;

import java.util.List;

public interface ISysUserService {
    /**
     * 查询用户列表
     */
    TableDataInfo selectUserList(SysUser user);
    
    /**
     * 根据条件分页查询已分配用户角色列表
     */
    TableDataInfo selectAllocatedList(SysUser user);
    
    /**
     * 根据条件分页查询未分配用户角色列表
     */
    TableDataInfo selectUnallocatedList(SysUser user);
    
    /**
     * 通过用户名查询用户
     */
    SysUser selectUserByUserName(String userName);
    
    /**
     * 通过用户ID查询用户
     */
    SysUser selectUserById(Long userId);
    
    /**
     * 查询用户所属角色组
     */
    String selectUserRoleGroup(String userName);
    
    /**
     * 查询用户所属岗位组
     */
    String selectUserPostGroup(String userName);
    
    /**
     * 校验用户名称是否唯一
     */
    String checkUserNameUnique(String userName);
    
    /**
     * 校验手机号码是否唯一
     */
    String checkPhoneUnique(SysUser user);
    
    /**
     * 校验email是否唯一
     */
    String checkEmailUnique(SysUser user);
    
    /**
     * 校验用户是否允许操作
     */
    void checkUserAllowed(SysUser user);
    
    /**
     * 新增保存用户信息
     */
    int insertUser(SysUser user);
    
    /**
     * 修改保存用户信息
     */
    int updateUser(SysUser user);
    
    /**
     * 修改用户状态
     */
    int updateUserStatus(SysUser user);
    
    /**
     * 修改用户基本信息
     */
    int updateUserProfile(SysUser user);
    
    /**
     * 修改用户头像
     */
    boolean updateUserAvatar(String userName, String avatar);
    
    /**
     * 重置用户密码
     */
    int resetPwd(SysUser user);
    
    /**
     * 重置用户密码
     */
    int resetUserPwd(String userName, String password);
    
    /**
     * 通过用户ID删除用户
     */
    int deleteUserById(Long userId);
    
    /**
     * 批量删除用户信息
     */
    int deleteUserByIds(Long[] userIds);
    
    /**
     * 导入用户数据
     */
    String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName);
    
    /**
     * 获取用户信息
     */
    LoginUser getLoginUser(String userName);
}

7. 处理分布式ID

为确保分表后ID的唯一性,建议使用雪花算法等分布式ID生成器:

package com.ruoyi.common.core.utils.idgen;

import org.apache.shardingsphere.infra.util.exception.ShardingSpherePreconditions;
import org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm;

import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;

public final class SnowflakeKeyGenerateAlgorithm implements KeyGenerateAlgorithm {
    
    public static final String TYPE = "SNOWFLAKE";
    
    private static final long EPOCH = 1577836800000L;
    
    private static final long SEQUENCE_BITS = 12L;
    
    private static final long WORKER_ID_BITS = 10L;
    
    private static final long SEQUENCE_MASK = (1L << SEQUENCE_BITS) - 1;
    
    private static final long WORKER_ID_LEFT_SHIFT = SEQUENCE_BITS;
    
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    
    private Properties props = new Properties();
    
    private AtomicLong sequence = new AtomicLong(0L);
    
    private long workerId;
    
    private long lastTimestamp = -1L;
    
    @Override
    public void init() {
        workerId = Long.parseLong(props.getProperty("worker-id", "1"));
        ShardingSpherePreconditions.checkArgument(workerId >= 0L && workerId < (1L << WORKER_ID_BITS), 
                "Worker ID must be between 0 and %s", (1L << WORKER_ID_BITS) - 1);
    }
    
    @Override
    public synchronized Comparable<?> generateKey() {
        long currentTimestamp = timeGen();
        ShardingSpherePreconditions.checkState(lastTimestamp <= currentTimestamp, 
                "Clock moved backwards, last time is %d milliseconds, current time is %d milliseconds", lastTimestamp, currentTimestamp);
        if (lastTimestamp == currentTimestamp) {
            if (0L == (sequence.incrementAndGet() & SEQUENCE_MASK)) {
                currentTimestamp = waitUntilNextTime(currentTimestamp);
            }
        } else {
            sequence.set(0L);
        }
        lastTimestamp = currentTimestamp;
        return ((currentTimestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT) | (workerId << WORKER_ID_LEFT_SHIFT) | sequence.get();
    }
    
    private long waitUntilNextTime(final long lastTimestamp) {
        long time = timeGen();
        while (time <= lastTimestamp) {
            time = timeGen();
        }
        return time;
    }
    
    private long timeGen() {
        return System.currentTimeMillis();
    }
    
    @Override
    public String getType() {
        return TYPE;
    }
    
    @Override
    public Properties getProps() {
        return props;
    }
    
    @Override
    public void setProps(final Properties props) {
        this.props = props;
    }
}

8. 处理关联查询

对于涉及用户表的关联查询,需要修改相关SQL和业务逻辑:

/**
 * 查询用户及其角色信息
 */
public List<SysUser> selectUserAndRoleList(SysUser user) {
    // 由于分表,这里需要特殊处理关联查询
    // 可以通过先查询用户,再查询角色的方式实现
    List<SysUser> userList = userMapper.selectUserList(user);
    for (SysUser sysUser : userList) {
        List<SysRole> roles = roleMapper.selectRolesByUserId(sysUser.getUserId());
        sysUser.setRoles(roles);
    }
    return userList;
}

9. 注意事项

  1. 跨库事务:分库后事务会变成分布式事务,需要使用Seata等分布式事务框架

  2. 全局查询:某些查询可能需要跨库跨表,性能会受影响

  3. 数据迁移:分表前需要考虑历史数据的迁移问题

  4. 分片键选择:合理选择分片键非常重要,本例中选择了dept_iduser_id

  5. 分页查询:分页查询需要特殊处理,ShardingSphere提供了相应的支持

通过以上步骤,你可以在若依框架中成功实现用户表的分库分表,提高系统的性能和扩展性。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐