.NET Core 6 API+React+PostgreSQL实现资源互斥操作方案咨询
数据项操作锁定机制实现方案(React + .NET Core 6 + PostgreSQL)
针对你提出的需求,以下是三种适配技术栈的可行方案,从轻量到分布式场景覆盖:
方案一:数据库级操作会话锁(单实例后端首选)
1. 数据库表调整
在目标数据项表(例如items)中新增两个字段:
locked_by: VARCHAR(50) 存储锁定用户的IDlocked_until: TIMESTAMP 存储锁的过期时间
2. .NET Core 后端实现
- 获取锁接口(用户开始操作时调用):
[HttpPost("lock/{itemId}")] public async Task<IActionResult> LockItem(Guid itemId, string userId) { var item = await _dbContext.Items.FindAsync(itemId); if (item == null) return NotFound(); // 校验是否被其他用户锁定且未过期 if (item.LockedBy != userId && item.LockedUntil > DateTime.UtcNow) { return Conflict(new { message = "该数据项正在被其他用户操作,无法删除" }); } // 锁定15分钟(可根据业务调整) item.LockedBy = userId; item.LockedUntil = DateTime.UtcNow.AddMinutes(15); await _dbContext.SaveChangesAsync(); return Ok(); } - 删除接口锁校验:
[HttpDelete("{itemId}")] public async Task<IActionResult> DeleteItem(Guid itemId, string userId) { var item = await _dbContext.Items.FindAsync(itemId); if (item == null) return NotFound(); // 非当前用户持有有效锁则禁止删除 if (item.LockedBy != userId && item.LockedUntil > DateTime.UtcNow) { return Conflict(new { message = "该数据项正在被其他用户操作,无法删除" }); } _dbContext.Items.Remove(item); await _dbContext.SaveChangesAsync(); return NoContent(); } - 解锁接口(操作完成/页面关闭时调用):
[HttpPost("unlock/{itemId}")] public async Task<IActionResult> UnlockItem(Guid itemId, string userId) { var item = await _dbContext.Items.FindAsync(itemId); if (item == null) return NotFound(); if (item.LockedBy == userId) { item.LockedBy = null; item.LockedUntil = null; await _dbContext.SaveChangesAsync(); } return Ok(); }
3. React前端实现
- 用户进入操作页面时,先调用
lock接口,成功后才展示编辑/操作组件;失败则提示用户 - 监听页面卸载事件(
beforeunload),自动调用unlock接口释放锁 - 删除按钮点击前,直接调用删除接口,根据返回的
Conflict状态码提示用户 - 可选:每10分钟调用一次
lock接口延长锁有效期,避免操作中途锁过期
方案二:Redis分布式锁(多实例后端场景)
如果后端是多实例部署,数据库锁可能存在一致性延迟,推荐结合Redis实现分布式锁:
1. 后端依赖
安装StackExchange.Redis包,配置Redis连接字符串
2. 核心逻辑
- 获取锁方法:
public async Task<bool> AcquireItemLock(string itemId, string userId, int expiryMinutes = 15) { var lockKey = $"item_lock:{itemId}"; // 仅当锁不存在时设置成功,同时绑定过期时间 return await _redisDatabase.StringSetAsync(lockKey, userId, TimeSpan.FromMinutes(expiryMinutes), When.NotExists); } - 删除接口校验:
[HttpDelete("{itemId}")] public async Task<IActionResult> DeleteItem(Guid itemId, string userId) { var lockKey = $"item_lock:{itemId}"; var lockedUserId = await _redisDatabase.StringGetAsync(lockKey); // 存在非当前用户的有效锁则禁止删除 if (!lockedUserId.IsNullOrEmpty && lockedUserId != userId) { return Conflict(new { message = "该数据项正在被其他用户操作,无法删除" }); } // 同时校验数据库状态(防止Redis锁过期但用户仍在操作) var item = await _dbContext.Items.FindAsync(itemId); if (item == null) return NotFound(); _dbContext.Items.Remove(item); await _dbContext.SaveChangesAsync(); // 删除锁 await _redisDatabase.KeyDeleteAsync(lockKey); return NoContent(); } - 前端逻辑与方案一一致,需在操作开始/结束时调用对应锁接口
方案三:乐观锁(轻量防并发场景)
如果不需要强锁定,仅需防止并发删除导致的数据异常,可使用乐观锁方案:
1. 数据库表调整
新增version字段(INT类型),每次数据更新时自动递增
2. 后端删除接口实现
[HttpDelete("{itemId}")] public async Task<IActionResult> DeleteItem(Guid itemId, int currentVersion) { var item = await _dbContext.Items.FindAsync(itemId); if (item == null) return NotFound(); // 版本不匹配说明数据已被其他用户修改 if (item.Version != currentVersion) { return Conflict(new { message = "该数据项已被其他用户修改,请刷新后重试" }); } _dbContext.Items.Remove(item); await _dbContext.SaveChangesAsync(); return NoContent(); }
3. 前端实现
- 获取数据时保存
version值 - 删除操作时提交当前版本号,若返回冲突则提示用户刷新页面重新获取数据
关键注意事项
- 锁过期时间需合理设置,避免用户意外退出导致数据项长期锁定
- 前端必须处理页面意外关闭的场景,确保锁能被及时释放
- 多实例部署时优先选择Redis分布式锁,保证锁的全局一致性
内容的提问来源于stack exchange,提问作者Pavithran
相关产品推荐
相关产品推荐

