分布式事務解決方案3--本地消息表(事務最終一致方案)


一、本地消息表原理

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方法中拋出異常。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM