znlgis 博客

GIS开发与技术分享

第十章:缓存管理

10.1 缓存概述与策略

缓存是提升应用性能的关键技术之一。通过将频繁访问的数据存储在高速存储介质中(如内存),可以显著减少数据库查询次数,降低响应延迟,提高系统吞吐量。Furion 框架完全兼容 ASP.NET Core 的缓存体系,并提供了便捷的集成方式。

10.1.1 常见缓存策略

策略 说明 适用场景
Cache-Aside(旁路缓存) 先查缓存,未命中则查数据库并写入缓存 通用场景,最常用
Read-Through(读穿透) 缓存层自动从数据源加载缺失数据 简化应用层逻辑
Write-Through(写穿透) 写操作同时更新缓存和数据源 数据一致性要求高
Write-Behind(异步写回) 写操作先更新缓存,异步回写数据源 高写入吞吐量
Refresh-Ahead(预刷新) 在缓存过期前提前刷新 热点数据预加载

10.1.2 缓存类型对比

类型 存储位置 速度 共享性 持久化 适用场景
内存缓存 应用进程内存 最快 不共享 不持久化 单机应用
分布式缓存 外部存储(Redis等) 可共享 可持久化 分布式/微服务
响应缓存 HTTP层 最快 基于HTTP 不持久化 静态内容/API响应
多级缓存 组合多层 综合最优 部分共享 部分持久化 高性能要求

10.2 内存缓存(IMemoryCache)

10.2.1 注册内存缓存

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 注册内存缓存
builder.Services.AddMemoryCache();

10.2.2 基本用法

using Microsoft.Extensions.Caching.Memory;
using Furion.DynamicApiController;

public class ProductService : IDynamicApiController
{
    private readonly IMemoryCache _cache;
    private readonly IRepository<Product> _productRepo;
    private readonly ILogger<ProductService> _logger;

    public ProductService(
        IMemoryCache cache,
        IRepository<Product> productRepo,
        ILogger<ProductService> logger)
    {
        _cache = cache;
        _productRepo = productRepo;
        _logger = logger;
    }

    /// <summary>
    /// 获取商品详情(带缓存)
    /// </summary>
    public async Task<Product> GetProduct(int id)
    {
        var cacheKey = $"product:{id}";

        // 尝试从缓存获取
        if (_cache.TryGetValue(cacheKey, out Product product))
        {
            _logger.LogDebug("命中缓存:{CacheKey}", cacheKey);
            return product;
        }

        // 未命中,从数据库查询
        _logger.LogDebug("未命中缓存,从数据库查询:{CacheKey}", cacheKey);
        product = await _productRepo.FindAsync(id);

        if (product != null)
        {
            // 写入缓存
            var cacheOptions = new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(30))    // 绝对过期时间
                .SetSlidingExpiration(TimeSpan.FromMinutes(10))     // 滑动过期时间
                .SetSize(1)                                         // 缓存大小
                .SetPriority(CacheItemPriority.Normal);             // 缓存优先级

            _cache.Set(cacheKey, product, cacheOptions);
        }

        return product;
    }

    /// <summary>
    /// 使用 GetOrCreate 模式(推荐)
    /// </summary>
    public async Task<List<Product>> GetHotProducts()
    {
        return await _cache.GetOrCreateAsync("hot_products", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            entry.SlidingExpiration = TimeSpan.FromMinutes(2);

            _logger.LogInformation("加载热门商品数据到缓存");
            return await _productRepo.Entities
                .OrderByDescending(p => p.SalesCount)
                .Take(20)
                .ToListAsync();
        });
    }

    /// <summary>
    /// 更新商品(同时更新缓存)
    /// </summary>
    public async Task UpdateProduct(int id, ProductUpdateInput input)
    {
        var product = await _productRepo.FindAsync(id);
        if (product == null) throw Oops.Oh("商品不存在");

        product.Name = input.Name;
        product.Price = input.Price;

        await _productRepo.UpdateAsync(product);
        await _productRepo.SaveNowAsync();

        // 更新缓存
        var cacheKey = $"product:{id}";
        _cache.Set(cacheKey, product, TimeSpan.FromMinutes(30));

        // 同时清除热门商品列表缓存
        _cache.Remove("hot_products");

        _logger.LogInformation("商品缓存已更新:{ProductId}", id);
    }

    /// <summary>
    /// 删除商品(清除缓存)
    /// </summary>
    public async Task DeleteProduct(int id)
    {
        await _productRepo.DeleteAsync(id);
        await _productRepo.SaveNowAsync();

        // 清除缓存
        _cache.Remove($"product:{id}");
        _cache.Remove("hot_products");
    }
}

