MySQL显式锁简单介绍

MySQL显式锁简单介绍

对于MySQL数据库而言, 事务的隔离级别在不同程度上保证了数据一致性。

我们知道,事务的四大特性:原子性、一致性、隔离性、持久性,其中隔离性就是通过锁机制来保证的。

另外3个性质,通过MySQL的redo logundo 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;

拓展阅读: