教你舞动手指速写一个Seata-AT框架解决棘手的分布式事务问题

 相信大家对于事务问题都不陌生,在传统的单库环境下开发,咱们可依赖于MySQL所提供的事务机制,来确保单个事务内的一组操作,要么全部执行成功,要么全部执行失败。

例如一个下单业务中,假设由「扣减库存、新增订单」两个操作组成,在单库中通过MySQL提供的事务机制,能够确保该事务中任意操作执行出现问题时,另一个操作变更的数据可以回滚,从而确保整库数据的一致性,避免产生库存扣了,但订单却未增加的情况出现。

在传统的单体架构中做单库开发,数据的一致性可以通过InnoDB事务机制来保障,但当项目换到分布式架构的环境时,或者当项目换到分库分表的环境时,答案亦是如此吗?并非如此,在分布式环境下,由于每个库都维护着自己的事务机制,相互之间无法感知对方的事务,因此就会出现分布式事务问题,这也是分布式系统中头疼多年的一个棘手问题!

本章的核心则是讲清楚分布式事务问题,以及该如何去解决这种棘手问题,但实际目前对于分布式事务的解决方案已经十分成熟了,即Spring Cloud Alibaba中的Seata框架,以及更早期的GTS、LCN、 Atomikos、RocketMQ、Sharding-Sphere...等框架都能够很好的解决分布式事务问题。

也正由于分布式事务问题的解决方案已经比较完善,基本上一个注解、几行代码、几行配置的事情,就能够轻松解决分布式事务问题,因此本章并非单纯去讲述这些框架的基本使用,而是从另一个角度来思考分布式事务问题,即假设没有这些成熟的解决问题,咱们遇到这个问题时又该如何处理呢?接下来会与大家一起,手把手的自己编写一个分布式事务框架。

因为涉及到了分布式事务框架的手写,可能内容会比较偏向于底层原理的分享,我会尽量在把这些内容写的简单一点,同时对于每个核心段落也会画图示例,但本身这个题材就比较硬核,因此想要彻底读懂这章,最好具备如下基础:

  • 分布式知识储备:主要是指SpringCloud微服务与RPC远程调用的基本使用。
  • 网络知识储备:主要是指Netty框架的使用、序列化知识、P2P网络通信的原理。
  • Spring相关的知识储备:主要是Transactional事务机制原理、AOP切面使用。
  • Java-JUC并发编程的知识储备:主要是ThreadLocal、Condition、Lock、线程池技术。
  • 其他的知识储备:主要是指自定义注解式开发、Maven打包技术、MySQL事务原理。

如若大家不具备上述基础,实则也无需担心,通篇读下来应该大致原理也能够弄懂,本章要做的是让诸位知其然并知其所以然,对技术不要停留在单纯的使用层面,而应该适当性的去参悟底层的实现原理,这才是大家与其他开发者拉开差距的核心竞争力。

因为个人还并未阅读过Seata框架的源码,因此本章是之前在阅读LCN这个老牌分布式事务框架仿写的,所以很多实现是借鉴于LCN的部分实现,但LCNSeata-AT模式大致相同,因此诸位也可将本篇当做Seata-AT模式的原理篇来阅读,在本章末尾也会提供源码实现。

一、何谓分布式事务问题?

   首先来聊聊啥是分布式事务问题,因为现在的分布式/微服务系统开发中,基本上每个核心服务都会具备自己的独享库,也就是垂直分库的模式,以前面的例子来说,订单服务有订单DB,库存服务有库存DB,每个服务之间的数据库都是独立的。此时先回顾原本单库环境中解决事务问题的方式,如下:

// 下单服务
@Transactional
public void placeAnOrder(){
    // 调用扣减库存的方法
    inventoryService.minusInventory();
    // 调用增加订单的方法
    orderService.insertOrder();
}
复制代码

一个下单业务的伪代码如上,会先调用「扣减库存」的方法,接着再调用「新增订单」的方法,为了确保下单这组操作的数据一致性,通常会在方法上加一个@Transactional注解,这样就会将事务托管给Spring来负责,如下:

  • 在该方法执行时,Spring会首先向数据库发送一条begin开启事务的命令。
  • 如果执行过程中出现异常,Spring会向数据库发送一条rollback回滚事务的命令。
  • 如果执行一切正常,Spring会向数据库发送一条commit提交事务的命令。

Spring注解式事务的逻辑图如下:


这种事务管理机制,在单体架构中显然十分好用,但放到分布式环境中,情况则不同,如下:

由于分布式系统都会根据业务去拆分子系统/子服务,因此不同业务之间只能通过RPC的方式,远程调用对方所提供的API接口,假设这里在库存服务本地的「扣减库存」方法上加一个@Transactional注解,同时在订单服务本地的「新增订单」方法也加一个@Transactional注解,Spring内部的处理逻辑如下:

  • 下单业务远程调用「减库存」接口时,Spring会先向库存DB发送一个begin命令开启事务。
  • 当扣减库存的业务执行完成后,Spring会直接向库存DB发送一个commit命令提交事务。
  • 下单业务调用本地的「新增订单」方法时,Spring又会向订单DB发送begin命令开启事务。
  • 当新增订单执行出现异常时,Spring会向订单DB发送一个rollback命令回滚事务。

此时分析如上场景,下单业务理论上应该属于同一个事务,但之前《MySQL事务篇》聊到过,InnoDB的事务机制是基于Undo-log日志实现的,那么减库存产生的回滚记录会记录到库存DBUndo-log中,而新增订单产生的回滚记录则会记录到订单DBUndo-log中,此时由于服务不同、库不同,因此相互之间无法感知到对方的事务。

当后续「新增订单」的操作执行出现异常,Spring框架发送的rollback命令,就只能根据订单DB中的回滚记录去还原数据,此时前面扣减过的库存数据就无法回滚,因此导致了整体数据出现了不一致性。

1.1、分布式事务问题演示

   前面简单讲述了分布式事务问题,但这样讲起来似乎有些令人费脑,那接下来直接上个案例,实际感受一下分布式事务造成的数据不一致问题,这里基于SpringCloud快速搭建了一个微服务项目,为了节省篇幅就不带着诸位一起走简单的搭建流程了,完整的源码地址会在最后给出,其中有订单、库存两个子服务,库存服务提供了一个减库存的接口,如下:

@RestController
@RequestMapping("/inventory")
public class InventoryAPI {
    
    // 注入本地的InventoryService
    @Autowired
    private InventoryService inventoryService;
    
    @RequestMapping("/minusInventory")
    public String minusInventory(Inventory inventory) {
        // 根据传入的商品ID先查询库存
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        // 如果库存不足则返回相应提示
        if (inventoryResult.getShopCount() <= 0) {
            return "库存不足,请联系卖家....";
        }
        
        // 如果商品还有剩余库存则对库存减一,接着修改数据库中的库存数量
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
        System.out.println("库存信息:" + inventoryResult.toString());
        
        // 扣减库存成功后,向客户端返回对应的提示
        if (n > 0) {
            return "端口:" + port + ",库存扣减成功!!!";
        }
        return "端口:" + port + ",库存扣减失败!!!";
    }
}

// 库存服务本地的InventoryService实现类
@Service
public class InventoryServiceImpl implements InventoryService {

    // 减库存会调用的修改方法,在上面添加了@Transactional注解
    @Override
    @Transactional
    public Integer updateByPrimaryKeySelective(Inventory record) {
        int i = inventoryMapper.updateByPrimaryKeySelective(record);
        return i;
    }
}
复制代码

而订单服务中提供了一个下单接口,如下:

@RestController
@RequestMapping("/order")
public class OrderAPI {
    
    // 注入本地的OrderService
    @Autowired
    private OrderService orderService;
    
    // 库存服务的远程调用地址
    private static final String URL_PREFIX =
        "http://localhost:8002/inventory/minusInventory";
    
    // 负责远程调用的RestTemplate
    @Autowired
    private RestTemplate restTemplate;
    
    // 下单接口
    @RequestMapping("/placeAnOrder")
    public String placeAnOrder(){
        // 随便指定一个商品的ID
        String inventoryId = "92b1162a-eb7a-4d72-9645-dea3fe03c8e2";
        // 然后通过HttpClient调用库存服务的减库存接口
        String result = HttpClient.get(URL_PREFIX +
                "/minusInventory?inventoryId=" + inventoryId);
        System.out.println("\n调用减库存接口后的响应结果:" + result + "\n");

        // 调用减库存接口成功后,向订单库中插入一笔订单记录
        String orderId = UUID.randomUUID().toString();
        Order order = new Order(orderId,"黄金竹子","8888.88",inventoryId);
        Integer n = orderService.insertSelective(order);
        System.out.println("\n\n\n" + n + "\n\n\n");

        return "下单调用成功,需要处理事物.....";
    }
}

