分布式事务解决方案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方法中抛出异常。

原文地址:https://www.cnblogs.com/linlf03/p/14009332.html