Java架构直通车——多数据源下事务最终一致性解决方案


上文书说明了 强一致性解决方案,这里说下最终一致性:最终一致性是 基于BASE的,BASE允许有软状态的存在,允许一段时间内的不一致性,但最终是一致的。

下面介绍几种最终一致性的解决方案。

解决方案一:基于本地消息表

基于本地消息表实现的原理是:

  • 将本事务外的操作记录在消息表中。
    比如说电商网站中,要进行下订单并支付的操作。这里下订单和支付是两个事务,因为支付回跳到支付宝、微信去支付,并等待一个回调;而下订单是在自己的电商系统中完成操作。
  • 其他事务,提供操作接口。操作接口成功返回success,失败返回fail。
    这里的操作接口在上面的例子中就是回调接口,支付宝或者微信将支付的消息通过回调接口传到你的系统当中,你的系统再去更改订单的状态。如果更改成功返回success。
  • 定时任务轮询本地任务表,将未执行的消息发送给操作接口。
    这里相当于微信或者支付宝的一个操作,它如果没有收到你的ack(可能网络中断,或者系统内部错误),会继续轮询你的回调接口,只不过间隔时间会越来越长,超过retry次数进行人工处理。

在这里插入图片描述

这种方式有什么优点呢:避免了分布式事务,实现了最终一致性。因为它把事务给拆分了,并没有同时的去执行这两个事务。
缺点在于:重试的时候需要去注意幂等性的操作。

本地消息表Demo

仍然使用前文的数据库337和338,不过我们这里多了一个本地消息表,所以,不光要使用账户表account还要使用一个消息表,我们将消息表放入数据库338中,并且使用338的账户表account
创建消息表:payment_msg
在这里插入图片描述
在另一个数据库337中,我们创建一个订单表order
在这里插入图片描述
为了简化,这里就不加入其他属性了。

然后使用generate-mapper生成一系列必要的类和mapper,不做赘述。
下面来看支付接口PaymentService:(类比于支付宝支付接口)

@Service
public class PaymentService {
    @Autowired
    private Account338Mapper account338Mapper;
    @Autowired
    private PaymentMsgMapper paymentMsgMapper;

    /**
     * @return 0:成功,1:用户不存在,2:余额不足
     */
    @Transactional(transactionManager = "tm337")
    public int payment(int userId, int orderId, BigDecimal amount){
        //支付操作
        Account338 account338=account338Mapper.selectByPrimaryKey(userId);
        if (account338==null)
            return 1;
        if (account338.getAmount().compareTo(amount)<0)
            return 2;
        account338.setAmount(account338.getAmount().subtract(amount));
        account338Mapper.updateByPrimaryKey(account338);

        //消息表中存放记录
        PaymentMsg paymentMsg=new PaymentMsg();
        paymentMsg.setOrderId(orderId);
        paymentMsg.setStatus(0);//未发送
        paymentMsg.setFailureCount(0);//重试次数
        paymentMsgMapper.insertSelective(paymentMsg);

        return 0;
    }
}

由于两个mapper操作的是同一个数据源,所以只需要一个事务就能控制。

再来看订单接口:(类比于本系统下订单)

@Service
public class OrderService {
    @Autowired
    private 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);//已支付
        orderMapper.updateByPrimaryKey(order);
        return 0;

    }
}

由于我们这里只写了回调接口,没有写下订单接口,所以需要去数据库自己初始化一个订单数据:
在这里插入图片描述
为上面两个类创建controller,在此不做赘述。

现在写定时任务,由于需要回调handlerOrder,所以引入包:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.11</version>
</dependency>

写一个定时任务:

@Service
public class OrderScheduler {

    @Autowired
    private PaymentMsgMapper paymentMsgMapper;

    @Scheduled(cron = "0/10 * * * * ?")
    public void Ordernotify() throws IOException {
        PaymentMsgExample example=new PaymentMsgExample();
        example.createCriteria().andStatusEqualTo(0);//0:未发送
        List<PaymentMsg> paymentMsgs=paymentMsgMapper.selectByExample(example);
        if (paymentMsgs==null||paymentMsgs.size()==0) return;
        for(PaymentMsg msg:paymentMsgs){
            int orderId=msg.getOrderId();
            //发送回调请求
            CloseableHttpClient httpClient=HttpClientBuilder.create().build();
            HttpPost httpPost=new HttpPost("http://localhost:8080/handleOrder");
            NameValuePair pair=new BasicNameValuePair("orderId",orderId+"");
            List<NameValuePair> pairs=new ArrayList<>();
            pairs.add(pair);
            HttpEntity httpEntity=new UrlEncodedFormEntity(pairs);
            httpPost.setEntity(httpEntity);
            //处理返回值
            CloseableHttpResponse response=httpClient.execute(httpPost);
            String result= EntityUtils.toString(response.getEntity());
            if ("success".equals(result)){
                msg.setStatus(1);//发送成功
            }else{
                Integer failureCount = msg.getFailureCount();
                msg.setFailureCount(failureCount+1);
                if (failureCount>5){
                    //超过重试次数
                    msg.setStatus(2);//重试失败
                }
            }
            paymentMsgMapper.updateByPrimaryKey(msg);
        }

    }
}

运行结果就不做展示了。

解决方案二:基于MQ

该方法与基于本地消息表的原理和流程基本一致。不同点在于:

  1. 使用MQ来替代本地消息表,存储方案不一样,数据库压力下降。
  2. 定时任务改为消费者,更高效,更可靠。

在这里插入图片描述

MQ更适合公司内的系统,而不用公司之间使用本地消息表更合适。

因为之前写过rabbitmq的文章,可以参考RabbitMQ池化方案ThreadLocal实现RabbitMQ消息的批量发送,这里代码就不做赘述了。

发布了419 篇原创文章 · 获赞 327 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/104927777