SaaS 系统架构,Spring Boot 动态数据源实现! (2)

要是在每个类方法都需要手机切换数据源,那也太不方便了,得益于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,获取租户信息

租户自定义域名解析,有些功能租户可以绑定自己的域名

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyygff.html