Blazor Server中使用EF+CancellationToken多次快速调用报错排查
问题分析与解决方案
错误原因
快速重复点击时,前一次请求的取消信号触发后,数据库会话可能仍处于未完全释放的状态,新请求复用了同一数据库连接(或会话),导致SQL Server检测到同一会话存在并行操作,抛出会话繁忙的错误。同时,EF的取消操作在某些时机不会直接抛出OperationCanceledException,而是触发SQL层面的取消信号,进而引发SqlException。
修复方案
1. 捕获并处理取消相关异常
在EF查询代码中捕获OperationCanceledException和包含“Operation cancelled by user”的SqlException,避免错误暴露到上层:
private async Task btnClick() { if (prevCts is not null) prevCts.Cancel(); cts = new CancellationTokenSource(); prevCts = cts; var token = cts.Token; try { using var cx = cxf.CreateDbContext(); var res = await cx.Lunches.ToArrayAsync(token); // 处理查询结果 } catch (OperationCanceledException) { // 前一次请求被取消,无需处理 } catch (SqlException ex) when (ex.Message.Contains("Operation cancelled by user")) { // 捕获SQL层面的取消异常,忽略即可 } }
2. 添加防抖逻辑减少重复请求
在按钮点击时设置最小响应间隔,短时间内重复点击只响应最后一次,从源头降低冲突概率:
private CancellationTokenSource cts; private DateTime _lastClickTime = DateTime.MinValue; private readonly TimeSpan _debounceInterval = TimeSpan.FromMilliseconds(300); private async Task btnClick() { var now = DateTime.UtcNow; if (now - _lastClickTime < _debounceInterval) { cts?.Cancel(); cts = new CancellationTokenSource(); return; } _lastClickTime = now; if (cts is not null) cts.Cancel(); cts = new CancellationTokenSource(); var token = cts.Token; try { using var cx = cxf.CreateDbContext(); var res = await cx.Lunches.ToArrayAsync(token); // 处理结果 } catch (OperationCanceledException) { } catch (SqlException ex) when (ex.Message.Contains("Operation cancelled by user")) { } }
3. 优化DbContext连接配置(可选)
虽然使用IDbContextFactory每次创建新的DbContext,仍可通过配置减少连接池复用带来的会话冲突:
// 在Program.cs中配置DbContextFactory builder.Services.AddDbContextFactory<MyContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("MyDb"), sqlOpts => sqlOpts.EnableRetryOnFailure() .CommandTimeout(30)); });
关键说明
- 取消操作不会立即终止数据库请求,只是发送信号,数据库需要时间响应,因此快速触发的新请求可能和未完全终止的旧请求产生会话冲突。
- 防抖逻辑可以有效减少不必要的重复请求,降低冲突概率。
- 捕获特定异常是为了避免取消操作引发的错误被误判为业务异常。
内容的提问来源于stack exchange,提问作者buga
相关产品推荐
相关产品推荐

