事务
脏写和脏读问题
今天我们来看看多个事务对缓存页里的同一条数据同时进行更新或者查询,此时会产生哪些问题?这里实际会涉及到脏写、脏读、不可重复读、幻读,四中问题。
这个脏写的话,它的意思是说有两个事务,事务A和事务B同时在更新一条数据,事务A先把它更新为A值,事务B紧接着就把它更新为B值。事务A是先更新的,它在更新之前,这行数据的值为NULL,所以此时事务A的undo log日志大概是这样的:更新之前这行数据的值为NULL,主键为XX 。
那么此时事务B更新完了数据的值为B,结果此时事务A突然回滚了,那么就会用它的undo log日志去回滚。此时事务A一回滚,直接就会把那行数据的值更新回之前的NULL值。所以对于事务B看到的场景,就是自己明明更新了,结果值却没了,这就是脏写。
假设事务A更新了一行数据的值为A,此时事务B去查询了一些这行数据的值,看到的值是A,然后事务B拿着刚查询到的A值去处理各种业务。但是此时不幸的事情发生了,事务A突然回滚了,导致它刚才更新的A值没了,此时那行数据的值回滚为NULL值。这就是所谓的脏读。它的本质是事务B去查询了事务A修改过的数据,但是此时事务A还没有提交,事务A随时会回滚导致事务B查询了一个不存在的值。
不可重复读和幻读问题
接着我们来看一下的问题,假设我们有一个事务A开启了,在这个事务A里会多次对一条数据进行查询。然后另外有两个事务,一个是事务B,一个是事务C,它们都是对一条数据进行更新的。假设缓存页里一条数据原来的值是A值,此时事务A开启之后,第一次查询这条数据,读取到的是A值。接着事务B更新了那行数据的值为B,同时提交事务,然后事务A第二次查询该行数据,此时查到的是事务B修改过的值B 。接着事务C更新了那行数据的值为C,同时提交事务,然后事务A第三次查询该行数据,此时查到的是事务C修改过的值C值。
那么上面的场景有什么问题呢?其实要说没问题也是可以的,毕竟事务B和C都提交事务了。但是要说有问题也是可以的,就是事务A可能第一次查询到的是A值,那么它可能希望的是在事务执行期间,如果多次查询数据,都是同样的一个A值。但是该场景下,A值明显不是可重复读的。
这种情况算不算一个问题呢?其实这是根据你的业务决定的。有的业务要的是可重复读,而有的业务却需要不可重复读。
假设一个事务A先发送一条SQL语句,里面有一个条件,要查询一批数据出来,比如“select * from table where id > 10”,类似这种SQL,它一开始查询出了10条数据。然后事务B往表里插入了几条数据,而且事务B还提交了。此时事务A再次查询,由于事务B插入了几条数据,导致这次它查询出来了12条数据。同样的SQL语句,两次的查询结果却不一样,所以开始怀疑自己是不是出现了幻觉?导致刚才幻读了?这就是幻读一词的由来。
4个事务隔离级别
在SQL标准中规定了4种事务隔离级别,就是说多个事务并发运行的时候,互相是如何隔离的,从而避免一些事务并发问题。这4种级别包括了:read uncommitted(读未提交)、read committed(读已提交)、repeatable read(可重复读)、serializable(串行化)。
第一个read uncommitted隔离级别是不允许发生脏写的。也就是说,不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发生脏读、不可重复读、幻读。所以一般来说,是没有人做系统开发的时候把事务隔离级别设置为读未提交这个级别的。
第二个是read committed隔离级别,也就是俗称的RC级别,这个级别不会发生脏写和脏读。也就是说,别的事务没提交的情况下修改的值,你是绝对读不到的。但是,可能会发生不可重复读和幻读问题。
第三个是repeatable read隔离级别,也就是俗称的RR级别,就是可重复读级别。这个级别下,不会发生脏写、脏读、不可重复读的问题。事务一旦开启,多次查询一个值,会一直读到同一个值。但是它会发生幻读的问题。
最后一个隔离级别,就是serializable级别,这种级别,根本不允许多个事务并发执行,只能串行执行,所以不可能有幻读问题。但是这种级别一般除非脑子坏了,否则不可能设置这种级别。
MySQL默认设置的事务隔离级别都是RR级别的,而且MySQL的RR级别是可以避免幻读发生的。
下面的命令可以修改MySQL的默认事务隔离级别:
//5.7版本使用,查看当前事物级别:
SELECT @@tx_isolation;
//8.0版本使用,查看当前事物级别:
select @@transaction_isolation;
//设置read uncommitted级别:
set session transaction isolation level read uncommitted;
//设置read committed级别:
set session transaction isolation level read committed;
//设置repeatable read级别:
set session transaction isolation level repeatable read;
//设置serializable级别:
set session transaction isolation level serializable;
另外,给大家一个彩蛋,假设你在开发业务系统的时候,比如用spring里的@Transaction注解来做事务这块,假设某个事务你就是有点手痒,想搞成RC级别,那么没问题,在@Transaction注解里是有一个isolation参数的,里面是可以设置事务隔离级别的,具体的设置方式如下:
@Transaction(isolation=Isolation.DEFAULT),默认的就是DEFAULT值,这个就是MySQL默认支持什么隔离就是什么隔离级别。但是你可以手动改成其它的隔离级别,比如,isolation = Isolation.READ_COMMITTED级别,此时你就可以读取到其它事务已提交的数据。
undo log版本链
简单来说,我们每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向了你更新这个事务之前生成的undo log,关于undo log之前都讲过了。
举个例子,假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示:
MVCC1.jpg
插入的这条数据的值是A,因为事务A的id是50,所以这条数据的trx_id就是50,roll_pointer指向一个空的undo log,因为之前这条数据是没有的。接着有一个事务B修改了一下这条数据,把值改成了B,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示:
mvcc2.jpg
那如果再有事务C,事务D修改这条数据呢,重复上面的步骤就行了,就不再重复叙述了。之前多个数据快照对应的undo log会通过roll_pointer指针串联起来,形成一个undo log版本链。我们再看一张图就清晰了:
MVCC3.jpg
网友评论