什么是事务
事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
概述
当事务被提交给了DBMS(数据库管理系统),则DBMS需要保证该事务的所有操作成功完成,并且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则数据库中的所有操作都需要回滚,回到事务之前的状态,同时该事务对数据库其他事务的执行无影响,所有事务好像都在独立的运行。
事务的ACID特性
Atomicity 原子性
1 | 事务作为一个整体被执行,要么全部执行,要么全部不执行(由undo-log保证,事务某处发生错误的时候通过undo-log进行回滚,保证了原子性) |
Consistency 一致性()
1 | 事务应确保数据库的状态从一个一致状态转变为另一个一致状态,一致状态的含义是数据库内的数据应满足完整性约束. |
Isolation 隔离性
1 | 多个事务并发执行时,一个事务的执行应该不影响其他事务的执行。(由MVCC||加锁保证) |
Durability 持久性
1 | 已经提交的事务对数据库的修改应该永久保存在数据库当中。(由redo-log保证,数据库保存数据的时候会先写入) |
事务的隔离级别
READ-UNCOMMITED(未提交读)
该隔离级别下的事务会读到其他未提交的事务的数据,这种现象也被称之为脏读
READ-COMMITED(提交读)
一个事务可以读取另一个已经提交的事务,多次读取会产生不一样的结果,此现象被称为不可重复读
REPEATABLE-READ(可重复读)
同一个事务内,同样的查询语句多次执行所获取的结果是一致的,但是可能会出现幻读现象
SERIALIZABLE(序列化)
该隔离级别下,所有事务都是串行执行的。此级别是最高隔离级别,同时性能也是最差的
事务的并发带来的问题
脏读
当一个事务正在访问数据,并且对数据进行了修改,但是该事务还未提交,这时另一个事务访问到了该事务中未提交的数据,产生了脏读
时间 | 事务1 | 事务2 |
---|---|---|
t1 | 开启事务 | 开启事务 |
t2 | — | update table set property = ‘newValue’ where id = 1; |
t3 | select * from table where id = 1 | — |
t4 | commit | commit |
假设事务2未更新前是’oldValue’,事务2更改后修改还没有提交,事务1读到了事务2未提交的’newValue’,这时,事务2进行了回滚,导致事务1拿到的数据变为无效数据,因为事务2对该数据的修改还是未提交的,那么事务1此时读到的数据就是脏数据,对脏数据进行的操作可能是不正确的。
幻读
多次读取一定范围内的记录,发现结果不一致(一般指记录增多或记录减少,下述操作在mysql8.0.0版本未能出现,mysql innodb 引擎部分解决了幻读问题,可以尝试在其他数据库执行该操作)
时间 | 事务1 | 事务2 |
---|---|---|
t1 | 开启事务 | 开启事务 |
t2 | 查询所有年龄为20的person,结果为20 | — |
t3 | — | 新增一个年龄为20的person并commit |
t4 | 查询所有年龄为20的person,结果为21 | — |
t5 | commit | commit |
假设事务2插入前,年龄为20的人有20条记录, 事务1第一次查询的时候发现有20个年龄为20的人,第二次查询却发现年龄为20的人多了一个,好像产生了幻觉
不可重复读
多次读取同一条记录,发现该记录列值被修改过
时间 | 事务1 | 事务2 |
---|---|---|
t1 | 开启事务 | 开启事务 |
t2 | 查询张三的年龄,结果为20 | — |
t3 | — | 更新张三年龄为21 |
t4 | 查询张三的年龄,结果为21 | — |
t5 | commit | commit |
假设张三原来的年龄为20,发现张三第一次查询的结果20和第二次查询的结果21不一致,这就是不可重复读
各事务隔离级别对应解决的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITED | √ | √ | √ |
READ-COMMITED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
mysql的InnoDB存储引擎解决了RR模式下部分幻读的问题,但是并不能完全避免幻读,解释见下文快照读和当前读部分
github上面有关于这个问题的讨论
并发事务解决方案
锁机制
解决写-写冲突问题,效率很差
悲观锁 依靠数据库提供的锁机制实现 当一个线程需要对共享资源进行操作的时候,首先对其进行加锁,当该线程持有该资源的时候,其他线程对该资源操作的时候就会阻塞
乐观锁 基于版本号实现
当一个线程需要操作共享资源的时候,不进行加锁,而是在操作之后,通过一个版本号进行控制,如果在该线程执行操作完成后通过版本号进行判断,如果发现已经被其他线程操作了,则通知该线程操作失败。
MVCC(多版本并发控制)
解决读写冲突问题,通过一种快照机制生成一个数据请求时间节点的数据快照,通过这个快照来进行数据的读取,这样,因为不需要加锁,读写操作都不会相互阻塞(仅作用于RR和RC)。
Mysql InnoDB 引擎在每一行数据都都加了几个隐藏列(参考自mysql官方文档)
1 | Internally, InnoDB adds three fields to each row stored in the database. A 6-byte DB_TRX_ID field indicates the transaction identifier for the last transaction that inserted or updated the row. |
DB_TRX_ID 6字节的事务id,代表最近一次插入或更新该行记录的事务id
在inno db存储引擎下,delete操作被视为update操作,做法就是在记录行中有一个特殊的标记位标记该行是否被删除,而并非真删除DB_ROLL_PTR:7字节回滚指针,每次对一行数据进行修改的时候,会生成一条undo log,保留数据修改前的状态,回滚指针即指向该undo log记录
DB_ROW_ID: 隐藏的自增id,当建表时候没有指定主键的时候,就会使用该rowId创建聚簇索引,否则聚簇索引中将不包含该DB_ROW_ID内容
read-view(又称快照snapshot)
事务的快照主要是用来存储数据库的事务运行情况,通常用来进行可见性判断。
- 查看当前所有未提交的活跃事务,存储在数组中记作m_ids
RR是在第一次快照读操作前生成readView,之后的查询操作复用该readView
RC是在每一次快照读操作前都生成一个readView
可见性比较算法
如果 DB_TRX_ID < m_ids中最小事务id,则代表被访问的数据版本在生成快照之前就已经提交,当前事务可以获得该条记录的稳定版本
如果 DB_TRX_ID < m_ids中最大事务id,则代表生成该事务的版本在生成快照之后,所以该版本对当前事务是不可见的
如果DB_TRX_ID在最大事务id和最小事务id之间,则需要判断下DB_TRX_ID是否在m_ids当中,如果存在,则说明事务还是活跃的,该版本数据不可访问,如果不存在,说明该事务在生成快照之后已经提交,该数据版本可以被该事务访问
当前读和快照读
- 快照读 普通的select操作(不包含 select … for update, select … lock in share mode)
- 当前读
- select … in share mode
- select … for update
- insert
- update
- delete
时间 | 事务1 | 事务2 |
---|---|---|
t1 | 开启事务 | 开启事务 |
t2 | 查询年龄为20的人,结果为20 | — |
t3 | — | 新增一个人李四,id为31 |
t4 | — | commit |
t5 | 更新李四的年龄为31,可以更新成功,此时再次查询年龄为20的人,结果为21,出现幻读 | — |
t6 | commit | — |
inno db引擎只能部分避免幻读,这个部分指的是只能避免当前读的幻读,而对于上表中快照读的情况却不能避免
版本链
每次对记录进行改动,都会记录一条undo-log,每条undo-log上面也会有一个roll-ptr属性,指向历史版本的undo-log,可以将这些undo-log串联起来,形成一个版本链,如下图(图片引用自参考文章中《MySQL事务隔离级别和MVCC》
)
MVCC总结
所谓的MVCC,就是指RR,RC这两个事务隔离级别的事务在执行快照读操作时访问版本链的过程,使得不同事务的读-读,读-写操作并发执行,从而提升系统性能