// 订单服务本地的OrderService实现类
@Service
public class OrderServiceImpl implements OrderService {
    
    // 新增订单会调用的插入方法
    @Override
    @Transactional
    public Integer insertSelective(Order record) {
        // 刻意制造出一个异常
        int i = 100 / 0;
        return orderMapper.insertSelective(record);;
    }
}
复制代码

要注意看,在orderService.insertSelective(order)插入订单数据的方法中,我们通过100/0手动制造了一个异常,以此来模拟出「扣减库存」执行成功、「新增订单」执行失败的场景,接着看看库存DB、订单DB中对应的库存表、订单表数据,如下:


很明显,目前订单表中还没有任何数据,而库存表中仅有一条测试数据,但要注意:这两张表分别位于db_inventory、db_order两个不同的库中,此时「黄金竹子」的库存数量为100,现在分别启动库存服务、订单服务来做简单模拟:

  • 订单服务的下单接口:http://localhost:8001/order/placeAnOrder

这里就直接用浏览器做测试,浏览器调用下单接口后,控制台的日志如下:


两个服务对应的数据库中的数据如下:

结果十分明显,此时对应商品的库存扣掉了,但由于新增订单时出现异常,所以订单却并未增加,最终造成了数据不一致问题,这也就是前面所说到的分布式事务问题,这也是分布式系统中,需要解决的一个棘手问题。

1.2、该如何解决分布式事务问题呢?

   早年间分布式架构并不像如今这么主流,一般只有一些互联网大厂才会使用,因此相关的技术生态和解决方案,并不像那么成熟,而分布式事务问题,也成为了使用分布式架构不得不解决的棘手问题,在分布式事务问题被发现后,期间推出了各种各样的解决方案,但如今保留下来的主流方案共有四种:

  • ①基于Best Efforts 1PC模式解决分布式事务问题。
  • ②基于XA协议的2PC、3PC模式做全局事务控制。
  • ③基于TTC方案做事务补偿。
  • ④基于MQ实现事务的最终一致性。

但上述四种仅是方法论,也就是一些虚无缥缈的理论,想要使用时还得根据其概念去自己落地,但如今分布式/微服务生态已经十分成熟,所以也有很多现成的落地技术,早已能够解决分布式事务问题,如Seata、GTS、LCN、 Atomikos、RocketMQ、Sharding-Sphere...等框架,都提供了完善的分布式事务支持,目前较为主流的是引入Seata框架解决,其内部提供了AT、TCC、XA、Seaga-XA四种模式,主推的是AT模式,使用起来也较为简单,大体步骤如下:

  • ①引入相关的Maven依赖。
  • ②修改相关的配置文件。
  • ③在需要保障分布式事务的方法上加一个@GlobalTransactional注解。

经过上述三步后,就能够轻松解决早期困扰大厂多年的分布式事务问题,是不是尤为轻松?其他的分布式事务框架使用步骤也相差无几,引入依赖、修改配置、加一个注解即可。

二、手写分布式事务框架的思路分析

   前面对分布式事务问题的描述讲了一大堆,但真正要解决对应问题时,似乎仅靠一个注解就够了,这这这......,到底是怎么解决的呢?相信诸多使用Seata或其他分布式事务框架的小伙伴,心中难免都会产生这个疑惑。OK,咱们先假设现在没有这些成熟的分布式事务框架,如果自己要解决分布式事务问题,又该如何去实现呢?那么接下来就重点说说这块,真正让大家做到知其然并知其所以然!

不过做任何事情得先有规划,没有提前做好准备与计划,任何事一般都不会有太好的结果,因此先分析一下手写分布式事务框架的思路,把思路捋清楚之后,接着再去编写对应的代码实现。

前面讲的分布式事务问题,本质原因在于Spring是依靠数据库自身的事务机制来保障数据一致性的,而两个服务对应着两个库,两个库各自都维护着自己的事务机制,同时无法感知对方的事务状况,最终造成库存服务先提交了事务,而订单服务后回滚事务的情况出现。

所以想要自己解决分布式事务问题,首先就不能依靠

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值