关于mysql:事务那些事儿

7次阅读

共计 7938 个字符,预计需要花费 20 分钟才能阅读完成。

前言

有段时间没更新博客了,最近在学习一些无关事务的知识点,明天来总结一下,本文会波及到以下几个知识点:

  • MySQL 事务
  • Spring 事务
  • 分布式事务

什么是事务

事务是一系列操作组成的工作单元,该工作单元内的操作是不可分割的,即要么所有操作都做,要么所有操作都不做,这就是事务。

举个例子:

张三要给李四转账 100 元,那么咱们会有这样的一段 SQL:

begin transaction;
    update account set money = money-100 where name = '张三';
    update account set money = money+100 where name = '李四';
commit transaction;

事务的体现:这两个 SQL 要么全副胜利,要么全副失败。

事务是否失效数据库引擎是否反对事务是要害。比方罕用的 MySQL 数据库默认应用反对事务的 innodb 引擎。然而,如果把数据库引擎变为 myisam,那么程序也就不再反对事务了!

ACID

事务具备以下四个个性:

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

原子性(Atomicity)

一般来说,原子是指不能分解成小局部的货色。例如,在多线程编程中,如果一个线程执行一个原子操作,这意味着另一个线程无奈看到该操作的一半后果。零碎只能处于操作之前或操作之后的状态,而不是介于两者之间的状态。

一致性(Consistency)

事务一致性是指数据库中的数据在事务操作前后都必须满足业务规定束缚。
比方 A 转账给 B,那么转账前后,AB 的账户总金额应该是统一的。

隔离性(Isolation)

一个事务的执行不能被其它事务烦扰。即一个事务外部的操作及应用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能相互烦扰。
(设置不同的隔离级别,相互烦扰的水平会不同)

持久性(Durability)

事务一旦提交,后果便是永久性的。即便产生宕机,依然能够依附事务日志实现数据的长久化。

日志包含回滚日志(undo)和重做日志(redo),当咱们通过事务批改数据时,首先会将数据库变动的信息记录到重做日志中,而后再对数据库中的数据进行批改。这样即便数据库系统产生奔溃,咱们还能够通过重做日志进行数据恢复。

MySQL 事务隔离级别

MySQL 有以下四个事务隔离级别:

  • 未提交读(READ UNCOMMITTED)
  • 已提交读(READ COMMITTED)
  • 可反复读(REPEATABLE READ)
  • 串行化(SERIALIZABLE)

各个隔离级别可能会存在以下的问题:

那么什么是脏读、不可反复读和幻读?

脏读: 指一个事务能够看到另一个事务未提交的数据

比如说事务 A 批改了一个值然而还未提交,这时事务 B 能够看到 A 批改的值,这就是脏读。

不可反复读:一个事务执行两次同样的查问语句,前后得出的数据却不统一

比如说事务 A 执行了 select 语句,事务 B 批改了某个值,事务 A 再次执行 select 语句时发现后果和上次不统一,因而叫做不可反复读。

幻读:在同一个事务中,同一个查问屡次返回的记录行数不统一(这里的后果特指查问到的记录行数,幻读能够看做不可反复读的一种非凡状况)

比如说事务 A 执行了 select 语句,事务 B 插入数据,事务 A 再次执行 select 语句时发现多了几条记录,如同呈现了幻觉一样,因而叫做幻读。

Read Commit(读已提交)级别是如何解决脏读的?

先说论断:通过扭转锁的开释机会来解决脏读问题

首先先理解一下为什么会呈现脏读?起因就是在 未提交读 这个级别下,当事务 A 批改了数据之后就立马开释了锁,因而事务 B 能够读取到这个未提交的数据。

已提交读 级别下 写操作加的锁会到事务提交后开释,所以事务 B 不会读到事务 A 未提交的数据,通过扭转锁的开释机会解决了脏读的问题。

Repeatable Read(可反复读)级别是如何解决不可反复读的?

论断:可反复读 级别就是通过 MVCC 机制来解决不可反复读问题的

MVCC

多版本并发管制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体形式,用于实现提交读和可反复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需应用 MVCC。可串行化隔离级别须要对所有读取的行都加锁,单纯应用 MVCC 无奈实现。

