spring事务与分布式事务

spring事务

理论

事务四大特性
A 原子性,事务内写操作要么同时成功,要么同时失败
C 一致性 事务中多次读取同一行数据(本事务未修改),结果应是相同的
I 隔离性 分四种级别,不可重复读(事务间相当于没有隔离),读已提交,可重复读(防不了幻读),串行化
D 持久性,事务提交后,所更新内容保证不会丢失
如果数据库完整支持这四大特性,那么数据库就完整支持的事务

其中A,D我们一般不用关心,因为我们使用的数据库都默认支持,即使不同模式的分布式事务对也做到对A,D的支持

重点看隔离性
mysql在InnoDb下默认采用RR(可重复读)的隔离级别 即本事务内对同一行数据的多次读取肯定是相同的
postgres 默认采用RC(读已提交)的隔离级别即本事务内可读到其他事务提交的更改

spring事务的实现

在这里插入图片描述

在使用事务时,往往不用关心上面这个过程,只需加个注解就搞定,那spring是如何实现的以上步骤

  1. 根据注解判断指定TransactionManager(实际上就是数据源)
  2. 事务切面中需要判断需不需要开启新事务

2.1 如果当前不存在已经绑定到当前线程的事务,则开始事务
2.1.1 从数据库连接池中取出一条连接 (连接池的连接时有限的,一条连接同时只能有一个事务使用)
2.1.2 设置事务的隔离级别 不指定,为数据库默认的隔离级别,pg就是读已提交
2.1.3 发送执行开启事务指令 set autocommit=false;
2.1.4 设置事务超时时间 (为什么事务要有超时时间?)
2.1.5 讲新建的事务绑定到ThreadLocal中即当前线程可见

2.2 如果当前线程已经存在事务,则根据指定的事务传播判断开启新事务还是沿用已经存在的事务
2.3 假设事务传播就是默认的_PROPAGATION_REQUIRED (意思就是,进入当前方法时,当前线程如果已经绑定事务,就继续使用本事务,如果没有,则新建事务绑定到线程) 一个事务的线程的范围一定在一个线程上,开启新线程当前事务不能传递到新线程_

  1. 执行事务方法,在try catch中执行事务方法,事务切面中catch的时Throwable 也就是说,任何类型的异常都会被捕捉到。
  2. 如果未捕捉到异常,则提交事务。如果捕捉到异常则回滚事务。

spring事务源码分析过程

分布式事务理论

为什么需要分布式事务

如果全部业务都在一台服务器上处理运行,那数据库为我们提供的事务完全够用了,但是现在基本都是微服务,不同系统间的调用已经非常频繁。
为什么spring事务处理不了系统间的调用
例子下订单操作。 创建订单前前要扣库存

业务系统

//@GlobalTransactional     
public void doBusiness(){
     //扣库存
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        ObjectResponse storageResponse = storageDubboService.decreaseStorage(commodityDTO);
        //2、创建订单
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        ObjectResponse<OrderDTO> response = orderDubboService.createOrder(orderDTO);
     }

//库存系统

  public ObjectResponse decreaseStorage(CommodityDTO commodityDTO) {
      ···内容省略···
         扣减数据库中的库存,并更新数据库
      
  }

//订单系统

    public ObjectResponse<OrderDTO> createOrder(OrderDTO orderDTO) {
          ···内容省略···
          创建订单并入库 
        }

其中库存,和订单分别属于不同的服务器上面

假设扣减库存和创建订单方法上都加了spring的事务,系统间采用dubbo调用

情况一:库存系统扣减失败,并抛出异常
库存系统的spring事务会回滚本方法内的操作,同时dubbo会把异常抛到上游调用方,即业务系统方法也会抛出异常。如果业务系统有数据库操作,也会回滚,这种情况没有任何问题

情况二:订单系统扣减失败,并抛出异常
库存系统的spring事务会回滚本方法内的操作,同时dubbo会把异常抛到上游调用方,即业务系统方法也会抛出异常。如果业务系统有数据库操作,也会回滚,可是库存系统在这之前已经执行完毕已经没办法回滚了,就造成了数据不一致的问题。

我们发起流程要加分布式事务的目的就是防止,业务系统最终失败回滚了,但是流程中心的流程却发起了。

分布式事务解决方案

  1. 两阶段提交(2PC, Two-phase Commit)方案(这个也是我们使用的,主要讲这个)

