天天看点

关于事务(Transaction)那些事儿?

目录

    • 引入
    • 操作事务
    • 事务的开启和结束
    • 隔离性
          • 读未提交:read uncommitted
          • 读已提交:read committed
          • 可重复读:repeatable read
          • 串行化:serializable
    • 事务的传播属性
    • 小结
    • 分布式事务
          • 分布式事务是什么?
            • CAP
            • BASE
          • 分布式事务解决方案
            • 2PC(Two-phase Commit)
            • 3PC(Three-phase commit)
            • TCC(Try-Confirm-Cancel)补偿事务
            • 本地消息表
            • MQ事务
    • 总结

武汉加油!!!

事务

, 一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务, 那么一个

完整的业务

又由什么构成的呢?

一个完整的业务需要一条或者多条

(insert, update, delete)

共同联合完成, 我们把

(insert, update, delete)

这种语句又称为

DML

.

看完定义, 稍作补充:

一个例子

: 最经典的例子便是银行的

转账业务

(张三给李四转1000元)

  • 张三账户减去1000元
  • 李四账户增加1000元

可以看到上面的一个转账业务实际发生了两件事, 我们就把这两件事合起来称作一个事务.此例子事务中只包含了两件事, 但是并不代表所有事物都包含两件事, 这个是根据具体业务区分的

基于上面的例子我们可以辅助理解事物的定义, 也就是知道了事务是什么, 那下面我们还是基于上面的例子, 进一步认识一下事务

在上面的例子中, 事务中包含了一个账户的减少, 一个账户的增加两个动作, 要想转账成功那么两个动作就必须成功, 一旦有一个动作失败了, 整个事务就只能是失败的, 这便是事务的

第一个特性

. 其次在转账完成之后, 不论是账户是增加了还是减少了, 但是两个账户的总数是不变的这边是事务的

第二个特性

, 再然后一旦转账业务完成了. 对数据库的修改是确定的, 这便是

第三个特性

, 前面的三个特性都是针对

一个事务本身

的, 另外, 在不同的事务之间, 避免互相影响还有

最后一个特性

叫隔离性

总结一下,事务的四大特性ACID:

  • 原子性

    : 一个事务中的所有操作,要不 操作全部成功,要不全部失败.
  • 一致性

    : 事务必须使得数据库从一个一致性状态转变到另一个一致性状态(总数不变).
  • 持久性

    : 对数据库的修改是持久性的,这些是数据库数据存放到硬盘中,不会丢失的.
  • 隔离性

    : 是指多个用户同时请求数据库,开启多个事务同时处理某个数据库,隔离性保证了各个事务之间均不受干扰,每个事务都感觉不到其他事务的存在.

开启事务:

Start Transaction

结束:

End Transaction

提交事务:

Commit Transaction

回滚事务:

Rollback Transaction

commit:

提交

rollback:

回滚

我们说到了一个事务中包含有一条或者多条

DML

语句, 那么关于一件事务的开启的标志是什么呢?

开始:

一个事务中任何一条DML语句的执行, 标志着这项事务开始了

结束: 有开始就有结束, 关于事务的结束有两种情况来区分(

事务成功还是失败

)

成功: 成功的结束, 将所有的DML语句操作历史记录和底层硬盘数据来一次同步

失败回滚: 将所有的DML语句操作历史记录全部清空, 不会修改数据库数据

备注: 在MySQL中,默认情况下,事务是自动提交的,也就是说,只要执行一条DML语句就开启了事物,并且提交了事务

我们在一开始的时候说到了事务的四大特性(原子性、一致性、持久性、

隔离性

)前三个在理解的时候比较容易, 内容也稍微少一些, 关于

隔离性

的内容, 相信有人看完会觉得还是不够清晰. 下面重点介绍一下

隔离性的存在, 其实主要是为了保证数据在使用的时候是正确的. 不会出现问题, 那么在说隔离性之前我们先看看不同事物之间可能存在的问题

  • 1.

    丢失

    • A和B两个事物同时修改一份数据, 不过A的事务发生在B事务发生之前, A修改的数据被B修改了, 导致A修改的并没有生效
  • 2.

    脏读

    • B事务修改了一个数据并未提交,A事物读取了这个数据,然后B事务回滚了,最后A又读取了一次,两次读取的数据不一致
  • 3.

    不可重复读

    • A事务读取了一个数据后,B事务修改了这个数据,A事务又读取了这个数据,两次读取的数据也不一致
  • 4.

    幻读

    • 范围是

      整个数据表

      的, 事务在插入已经检查过不存在的记录时,惊奇的发现这些数据已经存在了

看到这关于不可重复读和幻读, 又不太容易理解了.我们多说一点

幻读范围在

一张表

里面, 例如, 事务A查询student表id为1的用户是否存在, 如果不存在就插入一条id为1的数据.
  1. 首先, 事务A查询是否存在, 查询完之后发现不存在id为1的用户
  2. 与此同时, 事务B往student表中插入了一条id为1的数据.
  3. 现在事务A查询完(

    事务A认为id为1的不存在

    ), 那么边执行insert操作, 插入id为1的用户
  4. 结果, 事务A插入失败, 因为主键冲突(因为事务B已经插入了)

    这一现象便为幻读, 也就是我们说的事务在插入已经检查过不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测获取到的数据如同鬼影一般。

不可重复读范围在

单条数据

, 也就是在一次事务中包含两次读取操作, 第一次读取和第二次读取结果不一样.
  1. 事务A开始读取student表中学生A年龄为20
  2. 与此同时事务B修改了学生A年龄为21
  3. 事务A第二次读取学生A的年龄发现变为21

    这一现象便为不可重复读

上面我们说到了事务的一些可能发生的问题,首先肯定的一点是, 不是所有事务都存在上述的4个问题, 在一定的几率下才会出现. 那么如何去避免上面的4个问题呢, 这便是

隔离性

中的重要内容, 设置

隔离级别

,

隔离级别

, 便是采取一定的隔离方式, 将事务A和事务B隔离开, 避免相互影响. 隔离级别包含:
  • 1 读未提交:read uncommitted
  • 2 读已提交:read committed
  • 3 可重复读:repeatable read
  • 4 串行化:serializable
两个事务, 事务A

未提交

的数据(事务A还在执行中), 事务B可以读取到(事务B读取到的便是

脏读

), 这种隔离级别是最低的. 实际生产中数据库的隔离级别都要高于此级别

提交

的数据(事务A执行完成), 事务B才能读取到, 该级别可以避免

脏读

, 但是该级别有

不可重复读的问题

提交

的数据(事务A执行完成), 事务B读取不到, 级别高于前两者, 也就是说B事务

可以重复读

, 因为不受事务A的干扰, 所以事务B每次读到的数据都是一致的.
两个事务, 事务A和事务B, 事务A在操作数据库的时候, 事务B只能排队等待. 这是一种最高级别的隔离, 可以解决幻读问题, 但是这种级别却

很少用

, 因为它的吞吐量太低了, 响应太慢. 因为我们说事务之间的冲突并不是每一次操作都会出现的, 因为我们多个事务操作同一条数据几率比较少(

相对而言

). 一旦设置这个级别所有事务只能排队执行, 可想有多慢

上面我们说到了四种隔离级别, 自上而下级别逐步提升, 级别越高越严谨.

另外在于不同的数据库中默认隔离级别有不同

mysql默认级别: 可重复读:repeatable read

oracle默认级别: 读已提交:read committed

如果发现默认级别满足不了业务的时候, 也可以手动配置,

隔离级别的等级

以及

作用范围

