现在有个项目,要做数据迁移,要把A库中的 数据迁移到B库,以后新的功能都在B库上开发,两个库都是mysql的。但是很多旧的的项目 还在要使用A库的数据,所以需要一个过渡期,在写B库的同时 也要保证能写到A库。为了保证两个数据库的数据完整性和一致性,只能同时操作两个库,并保证操作的原子性。
我们要保证多数据源 操作的原子性,就要使用分布式事物。幸好两个数据源都是同类型 mysql 库。不同类型的如 mysql 和 redis 之前同步要复杂点,这个后续给出解决方法。
使用spring 事物管理机制解决分布式事物问题。可以参考博文:http://www.open-open.com/lib/view/open1429863503010.html#articleHeader8。解决方案主要包括两大类:
(1)XA方式
(2)非XA方式
(3)用消息队列消除分布式事务
使用XA方式效率较低,使用消息队列消除分布式式事务又太过复杂。基于效率 和时间成本考虑,我选用spring的链式事务管理器 非XA方式 ChainedTransactionManager。它是最大努力一阶段提交模式中,一个粗糙的事务管理器实现仅仅是将一系列其他的事务管理器链接在一起,去实现事务同步。倘若业务处理成功,所有的事务将会提交, 否则它们都能回滚。最大努力一次提交模式的安全性不如XA事务但也是相当不错,因此能够承受风险获得较高的吞吐量收益。如果我们将关键业务处理服务设计为一个幕等式 (idempotent),这样发生错误的可能性也很小。
(1)数据源配置文件:
spring.datasource.primary.url=jdbc:mysql://localhost:3306/primary
spring.datasource.primary.username=root
spring.datasource.primary.password=
spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.secondary.url=jdbc:mysql://localhost:3306/secondary
spring.datasource.secondary.username=root
spring.datasource.secondary.password=
spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver
(2)spring boot加载数据源
以下配置了两个数据源,但是没有配置事务管理。
@Configuration
public class DataSourceConfig {
@Bean(name = "primaryDataSource")
@Qualifier("primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@Qualifier("secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dataSource")
@Qualifier("dataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "primaryJdbcTemplate")
public JdbcTemplate primaryJdbcTemplate(
@Qualifier("primaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean(name = "secondaryJdbcTemplate")
public JdbcTemplate secondaryJdbcTemplate(
@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
(3)测试用例
我没有使用单元测试,因为单元测试里面如果加上了@Transactional 会自动回滚事务,需要在单元测试上面加上 @Rollback(false),但是这样就失去了测试的意义,我们就是要测试事务的原子性,不能手动设置事务的回滚方式。所以单独写了一个 Rest 接口用于调试如下:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
PrimaryUserService primaryUserService;
@Autowired
SecondaryUserService secondaryUserService;
@Autowired
@Qualifier("primaryJdbcTemplate")
JdbcTemplate primaryJdbcTemplate;
@Autowired
@Qualifier("secondaryJdbcTemplate")
JdbcTemplate secondaryJdbcTemplate;
@RequestMapping(value = "add/{name}", method = RequestMethod.POST)
@ResponseBody
@Transactional
public String addUser(@PathVariable String name) {
int count = secondaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
Assert.isTrue(count == 0);//会抛异常
primaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
//用业务层封装下
//int count = primaryUserService.create(name);
//Assert.isTrue(count == 0);//会抛异常
//secondaryUserService.create(name);
return name;
}
}
但是不使用ChainedTransactionManager
(1)正常情况没有问题 ,都能写入数据。
@RequestMapping(value = "add/{name}", method = RequestMethod.POST)
@ResponseBody
@Transactional
public String addUser(@PathVariable String name) {
int count = secondaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
primaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
return name;
}
(2)如果出现异常,数据不能保持一致了。如下所示。虽然把他们用spring的事务注解放到了一起,但是当secondaryJdbcTemplate执行完并报错后,primaryJdbcTemplate就无法正常添加数据,但是secondaryJdbcTemplate却并不受影响。
@RequestMapping(value = "add/{name}", method = RequestMethod.POST)
@ResponseBody
@Transactional
public String addUser(@PathVariable String name) {
int count = secondaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
Assert.isTrue(count == 0);//会抛异常
primaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
return name;
}
备注:事实上上面两个操作根本不受 @Transactional 注解的影响。因为单元测试里面如果加上了@Transactional 会自动回滚事务,需要在单元测试上面加上 @Rollback(false),但是上面的操作完全没有要回滚的意识。
首先在spring boot 配置中添加如下内容:
@Bean
@Primary
public ChainedTransactionManager transactionManager(@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
DataSourceTransactionManager primaryTransactionManager = new DataSourceTransactionManager(primaryDataSource);
DataSourceTransactionManager secondaryTransactionManager = new DataSourceTransactionManager(
secondaryDataSource);
ChainedTransactionManager chainedTransactionManager = new ChainedTransactionManager(primaryTransactionManager,
secondaryTransactionManager);
return chainedTransactionManager;
}
(2) 如果spring业务层出现异常,数据还能保持一致
@RequestMapping(value = "add/{name}", method = RequestMethod.POST)
@ResponseBody
@Transactional
public String addUser(@PathVariable String name) {
int count = secondaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
Assert.isTrue(count == 0);//会抛异常
primaryJdbcTemplate.update("insert into USER(NAME) values(?)", name);
//用业务层封装下
//int count = primaryUserService.create(name);
//Assert.isTrue(count == 0);//会抛异常
//secondaryUserService.create(name);
return name;
}
ChainedTransactionManager只是同时开启两个事务,自动的对两个事务,同时开启,同时提交(或回滚)。它对多个数据写数据,并不是一个原子操作,不能像XA那样保证对数据操作的完整性,一致性。
看源代码:它用一个列表来管理多个数据源的事务管理器。
用MultiTransactionStatus封装多个数据源的事务,统一管理。
逐个Commit 事务的时候,一旦发现某个事务不能commit,立刻回滚后面的事务。
待续。。。。。
待续。。。。。