10.2.3 缓存过期策略

/// <summary>
/// 缓存过期策略示例
/// </summary>
public class CacheExpirationDemo : IDynamicApiController
{
    private readonly IMemoryCache _cache;

    public CacheExpirationDemo(IMemoryCache cache)
    {
        _cache = cache;
    }

    public void DemoExpiration()
    {
        // 1. 绝对过期 - 到达指定时间后过期
        _cache.Set("key1", "value1", new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(30)));

        // 2. 滑动过期 - 如果在指定时间内没有访问,则过期
        _cache.Set("key2", "value2", new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(10)));

        // 3. 组合过期 - 滑动过期 + 绝对过期上限
        _cache.Set("key3", "value3", new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(10))
            .SetAbsoluteExpiration(TimeSpan.FromHours(2)));

        // 4. 过期回调
        var options = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                // 缓存被驱逐时的回调
                Console.WriteLine($"缓存 {key} 被驱逐,原因:{reason}");
            });
        _cache.Set("key4", "value4", options);

        // 5. 基于 Token 的过期(手动控制)
        var cts = new CancellationTokenSource();
        _cache.Set("key5", "value5", new MemoryCacheEntryOptions()
            .AddExpirationToken(new CancellationChangeToken(cts.Token)));
        // 手动触发过期
        // cts.Cancel();
    }
}

10.3 分布式缓存(IDistributedCache)

10.3.1 注册分布式缓存

// Program.cs

// 方式1:内存分布式缓存(开发环境使用)
builder.Services.AddDistributedMemoryCache();

// 方式2:SQL Server 分布式缓存
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("CacheConnection");
    options.SchemaName = "dbo";
    options.TableName = "DistributedCache";
});

10.3.2 使用分布式缓存

using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

public class DistributedCacheService : IDynamicApiController
{
    private readonly IDistributedCache _cache;
    private readonly IRepository<User> _userRepo;

    public DistributedCacheService(IDistributedCache cache, IRepository<User> userRepo)
    {
        _cache = cache;
        _userRepo = userRepo;
    }

    /// <summary>
    /// 获取用户信息(分布式缓存)
    /// </summary>
    public async Task<User> GetUser(int id)
    {
        var cacheKey = $"user:{id}";

        // 从缓存获取
        var cachedData = await _cache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cachedData))
        {
            return JsonSerializer.Deserialize<User>(cachedData);
        }

        // 从数据库查询
        var user = await _userRepo.FindAsync(id);
        if (user != null)
        {
            // 写入缓存
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
                SlidingExpiration = TimeSpan.FromMinutes(10)
            };

            var json = JsonSerializer.Serialize(user);
            await _cache.SetStringAsync(cacheKey, json, options);
        }

        return user;
    }

    /// <summary>
    /// 删除缓存
    /// </summary>
    public async Task RemoveCache(string key)
    {
        await _cache.RemoveAsync(key);
    }

    /// <summary>
    /// 刷新缓存(重置滑动过期时间)
    /// </summary>
    public async Task RefreshCache(string key)
    {
        await _cache.RefreshAsync(key);
    }
}

10.3.3 分布式缓存扩展方法

/// <summary>
/// 分布式缓存扩展方法
/// </summary>
public static class DistributedCacheExtensions
{
    /// <summary>
    /// 获取或创建缓存(泛型版本)
    /// </summary>
    public static async Task<T> GetOrCreateAsync<T>(
        this IDistributedCache cache,
        string key,
        Func<Task<T>> factory,
        DistributedCacheEntryOptions options = null) where T : class
    {
        var cachedData = await cache.GetStringAsync(key);
        if (!string.IsNullOrEmpty(cachedData))
        {
            return JsonSerializer.Deserialize<T>(cachedData);
        }

        var data = await factory();
        if (data != null)
        {
            options ??= new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
            };

            var json = JsonSerializer.Serialize(data);
            await cache.SetStringAsync(key, json, options);
        }

