c# 高并发下的缓存预热和数据同步方案

发布时间 - 2026-01-12 00:00:00    点击率:
缓存预热需在应用启动后、首个请求前同步完成,避免缓存击穿;分布式环境必须使用带版本号的 IDistributedCache 并禁用滑动过期;数据更新须执行“删-更-延时再删”三步法,并用 SemaphoreSlim 控制并发重建。

缓存预热必须在应用启动后、首个请求前完成

高并发下,如果等第一个请求触发缓存加载,必然导致大量线程争抢初始化(即“缓存击穿”),尤其当 GetOrAdd 用的是非线程安全的工厂函数时,可能重复执行耗时操作。正确做法是:在 Program.csStartup.ConfigureServices 中显式调用预热逻辑,并确保其同步阻塞到完成。

  • 使用 Task.Run(() => PreheatCache()).Wait() 强制同步等待(注意不要在 ASP.NET Core 的同步上下文里用 .Result,易死锁)
  • 预热函数内部应避免依赖 IHttpContextAccessor 等请求作用域服务;改用 IServiceScopeFactory 创建独立 scope
  • 若预热数据量大,可分批 + await Task.Delay(1) 防止单次占用主线程太久,但整体仍需在 WebApplication 构建完成前结束

分布式环境必须用带版本号的缓存键 + 原子写入

单机 MemoryCache 在多实例部署下完全失效。必须切换为 IDistributedCache(如 Redis),且不能直接存原始对象——否则多个节点同时更新会覆盖彼此,造成脏数据。

  • 缓存键格式建议为:$"user:profile:v2:{userId}",其中 v2 是业务版本号,每次数据结构变更就升级,强制全量刷新
  • 写入时用 distributedCache.SetStringAsync(key, json, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) }),禁用滑动过期(SlidingExpiration),防止“越用越旧”
  • 关键数据更新后,必须同步调用 distributedCache.RemoveAsync(key),而不是等过期;删除失败要记录告警,不可静默忽略

数据库与缓存一致性靠“先删缓存,再更DB”+ 延迟双删兜底

“先更DB,再删缓存”在并发更新时有概率导致缓存残留旧值(DB 更新成功,但删缓存失败或被覆盖)。生产环境必须用“删除-更新-延迟再删”三步法。

  • 第一步:调用 distributedCache.RemoveAsync(key)
  • 第二步:执行 EF Core 的 SaveChangesAsync()
  • 第三步:启动后台任务,await Task.Delay(TimeSpan.FromSeconds(500)) 后再次 RemoveAsync(key) —— 覆盖因主从延迟、重试机制导致的缓存回写
  • 所有删缓存操作必须包裹 try/catch,失败时写入本地队列(如 ConcurrentQueue),由后台服务重试,不能丢弃

并发读场景下避免 Cache Stampede,用 SemaphoreSlim 限流重建

即使做了预热,缓存过期瞬间仍可能有上百请求同时发现缓存为空,全部涌入 DB。不能靠 GetOrCreateAsync 默认行为扛住——它的 factory 函数不是原子的。

  • 为每个缓存键维护一个 ConcurrentDictionary,键为 cache key,值为独占信号量
  • 读取时:先 cache.TryGetValue(key, out var value);若为空,则 semaphores.GetOrAdd(key, _ => new SemaphoreSlim(1,1)).WaitAsync()
  • 获得信号量后,再次检查缓存(double-check),未命中才重建;完成后 Release() 并移除该 semaphore(避免内存泄漏)
  • 注意:不要用 lock,它跨进程无效;也不要复用同一个 SemaphoreSlim 实例,会导致不同 key 互相阻塞
private static readonly ConcurrentDictionary _semaphores = new();
public async Task GetProfileAsync(int userId)
{
    var key = $"user:profile:v2:{userId}";
    if (_cache.TryGetValue(key, out UserProfile profile))
        return profile;

    var semaphore = _semaphores.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
    await semaphore.WaitAsync();
    try
    {
        // Double-check after acquiring semaphore
        if (_cache.TryGetValue(key, out profile))
            return profile;

        profile = await _db.Users.FirstAsync(u => u.Id == userId);
        await _cache.SetStringAsync(key, JsonSerializer.Serialize(profile), 
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });
        return profile;
    }
    finally
    {
        semaphore.Release();
        _semaphores.TryRemove(key, out _);
    }
}
缓存键设计、删除时机、并发重建这三点一旦漏掉任意一个,高并发下的数据不一致就会变成偶发性线上事故——而这类问题往往在压测时不出,上线后半夜爆发。


