在数据库中,SELECT ... FOR UPDATE
语句用于获取行级锁,它能够确保在当前事务持有锁的情况下,其他事务无法读取或修改被锁定的行。要理解这一点,先要了解数据库事务和锁的工作机制。
事务隔离级别与行级锁
数据库的事务隔离级别定义了一个事务与其他事务之间的可见性和相互影响。常见的隔离级别包括:
- Read Uncommitted(读未提交):一个事务可以读取另一个未提交事务的更改,容易导致脏读。
- Read Committed(读已提交):一个事务只能读取已经提交的事务所做的更改,防止脏读。
- Repeatable Read(可重复读):一个事务可以多次读取同一行数据,并且在这个事务期间,不允许其他事务修改该数据,从而防止不可重复读和脏读。
- Serializable(可串行化):事务完全隔离,仿佛它们是一个接一个地顺序执行,避免幻读。
SELECT ... FOR UPDATE
通常在Read Committed
或更高的隔离级别中使用,并且会在事务期间对查询的行加上排他锁(Exclusive Lock)。
FOR UPDATE
如何工作?
当执行SELECT ... FOR UPDATE
时,数据库会对选中的行加上排他锁。这意味着:
- 锁的作用范围:只有当前事务可以读取或修改这些行,其他事务在试图读取或修改这些行时会被阻塞,直到当前事务结束(提交或回滚)。
- 事务提交前的状态:在当前事务提交之前,其他事务无法看到这些行的变化,因为锁会阻止其他事务对这些行的访问。
- 事务提交后的状态:一旦当前事务提交,锁会被释放,其他事务就可以读取这些行的新状态,并且可能对其进行修改。
示例分析
假设有两个事务 T1
和 T2
,它们都尝试更新同一行数据。
- 事务
T1
执行SELECT ... FOR UPDATE
,数据库锁定了该行。 - 事务
T2
尝试执行同样的查询,但由于行已被锁定,T2
会被阻塞,直到T1
结束(提交或回滚)。
-- T1: 开始事务
BEGIN;
-- T1: 锁定 id 为 1 的账户行
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- T2: 开始事务
BEGIN;
-- T2: 尝试锁定相同的行,因 T1 持有锁而被阻塞
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- T1: 提交事务,释放锁
COMMIT;
-- T2: 现在可以锁定并操作该行
事务提交的重要性
- 锁的生命周期:锁的存在时间是从
SELECT ... FOR UPDATE
执行时开始,到事务提交或回滚时结束。在锁存续期间,其他事务无法获取该行的锁,因此也无法读取或修改。 - 提交后的可见性:一旦事务提交,锁被释放,其他事务可以立即读取或修改该行数据,并且可以看到提交后的最新状态。
为什么必须提交事务后,其他事务才能看到更改?
事务未提交时,任何操作仅对当前事务可见。未提交的事务可能会回滚(撤销更改),因此数据库需要保持数据的一致性。在FOR UPDATE
场景中,未提交事务锁定的数据对其他事务不可见,防止其他事务读取到不完整或不一致的数据。
总结
SELECT ... FOR UPDATE
语句通过在行上加锁,确保数据的并发访问是串行的。事务提交之前,其他事务无法看到或修改被锁定的行数据。这种机制有效地避免了并发引发的数据一致性问题,同时保证了事务隔离级别的正确性。