MySQL事务隔离级别的最佳实践
发布日期:2021-06-30 12:24:54 浏览次数:2 分类:技术文章

本文共 8215 字,大约阅读时间需要 27 分钟。

事务

一组SQL语句组成的逻辑处理单元。

  • 原子性(Actomicity)
    事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
  • 一致性(Consistent)
    在事务开始和完成时,数据都必须保持一致状态
    这意味着所有相关的数据规则都必须应用于事务的修改,以保持完整性
    事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的
  • 隔离性(Isolation)
    一个事务所做的修改在最终提交前对其他事务不可见
  • 持久性(Durability)
    一旦事务提交,它对于数据的修改会持久化到DB

事务的问题

相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户

但并发事务处理也会带来一些问题,主要包括以下几种情况

更新丢失(Lost Update)

当多个事务选择同一行,然后基于最初选定值更新该行时,由于事务隔离性,最后的更新覆盖了其他事务所做的更新.

例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改保存其更改副本的编辑人员覆盖另一个编辑人员所做的修改;
如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题

脏读(Dirty Reads)

一个事务正在对一条记录做修改,在该事务提交前,这条记录的数据就处于不一致状态。

这时,另一个事务也来读取同一条记录,读取了这些未提交的数据,例如: 早期版本、回滚。

不可重复读(Non-Repeatable Reads)

一个事务在读取某些数据,发现已经发生改变或某些记录已被删除。

不加锁时,其他事务的 UPDATE 或 DELETE 会影响查询结果。

幻读(Phantom Reads)

加锁后,不锁定间隙,其他事务可以 INSERT。

比如一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。

很多人容易搞混不可重复读和幻读,这两者确实非常相似:

  • 不可重复读主要是多次读取一条记录,发现该记录中某些列值被修改过
  • 幻读主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或做聚合统计), 发现结果不一致。一般指记录增多!记录的减少也算幻读!

事务并发下的异常

SQL-92标准中定义了如下异常:

  • 脏读(Dirty Read)
  • 不可重复读(Nnrepeatable Read)
  • 幻读(Phantom Read)

比如说我们有个英雄表heros_temp,如下所示:

这张英雄表,我们会记录很多英雄的姓名,假设我们不对事务进行隔离操作,那么数据库在进行事务的并发处理时会出现怎样的情况?

第一天,小张访问数据库,正在进行事务操作,往里面写入一个新的英雄“吕布”:

SQL> BEGIN;

SQL> INSERT INTO heros_temp values(4, ‘吕布’);
当小张还没有提交该事务的时候,小李又对数据表进行了访问,他想看下这张英雄表里都有哪些英雄:

SQL> SELECT * FROM heros_temp;

这时,小李看到的结果如下:

你有没有发现什么异常?这个时候小张还没有提交事务,但是小李却读到了小张还没有提交的数据,这种现象我们称之为“脏读”。

那么什么是不可重复读呢?

第二天,小张想查看id=1的英雄是谁,于是他进行了SQL查询:

SQL> SELECT name FROM heros_temp WHERE id = 1;

运行结果:

然而此时,小李开始了一个事务操作,他对id=1的英雄姓名进行了修改,把原来的“张飞”改成了“张翼德”:

SQL> BEGIN;

SQL> UPDATE heros_temp SET name = ‘张翼德’ WHERE id = 1;
然后小张再一次进行查询,同样也是查看id=1的英雄是谁:

SQL> SELECT name FROM heros_temp WHERE id = 1;

运行结果:

这个时候你会发现,两次查询的结果并不一样。小张会想这是怎么回事呢?他明明刚执行了一次查询,马上又进行了一次查询,结果两次的查询结果不同。实际上小张遇到的情况我们称之为“不可重复读”,也就是同一条记录,两次读取的结果不同。

什么是幻读?

第三天,小张想要看下数据表里都有哪些英雄,他开始执行下面这条语句:

SQL> SELECT * FROM heros_temp;

这时当小张执行完之后,小李又开始了一个事务,往数据库里插入一个新的英雄“吕布”:

SQL> BEGIN;

SQL> INSERT INTO heros_temp values(4, ‘吕布’);
不巧的是,小张这时忘记了英雄都有哪些,又重新执行了一遍查询:

SQL> SELECT * FROM heros_temp;

他发现这一次查询多了一个英雄,原来只有3个,现在变成了4个。这种异常情况我们称之为“幻读”。

脏读:读到了其他事务还没有提交的数据。

不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。
幻读:事务A根据条件查询得到了N条数据,但此时事务B更改或者增加了M条符合事务A查询条件的数据,这样当事务A再次进行查询的时候发现会有N+M条数据,产生了幻读。

事务隔离级别

