本文源代碼位置在 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();
}
}
}