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实现类

代码如下

最后更新于