SpringBoot2.7构建多租户动态切换数据库SaaS系统

引言

SaaS(Software-as-a-Service,软件即服务)是一种基于云计算的软件交付模式,用户可以通过互联网按需使用软件应用,多租户系统它是一种软件架构模式。

传统模式与多租户模式系统架构的区别:

传统系统架构图

多租户系统架构图

实现多租户问题

第一、多租户数据存储方式

多租户数据存储在数据库中时,有几种不同的方案可供选择,这些方案具有不同的特点和适用场景。

  1. 独立数据库:在这种方案中,每个租户都有自己的数据库,这是一种数据隔离级别最高的方案。这种方案适用于对数据安全性要求非常高的场景,如银行、医院等机构。但是,这种方案的成本相对较高,因为它需要为每个租户安装和维护独立的数据库实例。
  2. 共享数据库,隔离数据架构:在这种方案中,多个或所有租户共享同一个数据库,但每个租户都有自己的表。这种方案提供了较高的安全性,同时减少了数据库的安装数量和成本。每个租户的数据被自动隔离在自己的表中,其他租户无法访问。但是,如果需要跨租户统计数据,可能会存在一定的困难。
  3. 共享数据库,共享数据架构:在这种方案中,所有租户共享同一个数据库和同一张表,但在表中通过特定的标识符(如租户ID)来区分不同的租户数据。这种方案减少了数据库的安装数量和成本,同时提供了跨租户数据访问的便利性。但是,如果某个租户的数据出现问题,可能会影响到其他租户。
  4. 分布式数据库:利用分布式数据库系统将数据存储在多个节点上,以实现数据的分布式存储和共享。这种方案可以处理大量数据,提供高可用性和高扩展性,适用于大规模应用和高并发访问。

第二、实现多租户动态添加租户库以及访问库问题

问题1、租户如何动态切换访问数据库

问题2、如何动态添加租户数据库

问题1:针对不同租户如何动态切换数据库的问题,可以参考如下解决方案:

  1. 在请求头Header 设置租户信息,服务端通过解析Header中数据获取租户信息
  2. 将租户信息作为请求Url中的参数进行传递给服务端,服务端进行识别获取数据,
    例如:crazy.com?tenantCode=xige1,crazy.com?tenantCode=xige2
  3. 通过域名的方式来识别租户,给每个租户分配一个二级域名,通过二级域名实现区分租户比如:xige.crazy.com,xige2.crazy.com

问题2:针对如何动态添加租户数据库的问题,可以参考如下解决方案进行处理:

动态添加数据库,可以使用mybatis-plus框架来实现,官网提供了两种方式实现多数据库切换的框架,随着租户的体量越来越大,需要延伸多数据源的扩展,官方目前提供了两种框架来实现,具体如下介绍(如下摘自官网):

这里使用dynamic-datasource框架,具体实现过程可参考下一节。


多租户应用落地实现

项目实现采用独立数据库模式实现多租户,即一个租户一个数据库,技术栈采用springboot2.7.6集成mybatis-plusdynamic-datasoruce框架来实现,数据库采用mysql进行数据存储。

系统架构图参考

第一步、准备租户数据表

