高并发下UPDATE不加锁会丢数据,安全写法需显式控制并发:用SELECT...FOR UPDATE(须命中索引)、原子UPDATE+行数判断、统一锁序防死锁,或引入Redis/分片/消息队列等应用层方案。

UPDATE 语句在高并发下不加锁、不校验,大概率会丢数据——这不是 MySQL 或 SQL Server 的 bug,而是默认行为。真正安全的写法必须显式控制并发访问路径。
用 SELECT ... FOR UPDATE 加行级排他锁
这是最常用也最容易出错的方式。它只在事务内有效,且**必须命中索引**,否则可能升级为表锁。
-
SELECT * FROM accounts WHERE user_id = 123 FOR UPDATE安全的前提是user_id有唯一索引或主键;没索引时,MySQL 5.7 及以前版本会锁整张表 - 别在
FOR UPDATE后加LIMIT 1期望“只锁一行”——InnoDB 可能锁住间隙(next-key lock),尤其当user_id不连续时 - 事务中不能混入 DDL(如
ALTER TABLE)或非事务引擎操作(如写 MyISAM 日志表),否则会隐式提交,FOR UPDATE锁立刻释放 - SQL Server 中等价的是
SELECT ... WITH (UPDLOCK, HOLDLOCK),HOLDLOCK等效于SERIALIZABLE的范围锁,比单纯UPDLOCK更稳妥
改用原子性 UPDATE + 影响行数判断
绕过读-改-写三步流程,直接用单条 UPDATE 尝试修改,再根据返回的影响行数决定是否补 INSERT。
- MySQL 示例:
UPDATE stats SET count = count + 1 WHERE id = 123;,检查mysql_affected_rows()是否为 1;若为 0,说明记录不存在,再执行INSERT - SQL Server 更推荐
MERGE语句,但要注意:它在READ COMMITTED下仍可能因谓词重计算导致重复插入,必须配合HOLDLOCK提示,如ON (t.id = s.id) WITH (HOLDLOCK) - 所有
UPDATE必须带确定的WHERE条件,禁用模糊匹配(如WHERE status = 'pending'),否则锁范围不可控,易引发死锁
避免死锁:强制统一资源访问顺序
死锁不是并发高导致的,是多个事务对同一组记录加锁顺序不一致触发的。
- 批量更新多行时,
IN子句里的 ID 必须提前排序,例如把IN (105, 102, 108)改成IN (102, 105, 108);否则优化器执行顺序不确定,A 事务锁 105→102,B 事务锁 102→105,就卡住了 - 跨表操作(如同时更新
orders和inventory)必须拆到独立事务,或至少保证每张表内部按主键升序访问 - 不要在一个事务里既
SELECT FOR UPDATE用户,又UPDATE订单,再INSERT日志——不同实体锁粒度和顺序难以对齐,死锁概率陡增
热点数据场景下,数据库层已不够用
单行计数器类字段(如浏览量、库存总量)被高频更新时,哪怕加了行锁,也会成为瓶颈。
- 应用层用 Redis 做累加缓冲,再定时批量回写,比如每 5 秒聚合一次
INCRBY结果,生成单条UPDATE ... SET total = total + ? - 数据库分片:把
page_views拆成page_views_sharded(page_id, shard_id, count),每次更新随机选一个shard_id,查时SUM()汇总 - 消息队列削峰:把更新请求发到 Kafka,由消费者串行消费,天然规避并发冲突,适合允许秒级延迟的业务
真正难的不是写对一条 SQL,而是在整个服务链路里保持锁策略一致——从连接池配置(autocommit=0)、ORM 是否自动开启事务、到中间件是否透传隔离级别,任何一环松动,FOR UPDATE 就形同虚设。

















