ASP.NET Core 数据库日志怎么做?为什么日志写入需要有界队列?
把日志写进数据库看起来很直接INSERT一下不就行了真正跑起来才会遇到三个问题——数据库慢/挂了怎么办、内存会不会涨爆、日志本身会不会变成故障源。框架的答案是有界队列 后台批量写入 退避重试。一、整体链路业务代码 ILogger.LogError(...) ↓ DatabaseLogger.Log ↓ 补充上下文TraceId / RequestPath / UserId / TenantCode ↓ TryEnqueue有界 Channel容量 5000写满丢最旧 ↓ DatabaseLoggerBackgroundServiceBackgroundService ↓ 攒批BatchSize 100或到点FlushInterval 5s ↓ FreeSql 批量插入 SysLog ↓ 失败 → 指数退避重试2s → 最大 2min日志进有界重试缓冲核心设计写日志的调用方永远不等待数据库。这句决定了后面所有细节。二、为什么必须有界如果用一个无界队列数据库一旦不可用数据库挂了 → 队列只进不出 → 内存持续增长 → 应用 OOM 崩溃 → 而崩溃又让数据库看起来没问题因为没人在写所以队列是固定容量的有界 Channel/// summary/// 数据库日志的有界队列。////// 队列容量固定默认 5000写满时按 DropOldest 策略丢弃最旧的日志/// 保证内存占用有上界被丢弃的条数通过 DroppedCount 暴露以便监控。/// /summarypublicsealedclassDatabaseLoggerQueue{/// summary队列容量达到上限后丢弃最旧日志/summarypublicconstintCapacity5000;privatelong_droppedCount;privatereadonlyChannelDatabaseLogEntry_channelChannel.CreateBoundedDatabaseLogEntry(newBoundedChannelOptions(Capacity){FullModeBoundedChannelFullMode.DropOldest,SingleReadertrue,SingleWriterfalse});/// summary因队列已满而被丢弃的日志条数/summarypubliclongDroppedCountInterlocked.Read(ref_droppedCount);}配置值含义Capacity5000内存中最多缓存 5000 条日志FullModeDropOldest写满时丢最旧的优先保留最近日志SingleReadertrue只有一个后台服务读允许内部优化SingleWriterfalse多线程都能写日志必然如此为什么丢最旧的而不是最新的出故障时最近发生了什么更有价值而且丢最旧的不需要阻塞正在写日志的线程。DroppedCount是给监控用的日志开始被丢弃说明数据库写入跟不上这本身就是一个需要告警的信号。三、写日志时补充上下文publicvoidLogTState(LogLevellogLevel,EventIdeventId,TStatestate,Exception?exception,FuncTState,Exception?,stringformatter){if(!IsEnabled(logLevel))return;varmessageformatter(state,exception);// 尽力补充请求上下文TraceId / 路径 / 用户 / 租户。// 该补充过程绝不能抛异常否则日志本身会变成故障源。string?traceIdnull;string?requestPathnull;long?userIdnull;string?tenantCodenull;try{varhttpContextResolveHttpContext();if(httpContext!null){traceIdSystem.Diagnostics.Activity.Current?.Id??httpContext.TraceIdentifier;requestPathhttpContext.Request?.Path.Value;}varadminContext_serviceProvider?.GetService(typeof(AdminContext))asAdminContext;if(adminContext!null){userIdadminContext.User?.Id;tenantCodeadminContext.TenantCode;}}catch(ObjectDisposedException){// 作用域已释放忽略上下文补充}catch(InvalidOperationException){// 无法解析作用域服务忽略上下文补充}_queue.TryEnqueue(newDatabaseLogEntry(...){TraceIdTruncate(traceId,100),RequestPathTruncate(requestPath,500),UserIduserId,TenantCodeTruncate(tenantCode,50),EventIdeventId.Id});}这段代码体现了三条纪律上下文补充失败不能影响写日志。整个块被 try/catch 包住只捕获ObjectDisposedException/InvalidOperationException作用域释放、无法解析服务等预期情况。所有字段都截断Truncate避免超长消息把数据库列写爆消息 2000、异常 12000、路径 500、TraceId 100。TraceId 优先取Activity.Current?.Id没有分布式追踪时退化为HttpContext.TraceIdentifier——这条让用户看到的错误提示和服务端日志能对上第 18 篇里GenericFailure返回的 TraceId 是同一套思路。落库的实体字段publicclassSysLog:Entitylong{[DisplayName(异常时间)]publicDateTimeCreatedTime{get;set;}[Column(StringLength50)]publicstringLogLevel{get;set;}string.Empty;[Column(StringLength200)]publicstringCategory{get;set;}string.Empty;[Column(StringLength2000)]publicstringMessage{get;set;}string.Empty;[Column(StringLength-2)]publicstringException{get;set;}string.Empty;[Column(StringLength100)]publicstring?TraceId{get;set;}[Column(StringLength500)]publicstring?RequestPath{get;set;}publiclong?UserId{get;set;}[Column(StringLength50)]publicstring?TenantCode{get;set;}publicint?EventId{get;set;}}注意TenantCode这一列出问题时能直接筛出是哪个租户报的错这是多租户场景排查的关键字段。四、后台批量写入publicsealedclassDatabaseLoggerBackgroundService(DatabaseLoggerQueuequeue,MainOrmHandlemainOrmHandle):BackgroundService{privateconstintBatchSize100;privatestaticreadonlyTimeSpanFlushIntervalTimeSpan.FromSeconds(5);/// summary数据库不可用时的初始退避时间/summaryprivatestaticreadonlyTimeSpanInitialBackoffTimeSpan.FromSeconds(2);/// summary退避时间上限避免长时间不落库/summaryprivatestaticreadonlyTimeSpanMaxBackoffTimeSpan.FromMinutes(2);/// summary/// 数据库持续失败时本地缓冲区上限。达到上限后丢弃最旧的日志DropOldest/// 保证内存不会无限增长同时优先保留最近、最相关的日志。/// /summaryprivateconstintMaxRetryBuffer2000;}主循环等两件事攒够 100 条或者距上次写满 5 秒usingvartimernewPeriodicTimer(FlushInterval);varreadTask_queue.ReadAsync(stoppingToken).AsTask();vartimerTasktimer.WaitForNextTickAsync(stoppingToken).AsTask();while(!stoppingToken.IsCancellationRequested){varcompletedTaskawaitTask.WhenAny(readTask,timerTask);if(completedTaskreadTask){AppendBounded(buffer,retryBuffer,awaitreadTask);if(buffer.CountBatchSize){varsucceededawaitFlushAsync(buffer,retryBuffer,stoppingToken);backoffawaitAdjustBackoffAsync(succeeded,backoff,stoppingToken);}readTask_queue.ReadAsync(stoppingToken).AsTask();}else{if(awaittimerTask){varsucceededawaitFlushAsync(buffer,retryBuffer,stoppingToken);backoffawaitAdjustBackoffAsync(succeeded,backoff,stoppingToken);timerTasktimer.WaitForNextTickAsync(stoppingToken).AsTask();}elsebreak;}}这个批 or 超时的模式是日志落库的标准做法日志多时按量刷避免每条一次INSERT日志少时按时间刷避免日志在内存里待太久最多 5 秒用PeriodicTimer而不是Task.Delay循环避免累积漂移。再有一层有界缓冲privatestaticvoidAppendBounded(ListDatabaseLogEntrybuffer,QueueDatabaseLogEntryretryBuffer,DatabaseLogEntryentry){if(buffer.CountMaxRetryBuffer){buffer.RemoveAt(0);}buffer.Add(entry);// 队列越长说明数据库越不可用此时同步压缩重试缓冲避免内存持续增长while(retryBuffer.Countbuffer.CountMaxRetryBufferretryBuffer.Count0){retryBuffer.Dequeue();}}为什么 Channel 已经有界了本地缓冲还要再有界因为数据库写入失败时日志会从队列挪到本地缓冲等待重试。如果缓冲无界一样会涨爆。所以Channel 队列 5000 ← 对业务写日志的缓冲 本地重试缓冲 2000 ← 对数据库故障的缓冲 两者都是 DropOldest两层加起来内存占用有明确上界——这正是有界队列的核心价值。五、数据库不可用时的退避privatestaticasyncTaskTimeSpanAdjustBackoffAsync(boolsucceeded,TimeSpanbackoff,CancellationTokenstoppingToken){if(succeeded)returnInitialBackoff;varnextbackoff*2;if(nextMaxBackoff)nextMaxBackoff;// 退避等待避免数据库不可用时持续高频重试try{awaitTask.Delay(backoff,stoppingToken);}catch(OperationCanceledException){// 停止中直接返回下次退避时间}returnnext;}退避序列2s → 4s → 8s → 16s → 32s → 64s → 120s封顶成功后立刻复位到 2 秒。好处是数据库短时抖动后能快速补写长时间不可用也不会把 CPU 和连接数浪费在无休止重试上期间应用照常运行。进程退出时还会做最后一次冲刷finally{usingvarflushCtsnewCancellationTokenSource(TimeSpan.FromSeconds(5));awaitFlushAsync(buffer,retryBuffer,flushCts.Token);}注意这 5 秒的收尾冲刷是尽力而为。容器里被SIGKILL时不会有这个机会所以关键审计日志不能只依赖异步落库。六、注册与级别控制// 注册日志服务并添加 DatabaseLoggerProviderbuilder.Services.AddSingletonDatabaseLoggerQueue();builder.Services.AddHostedServiceDatabaseLoggerBackgroundService();// 通过工厂注入 IServiceProvider日志写入时尽力补充 TraceId/路径/用户/租户上下文builder.Services.AddSingletonILoggerProvider(spnewDatabaseLoggerProvider(sp.GetRequiredServiceDatabaseLoggerQueue(),sp));级别由配置控制publicclassDatabaseLoggerConfiguration{publicLogLevelLogLevel{get;set;}LogLevel.Information;}publicboolIsEnabled(LogLevellogLevel)_getCurrentConfig().LogLevellogLevel;测试验证了语义[Fact]publicvoidDatabaseLogger_IsEnabled_RespectsLevel(){varconfignewDatabaseLoggerConfiguration{LogLevelLogLevel.Warning};...logger.IsEnabled(LogLevel.Critical).Should().BeTrue();logger.IsEnabled(LogLevel.Error).Should().BeTrue();logger.IsEnabled(LogLevel.Warning).Should().BeTrue();logger.IsEnabled(LogLevel.Information).Should().BeFalse();logger.IsEnabled(LogLevel.Debug).Should().BeFalse();logger.IsEnabled(LogLevel.Trace).Should().BeFalse();}IsEnabled判断在Log的第一行被过滤掉的日志根本不会进队列也就没有后续开销。七、和操作日志的区别框架里有两套日志职责不同不要混数据库日志本篇操作日志记录内容程序异常、Warning/Error、框架内部日志谁在什么时候做了什么业务操作写入方式有界队列 后台批量异步业务操作成功后记录典型表SysLog错误日志页SysOperationLog操作日志页触发方式ILogger自动[OperationLog]特性 /OperationLogService.AddLog丢数据可接受度极端情况下可丢有DroppedCount监控审计要求下不应丢像角色分配菜单这种审计敏感操作用的是操作日志[AdminButton(alloc_menus)][OperationLog(修改角色菜单权限)]privateasyncTaskOnSaveMenu()结论异步有界队列适合程序日志不适合审计日志。审计要同步落库。八、小结问题答案为什么要有界队列数据库故障时不能把应用内存撑爆满了丢谁丢最旧的DropOldest并统计DroppedCount供监控为什么还要本地重试缓冲写库失败时日志从队列转移到重试缓冲同样必须有界怎么避免雪崩重试指数退避 2s → 120s成功立即复位怎么保证性能攒批 100 条或 5 秒刷一次批量写入怎么定位问题TraceId / RequestPath / UserId / TenantCode 四个上下文日志会不会反过来搞挂应用上下文补充包 try/catch、字段截断、写入不阻塞调用方有界不只是技术细节它是一条设计承诺日志系统在任何情况下都不能成为压垮应用的最后一根稻草。如果你正在用 .NET 10 Blazor 做后台日志落库是很常见的需求。EasyAdminBlazor 的DatabaseLogger可以直接用也可以只借鉴它的有界队列 批量 退避这套结构。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor

相关新闻

32140电池化成工艺详解:SEI膜形成机理与大圆柱电池化成控制技术

32140电池化成工艺详解:SEI膜形成机理与大圆柱电池化成控制技术

随着储能电池和动力电池市场持续发展,大圆柱电池逐渐成为行业关注的重要技术路线。32140作为典型的大圆柱电池型号之一,其化成工艺对于循环寿命、一致性以及安全性能具有重要影响。本文从工程角度分析32140电池化成原理及关键控制技术。一、什么是32140电…

2026/10/27 11:26:35 阅读更多 →
【AI大模型】显存占用过高:推理显存优化技巧全总结

【AI大模型】显存占用过高:推理显存优化技巧全总结

【AI大模型】显存占用过高:推理显存优化技巧全总结 核心结论:大模型推理显存高,占用主要来自三部分:模型权重、KV Cache、激活与临时张量。要降低显存,最有效的三板斧是量化压缩权重、控制KV Cache规模、精简上下文与输出长度;再辅以分片加载、梯度释放、内存碎片治理等…

2026/10/26 14:38:40 阅读更多 →
《Agentic Design Patterns》第 6 章导读:规划(Planning)

《Agentic Design Patterns》第 6 章导读:规划(Planning)

《Agentic Design Patterns》第 6 章导读:规划(Planning)本文是对开源书籍《Agentic Design Patterns》第 6 章的解读与导读,内容忠实呈现原文,并附个人思考。 原书在线阅读:https://adp.xindoo.xyz/ &…

2026/10/25 12:40:22 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/6 15:21:35 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/7 12:08:40 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/6 22:01:11 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/6 22:01:11 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/7 12:08:40 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/6 22:01:09 阅读更多 →

日新闻

周新闻

月新闻