CREATE TABLE `sys_tenant` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_name` varchar(255) DEFAULT NULL COMMENT '租户名称',
  `datasource_schema` varchar(500) DEFAULT NULL COMMENT 'schema',
  `datasource_url` varchar(500) DEFAULT NULL COMMENT '数据库url,添加后不允许修改',
  `datasource_username` varchar(255) DEFAULT NULL COMMENT '数据库用户名',
  `datasource_password` varchar(255) DEFAULT NULL COMMENT '数据库密码',
  `datasource_driver` varchar(255) DEFAULT NULL COMMENT '数据库驱动,默认为mysql驱动',
  `status` tinyint(1) DEFAULT NULL COMMENT '租户状态,1-正常,2-停用',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='租户';

第二步、系统搭建

步骤一:添加依赖

在你的项目中引入 dynamic-datasourcemybatis-plus 的相关依赖。可以在 pom.xml 文件中添加以下依赖:

        
            com.alibaba
            druid-spring-boot-starter
            1.2.11
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.3.1
        
        
            com.baomidou
            dynamic-datasource-spring-boot-starter
            3.6.1
        

步骤二:配置数据源

application.yml 或者 application.properties 文件中配置你的数据源。例如:

spring:
  autoconfigure:
    # 排除原有的连接池
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    # 使用dynamicDatasource框架
    dynamic:
      #严格匹配数据源,默认false,true为匹配到指定数据源时抛出异常,false使用默认数据源
      strict: true
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/crazy_saas?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver

步骤三:动态创建多租户数据库

动态创建租户数据库,可以定义一个接口实现。例如:

package net.crazychina.app.controller;


@RestController
@Api(tags = "租户管理中心")
public class TenantController {

    @Resource
    private MultiTenantService multiTenantService;

    @Resource
    private DataSource dataSource;

    @PostMapping("addTenant")
    @ApiOperation("添加租户库")
    public Result initTenantDb(@RequestBody DataSourceDTO dto) {

        if (multiTenantService.getSingleTenant(dto.getDatasourceSchema()) > 0) {
            return Result.error("数据源已经存在,无需重复创建!");
        }
        int result = multiTenantService.insertTenant(dto);
        if (result > 0) {
            tenantDatasource(dto);
        }

        return Result.success("新增租户数据源成功");
    }

    // 动态添加数据源
    private void tenantDatasource(DataSourceDTO dto) {
        DruidDataSource tmpdb = new DruidDataSource();
        tmpdb.setUrl(dto.getDatasourceUrl());
        tmpdb.setUsername(dto.getDatasourceUsername());
        tmpdb.setPassword(dto.getDatasourcePassword());
        tmpdb.setDriverClassName(dto.getDatasourceDriver());
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        try {
            ds.addDataSource(dto.getDatasourceSchema(), tmpdb);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
@Data
public class DataSourceDTO implements Serializable {
    @NotBlank
    private String tenantName;

    @NotBlank
    private String datasourceUrl;

    @NotBlank
    private String datasourceDriver;

    @NotBlank
    private String datasourceSchema;

    @NotBlank
    private String datasourceUsername;

    @NotBlank
    private String datasourcePassword;

}

步骤四:动态切换数据库

定义拦截器,对前端传过来的租户信息进行拦截处理,进而进行数据库的动态切换。可以如下参考:

动态切换租户库核心代码

DynamicDataSourceContextHolder.push("数据源名称"); //动态切换租户库代码

@Slf4j
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Resource
    private TenantInterceptor tenantInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.tenantInterceptor);
    }
}
@Slf4j
@Component
public class TenantInterceptor implements HandlerInterceptor {
    final static ThreadLocal threadLocal=new ThreadLocal<>();

    /**
     * 在请求处理前调用
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(!(handler instanceof HandlerMethod)){
            // 不是http request请求直接放行
            return true;
        }
        String tenantCode = request.getHeader("tenantCode");
        //如果tenantCode为空,则使用默认数据源
        if (StringUtils.isNotEmpty(tenantCode)){
            DynamicDataSourceContextHolder.push(tenantCode);
            threadLocal.set(true);
        }
        return true;
    }

    /**
     * 在整个请求结束之后被调用
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 方法执行完毕或者执行异常后,移除数据源
        if (null != threadLocal.get() && threadLocal.get()) {
            DynamicDataSourceContextHolder.clear();
        }
        threadLocal.remove();
    }
}


总结

如果你也有这方面的需求,实现一个Saas多租户系统,又不想使用比较重的分库分表插件,可以采用Mybatis-Plus集成Dynamic-datasource插件来实现多租户系统,该插件满足大部分Saas场景功能实现,可以满足不同的租户创建独立的数据库,也支持跨库查询、分布式事务等功能,是一个实现Saas应用不错的选择。

展开阅读全文

页面更新:2024-05-22

标签:租户   数据库   动态   系统   数据源   分布式   架构   框架   方案   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top