靠 WHERE 条件 + 业务状态判断实现幂等,即通过原子性 WHERE 校验(如 status != 'shipped')确保重复执行不改变最终状态,避免 SELECT+UPDATE 或加锁,DDL 需前置存在性检查。

重复执行 UPDATE 脚本导致数据错乱怎么办
直接结论:靠 WHERE 条件 + 业务状态判断实现幂等,而不是依赖“先查再更新”或“加锁”。多数线上事故源于把幂等当成“只运行一次”,实际它要求“运行 N 次结果一致”。
关键不是阻止重复执行,而是让重复执行不改变最终状态。比如更新用户积分,不能写 UPDATE user SET score = score + 10(每次跑都加 10),而应锁定目标值或基于确定性条件更新。
用 WHERE 子句做幂等校验最可靠
WHERE 是数据库层面天然的原子性屏障,只要条件表达式能唯一标识“该不该改”,就能避免误更新。
- 错误写法:
UPDATE order SET status = 'shipped' WHERE id = 123 —— 重复执行无害但不保险,没校验当前状态是否已是 shipped
- 推荐写法:
UPDATE order SET status = 'shipped', updated_at = NOW() WHERE id = 123 AND status != 'shipped' —— 即使脚本跑十遍,只有第一次生效
- 更严谨场景(如需记录首次变更时间):
UPDATE order SET status = 'shipped', shipped_at = COALESCE(shipped_at, NOW()) WHERE id = 123 AND status = 'confirmed'
注意:COALESCE 和 NOW() 在不同数据库中行为略有差异(如 MySQL 的 NOW() 是语句开始时间,PostgreSQL 中可能随事务变化),若对时间精度敏感,建议在应用层生成时间戳传入。
避免用 SELECT + UPDATE 实现幂等
这种两阶段操作在并发下大概率失效,除非加 SELECT ... FOR UPDATE,但会显著拖慢性能、引发锁等待。
- 典型陷阱:
SELECT status FROM order WHERE id = 123 → 应用判断不是 shipped → 再 UPDATE ...。两个请求同时读到 'confirmed',都会执行更新
-
SELECT ... FOR UPDATE 虽可解决,但把行锁暴露给脚本调度逻辑,容易因超时、连接中断导致锁残留
- 运维脚本通常无事务上下文,
FOR UPDATE 可能被忽略或报错(如某些 ORM 自动 commit 后释放锁)
真正需要强一致性时,应由应用服务统一控制状态机流转,而不是靠 DB 脚本兜底。
ALTER TABLE 类变更也要考虑幂等
DDL 脚本重复执行常直接报错(如 ADD COLUMN 已存在),但错误类型因数据库而异,不能靠捕获异常来“跳过”。
- MySQL:没有原生
ADD COLUMN IF NOT EXISTS,得用存储过程或先查 information_schema.COLUMNS
- PostgreSQL:支持
ALTER TABLE ... ADD COLUMN IF NOT EXISTS,但仅限 9.6+,且不适用于索引、约束等
- 安全做法:所有 DDL 脚本开头加校验,例如
DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'idx_user_email' AND n.nspname = 'public') THEN CREATE INDEX idx_user_email ON users(email); END IF; END $$;
这类逻辑一旦写错,轻则脚本失败,重则建出重复索引(MySQL 允许同名索引共存但不生效,PG 则直接报错)。
幂等最难的不是语法,是定义清楚“什么才算一次有效变更”——这取决于业务状态机的设计粒度,而不是 SQL 写得够不够花哨。
UPDATE order SET status = 'shipped' WHERE id = 123 —— 重复执行无害但不保险,没校验当前状态是否已是 shippedUPDATE order SET status = 'shipped', updated_at = NOW() WHERE id = 123 AND status != 'shipped' —— 即使脚本跑十遍,只有第一次生效UPDATE order SET status = 'shipped', shipped_at = COALESCE(shipped_at, NOW()) WHERE id = 123 AND status = 'confirmed'
SELECT ... FOR UPDATE,但会显著拖慢性能、引发锁等待。
- 典型陷阱:
SELECT status FROM order WHERE id = 123→ 应用判断不是 shipped → 再UPDATE ...。两个请求同时读到'confirmed',都会执行更新 -
SELECT ... FOR UPDATE虽可解决,但把行锁暴露给脚本调度逻辑,容易因超时、连接中断导致锁残留 - 运维脚本通常无事务上下文,
FOR UPDATE可能被忽略或报错(如某些 ORM 自动 commit 后释放锁)
ALTER TABLE 类变更也要考虑幂等
DDL 脚本重复执行常直接报错(如 ADD COLUMN 已存在),但错误类型因数据库而异,不能靠捕获异常来“跳过”。
- MySQL:没有原生
ADD COLUMN IF NOT EXISTS,得用存储过程或先查 information_schema.COLUMNS
- PostgreSQL:支持
ALTER TABLE ... ADD COLUMN IF NOT EXISTS,但仅限 9.6+,且不适用于索引、约束等
- 安全做法:所有 DDL 脚本开头加校验,例如
DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'idx_user_email' AND n.nspname = 'public') THEN CREATE INDEX idx_user_email ON users(email); END IF; END $$;
这类逻辑一旦写错,轻则脚本失败,重则建出重复索引(MySQL 允许同名索引共存但不生效,PG 则直接报错)。
幂等最难的不是语法,是定义清楚“什么才算一次有效变更”——这取决于业务状态机的设计粒度,而不是 SQL 写得够不够花哨。
ADD COLUMN IF NOT EXISTS,得用存储过程或先查 information_schema.COLUMNS
ALTER TABLE ... ADD COLUMN IF NOT EXISTS,但仅限 9.6+,且不适用于索引、约束等DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'idx_user_email' AND n.nspname = 'public') THEN CREATE INDEX idx_user_email ON users(email); END IF; END $$;










