ABP VNext + Redis Bloom Filter:大规模缓存穿透防护与请求去重

发布于:2025-08-03 ⋅ 阅读:(12) ⋅ 点赞:(0)

ABP VNext + Redis Bloom Filter:大规模缓存穿透防护与请求去重 🚀



TL;DR ✨

  • 在 ABP VNext 应用层,通过 BloomFilter.Redis.NetCore + StackExchange.Redis 实现高性能、可配置的请求去重与缓存穿透防护 (NuGet)
  • 支持 IOptionsMonitor<BloomSettings> 动态调整预估容量、误判率与 Filter Key
  • 使用 SHA-256 生成紧凑稳定的请求 Key 并结合 ContainsAsync + AddAsync 避免并发竞态
  • 自定义 DuplicateRequestException 映射 HTTP 409,嵌入日志(ILogger)与 Prometheus 指标,全链路可观测 (NuGet)

1. 引言 🎉

在高并发场景下,比如秒杀、验证码、消息幂等等业务,常会遇到:

  • 缓存穿透:恶意或无效 Key 直击数据库,拖垮后端 😱
  • 请求去重:同一业务上下文多次触发同一逻辑,浪费计算与 I/O 🔄

如何在 ABP VNext (.NET 7/8) 中:

  1. 请求去重:管道最前端拦截重复请求
  2. 缓存穿透防护:访问 DB/缓存前快速判断 Key 是否“可能存在”

并在生产级角度引入:

  • 配置化 (IOptionsMonitor<BloomSettings>)
  • SHA-256 Key 生成
  • ContainsAsync + AddAsync 竞态处理
  • Prometheus 全链路监控
ContainsAsync false
ContainsAsync true
客户端发起请求
BloomFilterBehavior
AddAsync + 下游执行
抛出 DuplicateRequestException
调用下游 Handler
返回 HTTP 409

2. 环境与依赖 🛠️

dotnet add package StackExchange.Redis
dotnet add package BloomFilter.Redis.NetCore --version 2.5.2
dotnet add package prometheus-net.AspNetCore
  • BloomFilter.Redis.NetCore:Redis 后端 Bloom Filter 实现 (NuGet)
  • prometheus-net.AspNetCore:ASP.NET Core 集成的 Prometheus 指标服务 (NuGet)

appsettings.json 示例:

{
  "Redis": {
    "Configuration": "127.0.0.1:6379"
  },
  "BloomSettings": {
    "FilterKey": "bf:requests",
    "ExpectedItems": 1000000,
    "FalsePositiveRate": 0.001
  }
}

3. Bloom Filter 原理简述 🎓

false
true
请求 Key
ContainsAsync?
AddAsync + 放行
抛出 409 异常
  • 结构:大小为 m 的位数组 + k 哈希函数

  • 误判率

    • k = ln2·(m/n)
    • 误判 ≈ (1–e–k·n/m)k
  • 对比 LRU

    • LRU:存全量 Key,无误判
    • Bloom:低内存、高速,允许小概率误判

4. ABP 中的配置化注入 🔧

4.1 配置类

public class BloomSettings
{
    public string FilterKey { get; set; }
    public long ExpectedItems { get; set; }
    public double FalsePositiveRate { get; set; }
}

4.2 Module 注册

public override void ConfigureServices(ServiceConfigurationContext context)
{
    var config = context.Services.GetConfiguration();

    // 绑定配置
    Configure<BloomSettings>(config.GetSection("BloomSettings"));

    // 注入 Redis 连接
    context.Services.AddSingleton<IConnectionMultiplexer>(sp =>
        ConnectionMultiplexer.Connect(config["Redis:Configuration"]));

    // 注入 BloomFilter(FilterRedisBuilder.Build 来自 BloomFilter.Redis.NetCore)
    context.Services.AddSingleton<IBloomFilter>(sp =>
    {
        var settings = sp.GetRequiredService<IOptionsMonitor<BloomSettings>>().CurrentValue;
        return FilterRedisBuilder.Build(
            config["Redis:Configuration"],   // Redis 连接字符串
            settings.FilterKey,              // Bloom Filter Key & 名称
            (int)settings.ExpectedItems,     // 预估容量
            settings.FalsePositiveRate       // 误判率
        );
    });

    // 注册 Pipeline Behavior
    context.Services.AddTransient(typeof(IPipelineBehavior<,>),
                                  typeof(BloomFilterBehavior<,>));
}

