https://blog.51cto.com/u_15444123/4713797

什么是多租户?

多租户技术或称多重租赁技术,简称**SaaS**,是一种软件架构技术,是实现如何在多用户环境下(多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。

简单说就是每个用户的数据隔离了。比如一个用户一个数据库,这个用户指的是机构学校公司等用户。一个用户数据库里面又包含了很多用户的用户。

多租户架构以及数据隔离方案

1.独立数据库

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

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

    2.共享数据库,独立 Schema

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

  • 优点:为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。

  • 缺点:如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据;

    3.共享数据库,共享 Schema,共享数据表

    也就是说 共同使用一个数据库一个表 使用字段进行数据隔离
    即租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
    简单来讲,即每插入一条数据时都需要有一个客户的标识。这样才能在同一张表中区分出不同客户的数据,这也是我们系统目前用到的(tenant_id)

  • 优点:三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。

  • 缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;数据备份和恢复最困难,需要逐表逐条备份和还原。

    springboot动态数据源原理

    image.png

    1.继承AbstractRoutingDataSource得到数据源

    抽象类AbstractRoutingDataSource,通过继承这个类实现根据不同的请求切换数据源。
    AbstractRoutingDataSource继承自AbstractDataSource,如果声明一个类继承AbstractRoutingDataSource则这个类本身就是数据源。

    2.数据源的getConnection()方法

    既然是数据源一定会用到getConnection()方法,下面看源码: ```java public Connection getConnection() throws SQLException {

    1. return this.determineTargetDataSource().getConnection();

    }

    public Connection getConnection(String username, String password) throws SQLException {

    1. return this.determineTargetDataSource().getConnection(username, password);

    }

  1. 通过上面源码能分析得到数据库连接是由determineTargetDataSource()得来,下面继续分析determineTargetDataSource()方法。
  2. <a name="KAtid"></a>
  3. ## 3.解析determineTargetDataSource方法
  4. 这个方法是用来设置数据源的,重写的时候也是这个方法设置数据源
  5. ```java
  6. protected DataSource determineTargetDataSource() {
  7. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  8. Object lookupKey = this.determineCurrentLookupKey();
  9. DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
  10. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  11. dataSource = this.resolvedDefaultDataSource;
  12. }
  13. if (dataSource == null) {
  14. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  15. } else {
  16. return dataSource;
  17. }
  18. }

通过这段源码能否得到数据源首先需要获取 lookupKey:
Object lookupKey = this.determineCurrentLookupKey();
然后通过这个key得到对应的数据源:
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);

4.解析afterPropertiesSet

看代码resolvedDataSources的属性,首先它是Map,通过Object可以得到DataSoure

  1. //用户设置的目标数据源和用户设置的默认目标数据源,需要转换成最终数据源才能使用
  2. @Nullable
  3. private Map<Object, Object> targetDataSources;
  4. @Nullable
  5. private Object defaultTargetDataSource;
  6. //最终决定的数据源,最终使用这个resolved数据源作为最终数据源
  7. @Nullable
  8. private Map<Object, DataSource> resolvedDataSources;
  9. @Nullable
  10. private DataSource resolvedDefaultDataSource;

然后这个属性的赋值代码:

  1. public void afterPropertiesSet() {
  2. if (this.targetDataSources == null) {
  3. throw new IllegalArgumentException("Property 'targetDataSources' is required");
  4. } else {
  5. this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
  6. this.targetDataSources.forEach((key, value) -> {
  7. Object lookupKey = this.resolveSpecifiedLookupKey(key);
  8. DataSource dataSource = this.resolveSpecifiedDataSource(value);
  9. this.resolvedDataSources.put(lookupKey, dataSource);
  10. });
  11. if (this.defaultTargetDataSource != null) {
  12. this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
  13. }
  14. }
  15. }

可以看到是将targetDataSources对象的内容赋值给了它。就是说用户自己设置的数据源最终会被这个方法转换成系统最终决定的数据源。

倒入依赖

  1. <dependency>
  2. <groupId>com.baomidou</groupId>
  3. <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  4. <version>3.5.1</version>
  5. </dependency>

