dynamic-datasource实现数据读写分离
dynamic-datasource实现数据读写分离
背景
随着业务流量越来越大,所有数据库请求都访问mysql主库,给主库造成了较大的压力
目标
减轻mysql主库压力
现状
当前数据库部署架构为一主一从,从库只是单纯作为备份,没有承接线上流量,资源有闲置
方案
方案对比
通常会有其中思路
分库
分库的思路是讲数据库中表根据业务紧密程度拆分成几个不同的库,不同的表访问不同的库,不同的库分布在不同的节点,从而可以减轻单个库的压力,是一种横向扩展的思路,类似的还有分表,这里不做介绍
分库通常会涉及到系统的改造,比如原先在一个库的表,一个事务就可以保证,拆分成多个库之后,原先一个事务可以完成的,现在不能保证正确,通常涉及到分布式事务或者BASE等,改造成本比较高
读写分离
读写分离也是业界常用的方案之一,思路是在数据库主从同步延迟可以接受的范围内,讲一部分查询请求分流到数据库从库,从来降低主库压力
考虑到目前的数据容量以及改造成本,现阶段先采用读写分离的方式
实施
因为系统已经集成了dynamic-datasource介入了多个不同的数据源,因此方案基于此作
先了解下dynamic-datasource核心类
首先是com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
该Configuration定义了几个bean
ymlDynamicDataSourceProvider,用于创建实际的数据库如druid/c3p等,代码比较简单这里忽略
dataSource,实际类型是DynamicRoutingDataSource,是对众多dataSource的封装,接下来会细讲
dynamicDatasourceAnnotationAdvisor,主要功能就是解析接口或者方法上的@DS注解并将值并放入线程上下文(DynamicDataSourceContextHolder)中
接下来看DynamicRoutingDataSource类
先看afterPropertiesSet
主要是讲所有的实际数据库连接放到两个map
Map<String, DataSource>,最简单的分组,key是配置的数据库name,value是具体的数据库
Map<String, GroupDataSource>,将配置的数据库name按照下划线分割之后,取第一个分组
代码如下
接下来看下AbstractRoutingDataSource,是DynamicRoutingDataSource的基类
getConnection方法可以看到动态切换数据的逻辑,具体逻辑留给了子类实现即determineDataSource方法
接下来,我们DynamicDataSourceAutoConfiguration.determineDataSource方法
可以发现,如果从上下文可以获取到ds,将根据ds选择数据库,如果没有ds,将直接根据配置的primary选择数据库
还记得上文中提到的两个map吗,可以发现如果ds命中第一map时,返回的数据库是固定的,也就无法实现所谓的读写分离;除非每个方法都手动通过@DS注解指定,这种方式不友好;当第一个map不命中且第二个map命中时,实现轻松在多个数据库之间动态切换,且不需要在方法上指定@DS注解
因此第一个关键点就是要对 数据库进行分组,配置方式如下
如果走到分组,具体分组逻辑是GroupDataSource.determineDataSource方法
可以看到,具体选择分组中的那个数据库,是根据dynamicDataSourceStrategy来决定的,因此我们只需要实现一个dynamicDataSourceStrategy的实现能够根据读写请求选择不同的ds即可。
策略基本如下:
1、普通读请求,走从库或者主库都可以
2、普通写请求,走主库
3、强制指定了走主库的请求,走主库
4、事务执行,强制走主库
接下来难点在于
1、如何判断是读请求还是写请求
2、如何判断强制走主库
3、对于事物,比较特殊,在事务开启之前就需要切到主库然后获取数据库连接
一个一个解决
首先判断是否读请求还是写请求,这个需要使用mybatis拦截器,拦截Executor方法
通过第一个参数MappedStatement可以判断
接下来就是如何判断强制走主库了,这个需要自定义实现,通常是使用注解+AOP方式
所以整体实现比较简单
首先定义上下文,保存是否走主库标志
接下来实现策略,根据标志位选择主库或者从库
需要修改配置,指定spring.datasource.dynamic.strategy=Xxx.class才会生效
定义mybatis拦截器,设置
接下来自定义强制走主库注解
定义Aop
最后如何判断事务即将开启,spring-tx开启事务在AbstractPlatformTransactionManager.getTransaction方法,代码如下
最终会走到doBegin方法,具体实现类在DataSourceTransactionManager中
可以看到doBegin方法中获取了数据连接(Connection对象),结合上面的两个方法,可以发现两点
1、在获取数据库连接之前没有任何的回调和埋点,也就是说无法通过添加钩子在做额外逻辑,也不能通过上下文去判断
2、方法要么是protected,要么是final修饰,也就是说不能通过AOP来扩展
因此只剩下一种方法,通过继承去扩展,且需要修改默认的PlatformTransactionManager实现类
代码如下
最后更新于