在并发事务问题中,“更新丢失”应该完全避免,但防止更新丢失,并不能单靠数据库事务控制器,需要应用程序对要更新的数据加必要的锁来解决。

因此,防止更新丢失应该是应用的责任

脏读、不可重复读和幻读都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制。

数据库实现事务隔离的方式如下:

  • 在读取数据前,对其加锁,防止其他事务对数据进行修改
  • 不加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别(语句级或事务级)的一致性读取
    从用户的角度,好像是数据库可以提供同一数据的多个版本,又称为数据多版本并发控制(MultiVersion Concurrency Control,MVCC)或多版本数据库

数据库的事务隔离级别越严格,并发副作用越小,但付出的代价也越大。因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”矛盾。

为解决“隔离”与“并发”的矛盾,ANSI SQL定义了4种隔离级别:

隔离级别/读数据一致性及允许的并发副作用 读数据一致性 脏读 不可重复读 幻读
未提交读(Read uncommitted) 最低级别,只能保证不读取物理上损坏的数据
已提交度(Read committed) 语句级
可重复读(Repeatable read) 事务级
可序列化(Serializable) 最高级别,事务级

隔离性用于防止数据库在并发时的数据不一致。

最严格时,可用串行化执行每个事务,事务之间相互独立,不存在并发。
但是在实际生产环境下,考虑到随着用户量的增多,会存在大规模并发访问,要求数据库有更高吞吐量,这时串行化无法满足数据库高并发访问需求,还要降低隔离标准,换取事务间的并发力。

有时需牺牲一定的正确性换取效率的提升,即需要通过设置不同的隔离等级,以便在正确性和效率之间进行平衡。同时,随着RDBMS种类和应用场景的增多,数据库的设计者需要统一对数据库隔离级别进行定义,说明这些隔离标准都解决了哪些问题。

事务隔离级别

脏读、不可重复读和幻读这三种异常情况,是在SQL-92标准中定义的,同时SQL-92标准还定义了4种隔离级别来解决这些异常情况。

解决异常数量从少到多的顺序(比如读未提交可能存在3种异常,可串行化则不会存在这些异常)决定了隔离级别的高低,这四种隔离级别从低到高:

读未提交(READ UNCOMMITTED,RU )

允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读,不能保证一致性,很少使用。

读已提交(READ COMMITTED,RC)

只能读到已经提交的内容,可以避免脏读的产生。

如果想避免不可重复读或幻读,就需要在SQL查询时编写加锁的SQL语句。

特点

  • 每次查询都会设置和读取自己的新快照
  • 仅支持基于行的 bin-log
  • UPDATE 优化
    半一致读(semi-consistent read)
  • 不可重复读
  • 幻读(Phantom)
    加锁后,不锁定间隙,其他事务可以 INSERT

锁定索引记录,而不锁定记录之间的间隙。

可重复读(REPEATABLE READ)

InnoDB默认的隔离级别。

保证同一事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。

原理

  • 使用事务第一次读取时创建的快照
  • 多版本技术

使用的锁

  • 使用唯一索引的唯一查询条件时,只锁定查找到的索引记录,但不锁定间隙
  • 其他查询条件, 会锁定扫描到的索引范围,通过间隙锁或next-key锁阻止其他会话在这个范围插值

可能的问题

InnoDB 不能保证没有幻读,需要加锁。

可串行化(SERIALIZABLE)

将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。

查看MySQL的事务隔离级别

mysql> show variables like '%tx_isolation%';

mysql8 之后

之前是tx_isolation,MySQL8改成了transaction_isolation

mysql> select @@global.transaction_isolation,@@transaction_isolation;+--------------------------------+-------------------------+| @@global.transaction_isolation | @@transaction_isolation |+--------------------------------+-------------------------+| REPEATABLE-READ                | REPEATABLE-READ         |+--------------------------------+-------------------------+1 row in set (0.00 sec)

mysql的默认隔离级别是可重复读,但其实对高并发业务来说,可重复读并不是最合适的,最合适的是读提交,主要是因为MySQL 5.0之前,MySQL的主从复制在度提交这个隔离级别下是有bug的。

修改MySQL隔离级别命令:

修改全局隔离级别为读提交:

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

修改会话隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

注:以上命令可大小写混用

这里要吐槽一下MySQL的命令,感觉比较混乱。比如查询出来的隔离级别是两个单词用短横线连接(READ-COMMITTED),但是设置的时候又是要把两个单词用空格分隔。风格很不统一。

MySQL客户端模拟异常

  • heros_temp表

开两个MySQL客户端1和2(后文简称为 C1,C2)分别都执行如下操作:

查看下当前会话隔离级别,把隔离级别降到最低

