Redis缓存穿透布隆过滤器解决

wen java案例 3

Redis缓存穿透与布隆过滤器解决方案

什么是缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中不存在,每次请求都会穿透到数据库,导致数据库压力过大。

Redis缓存穿透布隆过滤器解决

布隆过滤器原理

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中。

特点:

  • 判断不存在:100%准确
  • 判断存在:可能存在误判(有概率说存在实际不存在)
  • 无法删除元素(除非使用增强版Counting Bloom Filter)

使用布隆过滤器解决缓存穿透的流程

请求 -> 布隆过滤器 -> 缓存 -> 数据库
  1. 请求先经过布隆过滤器
  2. 布隆过滤器判断不存在 → 直接返回(拦截)
  3. 布隆过滤器判断存在 → 查询缓存
  4. 缓存命中 → 返回数据
  5. 缓存未命中 → 查询数据库
  6. 数据存在 → 写入缓存并返回
  7. 数据不存在 → 不写入缓存(或写入空值)

Java实现示例

使用Google Guava的布隆过滤器(单机版)

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
@Component
public class BloomFilterService {
    private BloomFilter<String> bloomFilter;
    // 预计数据量
    private static final int EXPECTED_INSERTIONS = 1000000;
    // 误判率
    private static final double FPP = 0.001;
    @PostConstruct
    public void init() {
        bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            EXPECTED_INSERTIONS,
            FPP
        );
        // 初始化时加载所有用户ID到布隆过滤器
        loadAllUserIds();
    }
    private void loadAllUserIds() {
        // 从数据库加载所有用户ID
        List<String> userIds = userMapper.getAllUserIds();
        for (String userId : userIds) {
            bloomFilter.put(userId);
        }
    }
    public boolean mightContain(String userId) {
        return bloomFilter.mightContain(userId);
    }
    // 新增用户时更新布隆过滤器
    public void addUserId(String userId) {
        bloomFilter.put(userId);
    }
}

使用Redis BitMap实现布隆过滤器(分布式版)

import redis.clients.jedis.Jedis;
import org.springframework.stereotype.Component;
import java.util.BitSet;
@Component
public class RedisBloomFilter {
    private static final String BLOOM_KEY = "user:bloom";
    // 布隆过滤器参数
    private static final int BIT_SIZE = 2 << 28; // 2^29 = 5.36亿
    private static final int NUM_HASH_FUNCTIONS = 5;
    private Jedis jedis;
    /**
     * 添加元素到布隆过滤器
     */
    public void add(String value) {
        int[] offsets = getHashOffsets(value);
        for (int offset : offsets) {
            jedis.setbit(BLOOM_KEY, offset, true);
        }
    }
    /**
     * 判断元素是否可能存在
     */
    public boolean mightContain(String value) {
        int[] offsets = getHashOffsets(value);
        for (int offset : offsets) {
            if (!jedis.getbit(BLOOM_KEY, offset)) {
                return false; // 只要有一个位为0,肯定不存在
            }
        }
        return true; // 所有位都为1,可能存在(有误判)
    }
    /**
     * 计算多个哈希函数的偏移量
     */
    private int[] getHashOffsets(String value) {
        int[] offsets = new int[NUM_HASH_FUNCTIONS];
        int h1 = Math.abs(value.hashCode());
        int h2 = Math.abs(h1 >> 16);
        for (int i = 1; i <= NUM_HASH_FUNCTIONS; i++) {
            offsets[i-1] = Math.abs((h1 + i * h2) % BIT_SIZE);
        }
        return offsets;
    }
}

使用Redisson(推荐方案)

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private UserRepository userRepository;
    private RBloomFilter<String> bloomFilter;
    @PostConstruct
    public void init() {
        // 创建布隆过滤器
        bloomFilter = redissonClient.getBloomFilter("userBloomFilter");
        // 初始化(预计数据量100万,误判率0.001)
        bloomFilter.tryInit(1000000L, 0.001);
        // 加载现有数据
        loadExistingData();
    }
    private void loadExistingData() {
        List<String> userIds = userRepository.getAllUserIds();
        for (String userId : userIds) {
            bloomFilter.add(userId);
        }
    }
    public User getUserById(String userId) {
        // 1. 布隆过滤器判断
        if (!bloomFilter.contains(userId)) {
            // 肯定不存在,直接返回
            return null;
        }
        // 2. 查询缓存
        String cacheKey = "user:" + userId;
        User user = getFromCache(cacheKey);
        if (user != null) {
            return user;
        }
        // 3. 查询数据库
        user = userRepository.findById(userId);
        if (user != null) {
            // 写入缓存
            setToCache(cacheKey, user);
        }
        return user;
    }
    // 新增用户时更新布隆过滤器
    public void addUser(User user) {
        userRepository.save(user);
        bloomFilter.add(user.getId());
    }
}

项目中的完整缓存方案

@Service
public class OptimizedUserService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserMapper userMapper;
    private RBloomFilter<String> bloomFilter;
    @PostConstruct
    public void init() {
        bloomFilter = redissonClient.getBloomFilter("userBloomFilter");
        bloomFilter.tryInit(1000000L, 0.001);
    }
    public User getUserById(String userId) {
        // 1. 布隆过滤器过滤
        if (!bloomFilter.contains(userId)) {
            log.warn("用户不存在,已被布隆过滤器拦截: {}", userId);
            return null;
        }
        // 2. 查询缓存
        String cacheKey = "user:" + userId;
        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
        // 3. 缓存命中
        if (cacheValue != null) {
            // 判断是否是空值缓存
            if ("NULL".equals(cacheValue)) {
                return null;
            }
            return JSON.parseObject(cacheValue, User.class);
        }
        // 4. 缓存未命中,查询数据库
        User user = userMapper.findById(userId);
        // 5. 缓存处理
        if (user != null) {
            // 数据存在,写入缓存
            redisTemplate.opsForValue().set(
                cacheKey, 
                JSON.toJSONString(user),
                30, 
                TimeUnit.MINUTES
            );
        } else {
            // 数据不存在,缓存空值(防止缓存穿透)
            redisTemplate.opsForValue().set(
                cacheKey, 
                "NULL",
                5, 
                TimeUnit.MINUTES
            );
        }
        return user;
    }
}

布隆过滤器参数配置建议

参数 说明 建议值
expectedInsertions 预计数据量 根据业务评估,预留20%余量
fpp (false positive probability) 误判率 001 (千分之一)
bitSize 位图大小 N * ln(FPP) / (ln2)^2
numHashFunctions 哈希函数数量 (bits / N) * ln2

优缺点分析

优点:

  • 节省内存(比HashSet节省90%以上空间)
  • 查询速度快(O(k),k为哈希函数数量)
  • 能有效防止缓存穿透

缺点:

  • 存在误判率(不能100%确定存在)
  • 无法删除元素
  • 需要定期重建(数据量变化大时)

最佳实践建议

  1. 定期重建:定期从数据库全量加载数据重建布隆过滤器
  2. 配合空值缓存:布隆过滤 + 空值缓存双重保险
  3. 监控误判率:监控实际误判率,适时调整参数
  4. 冷热数据分离:热点数据使用布隆过滤器,冷数据使用其他方案

这种方案能有效解决缓存穿透问题,将无效请求拦截在Redis之前,保护数据库安全。

抱歉,评论功能暂时关闭!