# redis  # js  # json  # app  # access  # ai  # c#  # 作用域  # .net  # red  # 分布式  # try  # catch  # double  # 数据结构  # 线程  # 主线程  # var  # 并发  # 对象  # 数据库  # 信号量  # 死锁  # 首个  # 重试  # 就会  # 第一个  # 多个  # 不出  # 后半夜 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 如何在橙子建站上传落地页?操作指南详解  Laravel如何实现文件上传和存储?(本地与S3配置)  Laravel怎么使用Blade模板引擎_Laravel模板继承与Component组件复用【手册】  html5如何实现懒加载图片_ intersectionobserver api用法【教程】  Laravel怎么实现观察者模式Observer_Laravel模型事件监听与解耦开发【指南】  Laravel storage目录权限问题_Laravel文件写入权限设置  个人摄影网站制作流程,摄影爱好者都去什么网站?  Laravel怎么自定义错误页面_Laravel修改404和500页面模板  Python文件流缓冲机制_IO性能解析【教程】  iOS验证手机号的正则表达式  简单实现Android文件上传  Laravel如何构建RESTful API_Laravel标准化API接口开发指南  公司门户网站制作公司有哪些,怎样使用wordpress制作一个企业网站?  Laravel如何保护应用免受CSRF攻击?(原理和示例)  edge浏览器无法安装扩展 edge浏览器插件安装失败【解决方法】  Laravel distinct去重查询_Laravel Eloquent去重方法  微信小程序 wx.uploadFile无法上传解决办法  Python文件异常处理策略_健壮性说明【指导】  *服务器网站为何频现安全漏洞?  Win11怎么查看显卡温度 Win11任务管理器查看GPU温度【技巧】  Laravel如何发送邮件和通知_Laravel邮件与通知系统发送步骤  Laravel怎么配置.env环境变量_Laravel生产环境敏感数据保护与读取【方法】  网站制作软件免费下载安装,有哪些免费下载的软件网站?  C++时间戳转换成日期时间的步骤和示例代码  Laravel如何使用Livewire构建动态组件?(入门代码)  悟空识字如何进行跟读录音_悟空识字开启麦克风权限与录音  Laravel如何安装Breeze扩展包_Laravel用户注册登录功能快速实现【流程】  laravel怎么通过契约(Contracts)编程_laravel契约(Contracts)编程方法  如何为不同团队 ID 动态生成多个“认领值班”按钮  哪家制作企业网站好,开办像阿里巴巴那样的网络公司和网站要怎么做?  图册素材网站设计制作软件,图册的导出方式有几种?  Laravel Artisan命令怎么自定义_创建自己的Laravel命令行工具完全指南  Python函数文档自动校验_规范解析【教程】  php嵌入式断网后怎么恢复_php检测网络重连并恢复硬件控制【操作】  如何用y主机助手快速搭建网站?  javascript如何操作浏览器历史记录_怎样实现无刷新导航  Microsoft Edge如何解决网页加载问题 Edge浏览器加载问题修复  C语言设计一个闪闪的圣诞树  Gemini怎么用新功能实时问答_Gemini实时问答使用【步骤】  如何使用 jQuery 正确渲染 Instagram 风格的标签列表  Python3.6正式版新特性预览  如何在万网自助建站平台快速创建网站?  详解ASP.NET 生成二维码实例(采用ThoughtWorks.QRCode和QrCode.Net两种方式)  BootStrap整体框架之基础布局组件  Laravel如何使用Sanctum进行API认证?(SPA实战)  UC浏览器如何设置启动页 UC浏览器启动页设置方法  Laravel Livewire是什么_使用Laravel Livewire构建动态前端界面  深圳网站制作的公司有哪些,dido官方网站?  如何快速上传自定义模板至建站之星?  Laravel如何记录日志_Laravel Logging系统配置与自定义日志通道