一、事务
1.1 什么是事务?为什么需要事务?
事务是保证持久化数据的可靠机制。事务的隔离性可以避免并行事务的场景下,错误数据的产生。
1.2 事务为什么需要隔离?不同隔离级别的目的是什么?
多个事务在并行执行时,需要避免相互的干扰,所以需要隔离执行。而隔离级别定义了事务间数据的可见程度。
1.2.1 SQL标准中的四种事务隔离级别:
Serializable(串行化):
对行所在的表,启用表级排他锁,阻止其他事务加锁完成读、写和修改操作;避免了幻读、不可重复读、脏读问题。serializable 大大降低了并发的性能,适合几乎没有并发的情况下才考虑使用该锁;
Repeatable-Read(可重复读):
该模式下会出现幻读(Phantom Problem)的问题。幻读 是指在同一事务下, 不同时间执行相同的 SQL ,第二次的 SQL 返回了之前不存在的行。和新增行有关。
是 InnoDB 默认的隔离级别,但存储引擎通过 Next-Key-Lock 机制解决了幻读问题。
InnoDB 锁的更多介绍
Read Committed(提交读/不可重复读):
事务中的修改只有提交后 才能被其他事务读取到。该模式下会除了会发生幻读,还会发生不可重复读问题。事务A中,第一次读取某些数据后,事务B操作该数据并提交,当事务A第二次读取该数据时,出现的同一事务内两次读取数据不一致的问题,这称之为不可重复读。只和修改行数据有关。
Read Uncommitted(未提交读):
事务中的修改即使没有提交,对其他事务也都是可见的。改隔离级别下,会发生幻读、不可重复读、和脏读。事务读取到了其他事务未提交的数据,这称之为脏读
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不会 | 可能 | 可能 |
可重复读 | 不会 | 不会 | 可能 |
可串行化 | 不会 | 不会 | 不会 |
1.3 幻读问题详解
1.3.1 什么是幻读?
幻读是不同事务新增记录时,导致的某个事务出现数据幻觉的情况。即一个事务按照某个相同条件多次读取记录,第二次取到了之前没有读到的记录,而这个记录来自另一个事务新增的新记录。
RR隔离级别下的幻读实验:
说明:
- 开启事务A,查询不到id=6的数据
- 开启事务B,新增id=6的数据,并提交
- 由于 MVCC 的版本控制,事务A无法找到已提交的id=6的数据
- 更新id=6的数据,MVCC将数据的修改版本更新到为前事务的版本,此时便可以在事务A中查到id=6的数据
对于其他事务新增的数据,事务A在读取时,出现了幻觉,所以 MVCC 下的快照读取机制,是无法解决幻读问题的。
1.3.2 如何解决幻读?
InnoDB 中使用 Next-Key Locks 和 MVCC来解决幻读问题。在进行搜索和索引扫描时,使用 for update 或 lock in share model,对数据行 或 数据的前后间隔添加锁,阻塞对应位置的数据操作,防止了幻读的发生。
Next-Key Locks 更多关于 Next-Key Locks 可参考 InnoDB 对锁的实际应用
1.4 事务的特性 – ACID
-
原子性(Atomicity):事务是一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交,要么全部失败回滚,不可能执行其中的一部分操作
-
一致性(Consistent):数据库中的业务数据总是从一个一致性状态,转移到另一个一致性状态
-
隔离性(Isolation):通常来说,一个事务所做的修改在最终提交以前,其他事务是不可见的
-
持久性(Durable):一旦事务提交,其所做的修改就会永久保存到数据库中
1.4.1 ACID分别用什么机制来保证的?
Atomicity: 由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
Consistent: 一般由代码层面来保证业务数据一致性
Isolation: 由MVCC来保证,MVCC的更多内容 参考
Durable:由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交时通过redo log刷盘,宕机的时候可以从redo log恢复
思考:
1. 在默认隔离级别中,两个事务插入相同 UK 时,事务会报主键冲突错误还是等待?
MySQL的默认隔离级别为Repeatable-Read。当事务1中插入数据时,对UK_a加上排它锁进行写入,事务2无法对UK_a加任何锁,故只能等锁释放或等待超时,此时便有两种场景,
- 事务1释放X锁后,事务2可以添加X锁,尝试写入数据,但违反UK约束抛出异常
- 两个事务中插入PrimaryKey
- 两个事务中插入UniqueKey
- 事务1持有的X锁不释放,事务2等待锁超时,事务结束。
分布式事务的实现方案
1. XA 方案:两阶段提交
XA方案中,有事务管理器的概念,负责协调多个数据库的事务,适合单个应用跨多个库的分布式事务。其实现依赖于数据库层面提供的工具,所以该方案的事务时间长,锁数据时间长,吞吐量低(实现方式参考 JTA)。
该方案的两个特点,不适用于目前的微服务结构体系,不做深入讨论。
2. TCC 方案:Try Confirm Cancel
- Try 阶段:对各个服务的资源做检测,以及对资源进行锁定或者预留。
- Confirm 阶段:在各个服务中执行实际的操作,失败后需要重试,所以需要保证该阶段操作具备幂等性。
- Cancel 阶段:如果任何一个服务的业务方法执行出错,那么就需要执行 Confirm 阶段的业务回滚和释放 Try 阶段锁定的资源操作
解决的问题:
- 解决了协调者单点问题:由主业务方发起并完成整个业务活动,业务活动管理器可以为集群。
- 优化阻塞方式:引入超时机制,超时后进行重试或回退,缩小了锁定的资源粒度。
- 保证了数据一致性
缺陷:
- TCC 方案中的三个阶段,分别对应三个功能接口,所以该方案下的代码量和工作量是比较大的。
该方案保证了强一致性,适用于对数据要求非常严格的场景,有更好的并发量。
3. 本地消息表
工作流程:
-
A 系统在自己本地一个事务里操作业务数据和插入一条相关数据到消息表中;
-
接着 A 系统脚本定期轮询本地消息表,将未发送、未处理的消息发送到 MQ 中去
-
B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息;
-
B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态(更新的手段可以借助 ZK 或 A系统提供一个接口来实现,其意义在于保证A的消息被B处理到);
-
如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
-
这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。
4.可靠消息最终一致性方案
该方案类似于方案3,但区别在于使用了 RocketMq 的 prepare \ confirm (消息事务机制) 去替代本地消息表。
方案三和方案四都是分布式事务的常用解决方案,具体的选择,可以考虑团队更适合哪一种。
5.最大努力通知方案
这个方案的大致思想是:
- 系统 A 本地事务执行完之后,发送个消息到 MQ;
- 做一个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。
该方案常用在支付服务收到第三方支付成功通知后,调用订单服务更新订单的场景