需要保证调用所有的模块 有一个异常回滚,则全局回滚,所有服务均提交,则全局事务提交

  1. eBay 事件队列方案

    eBay 的架构师Dan Pritchett,曾在一篇解释BASE 原理的论文《Base:An Acid Alternative》中提到一个eBay 分布式系统一致性问题的解决方案。它的核心思想是将需要分布式处理的任务通过消息或者日志的方式来异步执行,消息或日志可以存到本地文件、数据库或消息队列,再通过业务规则进行失败重试,它要求各服务的接口是幂等的。
    现实中可使用 mq的事务消息实现,即保证,事务提交后,消息才发送出去

  2. TCC 补偿模式

    某业务模型如图,由服务 A、服务B、服务C、服务D 共同组成的一个微服务架构系统。服务A 需要依次调用服务B、服务C 和服务D 共同完成一个操作。当服务A 调用服务D 失败时,若要保证整个系统数据的一致性,就要对服务B 和服务C 的invoke 操作进行回滚,执行反向的revert 操作。回滚成功后,整个微服务系统是数据一致的。

在这里插入图片描述

优点是 性能高于二段提交
缺点是业务入侵 回滚逻辑需要程序员手动编写

两段提交实现 Seata AT模式(业务无入侵)

现在假设,扣减库存,和创建订单方法上都有spring的事务注解,是有数据库事务的方法
在这里插入图片描述

  1. 开启全局事务,@GlobalTransactional注解所在方法进入切面,切面中向TC发送一个开启全局事务的请求,TC,TC服务器上创建一个事务会话并保存起来,生成一个全局事务标识的XID。并返回给TM,TM收到这个全局事务标识,会绑定到当前线程中。

  2. 调用扣库存服务,seata可以通过dubbo将全局事务XID透传到库存服务,库存服务检测到XID不为空,也会把XID绑定到当前线程上

    2.1 扣库存方法在执行update语句时,会记录undo_log(用于回滚),记录修改行,两种数据,beforeImage(修改前)和afterImage 修改后的数据,并且会记录修改行数据的主键用于全局锁,即事务未提交前,其他事务无法再修改改行。
    2.2 扣库存方法提交本地事务(spring的事务)时,会先注册分支事务

  3. 库存服务开启分支事务,向TC发送一个注册分支事务的请求,TC收到请求后,会断此分支事务需要加的全局锁,是否已经存在,如果已经存在,说名锁冲突,此事务提交失败,整个全局事务失败回滚,如果此前不存在初建锁,则将锁记录在TC服务器,创建分支事务,将分支事务放到全局事务下面返回 分支事务标识ID,branchId。库存服务收到branchId会记录下来,并和之前记录的undo_log一起刷到数据库中存到undo_log表。

在这里插入图片描述

  1. 提交库存事务的本地事务,提交分支事务(库存服务完成)。

  2. 创建订单,请求,同样通过dubbo透传XID

  3. 订单服务。和库存服务同样的操作,订单服务也会注册分支事务也会记录undoLog和加全局锁,

  4. 和库存服务同样的操作,最终提交分支事务

  5. TC,全局事务提交/回滚

       8.1全局提交:如果发起事务的方法正常执行完毕,则提交全局事务,全局事务完成
       8.2全局回滚:情况1 整个分布式事务发生异常,全局回滚
                     情况2 事务超时,全局事务开启后,过了超时时间仍未提交,全局回滚
       8.3 回滚过程
             1.TC 触发全局回滚,向所有注册分支事务的RM发送回滚请求,请求内容为XID,和branchId
              例如,库存服务在收到回滚请求,根据XID和branchId从undo_log表中查询出回滚记录,进行回滚
              2. 回滚时需要判断 当前表中的数据,和undoLog记录的afterImage(修改后记录)是否相等,如果不                   相等,说明,数据在事务期间被更改了(全局锁只能防止分布式事务更新相等行,其他的方式依然时可                  以修改数据的),如果强行回滚,会丢失数据,因此此时回滚失败。报出错误
             3.如果回滚操作超时,TC服务器会不断重试回滚。
    

注意点:

  1. 二段提交,分布式事务的达到了那种隔离级别

    将整个全局事务综合起来看,比如库存分支事务已经提交完毕(spring事务也提交了),但是全局事务仍未提交,那么此时由于数据库的默认隔离级别,为读已提交,别的全局事务在执行时,也能读到库存分支事务提交的内容。因此seata的AT模式,隔离级别为读未提交
    如果应用一定要达到Read Committed级别,可以使用SELECT FOR UPDATE 语句。对于这种语句,Seata会通过SELECT FOR UPDATE 持有数据的行锁,直到全局锁是已提交的,才返回(现实中基本不可能这么做)。

  2. 全局锁何时释放

LockConflictException occur!
如果全局锁冲突会报这个异常,说明此时要修改的数据行,正在被其他全局事务占据。
全局锁只会在,全局事务提交,回滚成功后释放。

  1. 二段提交的优点与缺陷

优点明显就是业务无入侵,只需要一个注解
缺点 隔离级别太低,多事务一起执行需要考虑的问题更多,效率低,比普通事务多了一倍的数据库写操作(undo_log),还要多次与TC服务器交互的网络IO。整体效率比普通事务慢很多。

seata AT模式源码分析

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值