分布式事务解决方案

1.分布式事务

事务的参与者,支持事务的服务器,资源服务器分别位于不同的分布式系统的不同节点之上,且属于不同的应用。分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上说,分布式事务就是保证不同数据库的一致性。

最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到多个对数据库资源的访问。
image

当一个服务需要调用另外一个服务时,这时的事务就需要跨越多个服务了,在这种情况下,起始于某个服务的事务在调用另外一个服务时,需要以某种机制流转到另外一个服务,从而使被调用的服务的访问的资源也自动加入到该事务中来。
image

若将以上两种场景结合起来,对此延伸,整个分布式事务的参与者会组成一个树形拓扑结构,服务A有自己的调用资源,也调用了服务B和服务C,服务C调用了自己的资源,服务B调用了服务D和服务E......
image

2.分布式事务相关理论

CAP定理
image
分布式系统有三个指标:

  • Consistenct:一致性

  • Availability:可用性

  • Partition tolerance:分区容错

这三个指标不可能同时做到,这个结论叫做CAP定理。

Partition tolerance:分区容错 :
大多数分布式系统都分布在多个子网络,每个子网络叫做一个区。分区容错的意思就是,区间通信可能失败,可以理解为网络故障造成的问题。
image

上图中,G1和G2是两台跨区的服务器,G1向G2发送一条数据,G2可能无法收到,系统设计时必须考虑到这种情况。

一般来说,分区容错无法避免,因此可以认为CAP的P总是成立的,CAP定理告诉我们,剩下的C和A无法同时做到。

Availability:可用性 :

可用性意思是,只要收到用户的请求,服务器就必须做出回应。

用户可以向G1和G2发起读操作,不管是哪台服务器,只要收到请求,就必须告诉用户到底是v0还是V1,否则就不满足可用性了。
image

Consistenct:一致性

意思是写之后的读操作,必须返回该值。

如某条记录是V0,用户向G1发起一个写操作,将其改为V1,而用户有可能向G2发起读操作,G2的值没有变化,因此返回的的是V0,G1和G2读操作的结果不一样,这就不满足一致性了。
image

为了能让G2也能变为V1,就要在G1写操作时,让G1向G2发送一条消息,要求G2也改为V1。
image

一致性和可用性的矛盾 :

一般来说,分区容错无法避免,因此可以认为CAP的P总是成立的。

一致性和可用性为什么不可能同时成立?因为可能通信失败(即出现分区容错)。

如果保证G2的一致性,那么G1必须在写操作时,锁定住G2的读操作和写操作,只有数据同步后,才能重新开放读写。锁定期间,G2没有可用性。

如果保证G2的的可用性,那么势必不能锁住G2,所以一致性不能成立。

综上,G2无法同时做到一致性可可用性,系统设计时只能选择一个目标,如果追求一致性,那么无法保证所有节点的可用性,如果追求所有节点的可用性,那么无法保证所有节点的一致性。

Base理论

Base:全称:Basically Available(基本可用),Soft stat(软状态),和Eventually consistent(最终一致性)三个短语的缩写,是对CAP中一致性和可用性权衡的结果,是基于CAP定理演化而来的,其核心思想是:

即使无法做到强一致性(strong consistency),但每个应用都可以根据自身业务的特点,采用适当的方式来使系统达到最终一致性

Basically Available(基本可用):
假设系统出现了不可预知的故障,但还是能用,相比较于正常的系统而言:

1.相应时间上的损失,如正常搜索引擎0.5秒相应,而基本可用的搜索引擎1秒作用返回结果。

2.功能上的损失,如在一个电商网站上,正常情况下用户可以完成每一笔订单,但是在大促销期间,为了维护系统的稳定,部分消费者可能会被引导到一个降级页面。

Soft stat(软状态):
相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”

软状态是指允许系统中的数据存在中间状态,及允许系统在多个节点的数据副本存在数据延迟。

Eventually consistent(最终一致性)
系统能够保证在没有其他更新操作的情况下,数据最终能达到一致的状态。

3.分布式事务解决方案

1.基于XA协议的两阶段提交
首先看下分布式事务处理的XA规范:
image

XA规范中分布式事务有AP,RM,和TM组成;

应用程序(AP):定义事务边界(事务开始和结束)并访问事务边界内的资源。

资源管理器(RM):管理计算机的共享资源,资源包括如数据库,文件系统等。

事务管理器(TM):负责管理全局事务,分配事务的唯一标识,监控事务的执行进度,并负责事务的提交,回滚,失败恢复等。

两阶段提交:
第一阶段,第一阶段预提交,AP将分布式事务提交到资源管理器(RM),事务中每个对数据库的操作都有一个对应的资源管理器来负责,如果每个事务都成功了,就会通知全局的事务管理器,第二阶段提交再去真正执行每一个提交动作。若其中一个事务执行失败,每个资源管理器也会告诉事务管理器,事务管理器在通知每一个资源管理器进行事务的回滚。

优点:尽量保证了数据的强一致性,适合对数据强一致性要求很高的领域。

缺点:实现复杂,牺牲了可用性,对性能影响较大。

2.TCC补偿机制
其核心思想是,针对每个操作,都要注册一个与其对应的确认和补偿(撤销操作)。它分为三个阶段:

  • Try阶段主要对业务系统做检测和资源预留。

  • Confirm阶段主要对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的,即:只要Try成功,Confirm一定成功。

  • Canale阶段主要在业务执行错误,需要回滚的时候执行业务的取消,预留资源释放。
    image

image

3.消息最终一致性