        return data;
    }

    /// <summary>
    /// 设置对象到缓存
    /// </summary>
    public static async Task SetObjectAsync<T>(
        this IDistributedCache cache,
        string key,
        T value,
        DistributedCacheEntryOptions options = null)
    {
        options ??= new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
        };

        var json = JsonSerializer.Serialize(value);
        await cache.SetStringAsync(key, json, options);
    }

    /// <summary>
    /// 从缓存获取对象
    /// </summary>
    public static async Task<T> GetObjectAsync<T>(
        this IDistributedCache cache,
        string key) where T : class
    {
        var cachedData = await cache.GetStringAsync(key);
        return string.IsNullOrEmpty(cachedData) ? null : JsonSerializer.Deserialize<T>(cachedData);
    }
}

10.4 Redis 缓存集成

10.4.1 安装与配置

# 安装 Redis 缓存包
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package StackExchange.Redis
// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "FurionApp:";
});

appsettings.json 配置:

{
  "ConnectionStrings": {
    "Redis": "localhost:6379,password=your_password,defaultDatabase=0,ssl=false,abortConnect=false,connectTimeout=5000"
  }
}

10.4.2 Redis 连接管理

using StackExchange.Redis;

/// <summary>
/// Redis 连接管理(单例)
/// </summary>
public class RedisConnectionManager : IDisposable
{
    private readonly Lazy<ConnectionMultiplexer> _connection;

    public RedisConnectionManager(IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("Redis");
        _connection = new Lazy<ConnectionMultiplexer>(() =>
            ConnectionMultiplexer.Connect(connectionString));
    }

    public IDatabase GetDatabase(int db = -1) => _connection.Value.GetDatabase(db);
    public IServer GetServer(string host, int port) => _connection.Value.GetServer(host, port);
    public ISubscriber GetSubscriber() => _connection.Value.GetSubscriber();

    public void Dispose()
    {
        if (_connection.IsValueCreated)
        {
            _connection.Value.Dispose();
        }
    }
}

// 注册 Redis 连接管理
builder.Services.AddSingleton<RedisConnectionManager>();

10.4.3 Redis 缓存服务

using StackExchange.Redis;
using System.Text.Json;

/// <summary>
/// Redis 缓存服务
/// </summary>
public class RedisCacheService : IDynamicApiController
{
    private readonly IDatabase _redis;
    private readonly ILogger<RedisCacheService> _logger;

    public RedisCacheService(
        RedisConnectionManager connectionManager,
        ILogger<RedisCacheService> logger)
    {
        _redis = connectionManager.GetDatabase();
        _logger = logger;
    }

    /// <summary>
    /// 设置字符串缓存
    /// </summary>
    public async Task SetString(string key, string value, int expireMinutes = 30)
    {
        await _redis.StringSetAsync(key, value, TimeSpan.FromMinutes(expireMinutes));
    }

    /// <summary>
    /// 获取字符串缓存
    /// </summary>
    public async Task<string> GetString(string key)
    {
        return await _redis.StringGetAsync(key);
    }

    /// <summary>
    /// 设置对象缓存
    /// </summary>
    public async Task SetObject<T>(string key, T value, int expireMinutes = 30)
    {
        var json = JsonSerializer.Serialize(value);
        await _redis.StringSetAsync(key, json, TimeSpan.FromMinutes(expireMinutes));
    }

    /// <summary>
    /// 获取对象缓存
    /// </summary>
    public async Task<T> GetObject<T>(string key) where T : class
    {
        var value = await _redis.StringGetAsync(key);
        return value.HasValue ? JsonSerializer.Deserialize<T>(value) : null;
    }

    /// <summary>
    /// 删除缓存
    /// </summary>
    public async Task<bool> Remove(string key)
    {
        return await _redis.KeyDeleteAsync(key);
    }