实现动态数据源

  1. package com.lyd.holder;
  2. import com.alibaba.druid.pool.DruidDataSource;
  3. import com.lyd.utils.RedisCache;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.jdbc.core.JdbcTemplate;
  6. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  7. import org.springframework.stereotype.Component;
  8. import javax.sql.DataSource;
  9. import java.sql.Connection;
  10. import java.sql.SQLException;
  11. import java.util.HashMap;
  12. import java.util.List;
  13. import java.util.Map;
  14. /**
  15. * 继承AbstractRoutingDataSource实现动态数据源
  16. * 需要放入容器中,这个类写完了动态数据源就成了,能切换,不过细节就是怎么切换,需要再附加一些代码
  17. * 怎么得到数据源的key是关键
  18. */
  19. @Component
  20. public class DynamicDataSource extends AbstractRoutingDataSource {
  21. //redis操作类,可有可无,用来判断当前数据源的,还有其他办法
  22. @Autowired
  23. RedisCache redisCache;
  24. //默认数据源
  25. private static DataSource defaultDataSource;
  26. //保存所有的数据源,key就是数据源的名字,value就是数据源
  27. private static Map<Object,Object> targetDataSources = new HashMap<>();
  28. static {
  29. //初始化默认数据源,用的Druid数据源
  30. DruidDataSource source = new DruidDataSource();
  31. source.setUrl("jdbc:sqlserver://127.0.0.1:1433;DatabaseName=CATALOG");
  32. source.setUsername("sa");
  33. source.setPassword("123456");
  34. source.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
  35. source.setInitialSize(2);
  36. source.setMinIdle(2);
  37. source.setMaxActive(5);
  38. defaultDataSource = source;
  39. }
  40. /**
  41. * 获取数据库连接
  42. * @return
  43. * @throws SQLException
  44. */
  45. @Override
  46. public Connection getConnection() throws SQLException {
  47. return super.getConnection();
  48. }
  49. /**
  50. * 设置默认数据源
  51. */
  52. @Override
  53. public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
  54. super.setDefaultTargetDataSource(defaultDataSource);
  55. }
  56. /**
  57. * 设置用户的数据源,并制作成系统用的最终数据源
  58. */
  59. @Override
  60. public void afterPropertiesSet() {
  61. //初始化所有租户的数据源
  62. initTargetDataSources();
  63. //一些参数设置操作
  64. super.afterPropertiesSet();
  65. }
  66. /**
  67. * 初始化所有租户的数据源
  68. */
  69. public void initTargetDataSources(){
  70. //去数据库里面查询得到所有动态数据源
  71. //这里不能用mybatis mapper查询,会照成循环依赖,或者用其他的办法,数据源存在其他地方等,总之就是在这个地方要获取到用户的全部数据源信息
  72. JdbcTemplate jdbcTemplate = new JdbcTemplate(defaultDataSource);
  73. List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from dbset");
  74. //遍历这些数据源信息,把他们组装成数据源,加入到上面定义的用来装全部数据源的map里面
  75. for(Map<String, Object> dbSetMap : maps){
  76. try {
  77. //创建租户的数据源
  78. DataSource tenantDataSource = createDataSourceByTTenant(dbSetMap);
  79. //放到所有数据源的容器中
  80. targetDataSources.put(dbSetMap.get("setid"), tenantDataSource);
  81. } catch (Exception e) {
  82. //e.printStackTrace();
  83. }
  84. }
  85. //传给父类,设置所有的数据源Map
  86. super.setTargetDataSources(targetDataSources);
  87. }
  88. /**
  89. * 每次执行sql请求,都会调用这个方法,返回一个key,然后在数据源map里面通过这个key查找对应的数据源,就用这个key的数据源进行sql操作
  90. * 这个方法返回的key是怎么来的是关键,每个请求是那个数据源,主要这个决定的,我这里写的是从redis里面取
  91. * 比如用户发请求带的token里面的的用户id是2,就去redis里面找用户2的数据源key,然后这个方法返回用户2的数据源key,去全部数据源里面找到用户2的数据源,数据源就切换到了用户2的
  92. */
  93. @Override
  94. protected Object determineCurrentLookupKey() {
  95. return redisCache.getCacheObject("k1");
  96. }
  97. /**
  98. * 每次sql请求会决定使用哪个数据源
  99. * 和上面的方法是联动的,看上面的注解
  100. */
  101. @Override
  102. protected DataSource determineTargetDataSource() {
  103. DataSource dataSource = null;
  104. //如果未获取到
  105. if (null == determineCurrentLookupKey()) {
  106. dataSource = defaultDataSource;
  107. }else {
  108. dataSource = (DataSource) targetDataSources.get(determineCurrentLookupKey());
  109. }
  110. return dataSource;
  111. }
  112. /**
  113. * 根据表里数据源 初始化话数据源
  114. * 这个完全就是自定义的,我的数据源来源,结构等不定,但是最终都要封装成一个数据源返回,用的是druid
  115. * @return
  116. */
  117. public DataSource createDataSourceByTTenant(Map<String, Object> dbSet){
  118. String ip=dbSet.get("dbserver").toString().split(",")[0];
  119. String port=dbSet.get("dbserver").toString().split(",")[1];
  120. String dbname=dbSet.get("dbname").toString();
  121. String username=dbSet.get("dbuser").toString();
  122. String pwd=dbSet.get("dbpass").toString();
  123. //创建一个数据源返回
  124. DruidDataSource source = new DruidDataSource();
  125. source.setUrl("jdbc:sqlserver://"+ip+":"+port+";DatabaseName="+dbname);
  126. source.setUsername(username);
  127. source.setPassword(pwd);
  128. source.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
  129. source.setInitialSize(2);
  130. source.setMinIdle(1);
  131. source.setMaxActive(3);
  132. return source;
  133. }
  134. }