使用行级锁来让你的事务更加稳定

155 阅读3分钟

前言

我们都知道,数据库中,为了维护并发访问和修改的一致性,可以使用事务来进行一系列的SQL语句操作。但是使用了事务,就一定没有问题了吗?

以下是在 READ COMMITTED 的事务隔离级别下(PostgreSQL 默认级别),事务并发时会出现的不可重复读(Non-repeatable Read)问题:

  • 事务A第一次读取属性a,值为10。
  • 事务B将属性a更新为20并提交。
  • 事务A第二次读取属性a,值为20。

如果隔离等级更高一些呢?在 REPEATABLE READ 的事务隔离级别下(MySQL 默认级别),事务并发时可能会出现的幻读(Phantom Read)问题:

  • 事务A查询表中所有属性a大于10的行,得到两行结果。
  • 事务B插入一行属性a为15的数据并提交。
  • 事务A再次查询表中所有属性a大于10的行,得到三行结果。

当然,我们也可以将事务隔离等级调整到最高:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 事务操作

COMMIT;

SERIALIZABLE 提供最高级别的隔离,完全防止脏读、不可重复读和幻读。它通过在事务之间引入更严格的锁定机制来实现,但可能会导致更多的锁争用和性能下降。

所以有没有比较优雅且高效的解决方案呢?

行级锁(Row-Level Lock)

行级锁是针对单行数据的锁定机制。它允许多个事务并发访问同一个表,只要它们访问的行不冲突。

  1. 锁类型
    • 共享锁(Share Lock, S) :允许其他事务读取,但不允许修改。
    • 排他锁(Exclusive Lock, X) :不允许其他事务读取或修改。
  1. 使用场景:行级锁通常用于需要对特定行进行细粒度控制的场景。例如,在并发更新某些行时,使用行级锁可以防止其他事务同时修改这些行。
  2. 性能:行级锁的粒度较细,通常会带来更高的并发性能,因为锁定的范围较小,减少了事务之间的冲突。
  3. 事务(Transaction)和锁: 事务可以使用行种锁机制来实现隔离性。

当然,除了行级锁,现代数据库也支持其他几种锁机制,如表级锁和全局锁。这里只介绍行级锁主要哈是因为其性能更好,解决前言中的问题更加实用。

显式使用行级锁

MySQL:

使用SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE语句显式锁定行,防止其他事务同时修改这些行。(注意要使用行级锁,MySQL 必须要 InnoDB 引擎)

SELECT ... FOR UPDATE 语句用于在事务中锁定选定的行,确保其他事务无法修改这些行,直到当前事务提交或回滚。它会对选定的行加上排他锁(Exclusive Lock)。

SELECT ... LOCK IN SHARE MODE 语句用于在事务中锁定选定的行,允许其他事务读取这些行,但不允许它们修改这些行。它会对选定的行加上共享锁(Shared Lock)。

BEGIN;

-- 事务A
SELECT a FROM my_table WHERE id = 1 FOR UPDATE;
-- 修改属性a的值
UPDATE my_table SET a = 20 WHERE id = 1;

COMMIT;

PostgreSQL:

使用SELECT ... FOR UPDATESELECT ... FOR SHARE(注意语法上和 MySQL 略微不同)语句显式锁定行。

-- 开始事务
BEGIN;

-- 锁定 id = 1 的行,允许其他事务读取但不允许修改
SELECT * FROM my_table WHERE id = 1 FOR SHARE;

-- 进行一些操作,比如读取数据
-- 这里可以读取,但不能修改 id = 1 的行

-- 提交事务
COMMIT;