必须带WHERE条件,否则会锁表或引发幻读/死锁;应使用主键或唯一索引(如account_id)限定单行,结合AND balance >= ?做原子校验,并在事务中执行。

UPDATE语句必须带WHERE条件锁定单行
直接对balance字段做UPDATE account SET balance = balance + ?而不加WHERE,会锁住整张表(尤其在MyISAM)或引发幻读/死锁(InnoDB)。真实业务中,账户ID是唯一确定的更新依据,漏掉WHERE user_id = ?等于批量误更新。
实操建议:
- 永远用主键或唯一索引列作为
WHERE条件,例如WHERE account_id = 123 - 执行前确认该
account_id存在,否则UPDATE影响行数为0,但事务仍成功——需检查ROW_COUNT()或ORM返回值 - 避免用
name等非唯一字段查余额,防止多账户同名导致并发覆盖
必须用事务包裹,且隔离级别至少为READ COMMITTED
单条UPDATE在InnoDB里自带行级锁,但若逻辑包含“先查余额再决定是否扣款”(即SELECT + UPDATE),不加事务就会出现脏读或超扣。MySQL默认隔离级别是REPEATABLE READ,虽能防止不可重复读,但对余额类场景反而增加间隙锁风险;READ COMMITTED更轻量、更符合直觉。
实操建议:
- 显式开启事务:
BEGIN→ 执行操作 →COMMIT或ROLLBACK - 应用层捕获SQL异常(如死锁错误
Deadlock found when trying to get lock),触发重试逻辑 - 不要依赖自动提交(autocommit=1),尤其在存储过程或批量操作中
推荐用UPDATE ... SET ... WHERE ... AND balance >= ?做乐观校验
单纯靠数据库锁不能解决业务逻辑冲突。比如用户A要扣100元,但当前余额只剩80元——锁住了行,却没阻止非法操作。把余额校验塞进WHERE子句,让SQL自己判断是否允许更新,失败时ROW_COUNT()返回0,比在应用层查+判+更安全。
示例语句:
UPDATE account SET balance = balance - 100 WHERE account_id = 123 AND balance >= 100;
这样写的好处:
- 原子性:校验和更新一步完成,无竞态窗口
- 无额外SELECT:减少一次网络往返和锁持有时间
- 失败明确:影响行为0即表示余额不足,无需再查当前值
高并发下慎用自增ID或UUID作账户主键
如果账户表主键是account_id,而它由AUTO_INCREMENT生成,大量并发更新会争抢同一索引页的锁(InnoDB的聚簇索引特性)。UUID虽分散写入,但随机性导致页分裂和缓存失效。两者都可能成为热点瓶颈。
更稳的做法:
- 用业务有意义的字段作主键,如
user_id(数字型、递增、有业务含义) - 若必须用UUID,考虑
UUID_TO_BIN()转为二进制存储,并用ORDER BY重排插入顺序 - 监控
Innodb_row_lock_waits指标,持续升高说明锁竞争已成问题
实际写的时候,最常被跳过的不是语法,而是“扣款前是否真有钱”这个判断——它必须落在数据库层面,而不是应用内存里算完再交过去。

















