唯一索引是防重最可靠的第一道防线,通过数据库原子性拒绝重复写入,并需配合版本适配的异常捕获机制(如SQL Server用TRY...CATCH捕获2627/2601,MySQL用HANDLER处理1062或'23000')。

唯一索引是防重最可靠的第一道防线
重复提交的本质是业务上不允许相同逻辑记录多次入库,比如同一订单号重复插入、同一用户对同一商品的重复评价。靠应用层判断“有没有再查一遍”不可靠——并发时查不到,插进去就重复了。唯一索引让数据库自己拒绝非法写入,不依赖任何代码逻辑。
实操建议:
- 在真正具备业务唯一性的字段组合上建
UNIQUE INDEX,例如(user_id, product_id, order_date),而不是只用id(那是主键,不是业务唯一性) - 避免用
CHAR类型字段做唯一键,末尾空格可能被忽略导致冲突失效;优先选VARCHAR或带COLLATE utf8mb4_bin的严格比对 - MySQL 中若用
INSERT IGNORE或ON DUPLICATE KEY UPDATE,会静默吞掉唯一冲突,掩盖问题;存储过程中应显式捕获异常而非绕过
SQL Server 存储过程里怎么捕获唯一索引冲突异常
SQL Server 把唯一约束冲突归为错误号 2627(主键)或 2601(唯一索引),必须用 TRY...CATCH 捕获,不能靠 @@ERROR 判断——因为 INSERT 执行后它才更新,且并发下易被覆盖。
示例片段:
BEGIN TRY
INSERT INTO orders (order_no, user_id, amount)
VALUES (@order_no, @user_id, @amount);
END TRY
BEGIN CATCH
IF ERROR_NUMBER() IN (2627, 2601)
THROW 50001, '订单号已存在,禁止重复提交', 1;
ELSE
THROW; -- 其他错误原样抛出
END CATCH
注意:THROW 是 SQL Server 2012+ 推荐方式,RAISERROR 在某些版本中无法正确传递错误状态。
MySQL 存储过程中如何识别和响应 Duplicate Entry 错误
MySQL 不支持标准 TRY/CATCH,得靠 DECLARE ... HANDLER 捕获 SQLSTATE 或 MySQL 错误码。唯一冲突对应 SQLSTATE '23000' 或错误码 1062,但要注意:不同存储引擎报错细节略有差异(InnoDB 稳定,MyISAM 可能不触发)。
关键点:
- Handler 必须在
BEGIN...END块开头声明,且仅对后续语句生效 - 不要只捕获
1062就返回成功——要区分是业务重复(可接受)还是其他约束失败(需告警) - 建议在 handler 中设标志变量,再由主逻辑决定是
RETURN还是记录日志后继续
简例:
DECLARE CONTINUE HANDLER FOR SQLSTATE '23000'
SET @is_duplicate = TRUE;
<p>SET @is_duplicate = FALSE;
INSERT INTO payments (tx_id, amount) VALUES (p_tx_id, p_amount);
IF @is_duplicate THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '交易ID已存在';
END IF;</p>
为什么不能只靠应用层加锁或时间戳校验
加锁(如 Redis 分布式锁、SELECT ... FOR UPDATE)成本高、易死锁、超时难控;时间戳或随机 token 校验依赖客户端配合,且无法防止重放攻击或脚本暴力提交。而唯一索引 + 异常捕获是数据库层原子保障,不增加网络往返,也不依赖调用方是否“守规矩”。
容易踩的坑:
- 把防重逻辑写在存储过程末尾(比如先 INSERT 再 SELECT COUNT),这已经晚了——重复数据早已落库
- 在事务中嵌套多个 INSERT 却只对最后一个做异常捕获,前面的失败会被忽略
- 没考虑字符集/排序规则导致“张三”和“张三 ”(含全角空格)被判定为不同值,实际业务中却算同一人
唯一索引不是万能胶,但它是最薄、最硬、最不该省的一层。异常捕获的写法必须匹配你用的数据库版本和引擎特性,否则看起来“处理了”,其实什么也没拦住。










