MSSQL重复键行异常解决:规则引擎分类文章的冲突处理
你遇到的其实是MERGE在并发场景下的常见坑——虽然MERGE看起来是原子性的操作,但它的ON条件检查和INSERT执行之间存在一个时间窗口,其他事务完全可能在这个空隙里插入相同的(articleId, categoryId)组合,导致你的MERGE执行插入时触发唯一索引冲突。下面是具体的解决办法和预防方案:
一、给MERGE加锁提示,堵住并发间隙
在目标表上添加UPDLOCK, HOLDLOCK锁提示,强制事务在执行期间持有排他锁,彻底阻止其他事务插入冲突记录:
MERGE article_category WITH (UPDLOCK, HOLDLOCK) AS [target] USING ( SELECT articleId, @categoryId, @creator, @now, @ruleId, 2 FROM @articleIdList ) AS [source] (articleId, categoryId, creator, createDate, ruleId, assignmentTypeId) ON ( target.articleId = source.articleId AND target.categoryId= source.categoryId ) WHEN NOT MATCHED THEN INSERT (articleId, categoryId, creator, createDate, ruleId, assignmentTypeId) VALUES (source.articleId, source.categoryId, source.creator, source.createDate, source.ruleId, source.assignmentTypeId);
UPDLOCK:获取更新锁,允许其他事务读取但不允许修改或获取同类型锁HOLDLOCK:把锁的持有时间延长到整个事务结束,确保MERGE的检查和插入过程中,没有其他事务能插进来抢位置
二、用TRY/CATCH捕获冲突,静默处理(适合允许忽略重复的场景)
如果业务上只需要最终记录存在即可,不在乎重复插入的请求被忽略,可以用TRY/CATCH块包裹MERGE,捕获唯一键冲突的异常(错误号2601或2627)并跳过:
BEGIN TRY MERGE article_category AS [target] USING ( SELECT articleId, @categoryId, @creator, @now, @ruleId, 2 FROM @articleIdList ) AS [source] (articleId, categoryId, creator, createDate, ruleId, assignmentTypeId) ON ( target.articleId = source.articleId AND target.categoryId= source.categoryId ) WHEN NOT MATCHED THEN INSERT (articleId, categoryId, creator, createDate, ruleId, assignmentTypeId) VALUES (source.articleId, source.categoryId, source.creator, source.createDate, source.ruleId, source.assignmentTypeId); END TRY BEGIN CATCH -- 只处理唯一键冲突的异常,其他异常正常抛出 IF ERROR_NUMBER() IN (2601, 2627) BEGIN -- 这里可以加日志记录,或者直接print提示 PRINT '已忽略重复的文章分类分配请求'; END ELSE BEGIN THROW; END END CATCH
三、其他预防方案
1. 应用层先做去重+分布式锁
在调用存储过程之前,先在应用代码里对要分配的(articleId, categoryId)组合做去重,避免重复调用。同时可以用分布式锁(比如Redis锁),确保同一(articleId, categoryId)组合同一时间只有一个请求在处理,从根源上减少并发冲突。
2. 回到IF NOT EXISTS写法,但必须加锁
如果不想用MERGE,也可以回到传统的“检查再插入”逻辑,但一定要加锁提示,不然一样会有并发问题:
BEGIN TRANSACTION; INSERT INTO article_category (articleId, categoryId, creator, createDate, ruleId, assignmentTypeId) SELECT articleId, @categoryId, @creator, @now, @ruleId, 2 FROM @articleIdList source WHERE NOT EXISTS ( SELECT 1 FROM article_category WITH (UPDLOCK, HOLDLOCK) WHERE articleId = source.articleId AND categoryId = @categoryId ); COMMIT TRANSACTION;
这里的锁提示和MERGE方案里的作用一样,都是为了锁住检查的范围,不让其他事务插进来。
3. 修改唯一索引的IGNORE_DUP_KEY属性
可以修改你的唯一索引IX_article_category_no_duplicates,开启IGNORE_DUP_KEY = ON,这样当插入重复记录时,SQL Server会自动忽略重复行,而不是抛出异常:
-- 先删除原索引(如果存在) DROP INDEX IF EXISTS IX_article_category_no_duplicates ON dbo.article_category; -- 创建带IGNORE_DUP_KEY的新索引 CREATE UNIQUE INDEX IX_article_category_no_duplicates ON dbo.article_category (articleId, categoryId) WITH (IGNORE_DUP_KEY = ON);
注意:这个设置只对普通INSERT生效,MERGE的INSERT分支遇到重复时还是会报错,所以如果用这个方法,可能需要把MERGE改成普通INSERT加WHERE NOT EXISTS的写法。
内容的提问来源于stack exchange,提问作者xeraphim

