这段时候在准备从零开始做一套SaaS系统,之前的经验都是开发单数据库系统并没有接触过SaaS系统,所以接到这个任务的时候也有也些头疼,不过办法部比困难多,难得的机会。
在网上找了很多关于SaaS的资料,看完后使我受益匪浅,写文章之前也一直在关注SaaS系统的开发,通过几天的探索也有一些方向。
多租户系统首先要解决的问题就是如何组织租户的数据问题,通常情况有三种解决方案:
按数据的隔离级别依次为:
一个租户一个数据库实例(数据库级)
一个租户一个Schema (Schema)
每个租户都存储在一个数据库 (行级)
以上三种数据组织方案网上都有一些介绍,就不多啰嗦了。理解三种隔离模式后,起初觉得还是蛮简单的真正开始实施的时候困难不少。
租户标识接口定义一个TenantInfo来标识租户信息,关于获取当前租户的方式,后面会再提到。
public interface TenantInfo { /** * 获取租户id * @return */ Long getId(); /** * 租户数据模式 * @return */ Integer getSchema(); /** * 租户数据库信息 * @return */ TenantDatabase getDatabase(); /** * 获取当前租户信息 * @return */ static Optional<TenantInfo> current(){ return Optional.ofNullable( TenantInfoHolder.get() ); } } DataSource 路由以前开发的系统基本都是一个DataSource,但是切换为多租户后我暂时分了两种数据源:
租户数据源(TenantDataSource)
系统数据源(SystemDataSource)
起初我的设想是使用Schema级但是由于是使用的Mysql中的Schema和Database是差不多的概念,所以后来的实现是基于数据库级的。使用数据库级的因为是系统是基于企业级用户的,数据都比较重要,企业客户很看重数据安全性方面的问题。
下面来一步步的解决动态数据源的问题。
DataSource 枚举 public enum DataSourceType { /** * 系统数据源 */ SYSTEM, /** * 多租户数据源 */ TENANT, } DataSource 注解定义DataSourceType枚举后,然后定义一个DataSource注解,名称可以随意,一时没想到好名称,大家看的时候不要跟javax.sql.DataSource类混淆了:
@Retention(RetentionPolicy.RUNTIME) @Documented @Target({ElementType.TYPE, ElementType.METHOD}) public @interface DataSource { /** * 数据源key * @return */ com.csbaic.datasource.core.DataSourceType value() default com.csbaic.datasource.core.DataSourceType.SYSTEM; } 处理 SpringBoot 自动装配的 DataSource如果你熟悉SpringBoot,应该知道有一个DataSourceAutoConfiguration配置会自动创建一个javax.sql.DataSource,由于在多租户环境下随时都有可能要切换数据源,所以需要将自动装配的javax.sql.DataSource替换掉:
@Slf4j public class DataSourceBeanPostProcessor implements BeanPostProcessor { @Autowired private ObjectProvider<RoutingDataSourceProperties> dataSourceProperties; @Autowired private ObjectProvider<TenantDataSourceFactory> factory; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if(bean instanceof DataSource){ log.debug("process DataSource: {}", bean.getClass().getName()); return new RoutingDataSource((DataSource) bean, factory, dataSourceProperties); } return bean; } }基于BeanPostProcessor的处理,将自动装配的数据源替换成RoutingDataSource,关于RoutingDataSource后面会再提到。这样可将自动装配的数据源直接作为系统数据源其他需要使用数据源的地方不用特殊处理,也不需要在每个服务中排除DataSourceAutoConfiguration的自动装配。
使用 ThreadLocal 保存数据源类型数据源的切换是根据前面提到的数据源类型枚举DataSourceType来的,当需要切换不到的数据源时将对应的数据源类型设置进ThreadLocal中:
public class DataSourceHolder { private static final ThreadLocal<Stack<DataSourceType>> datasources = new ThreadLocal<>(); /** * 获取当前线程数据源 * @return */ public static DataSourceType get(){ Stack<DataSourceType> stack = datasources.get(); return stack != null ? stack.peek() : null; } /** * 设置当前线程数据源 * @param type */ public static void push(DataSourceType type){ Stack<DataSourceType> stack = datasources.get(); if(stack == null){ stack = new Stack<>(); datasources.set(stack); } stack.push(type); } /** * 移除数据源配置 */ public static void remove(){ Stack<DataSourceType> stack = datasources.get(); if(stack == null){ return; } stack.pop(); if(stack.isEmpty()){ datasources.remove(); } } }在DataSourceHolder.datasources是使用的Stack而不是直接持有DataSource这样会稍微灵活一点,试想一下从方法A中调用方法B,A,B方法中各自要操作不同的数据源,当方法B执行完成后,回到方法A中,如果是在ThreadLocal直接持有DataSource的话,A方法继续操作就会对数据源产生不确定性。
AOP 切换数据源