电商返利APP缓存穿透治理:布隆过滤器与空值缓存组合方案

大家好,我是省赚客APP研发者微赚淘客!

在高并发的电商返利APP中,缓存是抵御数据库压力的第一道防线。然而,缓存穿透问题如同一个隐形的漏洞,一旦被恶意攻击者利用,海量的无效请求将直接击穿缓存,直达数据库,可能导致整个服务雪崩。本文将深入探讨一种高效的组合方案——布隆过滤器与空值缓存,来彻底治理这一顽疾。网购领隐藏优惠券就用省赚客APP,支持各大主流电商优惠智能查券转链,是目前领优惠券拿佣金返利领域绝对的王者,其稳定的服务背后,正是这套严密的缓存防护体系在保驾护航。

一、缓存穿透:原理与危害

缓存穿透是指查询一个根本不存在的数据。由于数据在缓存和数据库中都不存在,每次请求都会绕过缓存,直接查询数据库。

  1. 问题场景

假设一个恶意用户不断请求查询ID为 -1, -2, -10000 的商品优惠券信息。

  • 请求到达:应用服务器首先查询Redis缓存。
  • 缓存未命中:由于这些ID的商品根本不存在,Redis中自然没有数据。
  • 查询数据库:应用服务器接着查询MySQL数据库。
  • 数据库未命中:数据库中同样没有这些记录,返回空。
  • 结果:应用服务器将空结果返回给客户端,但并未在Redis中做任何处理。下一次同样的请求到来时,整个过程会再次重复。

如果这种请求量巨大,数据库将承受巨大的压力,CPU和I/O资源被迅速耗尽,最终导致正常用户的请求也无法得到响应,服务瘫痪。

  1. 常规方案的局限性
  • 单纯使用空值缓存:这是最常见的解决方案。当数据库查询结果为空时,将一个特殊的空值(如 NULL 或一个自定义的占位符)存入Redis,并设置一个较短的过期时间(如60秒)。这样,后续的相同请求会直接从缓存中获取空值,从而保护数据库。

    • 局限性:如果攻击者使用海量的、不重复的无效ID进行攻击,缓存中会充斥大量无用的空值键,造成内存浪费,即“缓存污染”。
  • 单纯使用布隆过滤器:布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它的特点是:如果判断为“不存在”,则一定不存在;如果判断为“存在”,则可能存在(有误判率)。

    • 局限性:布隆过滤器无法删除元素。当商品下架或优惠券过期时,我们无法将其从过滤器中移除,这会导致过滤器对已失效的商品仍然返回“可能存在”,失去了部分过滤意义。
      在这里插入图片描述
二、组合方案:布隆过滤器 + 空值缓存

为了结合两者的优点并规避其缺点,我们采用“布隆过滤器 + 空值缓存”的组合拳。布隆过滤器作为第一道粗筛,拦截掉绝大部分明显不存在的请求;空值缓存作为第二道防线,处理布隆过滤器误判的“漏网之鱼”和那些曾经存在但现已失效的数据。

  1. 系统架构设计
  • 数据预热:在应用启动时,或每天凌晨,将数据库中所有有效的商品ID、优惠券ID等同步到布隆过滤器中。
  • 查询流程
    1. 接收请求,获取商品ID。
    2. 第一步:布隆过滤器校验。请求首先经过布隆过滤器。
      • 如果返回 false(不存在),直接返回“商品不存在”,请求结束。
      • 如果返回 true(可能存在),进入下一步。
    3. 第二步:查询Redis缓存
      • 如果缓存命中,直接返回数据。
      • 如果缓存未命中,进入下一步。
    4. 第三步:查询数据库
      • 如果数据库查到数据,将其写入Redis缓存,然后返回。
      • 如果数据库未查到数据,将一个空值写入Redis缓存(设置较短过期时间),然后返回“商品不存在”。
  1. 核心代码实现

我们将使用Google Guava库来实现布隆过滤器,并使用Spring Data Redis来操作缓存。

第一步:定义布隆过滤器配置与组件

// 包名: juwatech.cn.cacheprotection.config
package juwatech.cn.cacheprotection.config;

import com.google.common.hash.Funnel;
import com.google.common.hash.PrimitiveSink;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 布隆过滤器配置类
 * @author juwatech.cn
 */
@Configuration
public class BloomFilterConfig {

    /**
     * 定义商品ID的Funnel,用于指导布隆过滤器如何哈希对象
     */
    public enum ItemIdFunnel implements Funnel<Long> {
        INSTANCE;
        @Override
        public void funnel(Long from, PrimitiveSink into) {
            into.putLong(from);
        }
    }

