一、本地消息表原理
1、本地消息表方案介紹
本地消息表的最終一致方案
采用BASE原理,保證事務最終一致
在一致性方面,允許一段時間內的不一致,但最終會一致。
在實際系統中,要根據具體情況,判斷是否采用。(有些場景對一致性要求較高,謹慎使用)
2、本地消息表的使用場景
基於本地消息表的方案中,將本事務外操作,記錄在消息表中
其他事務,提供操作接口
定時任務輪詢本地消息表,將未執行的消息發送給操作接口。
操作接口處理成功,返回成功標識,處理失敗,返回失敗標識。
定時任務接到標識,更新消息的狀態
定時任務按照一定的周期反復執行
對於屢次失敗的消息,可以設置最大失敗次數
超過最大失敗次數的消息,不進行接口調用
等待人工處理
例如使用支付寶的支付場景,系統生成訂單,支付寶系統支付成功后,調用我們系統提供的回調接口,回調接口更新訂單狀態為已支付。回調通知執行失敗,支付寶會過一段時間再次調用。
3、本地消息表架構圖

4、優缺點
優點: 避免了分布式事務,實現了最終一致性
缺點: 注意重試時的冪等性操作
二、本地消息表數據庫設計
整體工程復用前面的my-tcc-demo
1、兩台數據庫 134和129。user_134 創建支付消息表payment_msg, user_129數據庫創建訂單表t_order

2、使用MyBatis-generator 生成數據庫映射文件,生成后的結構如下圖所示

三、支付接口
1、創建支付服務PaymentService
@Service
public class PaymentService {
@Resource
private AccountAMapper accountAMapper;
@Resource
private PaymentMsgMapper paymentMsgMapper;
/**
* 支付接口
* @param userId 用戶Id
* @param orderId 訂單Id
* @param amount 支付金額
* @return 0: 成功; 1:用戶不存在 2:余額不足
*/
@Transactional(transactionManager = "tm134")
public int payment(int userId, int orderId, BigDecimal amount){
//支付操作
AccountA accountA = accountAMapper.selectByPrimaryKey(userId);
if(accountA == null){
return 1;
}
if(accountA.getBalance().compareTo(amount) < 0){
return 2;
}
accountA.setBalance(accountA.getBalance().subtract(amount));
accountAMapper.updateByPrimaryKey(accountA);
PaymentMsg paymentMsg = new PaymentMsg();
paymentMsg.setOrderId(orderId);
paymentMsg.setStatus(0); //未發送
paymentMsg.setFailCnt(0); //失敗次數
paymentMsg.setCreateTime(new Date());
paymentMsg.setCreateUser(userId);
paymentMsg.setUpdateTime(new Date());
paymentMsg.setUpdateUser(userId);
paymentMsgMapper.insertSelective(paymentMsg);
return 0;
}
}
2、創建Controller層
@RestController
public class PaymentController {
@Autowired
private PaymentService paymentService;
//localhost:8080/payment?userId=1&orderId=10010&amount=200
@RequestMapping("payment")
public String payment(int userId, int orderId, BigDecimal amount){
int result = paymentService.payment(userId, orderId,amount);
return "支付結果:" + result;
}
}
3、調用接口
localhost:8080/payment?userId=1&orderId=10010&amount=200

查看表。賬號表account_a 扣掉了200元, 支付消息表插入了一條支付記錄。

四、訂單操作接口
1、創建訂單服務
@Service
public class OrderService {
@Resource
OrderMapper orderMapper;
/**
* 訂單回調接口
* @param orderId
* @return 0:成功 1:訂單不存在
*/
public int handleOrder(int orderId){
Order order = orderMapper.selectByPrimaryKey(orderId);
if(order == null){
return 1;
}
order.setOrderStatus(1); //已支付
order.setUpdateTime(new Date());
order.setUpdateUser(0); //系統更新
orderMapper.updateByPrimaryKey(order);
return 0;
}
}
2、創建Controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
//localhost:8080/handlerOrder?orderId=10010
@RequestMapping("handlerOrder")
public String handlerOrder( int orderId){
try {
int result = orderService.handleOrder(orderId);
if(result == 0){
return "success";
}
return "fail";
}catch (Exception e){
return "fail";
}
}
}
調用方式: localhost:8080/handlerOrder?orderId=10010
五、定時任務
1、增加注解EnableScheduling
@SpringBootApplication
@EnableScheduling //表明項目中可以使用定時任務
public class MyTccDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MyTccDemoApplication.class, args);
}
}
2、創建服務OrderSchedule
@Service
public class OrderSchedule {
@Resource
private PaymentMsgMapper paymentMsgMapper;
//給訂單處理接口發送通知
@Scheduled(cron = "0/10 * * * * ?")
public void orderNotify() throws IOException {
List<PaymentMsg> list = paymentMsgMapper.selectUnSendMsgList();
if (list == null || list.size() == 0) {
return;
}
for (PaymentMsg paymentMsg : list) {
int orderId = paymentMsg.getOrderId();
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost("http://localhost:8080/handlerOrder");
NameValuePair orderIdPair = new BasicNameValuePair("orderId", orderId + "");
List<NameValuePair> nvlist = new ArrayList<>();
nvlist.add(orderIdPair);
HttpEntity httpEntity = new UrlEncodedFormEntity(nvlist);
httpPost.setEntity(httpEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
String s = EntityUtils.toString(response.getEntity());
if("success".equals(s)){
paymentMsg.setStatus(1); //發送成功
paymentMsg.setUpdateTime(new Date());
paymentMsg.setUpdateUser(0); //系統更新
paymentMsgMapper.updateByPrimaryKey(paymentMsg);
}else {
int failCnt = paymentMsg.getFailCnt();
failCnt ++;
paymentMsg.setFailCnt(failCnt);
if(failCnt > 5){
paymentMsg.setStatus(2); //超過5次,改成失敗
}
paymentMsg.setUpdateUser(0); //系統更新
paymentMsg.setUpdateTime(new Date());
paymentMsgMapper.updateByPrimaryKey(paymentMsg);
}
}
}
}
3、模擬
1) 將訂單表的狀態改成0: 未支付

2) 清空消息表

3) 將UserID為1的用戶金額改成1000

4) 調用支付接口
http://localhost:8080/payment?userId=1&orderId=10010&amount=200
支付成功后,用戶A的金額變成了800,並在支付消息表中生成了一條支付記錄。
定時任務查詢支付消息表,查找未支付的支付消息記錄,然后調用訂單操作接口。訂單操作接口調用后,將訂單狀態改成1:成功。訂單操作接口返回成功后,則將支付消息的狀態改成已支付。
5、處理失敗模擬
在handleOrder方法中拋出異常。