    /// <summary>
    /// 批量删除(按前缀)
    /// </summary>
    public async Task RemoveByPrefix(string prefix)
    {
        var server = _redis.Multiplexer.GetServer(_redis.Multiplexer.GetEndPoints().First());
        var keys = server.Keys(pattern: $"{prefix}*").ToArray();

        if (keys.Length > 0)
        {
            await _redis.KeyDeleteAsync(keys);
            _logger.LogInformation("批量删除缓存,前缀:{Prefix},数量:{Count}", prefix, keys.Length);
        }
    }

    /// <summary>
    /// 检查缓存是否存在
    /// </summary>
    public async Task<bool> Exists(string key)
    {
        return await _redis.KeyExistsAsync(key);
    }

    /// <summary>
    /// 设置过期时间
    /// </summary>
    public async Task<bool> SetExpire(string key, TimeSpan expiry)
    {
        return await _redis.KeyExpireAsync(key, expiry);
    }

    /// <summary>
    /// 原子递增
    /// </summary>
    public async Task<long> Increment(string key, long value = 1)
    {
        return await _redis.StringIncrementAsync(key, value);
    }

    /// <summary>
    /// 原子递减
    /// </summary>
    public async Task<long> Decrement(string key, long value = 1)
    {
        return await _redis.StringDecrementAsync(key, value);
    }

    /// <summary>
    /// Hash 操作 - 设置
    /// </summary>
    public async Task HashSet(string key, string field, string value)
    {
        await _redis.HashSetAsync(key, field, value);
    }

    /// <summary>
    /// Hash 操作 - 获取
    /// </summary>
    public async Task<string> HashGet(string key, string field)
    {
        return await _redis.HashGetAsync(key, field);
    }

    /// <summary>
    /// Hash 操作 - 获取所有字段
    /// </summary>
    public async Task<Dictionary<string, string>> HashGetAll(string key)
    {
        var entries = await _redis.HashGetAllAsync(key);
        return entries.ToDictionary(e => e.Name.ToString(), e => e.Value.ToString());
    }

    /// <summary>
    /// 分布式锁
    /// </summary>
    public async Task<bool> LockTake(string lockKey, string lockValue, TimeSpan expiry)
    {
        return await _redis.LockTakeAsync(lockKey, lockValue, expiry);
    }

    /// <summary>
    /// 释放分布式锁
    /// </summary>
    public async Task<bool> LockRelease(string lockKey, string lockValue)
    {
        return await _redis.LockReleaseAsync(lockKey, lockValue);
    }
}

10.5 缓存键管理

10.5.1 缓存键命名规范

/// <summary>
/// 缓存键管理(统一定义缓存键)
/// </summary>
public static class CacheKeys
{
    /// <summary>
    /// 键前缀
    /// </summary>
    private const string Prefix = "furion";

    // 用户相关
    public static string UserInfo(int userId) => $"{Prefix}:user:info:{userId}";
    public static string UserPermissions(int userId) => $"{Prefix}:user:permissions:{userId}";
    public static string UserToken(int userId) => $"{Prefix}:user:token:{userId}";
    public static string UserList => $"{Prefix}:user:list";

    // 商品相关
    public static string ProductDetail(int productId) => $"{Prefix}:product:detail:{productId}";
    public static string ProductList(string category) => $"{Prefix}:product:list:{category}";
    public static string HotProducts => $"{Prefix}:product:hot";

    // 系统配置
    public static string SystemConfig(string key) => $"{Prefix}:config:{key}";
    public static string DictData(string dictType) => $"{Prefix}:dict:{dictType}";

    // 验证码
    public static string CaptchaCode(string captchaId) => $"{Prefix}:captcha:{captchaId}";

    // 速率限制
    public static string RateLimit(string clientId, string api) => $"{Prefix}:ratelimit:{clientId}:{api}";

    // 分布式锁
    public static string Lock(string resource) => $"{Prefix}:lock:{resource}";
}

10.5.2 缓存键使用示例

public class UserCacheService : IDynamicApiController
{
    private readonly IDistributedCache _cache;
    private readonly IRepository<User> _userRepo;

    public UserCacheService(IDistributedCache cache, IRepository<User> userRepo)
    {
        _cache = cache;
        _userRepo = userRepo;
    }

