SELECT FOR UPDATE的锁粒度
前言
- 在数据库事务处理中,处理数据库并发访问的请求一个复杂且重要的问题,当多个事务同时访问并尝试修改同一行数据时,可能会导致数据的一致性问题,例如丢失更新或脏读的情况。为了解决这些问题,数据库提供了一些机处理并发事务,其中之一便是
SELECT ... FOR UPDATE
语句。 - 在并发访问的环境中,
SELECT ... FOR UPDATE
允许事务在选择数据的同时,锁定这些数据,防止其他事务修改
这些数据,直到当前事务完成并释放锁。从本质上,SELECT ... FOR UPDATE
是一种悲锁的用法,一般情况下,只会锁住一行数据,但是如果没有正确使用的话,会把整张表都锁住的。 - 我也在实际项目中使用过,例如免费字数抵扣金额下单的场景。
实践
- 虽然在MySQL中是通过
SELECT ... FOR UPDATE
语句来实现的行锁的功能。但是如果你在实际工作中使用不正确,也容易把整张表锁住,严重影响性能。SELECT ... FOR UPDATE
语句的用法是否正确,跟WHERE条件
中的参数有很大的关系。我们先来简单建个表,然后分析一下下面几种情况。
1 | CREATE TABLE user_info ( |
主键字段
- 当
WHERE
条件使用主键时
- 在事务一中使用
FOR UPDATE
,加一个行锁,注意此时没有COMMIT
提交事务
1 | START TRANSACTION; -- 开启事务 |
- 在事务二中尝试修改该行,那么在执行的过程中,会一直等待事务一释放锁。
1 | UPDATE user_info SET amount = 10000 WHERE id = 1; |
- 如果事务一,一直都不释放锁,那么事务二最终会报这个异常
1 | UPDATE user_info SET amount = 10000 WHERE id = 1 |
- 当事务一提交事务后,事务二才能正常执行
1 | COMMIT; |
- 如果此时开启一个事务三,修改其他行的值,那么是可以正常执行的
1 | UPDATE user_info SET amount = 10000 WHERE id = 2; |
- 执行结果
1 | UPDATE user_info SET amount = 10000 WHERE id = 2 |
唯一索引
- 当
WHERE
条件使用唯一索引时
- 在事务一中使用
FOR UPDATE
,加一个行锁,注意此时没有COMMIT
提交事务
1 | START TRANSACTION; |
- 在事务二中尝试修改该行,那么在执行的过程中,会一直等待事务一释放锁。
1 | UPDATE user_info SET amount = 10000 WHERE username = 'john_doe'; |
- 如果事务一,一直都不释放锁,那么事务二最终会报这个异常
1 | UPDATE user_info SET amount = 10000 WHERE username = 'john_doe' |
- 当事务一提交事务后,事务二才能正常执行
1 | COMMIT; |
- 如果此时开启一个事务三,修改其他行的值,那么是可以正常执行的
1 | UPDATE user_info SET amount = 1000 WHERE username = 'alice_smith'; |
- 执行结果
1 | UPDATE user_info SET amount = 1000 WHERE username = 'alice_smith' |
普通索引
- 当
WHERE
条件使用普通索引时
- 在事务一中使用
FOR UPDATE
,加一个行锁,注意此时没有COMMIT
提交事务
1 | START TRANSACTION; |
- 在事务二中尝试修改该行,那么在执行的过程中,会一直等待事务一释放锁。
1 | UPDATE user_info SET amount = 1000 WHERE email = 'john.doe@example.com'; |
- 如果事务一,一直都不释放锁,那么事务二最终会报这个异常
1 | UPDATE user_info SET amount = 1000 WHERE email = 'john.doe@example.com' |
- 当事务一提交事务后,事务二才能正常执行
1 | COMMIT; |
- 如果此时开启一个事务三,修改其他行的值,那么是可以正常执行的
1 | UPDATE user_info SET amount = 10000 WHERE email = 'alice.smith@example.com'; |
- 执行结果
1 | UPDATE user_info SET amount = 10000 WHERE email = 'alice.smith@example.com' |
普通字段
- 当
WHERE
条件使用普通字段时
- 在事务一中使用
FOR UPDATE
,加一个行锁,注意此时没有COMMIT
提交事务
1 | START TRANSACTION; |
- 在事务二中尝试修改该行,那么在执行的过程中,会一直等待事务一释放锁。
1 | UPDATE user_info SET amount = 1000 WHERE amount = 2500; |
- 如果事务一,一直都不释放锁,那么事务二最终会报这个异常
1 | UPDATE user_info SET amount = 1000 WHERE amount = 2500 |
- 当事务一提交事务后,事务二才能正常执行
1 | COMMIT; |
- 如果此时开启一个事务三,修改其他行的值,会发现也是被阻塞了的
1 | UPDATE user_info SET amount = 1000 WHERE amount = 2000; |
- 执行结果
1 | UPDATE user_info SET amount = 1000 WHERE amount = 2000 |
结论
- 总结一下
SELECT ... FOR UPDATE
加锁的情况:- 主键字段:加行锁。
- 唯一索引字段:加行锁。
- 普通索引字段:加行锁。
- 普通字段:加表锁。
- 如果事务一加了
行锁
,一直没有释放锁,事务二操作相同行的数据时,会一直等待直到超时。 - 如果事务一加了
表锁
,一直没有释放锁,事务二不管操作的是哪一行数据,都会一直等待直到超时。
评论