用 SQL Server 应用锁解决并发重复提交


文章封面

用 SQL Server 应用锁解决并发重复提交

移动端报修提交时,经常会遇到一种问题:前端已经做了按钮 loading,后端也查了“同一设备是否已有未完成工单”,但现场仍然出现了同一设备生成两张未完成工单。

根本原因不是按钮没有禁用,而是后端的“查重”和“新增”不是一个不可打断的整体。

问题在哪里

普通写法大概是这样:

bool isHave = entities.engsys_taskDoc.Any(w => w.assetsCode == data.assetsCode && w.flowStatus == 1);
if (isHave)
{
    return GZAPIBadRequest($"固定资产编号:【{data.assetsCode}】有未完成的工单,不允许重复提交工单");
}

entities.engsys_taskDoc.Add(data);
entities.SaveChanges();

单个请求看起来没问题,但两个请求并发时会出现这个过程:

请求 A:查询,没有未完成工单
请求 B:查询,也没有未完成工单
请求 A:新增工单
请求 B:新增工单

于是同一设备就可能被重复报修。

为什么不用内存缓存

接口开始时写一个内存缓存,结束时删除缓存,确实能挡住一部分重复点击。但它不适合作为最终限制。

方案问题
前端 loading只能挡用户连点,挡不住重复请求或接口重放
后端内存缓存和数据库提交不是一个事务整体
缓存过期时间时间短了挡不住慢请求,时间长了误拦正常提交
SQL Server 应用锁和数据库事务绑定,能保护“查重 + 新增”

这个规则本质上是数据规则:

同一设备不能同时存在两张未完成工单

所以限制应该放在数据库提交附近。

使用 sp_getapplock 加事务级排他锁

SQL Server 提供了 sp_getapplock,可以给一个自定义资源名加锁。这里可以把资产编号作为锁名的一部分:

DECLARE @result int;
EXEC @result = sp_getapplock
    @Resource = @LockResource,
    @LockMode = 'Exclusive',
    @LockOwner = 'Transaction',
    @LockTimeout = 10000;
SELECT @result;

关键参数说明:

参数含义
@Resource锁的名字,例如 engsys_taskDoc.assetsCode:AG00264
@LockMode = 'Exclusive'排他锁,同一时间只允许一个事务拿到
@LockOwner = 'Transaction'锁跟随事务,提交或回滚后自动释放
@LockTimeout = 10000最多等待 10 秒,不是固定等待 10 秒

后端实现方式

核心思路是:开启事务后,先按资产编号加锁,再查重,再新增或修改。

using (var trans = entities.Database.BeginTransaction())
{
    try
    {
        var validateResult = ValidateFixedAssetsDocNotExists(data);
        if (validateResult != null)
        {
            trans.Rollback();
            return validateResult;
        }

        entities.engsys_taskDoc.Add(data);
        entities.SaveChanges();

        trans.Commit();
    }
    catch (Exception ex)
    {
        trans.Rollback();
        return GZAPIThrowException(ex);
    }
}

校验函数示例:

private GZAPIResult ValidateFixedAssetsDocNotExists(engsys_taskDoc data, string excludeRowID = null)
{
    if (data.assetsType != 1)
    {
        return null;
    }

    if (String.IsNullOrEmpty(data.assetsCode))
    {
        return GZAPIBadRequest("固定资产编号不能为空");
    }

    data.assetsCode = data.assetsCode.Trim();

    var lockResult = entities.Database.SqlQuery<int>(
        @"DECLARE @result int;
EXEC @result = sp_getapplock @Resource = @LockResource, @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = 10000;
SELECT @result;",
        new SqlParameter("@LockResource", "engsys_taskDoc.assetsCode:" + data.assetsCode)
    ).Single();

    if (lockResult < 0)
    {
        return GZAPIBadRequest($"固定资产编号:【{data.assetsCode}】正在提交工单,请稍后再试");
    }

    var query = entities.engsys_taskDoc.Where(w => w.assetsCode == data.assetsCode && w.flowStatus == 1);
    if (!String.IsNullOrEmpty(excludeRowID))
    {
        query = query.Where(w => w.rowID != excludeRowID);
    }

    if (query.Any())
    {
        return GZAPIBadRequest($"固定资产编号:【{data.assetsCode}】有未完成的工单,不允许重复提交工单");
    }

    return null;
}

锁什么时候释放

因为使用了:

@LockOwner = 'Transaction'

所以不需要手写释放代码。

情况释放时间
trans.Commit()事务提交时自动释放
trans.Rollback()事务回滚时自动释放
SQL 连接断开SQL Server 自动清理事务和锁
接口异常进入 catch回滚事务后释放

@LockTimeout = 10000 的意思是最多等 10 秒,不是每次都等 10 秒。前一个请求 0.2 秒处理完,后一个请求就只等 0.2 秒。

最终效果

同一个设备并发提交时,执行顺序会变成:

请求 A:拿到 AG00264 的锁
请求 A:查重,没有未完成工单
请求 A:新增工单
请求 A:提交事务,释放锁
请求 B:拿到 AG00264 的锁
请求 B:查重,发现已有未完成工单
请求 B:拒绝提交

这样就能保证“查重 + 新增”不会被另一个请求插队。

小结

前端防重复点击可以保留,但它只是用户体验层面的保护。真正防止同一设备重复报修,应该放在后端数据库事务里。

对于这种“先查重、再插入”的并发问题,SQL Server 的事务级应用锁是一个很合适的方案:不需要新增表,也不依赖内存缓存,并且锁会随着事务自动释放。

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
MCP自动发表文章
上一篇:Windows 查看端口占用进程的方法
下一篇:Markdown 渲染引擎怎么选:marked、markdown-it 和 Markdig 对比
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

SQL Server 应用解决并发重复提交
EF并发处理,防止并发修改数据
SQL Server中常全局变量介绍
接口并发控制,限制重要接口的并发请求
Git 提交文件名大小写变更无效? git mv 正确提交文件名大小写修改的方法
SQL Server 中master..spt_values的应用
Python使用SQL Server数据库
Microsoft Store 桌面应用发布流程(二)之提交应用
解决new Thread().Start导致高并发CPU 100%的问题
sql server系统表详细说明 之 sys.sql_dependencies
SQL Server 2022安装指南
WinUI 3 开发了一个摸鱼应用
SQL Server 查询参数管理类
Linq to SQL语句基础讲解
GZDBHelper连接SQL Server 数据库
安装 SQL Server 2012 的硬件和软件要求(官方全面)
支付系统中要避免支付状态并发BUG
sql server系统表详细说明 之 sys.stats
sql server系统表详细说明 之 sys.masked_columns
sql server系统表详细说明 之 sys.table_types

联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码