    public async Task<User> GetUser(int userId)
    {
        var cacheKey = CacheKeys.UserInfo(userId);
        return await _cache.GetOrCreateAsync(cacheKey, async () =>
        {
            return await _userRepo.FindAsync(userId);
        });
    }

    public async Task<List<string>> GetUserPermissions(int userId)
    {
        var cacheKey = CacheKeys.UserPermissions(userId);
        return await _cache.GetOrCreateAsync(cacheKey, async () =>
        {
            return await _userRepo.Entities
                .Where(u => u.Id == userId)
                .SelectMany(u => u.Roles)
                .SelectMany(r => r.Permissions)
                .Select(p => p.Code)
                .Distinct()
                .ToListAsync();
        }, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60)
        });
    }

    /// <summary>
    /// 用户信息变更时清除相关缓存
    /// </summary>
    public async Task InvalidateUserCache(int userId)
    {
        await _cache.RemoveAsync(CacheKeys.UserInfo(userId));
        await _cache.RemoveAsync(CacheKeys.UserPermissions(userId));
        await _cache.RemoveAsync(CacheKeys.UserToken(userId));
    }
}

10.6 缓存失效策略

策略 说明 优点 缺点
定时过期 到达指定时间后自动失效 实现简单 可能存在数据不一致窗口
主动失效 数据变更时主动删除缓存 数据一致性好 需要在所有变更点处理
延迟双删 先删缓存、更新数据库、延迟后再删缓存 减少不一致窗口 实现稍复杂
版本号 通过版本号判断缓存是否有效 灵活性高 需要额外存储版本号
发布订阅 通过消息通知各节点失效缓存 适合分布式环境 依赖消息中间件
/// <summary>
/// 延迟双删策略示例
/// </summary>
public class DoubleDeleteCacheService
{
    private readonly IDistributedCache _cache;
    private readonly IRepository<Product> _productRepo;

    public DoubleDeleteCacheService(IDistributedCache cache, IRepository<Product> productRepo)
    {
        _cache = cache;
        _productRepo = productRepo;
    }

    public async Task UpdateProduct(int id, ProductUpdateInput input)
    {
        var cacheKey = CacheKeys.ProductDetail(id);

        // 第一次删除缓存
        await _cache.RemoveAsync(cacheKey);

        // 更新数据库
        var product = await _productRepo.FindAsync(id);
        product.Name = input.Name;
        product.Price = input.Price;
        await _productRepo.UpdateAsync(product);
        await _productRepo.SaveNowAsync();

        // 延迟后再次删除缓存(处理并发场景下的脏读)
        _ = Task.Run(async () =>
        {
            await Task.Delay(500);  // 延迟500ms
            await _cache.RemoveAsync(cacheKey);
        });
    }
}

10.7 多级缓存

10.7.1 两级缓存实现

/// <summary>
/// 多级缓存服务(L1: 内存缓存, L2: Redis 缓存)
/// </summary>
public class MultiLevelCacheService
{
    private readonly IMemoryCache _l1Cache;           // 一级缓存:内存
    private readonly IDistributedCache _l2Cache;      // 二级缓存:Redis
    private readonly ILogger<MultiLevelCacheService> _logger;

    public MultiLevelCacheService(
        IMemoryCache l1Cache,
        IDistributedCache l2Cache,
        ILogger<MultiLevelCacheService> logger)
    {
        _l1Cache = l1Cache;
        _l2Cache = l2Cache;
        _logger = logger;
    }

