MySQL (四)事务
MySQL事务的四⼤特性
事务是由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的。
不过并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,也正是这样,所以大多数 MySQL 的引擎都是用 InnoDB。
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。由undo log日志保证
- 事务的原子性是通过undo log实现的,在事务还没提交前,历史数据会记录在undo log中,如果事务执行过程中,出现了错误或者用户执行了ROLLBACK语句,MySQL可以利用undo log中的历史数据,将数据恢复到事务开始之前的状态,从而保证了事务的原子性。
一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。通过持久性+原子性+隔离性来保证
- 比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的
- 可重复读隔离级别下的快照读(普通select),是通过MVCC来保证事务隔离性的,当前读(update、select.. for update)是通过行级锁来保证事务隔离性的。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。持久性是通过 redo log 来保证的
- 事务的持久性是由redo log保证的,因为MySQL通过WAL(先写日志再写数据)机制,在修改数据的时候,会将本次对数据页的修改以redo log的形式记录下来,这个时候更新就算完成了,Buffer Pool的脏页会通过后台线程刷盘,即使在脏页还没刷盘的时候发生了数据库重启,由于修改操作都记录到了redo log,之前已提交的记录都不会丢失,重启后就通过redo log,恢复脏页数据,从而保证了事务的特久性。
MySQL事务和Redis事务有什么区别
MySQL事务能够实现ACID四大特性,而Redis事务没保证原子性和持久性。
Redis事务没有回滚功能,没办法实现跟MySQL事务一样的原子性,就是没办法保证事务执行期间,要不全部失败,要不全部成功,Redis事务执行过程中,如果中途有命令执行出错了,不会停止和回滚,而是继续执行,那么就可能出现半成功的状态。
Redis不管是AOF模式,还是RDB快照,都没办法保证数据不丢失,所以Redis事务不具有持久性。
事务的隔离级别,分别解决什么问题
数据库事务的隔离级别有4种,由低到高分别为Read Uncommited、Read Commited、Repeatable Read、Serializable。并发数据访问时可能会出现以下问题,3类数据读取问题(脏读、不可重复读、幻读)。
- Read Uncommited,读未提交,即一个事务可以读取另一个未提交事务的数据;并发操作会导致脏读
- Read Commited,读已提交,即一个事务要等到另一个事务提交后才能读取数据;解决脏读问题;并发操作会导致不可重复读(对应更新操作)
- Repeatable Read,重复读,即开始读取数据(事务开启)时,不再允许修改操作;解决不可重复读问题;并发操作会导致幻读(对应insert操作)
- Serializable,序列化,最高的事务隔离级别,该级别下,事务串行化顺序执行;避免脏读、不可重复读与幻读;但是该级别效率低下,比较消耗数据库性能,一般不用。
并发问题
- 脏读:一个事务读取另一个未提交的数据。
- 不可重复读:一个事务范围内两个相同的查询却返回了不同数据。(对应的是更新操作)
幻读:一个事务范围内两个相同的查询却返回了不同数据。(对应的是插入操作)
脏读 | 不可重复读 | 幻读 | 备注 | |
---|---|---|---|---|
Read Uncommited | ||||
Read Commited | √ | 读取事务要等到这个更新操作事务提交后才能读取数据,可以解决脏读问题。(大多数数据库默认的隔离级别) | ||
Repeatable Read | √ | √ | 开始读取数据(事务开始)时,不允许修改操作(即update操作)。(MySQL的默认隔离级别) | |
Serializable | √ | √ | √ | 以上并发问题都不存在,但是效率低下,一般不用 |
对应Update操作 | 对应insert操作 |
这四种隔离级别具体是如何实现的呢?
- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 MVCC 来实现的,
- 在MVCC实现中,每条记录都会保存多个版本,每个版本都有一个版本号,事务在读取数据时,会根据事务开始时的版本号来读取数据,从而保证了事务的隔离性。可重复读隔离级别是在启动事务后,生成一个Read View,后续事务查询数据的时候都在复用Read View,所以保证了事务期间多次读到的数据都是一致的。
- 「读提交」和「可重复读」它们的区别在于创建 Read View 的时机不同:
- 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,所以每次select都能看到其他事务最近提交的数据。
- 而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View,所以一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的。
注意,执行「开始事务」命令,并不意味着启动了事务。在 MySQL 有两种开启事务的命令,分别是:
- 第一种:begin/start transaction 命令;
- 第二种:start transaction with consistent snapshot 命令;
这两种开启事务的命令,事务的启动时机是不同的:
- 执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了第一条 select 语句,才是事务真正启动的时机;
- 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。
介绍一下MVCC
MVCC是多版本并发控制,是通过记录历史版本数据,解决读写并发冲突问题,避免了读数据时加锁,提高了事务的并发性能。
MySQL将历史数据存储在undo log中,结构逻辑上类似一个链表,MySQL数据行上有两个隐藏列,一个是事务ID,一个就是指向undo log(旧版本记录)的指针。
事务开启后,执行第一条select语句的时候(启动事务),会创建ReadView,ReadView记录了当前未提交的事务,通过与历史数据的事务ID比较,就可以根据可见性规则进行判断,判断这条记录是否可见,如果可见就直接将这个数据返回给客客户端,如果不可见就继续往undo log版本链查找第一个可见的数据。需要展开说说可见性规则吗?
MVCC的如何判断行记录对某一个事务是否可见
我们每一条记录都有两个隐藏列,一个是事务id,一个是指向历史数据undo log的指针,然后Read View有四个字段,分别是创建Read View的事务id、活跃事务id列表、活跃事务id列表中最小的id、下一个事务的id。
主要有这几种判断规则:
- 如果记录的事务id小于活跃事务id列表中最小的id,就说明该记录是在创建Read View前就生成好了,所以记录是当前事务是可见的
- 如果记录的事务id大于下一个事务的id,就说明该记录是在创建Read View后才生成的,所以该记录是当前事务是不可见的
- 如果记录的事务id在最小的id和下一个事务的id之间,这时候就需要看记录的事务id是否在活跃事务id列表中:
- 如果记的事务id在活跃事务id列表中,说明修改该记录的事务还没提交,所以该记录是不可见的
- 如果记录的事务id不在活跃事务id列表中,说明修改该记录的事务已经提交了,那么该记录就是可见
为什么大多用读已提交隔离级别
读已提交的并发性能更好,因为读已提交没有间隙锁,只有记录锁,发生死锁的概率比较低。然后互联网业务对于幻读和不可重复读的问题都是能接受的,所以为了降低死锁的概率,提高事务的并发性能,都会选择使用读已提交隔离级别。
可重复读隔离级别是如何解决不可重复读的
MySQL提供了两种查询方式,一种是快照读,就是普通select语句,另外一种是当前读,比如select for update语句。不同的查询方式,解决不可重复读问题的方式是不一样的。
- 针对快照读的话,是通过MVCC机制来解决的,在可重复读隔离级别下,第一次select查询的时候,会生成readview,在第二次执行selecti查询的时候,会复用这个readview,这样前后两次查间的记录都是一样的,不会读到其他事务更新的操作,这样就不会发生不可重复读的问题了。
- 针对当前读的话,是靠行级锁中的记录锁来实现的,在可重复读隔离级别下,第一次select for update语句查询的时候,会对记录加next-key锁(间隙锁+记录锁),这个锁包含记录锁,这时候如果其他事务更新了加了锁的记录,都会被阻塞住,这样就不会发生不可重复读的问题了。
可重复读隔离级别是怎么解决幻读的
MySQL提供了两种查询方式,一种是快照读,就是普通select语句,另外一种是当前读,比如select for update语句。不同的查询方式,解决不可重复读问题的方式是不一样的。
- 针对快照读的话,是通过MVCC机制来解决的,在可重复读隔离级别下,第一次select查询的时候,会生成readview,在第二次执行select查询的时候,会复用这个readview,这样前后两次查间的记录都是一样的,不会读到其他事务插入的记录,这样就不会发生幻读的问题了。
- 针对当前读的话,是靠行级锁中的间隙锁来实现的,在可重复读隔离级别下,第一次select for update语句查询的时候,会对记录加next-key锁(间隙锁+记录锁),这时候如果其他事务往这个间隙插入新记录的话,都会被阻塞住,这样就不会发生幻读的问题了。
可重复读隔离级别解决了什么问题?有没有完全解决幻读
可重复读隔离级别解决了脏读、不可重复读问题,幻读也很大程度上避免了,但是我觉得并没有完全解决幻读,在些特殊的场景,还是会发生幻读的问题。
可重复读隔离级别为什么不能完全避免幻读?什么情况下出现幻读
事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,但没有id=5的记录,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
先快照读,再当前读
- T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
- T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
- T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
上面这两种发生幻读的场景,也是可以避免的,就是尽量在开启事务之后,马上执行select..for update语句,因为它会对记录加临键锁(next-key锁),这样就可以避免其他事务插入一条新记录,就避免了幻读的问题。
可重复读隔离级别,MVCC完全解决了不可重复读问题吗?
没有
比如表里现在有id=1,value=1的记录。
- 事务a,先执行select,查询到id=1的value是1
- 事务b,更新id=1的value为2,然后提交事务。
- 事务a,执行select for update,当前读,然后就读到id=1,,value=2的记录了,意味着发生了不可重复读。
如果前后两次查询都是快照读,就是普通的select的话,那就不会产生不可重复读的问题的。但是如果第一次查询是快照读,第二次查询是当前读,那么就可能会发生不可重复读的问题。