点击↑上方↑蓝色“编了个程”关注我~
![7b84b6a8c80d1ca59478e821d78b9506.png](https://i-blog.csdnimg.cn/blog_migrate/e9e74318abd71133aba15efea0c66205.png)
每周至少一篇原创文章
这是本公众号的第 31 篇原创文章
![9f7ea240efa389013d67aefe9234f8f4.png](https://i-blog.csdnimg.cn/blog_migrate/ef2c4671a97157eb2067db1f2bf1b6d1.png)
荒腔走板
这周暂时也没有荒腔走板环节,太忙了最近。。。
从事务的隔离级别谈起
众所周知,事务有四大特性,简称ACID:原子性、一致性、隔离性、持久性。
对于隔离性,简单来说就是多个事务之间是彼此隔离的,互不影响。但想要做到完全的互不影响是很难的,因为数据的强一致性,很多时候需要牺牲性能去达成。比如如果我们能接受事务的串行执行,那一定是互不影响的。然而现实是,MySQL作为一个数据库,必然是要支持一定程度的并行执行的,也就是多个事务同时去执行。
❝凡并行程序,往往是在性能和数据一致性上做取舍。较好的解决方案要么是最终一致,要么是尽量缩小串行执行的范围。
❞
如果多个事务同时并行执行,在没有隔离的情况,可能会发生脏读、不可重复读、幻读的问题。
案例数据(demo表):
id(主键) | c(普通索引) | d(无索引) |
---|---|---|
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
20 | 20 | 20 |
25 | 25 | 25 |
「脏读」
一个事务读取了另一个事务未提交的数据。
![dc615bbb821f2e4c87cf0dd49cfd8aa9.png](https://i-blog.csdnimg.cn/blog_migrate/1a01fd1bf3f55903bd7d508b8c317228.png)
「不可重复读」
一个事务读取同一行数据,多次读取结果不同。
![f8dbc2336b453c86a018320b2309e3e5.png](https://i-blog.csdnimg.cn/blog_migrate/cb7a8bbaad1b459448ed230f68eeb9b2.png)
「幻读」
一个事务读取到了别的事务插入的数据。
![ab460f5d31b68e2025591cfd291f8347.png](https://i-blog.csdnimg.cn/blog_migrate/673fd76e9b89fb13c8aa665c7c2d802c.png)
但InnoDB因为使用了MVCC,读取的是“快照”版本,有一些不同,但如果不上锁,同样可能会有幻读问题。
![a6c3e6b71724761ed63892b26b08ba8f.png](https://i-blog.csdnimg.cn/blog_migrate/c9e67d330fee301a61a40f7cc047b7bd.png)
事务用了四种不同的隔离级别用来解决这些问题。
- Read uncommitted(未提交读)
- Read Committed(已提交读,简称RC)
- Repeatable Reads(可重复读,简称RR)
- Serializable(串行化)
隔离级别越高,解决的问题越多,但并发性能也会越差。它们之间的关系如下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | 是 | 是 | 是 |
Read Committed | 是 | 是 | |
Repeatable Reads | 是 | ||
Serializable |
❝但InnoDB有些许不同,InnoDB默认的隔离级别是RR,但是通过MVCC和间隙锁来一定程度上的解决了幻读的问题。这也是我们今天这篇文章后面会详细介绍的。
❞
无锁思想:MVCC
MVCC即“多版本并发控制”,但是它在很多情况下避免了加锁操作,因此开销更低。
主流的关系型数据库都实现了MVCC,但实现机制各有不同。实际上MVCC也没有一个统一的标准。但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。本文以下内容所说的MVCC都指的是InnoDB实现的MVCC。
在Mysql的InnoDB引擎,是通过给每行记录后面保存两个隐藏的列来实现的。一个是保存行的创建时间,另一个保存了行的过期时间(或删除时间)。
❝实际上存储的并不是实际的一个时间戳,而是“系统版本号”。
❞
每次开启一个事务,系统版本号都会递增。事务开始时,系统版本号会作为事务的版本号,用来和查询到的行的版本号进行比较。
MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作,其它两个隔离级别不能工作。因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
在MySQL中,正常的SELECT
语句,后面不加FOR UPDATE
和LOCK IN SHARE MODE
的,就是用的MVCC去读。
❝MVCC和我们在应用层面去实现的“乐观锁”有一样的思想:用版本号,在尽量无锁的情况下实现一定程度的一致性。
❞
InnoDB行锁的概念
InnoDB的行锁(也称为临键锁) Next-Key Locks,「是MySQL对外暴露的锁的基本单位,它会智能选择记录锁或间隙锁,锁住一行或多行或一个间隙」。而记录锁又分为共享锁和排他锁,间隙锁的概念下面有一个插入意向锁。这些锁的关系大概是这样:
![21c5b9e0b33f629dd277040aef42fba8.png](https://i-blog.csdnimg.cn/blog_migrate/9228c86b6b84e0d23d6124ee12b7cd9f.png)
记录锁
所谓记录锁 Record Locks,就是锁住确定的一行行记录。它分为共享锁和排它锁。分别对应不同的SQL写法。
共享锁
共享锁 Shared Locks ,简称S锁。使用以下SQL可能触发:
SELECT ... LOCK IN SHARE MODE
之所以说“可能”触发,是因为它查到了数据库有确定的记录才会锁住这些记录,否则会变成间隙锁。这个其实很好理解,找到了数据,才锁它。如果没找到数据,就锁这个间隙。
排他锁
排他锁 Exclusive locks ,简称X锁。使用一下SQL可能触发:
SELECT ... FOR UPDATE
这里的“可能”含义与上面同理,不赘述。
间隙锁
间隙锁 Gap - Lock,顾名思义,锁住一个间隙。上文我们提到过,InnoDB默认的隔离级别是RR,但是通过间隙锁来一定程度上的解决了幻读的问题。它是怎么解决的呢?就是通过间隙锁来解决的。
上面两种SQL,如果没有查找到确定的记录,就会根据条件去锁住一个间隙。间隙锁是根据已有数据的一个左开右闭的区间。
还是这个案例数据(假设数据都是从1开始):
id(主键) | c(普通索引) | d(无索引) |
---|---|---|
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
20 | 20 | 20 |
25 | 25 | 25 |
对于下面这些区间的操作,会有对应的间隙锁:(0, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, 正无穷)。
什么意思呢?假如你的SQL查询的范围不同,那它锁住的区间就不同。比如:
-- 锁住(0, 5]
SELECT * FROM demo where id = 3;
-- 锁住(10, 15]
SELECT * FROM demo where id = 11;
间隙锁其实是“共享”的。也就是说,多个事务可以获取同一个区间的间隙锁。
![be493eca5599ed606dbad1fe4ae84dfd.png](https://i-blog.csdnimg.cn/blog_migrate/8ad16ca76e5b4e040129aac516f31563.png)
插入意向锁
插入意向锁 Insert Intention Locks,代表当前事务准备插入一行数据。使用INSERT/UPDATE/DELETE等语句会获得插入意向锁。
「插入意向锁和插入意向锁之间是兼容的,只要插入的键值不同,就不会相互阻塞」。比如以下两个SQL,在不同的事务中,哪怕它们在同一个间隙,只要没有间隙锁,就不会阻塞:
![8b0373449f39afa7d7e7c387f4854f18.png](https://i-blog.csdnimg.cn/blog_migrate/7c48de93d1a05c5e707ab965397b0313.png)
但如果两个事务插入同一个key,那就会阻塞。
![706e64925e24de0363b320dceda0ff67.png](https://i-blog.csdnimg.cn/blog_migrate/7ba811e74e5432852e74e92d0d8fabb6.png)
插入意向锁可以保证两个事务插入key不同的数据的时候不冲突,提升并发性。
「但是间隙锁会阻塞插入意向锁」!这也可以理解,因为InnoDB想在RR隔离级别就解决幻读问题。所以A事务用SELECT语句获取了一个间隙锁,自然不希望B事务在这个期间往这个间隙插入一条新的记录。
![40acda78e8cf1364b1795ebcd3d57080.png](https://i-blog.csdnimg.cn/blog_migrate/3a7407a2dcdd8d24782149b7e902be7e.png)
与索引的关系
不管哪种行级锁,「行级锁的其实都是索引」。所以在上面的demo中,如果对id(主键)或者column c(普通索引)操作,都会触发相应的行级锁,但如果对column d(无索引)做同样的操作,InnoDB就会对表中所有数据加锁,实际效果跟表级锁一样。
所以一定要注意,如果要上锁,需要注意是否走了索引,不要弄成了表级锁造成安全事故。
一个死锁案例
最后给一个关于间隙锁和插入意向锁的死锁案例吧,也是之前在项目上遇到过的真实案例。
过程
- 事务A select ... for update 查找一个不存在的数据,获得间隙锁;
- 事务B select ... for update 查找一个不存在的数据,获得相同位置的间隙锁;
- 事务A insert into ... 到这个间隙,尝试获得插入意向锁,但被事务B持有的间隙锁阻塞。
- 事务B insert into ... 到这个间隙,尝试获得插入意向锁,但被事务A持有的间隙锁阻塞,MySQL监测到死锁,回滚事务B;
- 因为事务B回滚,所以事务B持有的间隙锁被释放,所以之前事务A的插入语句不再阻塞,顺利执行插入操作。
❝报错信息:Deadlock found when trying to get lock; try restarting transaction
❞
用图把这个过程描述出来:
![7f4a1deb94fb44549703d22992971d64.png](https://i-blog.csdnimg.cn/blog_migrate/40ee92c29b7e0c34e80bc52504efcb01.png)
最终结果
事务A插入成功,事务B因为死锁被回滚。
总结
间隙锁主要还是用于防止幻读的情况,所以多个事务能够同时获取同一段间隙锁本身并没有问题,间隙锁能够阻塞插入意向锁也并没有问题。
而插入意向锁可以看成是一种特殊的间隙锁,是用于在同一个间隙,插入不同的数据,不会互相阻塞。它比普通间隙锁的数据一致性更低,但并发性能更好;
解决思路
普通间隙锁还是主要用于读操作防止幻读。所以我们在想进行插入操作的时候,其实没有必要对即将插入的间隙使用普通间隙锁,直接使用insert语句产生的插入意向锁就好了。
如果要保证数据的一致性,可以使用插入意向锁配合主键、唯一键等约束。
附录
那些在文章中用过的SQL:
-- 建表
create table demo
(
id int unsigned not null,
c int unsigned not null,
d int unsigned not null,
constraint demo_pk
primary key (id),
constraint idx_c
unique (c)
);
-- 插数据
INSERT INTO demo VALUES
(5, 5, 5),
(10, 10, 10),
(15, 15, 15),
(20, 20, 20),
(25, 25, 25);
-- 设置隔离级别 未提交读
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;
-- 设置隔离级别 已提交读
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED ;
-- 设置隔离级别 可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ ;
-- 设置隔离级别 串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
-- 禁止自动提交事务
SET AUTOCOMMIT = 0;
-- 开启事务
BEGIN;
-- 提交事务
COMMIT;
-- 会获取锁的查询
SELECT * FROM demo WHERE id 5 FOR SHARE ;
![3dbeb1769e5c015f0dbaa3360fc20816.png](https://i-blog.csdnimg.cn/blog_migrate/fb4f5d7d9e492b378d3fe4de2438c4e8.png)
我是Yasin,一个在不断精进的菜鸡
微信公众号:编了个程(blgcheng)
个人网站:https://yasinshaw.com
这里很多技术干货,关注肯定不后悔
加个星标可以第一时间看到最新文章
听说,转发和在看的人都升职加薪了
原创不易,四连走起,
关注在看,评论转发,
感谢支持!
![7f92c65783bc583bdb4004cbe69bf94a.png](https://i-blog.csdnimg.cn/blog_migrate/b324db51056df5ed7af68e13b0e90b56.png)
![0f6f6f4c8ebbde57c219e2a3141f19f6.gif](https://i-blog.csdnimg.cn/blog_migrate/1b4a2bf598526fa297f2e173b85c5ef6.gif)
Pipeline设计模式,了解一下?
关于Spring AOP的灵魂十问
源码解析 - Spring如何实现IoC的?
一个慢SQL引起的惨案