数据库事务
一、事务的四大特性
- A(atomicity),原子性。一个事务内的语句要么全部执行成功要么全部执行不成功。
- C(consistency),一致性。
一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。
一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
以转账为例,A给B转账,在转账结束后,A和B总额不变(维持业务逻辑一致性),A和B剩余金额皆为正数(维持数据完整性)
- I(isolation),隔离性 事务提交前对其他事务都不可见。
- D(durability),持久性 事务一旦提交就是永久性的,即便发生宕机数据库也能恢复,保证了高可靠性。
二、事务各个特性的实现原理
WAL机制
WAL全称为Write-Ahead Logging,预写日志系统。其主要是指MySQL在执行写操作的时候并不是立刻更新到磁盘上,而是先记录在日志中,之后在合适的时间更新到磁盘中。日志主要分为undo log、redo log、binlog
- 原子性
在事务回滚时撤销所有已经成功执行的sql语句。依靠undo log回滚至修改前的数据。
undo log实现原子性和持久性的例子:
A = 1, B = 2 //初始值
a. 事务开始
b. 记录A = 1 到undo log中 //记录修改前的数值,方便undo
c. 修改A = 2
d. 记录B = 2 到undo log中 //记录修改前的数值,方便undo
e. 修改B = 3
f. 将undo log写到磁盘 //undo log持久化
g. 数据写到磁盘 //数据持久化
h. 事务提交
**实现原子性和持久性的原因:**
1. 更新数据前记录undo log
2. 为了保证持久性,必须将数据在事务提交前写到磁盘,只要事务成功提交,那么数据必然已经持久化
3. undo log必须先于数据持久化到磁盘,如果在gh之间系统崩溃,undo log仍然是完整的,可以用于回滚(不是用于持久化)
4. 如果是a-f之间系统发生崩溃,系统尚未提交,仍然保持在事务开始前的状态
中间一直在纠结如果在g步骤是发生了宕机undo log要怎么回滚,但突然意识到,在g处宕机,首先该考虑的根本不是回滚,而是恢复,即利用redo log进行数据恢复(持久性)(此时redo log应该已经保存了),将磁盘数据先更新到正常状态,再考虑回滚的问题。由此实现了原子性和持久性。
- 持久性
由于和磁盘交互很慢,所以InnoDB提供了缓存作为访问数据库的缓冲,向数据库写入数据时,先写入缓冲再写入磁盘,缓冲中的数据定期刷新到磁盘中(刷脏)。
缓存提高了读写数据的效率,但如果MySql宕机,而此时缓存中数据尚未刷新到磁盘,就会导致数据丢失,事务持久性无法保证
redo log解决持久性问题,修改数据时,不但修改缓存上的数据,还再redo log记录本次操作。事务提交时,对redo log刷盘,如果mysql宕机,可以读取redo log的数据,对数据库进行恢复。
由于所有修改先写入日志再更新到缓存,不会因为宕机而丢失,满足了持久性要求。
————————————————————————————————————————————————————————————
为什么提交时将redo log写入磁盘而不是刷脏:
- 刷脏是随机IO、修改数据位置随机,redo log是追加操作,属于顺序IO
- 刷脏以数据页(16KB)为单位,有大量无效IO
- 隔离性
隔离性要求并发情形下事务之间互不干扰,以读和写操作为例
1. 写写互斥:加锁
锁按照粒度可以分为表锁、行锁以及其他位于二者之间的锁。
MyIsam只支持表锁、InnoDB同时支持表锁和行锁,大多情况使用行锁
2. 写读互斥:MVCC保证隔离性
InnoDB默认隔离级别是可重复读,使用MVCC(读不加锁,因此读写不冲突,具体下面那介绍)
- 一致性
事务追求的最终目标:原子性、隔离性、持久性均为其服务。
需要数据库和应用层面共同保障。
三、事务并发存在的问题
脏读
读到其他事务尚未提交的数据
不可重复读
一个执行时间较长的数据,在前后两次读取同一个数据得到不同的结果。
与脏读的区别:
脏读读到的是尚未提交的数据。
不可重复读每一次读取的确实是最新值(且被提交了),但是因为别的事务的修改导致读到的数不一样。
幻读
两次查询数据的数据结果数量不一致。
与不可重复读的区别:
幻读和不可重复读的本质是一样的,两者都表现为两次读取的结果不一致。但是不可重复读指的是两次读取同一条记录的值不同,而幻读指的是两次读取的记录数量不同。
不可重复读重点在于update和delete,而幻读的重点在于insert。
行锁可以避免不可重复读,但不能避免幻读(insert),如果使用读写隔离锁可以避免幻读,但这样代价很大(MVCC)
四、数据隔离级别及其实现
1. 读未提交
最差的,脏读都支持
2. 读提交
解决脏读问题,不解决不可重复读问题,本质就是只读取已经提交了的数据。
3. 可重复读
解决不可重复读的问题(行锁),但不解决幻读问题(插入)
InnoDB使用的Next-Key Lock算法实现行锁,也包含了间隙锁,会锁定一个范围,因此也解决了幻读问题。
但上述都是在加锁条件下实现的。
MVCC提供了一个读不加锁、读写不冲突的多版本数据共存的并发控制协议。解决脏读、不可重复读、幻读。
注意:MVCC与间隙锁不是两种算法二选一,而是为了解决幻读问题的策略的两个部分。
当前读采用间隙锁解决,快照读采用MVCC解决。
4. 串行化
不允许并行事务,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。
五、MVCC
首先, MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。
多版本的意思是同时存在多个版本的数据(某条记录),而不是多个版本的数据库,在某个事务进行操作时,需要查看记录的隐藏列事务版本id,对比事务id并根据事务隔离级别判断读取哪个版本的数据。
数据库并发问题有三类:
- 读-读问题,完全不冲突,不用管
- 读-写问题,会引起脏读、不可重复读、幻读问题。
- 写-写问题,会有更新丢失的问题,加锁,悲观锁或者乐观锁都可以。
MVCC的引进是为了解决读写冲突问题,避免读-写事务间的阻塞,写写问题不归它管,该加锁加锁。
1. 快照读和当前读
- 快照读:不加锁的非阻塞读,在读取之后可能会有其他事务修改(不加锁嘛),所以可能读取的是历史版本。
- 当前读:加锁(这里一般就得用间隙锁避免幻读了),增删改和加锁的查一般都是当前读。
2. 悲观锁和乐观锁和CAS
本质上不是数据库中具体的锁概念,而是我们定义出来,用来描述两种类别的锁的思想。
- 悲观锁
- 悲观锁默认访问数据期间会产生冲突,因此数据处理过程中全程加锁,保证同时只有一个线程可以访问数据。
通常利用数据库本身提供的锁机制实现。 - 解决读写、写写冲突。
- 适合写多读少情景。
- 悲观锁默认访问数据期间会产生冲突,因此数据处理过程中全程加锁,保证同时只有一个线程可以访问数据。
- 乐观锁
- 默认没有冲突存在,只在提交数据的时候才去检测是否产生了冲突,有再进行下一步处理(返回冲突信息、重试等)。
- 解决写写冲突。
- CAS思想
CAS指令全称Compare and Swap,是系统的指令集,整个CAS操作是一个原子操作不可分割。
CAS指令需要3个操作数,内存位置V、旧的预期值A,新值B。
CAS执行时,如果V的位置恰为A才更新B。否则不更新,但无论是否更新V的值,都会返回V的旧值A或者A’。
- 乐观锁的实现(如何解决ABA问题)
- 数据版本
在表里新增一个字段表示这个数据的版本,比较数据的时候同时比较数据版本号,只有一致的时候才更新,并将版本号+1- 时间戳
将版本号改成时间戳而已,一样的。
3. 什么是MVCC
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
4. OCC,PCC,MVCC三者的关系
- OCC:乐观并发控制,是一种用来解决
写-写冲突
的无锁并发控制 - PCC:悲观并发控制,用来解决
读写冲突
和写写冲突
的加锁并发控制 - MVCC:多版本并发控制,解决
读写冲突
的无锁并发控制
在数据库中,我们可以形成两个组合:
- MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突 - MVCC + 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题
5. MVCC的实现原理
三大要素:
两个隐式字段
、回滚日志(undo log
、read-view
两个隐式字段:DB_TRX_ID(创建这条记录最后一次修改该记录的事务id),DB_ROLL_PTR(回滚指针,指向这条记录的上一个版本)
回滚日志:每次进行更新操作时都会在undo log中记录旧值,产生版本链,是read-view获取数据的前提
read-view:在SQL执行查询语句时产生,由未提交事务id组成的数组和创建的最大事务id组成。
1. 隐式字段
InnoDB存储引擎中,如果有聚簇索引(主键id)每一行记录都会有两个隐藏字段,如果没有聚簇索引还会多一个隐藏字段作为隐藏主键。
DB_TRX_ID:
记录插入或更新该行的最后一个事务的事务标识符,也就是事务ID。此外,删除在内部也被认为是一种更新,所以其实还会有一个del flag字段表示记录被更新或删除,并不是真的删除。
DB_ROLL_PTR:
回滚指针,指向写入回滚段的撤消日志记录。 如果行已更新,则撤消日志记录将包含在更新行之前重建行内容所必需的信息。
2.undo log(回滚日志)
作用:
- 维持事务原子性
- 实现MVCC
undo log细分为两种:插入时产生的undo log、更新删除产生的undo log。
- 插入产生的undo log:
事务提交后就会被删除,因为新插入的数据没有历史版本,无需维护undo log
。 - 更新和删除操作产生的undo log:
由于存在历史版本数据,属于同一种类型,无论是事务回滚还是快照读都需要维护版本信息。
只有在快照读和事务回滚不涉及该日志时,对应的日志才会被purge线程同意删除。
undo log在MVCC中的作用:保存版本链
,使用DB_ROLL_PTR来连接。
3.read-view 一致性视图
数据库执行select语句时会产生一致性视图read view:查询时所有未提交的事务id组成一个数组,数组中id最小值记作min_id,已创建的最大事务id记作max_id。
undo log在mvcc中的作用就是为了根据存储的事务id和一致性视图作比较得到快照结果
4. 版本链对比规则
- 如果id < min_id,则该版本是已经提交的事务生成的,可见
- 如果id > max_id,该版本是将来启动的事务生成的,不可见
- 否则
– 如果id在数组中,表示由尚未提交的事务生成的,不可见;但如果恰好是自己的id,那么可见。
– 如果id不在数组中,表示已经提交的事务生成的,可见。
如果不可见则沿着版本链向下寻找,直至可见或版本链断裂。
对于已经删除的数据
在内部删除和更新是同一类型的undo log,删除一条数据时会将版本链上最新的数据
复制
一份,将trx_id修改为删除时的trx_id,同时在该记录的头信息中存一个del flag标记写为true,表示当前记录已经被删除。