zoukankan      html  css  js  c++  java
  • 分布式事务解决方案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方法中抛出异常。

  • 相关阅读:
    redis安装及简单命令
    struts2 第二天
    初学struts2-入门案列
    hibernate第二天
    hibernate入门
    同义词,索引,表分区
    表空间,序列
    orcale函数
    orcale错题分析
    orcale开篇
  • 原文地址:https://www.cnblogs.com/linlf03/p/14009332.html
Copyright © 2011-2022 走看看