一,事务
1,什么是事务
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
Mysql数据库中,存储引擎是InnoDB时,支持事务,MyISAM存储引擎不支持事务。
2,事务四大特性 ACID
1、原子性(Atomicity):一个事务开启后,事务中的全部操作是不可分割的,要么全部完成,要么全部不执行。如果事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。
2、一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果相一致。
3、隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
4、持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
二,事务的隔离级别
1,脏读,不可重复读,幻读
了解事务隔离级别之前,首先要了解什么是数据的 脏读,不可重复读,幻读 问题。
脏读:A事务读到B事务未提交的数据。一旦B事务回滚,那么A事务读到的数据就是脏数据,所以称为 "脏读"。
不可重复读:A事务执行第一次查询之后,B事务修改(update)了这部分数据并提交,A事务执行第二次查询,发现同一事物内对相同数据的前后两次查询数据不一致,称为 "不可重复读"。
幻读:A事务执行第一次查询之后,B事务对数据进行了增删(insert和delete)操作并提交,A事务执行第二次查询,发现同一事务内前后两次查询结果件数不一致,A事务仿佛出现了幻觉一样,称为 "幻读"。
简单来说,脏读就是读到其他事务未提交的数据,不可重复读就是其他事务对当前事务读取的数据进行了update,幻读就是其他事务往数据库 insert 或 delete 了满足当前事务查询条件的数据。
2,事务隔离级别
mysql对应解决以上 脏读,不可重复读,幻读 问题,有四种隔离级别,从低到高依次是,
Read uncommitted(读未提交) 、Read committed (读已提交 RC)、Repeatable read (可重复读 RR)、Serializable(串行化)。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read uncommitted(读未提交) | ✓ | ✓ | ✓ |
Read committed (读已提交) | ✕ | ✓ | ✓ |
Repeatable read (可重复读) | ✕ | ✕ | ✓ |
Serializable(串行化) | ✕ | ✕ | ✕ |
✕:不会出现;✓:可能会出现
Mysql默认使用的事务隔离级别是Repeatable read (可重复读 RR ),而Oracle,Sql Server 默认的是 Read committed(读已提交)。
脏读是读到其他事务未提交的数据嘛,所以使用RC隔离级别就可以避免脏读问题,这个好理解。
那么,RR下又是如何解决不可重复读的问题呢?
而且,从上表来看,RR隔离级别下,并没办法解决幻读的问题,Mysql选择RR为默认的隔离级别,又是如何解决幻读的问题呢?
三,MVCC
1,当前读和快照读
当前读:select lock in share mode (共享锁
), select for update; update; insert; delete (排他锁
)这些操作都是一种当前读,这些操作读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读(非阻塞读):不加锁的 select 操作就是快照读,即不加锁的非阻塞读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销。在RR隔离级别下,事务首次调用快照读的地方很关键,创建快照的时机决定了数据的版本。
RC与RR隔离级别下,InnoDB快照读的实现原理:
1,数据行里的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID隐式字段
- DB_TRX_ID: 记录最近创建或者更新这条行记录的事务 ID,大小为6个字节
- DB_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为7个字节,InnoDB便是通过这个指针找到之前版本的数据。
- DB_ROW_ID:行标识(单调自增ID),大小为6字节,如果表没有主键,InnoDB会自动生成一个隐藏主键,因此会出现这个列。
- deleted_bit :删除flg,一条数据被更新或者删除时,优先更新该数据记录删除flg为true,而不是真正删除数据。
2,undolog
- insert undo log:事务insert新数据时产生的log,只在事务回滚时需要,在事务提交后可立即丢弃。
- update undo log:事务对数据进行delete或者update参生的log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除,只有在快照读或者事务回滚不涉及该日志时,才会删除改日志记录,mysql的innoDB有专门的purge线程来删除上面提到删除flg为true的数据记录。
通过以上的隐式字段以及undolog,事务更新数据时,流程大致如下
- 数据库对被修改行加排他锁;
- 先将修改行数据拷贝至undolog,即旧记录备份在undolog,这也是undolog被叫做回滚日志的原因;
- 旧记录备份完成,修改数据行,DB_TRX_ID事务ID递增,DB_ROLL_PTR回滚指针指向旧记录备份地址;
- 事务提交,排他锁释放;
3,ReadView
ReadView就是事务进行快照读时产生的读视图,某一事务进行快照读时,对该记录创建一个ReadView,然后ReadView遵循一个可见性算法,会先取出更新对象数据的最新记录中的DB_TRX_ID,与数据库中当前活跃事务的DB_TRX_ID去比较,如果修改对象的事务ID大于等于活跃事务ID,则通过上面提及的DB_ROLL_PTR去取出undolog中上一层的DB_TRX_ID,直到取出的DB_TRX_ID小于当前活跃事务的DB_TRX_ID,那么这个DB_TRX_ID所在的记录就是当前事务能看到的最稳点的数据版本,可能是最新的数据,也可能旧版本的数据。
Mysql源码实现ReadView的一些关键属性,如下图
RC与RR隔离级别下,InnoDB快照读的区别:
RC隔离级别下,每一次快照读都会生成一个ReadView,即能看到最新的数据,
而RR下,一个事务的第一次快照读才会生成一个ReadView,后续快照读都是读取的同一个ReadView,所以RR下的快照读读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
所以,至此,RR下又是如何解决不可重复读,这个问题可以总结回答为,RR通过 快照读 + ReadView ,保证在同一个事务的快照读都读取的是同一个ReadView,即同一事务的多次快照读读到的数据一致,从而解决了不可重复读。
2,Next-Key锁(行锁+gap锁)
- Record Lock 行锁:对索引记录的锁,如果一个表没有定义索引,
InnoDB
创建一个隐藏的聚集索引(DB_ROW_ID)并使用该索引进行记录锁定。 - Gap Lock 间隙锁:间隙锁是对索引记录之间的间隙的锁,或者是对第一个索引记录之前或最后一个索引记录之后的间隙的锁。防止同一事务两次当前读出现幻读的情况,只有在RR以及串行化隔离级别下会有Gap锁。
- Next-Key锁:next-key 锁是索引记录上的行锁和索引记录之前的间隙上的间隙锁的组合。
Gap锁会用在非唯一索引或者不走索引的当前读中
- 如果where条件全部命中,则不会用Gap锁,只会加行锁。
- 如果where条件部分命中或者全都不命中,则会加Gap锁。
关于Gap锁上锁的范围,这里直接贴上Mysql官方文档的例子 ,传送门: Next-Key 锁
在一个事务已经对某些区间加上Gap锁的情况下,其他事务就无法往数据库插入或者删除这些区间的数据,这就一定程度上防止了幻读的产生。
四,模拟
1,模拟表结构
默认事务隔离级别是RR
2, 读未提交隔离级别 模拟 脏读
3,读已经提交隔离级别 模拟 不发生脏读
4,读已经提交隔离级别 模拟 不可重复读
初始状态 money = 800 ,事务2 update money为1000 ,事务1同一事物内前后两次查询money不一致
5,可重复读隔离级别 模拟 不发生不可重复读
步骤和上次 读已经提交隔离级别模拟 不可重复读 几乎一样,只不过一开始将事务隔离级别指定为 RR (REPEATABLE READ),所以就不贴分步的图了,贴个结果吧。
6,可重复读隔离级别模拟 不发生幻读
表结构如下
-- ----------------------------
-- Table structure for test_user
-- ----------------------------
DROP TABLE IF EXISTS `test_user`;
CREATE TABLE `test_user` (
`id` int(0) NOT NULL,
`name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
UNIQUE INDEX `index_id`(`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
初始数据
1, 如果where条件全部命中,则不会用Gap锁。
2,如果where条件部分命中或者全都不命中,则会加Gap锁。
全都不命中
部分命中