当前位置: 首页 > article >正文

多租户数据源隔离

多租户数据源隔离

1.前言

多租户数据隔离的实现主要有两种方式,一是在表中添加租户字段,根据租户字段实现数据的隔离;二是每个租户创建一个数据源,实现数据的隔离。本文主要介绍第二种方式。

2.快速入门

2.1准备工作

根据sql脚本创建表

-- 用于管理租户数据源
CREATE TABLE `datasource` (
  `ds_id` bigint(20) NOT NULL COMMENT '主键id',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户id',
  `ds_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据源标识',
  `jdbc_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库url',
  `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库名称',
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库密码',
  PRIMARY KEY (`ds_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 测试动态切换数据源使用
CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `name` varchar(16) DEFAULT '' COMMENT '名字',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='t_user';

2.2代码实现

  • 引入依赖
<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>2.7.18</version>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.5.10.1</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.33</version>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
			<version>4.3.1</version>
		</dependency>
		<dependency>
			<groupId>javax.validation</groupId>
			<artifactId>validation-api</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.34</version>
		</dependency>
	</dependencies>
  • yaml配置

这里配置了用于管理多数据源的数据库连接信息

spring:
  datasource:
    dynamic:
      #设置默认的数据源或者数据源组,默认值即为master
      primary: master
      #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      strict: false
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/tenant_datasource?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
          username: root
          password: root
  • 创建实体类
@Data
@Accessors(chain = true)
@TableName("datasource")
public class DataSourceEntity{

    @TableId(type = IdType.ASSIGN_ID)
    private Long dsId;

    private Long tenantId;

    private String dsCode;

    private String jdbcUrl;

    private String username;

    private String password;

}
@Data
@Accessors(chain = true)
@TableName("t_user")
public class TUser {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String name;
}
  • mapper层
@Mapper
public interface TenantMapper extends BaseMapper<DataSourceEntity> {

    List<DataSourceEntity> findAll();
}

@Mapper
public interface UserMapper extends BaseMapper<TUser> {

}
  • controller层
@Slf4j
@RestController
public class TenantController {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private DefaultDataSourceCreator dataSourceCreator;
    @Autowired
    private TenantService tenantService;
    @Autowired
    private UserService userService;

    //通用数据源会根据maven中配置的连接池根据顺序依次选择。
    //默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
    //通用添加数据源(推荐)
    @PostMapping("/add")
    public Set<String> add(@Valid @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        //添加数据源到数据库中
        tenantService.add(dto);
        return ds.getDataSources().keySet();
    }

    //删除数据源,name:数据源标识
    @DeleteMapping
    public String remove(String poolName) {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(poolName);
        //租户表中移除
        tenantService.remove(poolName);

        return "删除成功";
    }

    @GetMapping("/{ds_code}/user")
    // Get http://localhost:8080/llp/user
    // Get http://localhost:8080/master/user
    public List<TUser> getUsers(@PathVariable("ds_code") String dsCode) {
        try {
            DynamicDataSourceContextHolder.push(dsCode);
            return userService.findAll();
        } finally {
            DynamicDataSourceContextHolder.clear();
        }
    }

}
  • service
@Service
public class TenantServiceImpl extends ServiceImpl<TenantMapper, DataSourceEntity> implements TenantService {
	
    //添加数据源到表中
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void add(DataSourceDTO dto) {
        DataSourceEntity dataSource = new DataSourceEntity();
        dataSource.setTenantId(dto.getTenantId());
        dataSource.setDsCode(dto.getPoolName());
        dataSource.setJdbcUrl(dto.getUrl());
        dataSource.setUsername(dto.getUsername());
        dataSource.setPassword(dto.getPassword());
        baseMapper.insert(dataSource);
    }
	
    //删除数据源,同步删除租户数据源配置
    @Override
    public void remove(String poolName) {
        DataSourceEntity dataSource = baseMapper.selectOne(new LambdaQueryWrapper<DataSourceEntity>().eq(DataSourceEntity::getDsCode, poolName));
        baseMapper.deleteById(dataSource);
    }
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, TUser> implements UserService {
    @Override
    public List<TUser> findAll() {
        return baseMapper.selectList(null);
    }

    /**
     * 扩展: 从header头中提取数据源标识,动态切换数据源的方式
     * 默认有三个职责链来处理动态参数解析器 header->session->spel
     * @DS("#session.tenantName")//从session获取
     * @DS("#header.tenantName")//从header获取
     * @DS("#tenantName")//使用spel从参数获取
     * @DS("#user.tenantName")//使用spel从复杂参数获取
     * @return
     */
    @DS("#header.tenantName")//从header获取
    public List findAllByHeader() {
        return baseMapper.selectList(null);
    }

}
  • 数据源初始化配置

在项目启动时将数据库中配置的数据源加载到DynamicRoutingDataSource中便于后续进行数据源的操作:切换、删除等

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.llp.dynamicDataSource.entity.DataSourceEntity;
import com.llp.dynamicDataSource.mapper.TenantMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.List;

@Slf4j
@Configuration
public class DynamicDataSourceInit {

    @Autowired
    private DefaultDataSourceCreator dataSourceCreator;

    @Autowired
    private TenantMapper tenantMapper;

    @Autowired
    private DataSource dataSource;

    @Bean
    public void initDataSource() {
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        List<DataSourceEntity> datasources = tenantMapper.selectList(null);
        for (DataSourceEntity entity : datasources) {
            DataSourceProperty dsProperties = new DataSourceProperty();
            dsProperties.setPoolName(entity.getDsCode());
            dsProperties.setUrl(entity.getJdbcUrl());
            dsProperties.setUsername(entity.getUsername());
            dsProperties.setPassword(entity.getPassword());
            DataSource dataSource = dataSourceCreator.createDataSource(dsProperties);
            ds.addDataSource(dsProperties.getPoolName(), dataSource);
        }
        log.info("{}", ds.getDataSources().keySet());
    }
}
  • 启动类
@SpringBootApplication
//扫描mapper接口所在的包,用于创建实现类并注册到spring容器中
@MapperScan("com.llp.dynamicDataSource.mapper")
public class DynamicDataSourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DynamicDataSourceApplication.class);
    }
}

3.扩展

前面只是做了简单的时间,我们可以扩展datasource表,比如添加租户有效期,用于控制租户的使用时限,或者和一些资费进行绑定

CREATE TABLE `datasource` (
  `ds_id` bigint(20) NOT NULL COMMENT '主键id',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户id',
  `ds_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据源标识',
  `jdbc_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库url',
  `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库名称',
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库密码',
  PRIMARY KEY (`ds_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

另外创建租户数据源之后,可能还会超级管理员、租户管理员以及一些菜单权限的问题(可以在创建数据库时就准备好一个包含了这些角色菜单的数据库初始脚本) 或者做定制化开发

4.测试验证

  • 添加租户数据源

请求

POST

http://127.0.0.1:8080/add

{
  "poolName": "llp",
  "tenantId": 3,
  "driverClassName": "com.mysql.cj.jdbc.Driver",
  "url": "jdbc:mysql://127.0.0.1:3306/llp?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false",
  "username": "root",
  "password": "root"
}

响应

[
  "llp",
  "master"
]

image-20250212154835457

image-20250212155010981

  • 查询测试

image-20250212155154177

请求

GET

http://127.0.0.1:8080/llp/user

image-20250212155138254

响应

[
  {
    "id": 1,
    "name": "孙悟空"
  }
]

image-20250212155318860

image-20250212155247867


http://www.kler.cn/a/543395.html

相关文章:

  • 在Mac arm架构终端中运行 corepack enable yarn 命令,安装yarn
  • 每日一题——括号生成
  • C# ASP.NET的发展历程
  • http 与 https 的区别?
  • leetcode day17 二分查找 34+367 移除元素27
  • 用Python编写经典《贪吃蛇》小游戏
  • kindle.cn 无法接收邮件
  • pnpm的使用
  • 【工业安全】-CVE-2022-35555- Tenda W6路由器 命令注入漏洞
  • Java入门进阶
  • 日语学习-日语知识点小记-构建基础-JLPT-N4&N5阶段(4):~てもいいです & ~てはいきません征求许可
  • 在Mac M1上面安装Miniconda
  • 名词解释:npm,cnpm,yarn,vite,vue,electron
  • PySpark查找Dataframe中的非ASCII字符并导出Excel文件
  • 07贪心 + 动态规划(D1_基础学习)
  • 蓝桥杯试题:归并排序
  • PySide (PyQt)的视图(QGraphicsView)和场景(QGraphicsScene)
  • 深入理解现代前端框架:Vue.js 的进阶探秘
  • bash shell笔记——循环结构
  • 网络初识-
  • 基于微信小程序的超市售货管理平台
  • HttpServletRequest 作用
  • 从洗牌算法到前端工程化:如何用JavaScript实现真正的随机排序并应用到实际项目中
  • 2025常用的SEO工具有哪些?
  • 文档格式转换引擎开发:支持PDF与OFD的技术实现
  • 快速搭建支持图片实时处理的文件服务