MySQL事务中幻读实践

简介: MySQL事务中幻读实践

MySQL中默认使用REPEATABLE-READ的事务隔离级别,可以避免脏读,不可重复读但是仍然会出现幻读现象。确切的说,MySQL的InnoDB可以在一定程度上防止幻读,但是不能完全避免。

Oracle默认使用READ COMMITTED事务隔离级别,

By default, InnoDB operates in REPEATABLE READ transaction isolation
level and with the innodb_locks_unsafe_for_binlog system variable 
disabled. In this case, InnoDB uses next-key locks for searches and 
index scans, which prevents phantom rows (see Section 13.6.8.5, 
“Avoiding the Phantom Problem Using Next-Key Locking”).
To prevent phantoms, InnoDB uses an algorithm called next-key 
locking that combines index-row locking with gap locking.
You can use next-key locking to implement a uniqueness check in your 
application: If you read your data in share mode and do not see a 
duplicate for a row you are going to insert, then you can safely 
insert your row and know that the next-key lock set on the successor 
of your row during the read prevents anyone meanwhile inserting a 
duplicate for your row. Thus, the next-key locking enables you to 
“lock” the nonexistence of something in your table.

MySQL manual里对可重复读里的锁的详细解释:

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE 
MODE),UPDATE, and DELETE statements, locking depends on whether the 
statement uses a unique index with a unique search condition, or a 
range-type search condition. For a unique index with a unique search 
condition, InnoDB locks only the index record found, not the gap 
before it. For other search conditions, InnoDB locks the index range 
scanned, using gap locks or next-key (gap plus index-record) locks 
to block insertions by other sessions into the gaps covered by the 
range.

第一个测试示例如下(不手动加锁)

① 事务A查询表中id为9的数据

没有没有还是没有!

② 事务B向表中插入id为9的数据,暂不提交

start TRANSACTION;
insert into t_user(id,name,age)VALUES(9,'jane1',18);
SELECT * from t_user;

没提交的数据在日志中,没有持久化到数据库!!


③ 事务A去尝试更新id为9的数据

mysql> update t_user set name='jane00'where id=9;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

④ 事务B将事务提交,此时数据持久化到数据库


⑤ 事务A再次尝试更新id为9的数据

mysql> update t_user set name='jane00'where id=9;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

发生了什么?怎么成功了,不是没有该条数据么?让我再次查一下看看:

我擦我擦,见鬼了,凭空出现了数据!!!

update 时使用了当前读(读取最新数据),再次查询的时候会查询最新数据。

第二个测试实例如下(手动加共享锁)

① 事务A查询id为10的数据并使用共享锁

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user where id =10 lock in share mode;
Empty set (0.00 sec)

② 事务B尝试插入id为10的数据

start TRANSACTION;
SELECT * from t_user;
insert into t_user(id,name,age)VALUES(10,'jane1',18);

被阻塞了,等待然后到来的是锁等待超时(事务A已经加了行级锁中的共享锁,事务B只能读,不能写):

Err] 1205 - Lock wait timeout exceeded; try restarting transaction

③ 事务A尝试更新id为10的数据

mysql> update t_user set name='janei' where id =10;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
# 很显然 空语句,那就提交吧。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

④ 事务B再次插入数据并提交

insert into t_user(id,name,age)VALUES(10,'jane1',18);
SELECT * from t_user;
COMMIT;

此时数据表中有了id为10的数据:

唔,使用共享锁好像可以了,就是等待超时会抛异常。


第三个测试示例如下(使用排它锁/独占锁/互斥锁)

① 事务A查询当前表

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user for update;
# 没有使用索引,将会加表锁

唔,很好,没有id为11的数据。

② 事务B尝试插入id为11的数据

插入前先使用排它锁查一下吧

start TRANSACTION;
select * from t_user for update;
# 直接爆异常--表被事务加表锁了,不能再加其他锁
[SQL]select * from t_user for update;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

事务A不提交,事务B是没法执行的。使用共享锁和排它锁貌似挺好用的,应该没有其他问题了吧?


第四个测试示例如下(同样使用for update)

此时数据表数据如下:

20180802112611374.jpg

① 事务A对max(id)进行加锁

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select max(id) from t_user for update;
+---------+
| max(id) |
+---------+
|      10 |
+---------+
1 row in set (0.00 sec)
# [10,+∞)范围内的id都被加了间隙锁+X锁。

② 事务B尝试插入id 4 5 和20的 数据并提交事务。

start TRANSACTION;
insert into t_user(id,name,age)VALUES(4,'jane1',18);
insert into t_user(id,name,age)VALUES(5,'jane1',18);
insert into t_user(id,name,age)VALUES(20,'jane1',18);
COMMIT;

id为4和5数据插入正常,id为20数据插入失败。

[SQL]
insert into t_user(id,name,age)VALUES(20,'jane1',18);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

③ 事务A尝试更新id为4的记录并查询表记录数

mysql> update t_user set name='januie'where id=4;
Query OK, 1 row affected (42.56 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> select * from t_user ;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 小明    |   18 |
|  2 | janus  |   18 |
|  3 | 明天    |   18 |
|  4 | januie |   18 |
|  5 | jane1  |   18 |
|  8 | jane1  |   18 |
|  9 | jane00 |   18 |
| 10 | jane1  |   18 |
+----+--------+------+
8 rows in set (0.00 sec)

什么情况?事务A已经使用了for update,怎么事务B还能插进去?为什么插入 id 为4和 5 正常,插入id为20失败?

事务A不光update成功了,而且查询记录还多出来两条!!!


第五个测试示例(不加锁,事务A只做普通查询)

① 事务A查询表记录

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 小明    |   18 |
|  2 | janus  |   18 |
|  3 | 明天    |   18 |
|  4 | januie |   18 |
|  5 | jane1  |   18 |
|  8 | jane1  |   18 |
|  9 | jane00 |   18 |
| 10 | januie |   18 |
+----+--------+------+
8 rows in set (0.00 sec)

8条记录。

② 事务B插入id为6的记录并提交

start TRANSACTION;
insert into t_user(id,name,age)VALUES(6,'jane1',18);
SELECT * from t_user;
COMMIT;

此时数据库实际有9条数据。

③ 事务A再次查询数据表记录

mysql> select * from t_user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 小明      |   18 |
|  2 | janus  |   18 |
|  3 | 明天       |   18 |
|  4 | januie |   18 |
|  5 | jane1  |   18 |
|  8 | jane1  |   18 |
|  9 | jane00 |   18 |
| 10 | januie |   18 |
+----+--------+------+
8 rows in set (0.00 sec)

嗯,很好的保证了可重复读,还是8条数据。

④ 事务A提交事务后再次查询表记录

mysql> select * from t_user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 小明    |   18 |
|  2 | janus  |   18 |
|  3 | 明天    |   18 |
|  4 | januie |   18 |
|  5 | jane1  |   18 |
|  6 | jane1  |   18 |
|  8 | jane1  |   18 |
|  9 | jane00 |   18 |
| 10 | januie |   18 |
+----+--------+------+
9 rows in set (0.00 sec)

(ÒωÓױ),数据库有9条啊,事务A刚才看的不是最新数据,是历史数据!!!


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
存储 SQL 关系型数据库
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
|
10月前
|
SQL 关系型数据库 MySQL
MySQL锁机制:并发控制与事务隔离
本文深入解析了MySQL的锁机制与事务隔离级别,涵盖锁类型、兼容性、死锁处理及性能优化策略,助你掌握高并发场景下的数据库并发控制核心技巧。
|
11月前
|
存储 监控 Oracle
MySQL事务
MySQL事务具有ACID特性,包括原子性、一致性、隔离性和持久性。其默认隔离级别为可重复读,通过MVCC和间隙锁解决幻读问题,确保事务间数据的一致性和并发性。
MySQL事务
|
9月前
|
关系型数据库 MySQL 数据库
【赵渝强老师】MySQL的事务隔离级别
数据库并发访问时易引发数据不一致问题。如客户端读取到未提交的事务数据,可能导致“脏读”。MySQL通过四种事务隔离级别(读未提交、读已提交、可重复读、可序列化)控制并发行为,默认为“可重复读”,以平衡性能与数据一致性。
488 0
|
10月前
|
关系型数据库 MySQL 数据库
MySql事务以及事务的四大特性
事务是数据库操作的基本单元,具有ACID四大特性:原子性、一致性、隔离性、持久性。它确保数据的正确性与完整性。并发事务可能引发脏读、不可重复读、幻读等问题,数据库通过不同隔离级别(如读未提交、读已提交、可重复读、串行化)加以解决。MySQL默认使用可重复读级别。高隔离级别虽能更好处理并发问题,但会降低性能。
329 0
|
安全 关系型数据库 MySQL
mysql事务隔离级别
事务隔离级别用于解决脏读、不可重复读和幻读问题。不同级别在安全与性能间权衡,如SERIALIZABLE最安全但性能差,READ_UNCOMMITTED性能高但易导致数据不一致。了解各级别特性有助于合理选择以平衡并发性与数据一致性需求。
334 1
|
SQL 安全 关系型数据库
【MySQL基础篇】事务(事务操作、事务四大特性、并发事务问题、事务隔离级别)
事务是MySQL中一组不可分割的操作集合,确保所有操作要么全部成功,要么全部失败。本文利用SQL演示并总结了事务操作、事务四大特性、并发事务问题、事务隔离级别。
5988 56
【MySQL基础篇】事务(事务操作、事务四大特性、并发事务问题、事务隔离级别)
|
Ubuntu 关系型数据库 MySQL
容器技术实践:在Ubuntu上使用Docker安装MySQL的步骤。
通过以上的操作,你已经步入了Docker和MySQL的世界,享受了容器技术给你带来的便利。这个旅程中你可能会遇到各种挑战,但是只要你沿着我们划定的路线行进,你就一定可以达到目的地。这就是Ubuntu、Docker和MySQL的灵魂所在,它们为你开辟了一条通往新探索的道路,带你亲身感受到了技术的力量。欢迎在Ubuntu的广阔大海中探索,用Docker技术引领你的航行,随时准备感受新技术带来的震撼和乐趣。
577 16
|
SQL 关系型数据库 MySQL
MySQL事务日志-Undo Log工作原理分析
事务的持久性是交由Redo Log来保证,原子性则是交由Undo Log来保证。如果事务中的SQL执行到一半出现错误,需要把前面已经执行过的SQL撤销以达到原子性的目的,这个过程也叫做"回滚",所以Undo Log也叫回滚日志。
1002 7
MySQL事务日志-Undo Log工作原理分析
|
缓存 关系型数据库 MySQL
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
891 5

推荐镜像

更多