MySQL显式锁简单介绍
对于MySQL数据库而言, 事务的隔离级别在不同程度上保证了数据一致性。
我们知道,事务的四大特性:原子性、一致性、隔离性、持久性,其中隔离性就是通过锁机制来保证的。
另外3个性质,通过MySQL的
redo log
和undo log
来保证。
MySQL对每条SQL语句的执行,都添加了一个隐式事务,言外之意,就是添加了隐式锁。
除了隐式锁之外,MySQL还可以使用显式锁。
这是从锁的可见性(或者使用方式)上来区分锁。本文不讨论MySQL的粒度锁(表锁,行锁,页锁)。
实际开发中,很少的业务场景需要使用显式锁,基本上运用好MySQL的事务隔离机制,就可以处理好基本上95%大体上的问题。 本文简单地讨论了MySQL显式锁的使用。
MySQL显式锁可以简单地分为2类:
- 读锁(S锁,共享锁,Shared Lock)
- 写锁(X锁,排它锁,Exclusive Lock)
读锁 #
MySQL使用
select ... lock in share mode
或者(8)
select ... for share
意思很明确,读锁是共享的,不同的会话可以同时获取读锁,因为读并不会改变共享数据,可以提升并发性能。实际上,读”可能“不需要加锁。
耗时长的事务,可以对“读”加锁,来防止其他会话对数据进行改动。
在MVCC的行为中,可重复读隔离级别下,新会话是可以改动其他事务读取的数据的,不过这个改动,在已经开始的事务中是不可见的(快照,以及保证可重复读)。
1mysql> begin;
2Query OK, 0 rows affected (0.00 sec)
3
4mysql> select * from user where id = 1 for share;
5+----+------+--------+------+------------+
6| id | name | gender | age | note |
7+----+------+--------+------+------------+
8| 1 | anna | 1 | 17 | quiet girl |
9+----+------+--------+------+------------+
101 row in set (0.00 sec)
会话1: 开启事务,并且对id=1的数据行添加读锁
1mysql> begin;
2Query OK, 0 rows affected (0.00 sec)
3
4mysql> select * from user where id = 1 for share;
5+----+------+--------+------+------------+
6| id | name | gender | age | note |
7+----+------+--------+------+------------+
8| 1 | anna | 1 | 17 | quiet girl |
9+----+------+--------+------+------------+
101 row in set (0.00 sec)
11
12mysql> select * from user where id = 1 for update;
13^C^C -- query aborted
14ERROR 1317 (70100): Query execution was interrupted
15
16mysql> update user set age = 16 where id = 1 ;
17^C^C -- query aborted
18ERROR 1317 (70100): Query execution was interrupted
会话2: 开启事务,更新id=1的数据失败
很明显可以看到:
- 不同的会话可以对同一数据加读锁;
- 不同的会话无法获取同一数据的写锁;
- 加读锁可以保护数据免被修改;
写锁 #
MySQL使用
select ... for update
对数据加写锁。
1mysql> begin;
2Query OK, 0 rows affected (0.00 sec)
3
4mysql> select * from user where id = 1 for update;
5+----+------+--------+------+------------+
6| id | name | gender | age | note |
7+----+------+--------+------+------------+
8| 1 | anna | 1 | 17 | quiet girl |
9+----+------+--------+------+------------+
101 row in set (0.00 sec)
11
12mysql> update user set name = 'annie' where id = 1;
13Query OK, 1 row affected (0.00 sec)
14Rows matched: 1 Changed: 1 Warnings: 0
会话1: 开启事务,对id=1的数据行添加写锁
1mysql> select * from user where id = 1 for share ;
2^C^C -- query aborted
3ERROR 1317 (70100): Query execution was interrupted
4
5mysql> update user set name = 'anna' where id = 1 ;
6^C^C -- query aborted
7ERROR 1317 (70100): Query execution was interrupted
8mysql> select * from user where id = 1 for share skip locked;
9Empty set (0.00 sec)
10
11mysql> select * from user where id = 1;
12+----+------+--------+------+------------+
13| id | name | gender | age | note |
14+----+------+--------+------+------------+
15| 1 | anna | 1 | 17 | quiet girl |
16+----+------+--------+------+------------+
171 row in set (0.00 sec)
会话2: 开启事务,尝试获取id=1的数据行的锁
可以看到,对数据加写锁之后:
- 其他会话无法获取读锁;
- 其他会话无法更新数据;
- MySQL提供了
skip locked
语句来跳过锁 - 可以查询事务开始前的数据,无法获取事务开始后其他会话的更新(MVCC)。
在开发实践中,一般使用乐观锁机制。一些中间件框架(如
mybatis-plus
)支持使用乐观锁更新数据。乐观锁是一种思想,它默认没有其他会话更改数据,因此总是尝试直接更改数据,而不是去加锁。这样可以提高读取性能。
常用“版本号”来作为乐观锁的实现机制。意思就是在表中添加
version
字段。其基本逻辑是:
flowchart LR A(更新前获取版本号 v1) --> B(更新时获取版本号 v2) B --> C{v1 == v2?} C --> |Y| D(更新 v = v1 + 1) C --> |N| E(不更新)1update ... set version = version + 1 where `version` = version;
拓展阅读: