文
章
目
录
章
目
录
背景
在我的日常开发中,遇到了一个问题:我的应用需要接入多个数据源,但在服务层需要保证事务的一致性。我在服务层的方法上使用了 @Transactional 注解,但实际执行时并没有切换数据源,导致问题出现。从而在此复盘下该问题!
场景复现
这里为了简便,直接使用开源组件 dynamic-datasource-spring-boot-starter 实现多数据源切换,大家也可以自己动手实现。
1)创建 SpringBoot 工程;
2)引入 dynamic-datasource 依赖:
dependency>
groupId>com.baomidougroupId>
artifactId>dynamic-datasource-spring-boot-starterartifactId>
version>${version}version>
dependency>
3)按照 dynamic-datasource 规范添加数据源配置;
spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:h2:mem:master
driver-class-name: org.h2.Driver
init:
schema: classpath:master-schema-h2.sql
data: classpath:master-data-h2.sql
slave_1:
url: jdbc:h2:mem:slave_1
driver-class-name: org.h2.Driver
init:
schema: classpath:slave_1-schema-h2.sql
data: classpath:slave_1-data-h2.sql
strict: false
primary: master
4)添加 DDL 及数据:
-- master-schema-h2.sql
DROP TABLE IF EXISTS master_user;
CREATE TABLE master_user
(
id BIGINT NOT NULL COMMENT '主键ID',
user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
-- master-data-h2.sql
DELETE FROM master_user;
INSERT INTO master_user (id, user_name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
-- slave_1-schema-h2.sql
DROP TABLE IF EXISTS slave_user;
CREATE TABLE slave_user
(
id BIGINT NOT NULL COMMENT '主键ID',
user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
-- slave_1-data-h2.sql
DELETE FROM slave_user;
INSERT INTO slave_user (id, user_name, age, email) VALUES
(6, 'Jone', 18, 'test1@baomidou.com'),
(7, 'Jack', 20, 'test2@baomidou.com'),
(8, 'Tom', 28, 'test3@baomidou.com'),
(9, 'Sandy', 21, 'test4@baomidou.com'),
(10, 'Billie', 24, 'test5@baomidou.com');
4)创建数据对象及 Mapper,这里只列出了 Master 数据源数据对象及 Mapper;
package com.itschenxiang.multidatasource.entity;
import lombok.Data;
@Data
public class MasterUser {
private Long id;
private String userName;
private Integer age;
private String email;
}
package com.itschenxiang.multidatasource.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itschenxiang.multidatasource.entity.MasterUser;
import org.springframework.stereotype.Repository;
@Repository
public interface MasterUserMapper extends BaseMapper {
}
5)新增服务层构建场景;
@Service
public class MultiDataSourceService {
@Autowired
private MasterUserMapper masterUserMapper;
@Autowired
private SlaveUserMapper slaveUserMapper;
@Autowired
@Lazy
private MultiDataSourceService multiDataSourceService;
// 单master数据源
@DS("master")
public ListaccessPrimaryDataSource() {
return masterUserMapper.selectList(null);
}
// 单slave_1数据源
@DS("slave_1")
public ListaccessNotPrimaryDataSource() {
return slaveUserMapper.selectList(null);
}
// 多数据源,无@Transactional注解
public void multiDataSourceWithoutTransactional() {
multiDataSourceService.accessPrimaryDataSource();
multiDataSourceService.accessNotPrimaryDataSource();
}
// 多数据源,有@Transactional注解,mapper执行出错
@Transactional(rollbackFor = Exception.class)
public void multiDataSourceWithTransactional() {
accessPrimaryDataSource();
accessNotPrimaryDataSource();
}
}
6)添加单元测试复现问题;这里仅列举了 @Transactional 导致异常的 UT;
@SpringBootTest
@ActiveProfiles("ut")
@RunWith(SpringRunner.class)
public class MultiDataSourceServiceTest {
@Autowired
private MultiDataSourceService multiDataSourceService;
@Test
public void multiDataSourceWithTransactionalTest() {
try {
multiDataSourceService.multiDataSourceWithTransactional();
} catch (Exception e) {
e.printStackTrace();
Assert.assertTrue(e instanceof BadSqlGrammarException);
}
}
}
根因分析
经过分析,我发现在 Spring 开启事务后会维护一个 ConnectionHolder,保证整个事务都使用同一个数据库连接。这意味着使用了 @Transactional 注解后,Spring 会保证整个事务都使用同一个 connection。
然而需要注意的是,单库的事务仍然是可用的,只要事务下不切换数据源即可。
解决方案
针对确实需要单事务多数据源的场景,有以下解决方案:
- 删除事务注解:如果在业务场景中不需要事务的一致性,可以考虑直接删除 @Transactional 注解。
- 使用 Seata 事务:Seata 是一种分布式事务解决方案,可以解决多数据源下的事务一致性问题。
其实对于大部分合理的业务场景,应用可能会涉及多个数据源,但基本上都是单数据源事务。我在实际遇到的问题也是在单数据源事务中,数据源切换失败的问题。根本原因是自定义数据源切换切面的执行顺序在 @Transactional 之后,导致无法切换数据源。