在MySQL的众多存储引擎中,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。
在MySQL中,默认的隔离级别是REPEATABLE-READ(可重复读),mysql的默认隔离级别解决了脏读、幻读、不可重复读问题
对于具体隔离级别的选择可以通过命令设置:
数据库查询隔离级别: select @@tx_isolation;
数据库设置隔离级别:set global transaction isolation level 级别字符串;
文章目录
- 一、数据库的隔离级别
- 二、MVCC
- 1. 增删改查
- 2. 快照读和当前读
- 三、一致性非锁定读和锁定读
- 四、悲观锁和乐观锁
- 五、锁
- 六、理论分析
- 七、实验
一、数据库的隔离级别
(1)未提交读(Read Uncommitted)
A事务只要修改了数据,无论有没有提交,其他事务都能够读取到A事务修改后的结果。
潜在问题【对于同一记录进行修改操作】:A事务修改之后未提交,B事务读取到修改之后的数据,然后在这个基础上进行操作修改并提交。然后A提交失败,数据回滚,则数据就出现异常。这就是所谓的脏读。
解决方案:A事务提交之后B事务才能读取到A修改之后的数据,不让读取修改但未提交的数据
(2)提交读(Read Committed)
A事务成功commit之后,其他事务才能够读取到A提交事务之后的最新数据,否则只能读取到A提交之前的原始数据。这样就解决了脏读问题。
潜在问题【对于同一记录进行修改操作】:A事务开启第一次读取数据,然后B事务开启对数据进行了修改并提交成功, 此时A事务还没有结束,又进行了一次读操作,然后便发现A事务中的两次读取的数据不一致。这就是所谓的不可重复读。同一个事务中两次读取的数据不一样。
解决方案:在一个事务对该行数据进行操作的时候,其他事务不允许进行修改。可以理解为添加一个行级别的锁
(3)可重复读(Repeatable Read):
事务启动的时候就 “拍了个快照”,这样之后的获取数据都是根据这个快照来获取。但是也存在一个问题,就是无法看到其他事务之后对已有记录的更新。对于此隔离机制无法解决这个问题,需要使用行级锁来进行处理。
具体的实现是通过MVCC(多版本并发控制),为每行记录添加了一个版本号,每当修改数据时,版本号加一。在读取事务时,系统会给事务一个当前版本号,事务会读取版本号小于等于当前版本号的数据,这时就算另一个事务插入一个数据,并立马提交,新插入的这条数据的版本号会比读取事务的版本号高,因此读取事务读的数据还是不会变。
潜在问题【对于同表进行插入或删除操作】:A事务插入/删除了一条记录,但是B事务中却查询不到插入的新记录/依旧能查到删除的记录。这就是所谓的幻读。幻读是对于记录条数而言的,不是针对于某一条记录的数据而言的。这里需要注意!
解决方案:在有事务对该表进行操作的时候,不允许其他事务操作该表。添加表级锁。
(4)串行读(Serializable):
可以理解为:所有的事务串行操作该数据表。这样就可以解决幻读操作。
潜在问题:数据安全了 但是操作效率降低了。
二、MVCC
MVCC的全称是“多版本并发控制”。
这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证,换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值
这是一个可以用来增强并发性的强大的技术,因为这样的一来的话查询就不用等待另一个事务释放锁。这项技术在数据库领域并不是普遍使用的。一些其它的数据库产品,以及mysql其它的存储引擎并不支持它。
网上看到大量的文章讲到MVCC都是说给每一行增加两个隐藏的字段分别表示行的创建时间以及过期时间,它们存储的并不是时间,而是事务版本号。
事实上,这种说法并不准确,严格的来讲,InnoDB会给数据库中的每一行增加三个字段,它们分别是DB_TRX_ID
、DB_ROLL_PTR
、DB_ROW_ID
。
但是,为了理解的方便,我们可以这样去理解,索引接下来的讲解中也还是用这两个字段的方式去理解。
1. 增删改查
在InnoDB中,给每行增加两个隐藏字段来实现 MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。
在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。
于是乎,默认的隔离级别(REPEATABLE READ)下,增删查改变成了这样:
- SELECT:读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。
- INSERT:将当前事务的版本号保存至行的创建版本号
- UPDATE:新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号
- DELETE:将当前事务的版本号保存至行的删除版本号
2. 快照读和当前读
(1)快照读
select 操作是快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。
快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert 了一条数据然后 commit,这时候 A 执行 select,那么返回的数据中就会有 B 添加的那条数据。
之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。
(2)当前读
UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE
是当前读。
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。
在执行这几个操作时会读取最新的版本号记录,写操作后把版本号改为了当前事务的版本号,所以即使是别的事务提交的数据也可以查询到。
假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。
这里的幻读指的是,A事务插入一条数据,B事务无法读取到,或者A事务删除一条数据,B事务依然能访问到,解决办法就是加表级别的锁,A事务和B事务操作的不是同一个行记录
因为A事务操作行记录如果是通过唯一索引操作的行记录则加的是记录锁,相当于只是加了行级锁,B事务无法操作该行记录但是可以操作其他行记录
对于之前举的例子,A事务将所有记录中的1改为2,会加上间隙锁,相当于表级别的锁,此时B事务将无法修改操作这个表
三、一致性非锁定读和锁定读
(1)锁定读
在一个事务中,标准的SELECT语句是不会加锁,但是有两种情况例外。SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
:给记录假设共享锁,这样一来的话,其它事务只能读不能修改,直到当前事务提交
SELECT ... FOR UPDATE
:给索引记录加锁,这种情况下跟UPDATE的加锁情况是一样的
(2)一致性非锁定读
consistent read (一致性读),InnoDB用多版本来提供查询数据库在某个时间点的快照。
如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一个这样的读读到的快照;
如果是READ COMMITTED,那么一个事务中的每一个一致性读都会读到它自己刷新的快照版本。
Consistent read(一致性读)是READ COMMITTED和REPEATABLE READ隔离级别下普通SELECT语句默认的模式。
一致性读不会给它所访问的表加任何形式的锁,因此其它事务可以同时并发的修改它们。
四、悲观锁和乐观锁
悲观锁,正如它的名字那样,数据库总是认为别人会去修改它所要操作的数据,因此在数据库处理过程中将数据加锁。其实现依靠数据库底层。
乐观锁,如它的名字那样,总是认为别人不会去修改,只有在提交更新的时候去检查数据的状态。通常是给数据增加一个字段来标识数据的版本。
五、锁
有这样三种锁我们需要了解
(1)Record Locks(记录锁):在索引记录上加锁
也叫作行锁,就是对表中的记录加锁
(2)Gap Locks(间隙锁):在索引记录之间加锁,或者在第一个索引记录之前加锁,或者在最后一个索引记录之后加锁。
比如:小王、小李和小刘依次站成一排,此时如果想要新来的小张不能站在小李旁边,就可以在小王和小李之间和小李和小刘之间都加上间隙锁,这样小张便无法站到小李的旁边
这里的小王、小李、小刘便是数据库中的一条条记录,它们之间的空隙也就是间隙,封锁它们之间的锁便是间隙锁。
(3)Next-Key Locks:在索引记录上加锁,并且在索引记录之前的间隙加锁。它相当于是行锁和间隙锁的结合
锁定一个范围的同时锁定记录本身,InNoDB 默认的加锁就是 next-key 锁
六、理论分析
在默认的隔离级别中,普通的SELECT用的是一致性读不加锁。
而对于锁定读、UPDATE和DELETE,则需要加锁,至于加什么锁视情况而定。如果你对一个唯一索引使用了唯一的检索条件,那么只需锁定索引记录即可;
只锁定索引记录,相当于行级锁
如果你没有使用唯一索引作为检索条件,或者用到了索引范围扫描,那么将会使用间隙锁或者next-key锁以此来阻塞其它会话向这个范围内的间隙插入数据。
间隙锁或者next-key锁相当于表级锁,避免了幻读现象
举个很简单的例子,假设事务A更新表中id=1的记录,而事务B也更新这条记录,并且B先提交,如果按照前面MVVC说的,事务A读取id=1的快照版本,那么它看不到B所提交的修改,此时如果直接更新的话就会覆盖B之前的修改,这就不对了,可能B和A修改的不是一个字段,但是这样一来,B的修改就丢失了,这是不允许的。
所以,在修改的时候一定不是快照读,而是当前读。
而且,前面也讲过只有普通的SELECT才是快照读,其它诸如UPDATE、删除都是当前读。修改的时候加锁这是必然的,同时为了防止幻读的出现还需要加间隙锁。
一致性读保证了可重复读
间隙锁防止了幻读,间隙锁就相当于表级别的锁
七、实验
默认可重复读隔离级别:select @@tx_isolation;
默认情况下,MySQL 启用的是自动提交模式,变量 autommit 为 ON
这意味着, 只要你执行DML操作的语句,MySQL会立即隐式提交事务(Implicit Commit)所以首先需要关闭自动提交模式: set autocommit=0;
接下来就可以实验,
select 是快照读,update、delect、insert、select…lock in share mode、select…for update 是当前读
具体实验可参考:MySQL事务隔离级别的实现原理