在理解脏读(Dirty Read)之前,需要理解脏数据的概念。但是脏数据和之前所介绍的脏页完全是两种不同的概念。脏页指的是在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中。而所渭脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。
  对于脏页的读取,是非常正常的。脏页是因为数据库实例内存和磁盘的异步造成的,这并不影响数据的一致性(或者说两者最终会达到一致性,即当脏页都刷回到磁盘)。并且因为脏页的刷新是异步的,不影响数据库的可用性,因此可以带来性能的提高。
  脏数据却截然不同,脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。

一、READ UNCOMMITTED级别下的脏读

首先执行以下创表语句:

DROP TABLE IF EXISTS `t`;
CREATE TABLE `t`  (
  `id` int(11) NOT NULL,
  `code` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

在READ UNCOMMITTED级别下会出现脏读
首先设置当前会话的隔离级别为READ UNCOMMITTED

set session transaction isolation level read uncommitted;

时间点

会话A

会话B

时间点1

begin;

时间点2

select * from t;

时间点3

begin;

时间点4

insert into t values(10,10);

时间点5

select * from t;

sql server 语句实现脏读 mysql脏读问题的含义_数据库


这里可以看到,会话A在时间点2和时间点5执行查询的时候,返回的结果是不一样的。

在事务并没有提交的前提下,会话A中两次得到的数据是不同的,并且时间点5得到的这一条数据还是会话B中并未提交过的数据,即发生了脏读,违反了事务的隔离性。

脏读现象在生产环境中并不常发生,从上面的例子中就可以发现,脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED,而目前绝大部分的数据库都至少设置成READ COMMITTED。nnoDB存储引擎默认的事务隔离级别为READ REPEATABLE,Microsoft SQL Server数据库为READ COMMITTED,Oracle数据库同样也是READ COMM。

二、READ COMMITTED下的幻读

首先设置当前会话的隔离级别为READ COMMITTED

set session transaction isolation level read committed;

时间点

会话A

会话B

时间点1

begin;

时间点2

select * from t;

时间点3

begin;

时间点4

insert into t values(10,10);

时间点5

select * from t;

时间点6

commit;

时间点7

select * from t;

时间点8

commit;

sql server 语句实现脏读 mysql脏读问题的含义_数据库_02


这里可以看到会话A在时间点2和时间点5查询的结果是一样的,但在时间点6会话B提交后,会话A在时间点7查询的结果就发生了变化。这就造成了幻读,在同一事务下,两次查询的结果是不一致的。这里我们把事务隔离级别设置会REPEATABLE READ。

sql server 语句实现脏读 mysql脏读问题的含义_java_03


在REPEATABLE READ隔离级别下不会出现幻读的问题。

在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

而目前大部分的数据库都至少设置成READ COMMITTED。InnoDB存储引擎默认的事务隔离级别为READ REPEATABLE,Microsoft SQL Server数据库为READ COMMITTED,Oracle数据库同样也是READ COMMITTED。

四、REPEATABLE READ下可能存在的问题

首先我们把隔离级别设置为REPEATABLE READ

set session transaction isolation level repeatable read;

时间点

会话A

会话B

时间点1

begin;

时间点2

select * from t where code >8;

时间点3

begin;

时间点4

insert into t values(11,11);

时间点5

commit;

时间点6

select * from where code>8;

时间点7

insert into t values(11,11)

时间点8

commit;

sql server 语句实现脏读 mysql脏读问题的含义_数据库_04


这里会出现会话A在第二次查询中明明查询不到code为11的数据,但是插入后却发现插入失败的情况。

除此还会出现更新丢失的情况。
可以看一下以下的例子

时间点

会话A

会话B

时间点1

begin;

时间点2

select code into @code from t where id=10;

时间点3

select code into @code from t where id=10;

时间点4

update t set code=@code-1 where id=10;

时间点5

commit;

时间点6

update t set code=@code-1 where id=10;

时间点7

commit;

sql server 语句实现脏读 mysql脏读问题的含义_数据库_05


这里从会话B最后的查询结果可以看出,最终id为10的这一行数据code=9,但是按照正常的业务逻辑,这里的code应当为8。这就造成了丢失更新现象,如果此场景出现在银行转账的操作当中,那后果不堪设想。

那这里应该如何避免这一种现象呢,我们可以看一看以下这个例子:

时间点

会话A

会话B

时间点1

begin;

时间点2

select code into @code from t where id=10 for update;

时间点3

select code into @code from t where id=10 for update;# 等待:会话A的提交

时间点4

update t set code=@code-1 where id=10;

时间点5

commit;

时间点6

update t set code=@code-1 where id=10;

时间点7

commit;

sql server 语句实现脏读 mysql脏读问题的含义_java_06

这里我们可以看到最终查询id=10的结果字段code=8,符合我们的业务逻辑。这里可能有的人会产生疑问,为什么不在上述的例子中直接允许update语句,而首先要进行SELECT …FOR UPDATE的操作。直接使用UPDATE可以避免丢失更新问题的产生。然而在实际应用中,应用程序可能需要首先检测用户的余额信息,查看是否可以进行转账操作,然后再进行最后的UPDATE操作,因此在SELECT和UPDATE操作之间可能还存在一些其他的SQL操作。