Blazor交互式服务器应用:如何同步处理用户事件避免并发异常?
解决Blazor Interactive Server会话内报名事件同步问题
针对你遇到的同一用户会话内多次快速点击按钮引发的并发更新异常,完全可以通过会话级别的同步机制来强制事件处理器串行执行,以下是几种可行方案:
1. 基于Scoped服务的异步锁控制
Blazor Interactive Server中,每个用户会话对应一组Scoped生命周期的服务实例。我们可以创建一个Scoped的锁服务,用SemaphoreSlim实现会话内的异步操作串行化:
步骤1:定义Scoped锁服务
public class SessionOperationLock { // 初始许可数设为1,保证同一时间只有一个操作执行 public SemaphoreSlim RegistrationSemaphore { get; } = new SemaphoreSlim(1, 1); }
步骤2:注册Scoped服务
在Program.cs中添加服务注册:
builder.Services.AddScoped<SessionOperationLock>();
步骤3:在组件中使用锁控制事件执行
@inject SessionOperationLock SessionLock @inject YourDbContext _dbContext <button @onclick="() => UpdateRegistration(Status.NotAttending)">不参加</button> <button @onclick="() => UpdateRegistration(Status.Undecided)">未决定</button> <button @onclick="() => UpdateRegistration(Status.Interested)">感兴趣</button> <button @onclick="() => UpdateRegistration(Status.Attending)">参加</button> @code { private enum Status { NotAttending, Undecided, Interested, Attending } private int _recordId1 = 1; // 替换为实际记录ID private int _recordId2 = 2; private async Task UpdateRegistration(Status newStatus) { await SessionLock.RegistrationSemaphore.WaitAsync(); try { // 读取两条记录 var record1 = await _dbContext.ActivityRegistrations.FindAsync(_recordId1); var record2 = await _dbContext.ActivityRegistrations.FindAsync(_recordId2); // 更新报名状态 record1.Status = newStatus.ToString(); record2.Status = newStatus.ToString(); // 保存更改 await _dbContext.SaveChangesAsync(); } finally { // 无论操作成功或失败,都释放锁 SessionLock.RegistrationSemaphore.Release(); } } }
这个方案的核心是:SessionOperationLock作为Scoped服务,每个用户会话拥有独立的实例,因此SemaphoreSlim只会限制当前会话内的操作,不会影响其他用户的请求,完全符合你的需求。
2. 前端按钮禁用+后端锁的双重保障
为了从交互层面避免用户重复点击,可以结合前端按钮禁用逻辑,进一步降低并发触发的概率:
@inject SessionOperationLock SessionLock @inject YourDbContext _dbContext <button @onclick="() => UpdateRegistration(Status.NotAttending)" disabled="@_isProcessing">不参加</button> <button @onclick="() => UpdateRegistration(Status.Undecided)" disabled="@_isProcessing">未决定</button> <button @onclick="() => UpdateRegistration(Status.Interested)" disabled="@_isProcessing">感兴趣</button> <button @onclick="() => UpdateRegistration(Status.Attending)" disabled="@_isProcessing">参加</button> @code { private enum Status { NotAttending, Undecided, Interested, Attending } private int _recordId1 = 1; private int _recordId2 = 2; private bool _isProcessing; private async Task UpdateRegistration(Status newStatus) { if (_isProcessing) return; _isProcessing = true; try { await SessionLock.RegistrationSemaphore.WaitAsync(); try { var record1 = await _dbContext.ActivityRegistrations.FindAsync(_recordId1); var record2 = await _dbContext.ActivityRegistrations.FindAsync(_recordId2); record1.Status = newStatus.ToString(); record2.Status = newStatus.ToString(); await _dbContext.SaveChangesAsync(); } finally { SessionLock.RegistrationSemaphore.Release(); } } finally { _isProcessing = false; } } }
通过_isProcessing变量控制按钮禁用状态,用户点击后按钮立即不可用,直到操作完成才恢复,从源头减少重复触发的可能,再配合后端的会话级锁,彻底解决并发更新问题。
注意事项
- 务必将
SemaphoreSlim的释放操作放在finally块中,防止异常导致锁无法释放,造成会话内后续操作阻塞。 - 如果你需要处理跨会话的并发(如用户多浏览器操作),可以结合EF Core的乐观并发控制(在实体中添加
RowVersion属性),但根据你的需求,这一步可以忽略。
内容的提问来源于stack exchange,提问作者David Thielen
相关产品推荐
相关产品推荐

