首先我们先讨论一下问题,是不是在ACID的保护下,数据就一定不会产生不一致的现象呢?
在关系数据库库系统中,多个会话可以访问同一个数据库的同一个表的同一行,这样,对于数据而言,就意味着在同一个时间内,有多个会话可以对其施加操作(或读操作或写操作),读写操作施加的顺序不同以及事务A特性对事务结果的影响(或成功或失败,也就是要不提交要不中止)。这三种因素叠加在一起,会存在几种对数据有不同影响的情况。
1:读-读操作
如果同时只存在多个读操作,对于数据没有影响,也就是说读-读操作不影响数据的一致性,可以并发执行
2:读-写操作
如果读写操作都存在,写在前读在后(如脏读现象),读在前写在后(如不可重复读现象),或者读在前写在后然后再读(如幻读),就可能因为数据被写而导致另外一个读操作的会话读到错误的数据。这个操作可以根据动作发生的先后顺序被细分为读-写操作,写-读操作
3:写-写操作
如果同时存在多个写操作,写-写操作直接改变了同一时刻的语义,这就更不允许,所以写-写操作通常不允许并发执行,但是如果不做并发控制,写-写操作也会带来数据异常形象
这三种情况的第二种,对应的SQL标准中定义的三种数据异常,注意这三种异常主要是针对某个事物的读操作而言的,我们看下SQL2003对于数据异常现象的定义。
脏读:当一个事务被允许从另一个正在运行的事务修改而尚未提交的行中读取数据时,就会发生脏读
不可重复读:不可重复读取发生在事务过程中,当一行被检索两次,且该行中的某些值在读取之间有不同时。
幻读:当在事务过程中,另一个事务向正在读取的记录添加或删除新行时,就会发生幻读。
下面我们看一下四个隔离级别分别解决了什么问题
下面我们开始案例
首先我们创建表,以及向表里面填充一些数据
Create Table: CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(32) NOT NULL DEFAULT ” COMMENT ‘商品名称’,
`classify` tinyint(4) NOT NULL DEFAULT ‘0’ COMMENT ‘商品类型’,
PRIMARY KEY (`id`),
KEY `idx_classify` (`classify`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8
mysql> select * from goods;
+—-+———-+———-+
| id | title | classify |
+—-+———-+———-+
| 1 | 商品1 | 1 |
| 2 | 商品2 | 3 |
| 3 | 商品3 | 5 |
| 4 | 商品4 | 8 |
| 5 | 商品5 | 10 |
| 6 | 商品6 | 1 |
| 7 | 商品7 | 3 |
| 8 | 商品8 | 5 |
| 9 | 商品9 | 8 |
| 10 | 商品10 | 10 |
+—-+———-+———-+
10 rows in set (0.00 sec)
脏读:
我们可以看到T1执行step 5的时候,已经读取到了事务T2未提交的修改的行,这个时候就发生了脏读
不可重复读:
在上图中,我们可以看到,T2执行了step 4,并且在step 5上看到了最后修改的数据,然后T1执行了step6 ,这时候读取到的还是旧值,因为T2还没有提交,那么这就解决了脏读问题。
然后T2执行step 7提交,T1执行step8,这时候因为T2 已经提交了,所以看到了最新的值,那么这时候就发生了不可重复读问题。
幻读:
虽然T2在step 7提交了, 但T1在step8 查出来的title还是商品2,这说明解决了不可重复读问题,下面我们再看一下幻读问题是如何出现的。
我们可以看到T1在step 3的时候查出来是4条数据,接着T2执行了step 4,5,6 ,在这个过程,classify=3的数据是有5条的。
接着T1执行了step7 看到了还是4条,接着执行了T1的step8,这时候T1的step9就看到5条了,就发生了幻读问题。
幻读指的是执行同样的sql,第二次会返回之前不存在的行或者之前的行不见了,这就叫做幻读。
那么如何解决幻读问题呢,有两种方法
- 将隔离级别改为 SERIALIZABLE
- 隔离级别为 REPEATABLE READ,但是加上锁定读
方案1:将隔离级别改为 SERIALIZABLE
我们可以看到T2在step 4的时候就执行不下去,因为T1在step3的时候已经加了共享锁,然后T2在step4又去申请排他锁,因为T1没有释放classify=3行的锁,所以T2的step4最终因为锁等待超时而报错,那么在这种加锁的机制下,也就不存在幻读问题了。
方案2:隔离级别为 REPEATABLE READ,但是加上锁定读
我们可以看到T2在step 4的时候就执行不下去,因为T1在step3的时候已经加了共享锁,然后T2在step4又去申请排他锁,因为T1没有释放classify=3行的锁,所以T2的step4最终因为锁等待超时而报错,那么在这种加锁的机制下,也就不存在幻读问题了。
加锁方式分两种
- SELECT … LOCK IN SHARE MODE
- 在读取出来的记录上面加上一个共享锁,其他会话也可以读取这些记录,但在你的事务提交之前其他事务是不能修改这些记录的,如果这些行中的任何一行已被另一个尚未提交的事务更改,那么您的查询将一直等到该事务结束,然后使用最新值。
- SELECT … FOR UPDATE
- 对于搜索查到的索引记录,锁定行和任何关联索引项,就像你为这些行发送了update语句一样,其他事务如果修改这些行将会被阻塞,例如执行SELECT … LOCK IN SHARE MODE操作或者从某些事务隔离级别读取数据,一致读取忽略设置在读视图中的记录上的任何锁(老版本记录无法被锁定,它们通过在记录的内存副本中撤销日志来重建。
不可重复读和幻读的区别
首先,这两种异常对于T1来说,都是先读取了数据,之后因为T2“写”了数据而导致T1再次读取数据的时候出现了异常。
但是对应不可重复读来说,T1读取的是一个存在的确定的一行数据,这个行数据被T1使用删除或者更新操作而改变,而幻读对应T1读取的是满足条件的多行数据,意味着这是一个范围找到,数据集是不确定的。
所以从第一次读取数据的操作的角度看,前者是读取特定的行,后者读取的是多行,一行,或者零行,其次这两种异常,对于T2来说,都是’写’数据,但是写操作的具体动作不同。
不可重复读对T2的写操作是更新或者删除操作,而幻读对于T2的写操作是插入(插入的新条件满足where条件)或更新(使不满足where条件的数据在更新后满足where条件)操作,而且不可重复读和幻读最大的区别是前者只需要“锁住”已经读过的数据,而幻读需要对“不存在的数据”做出预防。
参考资料:
《高性能MySQL》
《数据库事务处理的艺术 事务管理与并发控制》