本文源代码位置在 https://gitee.com/zhangchao19890805/csdnBlog.git 仓库中的 blog134 文件夹就是项目文件夹。
在上一篇文章 【133】Spring Boot 1 + MyBatis 多数据源分布式事务(一)
中我简单介绍了 Spring Boot1 + MyBatis 多数据源分布式事务的方案。但是上回提到的方案还是有瑕疵的。文末我也做了说明:
假如系统执行完 t_user 表的数据库提交操作后,Tomcat 突然宕机,会造成 t_card 数据库没提交,导致两台机器的数据不一致。也就是 t_user 表已经有记录了,而 t_card 表却没有对应的记录。这个问题该怎么解决呢?我会在下一篇文章中讲到。
那么我们该怎么处理这个问题呢?
整体的思路是通过独立的一个线程来做校验。
第一步,在 t_user 表上加个新的列,叫 c_create_status 用户的创建状态。 t_user表结构如下:
CREATE TABLE `t_user` (
`c_id` varchar(70) CHARACTER SET utf8 NOT NULL,
`c_user_name` varchar(45) CHARACTER SET utf8 NOT NULL,
`c_password` varchar(45) CHARACTER SET utf8 NOT NULL,
`c_create_time` datetime NOT NULL,
`c_balance` decimal(9,2) NOT NULL DEFAULT '0.00',
`c_create_status` int(1) NOT NULL DEFAULT '0' COMMENT '0:创建中 1:创建完成',
PRIMARY KEY (`c_id`),
UNIQUE KEY `c_user_name_UNIQUE` (`c_user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
第二步:在保存用户的时候,第一次向 t_user 表中插入数据的时候,c_create_status 设置为创建中。
第三步:在保存事务执行完之后。新开启数据库回话,校验成功后把 c_create_status设置成创建完成。
代码如下:
UserController
@RequestMapping(value="/userAndCard", method=RequestMethod.POST)
public R save(@RequestBody UserAndCardDTO userAndCardDTO){
// 处理用户
User user = new User();
BeanUtils.copyProperties(userAndCardDTO, user);
String userId = UUID.randomUUID().toString();
user.setId(userId);
user.setCreateTime(new Timestamp(System.currentTimeMillis()));
user.setCreateStatus(UserCreateStatus.CREATING.getValue());
// 处理卡
Card card = new Card();
BeanUtils.copyProperties(userAndCardDTO, card);
card.setId(UUID.randomUUID().toString());
card.setCreateTime(new Timestamp(System.currentTimeMillis()));
card.setUserId(user.getId());
this.userService.save(user, card);
// 检查一致性
boolean flag = this.userService.doCheckSave(userId);
if (!flag) {
throw new RuntimeException("用户创建失败");
}
return R.ok();
}
第四步:利用SpringBoot 1 的计划任何,做一个独立的校验线程。这里要注意: @Scheduled(fixedDelay = 1000)
方法中的代码都是单线程执行的。并且 fixedDelay=1000 值方法中的代码执行完1000毫秒之后,在进行新一轮的执行。此处方法不用担心间隔时间太短造成对数据库并发访问的问题。
关键代码如下:
配置文件:
@Configuration
@EnableSwagger2
@EnableScheduling
public class QustMvcConfig extends WebMvcConfigurerAdapter {
// 此处代码省略...
}
ScheduledTaskService.java
package zhangchao.schedule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import zhangchao.domain.User;
import zhangchao.service.UserService;
/**
* 单线程任务
* @author zhangchao
*
*/
@Service
public class ScheduledTaskService {
@Autowired
private UserService userService;
/**
* fixedDelay = 1000表示当前方法执行完毕5000ms后,Spring scheduling会再次调用该方法
*/
@Scheduled(fixedDelay = 1000)
public void testFixDelay() {
User user = this.userService.selectUncheckUser();
if (null != user) {
this.userService.doCheckSave(user.getId());
}
}
}
UserService.java
/**
* 用户的服务类
* @author 张超
*
*/
@Service
public class UserService {
// 部分代码省略 ......
/**
* 检查指定的UserID的用户有没有正常创建。
* @param userId
* @return true表示校验成功,数据一致性良好。false表示数据一致性有问题,校验失败。
* 会把 t_user 和 t_card 中的数据都删除,作为回退操作。
*/
public boolean doCheckSave(String userId){
// 第一个数据库,放User表
SqlSession sqlSession_1 = FirstDBFactory.getInstance().openSession(true);
// 第二个数据库,放Card表
SqlSession sqlSession_2 = SecondDBFactory.getInstance().openSession(true);
boolean flag = false;
try {
User u = this.userDao.selectById(sqlSession_1, userId);
Card card = this.cardDao.selectByUserId(sqlSession_2, userId);
if (null != u && null != card) {
flag = true;
}
// this.userDao.update(sqlSession_1, user4Update); 只有一条语句
// 且是自动提交,没必要加回退。
// 另一个分支的两条删除语句,因为有校验,也没必要加事务。
if (flag) {
User user4Update = new User();
user4Update.setId(userId);
user4Update.setCreateStatus(UserCreateStatus.FINISH.getValue());
this.userDao.update(sqlSession_1, user4Update);
} else {
this.cardDao.deleteByUserId(sqlSession_2, userId);
this.userDao.delete(sqlSession_1, userId);
}
}finally {
sqlSession_1.close();
sqlSession_2.close();
}
return flag;
}
public User selectUncheckUser(){
// 第一个数据库,放User表
SqlSession sqlSession_1 = FirstDBFactory.getInstance().openSession(true);
try {
User user = this.userDao.selectUncheckUser(sqlSession_1);
return user;
} finally {
sqlSession_1.close();
}
}
}