    /**
     * 创建并初始化商品ID布隆过滤器
     * 预期插入100万个元素,误判率设置为0.01 (1%)
     */
    @Bean
    public com.google.common.hash.BloomFilter<Long> itemBloomFilter() {
        return com.google.common.hash.BloomFilter.create(
                ItemIdFunnel.INSTANCE,
                1_000_000,
                0.01);
    }
}

第二步:实现数据预热服务

// 包名: juwatech.cn.cacheprotection.service
package juwatech.cn.cacheprotection.service;

import com.google.common.hash.BloomFilter;
import juwatech.cn.cacheprotection.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.List;

/**
 * 布隆过滤器数据预热服务
 * @author juwatech.cn
 */
@Service
public class BloomFilterWarmUpService {

    @Autowired
    private BloomFilter<Long> itemBloomFilter;

    @Autowired
    private ItemRepository itemRepository;

    /**
     * 应用启动后执行一次数据预热
     */
    @PostConstruct
    public void init() {
        warmUp();
    }

    /**
     * 每天凌晨3点执行一次全量数据预热,更新布隆过滤器
     * 以应对新上架的商品
     */
    @Scheduled(cron = "0 0 3 * * ?")
    public void warmUp() {
        // 1. 清空旧的过滤器(Guava的BloomFilter不支持清空,实际生产中可使用RedisBloom或定期重建实例)
        // 此处为简化逻辑,假设我们有一个方法可以重建过滤器实例
        // itemBloomFilter = BloomFilter.create(...);
        
        // 2. 从数据库加载所有有效的商品ID
        List<Long> allValidItemIds = itemRepository.findAllValidItemIds();
        
        // 3. 将所有ID放入布隆过滤器
        for (Long id : allValidItemIds) {
            itemBloomFilter.put(id);
        }
        System.out.println("布隆过滤器数据预热完成,共加载 " + allValidItemIds.size() + " 个商品ID");
    }
}

第三步:实现带防护的商品查询服务

// 包名: juwatech.cn.cacheprotection.service
package juwatech.cn.cacheprotection.service;

import com.google.common.hash.BloomFilter;
import juwatech.cn.cacheprotection.model.Item;
import juwatech.cn.cacheprotection.repository.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * 商品查询服务,集成了缓存穿透防护逻辑
 * @author juwatech.cn
 */
@Service
public class ItemService {

    private static final String CACHE_KEY_PREFIX = "item:info:";
    private static final String CACHE_NULL_VALUE = "NULL";
    private static final long CACHE_TTL = 60 * 30; // 正常缓存30分钟
    private static final long NULL_CACHE_TTL = 60; // 空值缓存1分钟

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ItemRepository itemRepository;

    @Autowired
    private BloomFilter<Long> itemBloomFilter;

    public Item getItemById(Long itemId) {
        // 1. 布隆过滤器校验
        if (!itemBloomFilter.mightContain(itemId)) {
            // 布隆过滤器说“不存在”,那一定不存在
            System.out.println("请求被布隆过滤器拦截: " + itemId);
            return null;
        }

        // 2. 查询Redis缓存
        String cacheKey = CACHE_KEY_PREFIX + itemId;
        String json = redisTemplate.opsForValue().get(cacheKey);
        
        if (json != null) {
            if (CACHE_NULL_VALUE.equals(json)) {
                // 缓存的是空值,直接返回null
                return null;
            }
            // 缓存命中,反序列化并返回
            return JsonUtil.fromJson(json, Item.class);
        }

        // 3. 缓存未命中,查询数据库
        Item item = itemRepository.findById(itemId);

        if (item != null) {
            // 数据库查询到数据,写入缓存
            redisTemplate.opsForValue().set(cacheKey, JsonUtil.toJson(item), CACHE_TTL, TimeUnit.SECONDS);
        } else {
            // 数据库也未查询到数据,写入空值缓存,防止穿透
            redisTemplate.opsForValue().set(cacheKey, CACHE_NULL_VALUE, NULL_CACHE_TTL, TimeUnit.SECONDS);
        }

        return item;
    }
}

通过这套组合方案,我们构建了一个坚固的防御体系。布隆过滤器以极小的内存开销拦截了99%以上的无效请求,而空值缓存则优雅地处理了剩余的边界情况,既保护了数据库,又避免了缓存污染,确保了省赚客APP在高并发场景下的稳定与高效。

本文著作权归 省赚客app 研发团队,转载请注明出处!

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