ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Blazor Server 性能优化实战:Circuit、数据库、Redis 与并发

Blazor Server 性能优化实战:Circuit、数据库、Redis 与并发 这篇不写理论上应该怎样只讲这个项目的配置值、代码路径和排查方法。文中提到的每个数字都能在源码里找到。一、先理解成本模型Blazor Server 和传统 MVC 最大的差别组件状态活在服务端。每个在线用户 ≈ 一个 Circuit服务端对象图 组件状态 DI 作用域 内存占用 ≈ 在线用户数 × 每个 Circuit 的状态大小所以性能优化的第一性问题不是SQL 快不快而是同时在线多少人每个页面在 Circuit 里保留了多少状态断线后这些状态保留多久框架的 Circuit 配置直接对应第 3 个问题// 电路断开后保留时长后台 Tab 冻结导致连接断开时保留电路供切回时快速恢复builder.Services.ConfigureMicrosoft.AspNetCore.Components.Server.CircuitOptions(options{options.DisconnectedCircuitRetentionPeriodTimeSpan.FromMinutes(10);options.DisconnectedCircuitMaxRetained200;});配置值含义代价DisconnectedCircuitRetentionPeriod10 分钟断线后电路保留多久保留期间内存不释放DisconnectedCircuitMaxRetained200最多保留多少个断开的电路超出后最旧的被回收这两个值是体验换内存的旋钮。200 个保留电路对普通后台够用如果单机能承载的在线用户很多比如 2000要把这个数字和服务器内存一起算粗算每个 Circuit 常驻内存组件 状态 服务 × 200 再加上在线电路数 × 每电路内存调小MaxRetained或RetentionPeriod能省内存代价是用户切回标签页时可能已经重连。二、SignalR 连接参数builder.Services.AddSignalR(options{options.KeepAliveIntervalTimeSpan.FromSeconds(15);options.ClientTimeoutIntervalTimeSpan.FromMinutes(3);});配置默认本项目影响KeepAliveInterval15 秒15 秒服务端 ping 频率越大越省流量但发现断线越慢ClientTimeoutInterval30 秒3 分钟客户端多久无响应判定断线把超时从 30 秒放宽到 3 分钟是专门为后台标签页被浏览器冻结这个场景做的——默认值下切回标签页几乎必然触发重连。代价真正掉线的连接会更晚被发现服务端多保留一段时间的死连接。对后台场景用户数有限、操作不连续这个取舍是划算的。三、页面渲染避免查询跑两遍和空表格闪现1. 回调必须在 await 之前赋值AdminTable里有一段带警告注释的代码// ⚠️ OnQueryAsync/OnSaveAsync/OnDeleteAsync 必须在任何 await 之前先赋值// 否则 Blazor 在第一个 await 处就会调用 StateHasChanged() 触发首次渲染// 此时 OnQueryAsync 仍为 null导致首次数据加载失败。if(OnQueryAsyncnullItemsnull){OnQueryAsyncOnQueryDataAsync;// 首次渲染前即进入查询中状态避免查询期间空行闪现无数据_queryingtrue;}这不是代码风格问题而是真实的性能与体验问题赋值晚了 → 首次渲染拿到null回调 → 页面空一下再查 → 用户看到无数据闪烁某些实现会在渲染后再次触发查询 →一次页面打开查两次库。2. 长时间操作要给可见状态导出时的正在导出是服务端状态渲染的不依赖 JSprivateRenderFragmentExportProgressTemplate__builder{if(_exporting){spanclassme-2 text-info align-self-centericlassfa-solid fa-spinner fa-spin me-1/iCommonLocalizer[正在导出]/span}};_exportingtrue;awaitInvokeAsync(StateHasChanged);try{...}finally{_exportingfalse;awaitInvokeAsync(StateHasChanged);}导出是耗时操作没有进度的长任务在用户看来就是卡死了。这段代码的价值在于状态变化立即渲染用户知道系统还在干活。四、数据库层四个真正影响性能的点1. 分页与 Count 用同一个查询对象varqueryselect.WhereDynamicFilter(dynamicFilter).ApplyOrderT,TKey(options).Count(outvarcount);varitemsoptions.IsPage?awaitquery.Page(options.PageIndex,options.PageItems).ToListAsync():awaitquery.ToListAsync();好处不只是条件一致还避免了手写两遍条件带来的额外维护成本。成本提示Count在大表上本身有开销。如果你的表到了千万级需要评估是否必须有总数BootstrapBlazor 支持不显示总数。2. 导出走列投影而且导出时不 Include// 只查询导出列动态投影避免 SELECT * 把大文本/导航数据全部拉回来// 查询链路不变OnBeforeQuery 过滤 数据权限 动态过滤 排序Include 不会执行rowsawaitLoadExportRowsAsync(select,columns,context.Options,lookupService,exportOptions);LoadExportRowsAsync用Reflection.Emit动态生成 DTO 类型带缓存只SELECT需要的列。对照物是反例导出 1 万行 × 每行带一个正文大字段 Include 3 个导航集合 → 内存、网络、GC 全部放大通常直接超时3. 列表页谨慎 IncludeprivatevoidOnBeforeQuery(AdminQueryEventArgsArticlee){if(!e.IsExport){e.Select.Include(aa.Classify);}}框架专门在导出场景传IsExport true注释写得很直白/// summary/// 是否导出场景。导出全量数据时应跳过 Include/IncludeMany 导航集合加载避免上千行时极慢。/// /summary4. 批量写而不是逐行写导入默认走一条批量语句affectedRowsawait_repo.Orm.InsertOrUpdateTItem().SetSource(rows).UpdateColumns(updateColumns).ExecuteAffrowsAsync();批量删除也一样vardeleteIdsitems.Select(ii.Id).ToList();await_repo.Orm.UpdateTItem().SetByPropertyName(IsDeleted,true).Where(xdeleteIds.Contains(x.Id)).ExecuteAffrowsAsync();五、缓存层每类数据的 TTL 是设计出来的项目里的缓存不是随便加一层每个 TTL 都对应一类数据的变更频率数据缓存位置有效期失效方式系统配置SysConfigICacheService1 分钟到期自动用户角色ICacheService30 分钟权限版本号 1用户菜单ICacheService30 分钟权限版本号 1字典项ICacheService30 秒到期自动实体选择器选项ICacheService30 秒到期自动审批流程配置ICacheService由 Provider 控制改配置后按 key 失效权限缓存用版本号而不是逐个删键publicasyncTaskInvalidatePermissionCacheAsync(){varversionawaitGetPermissionVersionAsync();awaitcache.SetAsync(ScopedPermissionVersionKey,version1,TimeSpan.FromDays(30));}代价与边界ICacheService默认是进程内缓存MemoryCacheService。多实例部署时改权限只在当前实例生效的问题需要靠 Redis / FusionCache 解决第 19 篇。六、并发与资源几个容易被忽略的点1. 同一租户的初始化必须串行// 同一个 tenantCode 的初始化必须串行防止多个请求同时创建同一个租户的 FreeSql 与表结构。varinitLock_initLocks.GetOrAdd(tenantCode,_newSemaphoreSlim(1,1));initLock.Wait();如果没有这把锁第一个租户首次访问时N 个并发请求会同时建库建表。如源码注释所说多实例部署需要分布式锁配合。2. 审批用条件更新而不是锁第 17 篇讲过并发审批靠WHERE 状态 级次的条件更新affected 0即冲突。这比加锁更轻也不会因为持锁进程崩溃而卡住流程。3. 日志落库不阻塞业务_queue.TryEnqueue(newDatabaseLogEntry(...));TryEnqueue是非阻塞的写日志的线程不会等数据库第 27 篇。这就是有界队列带来的性能收益。4. 连接池// 多租户库varfsqlnewFreeSqlBuilder().UseConnectionString(tenantInfo.DataType,tenantInfo.ConenctionString.Replace({database},tenantCode)).UseAdoConnectionPool(true).Build();多租户意味着成倍的连接来源连接池不是可选项。5. FreeSql 实例的生命周期// 注册 IFreeSql 为 Singleton从 MainOrmHandle 解析。// 注意IFreeSql 实现了 IDisposable如果注册为 Scoped 则 DI 会在每次作用域结束时// 对其调用 Dispose()导致 Singleton 内部的 ObjectPool 被释放引发// Cannot access a disposed object 错误。故改为从 Singleton 的 MainOrmHandle 解析。builder.Services.AddSingletonIFreeSql(spsp.GetRequiredServiceMainOrmHandle().Orm);误注册成 Scoped 会导致间歇性的 “Cannot access a disposed object”而且往往在高并发下才出现——这类问题排查成本极高源码注释直接把它标出来了。6. 多实例部署要注意雪花 ID 的 WorkIdpublicclassEasyAdminBlazorOptions{publicushortWorkId{get;set;}1;...}多实例部署时每个实例必须配置不同的WorkId否则雪花算法可能生成重复主键同毫秒 同机器号 同序列。七、怎么排查现象 → 可能原因现象大概率原因从哪查页面打开后无数据闪一下再出现查询回调赋值时机问题OnParametersSetAsync的 await 前赋值切回标签页就重连Circuit 已释放 / 代理超时DisconnectedCircuitRetentionPeriod、反向代理 WebSocket 配置第 28 篇导出超时或内存飙升导出带了Include或未走投影OnBeforeQuery里的IsExport判断列表页越用越慢大表 模糊搜索 无索引打开UseMonitorCommand看 SQL 与执行计划权限改了不生效权限缓存未失效 / 多实例InvalidatePermissionCacheAsync、是否接入 Redis/FusionCache内存持续上涨断线电路堆积 / 日志队列异常 / 大对象缓存DisconnectedCircuitMaxRetained、DroppedCount、缓存键排查高并发下偶发Cannot access a disposed objectIFreeSql生命周期注册错误确认是 Singleton见上文注释多实例下主键冲突WorkId未区分每个实例配置不同WorkId打开 SQL 日志FreeSqlBuilderaa.UseConnectionString(DataType.Sqlite,configuration[ConnectionStrings:default]).UseMonitorCommand(cmdSystem.Console.WriteLine($[{DateTime.Now.ToString(HH:mm:ss)}]{cmd.CommandText}\r\n))//监听SQL语句只在诊断时打开。生产环境长期打印全量 SQL 既影响性能也可能把参数写进日志。八、小结层关键动作Circuit按内存预算调整DisconnectedCircuitRetentionPeriod/DisconnectedCircuitMaxRetained连接放宽ClientTimeoutInterval3 分钟换取标签页冻结时的稳定性渲染回调在 await 前赋值长任务显示进行中状态查询分页与 Count 同条件列表谨慎 Include导出走投影且不 Include写入批量操作审批用条件更新租户初始化用信号量缓存按数据变更频率设 TTL权限用版本号失效多实例上 Redis/FusionCache基础连接池、IFreeSqlSingleton、日志有界队列、多实例区分WorkId诊断慢 SQL 监控、DroppedCount、权限缓存与 Circuit 保留数Blazor Server 的性能优化一半在数据库和缓存另一半在电路与长连接这些服务端状态上。先把状态管好再谈 SQL 调优。如果你正在用 .NET 10 Blazor 做后台可以直接对照这篇检查自己的 Circuit、缓存和查询配置。EasyAdminBlazor 的相关参数都在源码里改起来不需要读框架内部实现。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor
返回列表