隔离做的好,数据操作没烦恼[MySQL]
古时的风筝 原创系列
经常提到数据库的事务,那你知道数据库还有事务隔离的说法吗,事务隔离还有隔离级别,那什么是事务隔离,隔离级别又是什么呢?本文就帮大家梳理一下。
MySQL 事务
数据库事务指的是一组数据操作,事务内的操作要么就是全部成功,要么就是全部失败,什么都不做,其实不是没做,是可能做了一部分但是只要有一步失败,就要回滚所有操作,有点一不做二不休的意思。
假设一个网购付款的操作,用户付款后要涉及到订单状态更新、扣库存以及其他一系列动作,这就是一个事务,如果一切正常那就相安无事,一旦中间有某个环节异常,那整个事务就要回滚,总不能更新了订单状态但是不扣库存吧,这问题就大了。
事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID,缺一不可。今天要说的就是隔离性。
概念说明
脏读
可重复读
不可重复读
幻读
而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
事务隔离级别
SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:
-
读未提交(READ UNCOMMITTED) -
读提交 (READ COMMITTED) -
可重复读 (REPEATABLE READ) -
串行化 (SERIALIZABLE)
从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。
只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。
一探究竟
如何设置隔离级别
我们可以通过以下语句查看当前数据库的隔离级别,通过下面语句可以看出我使用的 MySQL 的隔离级别是 REPEATABLE-READ,也就是可重复读,这也是 MySQL 的默认级别。
# 查看事务隔离级别 5.7.20 之后
show variables like 'transaction_isolation';
SELECT @@transaction_isolation
# 5.7.20 之后
SELECT @@tx_isolation
show variables like 'tx_isolation'
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
稍后,我们要修改数据库的隔离级别,所以先了解一下具体的修改方式。
比如下面这个语句的意思是设置全局隔离级别为读提交级别。
mysql> set global transaction isolation level read committed;
MySQL 中执行事务
事务的执行过程如下,以 begin 或者 start transaction 开始,然后执行一系列操作,最后要执行 commit 操作,事务才算结束。当然,如果进行回滚操作(rollback),事务也会结束。
begin;
select * from xxx;
commit; -- 或者 rollback;
另外,通过以下语句可以查询当前有多少事务正在运行。
select * from information_schema.innodb_trx;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) DEFAULT NULL,
`age` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
初始只有一条记录:
mysql> SELECT * FROM user;
+----+-----------------+------+
| id | name | age |
+----+-----------------+------+
| 1 | 古时的风筝 | 1 |
+----+-----------------+------+
读未提交
set global transaction isolation level read uncommitted;
设置完成后,只对之后新起的 session 才起作用,对已经启动 session 无效。如果用 shell 客户端那就要重新连接 MySQL,如果用 Navicat 那就要创建新的查询窗口。
读提交
set global transaction isolation level read committed;
之后需要重新打开新的 session 窗口,也就是新的 shell 窗口才可以。
每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的。
读提交解决了脏读的问题,但是无法做到可重复读,也没办法解决幻读。
可重复读
set global transaction isolation level repeatable read;
在这个隔离级别下,启动两个事务,两个事务同时开启。
可重复读做到了,这只是针对已有行的更改操作有效,但是对于新插入的行记录,就没这么幸运了,幻读就这么产生了。我们看一下这个过程:
要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,MySQL 的可重复读隔离级别其实解决了幻读问题,这会在后面的内容说明
串行化
MySQL 是如何实现事务隔离的
实现可重复读
按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录这使其产生的事务 ID,比如事务A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。
对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:
-
当前事务内的更新,可以读到; -
版本未提交,不能读到; -
版本已提交,但是却在快照创建后提交的,不能读到; 版本已提交,且是在快照创建前提交的,可以读到;
并发更新问题
update user set age=11 where id = 1
id 是这张表的主键,是有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,然后干净利落的加上行锁就可以了。
update user set age=11 where age=10
表中并没有为 age 字段设置索引,所以, MySQL 无法直接定位到这行数据。那怎么办呢,当然也不是加表锁了。
解决幻读
此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。
如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。
在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起的作用。
update user set name='风筝2号’ where age = 10;
的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边。
总结
读提交解决了脏读问题,行锁解决了并发更新的问题。并且 MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。
cnblogs.com/fengzheng/p/12557762.html