一、本地消息表原理
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方法中拋出異常。