1. 缓存穿透的噩梦现场

某电商平台大促期间突然出现数据库CPU飙升,每秒数万次请求查询不存在的商品ID。这就是典型的缓存穿透现象——恶意或异常的请求绕过缓存层,直接冲击底层数据库。这种攻击轻则导致服务降级,重则引发数据库雪崩。

2. 穿透原理拆解

缓存穿透发生在以下场景:

  • 恶意攻击:使用随机生成的非法ID进行高频请求
  • 业务缺陷:前端未校验参数直接提交0或负数的ID
  • 数据失效:缓存数据被误删但未重建完成时突发请求
// 典型的问题代码示例(使用StackExchange.Redis库)
public Product GetProduct(string productId)
{
    var cache = ConnectionMultiplexer.Connect("localhost").GetDatabase();
    var cacheKey = $"product:{productId}";
    
    // 直接从缓存读取
    var productJson = cache.StringGet(cacheKey);
    
    if (productJson.HasValue)
    {
        return JsonConvert.DeserializeObject<Product>(productJson);
    }
    
    // 缓存未命中时查询数据库(危险操作)
    var product = _db.Products.FirstOrDefault(p => p.Id == productId);
    
    // 将结果写入缓存
    cache.StringSet(cacheKey, JsonConvert.SerializeObject(product), TimeSpan.FromMinutes(5));
    
    return product;
}

3. 防御组合拳四式

3.1 空值缓存:给"不存在"打标记

public Product GetProductWithNullCache(string productId)
{
    var cache = ConnectionMultiplexer.Connect("localhost").GetDatabase();
    var cacheKey = $"product:{productId}";
    
    var productJson = cache.StringGet(cacheKey);
    
    // 检测到特殊空值标记
    if (productJson == "NULL_FLAG")
    {
        return null;
    }
    
    if (productJson.HasValue)
    {
        return JsonConvert.DeserializeObject<Product>(productJson);
    }
    
    var product = _db.Products.FirstOrDefault(p => p.Id == productId);
    
    if (product == null)
    {
        // 设置短期空值缓存(5分钟)
        cache.StringSet(cacheKey, "NULL_FLAG", TimeSpan.FromMinutes(5));
        return null;
    }
    
    cache.StringSet(cacheKey, JsonConvert.SerializeObject(product), TimeSpan.FromHours(1));
    return product;
}

应用场景:适合数据变更不频繁的业务,如用户基本信息查询
注意事项:设置合理的过期时间(建议5-30分钟),避免长期存储无效数据

3.2 布隆过滤器:建立数据白名单

// 使用BloomFilter.NET库
var filter = new BloomFilter(1000000, 0.01); // 百万数据量,1%误判率

// 预热阶段加载有效ID
foreach(var id in _db.Products.Select(p => p.Id))
{
    filter.Add(id);
}

public Product GetProductWithBloomFilter(string productId)
{
    // 先进行布隆过滤检查
    if (!filter.Contains(productId))
    {
        return null; // 快速拦截非法请求
    }
    
    // ...后续正常处理流程...
}

技术特点

  • 内存占用极低(百万数据约需1MB)
  • 存在概率性误判(可通过调整参数控制)
  • 不支持数据删除(需要定时重建)

3.3 互斥锁:请求合并术

// 使用RedLock.net实现分布式锁
public Product GetProductWithLock(string productId)
{
    var cache = ConnectionMultiplexer.Connect("localhost").GetDatabase();
    var cacheKey = $"product:{productId}";
    var lockKey = $"lock:{productId}";
    
    var productJson = cache.StringGet(cacheKey);
    
    if (productJson.HasValue)
    {
        return JsonConvert.DeserializeObject<Product>(productJson);
    }
    
    // 获取分布式锁(5秒超时)
    using (var redLock = RedLockFactory.Create().CreateLock(lockKey, TimeSpan.FromSeconds(5)))
    {
        if (redLock.IsAcquired)
        {
            var product = _db.Products.FirstOrDefault(p => p.Id == productId);
            
            if (product == null)
            {
                cache.StringSet(cacheKey, "NULL_FLAG", TimeSpan.FromMinutes(5));
            }
            else
            {
                cache.StringSet(cacheKey, JsonConvert.SerializeObject(product), TimeSpan.FromHours(1));
            }
            return product;
        }
        else
        {
            // 等待其他线程处理完成
            Thread.Sleep(100);
            return GetProductWithLock(productId); // 递归重试
        }
    }
}

最佳实践

  • 锁超时时间要短于接口响应超时时间
  • 配合熔断机制防止线程堆积
  • 在缓存重建完成前保持锁的有效性

3.4 请求校验:把好第一道关

// 在ASP.NET Core中使用模型验证
public class ProductRequest
{
    [Required]
    [RegularExpression(@"^[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}$")]
    public string ProductId { get; set; }
}

// 控制器方法
[HttpPost]
public IActionResult GetProduct([FromBody] ProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest("非法参数格式");
    }
    // ...后续处理...
}

防御策略

  • ID格式校验(长度、字符类型)
  • 数值范围校验(排除负数、超大数据)
  • 业务逻辑校验(用户权限验证)

4. 方案选型指南

方案 适用场景 优点 缺点
空值缓存 数据量小的低频查询 实现简单 内存消耗大
布隆过滤器 海量数据的精确防御 内存效率高 存在误判率
互斥锁 高频热点数据保护 保证数据一致性 增加系统复杂度
请求校验 所有查询场景 前置防御 依赖参数规则完善

黄金组合建议

  1. 前端实施严格的参数校验
  2. 网关层部署布隆过滤器
  3. 服务层采用空值缓存+互斥锁
  4. 监控异常Key的访问频次

5. 避坑指南

  1. 空值雪崩:不要给所有空值设置相同的过期时间,建议添加随机波动值
    var expire = TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(new Random().Next(0, 300)));
    
  2. 过滤器更新:当新增数据时,需要同步更新布隆过滤器
  3. 锁粒度控制:根据业务场景选择细粒度锁(单个Key)或粗粒度锁(分类锁)
  4. 监控报警:对缓存未命中率设置阈值报警(建议超过50%触发预警)

6. 总结与展望

缓存穿透的防御需要多层防护体系,就像给系统穿上防弹衣。在C#生态中,通过StackExchange.Redis+布隆过滤器+分布式锁的组合拳,能有效构建立体防御网络。随着Redis模块的发展,RedisBloom等官方解决方案逐渐成熟,未来可以探索更原生的实现方式。但无论技术如何演进,防御的核心思路始终是:宁可错杀三千非法请求,也不放过一个漏网之鱼。

实际部署时建议:

  1. 先在测试环境模拟攻击场景(使用Locust等压测工具)
  2. 逐步上线防御措施,观察系统指标变化
  3. 建立自动化的缓存预热机制
  4. 定期审计关键查询接口的防御策略

记住:好的防御体系不是一次性工程,而是需要持续优化的生命体。保持对新技术的学习,才能在这个攻防不断升级的战场上立于不败之地。