ASP.NET MVC中IActionResult的Lock/Monitor实现问题
ASP.NET MVC 实现概念编辑的独占锁定方案
核心思路
基于本地数据库实现编辑锁(进程内的Lock/Monitor仅适用于单进程场景,无法跨用户/进程控制,因此用数据库字段记录锁定状态更可靠),通过在概念表中添加锁定相关字段,结合用户标识和过期时间,实现同一概念仅允许单个用户编辑的控制逻辑。
步骤1:修改数据库表
在你的Concept表中添加锁定状态相关字段:
ALTER TABLE Concept ADD IsLocked BIT NOT NULL DEFAULT 0, LockedBy NVARCHAR(128) NULL, -- 存储用户ID(如ASP.NET Identity的UserId)或用户名 LockExpiration DATETIME NULL; -- 锁的过期时间,防止用户异常退出导致死锁
步骤2:仓储层实现锁定逻辑
在仓储接口(如IConceptRepository)中定义锁定相关方法:
public interface IConceptRepository { // 尝试锁定概念,返回是否成功 bool TryLockConcept(int conceptId, string userId); // 释放指定用户持有的概念锁 void ReleaseLock(int conceptId, string userId); // 检查概念的锁定状态 (bool IsLocked, string LockedBy, DateTime? LockExpiration) CheckLockStatus(int conceptId); // 刷新锁的过期时间(用户操作时延长锁定时长) bool RefreshLock(int conceptId, string userId); }
以下是EF Core实现示例:
public class ConceptRepository : IConceptRepository { private readonly YourDbContext _context; private readonly TimeSpan _lockDuration = TimeSpan.FromMinutes(15); // 锁定有效期15分钟 public ConceptRepository(YourDbContext context) { _context = context; } public bool TryLockConcept(int conceptId, string userId) { var concept = _context.Concepts.Find(conceptId); if (concept == null) return false; // 未锁定或锁已过期,直接加锁;如果是当前用户持有的锁,自动刷新时间 if (!concept.IsLocked || concept.LockExpiration <= DateTime.Now) { concept.IsLocked = true; concept.LockedBy = userId; concept.LockExpiration = DateTime.Now.Add(_lockDuration); _context.SaveChanges(); return true; } else if (concept.LockedBy == userId) { RefreshLock(conceptId, userId); return true; } return false; } public void ReleaseLock(int conceptId, string userId) { var concept = _context.Concepts.Find(conceptId); if (concept != null && concept.LockedBy == userId) { concept.IsLocked = false; concept.LockedBy = null; concept.LockExpiration = null; _context.SaveChanges(); } } public (bool IsLocked, string LockedBy, DateTime? LockExpiration) CheckLockStatus(int conceptId) { var concept = _context.Concepts.Find(conceptId); if (concept == null) return (false, null, null); // 锁已过期则自动解锁 if (concept.IsLocked && concept.LockExpiration <= DateTime.Now) { concept.IsLocked = false; concept.LockedBy = null; concept.LockExpiration = null; _context.SaveChanges(); return (false, null, null); } return (concept.IsLocked, concept.LockedBy, concept.LockExpiration); } public bool RefreshLock(int conceptId, string userId) { var concept = _context.Concepts.Find(conceptId); if (concept != null && concept.LockedBy == userId) { concept.LockExpiration = DateTime.Now.Add(_lockDuration); _context.SaveChanges(); return true; } return false; } }
步骤3:Controller层处理编辑请求
GET 请求(进入编辑页面)
public IActionResult EditConcept(int id) { var userId = User.Identity.Name; // 或用User.FindFirstValue(ClaimTypes.NameIdentifier)获取用户ID var lockStatus = _conceptRepository.CheckLockStatus(id); // 被其他用户锁定,返回提示页 if (lockStatus.IsLocked && lockStatus.LockedBy != userId) { ViewBag.LockMessage = $"该概念已被用户 {lockStatus.LockedBy} 锁定,预计 {lockStatus.LockExpiration?.ToString("yyyy-MM-dd HH:mm")} 后自动解锁"; return View("LockError"); } // 尝试锁定概念,失败则返回提示 var lockSuccess = _conceptRepository.TryLockConcept(id, userId); if (!lockSuccess) { return View("LockError"); } // 获取概念数据并返回编辑视图 var concept = _conceptRepository.GetById(id); return View(concept); }
POST 请求(提交编辑内容)
[HttpPost] [ValidateAntiForgeryToken] public IActionResult EditConcept(ConceptViewModel model) { var userId = User.Identity.Name; if (!ModelState.IsValid) { // 验证失败,返回编辑页并保留锁 return View(model); } try { // 执行编辑逻辑 _conceptRepository.Update(model); _context.SaveChanges(); // 编辑成功后释放锁 _conceptRepository.ReleaseLock(model.Id, userId); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", $"编辑失败:{ex.Message}"); // 编辑失败,保留锁让用户继续操作 return View(model); } }
可选优化:前端自动刷新锁
为避免用户长时间操作导致锁过期,可在编辑页添加定时AJAX请求刷新锁:
// 每10分钟刷新一次锁 setInterval(function() { $.ajax({ url: '@Url.Action("RefreshLock", "Concept")', type: 'POST', data: { conceptId: @Model.Id }, success: function(response) { if (!response.success) { alert("你的编辑锁已过期,请重新进入编辑页面"); window.location.href = '@Url.Action("Index", "Concept")'; } } }); }, 600000); // 10分钟=600000毫秒
对应的Controller方法:
[HttpPost] public IActionResult RefreshLock(int conceptId) { var userId = User.Identity.Name; var success = _conceptRepository.RefreshLock(conceptId, userId); return Json(new { success }); }
注意事项
- 不要使用进程内的
Lock/Monitor:ASP.NET MVC多进程环境下,进程内锁无法跨进程控制用户并发编辑。 - 合理设置过期时间:根据业务场景调整(如15-30分钟),避免用户临时离开导致锁长时间占用。
- 异常场景处理:用户关闭浏览器、崩溃等情况,依赖过期时间自动解锁,无需额外处理。
内容的提问来源于stack exchange,提问作者gailtye
相关产品推荐
相关产品推荐

