Skip to content

看到 select * from t where id = 1 for update,能想到什么?

约 1101 字大约 4 分钟

MySQL快手

2025-03-19

⭐ 题目日期:

快手 - 2024/12/29

📝 题解:

当看到 SELECT * FROM t WHERE id = 1 FOR UPDATE 时,可以从 锁机制事务隔离级别索引影响应用场景 等多个角度分析其行为。以下是详细解读:


1. 基本作用

  • 功能:对满足条件的行加 排他锁(X锁),阻塞其他事务对这些行的写操作(如 UPDATEDELETESELECT ... FOR UPDATE),确保当前事务提交前数据不被修改。
  • 适用场景:解决并发事务中的 脏写不可重复读 问题,常见于库存扣减、账户余额修改等需要强一致性的场景。

2. 锁的范围与类型

锁的具体行为取决于 索引类型事务隔离级别

(1) 主键/唯一索引查询

  • 锁定行:仅锁定 id = 1 的这行记录。

  • 示例

    -- id 是主键或唯一索引
    SELECT * FROM t WHERE id = 1 FOR UPDATE;
    • 锁类型:行级排他锁(Record Lock)。
    • 其他事务
      • 允许读取(如普通 SELECT,取决于隔离级别)。
      • 阻塞对同一行的 UPDATEDELETESELECT ... 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 实现跨服务锁。

总结

  • 核心作用:通过排他锁保证事务期间数据的一致性。
  • 关键点
    1. 索引决定锁范围(行锁、间隙锁或表锁)。
    2. 隔离级别影响锁行为(是否加间隙锁)。
    3. 注意死锁风险和性能优化(索引、事务粒度)。
  • 适用场景:高并发写操作需强一致性的业务(如金融交易、库存管理)。