几周前,我们在医院信息系统(HIS)的一次例行日志回查中发现了一个奇怪的现象:几名患者的检查记录明明显示为“已完成”,但在数据库的 CheckEventLog
表中,相关日志却根本没有出现。
这不是报错、也没有异常信息,更不是业务流程中断。唯一能说明问题的,就是这条日志——彻底失踪了。
事情发生在一个很常规的接口里:
POST /api/patient/CheckCompleted
这是检查完成后由前台工作站调用的接口,主要负责两件事:
- 更新患者状态(同步执行)
- 异步记录一条“检查完成”的操作日志(写入
CheckEventLog
)
当我们调取日志系统中的执行记录时,发现同步部分每次都运行良好,而异步的日志记录行为却非常不稳定。下面是一次典型的失败案例:
Check completed: PatientID=200871, Type=CT Scan
Calling SaveCheckEventLogAsync
Starting Task.Run for SaveCheckEventLogAsync - PatientID=200871
Task.Run executing SaveCheckEventLogAsync - PatientID=200871
SaveCheckEventLogAsync called - PatientID=200871, Type=CT Scan, Time=2025-08-06T17:07:18.080Z
【之后没有任何日志,写入中断】
对比成功的记录:
[Sync] SaveCheckEventLogSync called - PatientID=200871
[Sync] Created CheckEventLog entity
[Sync] Added to EF context
[Sync] SaveContextChanges completed
[Sync] Logging complete
进一步验证数据库,你会发现只存在同步写入的那条记录,异步路径下的数据完全缺失。
我们起初以为是线程调度问题,或者网络延迟
但很快发现,不管怎么改 Task.Run 的优先级,日志依然偶发丢失。于是我们开始深挖代码:
问题代码如下(C#):
private void SaveCheckEventLogAsync(Patient patient, string checkType, DateTime checkTime, string doctor)
{
_ = Task.Run(async () =>
{
try
{
using (var logManager = new CheckEventManager(_logger, _connectionString))
{
await logManager.SaveCheckEventLogInternalAsync(
patient?.PatientID,
checkType,
checkTime,
doctor
);
}
}
catch (Exception ex)
{
_logger?.Error($"Async check log failed: {ex.Message}", ex);
}
});
}
这是典型的 C# 异步“火-and-忘”模式,我们以为只要包在 Task.Run()
里就能后台安全执行。
但实际情况是:接口线程返回后,ASP.NET 请求生命周期结束,主线程释放了依赖注入的上下文与资源对象,而 Task 还没执行完,里面调用的服务直接失效。
换句话说,你还在往数据库写,EF 的上下文已经被清除了。
更糟的是,由于我们用了 Task.Run()
包裹,没有 await
,异常也不会冒泡到主线程,自然也没有日志能记录失败。
问题一旦定位,解决思路就清晰了:
我们要让异步任务彻底摆脱主线程资源依赖,自己活,自己吃饭。
于是我们做了以下改动:
✅ 第一步:在主线程中预先构造日志对象
var logEntry = new CheckEventLog
{
PatientID = patient.PatientID,
CheckType = checkType,
CheckTime = checkTime,
Doctor = doctor,
CreatedDate = DateTime.Now
};
不在异步中构造对象,避免日志对象在上下文释放后还依赖某些字段。
✅ 第二步:在 Task.Run 内部创建全新 Manager,使用独立 DbContext
_ = Task.Run(async () =>
{
try
{
using (var manager = new CheckEventManager(_logger ?? Log.Logger, _connectionString))
{
manager.CheckEventLogs.Add(logEntry);
await manager.SaveChangesAsync();
}
}
catch (Exception ex)
{
_logger?.Error($"Async check log failed: {ex.Message}", ex);
}
});
这里的 _connectionString
是从配置中读取,不依赖注入。_logger
也做了 null 兜底,确保任务线程能独立完成。
✅ 第三步:保持异步逻辑极简
我们只做 Add + SaveChanges
两件事,不加判断,不调用外部服务,确保任务体内无额外依赖。
修复后的效果非常明显:
我们在预发布环境进行了:
- 高并发接口调用测试
- 长时间高频写入测试
- 断点注入 / 网络阻塞测试
所有记录无一丢失,且日志写入时间更短,错误更容易追踪。
上线至今,已处理 20 万+ 条异步日志写入,从未再发生检查记录丢失问题。
这个 bug 看似是小问题,但如果放任不管,它迟早会在关键节点出错,比如法务审计、医保结算时发现“患者明明做了检查,但系统没记录”。
我们很庆幸发现得早,也庆幸最终选择了解耦结构,而不是让异步任务继续“搭顺风车”。
如果你在 C# 项目里也有类似需求:
后台异步写日志 / 上传审计记录 / 扫码结果入库……
记住一件事:ASP.NET 请求一旦结束,主线程的所有依赖都可能被清理干净,任务再晚一点执行,你的 DbContext 早就死透了。