MVCC 机制 (多版本并发管制) 就我集体了解来说其实就是给每行数据都增加了几个暗藏字段,用来示意数据的版本号,即 一个数据在 mysql 中会有多个不同的版本

在讲 MVCC 的实现原理之前,我觉很有必要先去理解一下 MVCC 的两种读模式。

有了 MVCC 之后咱们能够把 SQL 操作分为两类:

  • 快照读

读取以后事务可见的数据,默认的 select 操作就是快照读,读的是历史版本的数据。

  • 以后读

读取最新的数据,除了默认 select 操作外的 select..for updateupdateinsertdelete 等操作都是以后读,读取的都是最新的数据。

当初咱们有了 MVCC,当事务 A 执行一个一般的select 操作(快照读),MySQL 会把这次读取的数据保存起来,在这期间不论事务 B 执行 update 或是 insert 操作,事务 A 再次执行 select 操作读取到的数据是不会变的,因而通过可反复读级别通过 MVCC 解决了不可反复读问题,顺便解决了局部的幻读问题,没错 MVCC 并没有解决所有的幻读问题,只是解决了一部分。

那么什么时候会呈现幻读呢?

当事务 A 执行的是以后读,也就是加锁的 select 操作时如 select * from Employee for update,会去读取最新的数据,这样的话还是能够看到事务 B 提交的数据,因而 MySQL 提供了Next-Key Lock 算法来帮忙咱们对数据加锁。

Next-Key Lock

InnoDB 有三种行锁的算法:

  1. Record Lock:单个行记录上的锁。
  2. Gap Lock:间隙锁,锁定一个范畴,但不包含记录自身。GAP 锁的目标,是为了避免同一事务的两次以后读,呈现幻读的状况。

3. Next-Key Lock:1+2,锁定一个范畴,并且锁定记录自身。对于行的查问,都是采纳该办法,次要目标是解决幻读的问题。

Next-Key Lock 是 MySQL 的 InnoDB 存储引擎的一种锁实现。

MVCC 不能解决幻读的问题,Next-Key Lock 就是为了解决这个问题而存在的。在可反复读(REPEATABLE READ)隔离级别下,应用 MVCC + Next-Key Lock 能够解决幻读问题。

当查问的索引含有惟一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引自身,不是范畴。

它是 Record LockGap Lock 的联合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。

这样的话当事务 A 执行了 select * from Employee for update 之后,事务 B 插入数据会被阻塞,这样的话·Repeatable Read(可反复读)·级别应用 MVCC + Next-Key Lock 能够解决了不可反复读和幻读的问题。

Spring 事务

@Transaction 事务生效

在我的项目开发中咱们有时候会遇到 Spring 事务生效的场景,那么什么场景会导致事务生效呢?

@Transactional 注解属性 propagation 设置谬误

这种生效是因为配置谬误,若是谬误的配置以下三种 propagation,事务将不会产生回滚。

TransactionDefinition.PROPAGATION_SUPPORTS:如果以后存在事务,则退出该事务;如果以后没有事务,则以非事务的形式持续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务形式运行,如果以后存在事务,则把以后事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务形式运行,如果以后存在事务,则抛出异样。

@Transactional 利用在非 public 润饰的办法上

以下来自 Spring 官网文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

大略意思就是 @Transactional 只能用于 public 的办法上,否则事务不会生效,如果要用在非 public 办法上,能够开启 AspectJ 代理模式。

@Transactional 注解属性 rollbackFor 设置谬误

rollbackFor 能够指定可能触发事务回滚的异样类型。Spring 默认抛出了未查看 unchecked 异样(继承自 RuntimeException 的异样)或者 Error 才回滚事务;其余异样不会触发回滚事务。如果在事务中抛出其余类型的异样,但却冀望 Spring 可能回滚事务,就须要指定 rollbackFor属性。

// 心愿自定义的异样能够进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class

同一个类中办法调用,导致 @Transactional 生效

来看两个示例:

@Service
public class OrderServiceImpl implements OrderService {public void update(Order order) {updateOrder(order);
    }
    @Transactional
    public void updateOrder(Order order) {// update order;}
}

update 办法下面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 办法,updateOrder 办法上的事务管用吗?

@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void update(Order order) {updateOrder(order); 
   }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(Order order) {// update order;}
}

这次在 update 办法上加了 @Transactional,updateOrder 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

这两个例子的答案是:不论用!

因为 @Transactional 注解底层其实是 Spring 帮咱们生成了一个代理对象,当其它对象调用带有 @Transactional 的办法时,其实调的是代理对象,Spring 会在代理对象中帮咱们加上一系列的事务操作。

在下面的例子中它们产生了本身调用,就调用该类本人的办法,而没有通过 Spring 的代理类,默认只有在内部调用事务才会失效,这也是陈词滥调的经典问题了。

异样被吃了

这种状况是最常见的一种 @Transactional 注解生效场景

@Autowired
private B b;

@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void A(Order order) {
        try {b.insert();
         }catch (Exception e){//do something;}
    }
}

如果 b.insert()办法外部抛了异样,而 A 办法此时 try catch 了 B 办法的异样,那这个事务还能失常回滚吗?

答案:不能!而是会抛出上面异样:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

因为当 ServiceB 中抛出了一个异样当前,ServiceB 标识以后事务须要 rollback。然而 ServiceA 中因为你手动的捕捉这个异样并进行解决,ServiceA 认为以后事务应该失常 commit。此时就呈现了前后不统一,也就是因为这样,抛出了后面的 UnexpectedRollbackException 异样。

spring 的事务是在调用业务办法之前开始的,业务办法执行结束之后才执行 commit or rollback,事务是否执行取决于是否抛出 Runtime 异样。如果抛出 runtime exception 并在你的业务办法中没有 catch 到的话,事务会回滚。

在业务办法中个别不须要 catch 异样,如果非要 catch 肯定要抛出throw new RuntimeException(),或者注解中指定抛异样类型@Transactional(rollbackFor=Exception.class),否则会导致事务生效,数据 commit 造成数据不统一,所以有些时候 try catch 反倒会画龙点睛。

分布式事务

首先看下什么是分布式事务:

分布式事务就是指事务的参与者、反对事务的服务器、资源服务器以及事务管理器别离位于不同的分布式系统的不同节点之上。简略的说,就是一次大的操作由不同的小操作组成,这些小的操作散布在不同的服务器上,且属于不同的利用,分布式事务须要保障这些小操作要么全副胜利,要么全副失败。实质上来说,分布式事务就是为了保障不同数据库的数据一致性。

那么为什么须要分布式事务?间接用 Spring 提供的 @Transaction 注解 不行吗?

这里极其重要的一点:单块零碎是运行在同一个 JVM 过程中的,然而分布式系统中的各个系统运行在各自的 JVM 过程中。因而你间接加@Transactional 注解是不行的,因为它只能管制同一个 JVM 过程中的事务,然而对于这种跨多个 JVM 过程的事务无能无力。

分布式的几种解决方案

可靠消息最终一致性计划

咱们来解释一下这个计划的大略流程:

  1. A 零碎先发送一个 prepared 音讯到 mq,如果这个 prepared 音讯发送失败那么就间接勾销操作别执行了,后续操作都不再执行。
  2. 如果这个音讯发送胜利过了,那么接着执行 A 零碎的本地事务,如果执行失败就通知 mq 回滚音讯,后续操作都不再执行。
  3. 如果 A 零碎本地事务执行胜利,就通知 mq 发送确认音讯。
  4. 那如果 A 零碎迟迟不发送确认音讯呢?此时 mq 会主动定时轮询所有 prepared 音讯,而后调用 A 零碎当时提供的接口,通过这个接口反查 A 零碎的上次本地事务是否执行胜利 如果胜利,就发送确认音讯给 mq;失败则通知 mq 回滚音讯(后续操作都不再执行)。
  5. 此时 B 零碎会接管到确认音讯,而后执行本地的事务,如果本地事务执行胜利则事务失常实现。
  6. 如果零碎 B 的本地事务执行失败了咋办?基于 mq 重试咯,mq 会主动一直重试直到胜利,如果切实是不行,能够发送报警由人工来手工回滚和弥补。这种计划的要点就是能够基于 mq 来进行一直重试,最终肯定会执行胜利的。因为个别执行失败的起因是网络抖动或者数据库霎时负载太高,都是暂时性问题。通过这种计划,99.9% 的状况都是能够保证数据最终一致性的,剩下的 0.1% 出问题的时候,就人工修复数据呗。

实用场景: 这个计划的应用还是比拟广,目前国内互联网公司大都是基于这种思路玩儿的。

最大致力告诉计划

整个流程图如下所示:

这个计划的大抵流程:

