当前位置:   article > 正文

基于mybatis-plus实现的多租户架构_mybatis-plus多租戶数据源多租户

mybatis-plus多租戶数据源多租户

整体概述

多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。

多租户在数据存储上主要存在三种方案,独立数据库、共享数据库,独立Schema、共享数据库,共享 Schema,共享数据表。

独立数据库 

即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。

  • 优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
  • 缺点:增多了数据库的安装数量,随之带来维护成本和购置成本的增加。

共享数据库,独立Schema、共享数据库

共同使用一个数据库,使用表进行数据隔离,多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是DB2、ORACLE等,一个数据库下可以有多个SCHEMA。

  • 优点:为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
  • 缺点:如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;

方案选择

免费租户(体验租户)

免费租户或者体验租户,考虑到维护和购置成本,使用共享数据库,共享 Schema,共享数据表方案,通过tenant_id区分不同租户数据,实现数据隔离。

付费租户

收费租户为了获得更好的用户体验,结合MySql数据库,采用独立数据库方案,不同租户对应不同数据库,数据隔离级别最高,安全性最好。

租户升级

由免费租户(体验租户)升级到付费租户,不仅要为该租户创建独立数据库,还要同步历史数据,在不影响用户体验的情况,升级操作可以考虑异步定时处理。

技术选型

Mybatis-Plus简介

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
  • 多数据源切换数据源分组,适用于多种场景:纯粹多库、读写分离、一主多从、混合模式,提供对DruidMybatis-PlusP6syJndi的快速集成使用spel动态参数解析数据源,如从sessionheader或参数中获取数据源。

多租户SQL解析器

通过PaginationInterceptor拦截特定sql,加上tenant_id。具体代码如下:

  1. @Bean
  2. public PaginationInterceptor paginationInterceptor() {
  3. PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
  4. // SQL解析处理拦截:增加租户处理回调。
  5. List<ISqlParser> sqlParserList = new ArrayList<>();
  6. TenantSqlParser tenantSqlParser = new TenantSqlParser();
  7. tenantSqlParser.setTenantHandler(new TenantHandler() {
  8. @Override
  9. public Expression getTenantId(boolean where) {
  10. HttpServletRequest request = null;
  11. // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
  12. if (null != RequestContextHolder.getRequestAttributes()) {
  13. request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
  14. }
  15. Long currentTenantId;
  16. try {
  17. String tenantId;
  18. if (null == request) {
  19. tenantId = TenantIdContext.peek();
  20. } else {
  21. tenantId = request.getHeader("tenantId");
  22. if (StringUtils.isBlank(tenantId)) {
  23. HttpSession session = request.getSession();
  24. tenantId = session.getAttribute(tenantId) == null ? null : session.getAttribute(tenantId).toString();
  25. }
  26. if (StringUtils.isBlank(tenantId)) {
  27. tenantId = TenantIdContext.peek();
  28. }
  29. }
  30. currentTenantId = Long.valueOf(tenantId);
  31. } catch (Exception e) {
  32. throw new RuntimeException("getTenantId error.");
  33. }
  34. return new LongValue(currentTenantId);
  35. }
  36. @Override
  37. public String getTenantIdColumn() {
  38. return SYSTEM_TENANT_ID;
  39. }
  40. @Override
  41. public boolean doTableFilter(String tableName) {
  42. // 这里可以判断是否过滤表
  43. return false;
  44. }
  45. });
  46. sqlParserList.add(tenantSqlParser);
  47. paginationInterceptor.setSqlParserList(sqlParserList);
  48. paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {
  49. @Override
  50. public boolean doFilter(MetaObject metaObject) {
  51. /**
  52. * 如果在程序中,有部分SQL不需要加上租户ID的表示,需要过滤特定的sql
  53. */
  54. MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
  55. String msId = ms.getId();
  56. // 不是meeting下面的sql都不需要加租户ID
  57. if (msId.startsWith("com.hixiaoe.meeting.persistence.meeting.dao")) {
  58. return false;
  59. }
  60. return true;
  61. }
  62. });
  63. return paginationInterceptor;
  64. }

初始化数据源

DynamicRoutingDataSource继承了AbstractRoutingDataSource抽象类,实现了InitializingBean, DisposableBean接口,内部维护了一个LinkedHashMap,用于存放当前所有数据源。通过DynamicDataSourceCreator创建未初始化的数据源,并添加数据源到当前数据源map中,以供动态切换使用,具体实现如下:

  1. public static void addDataSource(String dsName, String username, String password, String driverName, String url) {
  2. DataSourceProperty dataSourceProperty = new DataSourceProperty();
  3. dataSourceProperty.setPollName(dsName);
  4. dataSourceProperty.setDriverClassName(driverName);
  5. dataSourceProperty.setUrl(url);
  6. dataSourceProperty.setUsername(username);
  7. dataSourceProperty.setPassword(password);
  8. DruidConfig druidConfig = new DruidConfig();
  9. druidConfig.setInitialSize(1);
  10. dataSourceProperty.setDruid(druidConfig);
  11. Map<String, DataSource> currentDataSources = dsUtil.dynamicRoutingDataSource.getCurrentDataSources();
  12. if (!currentDataSources.containsKey(dsName)) {
  13. DataSource ds = dsUtil.dynamicDataSourceCreator.createDruidDataSource(dataSourceProperty);
  14. dsUtil.dynamicRoutingDataSource.addDataSource(dsName, ds);
  15. }
  16. }

数据源切换

使用 @DS ("dsName")切换数据源,@DS 可以注解在方法上和类上,同时存在,方法注解优先于类上注解。强烈建议只注解在service实现上,没有@DS,则采用默认数据源。dsName可以为组名也可以为具体某个库的名称(可以从session,header或参数中获取数据源)。

存在会话

http请求header中,或者httpsession设置tenantId,通过sql解析器在特定sql中处理对应租户数据。

异步线程

通过ThreadLocal设置当前线程tenantId,以供sql解析器在特定sql中处理对应租户数据。

整体架构图

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/407615
推荐阅读
相关标签
  

闽ICP备14008679号