导览
前言
书接上文,继续展开基于Spring的各组件的原理解密。本文带这各位聊聊事务的那些事儿。当提到事务的时候,你会想到什么?各位盆友,是否能够立刻让“ACID”
脱口而出?
我们先从最基础事务谈起吧,Let’s go~
Q:什么是事务
1. 设计目的
事务由事务开始(begin transaction)与事务结束(end transaction)之间执行的全体操作组成。数据库事务通常包含了一个序列的对数据库的读/写操作。
通常,包含有以下两个目的:
- 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。 - 当事务被提交给了数据库管理系统(DBMS),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要回滚;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
2. 四大特性
事务其实就是并发控制的基本单位,也是一个序列操作。其中的操作要么都执行,要么都不执行,它是一个不可分割的工作单位。数据库事务的 ACID 四大特性是事务的基础。了解了 ACID 是如何实现的,也就清楚了事务的实现,接下来我们将依次介绍数据库是如何实现这四个特性的。
2.1 原子性(Atomicity)
原子性是指事务包含的所有操作,要么全部成功,要么全部回滚。因此事务的操作,如果成功,就必须要完全应用到数据库,如果失败,则不能对数据库有任何影响。
由于操作并不具有原子性,并且可以再分为多个操作,当这些操作出现错误或抛出异常时,整个操作就可能不会继续执行,而已经进行的操作造成的副作用就可能造成数据更新的丢失或者错误。想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚。比如,在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的。所有事务进行的修改都会先记录在回滚日志中,然后在写库。
具体过程可参考如下3图:
2.2 持久性(Durability)
持久性是指一个事务一旦被提交,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
与原子性一样,事务的持久性也是通过日志来实现的,比如MySQL 使用重做日志(redo log
)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,它是易失的;另一个是在磁盘上的重做日志文件,它是持久的。当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上。下图中的第 4、5 步就是在事务提交时执行的。
除了所有对数据库的修改会产生重做日志外,由于回滚日志也是需要持久存储的,它们也会创建对应的重做日志。在发生错误后,数据库重启时会从重做日志中找出未被更新到数据库磁盘中的日志重新执行以满足事务的持久性。
2.3 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始
。
即使每个事务都能确保一致性和原子性,如果几个事务并发执行,它们的操作会以某种人们所不希望的方式交叉执行,这也会导致不一致的状态。一种避免事务并发执行而产生的问题的途径是串行地执行事务,但事务并发执行能显著地改善性能。因此人们提出了多种允许多个事务并发执行的解决方法。
以常用的mysql InnoDB存储引擎为例:加入商品表items表
中有一个字段status
,status=1表示该商品未被下单,status=2表示该商品已经被下单,那么我们对每个商品下单前必须确保此商品的status=1。
此时,假设有一件商品,其id为10000,如果不使用锁,那么通过如下SQL实现:
//查出商品状态
select status from items where id=10000;
//根据商品信息生成订单
insert into orders(id,itemid)values(null,10000);
//修改商品状态为2
update items set status=2 where id=10000;
2.4 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
一致性要求事务的执行不改变A、B之和。如果没有一致性要求,金额可能会被事务凭空创造或者销毁!如果数据库在事务执行前是一致的,那么事务执行后仍将保持一致性,这是很容易验证的。确保单个事务的一致性是编写该事务的程序员的责任,完整性约束的自动检查给这项工作带来了便利。如果系统的状态不再反映数据库本应描述的现实世界的真实状态,称之为不一致状态。
ACID_64">3. ACID相互关系
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时要只要能满足原子性,就一定能满足一致性。
- 在事务并发情况下,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库奔溃的情况。
掌握了事务的基本原理后,我们开始正式谈谈Spring中的事务是如何管理的。
Q:什么是事务的隔离级别及其影响
1. 标准隔离级别
SQL事务中有四种标准的隔离级别:
1.1 未提交读(READ UNCOMMITTED)
事务中的修改,即使没有提交,对其它事务也是可见的。使用查询语句不会加锁,可能会读到未提交的行(Dirty Read
)。
1.2 提交读(READ COMMITTED)
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read
)。
1.3 可重复读(REPEATABLE READ)
保证在同一个事务中多次读取同样数据的结果是一样的。多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read
)。
1.4 可串行化(SERIALIZABLE)
强制事务串行执行。
博主通过一个表格加以总结:
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
未提交读 | YES | YES | YES |
提交读 | NO | YES | YES |
可重复读 | NO | NO | YES |
可串行化 | NO | NO | NO |
以上,所有的事务隔离级别都不允许脏写入(Dirty Write),也就是当前事务更新了另一个事务已经更新但是还未提交的数据,大部分的数据库中都使用了 READ COMMITED 作为默认的事务隔离级别,但是 MySQL 使用了 REPEATABLE READ 作为默认配置。
请参考:
2. 并发一致性问题
2.1 丢失修改
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。从而产生“丢失修改”
的问题。
2.2 不可重复读
T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
2.3 幻读
T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
那么,有什么办法解决它们呢?
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
结语
本文通过精讲的方式,阐述了事务及其基本原理、特性和应用问题。内容丰富,知识点较多,所以很重要,同样也是面试必答环节,好好研习一下,可助你通关哦。
走过的、路过的盆友们,点点赞,收收藏,并加以指导,以备不时之需哈~
精彩回顾
一文读懂Spring Security的工作原理和机制(面试经)
一文读懂Spring AOP的工作原理和机制(面试经)
一文读懂Spring IoC的工作原理和机制(面试经)
一文读懂SpringMVC的工作原理
Springboot中基于X509完成SSL检验的原理与实践
基于springboot+enum配置化实践