You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core API:缓存可分页、排序、搜索数据库结果的最佳实践

当然可行!这种带分页、排序、搜索条件的查询结果缓存,在高并发场景下能大幅减轻数据库压力,我在.NET Core Web API项目里实操过很多次,下面分享一些实战中的最佳实践:

一、核心:设计唯一且精准的缓存键

不同的分页参数、排序规则、搜索关键词对应完全不同的查询结果,所以缓存键必须包含所有影响结果的参数,否则会出现缓存混乱的问题。

举个例子,针对帖子查询,我通常会这么构建缓存键:

var cacheKey = $"Posts_Page{page}_Size{pageSize}_Search{searchKeyword ?? "Empty"}_SortBy{sortField}_SortDir{sortDescending ? "Desc" : "Asc"}";

这里要注意几个细节:

  • 加业务前缀(比如Posts_):避免和其他模块的缓存键冲突
  • 处理空值:比如搜索关键词为空时用Empty代替,防止键里出现无效字符或空片段
  • 包含所有可变参数:页码、每页条数、搜索词、排序字段、排序方向一个都不能少
二、缓存策略:平衡性能与数据新鲜度

缓存不是越久越好,要根据业务场景调整过期规则:

  • 滑动过期+绝对过期结合:比如设置滑动过期10分钟(如果用户频繁访问,缓存会持续保留),同时设置绝对过期1小时(防止缓存永久驻留导致数据太旧),这是最常用的组合
  • 主动失效优先:当帖子新增、修改、删除时,一定要主动清除相关缓存,避免用户看到过期数据。如果用MemoryCache,因为它不支持标签化缓存,我会维护一个缓存键集合,把所有帖子相关的缓存键都存起来,更新时遍历删除;或者如果业务允许短时间不一致,靠过期时间兜底也可以
三、MemoryCache使用避坑指南
  • 设置内存大小限制:MemoryCache默认会根据系统内存压力自动回收,但最好手动设置大小限制(比如100MB),防止缓存占用过多内存导致服务崩溃:
    builder.Services.AddMemoryCache(options => 
    { 
        options.SizeLimit = 1024 * 1024 * 100; // 100MB
    });
    
    同时记得给每个缓存条目设置Size(比如每个分页结果设为1,或者根据实际数据大小估算),这样内存限制才会生效
  • 避免缓存击穿:用GetOrCreateAsync方法代替先查缓存再手动创建,这个方法内部会处理并发,确保同一个缓存键只有一个请求去查询数据库,其他请求等待缓存结果,避免瞬间打垮数据库
四、实战代码示例

下面是一个完整的服务层方法示例,包含缓存逻辑:

private readonly IMemoryCache _memoryCache;
private readonly AppDbContext _dbContext;

public PostService(IMemoryCache memoryCache, AppDbContext dbContext)
{
    _memoryCache = memoryCache;
    _dbContext = dbContext;
}

public async Task<PagedResult<Post>> GetPostsAsync(int page, int pageSize, string searchKeyword, string sortField = "Date", bool sortDescending = true)
{
    // 构建唯一缓存键
    var cacheKey = $"Posts_Page{page}_Size{pageSize}_Search{searchKeyword ?? "Empty"}_SortBy{sortField}_SortDir{sortDescending ? "Desc" : "Asc"}";

    // 从缓存获取或创建
    var result = await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
    {
        // 设置缓存过期策略
        entry.SlidingExpiration = TimeSpan.FromMinutes(10);
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
        entry.Size = 1; // 假设每个分页结果占1单位大小

        // 数据库查询逻辑
        var query = _dbContext.Posts.AsQueryable();

        // 搜索过滤
        if (!string.IsNullOrEmpty(searchKeyword))
        {
            query = query.Where(p => p.Title.Contains(searchKeyword) || p.Content.Contains(searchKeyword));
        }

        // 动态排序
        query = sortDescending 
            ? query.OrderByDescending(p => EF.Property<object>(p, sortField)) 
            : query.OrderBy(p => EF.Property<object>(p, sortField));

        // 分页处理
        var totalCount = await query.CountAsync();
        var posts = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();

        return new PagedResult<Post>
        {
            Items = posts,
            TotalCount = totalCount,
            Page = page,
            PageSize = pageSize
        };
    });

    return result;
}

// 分页结果DTO
public class PagedResult<T>
{
    public List<T> Items { get; set; } = new();
    public int TotalCount { get; set; }
    public int Page { get; set; }
    public int PageSize { get; set; }
}
五、特殊场景的权衡

如果遇到以下情况,要谨慎使用这种缓存:

  • 数据实时性要求极高:比如秒杀订单、实时监控数据,缓存会导致数据延迟,不如直接查数据库
  • 查询组合爆炸:如果用户的搜索词、排序规则组合太多,会导致缓存条目急剧增加,内存占用过高。这种情况下可以考虑只缓存热门查询(比如统计访问量高的参数组合),或者改用分布式缓存(比如Redis)来扩容

总的来说,只要处理好缓存键的唯一性、过期策略和失效机制,这种缓存方式能让你的Web API性能提升一大截!

内容的提问来源于stack exchange,提问作者brad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:56:52