基于Mybatis-Plus 3.4.0实现多租户方案

大家好,我是一安,今天介绍Mybatis-Plus一个多租户插件,可以实现同一套程序下用户数据的隔离

前言

多租户是一种软件架构技术,在多用户的环境下,共有同一套系统,并且要注意数据之间的隔离性。

简单来说就是,在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务,且保证用户间数据隔。

实现方案大致分三类:

  • 独立数据库:即不同租户使用不同的数据库,隔离级别最高,安全性最好,但成本也相对最高
  • 共享数据库、隔离数据架构:多租户使用同一个数据库,但是每个租户对应一个Schema
  • 共享数据库、共享数据架构:用同一个数据库和Schema,但是在表中增加了租户ID的字段,隔离级别最高,隔离级别最低,成本最低基于Mybatis-Plus 3.4.0实现多租户方案

具体实现

参考官网:https://baomidou.com/pages/aef2f2/#tenantlineinnerinterceptor

说明:

1.多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!

2.启用多租户后所有执行的method的sql都会进行处理.

前两种,可通过请求头或请求参数携带租户ID实现动态切换,下面列出核心代码实现,具体实现可参考《实现多数据源切换(读写分离经典案例)

1.根据这个类的 determineCurrentLookupKey()方法获取key对应的数据源,changeDataSource()实现切换数据源

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);

    private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();

    static void changeDataSource(String dataSourceKey) {

        DATA_SOURCE_KEY.set(dataSourceKey);
    }

    static void clearDataSource() {
        DATA_SOURCE_KEY.remove();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String key = DATA_SOURCE_KEY.get();
        LOGGER.info("current data-source is {}", key);
        return key;
    }
}

2.在请求进入Service层的时候根据注解动态的切换数据库

@Component
@Aspect
public class DataSourceConfig {
    /*execution()                  表达式的主体;
    第一个”*“符号        表示返回值的类型任意;
    com.sample.service.impl       AOP所切的服务的包名,即,我们的业务部分
    包名后面的”..“         表示当前包及子包
    第二个”*“           表示类名,*即所有类。此处可以自定义,下文有举例
    .*(..)             表示任何方法名,括号表示参数,两个点表示任何参数类型*/

    //根据方法参数切换数据源
    @Pointcut("execution(public * com.capitek.service.impl.*.*(..))")
    public void pointCut() { }
    @Before("pointCut()")
    public void beforeSwitchDataSource(JoinPoint joinPoint) {
        // 参数值
        Object[] args = joinPoint.getArgs();
        Arrays.asList(args).forEach(arg -> {
            if(arg instanceof Map<?, ?>){
                Map<String, Object> map = (Map<String, Object>) arg;
                if(null!=map.get("city")){
                    if(map.get("city").equals("010")){
                        DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
                    }else if(map.get("city").equals("029")){
                        DynamicDataSource.changeDataSource(MultiDataSourceConfig.SLAVE_DATA_SOURCE);
                    }else{
                        //默认使用北京库
                        DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
                    }
                }
            }else if(arg instanceof JSONObject){
                JSONObject param = (JSONObject) arg;
                String city = param.getString("city");
                if(null!=city){
                    if(city.equals("010")){
                        DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
                    }else if(city.equals("029")){
                        DynamicDataSource.changeDataSource(MultiDataSourceConfig.SLAVE_DATA_SOURCE);
                    }else{
                        //默认使用北京库
                        DynamicDataSource.changeDataSource(MultiDataSourceConfig.MASTER_DATA_SOURCE);
                    }
                }
            }
        });
    }
    @After("pointCut()")
    public void afterSwitchDataSource() {
        DynamicDataSource.clearDataSource();
    }

下面我们来着重看一下第三种:

共享数据库,共享数据架构:因为这种方案服务器成本最低,但是提高了开发成本,好在Mybatis-plus提供了一种多租户的解决方案,实现方式是基于分页插件进行实现的,使用的Mybatis-plus版本为3.4.0

1.环境准备

CREATE TABLE user
(
 id BIGINT(20) NOT NULL COMMENT '主键ID',
 tenant_id BIGINT(20) NOT NULL COMMENT '租户ID',
 name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
 PRIMARY KEY (id)
);

DROP TABLE IF EXISTS user_addr;

CREATE TABLE USER_ADDR
(
  id BIGINT(20) NOT NULL COMMENT '主键ID',
  user_id BIGINT(20) NOT NULL COMMENT 'user.id',
  name VARCHAR(30) NULL DEFAULT NULL COMMENT '地址名称',
  PRIMARY KEY (id)
);


INSERT INTO user (id, tenant_id, name) VALUES
(1, 1, 'Jone'),(2, 1, 'Jack'),(3, 1, 'Tom'),
(4, 0, 'Sandy'),(5, 0, 'Billie');

INSERT INTO user_addr (id, USER_ID, name) VALUES
(1, 1, 'addr1'),(2,1,'addr2');

2.配置Mybatis

@Configuration
public class MybatisPlusConfig {