是业界使用最多的,核心思想是将分布式事务拆分成本地事务进行处理。

image

基本实现思路是:

消息生产方,需要额外见一个消息表,记录消息的发送状态,消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里,然后消息会进过MQ发送到消费方。

消息消费方,需要处理这个消息,完成自己的业务逻辑,如果本地事务处理成功,表明已经处理成功了,如果失败,会重试执行,如果是业务上的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚操作。

生产方和消费方定时扫描本地的消息表,把还没有处理完的消息或者失败的消息再发送一遍,如果有靠谱的自动补偿逻辑。

优点:一种非常经典的实现方式,避免了分布式事务,实现了最终一致性。

缺点:消息表耦合到业务系统中,会有很多杂活要处理。

4.例:库存扣减分布式事务的实现

如果在创建订单是发生了异常,此时库存已经扣减了,这样库存就比实际的要少,造成数据的不一致性,需要采用最终消息一致性的分布式事务解决方案。

1.实现思路:

1.当订单服务发生异常时,发送消息给MQ,消息内容为购物车数据,用于恢复库存

2.商品服务从MQ中提取消息,保存到回滚表中(消息表,中间状态)。

3.在管理后台开启定时任务,定时扫描库存回滚表执行库存回滚

image

库存回滚表tb_stock_back的设计:
image

联合主键的设置是因为消息在发送时,可能因为失败而重复发送,而重复发送可能造成回滚数据的重复,订单编号+sku编号组合起来一定是同一笔数据。

代码实现:

1.将订单服务(消息生产者)的需要加入事务的代码用try cache包裹,在异常处理部分发送消息到MQ,本地事务照常回滚;

try {
            //保存订单主表
           ......
		   
            //保存订单明细表
			......

        } catch (Exception e) {
            e.printStackTrace();
            //发送回滚消息
            rabbitTemplate.convertAndSend("","queue.skuback", JSON.toJSONString(orderItemList));
            throw new RuntimeException("创建订单失败");

        }

2.生成库存回滚记录

商品服务(消息消费者)从消息队列中取出购物车列表json,转为列表后查询到库存回滚表。

1.库存回滚表添加记录的逻辑代码

@Service(interfaceClass = StockBackService.class)
public class StockBackServiceImpl implements StockBackService {

    @Autowired
    private StockBackMapper stockBackMapper;

    @Transactional
    public void addList(List<OrderItem> orderItemList) {
        for (OrderItem orderItem : orderItemList) {
            StockBack stockBack = new StockBack();
            stockBack.setOrderId(orderItem.getOrderId());
            stockBack.setSkuId(orderItem.getSkuId());
            stockBack.setStatus("0");
            stockBack.setNum(orderItem.getNum());
            stockBack.setCreateTime(new Date());
            stockBackMapper.insert(stockBack);
        }
    }
}

2.创建MQconsumer的配置文件,创建监听类监听MQ去调用库存回滚表的逻辑代码,在库存回滚表里产生记录,等待定时任务去定时扫描回滚记录表中的数据,执行真正的回滚操作。

public class BackMessageConsumer implements MessageListener {

    @Autowired
    private StockBackService stockBackService;

    public void onMessage(Message message) {
        try {
            //提取消息
            String jsonString = new String(message.getBody());
            List<OrderItem> orderItemList = JSON.parseArray(jsonString, OrderItem.class);
            stockBackService.addList(orderItemList);
        } catch (Exception e) {
            e.printStackTrace();
            //记录日志,之后人工干预修改
        }
    }
}

3.定时执行库存回滚

编写定时任务,间隔一小时执行库存回滚,查询库存回滚记录表中状态为0的记录,执行回滚逻辑。

1.在StockBackServiceImpl中新增方法doBack(),查询回滚记录表中数据为0的记录,去执行回滚逻辑

@Service(interfaceClass = StockBackService.class)
public class StockBackServiceImpl implements StockBackService {

    @Autowired
    private StockBackMapper stockBackMapper;

    @Transactional
    public void addList(List<OrderItem> orderItemList) {
        for (OrderItem orderItem : orderItemList) {
            StockBack stockBack = new StockBack();
            stockBack.setOrderId(orderItem.getOrderId());
            stockBack.setSkuId(orderItem.getSkuId());
            stockBack.setStatus("0");
            stockBack.setNum(orderItem.getNum());
            stockBack.setCreateTime(new Date());
            stockBackMapper.insert(stockBack);
        }
    }

    @Autowired
    private SkuMapper skuMapper;

    @Transactional
    public void doBack() {
	System.out.println("库存回滚开始");
        //查询库存回滚表中状态为0的记录
        StockBack stockBack0 = new StockBack();
        stockBack0.setStatus("0");

        List<StockBack> stockBackList = stockBackMapper.select(stockBack0);
        for (StockBack stockBack : stockBackList) {
            //添加库存
            skuMapper.deductionStock(stockBack.getSkuId(),-stockBack.getNum());
            //减少销量
            skuMapper.addSaleNum(stockBack.getSkuId(),-stockBack.getNum());
            stockBack.setStatus("1");
            stockBackMapper.updateByPrimaryKey(stockBack);
        }
	System.out.println("库存回滚结束");
    }
}

2.定时任务

@Component
public class SkuTask {

    @Reference
    private StockBackService stockBackService;

    /**
     * 间隔一小时执行库存回滚
     */
    @Scheduled(cron = "0 0 0/1 * * ?")
    public void skuStockBack(){
        stockBackService.doBack();
    }

}

测试

posted @ 2021-08-18 11:46  Chcode  阅读(94)  评论(0)    收藏  举报