SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
语句显式锁定目标行,或执行UPDATE
/DELETE
操作时数据库引擎(如InnoDB)会自动对涉及的行加锁。好的,这是一篇关于数据库如何加行锁的详细文章,旨在为网站访客提供清晰、专业且符合搜索引擎优化(E-A-T)原则的解释:
在数据库的世界里,行锁(Row-Level Locking) 是一种至关重要的并发控制机制,它的核心目标是允许多个用户或事务同时访问数据库的不同部分(即不同的行),同时保证当它们访问同一行数据时,能够有序地进行,防止数据混乱(如丢失更新、脏读、不可重复读等),行锁就是给数据库表中的单行数据加上一把“锁”,控制谁可以读、谁可以写。
我们如何在数据库中显式地给一行数据加上锁呢?
实现行锁的具体语法和行为会因你使用的数据库管理系统(如 MySQL InnoDB, PostgreSQL, Oracle, SQL Server)而略有不同,但核心概念和主要方式是相似的,最常见的方式是使用 SELECT
语句的特定子句:
-
SELECT ... FOR UPDATE
(用于悲观锁 – 写锁/排他锁):- 目的: 这是最常用的方式,用于在读取一行数据时就锁定它,为后续的更新或删除做准备,它告诉数据库:“我现在要读这行数据,并且我打算稍后修改它,在我提交或回滚事务之前,别让其他事务修改这行数据(甚至在某些隔离级别下,阻止其他事务以
FOR UPDATE
或FOR SHARE
方式读取它)。” - 锁类型: 它会在匹配的行上施加排他锁(Exclusive Lock, X Lock)。
- 效果: 一旦一个事务对某行施加了排他锁:
- 其他事务不能对该行再施加任何类型的锁(包括共享锁和排他锁)。
- 其他事务不能修改(UPDATE)或删除(DELETE)该行。
- 其他事务能否读取该行,取决于数据库的事务隔离级别:
- 在
READ COMMITTED
级别:其他事务通常可以读取该行的最新已提交版本(非锁定读)。 - 在
REPEATABLE READ
或SERIALIZABLE
级别:其他事务的普通SELECT
可能读取快照,但如果它们尝试使用FOR UPDATE
或FOR SHARE
读取该行,则会被阻塞。
- 在
- 效果: 一旦一个事务对某行施加了排他锁:
- 语法示例 (通用概念):
START TRANSACTION; -- 开始一个事务 SELECT * FROM your_table WHERE primary_key_column = some_value FOR UPDATE; -- ... 在此处执行基于查询结果的业务逻辑和更新操作 ... UPDATE your_table SET some_column = new_value WHERE primary_key_column = some_value; COMMIT; -- 提交事务,释放锁
- 数据库差异:
- MySQL InnoDB: 完全支持
SELECT ... FOR UPDATE
,在REPEATABLE READ
隔离级别下,它还会自动添加间隙锁(Gap Lock) 来防止幻读。 - PostgreSQL: 支持
SELECT ... FOR UPDATE
,行为受transaction_isolation
设置影响。 - Oracle: 支持
SELECT ... FOR UPDATE
,是 Oracle 中实现行级锁定的标准方式。 - SQL Server: 默认行为更倾向于在需要时自动加锁,要达到类似效果,通常结合
UPDATE
语句或在SELECT
中使用WITH (ROWLOCK, UPDLOCK)
提示(如SELECT * FROM your_table WITH (ROWLOCK, UPDLOCK) WHERE ...
)。UPDLOCK
提示获取更新锁(U Lock),在更新时会升级为排他锁(X Lock),其效果与FOR UPDATE
类似。
- MySQL InnoDB: 完全支持
- 目的: 这是最常用的方式,用于在读取一行数据时就锁定它,为后续的更新或删除做准备,它告诉数据库:“我现在要读这行数据,并且我打算稍后修改它,在我提交或回滚事务之前,别让其他事务修改这行数据(甚至在某些隔离级别下,阻止其他事务以
-
SELECT ... FOR SHARE
/LOCK IN SHARE MODE
(用于悲观锁 – 读锁/共享锁):- 目的: 用于在读取一行数据时锁定它,防止其他事务修改它,但允许其他事务同时以
FOR SHARE
方式读取它,它告诉数据库:“我现在要读这行数据,并且我需要确保在我读取期间这行数据不被修改,但我允许其他人也来读它。” - 锁类型: 它会在匹配的行上施加共享锁(Shared Lock, S Lock)。
- 效果: 一旦一个事务对某行施加了共享锁:
- 其他事务可以对该行施加共享锁(允许多个读)。
- 其他事务不能对该行施加排他锁(即不能修改或删除该行),直到所有的共享锁都被释放。
- 效果: 一旦一个事务对某行施加了共享锁:
- 语法示例 (通用概念):
START TRANSACTION; SELECT * FROM your_table WHERE some_column = some_value FOR SHARE; -- 或 LOCK IN SHARE MODE (MySQL) -- ... 执行依赖于该行数据不被更改的业务逻辑(如生成报告) ... COMMIT;
- 数据库差异:
- MySQL InnoDB: 使用
SELECT ... LOCK IN SHARE MODE
(旧语法) 或SELECT ... FOR SHARE
(较新语法,推荐)。 - PostgreSQL: 使用
SELECT ... FOR SHARE
。 - Oracle: 没有完全直接的等价物,Oracle 的读一致性模型主要依赖多版本控制(MVCC),普通
SELECT
不阻塞写,写也不阻塞读(读取历史版本)。SELECT FOR UPDATE
是主要的显式锁定机制。 - SQL Server: 类似地,可以使用
WITH (ROWLOCK, HOLDLOCK)
或WITH (ROWLOCK, SERIALIZABLE)
等提示来模拟,但行为更复杂,通常不如FOR UPDATE
常用。
- MySQL InnoDB: 使用
- 目的: 用于在读取一行数据时锁定它,防止其他事务修改它,但允许其他事务同时以
除了显式加锁,数据库何时会自动加行锁?
非常重要的一点是,即使你不显式使用 FOR UPDATE
或 FOR SHARE
,数据库在执行某些操作时,为了维护数据的一致性和事务的 ACID 属性(尤其是隔离性 Isolation 和持久性 Durability),会自动施加行锁:
UPDATE
语句: 当修改一行数据时,数据库会自动在该行(以及可能相关的索引行)上施加排他锁(X Lock),这个锁会一直持有,直到事务结束(提交或回滚)。DELETE
语句: 当删除一行数据时,数据库会自动在该行上施加排他锁(X Lock),同样持有到事务结束。INSERT
语句: 插入新行时,数据库通常会在新插入的行上施加一种特殊的排他锁(有时称为插入意向锁或直接是行锁),以防止其他事务干扰新行的插入过程,并确保数据完整。
关键注意事项与最佳实践:
- 事务是核心: 行锁的生命周期是绑定在事务(Transaction) 上的,锁在事务开始时(或第一次执行需要锁的语句时)获取,在事务提交(
COMMIT
)或回滚(ROLLBACK
)时才会被释放,忘记提交或回滚事务会导致锁长时间持有,严重阻塞其他操作。 - 索引至关重要: 行锁的有效性高度依赖于索引,当你使用
WHERE
子句(无论是显式FOR UPDATE
还是隐式的UPDATE/DELETE
)来定位要锁定的行时:WHERE
条件能有效利用索引(特别是唯一索引或主键),数据库就能精确定位到目标行并施加行锁。WHERE
条件无法使用索引(全表扫描)或使用了非唯一索引,数据库可能被迫施加更粗粒度的锁(如页锁或表锁) 来保证安全,这会显著降低并发性能,确保查询条件有合适的索引是高效使用行锁的前提。
- 隔离级别的影响: 数据库的事务隔离级别(如 Read Uncommitted, Read Committed, Repeatable Read, Serializable)对行锁的行为有深远影响:
- 它决定了在读取数据时是否需要加锁(
REPEATABLE READ
下普通SELECT
可能使用快照而不加锁)。 - 它决定了锁定的范围(
REPEATABLE READ
下FOR UPDATE
会加间隙锁)。 - 它决定了锁持有的时间(
READ COMMITTED
下,语句执行完可能就释放部分锁)。
- 它决定了在读取数据时是否需要加锁(
- 死锁风险: 行锁虽然提高了并发度,但也引入了死锁(Deadlock) 的可能性,当两个或多个事务相互等待对方持有的锁时,就会发生死锁,数据库通常有死锁检测机制,会强制回滚其中一个事务来打破僵局,应用设计应尽量按相同顺序访问资源、使用短事务、设置合理的锁等待超时来减少死锁发生。
- 性能开销: 获取和释放锁本身有开销,管理大量的行锁也需要内存,过度或不必要地使用行锁(尤其是长时间持有)会成为性能瓶颈。只在必要时加锁,并尽快提交事务释放锁。
- 锁升级: 当单个事务持有的行锁数量过多,超过了数据库的某个阈值时,为了节省管理开销,数据库可能会自动将多个行锁升级(Lock Escalation) 为一个更粗粒度的锁(如表锁),这会严重损害并发性,应尽量避免(通过优化事务、减少锁数量或调整数据库配置)。
- 显式 vs 隐式: 优先依赖数据库的隐式锁定机制(即通过
UPDATE
/DELETE
自动加锁),只有在需要确保在读取数据到实际修改之间,数据不被其他事务更改(即实现“先检查后更新”的原子性)时,才需要使用显式的SELECT ... FOR UPDATE
。SELECT ... FOR SHARE
的使用场景相对较少。
给数据库加行锁的核心方法是使用 SELECT ... FOR UPDATE
(用于写操作前的排他锁定)和 SELECT ... FOR SHARE
/ LOCK IN SHARE MODE
(用于防止数据被修改的共享锁定),并确保这些操作在事务内执行。UPDATE
和 DELETE
语句会自动施加排他锁。有效使用行锁的关键在于理解事务、结合合适的索引、选择合适的隔离级别、保持事务简短以最小化锁持有时间,并警惕死锁风险。 务必查阅你所使用的具体数据库(MySQL, PostgreSQL, Oracle, SQL Server等)的官方文档,了解其行锁实现的精确语法、行为和配置选项。
引用说明:
- 本文核心概念和机制基于关系型数据库管理系统(RDBMS)的通用原理,参考了 ANSI SQL 标准关于事务和锁定的描述。
- 具体语法和行为的差异参考了以下主流数据库的官方文档概念:
- MySQL InnoDB Locking: https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
- PostgreSQL Explicit Locking: https://www.postgresql.org/docs/current/explicit-locking.html
- Oracle Data Concurrency and Consistency: https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/data-concurrency-and-consistency.html (特别是关于
FOR UPDATE
) - SQL Server Locking in the Database Engine: https://learn.microsoft.com/en-us/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver16 (特别是关于锁定提示如
UPDLOCK
,ROWLOCK
)
- 关于事务隔离级别(ACID)的标准解释参考了数据库理论经典文献(如 Jim Gray 的著作)及数据库教材。
- 死锁处理、锁升级等优化建议综合了数据库管理的最佳实践。
原创文章,发布者:酷盾叔,转转请注明出处:https://www.kd.cn/ask/47756.html