FilterRedisBuilder.Build 方法在 vla/BloomFilter.NetCore 库中提供,可配置容量、误判率并返回 IBloomFilter 实例 (GitHub)。


5. 请求去重 Behavior 🔐

5.1 Key 生成:SHA-256 哈希

public static class BloomKeyGenerator
{
    public static string Generate(object request)
    {
        var raw = $"{request.GetType().Name}:{JsonSerializer.Serialize(request, new JsonSerializerOptions {
            IgnoreNullValues = true
        })}";
        using var sha = SHA256.Create();
        var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(raw));
        return Convert.ToHexString(hash); // .NET 5+ API
    }
}

🔍 提示:极端高吞吐场景可选用 xxHash 等非加密哈希,进一步降 CPU 开销 (GitHub)。

5.2 自定义异常

public class DuplicateRequestException : BusinessException
{
    public DuplicateRequestException()
        : base("DUPLICATE_REQUEST", "请求已被拦截(重复请求)") { }
}

全局异常处理中映射为 HTTP 409 Conflict。

5.3 Behavior 实现

public class BloomFilterBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IBloomFilter _bloom;
    private readonly ILogger _logger;
    private static readonly Counter ContainsCount = Metrics
        .CreateCounter("bloom_contains_total", "BloomFilter ContainsAsync 调用总数");
    private static readonly Counter AddCount = Metrics
        .CreateCounter("bloom_add_total",      "BloomFilter AddAsync 调用总数");
    private static readonly Counter DuplicateCount = Metrics
        .CreateCounter("bloom_duplicate_total","重复请求拦截总数");

    public BloomFilterBehavior(
        IBloomFilter bloomFilter,
        ILogger<BloomFilterBehavior<TRequest, TResponse>> logger)
    {
        _bloom = bloomFilter;
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var key = BloomKeyGenerator.Generate(request);

        ContainsCount.Inc();
        if (await _bloom.ContainsAsync(key))
        {
            DuplicateCount.Inc();
            _logger.LogWarning("重复请求拦截: {Key}", key);
            throw new DuplicateRequestException();
        }

        AddCount.Inc();
        await _bloom.AddAsync(key);

        return await next();
    }
}

如所用库的 AddAsync 返回布尔值,可用返回值判断“新加”/“已存在”,进一步简化逻辑。


6. 缓存穿透防护策略 🔍

public async Task<ProductDto> GetAsync(Guid productId)
{
    var key = productId.ToString();

    // 1. Bloom 预判断
    if (!await _bloom.ContainsAsync(key))
    {
        // 未命中,尝试真实查询并预热
        var entity = await _repository.GetAsync(productId);
        if (entity != null)
        {
            await _bloom.AddAsync(key);
        }
        return entity; // null 或真实结果
    }

    // 2. 可能存在,直接查询缓存/DB
    return await _repository.GetAsync(productId);
}
false
entity ≠ null
entity = null
true
GetAsync(Product)
ContainsAsync?
Db.GetAsync
AddAsync
返回 null
Cache/DB 查询

✔️ 优化:此策略无需离线“预热”,首访合法请求正常命中并写入 Bloom。


7. Prometheus 全链路监控 📈

7.1 中间件配置

var builder = WebApplication.CreateBuilder(args);

// 注册 Prometheus 指标服务 & HTTP 埋点
builder.Services.AddMetricServer();
builder.Services.AddHttpMetrics();

var app = builder.Build();
app.UseHttpMetrics();   // 自动统计 HTTP 请求
app.UseMetricServer();  // 暴露 /metrics
app.MapControllers();
app.Run();

Prometheus 客户端包:prometheus-net.AspNetCore (NuGet)。

7.2 自定义指标

  • bloom_contains_total
  • bloom_add_total
  • bloom_duplicate_total
  • false_positive_total(业务层捕获误判并上报)

8. 从启动到拦截 🚀

docker run -d --name redis -p 6379:6379 redis:7
dotnet run --project YourAbpApp
Client API Redis DB 第 1 次 请求 ContainsAsync(key) false AddAsync(key) OK GetAsync(key) 返回数据 200 OK 第 2 次 请求 ContainsAsync(key) true 409 Conflict Client API Redis DB