可以通过修改配置文件, 或者动态方式修改(

推荐此方式

示例mysql动态修改

//设置隔离级别
   SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL <isolation-level>
        其中的<isolation-level>可以是:
    –   READ UNCOMMITTED
    –   READ COMMITTED
    –   REPEATABLE READ
    –   SERIALIZABLE
    例: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

  //设置作用范围
  –   事务隔离级别的作用范围分为两种:   
  –   全局级:对所有的会话有效 
  –   会话级:只对当前的会话有效 
  –  例如,设置会话级隔离级别为READ COMMITTED :
mysql> SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
或:
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
  –  设置全局级隔离级别为READ COMMITTED : 
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
           

到这, 我们关于事务的基础认识以及各个特性的介绍就结束了.

上面说到了很多内容其实大部分都是基于数据库的层面解释的, 那么作为一个程序开发者, 在写代码的时候又该怎么去处理事务呢(关于事务基础操作我们就不说了,大家都会是吧。重点看下如何处理事务传播)?

事务传播: 当我们在写代码的时候, 在一个开启了事务的方法中调用另一个方法, 被调用的方法是否要同样开启事务, 还是不开启呢, 关于这个问题我们的spring给出了答案, 规定了事务传播有7种配置方式
  • PROPAGATION_REQUIRED(需要)

    表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。
  • PROPAGATION_SUPPORTS (支持)

    表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行,如果不存在事务就不在事务中执行。
  • PROPAGATION_MANDATORY (强制必须)

    表示该当前方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
  • PROPAGATION_REQUIRED_NEW(要求新事物)

    表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
  • PROPAGATION_NOT_SUPPORTED(不支持新事物)

    表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
  • PROPAGATION_NEVER (从不)

    表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
  • PROPAGATION_NESTED(嵌套)

    表示如果当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与

    PROPAGATION_REQUIRED(需要)

    一样.

通过上述我们介绍了事务的基础知识, 以及四大特性是什么, 我们在小结部分在提及一下事务的实现方式

  • 原子性和一致性通过Undo log来实现
  • 事务的隔离性是通过数据库锁的机制实现的
  • 持久性通过redo log(重做日志)来实现

对于单机事务是通过将操作限制在一个会话内通过数据库本身的锁以及日志来实现ACID,但是在面对并发的时候我们为了提高效率通常对数据采用分库分表的模式, 那在这种模式下又如何来保证我们的ACID特性呢?

随着互联网快速发展, 单点的系统以及单点的数据库已经满足不了需求的时候, 微服务的架构模式诞生了.

一个例子

将支付宝的钱转入余额宝, 支付宝和余额宝分别为两套系统, 并且有各自的数据库, 那么要想实现 支付宝减去100元, 余额宝增加100元的操作就不能通过数据库的锁机制实现了, 因为他们数据放在不同的数据库里面.

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

稍作解释, 拿我们上面的例子, 来说

支付宝--->>余额宝

一件事的操作分别落在两个系统以及两个数据库中, 那么我们的分布式事务就是为了在不同的系统之间还能够保证我们的

类似ACID特性

, 从而推动我们的业务正常进行.

上面说到了一个词

类似ACID

因为数据分散在不同的数据库中, 那么我们传统的

ACID

不足以支撑我们的业务了, 在这先辈们又提出了新理论

CAP

CAP

首先需要明确一点

CAP

理论并不是为了

分布式事务

专门设定的, 他的设计初衷是为了

分布式系统(分布式系统不仅仅有分布式事务)

CAP又称为 布鲁尔定理

C(一致性)

: 对于在分布式的不同节点来说, 如果在某一个节点更新了数据, 那么在其他节点都能都读取到这个数据发生了改变, 那么就成为

强一致性

, 如果有某个节点没有读取到, 那么分布式就是不一致的

A (可用性)

:非故障的节点在合理的时间(

应该在合理的时间给出返回

)内返回合理的响应(

系统应该明确返回结果并且结果是正确的

)。可用性的两个关键一个是合理的时间,一个是合理的响应。

P (分区容错性)

:当出现网络分区后,系统能够继续工作。如果集群有多台机器,有台机器网络出现了问题,但是这个集群整体依旧可以工作.

上述是对于CAP的定义解释, 不过这里又有一个问题CAP是一个理论体系, 但是在实际过程中,我们的分布式系统往往不能够满足

C

A

P

同时存在, 因为这三点在某些情况下互斥(例如P会必然存在, 因为网络无法做到100%不出问题), 所以大家就开始两两组合, 比如

CA

CP

AP

  • CA: 当我们选择CA组合的时候, 这套分布式系统追求的就是

    一致性

    可用性

    . 那么一旦这个系统中出现了P的情况也就是某个节点宕机了. 此时此刻为了满足

    C(一致性)

    我们就应该拒绝某节点请求, 保证一致性, 但是

    A(可用性)

    又要求分布式中节点合理的时间给出合理的响应, 他不允许拒绝请求, 所以分布式系统不能选择

    CA

    架构
  • CP

    : 放弃

    A(可用性)

    追求

    强一致性和分区容错性

    , 这种模式是不是比较熟悉. 但是又想不起来到底是谁这么干的对吧, 给你个小提示, 想想

    ZooKeeper

    是个什么东西.
  • AP

    C(一致性)

    高可用和分区容错性

我们说的两两组合, 并不是说彻底放弃某一个, 而是在我们的分布式系统中最大可能保证我们所选择的两者.以上便是CAP理论.

BASE

说完 CAP理论我们再来看一种

BASE

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写,是对 CAP 中 AP 的一个扩展。

基本可用

:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。

软状态

: 系统执行的中间状态, 这个状态下可能出现节点间数据的不一致

最终一致

: 最终一致是系统在经过软状态一段时间之后, 所有节点的数据总能达到一致

BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态

有了上面的两套理论知识之后(其实关键是理解在分布式中一致性, 可用性等等概念), 我们就能开始阐述分布式事务的解决方案了

2PC又称作二阶段提交, 具体实现可先看下图
关于事务(Transaction)那些事儿?

事务协调器

首先会将

prepare消息

写入日志中,然后向AB数据库发出prepare消息,操作完成并不直接只提交, 而是返回

yes

或者

no

如果事务协调器接受到的消息都为

yes

那么全部提交. 如果有任意一个不是

yes

那那么全部进行回滚操作.
  • 优点

    : 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。
  • 缺点:
    • 单点问题: 由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(

      如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题

    • 同步阻塞: 执行过程中间,节点都处于阻塞状态。即节点之间在等待对方的相应消息时,它将什么也做不了
    • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本

与两阶段提交不同的是,三阶段提交有两个改动点。

  • 1、引入超时机制。同时在协调者和参与者中都引入超时机制。
  • 2、在第一阶段和第二阶段中插入一个准备阶段,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段
关于事务(Transaction)那些事儿?

实现:

  • CanCommit

    : 协调者向分布式事务参与者发送询问, 是否可以执行事务操作, 并且等待参与者返回

    Yes

    No

  • PreCommit

    : 事务预执行阶段, 如果

    CanCommit

    参与者都返回

    Yes

    , 那么协调者发送预执行命令给参与者, 并等待响应确认

    Ack

  • DoCommit

    : 真正事务提交阶段, 如果前两个阶段都通过了, 那么协调者发送提交指令给参与者, 并且等待反馈, 如果成功则提交成功, 如果失败则提交失败.

    特别注意: 在第三阶段中, 如果参与者因为网络或者某些原因, 半天没有返回

    yes

    no

    的响应, 当

    响应超时

    的时候, 本次事务也会提交(在这里是一种

    猜测或者概率

    , 在3PC模式中协调者认为, 已经通过了 第一第二阶段, 虽然第三阶段没有及时返回反馈, 但是他认为很大程度是执行成功了), 所以本次事务也会提交. 但是, 在第一第二阶段当

    响应超时

    的时候, 会中断事务.

优缺点:

  • 优点

    : 弥补了2PC的不足,主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态
  • 缺点

    : 因为在第三阶段响应超时的时候默认提交, 容易造成数据不一致的问题. 比如在别的参与者取消了提交, 但某个参与者却因为响应时间超时默认提交了.

TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

TCC 事务机制相比于上面介绍的 2PC,解决了如下几个缺点:

  • 解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 数据一致性,有了补偿机制之后,由业务活动管理器控制一致性。

定义不好理解?来, 看图

关于事务(Transaction)那些事儿?

图上的执行如下:

  • Try 阶段

    :尝试执行,完成所有业务检查(确保一致性),预留必需业务资源(准隔离性)。
  • Confirm 阶段

    :确认真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
  • Cancel 阶段

    :取消执行,释放 Try 阶段预留的业务资源,Cancel 操作满足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。

什么?看完说明书还不明白?

一个例子

你去KFC买香辣鸡腿堡

  • try

    : 你需要确认你微信的钱够不够

    35元

    (顺便问一下大家香辣鸡腿堡到底多少钱?没吃过).如果发现你的钱够, 那你立刻把这

    35元

    锁住. 与此同时, 售货员需要确认

    香辣鸡腿堡是否有货

    ,如果有货, 好立马锁住这个鸡腿堡不让别人拿走.
  • confirm

    : 如果try阶段, 你钱够, 鸡腿堡有, 则你开始扣钱, 售货员减少一个鸡腿堡的数量
  • cancel

    : 如果在try阶段, 你发现钱不够, 或者售货员说鸡腿堡卖完了, 只要有任何一个条件不满足, 则事务退出, 并且释放try阶段所有锁住的资源(

    在这指35元钱和香辣鸡腿堡

那么TCC的优点和缺点分别是什么呢:

  • 优点

    :强隔离性,严格一致性要,执行时间较短的业务
  • 缺点

    : TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码来实现.
本地消息表这个方案最初是 eBay 提出的, 此方案的

核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试

人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

这种方式是一种很经典, 实现起来容易的方式, 也是采纳较多的一种方案

看图

关于事务(Transaction)那些事儿?
  • 消息生产方

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

    : 需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了, 这修改中间表事务状态,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息修改中间表状态,通知生产方进行回滚等操作。
  • 优点

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

    : 因为消息生产者需要建立一个中间表, 消息表会耦合到业务系统中,如果没有封装好的解决方案,也有不小的工作量。
基于MQ实现的事务, 可以看做是对

本地消息表

方案的一种扩展, 将本地消息表的作用迁移到了MQ内部.

常见的ActiveMQ, RabbitMQ, RocketMQ都有各自的实现方案

下面看一个经典的案例

关于事务(Transaction)那些事儿?
  • 服务A

    处理任务前, 先发送任务消息到

    MQ

  • MQ

    接收到通知之后, 持久化到本地, 但是不投递给消费者, 也就是是此刻

    服务B

    完全不知道

    服务A已经对那个任务要动手了

  • MQ

    持久化完成, 返回

    服务A

    一个应答
  • 服务A

    收到确认应答之后, 开始执行任务,

    服务A

    执行任务之后, 通知

    MQ

    投递消息, 此刻

    服务A

    就可以去干别的事了, 因为他的任务执行完了
  • 服务B

    接收到到

    MQ

    的消息, 才知道

    服务A

    都已经对那个任务动手, 且活都干完啦.

    服务B

    就赶紧执行自己的任务
  • 服务B

    执行完任务了, 给

    MQ

    发一个应答, 告知该分布式事务完成了.
  • 如果

    服务B

    执行失败了, 则通知

    服务A

    回滚.
  • 优点

    : 相比本地表方式, 省去了强耦合的中间表维护工作, 并且系统之间的耦合度很低.
  • 缺点

    : 这种方式变通性很多, 虽然实现起来不复杂, 但是设计阶段会繁琐.