你们也许据说过 MySQL 的事务在高并发执行的时候可能会发生脏读、不可重复读、幻读等问题。对于有处理高并发经验的老鸟,可能认知会更深一些因此以为 so easy~「老鸟请点红叉离开,或者发起友好评论O(∩_∩)O哈哈~」,不过对于像我这种难以接触到高并发业务场景的初学者来讲,也就只能看几篇博文,了解一下概念,纸上谈兵/(ㄒoㄒ)/~~。不过本着「打破砂锅问到底」的精神,决定经过作实验来提升对其理解,顺便加强记忆(起码找工做被问到还能说两句)。mysql
MySql 事务隔离级别和容许并发反作用,分别以下表:sql
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read uncommitted) | 是 | 是 | 是 |
不可重复读(read committed) | 否 | 是 | 是 |
可重复读(repeatable read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
由上表可知,MySQL 共支持四种事务隔离级别。表由上到下容许并发反作用愈来愈弱,彷佛咱们只要选择串行化(serializable)
的事务隔离级别就不会发生脏读、不可重复读、幻读等问题了,可是选择串行化(serializable)
却会带来必定的性能降低。因此关于如何选择事务隔离级别咱们须要对脏读、不可重复读、幻读有必定认知,并肯定这几种反作用对应用的影响,而后选择合适的隔离级别。数据库
MySQL 的默认事务隔离级别为 可重复读(repeatable read)
因此咱们不用担忧「脏读」和「不可重复读」。后端
查询 MySQL 事务隔离级别的语句以下:session
select @@tx_isolation;
/* 输出结果: +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ */
复制代码
设置事务隔离级别:并发
-- 设置事务隔离级别为 read committed,仅在本次会话中生效
set session transaction isolation level read committed;
复制代码
或者能够修改 my.cnf
配置文件使其永久生效。高并发
[mysqld]
transaction-isolation = REPEATABLE-READ
复制代码
本次实验采用 MySql 5.7.21 版本(储存引擎为 Innodb),测试数据表结构以下:性能
/* +-------+----------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+----------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | <null> | auto_increment | | name | char(20) | NO | | <null> | | | money | float | NO | | 0 | | +-------+----------+------+-----+---------+----------------+ */
复制代码
脏读的概念以下:学习
事务中的修改,即便没有提交,对其余事务也都是可见的。事务能够读取未提交的数据,这也被称做脏读。测试
我的认为脏读的反作用是最大的,如今经过实验证实脏读的危害。
实验users
表以下:
/* +----+------+--------+ | id | name | money | +----+------+--------+ | 1 | 小王 | 1000.0 | | 2 | 小明 | 0.0 | +----+------+--------+ */
复制代码
实验步骤表:
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | 设置事务隔离级别为 read uncommitted | 设置事务隔离级别为 read uncommitted |
T2 | 开始事务 Abegin; |
|
T3 | 小王转款给小明 500 元update users set money=money-500 where id = 1; update users set money=money+500 where id = 2; |
|
T4 | 开始事务 Bbegin; |
|
T5 | 查询小明帐户余额select * from users where id = 2; 查询结果为 500 元,余额充足则执行支付逻辑 |
|
T6 | 小明帐户扣款 100 元update users set money=money-100 where id = 2; 本条语句将会阻塞 |
|
T7 | 事务 A 回滚rollback; |
语句执行完毕 |
T8 | 事务 B 提交commit; |
最后咱们查询users
表,结果以下:
/* +----+------+--------+ | id | name | money | +----+------+--------+ | 1 | 小王 | 1000.0 | | 2 | 小明 | -100.0 | +----+------+--------+ */
复制代码
使人惊讶的结果,小明的余额变成了 -100 元!这就是脏读的危害,咱们重点看上表的 T5,发如今事务 A 还未提交之时事务 B 便已经读取到了事务 A 更新后的结果,这直接致使了咱们程序判断余额充足从而执行了扣款的逻辑。若是事务 A 成功提交那么程序结果就是正确的,可是事务 A 最后没有成功提交而是进行了回滚,这就致使了用户余额被扣款为负数的灾难。
不可重复读的概念以下:
一个事务开始时,只能看见已经提交的事务所作的修改。换句话说,一个事务从开始直到提交以前,所作的任何修改对其余事务都是不可见的。可是两次执行一样的查询,可能会获得不同的结果。
实验users
表以下:
/* +----+------+--------+ | id | name | money | +----+------+--------+ | 1 | 小王 | 1000.0 | | 2 | 小明 | 0.0 | +----+------+--------+ */
复制代码
实验步骤表:
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | 设置事务隔离级别为 read committed | 设置事务隔离级别为 read committed |
T2 | 开始事务 Abegin; |
|
T3 | 查询小明余额select * from users where id = 2; 余额为 0 元 |
|
T4 | 开始事务 Bbegin; |
|
T5 | 小明帐户充值100元update users set money=money+100 where id = 2; |
|
T6 | 事务 B 提交commit; |
|
T7 | 查询小明余额select * from users where id = 2; 余额为 100 元 |
|
T8 | 事务 A 提交commit; |
不可重复读表如今于在同一个事务之中,两个相同的查询获得的查询结果却不一样。这是因为两个查询结果之间,出现另一个事务修改了包含以前查询结果的记录,致使第二次查询与第一次查询结果不一样。它与脏读的区别在于修改记录的事务 B 必须提交成功,查询事务 A 才能读取到修改后的记录,若是事务 B 回滚了,事务 A 的查询结果仍是同样的。
幻读概念以下:
所谓幻读,指的是当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,当以前的事务再次读取该范围的记录时,会产生幻行。InnoDB存储引擎经过多版本并发控制(MVCC)解决了幻读的问题。
通过本人测试发如今 可重复读(repeatable read)
的事务隔离级别下,MySQL 不会产生幻行可是能够经过写入一行数据来证实幻读问题的存在。
实验users
表以下:
/* +----+------+--------+ | id | name | money | +----+------+--------+ | 1 | 小王 | 1000.0 | | 2 | 小明 | 0.0 | +----+------+--------+ */
复制代码
实验步骤表:
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | 设置事务隔离级别为 repeatable read | 设置事务隔离级别为 repeatable read |
T2 | 开始事务 Abegin; |
|
T3 | 开始事务 Bbegin; |
|
T4 | 插入一行insert into users(id, name, money) values (3, "小红",1000); |
|
T5 | 事务 B 提交commit; |
|
T6 | 查询users 表select * from users; 并没有 id 为 3 的记录 |
|
T7 | 插入一行insert into users(id, name, money) values (3, "小红",1000); |
|
T8 | 出现报错:(1062, u"Duplicate entry '3' for key 'PRIMARY'") |
对于事务 A 来讲出现的报错就像见鬼了同样,由于事务 A 在查询 users
表的结果并不存在 id
为 3 的行!而在插入该行时却出现了该行已存在的报错……也许这就是叫幻读的缘由吧。
网上已有不少这种类型的文章,本文也参考了许多内容,之因此还要「老调重弹」是由于「纸上得来终觉浅,绝知此事要躬行」,实践才是检验真理的惟一标准,固然本文也可能出现谬误,欢迎指正。心里 OS:数据库真的后端的一块大头,不想成天 CRUD 就要更深刻的学啊。感受《高性能 MySQL》这本书不错,有空要研读一下,最后以为颇有必要学习关于 MySQL 锁相关的内容。