谈谈分布式事务的设计与实现

事务的具体定义

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。

简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。

数据库本地事务

ACID

说到数据库事务就不得不说,数据库事务中的四大特性 ACID:

A:原子性(Atomicity),一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。

事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

就像你买东西要么交钱收货一起都执行,要么发不出货,就退钱。

C:一致性(Consistency),事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation),指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。

由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

打个比方,你买东西这个事情,是不影响其他人的。

D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须***保存下来。

即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。

InnoDB 实现原理

InnoDB 是 MySQL 的一个存储引擎,大部分人对MySQL都比较熟悉,这里简单介绍一下数据库事务实现的一些基本原理。

在本地事务中,服务和资源在事务的包裹下可以看做是一体的,如下图:

我们的本地事务由资源管理器进行管理:

而事务的 ACID 是通过 InnoDB 日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过 Redo Log(重做日志)来实现,原子性和一致性通过 Undo Log 来实现。

Undo Log 的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为 Undo Log)。然后进行数据的修改。

如果出现了错误或者用户执行了 Rollback 语句,系统可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。

和 Undo Log 相反,Redo Log 记录的是新数据的备份。在事务提交前,只要将 Redo Log 持久化即可,不需要将数据持久化。

当系统崩溃时,虽然数据没有持久化,但是 Redo Log 已经持久化。系统可以根据 Redo Log 的内容,将所有数据恢复到***的状态。对具体实现过程有兴趣的同学可以去自行搜索扩展。

事务的隔离级别

这里扩展一下,对事务的隔离性做一个详细的解释。

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实能够完全保证数据的安全性,但在实际业务系统中,这种方式性能不高。因此,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。

事务并发执行会出现的问题
我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:

更新丢失
当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。
当数据库没有加任何锁操作的情况下会发生。

脏读
一个事务读到另一个尚未提交的事务中的数据。
该数据可能会被回滚从而失效。
如果第一个事务拿着失效的数据去处理那就发生错误了。

不可重复读
不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:

虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。

幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。

数据库的四种隔离级别

数据库一共有如下四种隔离级别:

Read uncommitted 读未提交

在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。
因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。

Read committed 读提交

在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。

Repeatable read 重复读

在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。

Serializable 序列化

该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

什么是分布式事务?

到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现支持跨数据库的事务支持,这也就是大家常说的“分布式事务”。

这里举一个分布式事务的典型例子——用户下单过程。

当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:

  1. 用户通过商品系统浏览商品,他看中了某一项商品,便点击下单

  2. 此时订单系统会生成一条订单

  3. 订单创建成功后,支付系统提供支付功能

  4. 当支付完成后,由积分系统为该用户增加积分

上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。

image
image

分布式分类

刚性分布式事务

强一致性

XA模型

CAP  满足CP
image

2PC

关于2pc想要详细学习得可以看这篇文章:
https://www.jianshu.com/p/948d843b0489

image

发起请求如下:


image

柔性分布式事务

    最终一致性
    CAP、BASE理论
        AP

柔性分布式事务是对XA协议得一种妥协,它通过降低数据库资源锁定时间提高可用性,主要满足CP,基于BASE思想,满足最终一致性。

典型架构实现:

    TCC模型
    Saga模型

TCC模型

  • Try-Confirm-Cancel

    对应两阶段提交里面得prepare,Confirm对应commit,Cancel对应协商提交的rallback。
    每个子业务都需要实现Try-Confirm-Cancel接口----业务侵入性大。

      资源锁定交由业务方。
    

    Try

      尝试执行业务,完成所有业务检查,预留必要的业务资源。
    

    Confirm

      真正执行业务,不再做业务检查
    

    Cancel
    是否try阶段预留的业务资源,回滚

    汇款服务、收款服务案例
    A用户向B用户汇款500块钱。

image

汇款服务中,confirm不做任何操作。
如果操作失败了,那么需要加回500块钱,从日志中扣减资源。在业务中实现。
收款服务


image

如果收款服务失败了,cancel,那么不做任何操作。

Saga模型

image

分布式事务对比

image

如何解决??

思路:

  1. 解决该问题本身。
  2. 让问题本身消失。(首选)

柔性分布式事务

通用处理思路

    本地事务-> 短事务
    分布式事务-> 长事务
    转变成多个短事务
    案例
        * A[下单]-> B[减库存]->C[支付]
            * A -> DB1
            * B -> DB2
            * C -> DB3
            * A / B / C 都成功
            * A/B 成功,c失败
                * 补偿 

业务场景

异步场景
    基于MQ消息驱动分布式事务
image
方案一 业务方提供本地操作成功回查功能

MQ发送方即业务方


image

image

image

优点

    通用

缺点

    业务方需要提供回查接口,对业务侵入性大
    发送消息非幂等,如果第一次发送半消息完了,又发送了一次,这时候MQ就需要处理多条。所以说发送的消息是非幂等的。
    
    消费端需要处理幂等,需要处理幂等的问题,多条相同消息的问题。
方案二 本地事务消息表
image

如果第四步失败了,这时候相当于超时了,微服务会再执行第二步,第三步的步骤。

image
同步场景
    基于异步补偿分布(也就是业务层驱动的场景)
image

由业务逻辑层来驱动。

解决方案:

  1. 基于异步补偿的分布式事务

     下单,减库存。支付,如果三个都成功了。不处理。如果操作c失败了,会自动回滚,需要去补偿A,补偿B。
     
     两种补偿办法:
     1.先告诉App失败,再补偿A、B。
     2.先补偿A、B,再去告诉App。
     
     根据fastfail机制,一旦失败,则马上返回app,再去补偿,因为已经失败了。所以此时补偿是异步的。
    
  2. 架构设计的三大设计点


    image
     请求调用链: 调用 A、B、C操作的链。
     基于补偿机制: 在业务层来提供补偿接口。
     需要做幂等,比如库存操作等。
    

总体架构

image
  1. 使用分布式事务补偿机制,那么业务层需要引入补偿服务的jar包。
  2. 建立一个事务的id,事务的状态(1.未执行 2.成功 3.失败 4.补偿成功),产生时间。。。
  3. proxy 需要将A的调用参数记在自己本地TDB,相应的还有B和C,然后开始分别调用A、B、C的事务。
  4. 假设此时A、B、C事务都成功了,那么此时执行的时候,需要把事务的执行状态改变,此时事务补偿机制不需要做什么事,但是需要把原来存储的记录删除。
  5. 假设A、B成功了,C失败了,此时需要把c的事务操作状态设置成失败,然后事务补偿机制就需要开始运作了,一般会是一个定时任务,几毫秒去查询一次事务中的状态,如果为3,表示执行失败,需要执行相应的补偿机制。补偿A、补偿B,把c事务中的状态补偿,改成4。补偿一般是在数据访问层提供,调用相应的补偿接口去补偿。
事务逻辑层的设计

如下是业务逻辑层的设计方式:


image

几个注意点:

txid保存在threadlocal里面,是为了保证多线程中每个线程有唯一的txid,会在进程间共享。

通过rpcProxy,在invoke中记录请求参数。

反向补偿:c失败后,会先补偿B,再补偿A。

数据库访问层的设计
image
  1. 业务方提供原子接口
  2. 幂等性根据订单状态做一些幂等性操作,比如加锁等。
分布式事务补偿服务的设计
image

actionid表示操作步骤
callmethod 表示补偿方法名

案例

成功


image
image

失败案例

image
image

分布式事务本身补偿

大思路

  1. 记录错误日志
  2. 报警
  3. 人工介入

你可能感兴趣的:(谈谈分布式事务的设计与实现)