数据库读写分离是构建高性能 Web 架构不可缺少的一环,其主要提升在于:
- 主从职责单一,主写从读,可以极大程度地缓解 X 锁和 S 锁的竞争,并且可以进行针对性调优
- 请求分流,减少主库压力
- 当读成为 DB 瓶颈时,很容易进行水平拓展
- 增加冗余,实现高可用,出现故障后可快速恢复,仅丢失少量数据或不丢失数据
读写分离首先需要 DB 实例的支持,配置主库、从库以及主从同步策略,此步骤一般交给 OP 即可。实例搭建完毕后,我们就可以开发相应模块,以实现真正的读写分离。
业界的实现方式一般分为两种:DB 中间件 和 应用层读写分离,二者均有各自的优缺点,详情见下表:
优点:对于应用透明;不限语言
缺点:专人部署 + 维护;保证 HA、LB;一般只支持 MySQL
优点:开发简单,团队内部可以自行消化;基于 JDBC 驱动或框架,理论支持任意类型的 DB
缺点:通用性差,各应用需要自己实现;手动指定数据源
用不用 DB 中间件需要考虑实际情况,如数据体量和有没有人维护等等,本文讲的是应用层读写分离。
通过自定义注解 @DataSourceRoute
,手动声明当前方法操作的数据源,再通过切面拦截该切入点,路由到目标数据源。
因为实际中还要与事务结合,所以又写了一套基于事务路由主从数据源的切面,使用起来较为繁琐。
//annotation
public @interface DataSourceRoute {
AccessType type() default AccessType.MASTER;
}
//aspect
public class DataSourceRouteAspect {
@Before("@annotation(DataSourceRoute)")
public void before(JoinPoint point) {
Method targetMethod = ((MethodSignature) point.getSignature()).getMethod();
DataSourceRoute annotation = targetMethod.getAnnotation(DataSourceRoute.class);
DynamicDataSourceHolder.route(annotation.type());
}
}
//dao
@DataSourceRoute(type=AccessType.SLAVE)
public Housedel findByPK(Long housedelCode) {
return mapper.findByPK(housedelCode);
}
其实总结一下我们使用读写分离的场景会发现,主库一般负责写入(偶尔用来读),从库则全部用来读取。而为了保障数据的正确性,我们在写入操作时一般会加上事务(这也是我推荐的最佳实践),也就是说,大部分事务操作是在写入,大部分非事务操作则是在读取,由此可见读写分离和事务之间是有一定关联的。
既然思路是可行的,那我们不妨思考一下,实际使用中具体有哪些场景呢?
序号 | 事务 | 数据源 | 操作 |
---|---|---|---|
1 | 无 | 从库 | 读 |
2 | 无 | 主库 | 读 |
3 | 有 | 从库 | 读 |
4 | 有 | 主库 | 写 |
第 1 种,无事务从库读取。典型的只读场景,我们的业务场景一般是读多写少,为了方便,可以作为默认选项。
第 2 种,无事务主库读取。主库中读取数据的情况还是比较少见的,一般是因为对数据的实时性要求较高,而 MySQL 的主从复制是异步的,中间会有短暂的时间差,为了保证数据的一致性,会直接从主库读取。
第 3 种,有事务从库读取。前面我们说道,事务一般加在写入操作上,但也有个别情况只读时也需要加入事务,比如在当前只读事务内,不希望其它事务更改数据,从而保证数据前后的一致性。
第 4 种,有事务主库写入。典型的写入场景,数据写入主库后,异步复制到从库。
那么如何实现呢?阅读 Spring 的源码会发现,DataSourceTransactionManager
是 Spring 用来管理事务的类,我们只需要自定义一个事务管理器,在开启事务之前指定数据源即可。
有了之前的分析,我们可以得到以下规则:默认无事务时路由到从库,有事务且非只读时路由到主库。
- 定义动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(Object defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.get();
}
}
public final class DynamicDataSourceHolder {
public static final String MASTER_DATA_SOURCE = "Master";
public static final String SLAVE_DATA_SOURCE = "Slave";
private static final ThreadLocal<String> CONTAINER = ThreadLocal.withInitial(
() -> DynamicDataSourceHolder.SLAVE_DATA_SOURCE
);
public static void routeMaster() {
CONTAINER.set(MASTER_DATA_SOURCE);
}
public static void routeSlave() {
CONTAINER.set(SLAVE_DATA_SOURCE);
}
public static String get() {
return CONTAINER.get();
}
public static void clear() {
CONTAINER.remove();
}
}
- 声明动态数据源
@Configuration
public class SpringDataSourceConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource masterDataSource() {
return new DruidDataSource();
}
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource slaveDataSource() {
return new DruidDataSource();
}
@Bean
@Primary
public DynamicDataSource dataSource(DruidDataSource masterDataSource, DruidDataSource slaveDataSource) {
Map<Object, Object> targetDataSources = ImmutableMap.builder()
.put(MASTER_DATA_SOURCE, masterDataSource)
.put(SLAVE_DATA_SOURCE, slaveDataSource)
.build();
return new DynamicDataSource(slaveDataSource, targetDataSources);
}
}
- 重写 Spring 默认的事务管理器
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
public DynamicDataSourceTransactionManager(DataSource dataSource) {
super(dataSource);
}
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
if (!definition.isReadOnly()) {
DynamicDataSourceHolder.routeMaster();
}
super.doBegin(transaction, definition);
}
@Override
protected void doCleanupAfterCompletion(Object transaction) {
DynamicDataSourceHolder.clear();
super.doCleanupAfterCompletion(transaction);
}
}
- 声明自定义的事务管理器
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
public class SpringDaoConfig implements TransactionManagementConfigurer {
@Autowired
private DynamicDataSource dataSource;
@Override
@Bean
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DynamicDataSourceTransactionManager(dataSource);
}
}
代码贴完了,让我们来看看能不能满足之前的 4 种场景呢?
其中 1、4 的区别仅仅是加不加事务,比较简单,那么待解决的还有 2 和 3。第 2 种因为没有事务,需要我们手动指定数据源,第 3 种则使用 Spring 提供的只读事务即可实现。
序号 | 事务 | 数据源 | 操作 | 实现方式 |
---|---|---|---|---|
1 | 无 | 从库 | 读 | 默认 |
2 | 无 | 主库 | 读 | 手动指定 DynamicDataSourceHolder.routeMaster() |
3 | 有 | 从库 | 读 | @Transactional(readOnly = true) |
4 | 有 | 主库 | 写 | @Transactional |
如此一来,之前的问题都已经解决。我们仅仅通过 Spring 自带的 @Transactional
注解即可指定数据源,对比之前简化不少。
由于篇幅原因,文章中没有展示具体的执行结果。完整代码已打包成 dynamic-data-source-demo
项目,并上传到 Github,项目中提供完整的单元测试,详情大家可以 Clone 到本地自己执行一遍。
dynamic-data-source-demo
项目基于 Spring Boot,集成了 MyBatis、通用 Mapper、PageHelper、Druid、Copiers,可以作为简单的脚手架使用,欢迎大家 Star 或者 Fork 到自己的仓库。
如果有问题,可以在 Github 上提 Issue,或者 QQ 交流,以下是联系方式:
我的 Github 地址:https://github.com/drtrang
项目 Github 地址:https://github.com/drtrang/dynamic-data-source-demo
BeanCopier 工具:https://github.com/drtrang/Copiers
QQ:349096849