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