    /// <summary>
    /// 获取缓存数据
    /// </summary>
    public async Task<T> GetAsync<T>(string key, Func<Task<T>> factory,
        TimeSpan? l1Expiry = null, TimeSpan? l2Expiry = null) where T : class
    {
        // 1. 先查一级缓存(内存)
        if (_l1Cache.TryGetValue(key, out T value))
        {
            _logger.LogDebug("L1缓存命中:{Key}", key);
            return value;
        }

        // 2. 再查二级缓存(Redis)
        var l2Value = await _l2Cache.GetObjectAsync<T>(key);
        if (l2Value != null)
        {
            _logger.LogDebug("L2缓存命中:{Key}", key);
            // 回写到一级缓存
            _l1Cache.Set(key, l2Value, l1Expiry ?? TimeSpan.FromMinutes(5));
            return l2Value;
        }

        // 3. 都未命中,从数据源获取
        _logger.LogDebug("缓存未命中,从数据源加载:{Key}", key);
        value = await factory();

        if (value != null)
        {
            // 写入两级缓存
            _l1Cache.Set(key, value, l1Expiry ?? TimeSpan.FromMinutes(5));
            await _l2Cache.SetObjectAsync(key, value,
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = l2Expiry ?? TimeSpan.FromMinutes(30)
                });
        }

        return value;
    }

    /// <summary>
    /// 删除多级缓存
    /// </summary>
    public async Task RemoveAsync(string key)
    {
        _l1Cache.Remove(key);
        await _l2Cache.RemoveAsync(key);
        _logger.LogDebug("多级缓存已删除:{Key}", key);
    }

    /// <summary>
    /// 刷新缓存
    /// </summary>
    public async Task RefreshAsync<T>(string key, Func<Task<T>> factory,
        TimeSpan? l1Expiry = null, TimeSpan? l2Expiry = null) where T : class
    {
        var value = await factory();
        if (value != null)
        {
            _l1Cache.Set(key, value, l1Expiry ?? TimeSpan.FromMinutes(5));
            await _l2Cache.SetObjectAsync(key, value,
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = l2Expiry ?? TimeSpan.FromMinutes(30)
                });
        }
    }
}

// 注册多级缓存服务
builder.Services.AddSingleton<MultiLevelCacheService>();

10.8 缓存穿透/击穿/雪崩防护

10.8.1 缓存穿透防护

缓存穿透是指查询一个不存在的数据,缓存和数据库都不命中,导致每次请求都直接打到数据库。

/// <summary>
/// 缓存穿透防护 - 布隆过滤器 + 空值缓存
/// </summary>
public class CachePenetrationGuard
{
    private readonly IDistributedCache _cache;
    private readonly IRepository<Product> _productRepo;

    // 空值缓存标识
    private const string NullCacheValue = "__NULL__";

    public CachePenetrationGuard(IDistributedCache cache, IRepository<Product> productRepo)
    {
        _cache = cache;
        _productRepo = productRepo;
    }

    /// <summary>
    /// 防穿透查询
    /// </summary>
    public async Task<Product> GetProduct(int id)
    {
        var cacheKey = CacheKeys.ProductDetail(id);
        var cachedData = await _cache.GetStringAsync(cacheKey);

        // 如果缓存中存储了空值标识,直接返回 null
        if (cachedData == NullCacheValue)
        {
            return null;
        }

        // 缓存命中
        if (!string.IsNullOrEmpty(cachedData))
        {
            return JsonSerializer.Deserialize<Product>(cachedData);
        }

        // 缓存未命中,查数据库
        var product = await _productRepo.FindAsync(id);

        if (product != null)
        {
            // 正常缓存
            await _cache.SetStringAsync(cacheKey,
                JsonSerializer.Serialize(product),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
                });
        }
        else
        {
            // 缓存空值(短过期时间)
            await _cache.SetStringAsync(cacheKey, NullCacheValue,
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
                });
        }

        return product;
    }
}

10.8.2 缓存击穿防护

缓存击穿是指某个热点 key 在过期的瞬间有大量并发请求,全部打到数据库。

using System.Collections.Concurrent;

/// <summary>
/// 缓存击穿防护 - 互斥锁
/// </summary>
public class CacheBreakdownGuard
{
    private readonly IDistributedCache _cache;
    private readonly IRepository<Product> _productRepo;
    private static readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();

    public CacheBreakdownGuard(IDistributedCache cache, IRepository<Product> productRepo)
    {
        _cache = cache;
        _productRepo = productRepo;
    }

