
高并发点查询(例如按主键或唯一键查询单行)本身并非锁瓶颈,真正拖累吞吐量的核心在于“查询+锁+等待”的组合逻辑——尤其是 SELECT FOR UPDATE 以及非原子性的“查询再修改”链路。
为什么 SELECT FOR UPDATE 在点查询场景下反而最为危险
它并非单纯的“查询”,而是“查询 + 加行锁 + 等待锁释放”。即便你的 WHERE 条件明确写为 id = 123,只要事务未及时提交,或中间插入了日志、HTTP 调用,锁就会持续悬停数秒。其他所有试图更新同一行的请求将全部卡在该语句上,形成一条锁等待链。
- 必须使用
EXPLAIN确认该语句确实走了主键索引(type: const,key显示主键名),否则可能退化为间隙锁甚至表锁 - 不要把
SELECT FOR UPDATE放在事务开头,应尽量靠近UPDATE或COMMIT之前执行,以缩短持锁时间 - 如果只是校验后插入(例如防重复下单),直接使用
INSERT INTO ... ON DUPLICATE KEY UPDATE,它仅执行一次索引查找并加一次行锁,无竞态、无等待
哪些“点查询”写法会意外扩大锁范围
表面上是在进行点查询,实际上却锁住了大片数据。根本原因在于 MySQL 的行锁对象是“索引记录”而非数据行——锁的粒度完全取决于是否命中索引、是否触发二级索引维护、是否引发间隙锁。
UPDATE t SET status = 'done' WHERE order_no = 'ORD123':如果order_no未建唯一索引,MySQL 可能先全表扫描再过滤,锁住所有扫描过的记录UPDATE t SET cnt = cnt + 1 WHERE user_id = 100 AND status = 'active':如果status无索引,WHERE 条件中的status = 'active'会导致扫描大量user_id = 100的行并加锁- 在 RC 隔离级别下也需警惕范围条件(例如
WHERE created_at > '2026-06-01'),即使只查一行,也可能因索引结构触发间隙锁
替代方案比调参更有效:绕过显式锁
许多所谓“点查询加锁”的需求,本质上是业务逻辑需要原子性保障,而非必须依赖数据库锁。优先使用数据库原生的原子能力来替代手写锁逻辑。
- 计数类操作(如库存扣减):使用
UPDATE t SET stock = stock - 1 WHERE id = 123 AND stock >= 1,检查影响行数是否为 1,失败即表示库存不足——完全不需要SELECT FOR UPDATE - 状态机推进(例如订单从 pending → paid):使用
UPDATE orders SET status = 'paid' WHERE id = 123 AND status = 'pending',同样依靠影响行数判断是否成功 - 防重插入:确保
UNIQUE KEY (biz_id)存在,然后统一走INSERT INTO t (biz_id, ...) VALUES (...) ON DUPLICATE KEY UPDATE updated_at = NOW()
真正拖累吞吐量的从来不是“查询慢”,而是“锁得久、锁得宽、锁得不必要”。优化重点不在于如何让锁更快,而在于如何让锁更少、更短、更精准——多数情况下,答案就是不加锁。
