事务的隔离级别以及Mysql事务的使用

  • 这篇文章原本我发在内网上,由于没有牵扯到内部资料,就发到博客上了


前言

因为最近在做数据库和搜素引擎同步相关的工作,学习了数据库事务相关的知识。事务相关的知识,尤其是数据库内的各种锁机制也是相当复杂的,在此仅作为一个初学者和使用者的角度作为一个分析和整理。整理过程中我放弃了许多书面而难懂的定义解释(网上很多资料杂乱难懂废话又多还不一定对),尝试找到比较容易理解的解释。有问题的话也欢迎修正。

ACID

首先作为数据库事务,需要保障的性质如下:
原子性:要么全部完成,要么全部不完成。
隔离性:事务的中间状态不能被另一个事务看到。(详见下面的隔离级别)
持久性:事务提交后即持久化到磁盘不会丢失。
一致性:数据库的完整性约束要得到保证。(例如转账,两个账户的和应该不变)

事务的隔离级别

在复习隔离级别之前,先看一下几个与之相关的定义:

  • 脏读:一个事务读取了其他事务还没有提交的变更
  • 不可重复读:同样的条件,读取过的数据,再次读取出来发现值不一样了。(因为另一个事务对该条件的数据 修改提交了)
  • 幻读:同样的条件,第一次和第二次读出来的记录数不一样。(因为另一个事务对该条件的数据 插入/删除 提交了)
  • 第一类丢失更新:一个事务的回滚覆盖了另一个事务提交的update。
  • 第二类丢失更新:一个事务提交的update覆盖了另一个事务提交的update。

而数据库的隔离级别可以看做这几种条件满足的组合:

隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITTED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许

MySql的隔离级别

需要明确的是,以上的ACID和隔离级别定义是在SQL规范层面的定义,不同数据库的实现方式和使用方式并不相同。(类似于JVM规范和JVM厂商的关系)

上面的隔离级别标准是SQL92基于读写锁的实现方式制定的规范,可惜之后的MVCC成为了各大厂商的主流实现方式(实际上可以理解为第五个隔离级别:SNAPSHOT ISOLATION),为了兼容落后的规范,数据库引擎们都想办法“贴”近四大隔离级别,但是和标准的预期肯定不会完全一致的。
除了MySql,包括Oracle在内的大多数数据库的默认隔离级别都是READ_COMMITED,而MySql的默认事务隔离级别是REPEATABLE_READ。(以下关于MySql的说明均指InnoDB引擎下的MySql)

MySql的REPEATABLE_READ隔离级别

先一句话总结:由于实现方式已经偏离了标准的预期,MySql没有严格遵守SQL92规范,在REPEATABLE_READ级别中,不会出现幻读,而且第二类丢失更新需要用户自己加锁实现。

从隔离级别规范上看,它阻止了脏读和不可重复读,这是通过MVCC多版本并发控制解决的。解决脏读的办法是consistent read,通过时间戳避免读到事务提交之前的操作。对于不可重复读,在consistent read的基础上,让一个事务的每次读操作 都去读该事务第一次读操作时的快照版本。详见http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html 
对于幻读,尽管SQL规范中,REPEATABLE READ允许幻读,但是MySql的REPEATABLE READ级别上不会出现幻读,因为它使用了间隙锁(Next-key lock)解决了幻读的问题。详见 http://dev.mysql.com/doc/refman/5.7/en/innodb-record-level-locks.html

但是。如果想解决第二类丢失更新,并不像我们想象中直接把代码或者sql语句套进事务里就可以了。如下:

悲观锁解决丢失更新的问题

举个例子:

begin;
SET @a = (SELECT restnum FROM book WHERE id =1)
SET @b = function(@a)      //抽象为某个业务逻辑操作 
UPDATE  book SET restnum=@b WHERE  id =1 
commit;

类似于上面的代码套进事务中,其实在传统数据库的REPEATABLE_READ下是安全的传统数据库对于事务的处理方式为:如果有写操作,则加独占锁,直到commit才会释放。独占锁意味着其他session既不能读也不能写这块数据。在高并发下依然很容易出现丢失更新。但是现代数据库引擎都是基于MVCC实现的,其最大特点就是即使独占锁锁住了这份数据,其他session是可以读到它写之前的副本的,带来的弊端就是没有严格的保证写读的先后顺序关系

因此在REPEATABLE_READ级别下,我们需要手动加锁。正确的写法是:

begin;    //注意要关闭auto commit
SET @a = (SELECT restnum FROM book WHERE id =1 FOR UPDATE)
SET @b = function(@a)      //抽象为某个业务逻辑操作 
UPDATE  book SET restnum=@b WHERE  id =1 
commit;

如上,SELECT...FOR UPDATE 是排他锁,会锁住数据(如果where指定了主键的话会锁行,否则会锁表),直到该事务结束(提交或回滚)。
与此类似的语句还有SELECT ... LOCK IN SHARE MODE是共享锁,session拿到该锁后,其他session只能读不能修改锁住的数据直到session结束,如果session拿锁时其他session对数据还有未提交的更改则会阻塞等待对方提交。
两种锁都是悲观锁,一行数据的排他锁同时只能一个session持有,而共享锁是可以多个session共享持有的,共享锁也因此容易产生 死锁 。
MySql的SERIALIZABLE隔离级别就是在REPEATABLE READ的基础上,把所有纯SELECT操作加上LOCK IN SHARE MODE。因此,上面的第一份代码,如果将隔离级别换为SERIALIZABLE,在高并发下的结果并不会像我们想象中逐个执行,而是会产生死锁。

乐观锁解决丢失更新的问题

在实际使用中,我们其实很少使用悲观锁,因为阻塞可能会造成未知的性能问题。使用乐观锁,通常是通过版本号或者时间戳,在最后提交时进行比对:

SELECT (restnum,version) FROM book WHERE id =1)
...... //业务逻辑操作 
UPDATE  book SET restnum=@xx WHERE  id =1 AND version = @version

当然,理想状态下,最好能直接用一个UPDATE语句实现,因为UPDATE会加排他锁,也就是不会出现上面的问题了。

你可能感兴趣的:(数据库)