    /**
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,
     * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false 
     * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            // 设置当前租户ID,实际可以从缓存,过滤器,请求参数,请求头都可以
            @Override
            public Expression getTenantId() {
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                String tenant_id = request.getHeader("tenant_id");
                return new LongValue(tenant_id);
            }

            //返回false表示该表需要拼多租户条件,true表示不需要
            //实际可通过配置的方式引入
            @Override
            public boolean ignoreTable(String tableName) {
                return "user_addr".equalsIgnoreCase(tableName);
            }
        }));
         /** 如果用了分页插件注意先 add TenantLineInnerInterceptor
          * 再 add PaginationInnerInterceptor
          *  用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
          */
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }

    /**
     * 配置公共库数据源
     * @return
     * @throws Exception
     */
    @Bean(name = "mysqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //此处设置为了解决找不到mapper文件的问题
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/mysql/*.xml"));
        //引入插件
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{
                mybatisPlusInterceptor()
        });
        //map接收返回值值为null的问题,默认是当值为null,将key返回
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setCallSettersOnNulls(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }
}

3.实体类

/**
 * 用户实体对应表 user
 */
@Data
public class User {
    private Long id;
    /**
     * 租户 ID
     */
    private Long tenantId;
    private String name;

    @TableField(exist = false)
    private String addrName;

}

4.Mapper和xml

/**
 * MP 支持不需要 UserMapper.xml 
 */
public interface UserMapper extends BaseMapper<User> {

    /**
     * 自定义SQL:默认也会增加多租户条件
     * 参考打印的SQL
     * @return
     */
    Integer myCount();

    List<User> getAddrAndUser(@Param("name") String name);

    List<User> getUserAndAddr(Page<User> pageParm,@Param("username") String username);
    
    void insertUser(User user);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.test.mapper.UserMapper">

    <select id="myCount" resultType="java.lang.Integer">
        select count(1) from user
    </select>

    <select id="getUserAndAddr" resultType="com.test.entity.User">
        select u.id, u.name, a.name as addr_name
        from user u
        left join user_addr a on a.user_id=u.id
        <where>
            <if test="username!=null">
                u.name like concat(concat('%',#{username}),'%')
            </if>
        </where>
    </select>

    <select id="getAddrAndUser" resultType="com.test.entity.User">
        select a.name as addr_name, u.id, u.name
        from user_addr a
        left join user u on u.id=a.user_id
        <where>
            <if test="name!=null">
                a.name like concat(concat('%',#{name}),'%')
            </if>
        </where>
    </select>
    
    <insert id="insertUser">
        insert into user (id,name) value (#{id},#{name})
    </insert>
</mapper>

5.测试验证

    @Autowired
    private UserMapper userMapper;

    @GetMapping("/get1")
    public void dSelect() { // 查询
        List<User> userList = userMapper.selectList(null);
        userList.forEach(System.out::println);
    }

    @GetMapping("/get2")
    public void manualSqlTenantFilterTest() {
        System.out.println(userMapper.getAddrAndUser("addr1"));

        Page<User> pageParm = new Page<>(1, 10);
        System.out.println(userMapper.getUserAndAddr(pageParm,"Jone"));
        
        
        User user = new User();
        user.setId(6l);
        user.setName("一安未来");
        userMapper.insertUser(user);
    }
2022-09-01 17:23:44.691 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getAddrAndUser     : ==>  Preparing: SELECT a.name AS addr_name, u.id, u.name FROM user_addr a LEFT JOIN user u ON u.id = a.user_id AND u.tenant_id = 1 WHERE a.name LIKE concat(concat('%', ?), '%')
2022-09-01 17:23:44.692 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getAddrAndUser     : ==> Parameters: addr1(String)
2022-09-01 17:23:44.693 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getAddrAndUser     : <==      Total: 1
[User(id=1, tenantId=null, name=Jone, addrName=addr1)]

2022-09-01 17:23:44.697 DEBUG 22200 --- [nio-8080-exec-4] c.t.m.UserMapper.getUserAndAddr_mpCount  : ==>  Preparing: SELECT COUNT(1) FROM user u WHERE u.name LIKE concat(concat('%', ?), '%') AND u.tenant_id = 1
2022-09-01 17:23:44.697 DEBUG 22200 --- [nio-8080-exec-4] c.t.m.UserMapper.getUserAndAddr_mpCount  : ==> Parameters: Jone(String)
2022-09-01 17:23:44.698 DEBUG 22200 --- [nio-8080-exec-4] c.t.m.UserMapper.getUserAndAddr_mpCount  : <==      Total: 1
2022-09-01 17:23:44.699 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getUserAndAddr     : ==>  Preparing: SELECT u.id, u.name, a.name AS addr_name FROM user u LEFT JOIN user_addr a ON a.user_id = u.id WHERE u.name LIKE concat(concat('%', ?), '%') AND u.tenant_id = 1 LIMIT ?
2022-09-01 17:23:44.700 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getUserAndAddr     : ==> Parameters: Jone(String), 10(Long)
2022-09-01 17:23:44.702 DEBUG 22200 --- [nio-8080-exec-4] c.t.mapper.UserMapper.getUserAndAddr     : <==      Total: 2
[User(id=1, tenantId=null, name=Jone, addrName=addr1), User(id=1, tenantId=null, name=Jone, addrName=addr2)]

2022-09-01 17:59:37.009 DEBUG 21308 --- [nio-8080-exec-2] c.t.mapper.UserMapper.insertUser   : ==>  Preparing: INSERT INTO user (id, name, tenant_id) VALUES (?, ?, 1)
2022-09-01 17:59:37.010 DEBUG 21308 --- [nio-8080-exec-2] c.t.mapper.UserMapper.insertUser   : ==> Parameters: 6(Long), 一安未来(String)
2022-09-01 17:59:37.035 DEBUG 21308 --- [nio-8080-exec-2] c.t.mapper.UserMapper.insertUser   : <==    Updates: 1

从打印sql语句可以看出user携带了租户ID,而user_addr并没有携带

插入的时候一定要注意不要携带租户ID,Mybatis-plus也会补齐

号外!号外!

如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!

基于Mybatis-Plus 3.4.0实现多租户方案

实际项目开发中如何完善系统日志记录


SpringBoot集成Kafka – 用Multi-Consumer实现数据高吞吐


MySql批量插入数据三种方式性能对比


基于Mybatis-Plus 3.4.0实现多租户方案

原文始发于微信公众号(一安未来):基于Mybatis-Plus 3.4.0实现多租户方案

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44890.html

(1)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!