  1. 零碎 A 本地事务执行完之后,发送个音讯到 MQ。
  2. 这里会有个专门生产 MQ 的最大致力告诉服务,这个服务会生产 MQ,而后写入数据库中记录下来,或者是放入个内存队列。接着调用零碎 B 的接口。
  3. 如果零碎 B 执行胜利就万事 ok 了,然而如果零碎 B 执行失败了呢?那么此时最大致力告诉服务就定时尝试从新调用零碎 B,重复 N 次,最初还是不行就放弃。

这套计划和下面的可靠消息最终一致性计划的区别:

可靠消息最终一致性计划能够保障的是只有零碎 A 的事务实现,通过不停(有限次)重试来保证系统 B 的事务总会实现。

然而最大致力计划就不同,如果零碎 B 本地事务执行失败了,那么它会重试 N 次后就不再重试,零碎 B 的本地事务可能就不会实现了。

至于你想管制它到底有“多致力”,这个须要联合本人的业务来配置。

比方对于电商零碎,在下完订单后发短信告诉用户下单胜利的业务场景中,下单失常实现,然而到了发短信的这个环节因为短信服务临时有点问题,导致重试了 3 次还是失败。

那么此时就不再尝试发送短信,因为在这个场景中咱们认为 3 次就曾经算是尽了“最大致力”了。

简略总结:就是在指定的重试次数内,如果能执行胜利那么大快人心,如果超过了最大重试次数就放弃,不再进行重试。

实用场景: 个别用在不太重要的业务操作中,就是那种实现的话是精益求精,但失败的话对我也没有什么坏影响的场景。

比方上边提到的电商中的局部告诉短信,就比拟适宜应用这种最大致力告诉计划来做分布式事务的保障。

TCC 强一致性计划

TCC 的全称是:

  • Try(尝试)
  • Confirm(确认 / 提交)
  • Cancel(回滚)。

这个其实是用到了弥补的概念,分为了三个阶段:

  1. Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留;
  2. Confirm 阶段:这个阶段说的是在各个服务中执行理论的操作;
  3. Cancel 阶段:如果任何一个服务的业务办法执行出错,那么这里就须要进行弥补,就是执行曾经执行胜利的业务逻辑的回滚操作。

还是给大家举个例子:

比方跨银行转账的时候,要波及到两个银行的分布式事务,如果用 TCC 计划来实现,思路是这样的:

  1. Try 阶段:先把两个银行账户中的资金给它解冻住就不让操作了;
  2. Confirm 阶段:执行理论的转账操作,A 银行账户的资金扣减,B 银行账户的资金减少;
  3. Cancel 阶段:如果任何一个银行的操作执行失败,那么就须要回滚进行弥补,就是比方 A 银行账户如果曾经扣减了,然而 B 银行账户资金减少失败了,那么就得把 A 银行账户资金给加回去。

实用场景:这种计划说实话简直很少有人应用,然而也有应用的场景。

因为这个事务回滚实际上是重大依赖于你本人写代码来回滚和弥补了,会造成弥补代码微小,十分之恶心。

比如说咱们,一般来说跟钱相干的,跟钱打交道的,领取、交易相干的场景,咱们会用 TCC,严格保障分布式事务要么全副胜利,要么全副主动回滚,严格保障资金的正确性,在资金上不容许呈现问题。

比拟适宜的场景:除非你是真的一致性要求太高,是你零碎中外围之外围的场景,比方常见的就是资金类的场景,那你能够用 TCC 计划了。你须要本人编写大量的业务逻辑,本人判断一个事务中的各个环节是否 ok,不 ok 就执行弥补 / 回滚代码。

而且最好是你的各个业务执行的工夫都比拟短。

然而说实话,个别尽量别这么搞,本人手写回滚逻辑,或者是弥补逻辑,切实太恶心了,那个业务代码很难保护。

总结

明天简略对事务做了一个总结,有什么不对的中央请多多指教!

参考

https://zhuanlan.zhihu.com/p/85790242

正文完
 0