消失的检查记录:一个 C# HIS 项目的异步日志陷阱

在这里插入图片描述

在这里插入图片描述
几周前,我们在医院信息系统(HIS)的一次例行日志回查中发现了一个奇怪的现象:几名患者的检查记录明明显示为“已完成”,但在数据库的 CheckEventLog 表中,相关日志却根本没有出现。

这不是报错、也没有异常信息,更不是业务流程中断。唯一能说明问题的,就是这条日志——彻底失踪了

事情发生在一个很常规的接口里:

POST /api/patient/CheckCompleted

这是检查完成后由前台工作站调用的接口,主要负责两件事:

  1. 更新患者状态(同步执行)
  2. 异步记录一条“检查完成”的操作日志(写入 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 早就死透了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十步杀一人_千里不留行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值