数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题
【tips】我们讨论隔离级别的场景,主要是在多个事务并发的情况下,因此,接下来的讲解都围绕事务并发。
【隔离性】
如果事务没有隔离性,就容易出现脏读、不可重复读和幻读等情况。
脏读
· 脏读是指一个事务正在访问数据,并且对数据进行了修改,但是这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
幻读
· 幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
不可重复读
· 不可重复读是指在一个事务内,多次读取同一个数据。
· 在这个事务还没有结束时,另外一个事务也访问了该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
【MySQL 包括的事务隔离级别】
读未提交(READ UNCOMITTED)
读提交(READ COMMITTED)
可重复读(REPEATABLE READ)
串行化(SERIALIZABLE)
事务的隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read uncommitted | √ | √ | √ |
Read committed--Sql Server , Oracle | × | √ | √ |
Repeatable read--MySQL | × | × | √ |
Serializable | × | × | × |
【查看事务隔离级别】
在 MySQL 中,可以通过以下两种方式查询当前的事务隔离界别。
show variables like '%tx_isolation%'
select @@tx_isolation;
另外,还可以使用下列语句分别查询全局和会话的事务隔离级别:
SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
注意:在MySQL 8.0.3 中,tx_isolation 变量被 transaction_isolation 变量替换了。在 MySQL 8.0.3 版本中查询事务隔离级别,只要把上述查询语句中的 tx_isolation 变量替换成 transaction_isolation 变量即可。
【修改事务隔离级别】
MySQL 提供了 SET TRANSACTION 语句,该语句可以改变单个会话或全局的事务隔离级别。语法格式如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
其中,SESSION 和 GLOBAL 关键字用来指定修改的事务隔离级别的范围:
· SESSION:表示修改的事务隔离级别将应用于当前 session(当前 cmd 窗口)内的所有事务;
· GLOBAL:表示修改的事务隔离级别将应用于所有 session(全局)中的所有事务,且当前已经存在的 session 不受影响;
· 如果省略 SESSION 和 GLOBAL,表示修改的事务隔离级别将应用于当前 session 内的下一个还未开始的事务。
任何用户都能改变会话的事务隔离级别,但是只有拥有 SUPER 权限的用户才能改变全局的事务隔离级别。
【重复读与幻读】
重复读是为了保证在一个事务中,相同查询条件下读取的数据值不发生改变,但是不能保证下次同样条件查询,结果记录数不会增加。
幻读就是为了解决这个问题而存在的,他将这个查询范围都加锁了,所以就不能再往这个范围内插入数据,这就是SERIALIZABLE 隔离级别做的事情。
【隔离级别与锁的关系】
在Read Uncommitted级别下,读操作不加S锁;
在Read Committed级别下,读操作需要加S锁,但是在语句执行完以后释放S锁;
在Repeatable Read级别下,读操作需要加S锁,但是在事务提交之前并不释放S锁,也就是必须等待事务执行完毕以后才释放S锁。
在Serialize级别下,会在Repeatable Read级别的基础上,添加一个范围锁。保证一个事务内的两次查询结果完全一样,而不会出现第一次查询结果是第二次查询结果的子集。
【Read uncommitted 读未提交】
顾名思义,读未提交就是可以读到未提交的内容。
如果一个事务读取到了另一个未提交事务修改过的数据,那么这种隔离级别就称之为读未提交。
在该隔离级别下,所有事务都可以看到其它未提交事务的执行结果。因为它的性能与其他隔离级别相比没有高多少,所以一般情况下,该隔离级别在实际应用中很少使用。
举个栗子:
公司发工资了,领导把30k打到小王的账号上,但是该事务并未提交,而小王正好去查看账户,发现工资已经到账,是30k整,非常高兴。可是不幸的是,领导发现发给小王的工资金额不对,是20k,于是迅速回滚了事务,修改金额后,将事务提交,最后小王实际的工资只有20k,小王空欢喜一场。
出现上述情况,即我们所说的脏读,两个并发的事务,“事务A:领导给小王发工资”、“事务B:小王查询工资账户”,事务B读取了事务A尚未提交的数据。
当隔离级别设置为Read uncommitted时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。
【Read committed 读提交】
顾名思义,读提交就是只能读到已经提交了的内容。
如果一个事务只能读取到另一个已提交事务修改过的数据,并且其它事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那么这种隔离级别就称之为读提交。
该隔离级别满足了隔离的简单定义:一个事务从开始到提交前所做的任何改变都是不可见的,事务只能读取到已经提交的事务所做的改变。
这是大多数数据库系统的默认事务隔离级别(例如 Oracle、SQL Server),但不是 MySQL 默认的。
接着上面的栗子说:
小王拿着工资卡去消费,系统读取到卡里确实有20k,而此时她的老婆也正好在网上转账,把小王工资卡的20k转到另一账户,并在小王之前提交了事务,当小王扣款时,系统检查到小王的工资卡已经没有钱,扣款失败,小王十分纳闷,明明卡里有钱,为何......
出现上述情况,即我们所说的不可重复读,两个并发的事务,“事务A:小王消费”、“事务B:小王的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
当隔离级别设置为Read committed时,避免了脏读,但是可能会造成不可重复读。
大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。
【Repeatable read 重复读】
顾名思义,可重复读是专门针对不可重复读这种情况而制定的隔离级别,可以有效的避免不可重复读。
在一些场景中,一个事务只能读取到另一个已提交事务修改过的数据,但是第一次读过某条记录后,即使其它事务修改了该记录的值并且提交,之后该事务再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。那么这种隔离级别就称之为可重复读。
可重复读是 MySQL 的默认事务隔离级别,它能确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。在该隔离级别下,如果有事务正在读取数据,就不允许有其它事务进行修改操作,这样就解决了可重复读问题。
继续上面的栗子:
当隔离级别设置为Repeatable read时,可以避免不可重复读。当小王拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),小王的老婆就不可能对该记录进行修改,也就是小王的老婆不能在此时转账。
虽然Repeatable read避免了不可重复读,但还有可能出现幻读。
小王的老婆工作在银行部门,她时常通过银行内部系统查看小王的信用卡消费记录。有一天,她正在查询到小王当月信用卡的总消费金额(select sum(amount) from transaction where month = 本月)为9k,而小王此时正好在外面胡吃海塞后在收银台买单,消费1k,即新增了一条1k的消费记录(insert transaction ... ),并提交了事务,随后小王的老婆将小王当月信用卡消费的明细打印到A4纸上,却发现消费总额为10k,小王的老婆很诧异,以为出现了幻觉,幻读就这样产生了。
MySQL的默认隔离级别就是Repeatable read。
【Serializable 串行化】
如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。那么这种隔离级别就称之为串行化。
SERIALIZABLE 是最高的事务隔离级别,主要通过强制事务排序来解决幻读问题。简单来说,就是在每个读取的数据行上加上共享锁实现,这样就避免了脏读、不可重复读和幻读等问题。但是该事务隔离级别执行效率低下,且性能开销也最大,所以一般情况下不推荐使用。
登录后可发表评论