隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted(读未提交) | 是 | 是 | 是 |
Read committed(读已提交) | 否 | 是 | 是 |
Repeatable read(可重复读) | 否 | 否 | 是 |
Serializable(串行化) | 否 | 否 | 否 |
事务的隔离级别基本是为了解决读一致性的问题。
下面我会通过一个经典的银行账户余额变更的例子来依次阐述。
创建表account,并插入两条数据。使用navicat打开
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(10) DEFAULT NULL,
`account` double DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
SHOW VARIABLES LIKE 'AUTOCOMMIT';
SET AUTOCOMMIT = 'off';
MYSQL默认是自动提交事务的,其实不关闭也没问题,因为我们会使用SQL语句显式的开启一个事务。
我分别使用navicat和sqlyog来表示两个客户端的请求。
set session transaction isolation level read uncommitted;
在navicat及sqlyog的会话窗口先执行此sql,修改事务的隔离级别为读未提交。
以下navicat的会话sql当做A,sqlyog的会话当做B。
BEGIN;
update account set account = account + 200 where id = 1;
注意此时A事务并没有提交!
BEGIN;
SELECT * FROM account;
我们看到在B事务中,张三的账户余额变为了1200。也就是说我们读到了A事务没有提交的数据,也就是说发生了脏读。假设此时我们将A事务回滚(执行CALLBACK),张三的账户余额会变回1000,那么B读取到的数据1200就是脏数据。
读未提交是最低的隔离级别,没有解决任何一个事务并发问题。发生脏读时B事务并不知道A事务是否提交会回滚,所以拿到的数据是很不安全的。
set session transaction isolation level read committed;
在navicat及sqlyog的会话窗口先执行此sql,修改事务的隔离级别为读已提交。
BEGIN;
update account set account = account + 200 where id = 1;
注意此时A事务并没有提交!
BEGIN;
SELECT * FROM account;
我们可以看到此时B事务并没有读取到A事务已变化的内容,我们并没有看到张三的account由1000变为1200。这一切似乎已经很完美了,解决了脏读的问题,我们已经读取不到没提交的事务所修改的数据了。
但是请注意:
当A事务提交后,B事务再次查询会看到account变为了1200。也就是说B事务两次查询的结果是不一致的,此时发生了不可重复读现象。不可重复读的意思就是在一个事务中相同的查询条件下却读取到不同的结果。
此时我们需要将mysql的隔离级别提高到可重复读
Repeatable read(可重复读),同时也是MySQL InnoDB引擎的默认事务隔离级别。
虽然读已提交解决了脏读的问题,但是不可重复读的问题却没能解决,此时需要提高隔离级别到Repeatable read(可重复读)
set session transaction isolation level repeatable read;
在navicat及sqlyog的会话窗口先执行此sql,修改事务的隔离级别为可重复读。简称RR隔离级别。
BEGIN;
update account set account = account + 200 where id = 1;
注意此时A事务并没有提交!
BEGIN;
SELECT * FROM account;
可以看到此时我们读取的是A事务没变更前的数据,即张三的account还是1000,首先解决了脏读的问题。其次我们对A事务进行提交。
在B事务中(B还未提交)再次查询结果。
发现B事务读取的仍然是1000。说明即便A事务提交了记录,B事务多次读取依旧能够读取到相同的结果,解决了不可重复读的问题。
此时我们提交B事务。再次执行查询
可以看到在A和B事务都提交后,可以查询出A事务中执行的修改内容,此时张三的account变为了1200。
Repeatable read(可重复读)解决了脏读,不可重复读的问题,是MySQL的InnoDB引擎默认的事务隔离级别,可以解决大部分的读一致性问题。
但是Repeatable read(可重复读)还有一个问题没能解决,那就是幻读。
幻读和不可重复读容易混淆,在我理解看来。
set session transaction isolation level serializable;
串行化是事务隔离级别中最高的一级,可以解决脏读、不可重复读、幻读的问题。但同时也是效率最低、并发度最差的一种,原因是串行化每次读写操作都会锁表,必须等待锁释放下一个事务才能进行相关的操作。所以一般很少用到,这里不再试验,下面将对幻读进行测试。
在Innodb的Repeatable read(可重复读)隔离级别下,幻读的问题其实已经被InnoDB解决了。所以按照下面的测试步骤,是看不到幻读的现象的。
set session transaction isolation level repeatable read;
BEGIN;
#插入一条id为3的记录
insert into account value(3,'王五',1000);
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM account WHERE id > 1;
如图,在A事务没提交的情况下,我们看到只查到一条数据,这也很合理。那么接下来我们提交A事务呢?
提交A事务
COMMIT;
同时在B事务中再次查询
SELECT * FROM account WHERE id > 1;
此时终于看到了A事务中插入的记录。
这么看来,我们所说的幻读现象并没有出现,那么怎么才能测试出来呢?
set session transaction isolation level repeatable read;
BEGIN;
#插入一条id为3,姓名为王五的记录
insert into account value(3,'王五',1000);
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
#插入一条id为3,姓名为赵六的记录
INSERT INTO account VALUE(3,'赵六',1000);
会发现B事务并不能插入,一直处于操作状态。等一小段时间过后,我们会在B事务的控制台看到报错信息
这说明我们当前的两个事务中存在着锁的冲突,为什么会这样呢?
因为我们操作A事务,执行insert id=3的操作,B事务同样操作insert id=3的操作,他们都是对id = 3这一行进行操作,也就是说A事务执行insert id =3时发生了行锁,将该行锁住,所以B事务对id=3这行进行操作时,是不能成功的,因为A事务并没有释放锁。
我们会立马看到下面的错误信息
A事务提交后,id = 3的记录被持久化到数据库中。所以B事务再插入id = 3的就报错了,因为id是主键。
Innodb实现了MVCC(多版本并发控制),使用next-key锁。
下面推荐两篇文章来了解学习。
Innodb中的MVCC
轻松理解MYSQL MVCC 实现机制
MySQL 在InnoDB引擎下有当前读和快照读两种模式。 当前读即加锁读,读取记录的最新版本号,会加锁保证其他并发事务不能修改当前记录,直至释放锁。插入/更新/删除操作默认使用当前读,显示的为select语句加lock in share mode或for update的查询也采用当前读模式。 快照读:不加锁,读取记录的快照版本,而非最新版本,使用MVCC机制,最大的好处是读取不需要加锁,读写不冲突,用于读操作多于写操作的应用,因此在不显示加[lock in share mode]/[for update]的select语句,即普通的一条select语句默认都是使用快照读MVCC实现模式。
------引自CSDN用户Aubade2017对博客的评论
很显然,快照读和当前读是两种读取机制。快照读使用了MVCC,读取不用加锁。
主要实现是通过在事务操作的当前行的记录上加上 DATA_TRX_ID 作为当前事务版本号(可以理解为创建时间),DATA_ROLL_PTR (可以理解为删除时间)作为回滚指针指向undo log的row_id。(指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针)
begin->用排他锁锁定该行->记录redo log->记录undo log->修改当前行的值,记录事务编号,回滚指针指向undo log中的修改前的行
注意:
insert的事务过程和update基本一致,只是insert时undo log是不能指向原始记录的,因为原始记录就不存在。所以insert操作的回滚,需要丢弃undo log。
Innodb检查每行数据,确保他们符合两个标准:
1、InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行
2、行的删除操作的版本一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前,行没有被删除
符合了以上两点则返回查询结果。
但我之前一直有个疑问。比如A事务先开启,执行一个insert操作,假如此时的事务版本号是1,那么insert的新记录的DATA_TRX_ID就是1。此时开启B事务(B事务假如编号为2),在B事务中查询,为什么没查询到小于当前事务版本号(2)的记录呢?
A事物插入数据是要加入排它锁的,B通过一致性非锁定读的时候不会读到这个锁定的数据,会去读这个数据的快照,即通过undo log来读取,然而**这条数据是新插入的所以undo log不存在所以读不到**,这就不会导致脏读。至于幻读innoDB是通过next key 锁来避免的,一致性非锁定读同样读取的是undo log当中的快照
-----引自CSDN用户wulei93对博客的评论
上面的这段评论解决了我的疑问。
推荐下面这篇文章
mysql锁 innodb下的记录锁,间隙锁,next-key锁
感谢阅读,有理解有误的地方希望积极指正,谢谢。在后续深入学习中有心得会再分享