事务的四个特性
ACID
- 原子性:事务中的操作要么全部成功,要么全部失败。通过 undo log 实现
- 一致性:数据库在事务执行前后都处于一个正确的状态。
- 隔离性:事务执行过程中,不应该收到其他事务的打扰,并发的事务要隔离。通过锁、MVCC实现
- 持久性:事务执行完成之后,数据将永远保存在数据库中,即使出现意外宕机的情况,也不应该对这部分数据造成任何影响。通过 redo log 实现
事务的四个隔离级别
- 读取未提交:事务的修改,即使没有提交,对其他事务也都是可见的。这种现象叫脏读。
- 读取已提交:事务读取已提交的数据,大多数数据库的默认隔离级别。当一个事务在执行过程中,前后读取的信息不一样,这种情况称为不可重复读。
- 可重复读:mysql 的默认隔离级别。它解决了脏读、不可重复读问题、存在幻读问题。幻读:当一个事务A读取某一个范围的数据时,另一个事务B在这个范围插入行,A事务再次读取这个范围的数据时,会产生幻读。
- 可串行化:所有事务依次逐个执行,互相之间没有干扰。
redo log & bin log
数据是存放在磁盘中的,如果每次读写数据都需做磁盘IO操作,并发场景下性能就会很差。为此 Mysql 引入缓存 Buffer Pool 来缓解数据库的磁盘压力。
当从数据库读数据时,首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入缓存;当向数据库写入数据时,先向缓存写入,此时缓存中的数据页数据变更,这个数据页称为脏页;Buffer Pool 中的数据会定期刷到磁盘中,这个过程称为刷脏页。
Mysql 宕机,如果刷脏页还未完成,更新就会丢失,无法保证事务的持久化。
redo log 是属于 InnoDB 的事务日志,它是物理日志,记录的是数据库中每个页的修改,而不是某一行或某几行修改成怎样,可以用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置。
redo log 使用了 write ahead logging 技术,即先写日志,再修改 Buffer Pool 中的数据,至于缓存何时刷盘则有后台线程异步处理了。如果写的顺序反过来,可能存在日志和数据不一致的情况。
bin log 是 Mysql Server 层面的,记录的所有的 DDL、DML 操作,用来备份数据和主备同步数据。想要做到主备数据一致就得保证 redo log 和 binlog 的一致性,因此 redo log 的写入使用两阶段提交,如下图,prepare阶段 redo log 写入磁盘,然后 binlog 写入磁盘,最后事务提交。
在 BC 两个点会出现数据不一致的问题。服务器重启后发现 redo log 中处于 prepare 状态的记录,再根据事务ID 检查 binlog 是否包含此条 redo log 的更新内容,如果不包含,redo log 丢弃此次变更,如果包含,事务会提交。
undo log
属于 InnoDB 的事务日志,属于逻辑日志,其回滚作用,是保障事务原子性的关键。
记录的是数据修改前的状态。
- 比如事务执行一条更新语句时,会先在 undo log 中写入一条相反操作的逻辑日志。
- 同一个事务中对同一条记录的修改不会记录多条日志,undo log 只保存了数据的原始版本。
MVCC
MVCC,即多版本并发控制,在 InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
当前读与快照读:
- 当前读:select for update、update、insert、delete 这些操作都是当前读,什么叫当前读?读取的是记录的最新版本,且会对读取的记录加锁,保证其他并发事务不能修改当前记录
- 快照读:快照读是 Mysql 为 MVCC 实现的一个非阻塞读并发功能,读取的不一定是最新记录,也有可能是某个历史记录。
MVCC解决的问题
数据库并发场景有三种:
- 读读:不存在任何问题,也不需要并发控制
- 读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
- 写写:有线程安全问题,可能存在更新丢失问题
MVCC 解决的问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题(加锁解决)
MVCC的实现原理
MVCC 由三个隐式字段、undo log、read view 三个组件实现。
三个隐式字段:每行记录除了我们自定义的字段外,还有数据库定义的一些隐式字段字段
- DB_TRX_ID:最近修改事务ID,记录创建这条记录或者最后一次修改该记录的事务ID
- DB_ROLL_PTR:回滚指针,指向 undolog 中上一个数据版本
- DB_ROW_JD:隐藏的主键,如果数据表没有主键,那么 innodb 会自动生成一个6字节的row_id
undo log:
undolog 会用链表有序存储一条记录的所有历史版本,链首是最新版本,链尾是最旧版本。
Read View:
Read View 是事务进行快照读的时候生成的视图,在事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。
Read View 的最大作用是用来做可见性判断的,有三个全局属性:
- trx_list:一个数值列表,存储活跃的事务ID
- up_limit_id:记录 trx_list 列表中事务ID最小的ID
- low_limit_id:记录 Read View 生成时刻系统尚未分配的下一个事务ID
具体的比较规则如下:
- 首先比较 DB_TRX_ID < up_limit_id,是:当前事务能看到 DB_TRX_ID 所在的记录,否:进入下一个判断
- 判断 DB_TRX_ID >= low_limit_id,是:代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的,对于当前事务肯定不可见,否:进入下一步判断
- 判断 DB_TRX_ID 是否在活跃事务中,是:当前事务是看不到活跃事务的,否:说明这个事务在 Read View 生成之前就已经提交了,那么修改的结果是能够看见的。
RC、RR级别下快照读有什么不同
- 在 RC 隔离级别下,每个快照读都会生成并获取最新的 Read View
- 在 RR 隔离级别下,同一个事务中的第一个快照读才会创建 Read View,之后的快照读获取的都是同一个 Read View
如何解决幻读问题
- 如果事务中都是用快照读,那么不会产生幻读的问题
- 快照读和当前读一起使用的时候就会产生幻读
- 如果都是当前读的话,通过间隙锁来解决幻读问题