之前学习数据库事务,主要是通过看网上各式各样的博客,建立起对数据库事务的第一印象。后来遇到一些真正好的博客才知道自己原来对数据库事务有很大的误解,而且我一直对这些知识不清楚,这次做一个总结,通过Mysql事务的实现来分析!
基本概念
数据库事务:是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
数据库事务有以下四个特性:
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
- 一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束
- 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性:已被提交的事务对数据库的修改应该永久保存在数据库中
数据库通过原子性(A)、隔离性(I)、持久性(D)来保证一致性(C)。其中一致性是目的,原子性、隔离性、持久性是手段。因此数据库必须实现AID三大特性才有可能实现一致性。
下面就分别介绍MySQL数据库如何保证这三个性质。
原子性
原子性主要是通过undo log(回滚日志)来实现的。undo log 主要记录了数据的逻辑变化,比如一条INSERT 语句,对应着有一条的DELETE的undo log。
注意:undo log是innodb引擎的日志
undo log作用:
- 回滚数据:当数据发生异常错误时,根据执行 undo log 就可以回滚到事务之前的数据状态,保证原子性,要么全部成功,要么全部失败
- MVCC 一致性视图:通过 undo log 找到对应的数据版本号,是保证 MVCC 视图的一致性的必要条件
持久性
持久性主要是通过redo log来实现的。
Mysql修改数据的大概流程是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。直观的解决方法就是事务提交前直接把数据写入磁盘,但是数据页刷盘效率太低。
用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
使用redo log的优点是redo log进行刷盘的效率要远高于数据页刷盘。
注意:redo log也是innodb的引擎日志
binlogo
既然把undo log和redo log都介绍了,再来介绍这个在MySQL中也很常用的日志吧!
记录了对Mysql数据库执行更改的所有操作,但是不包括SELECT 和 SHOW这类操作。以二进制的形式保存在磁盘中。是属于Mysql Server层记录,任何存储引擎的 mysql 数据库都会记录binlog日志。记录的是逻辑日志(可以简单理解为记录的就是sql语句,而redo log记录的是数据页的变更)
binlog 的主要使用场景有两个,分别是 主从复制 和 数据恢复。
- 主从复制:在 Master 端开启 binlog,然后将 binlog 推送到各个 Slave 从端,Slave 端重放 binlog 从而达到主从数据一致
- 数据恢复:通过使用 mysqlbinlog 工具来恢复数据
提问1: 为啥 Binlog 没有 crash-safe 功能?
redo log 和 binlog 有一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量日志。
当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innodb 判断哪些数据已经刷盘,哪些数据还没有
举个例子, binlog 记录了两条日志:
-
给 ID = 2 这一行的 c 字段加1
-
给 ID = 2 这一行的 c 字段加2
在记录1刷盘后,记录2未刷盘时,数据库 crash。重启后,只通过 binlog 数据库无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘,不管是两条都恢复至内存,还是都不恢复,对 ID=2 这行数据来说,都不对
但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中抹掉,数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。这就是为什么 redo log 具有 crash-safe 的能力,而 binlog 不具备
提问2: 保证 crash-safe 为啥要用两个日记,不能用一个日记吗(Redo log 或 Binglog)?
针对问题1我们知道只有 binlog 日志,没有 redo log 是不能做到故障恢复的。那么针对只有 redo log日志,没有 binlog 日志,这也是不行的,因为 redo log 是 innodb 持有的,且日志上的记录落盘后会被抹掉。因此需要 binlog 和 redo log 两者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。
隔离性
对于MySQL数据库,隔离性的实现主要是利用锁和MVCC机制。下面以四个隔离级别来分别介绍MySQL的实现原理
基本概念
- 共享锁:也叫做读锁,加了此锁的记录还可以再加共享锁来读数据,不可以加排他锁来修改数据
- 排他锁:也叫做写锁,加了此锁的记录不可以加锁了
- 记录锁:锁住某行记录
- 间隙锁:当用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,并对键值在条件范围内但并不存在的记录(间隙GAP)也会被加锁,这种锁机制就是间隙锁
- next-key锁:记录锁+间隙锁
读未提交
这个是隔离级别最低的,事务A会读到事务B未提交的数据,产生脏读现象,什么锁都不加,也不用mvcc机制。
串行化
最高的隔离级别,不会出现什么并发问题。完全是基于锁的并发控制。读操作加共享锁,写操作加排他锁,并发度非常低
读已提交
这个是基于mvcc和锁机制的
MVCC(Multi-Version Concurrency Control)
多版本的并发控制协议,最大的好处就是读不加锁,解决了读写冲突的问题。上面也说了,mvcc主要是依靠一致性视图(ReadView)来实现的。下面有些概念要解释:
- 快照读:简单的select操作,属于快照读,不加锁。eg:select * from table where ?;
- 当前读:每次读取最新的数据。插入/更新/删除操作,属于当前读,需要加锁。sql语句后面使用了lock in share mode和for update的查询语句也属于当前读
- readView:一致性视图,相当于一个快照。RC和RR模式下,readview的生成时间点不一样。RR模式下,只有事务的第一次读操作才会生成readview,而RC模式下,事务的每个读操作都会生成readview。
Read view 的几个重要属性
trx_ids: 当前系统活跃(未提交)事务版本号集合。
low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
creator_trx_id: 创建当前read view的事务版本号;
匹配条件:
1.数据事务ID <up_limit_id 则显示
2.数据事务ID>=low_limit_id 则不显示
3.up_limit_id <=数据事务ID<low_limit_id 则与活跃事务集合trx_ids里匹配
3.1 情况1: 如果事务ID不存在于trx_ids 集合(则说明read view产生的时候事务已经commit了),这种情况数据则可以显示。
3.2 情况2: 如果事务ID存在trx_ids则说明read view产生的时候数据还没有提交,但是如果数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。
3.3 情况3: 如果事务ID既存在trx_ids而且又不等于creator_trx_id那就说明read view产生的时候数据还没有提交,又不是自己生成的,所以这种情况下此数据不能显示。
4不满足read view条件时候,从undo log里面获取历史数据。然后数据历史版本事务号回头再来和read view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;
这个隔离级别会出现不可重复读和幻读问题。
- 不可重复读:多次读取同一行数据的结果不一样,就是读到其他事务已经提交的数据
- 幻读:两个完全相同的查询语句执行得到不同的结果集,一般指范围查询
对于修改和主动加锁查询操作,主要是通过加记录锁的方式来实现。
对于普通查询操作,主要是是通过快照读,不需要加锁,快照读读的是readview,在读已提交隔离级别下,事务中的每次读操作都会生成一个新的ReadView
可重复读
这是MySQL默认的隔离级别。按照数据库定义,此隔离级别下是会存在幻读现象的。但是MySQL通过next-key锁的方式基本解决了幻读问题。
对于普通查询操作,也是通过快照读的方式,readview生成的时间是在事务第一次读操作。
对于其他情况,该隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁)