最稳做法是用 declare exit handler for sqlstate '23000' 配合显式 start transaction 和 rollback,因'23000'覆盖所有主键/唯一键/外键冲突,且必须用 exit(非 continue)确保出错即退、不续执行。

用 EXIT HANDLER 捕获 SQLSTATE '23000' 是最稳的做法
主键冲突在 MySQL 中对应标准错误码 SQLSTATE '23000'(不是错误号 1062),这是唯一能覆盖所有主键/唯一键冲突场景的判断依据。用 1062 虽然常见,但会漏掉外键约束失败、唯一索引冲突等同属 '23000' 类型的异常。
必须用 EXIT HANDLER,不能用 CONTINUE:主键冲突后继续执行后续语句大概率导致数据错乱或二次报错。一旦触发,应立即退出当前作用域并回滚事务。
-
DECLARE EXIT HANDLER FOR SQLSTATE '23000'后必须包含ROLLBACK和错误信息赋值 - 避免在 handler 里再做 DML 操作(如写日志表),否则可能引发嵌套异常
- handler 声明要放在
BEGIN后、业务逻辑前,顺序错会导致不生效
START TRANSACTION 必须显式写,且不能被隐式提交破坏
MySQL 不会自动回滚失败事务,ROLLBACK 是否生效,取决于事务是否真正在运行中。很多存储过程看似加了 ROLLBACK,但实际没起作用——因为事务早已被隐式提交了。
- DDL 语句(如
CREATE TEMPORARY TABLE除外)、LOCK TABLES、SET AUTOCOMMIT = 1都会强制提交当前事务 - 务必把
START TRANSACTION放在 handler 声明之后、任何 DML 之前 - 如果业务需要建临时表,只用
CREATE TEMPORARY TABLE(它不触发提交) - 事务内不要调用含 DDL 或锁操作的其他存储过程
INSERT ON DUPLICATE KEY UPDATE 不等于“不用异常处理”
很多人以为用了 INSERT ... ON DUPLICATE KEY UPDATE 就高枕无忧,其实它只解决“单条插入时主键已存在”的场景。一旦遇到以下情况,照样抛 SQLSTATE '23000':
- UPDATE 子句里修改了其他唯一键字段,导致新值与其他行冲突
- 触发器中又执行了 INSERT/UPDATE 并撞上主键(比如 BEFORE INSERT 里给
NEW.id赋了一个已存在的值) - 并发下两个事务同时走
ON DUPLICATE KEY UPDATE,其中一条先 UPDATE 完,另一条 UPDATE 时发现被改过的字段又违反了其他唯一约束
所以即使主体 SQL 用了 ON DUPLICATE KEY UPDATE,整个存储过程仍需包裹 EXIT HANDLER FOR SQLSTATE '23000'。
别信“查一遍再插”的应用层逻辑
在存储过程中写 SELECT COUNT(*) 判断主键是否存在,再决定 INSERT 还是 UPDATE,是典型的时间窗口陷阱。并发请求下,两次 SELECT 之间完全可能被另一个事务插入相同主键。
这种逻辑不仅性能差(多一次查询),还根本不可靠。MySQL 的原子性保障只在单条语句或事务内有效,跨语句的“判断 + 执行”必然有竞争。
- 删掉所有
IF (SELECT ...) THEN ... ELSE ... END IF这类结构 - 用
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO替代,它们才是原子的 - 若必须用
REPLACE INTO,注意它底层可能是DELETE + INSERT,会改变LAST_INSERT_ID(),且可能触发 DELETE 触发器
真正容易被忽略的是:handler 只捕获当前作用域内的错误。如果存储过程里调用了另一个含 DML 的存储过程,而那个过程没设 handler,错误会直接向上冒泡——但此时外层的 ROLLBACK 可能已经执行过了,或者根本没来得及执行。











