不能直接在事务中先select再update,因为默认隔离级别下读到的数据可能被其他事务修改,导致更新覆盖或影响行为为0;应使用select ... for update加行锁,或改用带条件的原子update等方案。

为什么不能直接在事务里先 SELECT 再 UPDATE 就完事
因为多数 ORM 或数据库驱动默认不开启可重复读(REPEATABLE READ)或串行化(SERIALIZABLE)隔离级别,SELECT 查到的数据可能在 UPDATE 前被其他事务改掉,导致“查到的旧值被覆盖”——这不是 bug,是并发下的正常行为。
常见错误现象:UPDATE 影响行数为 0,但业务逻辑以为更新成功;或者两个请求同时读到同一行,都按自己逻辑更新,后写入者覆盖前写入者的修改。
- 使用场景:库存扣减、积分变更、状态机流转(如“待支付 → 已支付”)
- 关键点:不是“有没有事务”,而是“有没有防止读写冲突的机制”
- 参数差异:
SELECT ... FOR UPDATE在 MySQL InnoDB 中有效,但在 SQLite 或某些 PostgreSQL 配置下需配合SELECT ... FOR UPDATE NOWAIT或显式锁表
SELECT ... FOR UPDATE 怎么用才不踩坑
它本质是加行级写锁,让其他事务无法对这些行做 SELECT ... FOR UPDATE 或 UPDATE,直到当前事务结束。但容易忽略锁的范围和生命周期。
- 必须在同一个事务内:先
BEGIN,再SELECT ... FOR UPDATE,再UPDATE,最后COMMIT或ROLLBACK - WHERE 条件必须命中索引,否则会升级为表锁(MySQL 下尤其危险)
- 避免
SELECT ... FOR UPDATE查太多行,锁住过多数据会拖慢其他请求,甚至引发死锁 - PostgreSQL 中若没指定
FOR UPDATE OF table_name,多表 JOIN 时可能锁错表
示例(MySQL):
START TRANSACTION; SELECT stock FROM products WHERE id = 123 FOR UPDATE; -- 这里做业务判断,比如 stock > 5 UPDATE products SET stock = stock - 5 WHERE id = 123; COMMIT;
ORM 框架里怎么安全写批量更新
很多 ORM 默认不支持原生 SELECT ... FOR UPDATE,或者封装后行为不透明,容易误用。
- Django:
MyModel.objects.select_for_update().filter(...)必须配合transaction.atomic(),且不能用于bulk_create等非 SQL 更新操作 - SQLAlchemy:
session.query(Model).with_for_update().filter(...).all(),注意with_for_update(nowait=True)可避免阻塞,但要捕获sqlalchemy.exc.OperationalError - MyBatis:
<select ... forupdate="true"></select>仅在某些方言下生效,建议直接写SELECT ... FOR UPDATE的 SQL 片段 - Node.js(pg):
client.query('SELECT ... FOR UPDATE', values)后必须复用同一client执行UPDATE,跨连接无效
比 SELECT + UPDATE 更稳的替代方案
如果业务允许,绕过“读-改-写”流程,直接用原子 SQL 更可靠,也更轻量。
- 用带条件的
UPDATE:例如UPDATE products SET stock = stock - 5 WHERE id = 123 AND stock >= 5,然后检查rowsAffected是否为 1 - 用数据库函数:PostgreSQL 的
UPDATE ... RETURNING可一次返回更新前/后的值,避免二次查询 - 乐观锁:加
version字段,UPDATE ... SET ..., version = version + 1 WHERE id = ? AND version = ?,失败时重试 - 注意:Redis + Lua 脚本适合高并发简单计数,但无法替代需要 ACID 和复杂关联的场景
真正麻烦的从来不是语法,而是你没法靠 SELECT 看清其他事务正在干啥——锁是手段,理解谁在什么时候改哪行,才是关键。










