【134】Spring Boot 1 + MyBatis 多数据源分布式事务(二)

本文源代码位置在 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();
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章