    /// <summary>
    /// 防击穿查询(互斥锁方案)
    /// </summary>
    public async Task<Product> GetHotProduct(int id)
    {
        var cacheKey = CacheKeys.ProductDetail(id);

        // 先尝试从缓存获取
        var cachedData = await _cache.GetStringAsync(cacheKey);
        if (!string.IsNullOrEmpty(cachedData))
        {
            return JsonSerializer.Deserialize<Product>(cachedData);
        }

        // 获取互斥锁
        var lockObj = _locks.GetOrAdd(cacheKey, _ => new SemaphoreSlim(1, 1));

        await lockObj.WaitAsync();
        try
        {
            // 双重检查 - 可能其他线程已经加载了数据
            cachedData = await _cache.GetStringAsync(cacheKey);
            if (!string.IsNullOrEmpty(cachedData))
            {
                return JsonSerializer.Deserialize<Product>(cachedData);
            }

            // 查询数据库
            var product = await _productRepo.FindAsync(id);

            if (product != null)
            {
                await _cache.SetStringAsync(cacheKey,
                    JsonSerializer.Serialize(product),
                    new DistributedCacheEntryOptions
                    {
                        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
                    });
            }

            return product;
        }
        finally
        {
            lockObj.Release();
        }
    }
}

10.8.3 缓存雪崩防护

缓存雪崩是指大量缓存同时过期,导致大量请求直接打到数据库。

/// <summary>
/// 缓存雪崩防护 - 随机过期时间
/// </summary>
public class CacheAvalancheGuard
{
    private readonly IDistributedCache _cache;
    private static readonly Random _random = new();

    public CacheAvalancheGuard(IDistributedCache cache)
    {
        _cache = cache;
    }

    /// <summary>
    /// 设置缓存(带随机过期时间,防止雪崩)
    /// </summary>
    public async Task SetWithRandomExpiry<T>(string key, T value, int baseMinutes = 30)
    {
        // 在基础过期时间上增加随机偏移(0-10分钟)
        var randomOffset = _random.Next(0, 10);
        var expiry = TimeSpan.FromMinutes(baseMinutes + randomOffset);

        var json = JsonSerializer.Serialize(value);
        await _cache.SetStringAsync(key, json, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiry
        });
    }

    /// <summary>
    /// 批量预热缓存(分散过期时间)
    /// </summary>
    public async Task WarmupCache<T>(Dictionary<string, T> dataMap, int baseMinutes = 30)
    {
        var tasks = dataMap.Select(kvp =>
        {
            var randomOffset = _random.Next(0, baseMinutes / 2);
            return SetWithRandomExpiry(kvp.Key, kvp.Value, baseMinutes + randomOffset);
        });

        await Task.WhenAll(tasks);
    }
}

10.9 缓存使用最佳实践

10.9.1 最佳实践清单

实践项 说明
统一键命名 使用统一的缓存键命名规范,如 app:module:entity:id
合理设置过期时间 根据数据变化频率设置合适的过期时间
避免大对象缓存 缓存数据不宜过大,必要时进行压缩
缓存预热 应用启动时预加载热点数据
监控命中率 监控缓存命中率,优化缓存策略
序列化选择 选择高效的序列化方式(如 MessagePack)
防护策略 针对穿透、击穿、雪崩设计防护方案
数据一致性 制定缓存与数据库的一致性保障策略

10.9.2 缓存预热

using Microsoft.Extensions.Hosting;

/// <summary>
/// 缓存预热服务(应用启动时自动执行)
/// </summary>
public class CacheWarmupService : IHostedService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<CacheWarmupService> _logger;

    public CacheWarmupService(
        IServiceProvider serviceProvider,
        ILogger<CacheWarmupService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("开始缓存预热...");

        using var scope = _serviceProvider.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService<IDistributedCache>();
        var productRepo = scope.ServiceProvider.GetRequiredService<IRepository<Product>>();
        var configRepo = scope.ServiceProvider.GetRequiredService<IRepository<SystemConfig>>();

        try
        {
            // 预热热门商品
            var hotProducts = await productRepo.Entities
                .OrderByDescending(p => p.SalesCount)
                .Take(100)
                .ToListAsync();

            foreach (var product in hotProducts)
            {
                await cache.SetObjectAsync(
                    CacheKeys.ProductDetail(product.Id),
                    product,
                    new DistributedCacheEntryOptions
                    {
                        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
                    });
            }

            // 预热系统配置
            var configs = await configRepo.Entities.ToListAsync();
            foreach (var config in configs)
            {
                await cache.SetStringAsync(
                    CacheKeys.SystemConfig(config.Key),
                    config.Value,
                    new DistributedCacheEntryOptions
                    {
                        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)
                    });
            }

            _logger.LogInformation(
                "缓存预热完成,预热商品 {ProductCount} 个,配置 {ConfigCount} 条",
                hotProducts.Count, configs.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "缓存预热失败");
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

// 注册预热服务
builder.Services.AddHostedService<CacheWarmupService>();

10.10 性能优化

10.10.1 序列化性能优化

using System.Text.Json;

/// <summary>
/// 高性能缓存序列化配置
/// </summary>
public static class CacheSerializerOptions
{
    /// <summary>
    /// 高性能 JSON 序列化选项
    /// </summary>
    public static readonly JsonSerializerOptions HighPerformance = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = false,  // 不格式化,减少空间占用
        PropertyNameCaseInsensitive = true
    };

    /// <summary>
    /// 序列化为字节数组(比字符串更高效)
    /// </summary>
    public static byte[] SerializeToBytes<T>(T value)
    {
        return JsonSerializer.SerializeToUtf8Bytes(value, HighPerformance);
    }

    /// <summary>
    /// 从字节数组反序列化
    /// </summary>
    public static T DeserializeFromBytes<T>(byte[] bytes)
    {
        return JsonSerializer.Deserialize<T>(bytes, HighPerformance);
    }
}

10.10.2 缓存性能监控

/// <summary>
/// 缓存性能统计
/// </summary>
public class CacheMetrics
{
    private long _hitCount;
    private long _missCount;
    private long _setCount;
    private long _removeCount;

    public long HitCount => _hitCount;
    public long MissCount => _missCount;
    public long SetCount => _setCount;
    public long RemoveCount => _removeCount;
    public double HitRate => _hitCount + _missCount == 0
        ? 0
        : (double)_hitCount / (_hitCount + _missCount) * 100;

    public void RecordHit() => Interlocked.Increment(ref _hitCount);
    public void RecordMiss() => Interlocked.Increment(ref _missCount);
    public void RecordSet() => Interlocked.Increment(ref _setCount);
    public void RecordRemove() => Interlocked.Increment(ref _removeCount);

    public object GetStatistics() => new
    {
        HitCount = _hitCount,
        MissCount = _missCount,
        SetCount = _setCount,
        RemoveCount = _removeCount,
        HitRate = $"{HitRate:F2}%",
        TotalRequests = _hitCount + _missCount
    };

    public void Reset()
    {
        Interlocked.Exchange(ref _hitCount, 0);
        Interlocked.Exchange(ref _missCount, 0);
        Interlocked.Exchange(ref _setCount, 0);
        Interlocked.Exchange(ref _removeCount, 0);
    }
}

// 注册缓存指标(单例)
builder.Services.AddSingleton<CacheMetrics>();

// 查看缓存统计的接口
public class CacheMonitorService : IDynamicApiController
{
    private readonly CacheMetrics _metrics;

    public CacheMonitorService(CacheMetrics metrics)
    {
        _metrics = metrics;
    }

    /// <summary>
    /// 获取缓存统计信息
    /// </summary>
    public object GetCacheStatistics()
    {
        return _metrics.GetStatistics();
    }

    /// <summary>
    /// 重置统计信息
    /// </summary>
    public void ResetStatistics()
    {
        _metrics.Reset();
    }
}

10.10.3 性能优化总结

优化项 策略 预期效果
序列化 使用 UTF-8 字节序列化 减少 CPU 和内存开销
压缩 大对象使用 GZip 压缩 减少网络传输和存储空间
批量操作 使用 Pipeline 批量读写 减少网络往返次数
本地缓存 L1 内存 + L2 Redis 减少 Redis 网络请求
连接池 复用 Redis 连接 减少连接建立开销
异步操作 使用 async/await 提高线程利用率
合理过期 根据场景设置过期时间 平衡一致性和性能
缓存预热 启动时加载热点数据 减少冷启动影响