mysql> SHOW VARIABLES LIKE 'transaction_isolation';+-----------------------+-----------------+| Variable_name         | Value           |+-----------------------+-----------------+| transaction_isolation | REPEATABLE-READ |+-----------------------+-----------------+1 row in set (0.01 sec)mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;Query OK, 0 rows affected (0.00 sec)mysql> SHOW VARIABLES LIKE 'transaction_isolation';+-----------------------+------------------+| Variable_name         | Value            |+-----------------------+------------------+| transaction_isolation | READ-UNCOMMITTED |+-----------------------+------------------+1 row in set (0.01 sec)

MySQL默认自动提交事务,我们将autocommit参数设为0

mysql> SHOW VARIABLES LIKE 'autocommit';+---------------+-------+| Variable_name | Value |+---------------+-------+| autocommit    | ON    |+---------------+-------+1 row in set (0.01 sec)mysql> SET autocommit = 0;Query OK, 0 rows affected (0.00 sec)mysql> SHOW VARIABLES LIKE 'autocommit';+---------------+-------+| Variable_name | Value |+---------------+-------+| autocommit    | OFF   |+---------------+-------+1 row in set (0.00 sec)

模拟“脏读”

session1写一个新数据,注意不要提交

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t1 value(12,12);Query OK, 1 row affected (0.00 sec)

session2中,查看当前的数据

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> select * from t1;+----+------+| id | c    |+----+------+|  1 |    3 ||  2 |    2 ||  3 |    3 ||  4 |    4 || 10 |   10 ||  6 |    6 ||  7 |    7 ||  8 |    8 ||  9 |    9 ||  0 |    0 ||  5 |    5 || 11 |   11 || 12 |   12 |+----+------+13 rows in set (0.00 sec)

发现S2中读取了S1未提交的数据“12”,实际上S1可能马上回滚,从而造成“脏读”。

模拟“不可重复读”

session1查看id=1的数据

mysql> select * from t1 where id=1;+----+------+| id | c    |+----+------+|  1 |    1 |+----+------+1 row in set (0.00 sec)

session2对id=1的英雄姓名进行修改

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> update t1 set c=3 where id=1;Query OK, 1 row affected (0.00 sec)Rows matched: 1  Changed: 1  Warnings: 0

这时用客户端1再次进行查询

mysql> select * from t1 where id=1;+----+------+| id | c    |+----+------+|  1 |    3 |+----+------+1 row in set (0.00 sec)

ssession1的同一条查询语句出现“不可重复读”。

模拟“幻读”

session1查询数据表中的所有数据:

mysql> select * from t1;+----+------+| id | c    |+----+------+|  1 |    1 ||  2 |    2 ||  3 |    3 ||  4 |    4 || 10 |   10 ||  6 |    6 ||  7 |    7 ||  8 |    8 ||  9 |    9 ||  0 |    0 |+----+------+10 rows in set (0.00 sec)

session2,开始插入新的英雄“吕布”:

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into t1 value(5,5);Query OK, 1 row affected (0.01 sec)mysql> select * from t1;+----+------+| id | c    |+----+------+|  1 |    1 ||  2 |    2 ||  3 |    3 ||  4 |    4 || 10 |   10 ||  6 |    6 ||  7 |    7 ||  8 |    8 ||  9 |    9 ||  0 |    0 ||  5 |    5 |+----+------+11 rows in set (0.00 sec)

再用session1重新查看:

mysql> select * from t1;+----+------+| id | c    |+----+------+|  1 |    1 ||  2 |    2 ||  3 |    3 ||  4 |    4 || 10 |   10 ||  6 |    6 ||  7 |    7 ||  8 |    8 ||  9 |    9 ||  0 |    0 ||  5 |    5 |+----+------+11 rows in set (0.00 sec)

发现数据表多出一条数据。

总结

标准的价值在于,即使是不同的RDBMS都需要达成对异常问题和隔离级别定义的共识。这就意味着一个隔离级别的实现满足了下面的两个条件:

  • 正确性
    只要能满足某一个隔离级别,一定能解决这个隔离级别对应的异常问题。
  • 与实现无关
    实际上RDBMS种类很多,这就意味着有多少种RDBMS,就有多少种锁的实现方式,因此它们实现隔离级别的原理可能不同,然而一个好的标准不应该限制其实现的方式。

隔离级别越低,意味着系统吞吐量(并发程度)越大,但同时也意味着出现异常问题的可能性会更大。在实际使用过程中我们往往需要在性能和正确性上进行权衡和取舍,没有完美的解决方案,只有适合的。

转载地址:https://javaedge.blog.csdn.net/article/details/105999387 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:JDK源码解析之AbstractQueuedSynchronizer
下一篇:Java源码解析系列-ThreadLocal(不看血亏)

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月11日 23时45分44秒