基于C#缓存处理大数据:新旧缓存无缝切换方案问询
实现缓存平滑更新:海量数据加载时保留旧缓存方案
这个场景我太熟悉了——处理几十万级数据的缓存更新,最怕的就是加载过程中前端卡壳或者拿到半截数据。核心要做到的就是「后台异步刷新,前端无缝衔接」,下面给你一套落地的方案,分传统ASP.NET和ASP.NET Core两种情况:
核心思路
- 用异步任务在后台加载新数据,完全不阻塞主线程和用户请求
- 缓存存储的是完整的数据实例,新数据加载完成后原子性替换旧实例,保证前端永远拿到完整的有效数据
- 加并发控制,避免同一时间多个线程重复触发数据库查询(比如半小时刷新点同时有N个请求进来,不能都去查库)
- 定时任务用可靠的方式实现,避免因线程池问题导致刷新延迟
传统ASP.NET(基于HttpContext.Current.Cache)实现
我们可以封装一个静态缓存管理类,用线程安全的方式处理缓存更新:
public static class BookCacheManager { // 存储当前有效的缓存数据,volatile保证多线程可见性 private static volatile List<Book> _currentBookData; // 锁对象,控制并发刷新 private static readonly object _lockObj = new object(); // 定时刷新计时器 private static Timer _refreshTimer; static BookCacheManager() { // 程序启动时先加载一次初始缓存 _currentBookData = LoadBookDataFromDb(); // 启动定时任务:立即执行一次,之后每半小时刷新 _refreshTimer = new Timer(RefreshCacheAsync, null, TimeSpan.Zero, TimeSpan.FromMinutes(30)); } // 对外暴露的获取缓存方法,无阻塞,直接返回当前有效数据 public static List<Book> GetBookData() { return _currentBookData; } private static async void RefreshCacheAsync(object state) { // 双重检查锁定:避免多个线程同时触发刷新 if (Monitor.TryEnter(_lockObj)) { try { // 异步加载新数据,不阻塞线程池 var newBookData = await LoadBookDataFromDbAsync(); // 原子性替换旧缓存:这一步是瞬间完成的,前端不会拿到不完整数据 _currentBookData = newBookData; Debug.WriteLine("书籍缓存已成功更新"); } catch (Exception ex) { // 关键:加载失败时保留旧缓存,绝对不能清空或设为null Debug.WriteLine($"缓存更新失败,保留旧数据:{ex.Message}"); } finally { Monitor.Exit(_lockObj); } } else { Debug.WriteLine("已有线程正在更新缓存,本次刷新跳过"); } } // 同步加载数据(初始化用) private static List<Book> LoadBookDataFromDb() { // 替换成你的实际数据库查询逻辑 using (var conn = new SqlConnection("你的数据库连接字符串")) { conn.Open(); return conn.Query<Book>("SELECT * FROM Tab1").ToList(); } } // 异步加载数据(推荐用异步避免阻塞) private static async Task<List<Book>> LoadBookDataFromDbAsync() { using (var conn = new SqlConnection("你的数据库连接字符串")) { await conn.OpenAsync(); return (await conn.QueryAsync<Book>("SELECT * FROM Tab1")).ToList(); } } }
前端使用方式:直接调用BookCacheManager.GetBookData()即可,不管后台有没有在刷新,拿到的都是完整的旧数据,直到新数据加载完成后自动切换。
ASP.NET Core 版本(更推荐,替代HttpContext.Cache)
在Core里,我们用IMemoryCache结合BackgroundService实现定时刷新,框架托管的服务可靠性更高:
1. 创建后台缓存刷新服务
public class BookCacheRefreshService : BackgroundService { private readonly IMemoryCache _cache; private readonly ILogger<BookCacheRefreshService> _logger; private const string CacheKey = "BookData"; public BookCacheRefreshService(IMemoryCache cache, ILogger<BookCacheRefreshService> logger) { _cache = cache; _logger = logger; } // 初始化缓存(程序启动时执行) public async Task InitializeCacheAsync() { var initialData = await LoadBookDataFromDbAsync(); _cache.Set(CacheKey, initialData); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 先加载初始缓存 await InitializeCacheAsync(); // 定时刷新循环 while (!stoppingToken.IsCancellationRequested) { try { // 异步加载新数据 var newBookData = await LoadBookDataFromDbAsync(); // 原子性替换缓存,Core的IMemoryCache.Set是线程安全的 _cache.Set(CacheKey, newBookData); _logger.LogInformation("书籍缓存已成功更新"); } catch (Exception ex) { _logger.LogError(ex, "缓存更新失败,保留旧数据"); } // 等待半小时,同时响应程序终止信号 await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken); } } private async Task<List<Book>> LoadBookDataFromDbAsync() { // 实际的数据库异步查询逻辑 using (var conn = new SqlConnection("你的数据库连接字符串")) { await conn.OpenAsync(); return (await conn.QueryAsync<Book>("SELECT * FROM Tab1")).ToList(); } } }
2. 注册服务(Program.cs)
builder.Services.AddHostedService<BookCacheRefreshService>(); builder.Services.AddMemoryCache();
3. 控制器中使用缓存
public class BookController : Controller { private readonly IMemoryCache _cache; private const string CacheKey = "BookData"; public BookController(IMemoryCache cache) { _cache = cache; } public IActionResult Index() { // 直接获取缓存,永远拿到完整有效数据 var bookData = _cache.Get<List<Book>>(CacheKey); return View(bookData); } }
关键细节提醒
- 原子性替换:不管是用私有字段还是
IMemoryCache.Set,替换缓存的操作都是瞬间完成的,前端绝对不会看到半加载的状态 - 异常容错:数据库查询失败时,一定要保留旧缓存,不能让用户看到空数据或错误页面
- 异步优先:海量数据加载必须用异步方法,避免阻塞线程池,保证系统整体响应性
- 并发控制:传统ASP.NET用双重检查锁定避免重复查询,Core的后台服务是单线程循环,天然不会重复触发
内容的提问来源于stack exchange,提问作者Bilal Ahmed
相关产品推荐
相关产品推荐

