1 事务提供的安全保证
1.1 ACID特性
为保证事务(transaction)是正确可靠的,数据库引擎必须具备的四个特性:
原子性(Atomicity)
事务内的一系列操作应该是否一个整体单元,不存在中间态,或者说中间态对外不可见。
一致性(Consistency)
在ACID理论中,一致性主要是指对数据有特定的预期状态,任何数据更改必须满足这些状态约束(或者恒等条件)。但是,一致性并不是数据库自身的特性,而是通过满足原子性和隔离性而达到的效果。
隔离性(Isolation)
指代并发事务相互执行相互隔离,互不影响。
持久性(Durability)
数据库中,事务提交成功后,即使发生任何崩溃,也不能影响事务中的写入变动。
1.2 InnoDB如何保证ACID
InnoDB分别使用如下方式来保证了这四个特性:
- redo log重做日志用来保证事务的持久性
- undo log回滚日志保证事务的原子性
- undo log + redo log保证事务的一致性
- 锁(共享、排他)用来保证事务的隔离性
2 事务隔离级别
2.1 Read uncommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
这就可能发生脏读的问题,即A事务中读取到了B事务中未提交的数据,但是B事务进行了回滚,A事务中这时候就持有了并不存在的脏数据。
2.2 Read committed
读提交,或者叫读已提交,就是一个事务要等另一个事务提交后才能读取或者写入数据。
Mysql通过行级锁来防止脏写的发生(同时只能有一个写),用多版本控制来防止脏读(事务更新使用新值,其他事务读取扔读旧值)。
- A事务中读取当前钱包余额为1块钱,并把它增加到2块钱,当事务并未提交
- 这时B事务开始执行,将余额从1块钱修改为3块钱,并成功提交事务,当前数据为3块钱
- A事务中再次读取,发现余额从1块钱变为3块钱
这就是“不可重复读”问题。也就是说,一次事务中两次读取相同的数据却发现数据不一致了。
2.3 Repeatable read(Mysql默认)
可重复读,就是在开始读取数据(事务开启)时,不再允许修改操作,但允许写操作。
但是仍旧无法避免幻读问题。
什么是幻读?
一个事务里,多次查询的结果集个数不一致,称为幻读。
举个例子,假设数据库中订单表数据如下:
id | 订单号 | 金额(元) |
---|---|---|
3 | NO101 | 10.0 |
5 | NO102 | 20.0 |
6 | NO103 | 17.0 |
现在开启事务A,把id大于等于3且小于等于5的订单数据的支付金额进行求和,即30元。
但事务A尚未提交时,事务B在数据库中插入了一条id为4的数据:
id | 订单号 | 金额(元) |
---|---|---|
4 | NO104 | 5.0 |
那么当事务A再次查询发现此时订单表数据id在[3, 5]区间内的总额为35元。
Mysql彻底解决幻读了吗?
答案是没有!
InnoDB通过MVCC和Next-key Lock解决了部分幻读问题。
之所以说是“部分”,是因为GAP锁的作用前提是locking reads,快照读是不会触发GAP锁的,而每次当前读都会生成一个新的“读视图”,那么一个事务中,如果先进行快照读,再进行当前读,那么是会发生幻读的。
假设目前表中数据如下:
id | Name | Age |
---|---|---|
1 | 张三 | 5 |
2 | 李四 | 6 |
3 | 王五 | 7 |
事务1 | 事务2 |
---|---|
begin | |
select * from students where age > 6 (查询结果只有王五) |
|
begin | |
insert into students (age, name) values(8, ‘赵六’) | |
commit | |
select * from students where age > 6 for update (查询结果有王五和赵六两条数据,发生了幻读) |
|
commit |
结果就是,当前读由最新的数据创建了新的读视图,所以和原快照读的结果不一致。
2.4 Serializable 序列化
Serializable也被称为串行化,是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
序列化的问题
序列化最大问题就是性能较差。
数据库只有一种被广泛使用的串行化算法,那就是两阶段(2PL)加锁:事务执行前要获取锁,执行结束后要释放锁。
在2PL下,读取操作必须先获取读锁(共享锁),更新操作必须获取写锁(独占锁)。一个对象被独占锁占用时,其他事务必须等待。
这样的规则下,不仅加解锁消耗非常大,还降低了事务的并发性。
过多的使用锁,增加了死锁的概率,数据库检测到死锁会破坏死锁条件,即中止其中一个事务,那么则需要应用层对这个事务进行重试。
3 binlog、redo log和undo log的具体作用
3.1 binlog
binlog是Mysql Server层级的逻辑变更日志,它记录了数据库的所有变更,以二进制存储在磁盘上。
主要作用在两个方面:
-
增量备份
-
主从复制
3.2 redo log(持久性保证)
redo log是InnoDB存储引擎层的物理日志,记录的是数据具体做了什么修改,或者新数据的备份。
在事务提交前将redo log持久化。具体顺序如下:
- 写入redo log,暂不提交
- 写入binlog
- 提交redo log
数据写入时,实际上是写入了内存中的Buffer Pool,Buffer Pool中的数据根据策略定期写入磁盘。那么就带来一个问题,如果在写入磁盘前系统崩溃了,数据一致性难以保证,所以在事务完成前会写入redo log,系统崩溃重启后,通过redo log来恢复未持久化到页中的数据。
这样还能使写入变为一个异步操作,那么在写入磁盘前,数据库引擎还能对多次同一个键的修改进行合并,降低磁盘写入内容和次数。
3.3 undo log(一致性保证)
事务中的每一次修改,innodb 都会先记录对应的 undo log 记录。与 redo log 用于数据的灾后重新提交不同,undo log 主要用于数据修改的回滚。
与 redo log 记录的是物理页的修改不同,undo log 记录是数据修改前的快照。
在事务中每次更新记录后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被数据的roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。
当事务回滚时,通过undo log版本链的顺序对数据反向更新。这条版本链是MVCC机制的基础。
4 InnoDB保证事务隔离性的方式 - MVCC(多版本并发控制)
多版本的意思就是一条数据在数据库中同时存在多个版本,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事务隔离级别去判断读取哪个版本的数据。
隐藏属性 | 是否必须 | 描述 |
---|---|---|
row_id | 否 | 行ID,唯一标识一条记录(如果定义主键,则没有) |
transaction_id | 是 | 事务ID |
roll_pointer | 是 | DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本,形成版本链 |
MVCC机制下,每个事务都会生成自己的ReadView(后面简称RV),RV包含四个属性:
- M_ids:创建该Readview时,活跃的事务id集合。
- Min_trx_id:创建该readview时,最小的事务id,也即M_ids中的最小值。
- Max_trx_id:要分配给下一个事务的id。不是当前的最大事务ID,应该是当前的最大事务(不一定活跃,可能已经结束)id+1
- Creator_trx_id:创建该readview的事务id,也即该readview的拥有者。
查询数据时,会比较数据的版本号(trx_id)和该事务自身RV中的版本号:
- 如果 trx_id == Creator_trx_id,说明数据是自己修改的,那么可以读取。
- 如果 trx_id < min_trx_id,说明说明该记录已经在生成readview之前被提交了,可以读取。
- 如果min_trx_id < trx_id < max_trx_id,这时需要在m_ids中查找,如果trx_id在m_ids中,如果不活跃了表示事务已提交可访问,如果还在表示事务还在活跃尚未提交不可访问。这个过程就是遍历版本链,一个一个找,看哪个能访问,如果一个都找不到,那么查询结果里这条记录就不可见。
- 如果 trx_id >= max_trx_id,说明数据版本号高于当前事务,对当前事务来说属于“未来的数据”,所以不能读取。
5 事务持久化策略
5.1 steal / no-steal
是否允许一个uncommitted的事务将修改更新到磁盘
-
steal策略
那么此时磁盘上就可能包含uncommitted的数据,因此系统需要记录undo log,以防事务abort时进行回滚(roll-back)
-
no steal策略
就表示磁盘上不会存在uncommitted数据,因此无需回滚操作,也就无需记录undo log
5.2 force / no-force
事务在committed之后是否将所有更新立刻持久化到磁盘
-
force策略
在committed之后必须将所有更新立刻持久化到磁盘
-
no-force策略
在committed之后可以不立即持久化到磁盘,而是缓存更新批量持久化到磁盘
no-force的优点是提升顺序写,缺点是有可能在crash后导致事务数据丢失,所以需要记录redo log,系统重启后进行前滚(roll-forward)操作
现在DBMS常用的是steal/no-force策略,即无论事务提交与否,都不强制将数据写入磁盘。因此一般都需要记录redo log和undo log。这样可以获得较快的运行时性能,代价就是在数据库恢复(recovery)的时候需要做很多的事情,增大了系统重启的时间。