标题:【MySQL】MySQL解决事务问题:事务隔离机制
MySQL解决事务问题:事务隔离机制
在了解完事务可能出现的问题之后,我们就来学习数据库系统中为了解决这些问题所提供的策略,那就是 事务隔离机制 。其实从名字中就可以看出来,这个功能的主要作用就是隔离不同的事务,从而达到最终的事务一致性。
事务隔离级别
为了解决脏读、不可重复读、幻读问题,SQL标准规范中定义了4个事务的隔离级别,不同的隔离级别对事务的处理不同。它们分别是:
Read Uncommitted 未提交读(未授权读),允许脏读,但不允许更新丢失(上回我们没讲,因为这个是最基本的),也可以通过“排他写锁”实现。
Read Committed 已提交读(授权读取),允许不可重复读,但不允许脏读,也可以通过“瞬间共享读锁”和“排它写锁”实现。
Repeatable Read 可重复读,禁止不可重复读和脏读,但可能出现幻读。
Serializable 序列化,要求事务序列化执行,不能并发执行,杜绝全部问题。
上面四个级别是从低到高的,越是高级的事务隔离级别,对于数据的完整和一致性越能进行有效的保证,但是,越高的级别并发性能越差。从定义中就可以看出,在序列化级别,基本就是舍弃了并发能力,让事务像普通的单线程程序流一样一个一个的执行。显然,这样的隔离级别对性能的影响还是非常大的。
在 MySQL 以及大部分的数据库应用中,默认的事务隔离级别都是 Repeatable Read ,也就是可重复读。注意,在定义中,可重复读是“可能”出现幻读,它不是百分百保证,但大部分情况下我们使用这个隔离级别就足够了,毕竟,它还是能保证不错的并发性能的。
如果你的数据库中事务操作并不多,也可以将事务隔离级别再降低为 Read Committed ,这个并发性能更好,但有重复读的问题。再往下的话,也就是你的数据库是完全不需要考虑事务隔离级别的,那么其实你也可以考虑直接使用 MyISAM 完全放弃事务相关的操作。当然,InnoDB 也不止是事务这一块和 MyISAM 有不同,这个我们可以根据业务情况再进行详细的选择。
解决上篇文章中的问题
还记得我们在上篇文章中一上来就设置了一个参数吧。
-- my.cnf
[server]
transaction-isolation = READ-UNCOMMITTED
在 MySQL 中,就是通过 transaction-isolation 这个系统变量来设置事务的隔离级别的。我们可以直接使用上面的英文名称来设置,可以不在乎大小写,但中间的分隔符号一定是一个中划线。
我们一级一级向上设置,先设置为 Read Committed 。
-- my.cnf
[server]
transaction-isolation = READ-Committed
然后进行之前我们测试过的脏读问题。
-- 事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update test_user set username = 'aaa' where id = 2199993;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 事务B
mysql> select username from test_user where id = 2199993;
+----------+
| username |
+----------+
| bbb |
+----------+
可以看到,脏读问题已经解决了,剩下的其实不用我再多说了,在当前 已提交读 的级别下试试不可重复读。肯定还是失败的,这时我们就可以将事务隔离级别再升级到 重复读 Repeatable Read 级别,然后再次测试 不可重复读 的问题。
-- 事务B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select username from test_user where id = 2199993;
+----------------------+
| username |
+----------------------+
| aaa |
+----------------------+
1 row in set (0.00 sec)
-- 事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update test_user set username = 'bbb' where id = 2199993;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 1 row affected (0.00 sec)
-- 事务B
mysql> select username from test_user where id = 2199993;
+----------+
| username |
+----------+
| aaa |
+----------+
1 row in set (0.00 sec)
可以看到不可重复读的问题也被解决了,事务B 前后两次读取的数据结果是一致的,都是在 事务A 提交之前的原始数据。
在 重复读 Repeatable Read 这个级别下,你可以再试试我们上篇文章中幻读的例子,你会发现幻读的问题也不再出现了。其实这就是说明,重复读 Repeatable Read 其实是可以解决基本幻读问题的,但并不是全部的幻读问题,比如说如果涉及到更新操作。
-- tt表就一个 c 字段,varchar类型
-- 事务B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_user;
+------+
| c |
+------+
| 我 |
+------+
1 row in set (0.01 sec)
-- 事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test_user values('您');
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
-- 事务B
mysql> select * from test_user;
+------+
| c |
+------+
| 我 |
+------+
1 row in set (0.00 sec)
-- 上面查询没问题,虽然事务A新增加了一条数据,但是查询出来的还是一条数据,没有发生幻读,但是如果我们在事务B中再更新一下
mysql> update test_user set username='他' where username='您';
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2 Changed: 2 Warnings: 0
看出来问题了吗?有两条数据受到了影响,也就是说,两条数据被修改了。在事务B中,我们期望的结果其实应该是只有前面查出的那一条数据发生了修改,但是在修改之前,事务A插入了新的数据,于是两条数据被修改了。
这个例子比较极端,因为没有任何 WHERE 条件,但是,如果涉及到真实的业务开发,也会有不只是针对主键或唯一索引的批量修改操作,这种情况下,WHERE 条件也可能是多条数据的,这样就会出现幻读问题。
当然,这种问题和业务条件还是有关系的,比如我们可以在 WHERE 条件中再指定一个日期条件或者范围条件,保证新插入的数据不在更新的范围内。这样就可以在不改变事务隔离级别,也不用其它任何操作的情况下解决这种更新幻读问题。除了业务上的操作之外,最后一个隔离级别 序列化 Serializable 也可以解决这个问题。但是,它真的会完全撇弃并发,不信你拿上面的例子试试,在 事务B 没有提交之前,事务A 是会一直卡在 insert 操作上的,其实就是上了一个完全的排它读写锁,当前事务没有完成前,别的事务就根本不给写入操作的机会。
总结
对于事务这一块的内容我们就学习的差不多了,除了事务隔离级别之外,其实有一个 MVVC 的概念,它是基于乐观锁理论实现隔离级别的方式,用于 已提交读 和 可重复读 隔离级别的实现。在 重复读 的级别下,SELECT 会执行快照读,也就是以事务中第一个 SELECT 的结果为缓存,从而解决读取数据的幻读问题。但是,更新、添加、删除操作则不会走快照读,它们都需要知道最新的数据情况,所以 MVVC 和 重复读 无法解决这种情况下的 幻读 问题。这一块的内容大家可以再自行查阅相关的资料进行更深入的学习。