You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core应用多步骤业务操作的单用户独占访问实现方案咨询

多步骤业务对象的并发锁定方案(ASP.NET Core)

嘿,针对你这个ASP.NET Core里多步骤业务对象的并发锁定问题,我有几个比定时删数据库记录更严谨的方案,刚好能解决用户临时离开后返回继续操作的场景:

1. 带心跳机制的数据库锁表方案

这个方案是在你原有思路上的升级,核心是让锁的有效期跟着用户的操作“活”起来,而不是固定5分钟:

  • 核心思路:
    数据库锁表保留ObjectId、UserId、LockedAt、ExpiresAt字段。用户开始操作时获取锁,设置初始过期时间(比如1分钟);前端页面每隔30秒向后端发一次心跳请求,后端收到心跳就把ExpiresAt续期1分钟。如果用户离开超过1分钟没发心跳,锁自动过期释放;如果用户回来继续操作,只要在过期前发心跳就能保住锁。

  • 实现细节:

    • 用EF Core操作锁表,更新过期时间时要加事务,避免并发冲突。
    • 前端可以用setInterval定时发心跳,页面卸载时可以主动释放锁(可选)。
    • 检查锁的时候,要同时判断UserId和ExpiresAt > 当前时间。
  • 代码片段:

    // 锁表实体
    public class ObjectLock
    {
        public int ObjectId { get; set; }
        public string UserId { get; set; } = string.Empty;
        public DateTime LockedAt { get; set; }
        public DateTime ExpiresAt { get; set; }
    
        // 主键用ObjectId,因为一个对象只能被一个用户锁定
        public int Id { get; set; }
    }
    
    // 心跳接口示例
    [ApiController]
    [Route("api/locks")]
    public class LockController : ControllerBase
    {
        private readonly AppDbContext _dbContext;
    
        public LockController(AppDbContext dbContext)
        {
            _dbContext = dbContext;
        }
    
        [HttpPost("renew/{objectId}")]
        public async Task<IActionResult> RenewLock(int objectId, [FromBody] string userId)
        {
            using var transaction = await _dbContext.Database.BeginTransactionAsync();
            var lockRecord = await _dbContext.ObjectLocks
                .FirstOrDefaultAsync(l => l.ObjectId == objectId && l.UserId == userId);
    
            if (lockRecord == null || lockRecord.ExpiresAt < DateTime.UtcNow)
            {
                await transaction.RollbackAsync();
                return BadRequest("锁已过期或不存在");
            }
    
            lockRecord.ExpiresAt = DateTime.UtcNow.AddMinutes(1);
            await _dbContext.SaveChangesAsync();
            await transaction.CommitAsync();
            return Ok();
        }
    }
    
  • 优点:完全支持用户临时离开后返回继续操作,锁的有效期动态调整;实现简单,基于你原有的思路升级,学习成本低。


2. 绑定用户会话的Redis分布式锁

如果你的项目已经在用Redis做缓存或会话存储,这个方案会更高效,而且天然和用户会话绑定:

  • 核心思路:
    用Redis的键值对存储锁,键的格式可以是lock:business-object:{objectId}:session:{sessionId},值为当前用户ID。设置键的初始过期时间(比如1小时),用户每进行一步操作,就给这个键续期。当用户会话过期(比如浏览器关闭、超时),Redis键会自动删除,锁也就释放了。

  • 实现细节:

    • 用ASP.NET Core的IDistributedCache操作Redis,或者直接用StackExchange.Redis客户端。
    • 获取锁时用SET命令的NX参数(不存在才设置),确保只有第一个用户能拿到锁。
    • 前端每次操作都要带上会话ID,后端验证锁的归属。
  • 代码片段:

    public class LockService
    {
        private readonly IDistributedCache _distributedCache;
        private const string LockKeyPrefix = "lock:business-object:";
    
        public LockService(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }
    
        public async Task<bool> AcquireLock(int objectId, string userId, string sessionId)
        {
            var cacheKey = $"{LockKeyPrefix}{objectId}:session:{sessionId}";
            var existingOwner = await _distributedCache.GetStringAsync(cacheKey);
    
            // 如果锁存在且不是当前用户,返回失败
            if (!string.IsNullOrEmpty(existingOwner) && existingOwner != userId)
            {
                return false;
            }
    
            // 设置锁,1小时过期,每次操作续期
            await _distributedCache.SetStringAsync(cacheKey, userId, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
            });
            return true;
        }
    
        public async Task RenewLock(int objectId, string userId, string sessionId)
        {
            var cacheKey = $"{LockKeyPrefix}{objectId}:session:{sessionId}";
            var existingOwner = await _distributedCache.GetStringAsync(cacheKey);
            if (existingOwner == userId)
            {
                await _distributedCache.SetStringAsync(cacheKey, userId, new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
                });
            }
        }
    }
    
  • 优点:Redis性能远高于数据库,适合高并发场景;会话过期自动释放锁,不需要额外的定时清理逻辑;锁和用户会话绑定,更符合用户操作的生命周期。


3. 嵌入业务对象的乐观锁方案

如果不想额外建锁表,可以直接在业务对象上增加锁定字段,结合乐观锁来处理并发:

  • 核心思路:
    在业务对象实体里加LockingUserId(锁定用户ID)、LockExpiresAt(锁定过期时间)、OperationStatus(操作状态:Idle/InProgress/Completed)三个字段,再加一个RowVersion字段做并发令牌。用户开始操作时,检查对象是否处于Idle状态且未被锁定,然后更新为InProgress并设置锁定信息;每一步操作都验证当前用户是锁定者且锁未过期;操作完成后清除锁定信息,设置为Completed。

  • 实现细节:

    • 用EF Core的[Timestamp]标记RowVersion字段,处理并发更新冲突。
    • 操作过程中如果遇到并发异常,说明其他用户已经锁定了对象,返回友好提示。
  • 代码片段:

    public enum OperationStatus
    {
        Idle,
        InProgress,
        Completed
    }
    
    public class BusinessObject
    {
        public int Id { get; set; }
        // 其他业务字段...
    
        public string? LockingUserId { get; set; }
        public DateTime? LockExpiresAt { get; set; }
        public OperationStatus Status { get; set; } = OperationStatus.Idle;
    
        [Timestamp]
        public byte[]? RowVersion { get; set; }
    }
    
    public async Task<bool> StartMultiStepOperation(int objectId, string userId)
    {
        try
        {
            var businessObj = await _dbContext.BusinessObjects
                .FirstOrDefaultAsync(o => o.Id == objectId);
    
            if (businessObj == null || 
                businessObj.Status != OperationStatus.Idle ||
                (businessObj.LockingUserId != null && businessObj.LockExpiresAt > DateTime.UtcNow))
            {
                return false;
            }
    
            businessObj.LockingUserId = userId;
            businessObj.LockExpiresAt = DateTime.UtcNow.AddMinutes(5);
            businessObj.Status = OperationStatus.InProgress;
    
            await _dbContext.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateConcurrencyException)
        {
            // 并发冲突,说明其他用户已经锁定了对象
            return false;
        }
    }
    
  • 优点:不需要额外的锁表,数据结构更紧凑;结合操作状态,能清晰跟踪业务流程的进展;乐观锁机制避免了长时间的数据库锁,性能更好。


额外建议

  • 前端体验优化:锁定期间给其他用户显示“该对象正在被{用户名}处理,请稍后再试”的提示;当前用户的页面定时发心跳,锁即将过期时提醒用户“操作即将超时,请继续操作以保持锁定”。
  • 异常处理:提供管理员强制解锁功能,应对用户操作异常导致锁一直占用的情况;操作完成或页面卸载时主动释放锁,减少资源占用。

内容的提问来源于stack exchange,提问作者pablocity

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:44:14