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/10 2:55:56 阅读更多 →
【AI大模型】显存占用过高:推理显存优化技巧全总结

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

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

2026/10/10 22:22:58 阅读更多 →
《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/10 1:19:02 阅读更多 →

最新新闻

2026年10款硬核降AI率网站推荐:AIGC检测轻松绿灯过关

2026年10款硬核降AI率网站推荐:AIGC检测轻松绿灯过关

随着知网、维普、万方等主流学术平台对AIGC检测标准持续升级,论文合规性成为研究者必须面对的挑战。选择一款高效的降AI工具,是规避风险、提升通过率的关键。本文将实测对比10款主流工具,为读者提供精准的解决方案参考。为什么需要降 AI 率工…

2026/10/11 5:03:30 阅读更多 →
opencode 工具层与服务面实战:从能跑到好用的工程化配置指南

opencode 工具层与服务面实战:从能跑到好用的工程化配置指南

1. 从"能跑"到"好用":opencode 工具层到底解决了什么问题很多人第一次接触 opencode,注意力都放在"它能不能连上模型、能不能出结果"上。等真正把基础链路跑通之后才会发现,决定一个 AI 编程助手能不能长期留在…

2026/10/11 5:03:30 阅读更多 →
PCL2启动器完全指南:从选型到模组加载与性能调优

PCL2启动器完全指南:从选型到模组加载与性能调优

1. 为什么我最终选择了PCL2:一个启动器的选型逻辑1.1 从原版启动器到第三方启动器的认知转变很多刚接触Minecraft的玩家,最开始用的都是官方原版启动器。说实话,原版启动器能用,但用起来总觉得差点意思——正版验证要联网、多版本…

2026/10/11 5:03:30 阅读更多 →
如何高效搭建AI日报:信息源、筛选与结构化处理实战

如何高效搭建AI日报:信息源、筛选与结构化处理实战

1. 一份AI日报的诞生逻辑:从信息洪流到决策参考每天早上七点半,我的浏览器会同时打开十几个标签页。这不是什么仪式感,纯粹是被逼出来的——过去三年我一直在一家做智能硬件的公司负责技术情报和竞品追踪,老板要求每天九点前把过去…

2026/10/11 5:03:30 阅读更多 →
五、AI 量化交易完整学习路线(从零到实战)

五、AI 量化交易完整学习路线(从零到实战)

这套课程大纲,已经不是“普通量化课”了,本质上是在培养: “AI 多智能体 量化交易 自动化交易系统”的复合型能力。 它覆盖了: 传统量化AI AgentMCP 工具协议多智能体协作自动化交易AI 投研风控强化学习实盘闭环 如果按照正确…

2026/10/11 5:03:30 阅读更多 →
Buzz 离线语音转文字完整指南:本地 Whisper 转录与字幕生成

Buzz 离线语音转文字完整指南:本地 Whisper 转录与字幕生成

Buzz 离线语音转文字完整指南:本地 Whisper 转录与字幕生成 【免费下载链接】buzz Buzz transcribes and translates audio offline on your personal computer. Powered by OpenAIs Whisper. 项目地址: https://gitcode.com/GitHub_Trending/buz/buzz 给两小…

2026/10/11 5:02:30 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →