前言
之前写过一篇针对数据源的隔离方案的文章https://blog.csdn.net/qq_39007838/article/details/128206830,主要是针对自动化的租户注册这块的实现,但是关于进来的租户切换这块的一些细节没有具体 debug,这篇文章也是这样一个场景
如何根据租户信息切换数据源的
我们看到 TenantInterceptor 这个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59@Component @Slf4j public class TenantInterceptor implements HandlerInterceptor { @Autowired private IMasterTenantService masterTenantService; @Autowired private DynamicRoutingDataSource dynamicRoutingDataSource; @Value("${spring.datasource.driverClassName}") private String driverClassName; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getServletPath(); String tenant= request.getHeader("tenant"); log.info("&&&&&&&&&&&&&&&& 租户拦截 &&&&&&&&&&&&&&&&"); if (StringUtils.isNotBlank(tenant)) { if (!dynamicRoutingDataSource.existDataSource(tenant)) { //搜索默认数据库,去注册租户的数据源,下次进来直接session匹配数据源 MasterTenant masterTenant = masterTenantService.selectMasterTenant(tenant); if (masterTenant == null) { throw new RuntimeException("无此租户:"+tenant ); }else if(TenantStatus.DISABLE.getCode().equals(masterTenant.getStatus())){ throw new RuntimeException("租户["+tenant+"]已停用" ); }else if(masterTenant.getExpirationDate()!=null){ if(masterTenant.getExpirationDate().before(DateUtils.getNowDate())){ throw new RuntimeException("租户["+tenant+"]已过期"); } } Map<String, Object> map = new HashMap<>(); map.put("driverClassName", driverClassName); map.put("url", masterTenant.getUrl()); map.put("username", masterTenant.getUsername()); map.put("password", masterTenant.getPassword()); dynamicRoutingDataSource.addDataSource(tenant, map); log.info("&&&&&&&&&&& 已设置租户:{} 连接信息: {}", tenant, masterTenant); }else{ log.info("&&&&&&&&&&& 当前租户:{}", tenant); } }else{ throw new RuntimeException("缺少租户信息"); } // 为了单次请求,多次连接数据库的情况,这里设置localThread,AbstractRoutingDataSource的方法去获取设置数据源 DynamicDataSourceContextHolder.setDataSourceKey(tenant); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 请求结束删除localThread DynamicDataSourceContextHolder.clearDataSourceKey(); } }
在 ResourcesConfig 里面注册了这个拦截器
1
2
3
4
5
6
7@Override public void addInterceptors(InterceptorRegistry registry) { // .... registry.addInterceptor(tenantInterceptor).addPathPatterns("/**").excludePathPatterns("/captchaImage").excludePathPatterns("/register"); }
可以看到,排除了注册和验证码接口,这两个接口显然是不需要切换租户数据源的
具体看下拦截器实现
-
所有 header 会带上 tenant ,解析出 租户名
-
从 主数据源拿到租户数据源配置信息
复制代码1
2
3
4
5
6
7
8@Override @DataSource(DataSourceType.MASTER) public MasterTenant selectMasterTenant(String tenant) { MasterTenant masterTenant = new MasterTenant(); masterTenant.setTenant(tenant); return masterTenantMapper.selectMasterTenant(masterTenant); }
这块注意一点要手动指定数据源为 master,后面一小节会简单说下 若依的多数据源实现,以及和这里的多租户插件是否存在冲突的分析
-
给当前线程绑定数据源 DynamicDataSourceContextHolder.setDataSourceKey(tenant);
DynamicDataSourceContextHolder 实现就是创建了 ThreadLocal,用于绑定数据源信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/** * 数据源切换处理 * * @author devjd */ @Slf4j public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> db = new ThreadLocal<>(); public static void setDataSourceKey(String key) { db.set(key); } public static String getDataSourceKey() { return db.get(); } public static void clearDataSourceKey() { db.remove(); } }
- 请求结束清理 DynamicDataSourceContextHolder
若依多数据源的实现
DynamicRoutingDataSource 继承spring 的 AbstractRoutingDataSource来扩展实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49/** * 动态数据源 * * @author devjd */ @Slf4j public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private static Map<Object, Object> targetTargetDataSources = new ConcurrentHashMap<>(); @Override protected Object determineCurrentLookupKey() { // 每次连接数据库,都会去设置数据源 return DynamicDataSourceContextHolder.getDataSourceKey(); } // 设置targetDataSources并记录数据源(这里可以记录每个数据源的最近使用时间,可以做删除不经常使用的数据源) @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); targetTargetDataSources = targetDataSources; } // 添加数据源 public void addDataSource(String tenant, Map<String, Object> dataSourceProperties) { targetTargetDataSources.put(tenant, dataSource(dataSourceProperties)); super.setTargetDataSources(targetTargetDataSources); afterPropertiesSet(); } // 判断是否存在数据源,存在直接取 public boolean existDataSource(String tenant) { return targetTargetDataSources.containsKey(tenant); } // 组装数据源 public DataSource dataSource(Map<String, Object> dataSourceProperties) { DataSource dataSource; try { dataSource = DruidDataSourceFactory.createDataSource(dataSourceProperties); } catch (Exception e) { log.error("dataSource: {}", e); throw new RuntimeException(); } return dataSource; } }
芋道 Spring Boot 多数据源(读写分离)入门 | 芋道源码 —— 纯源码解析博客 (iocoder.cn)
读取到注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56/** * 多数据源处理 * * @author ruoyi */ @Aspect @Order(1) @Component public class DataSourceAspect { protected Logger logger = LoggerFactory.getLogger(getClass()); @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)" + "|| @within(com.ruoyi.common.annotation.DataSource)") public void dsPointCut() { } @Around("dsPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { DataSource dataSource = getDataSource(point); if (StringUtils.isNotNull(dataSource)) { DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().name()); } try { return point.proceed(); } finally { // 销毁数据源 在执行方法之后 DynamicDataSourceContextHolder.clearDataSourceKey(); } } /** * 获取需要切换的数据源 */ public DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); if (Objects.nonNull(dataSource)) { return dataSource; } return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); } }
实现主要也是这一行,DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().name()); 其实和我们设置多租户时候用的办法是一样的,都是修改 DynamicDataSourceContextHolder,然后 又通过重写 AbstractRoutingDataSource,每次数据源读取的时候改为从 DynamicDataSourceContextHolder 读取
那么我们如果在经过多租户插件后,又在 mapper 层手动配置了 多数据源会用哪个呢?
答案是,按照先后顺序,会使用最后读取到的 DynamicDataSourceContextHolder
最后
以上就是落寞爆米花最近收集整理的关于若依多租户集成浅析(基于数据源隔离)2-多租户数据源切换插件实现的全部内容,更多相关若依多租户集成浅析(基于数据源隔离)2-多租户数据源切换插件实现内容请搜索靠谱客的其他文章。
发表评论 取消回复