要是在每个类方法都需要手机切换数据源,那也太不方便了,得益于AOP编程可以在调用需要切换数据源的方法的时候做一些手脚:
@Slf4j @Aspect public class DataSourceAspect { @Pointcut(value = "(@within(com.csbaic.datasource.annotation.DataSource) || @annotation(com.csbaic.datasource.annotation.DataSource)) && within(com.csbaic..*)") public void dataPointCut(){ } @Before("dataPointCut()") public void before(JoinPoint joinPoint){ Class<?> aClass = joinPoint.getTarget().getClass(); // 获取类级别注解 DataSource classAnnotation = aClass.getAnnotation(DataSource.class); if (classAnnotation != null){ com.csbaic.datasource.core.DataSourceType dataSource = classAnnotation.value(); log.info("this is datasource: "+ dataSource); DataSourceHolder.push(dataSource); }else { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); DataSource methodAnnotation = method.getAnnotation(DataSource.class); if (methodAnnotation != null){ com.csbaic.datasource.core.DataSourceType dataSource = methodAnnotation.value(); log.info("this is dataSource: "+ dataSource); DataSourceHolder.push(dataSource); } } } @After("dataPointCut()") public void after(JoinPoint joinPoint){ log.info("执行完毕!"); DataSourceHolder.remove(); } }DataSourceAspect很简单在有com.csbaic.datasource.annotation.DataSource注解的方法或者类中切换、还原使用DataSourceHolder类切换数据源。
动态获取、构造数据源前面说了那么多都是在为获取、构建数据源做准备工作,一但数据源切换成功,业务服务获取数据时就会使用javax.sql.DataSource获取数据库连接,这里就要说到RoutingDataSource了:
@Slf4j public class RoutingDataSource extends AbstractDataSource { /** * 已保存的DataSource */ private final DataSource systemDataSource; /** * 租户数据源工厂 */ private final ObjectProvider<TenantDataSourceFactory> factory; /** * 解析数据源 * @return */ protected DataSource resolveDataSource(){ DataSourceType type = DataSourceHolder.get(); RoutingDataSourceProperties pros = properties.getIfAvailable(); TenantDataSourceFactory tenantDataSourceFactory = factory.getIfAvailable(); if(tenantDataSourceFactory == null){ throw new DataSourceLookupFailureException("租户数据源不正确"); } if(pros == null){ throw new DataSourceLookupFailureException("数据源属性不正确"); } if(type == null){ log.warn("没有显示的设置数据源,使用默认数据源:{}", pros.getDefaultType()); type = pros.getDefaultType(); } log.warn("数据源类型:{}", type); if(type == DataSourceType.SYSTEM){ return systemDataSource; }else if(type == DataSourceType.TENANT){ return tenantDataSourceFactory.create(); } throw new DataSourceLookupFailureException("解析数据源失败"); } }在resolveDataSource方法中,首先获取数据源类型:
DataSourceType type = DataSourceHolder.get();然后根据数据源类型获取数据源:
if(type == DataSourceType.SYSTEM){ return systemDataSource; }else if(type == DataSourceType.TENANT){ return tenantDataSourceFactory.create(); }系统类型的数据源较简单直接返回,在租户类型的数据时就要作额外的操作,如果是数据库级的隔离模式就需要为每个租户创建数据源,这里封装了一个TenantDataSourceFactory来构建租户数据源:
public interface TenantDataSourceFactory { /** * 构建一个数据源 * @return */ DataSource create(); /** * 构建一个数据源 * @return */ DataSource create(TenantInfo info); }实现方面大致就是从系统数据源中获取租户的数据源配置信息,然后构造一个javax.sql.DataSource。
注意:租户数据源一定要缓存起来,每次都构建太浪费。。。
小结经过上面的一系统配置后,相信切换数据已经可以实现了。业务代码不关心使用的数据源,后续切换成隔离模式也比较方便。但是呢,总觉得只支持一种隔离模式又不太好,隔离模式更高的模式也可以作为收费项的麻。。。
使用 Mybatis Plus 实现行级隔离模式上前提到动态数据源都是基于数据库级的,一个租户一个数据库消耗还是很大的,难达到SaaS的规模效应,一但租户增多数据库管理、运维都是成本。
比如有些试用用户不一定用购买只是想试用,直接开个数据库也麻烦,况且前期开发也麻烦的很,数据备份、还原、字段修改都要花时间和人力的,所以能不能同时支持多种数据隔离模式呢?答案是肯定的,利益于Mybatis Plus可的多租户 SQL 解析器以轻松实现,详细文档可参考:
多租户 SQL 解析器:https://mp.baomidou.com/guide/tenant.html只需要配置TenantSqlParser和TenantHandler就可以实现行级的数据隔离模式:
public class RowTenantHandler implements TenantHandler { @Override public Expression getTenantId(boolean where) { TenantInfo tenantInfo = TenantInfo.current().orElse(null); if(tenantInfo == null){ throw new IllegalStateException("No tenant"); } return new LongValue(tenantInfo.getId()); } @Override public String getTenantIdColumn() { return TenantConts.TENANT_COLUMN_NAME; } @Override public boolean doTableFilter(String tableName) { TenantInfo tenantInfo = TenantInfo.current().orElse(null); //忽略系统表或者没有解析到租户id,直接过滤 return tenantInfo == null || tableName.startsWith(SystemInfo.SYS_TABLE_PREFIX); } }回想一下上面使用的TenantDataSourceFactory接口,对于行级的隔离模式,构造不同的数据源就可以了。
如何解析当前租户信息?多租户环境下,对于每一个http请求可能是对系统数据或者租户数据的操作,如何区分租户也是个问题。
以下列举几种解析租户的方式:
系统为每个用户生成一个二级域名如:tenant-{id}.csbaic.com业务系统使用Host、Origin、X-Forwarded-Host等请求头按指定的模式解析租户
前端携带租户id参数如:?tenantId=xxx
根据请求uri路径获取如:{tenantId}
解析前端传递的token,获取租户信息
租户自定义域名解析,有些功能租户可以绑定自己的域名