外观
看到 select * from t where id = 1 for update,能想到什么?
⭐ 题目日期:
快手 - 2024/12/29
📝 题解:
当看到 SELECT * FROM t WHERE id = 1 FOR UPDATE
时,可以从 锁机制、事务隔离级别、索引影响 和 应用场景 等多个角度分析其行为。以下是详细解读:
1. 基本作用
- 功能:对满足条件的行加 排他锁(X锁),阻塞其他事务对这些行的写操作(如
UPDATE
、DELETE
、SELECT ... FOR UPDATE
),确保当前事务提交前数据不被修改。 - 适用场景:解决并发事务中的 脏写 和 不可重复读 问题,常见于库存扣减、账户余额修改等需要强一致性的场景。
2. 锁的范围与类型
锁的具体行为取决于 索引类型 和 事务隔离级别:
(1) 主键/唯一索引查询
锁定行:仅锁定
id = 1
的这行记录。示例:
-- id 是主键或唯一索引 SELECT * FROM t WHERE id = 1 FOR UPDATE;
- 锁类型:行级排他锁(Record Lock)。
- 其他事务:
- 允许读取(如普通
SELECT
,取决于隔离级别)。 - 阻塞对同一行的
UPDATE
、DELETE
、SELECT ... FOR UPDATE
。
- 允许读取(如普通
(2) 非唯一索引或无索引查询
锁定范围:可能加 间隙锁(Gap Lock) 或 临键锁(Next-Key Lock),防止其他事务插入到锁定范围内。
示例:
-- 假设 id 是普通索引或无索引 SELECT * FROM t WHERE id = 1 FOR UPDATE;
- 锁类型:
- 非唯一索引:锁定
id = 1
的行和相邻的间隙(如(1, 5)
)。 - 无索引:可能升级为 表级锁(全表扫描加锁)。
- 非唯一索引:锁定
- 其他事务:
- 阻塞插入
id = 1
的新记录(如INSERT INTO t (id) VALUES (1)
)。
- 阻塞插入
- 锁类型:
3. 事务隔离级别的影响
不同隔离级别下,FOR UPDATE
的行为有所差异:
隔离级别 | 行为说明 |
---|---|
READ UNCOMMITTED | 无锁,直接读取最新数据(含未提交的修改),不推荐使用。 |
READ COMMITTED | 仅锁定当前扫描到的行(无间隙锁),可能导致幻读。 |
REPEATABLE READ | 默认隔离级别。锁定行及间隙(防止幻读),但可能增加锁冲突。 |
SERIALIZABLE | 最高隔离级别,所有读操作隐式加锁(类似 FOR UPDATE ),牺牲并发性能。 |
示例: 在 REPEATABLE READ 下,若 id
是非唯一索引,FOR UPDATE
会加 临键锁,阻塞其他事务插入 id = 1
的新记录。
4. 潜在问题与优化
(1) 死锁风险
- 场景:事务A锁定了
id=1
,事务B锁定了id=2
,随后两者尝试锁定对方的资源。 - 解决方案:
- 保持事务简短,尽快提交。
- 按固定顺序访问资源(如按
id
升序加锁)。 - 启用死锁检测(
innodb_deadlock_detect = ON
)或设置超时(innodb_lock_wait_timeout
)。
(2) 性能问题
- 全表扫描加锁:若
WHERE
条件无索引,会导致表级锁,阻塞所有写操作。 - 优化:为查询字段添加索引,缩小锁范围。
(3) 锁冲突监控
查看锁信息:
SHOW ENGINE INNODB STATUS; -- 查看当前锁和事务状态 SELECT * FROM information_schema.INNODB_LOCKS; -- 查看活跃锁
5. 实际应用示例
(1) 库存扣减
BEGIN;
-- 锁定 id=1001 的商品库存行
SELECT stock FROM products WHERE id = 1001 FOR UPDATE;
-- 检查库存并扣减
UPDATE products SET stock = stock - 1 WHERE id = 1001;
COMMIT;
- 作用:防止超卖。
(2) 转账事务
BEGIN;
-- 锁定转出账户
SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE;
-- 锁定转入账户
SELECT balance FROM accounts WHERE user_id = 2 FOR UPDATE;
-- 执行转账操作
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
- 作用:确保原子性,防止中间状态被其他事务读取。
6. 替代方案
(1) 乐观锁(Optimistic Locking)
使用版本号或时间戳:
-- 查询时获取版本号 SELECT version FROM t WHERE id = 1; -- 更新时检查版本号 UPDATE t SET data = 'new', version = version + 1 WHERE id = 1 AND version = old_version;
适用场景:低冲突环境,避免悲观锁的性能开销。
(2) 分布式锁
- 在分布式系统中,使用 Redis 或 ZooKeeper 实现跨服务锁。
总结
- 核心作用:通过排他锁保证事务期间数据的一致性。
- 关键点:
- 索引决定锁范围(行锁、间隙锁或表锁)。
- 隔离级别影响锁行为(是否加间隙锁)。
- 注意死锁风险和性能优化(索引、事务粒度)。
- 适用场景:高并发写操作需强一致性的业务(如金融交易、库存管理)。