Redis 缓存穿透

139 阅读3分钟

一、Bug 场景

在一个电商商品查询系统中,使用 Redis 作为缓存以减轻数据库压力。用户通过商品 ID 查询商品信息时,系统先从 Redis 缓存中查找,若未找到则查询数据库,并将查询结果存入 Redis 缓存。然而,在遭受恶意攻击时,大量不存在的商品 ID 查询涌入系统,导致所有查询都穿透到数据库,数据库压力剧增,甚至出现服务不可用的情况。

二、代码示例

商品服务(有缺陷)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate redisTemplate;

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息或null
        return null;
    }

    public Object getProduct(String productId) {
        Object product = redisTemplate.opsForValue().get(productId);
        if (product == null) {
            product = queryProductFromDB(productId);
            if (product != null) {
                redisTemplate.opsForValue().set(productId, product);
            }
        }
        return product;
    }
}

三、问题描述

  1. 预期行为:正常情况下,大部分商品查询能从 Redis 缓存中获取数据,只有少部分缓存未命中的查询才会穿透到数据库,数据库压力处于可控范围。
  2. 实际行为:当大量不存在的商品 ID 查询进入系统时,由于这些 ID 在 Redis 中没有对应的缓存数据,系统会不断查询数据库,导致数据库压力剧增。这是因为系统没有对不存在的数据进行有效处理,每次查询都认为是正常的缓存未命中,从而持续访问数据库。恶意攻击者可以利用这一点,通过大量查询不存在的商品 ID,耗尽数据库资源,导致系统崩溃。

四、解决方案

  1. 布隆过滤器(Bloom Filter) :在查询数据库之前,使用布隆过滤器判断商品 ID 是否存在。布隆过滤器可以快速判断一个元素一定不存在或者可能存在。如果布隆过滤器判断商品 ID 一定不存在,直接返回,不再查询数据库;如果判断可能存在,再查询数据库。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate redisTemplate;
    private static final BloomFilter bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(java.nio.charset.StandardCharsets.UTF_8), 1000000, 0.01);

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息或null
        return null;
    }

    public Object getProduct(String productId) {
        if (!bloomFilter.mightContain(productId)) {
            return null;
        }
        Object product = redisTemplate.opsForValue().get(productId);
        if (product == null) {
            product = queryProductFromDB(productId);
            if (product != null) {
                redisTemplate.opsForValue().set(productId, product);
                bloomFilter.put(productId);
            }
        }
        return product;
    }
}
  1. 缓存空值:当查询数据库发现商品不存在时,将空值存入 Redis 缓存,并设置较短的过期时间。这样,后续相同的查询直接从 Redis 获取空值,避免再次查询数据库。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate redisTemplate;

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息或null
        return null;
    }

    public Object getProduct(String productId) {
        Object product = redisTemplate.opsForValue().get(productId);
        if (product == null) {
            product = queryProductFromDB(productId);
            if (product != null) {
                redisTemplate.opsForValue().set(productId, product);
            } else {
                // 缓存空值,设置较短过期时间
                redisTemplate.opsForValue().set(productId, null, 10, TimeUnit.MINUTES);
            }
        }
        return product;
    }
}