Redis 缓存击穿 缓存穿透 缓存雪崩解决方案及布隆过滤器(Bloom Filter/RedisBloom)

  • 缓存击穿

指的是单个key在缓存中查不到,去数据库查询,这样如果数据量不大或者并发不大的话是没有什么问题的。

如果数据库数据量大并且是高并发的情况下那么就可能会造成数据库压力过大而崩溃

解决思路:

采用锁 + 双重检查机制:某个key只让一个线程查询,阻塞其它线程,在同步块中,继续判断检查,保证不存在,才去查DB。


import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ProjectName demo
 * @ClassName App
 * @Description TODO
 * @Author Wang
 * @Date 2020/6/3 9:25 上午
 * @Version 1.0.0
 **/
@Slf4j
public class App {

    /**
     * 模拟数据库数据
     */
    private static List<Map<String, Object>> DATA;

    /**
     * 缓存默认前缀
     */
    private static final String DEFAULT_PREFIX = "default:prefix";

    private static final transient ReentrantLock lock = new ReentrantLock();

    static {
        DATA = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            HashMap<String, Object> map = new HashMap<>(16);
            map.put("id", i);
            map.put("name", "name" + i);
            DATA.add(map);
        }
    }

    /**
     * 模拟查询业务
     **/
    private static Map<String, Object> query(Integer id) {

        Jedis jedis = jedis();
        String cacheKey = DEFAULT_PREFIX + ":" + id;

        Map<String, Object> map = new HashMap<>(16);
        Gson gson = new Gson();

        String cacheValue = jedis.get(cacheKey);
        log.info("第一次查询缓存数据为 -> [{}]", cacheValue);

        if (StringUtils.isNotBlank(cacheValue)) {
            log.info("第一次查询缓存数据不为NULL., 直接返回...");
            return gson.fromJson(cacheValue, Map.class);
        }

        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                cacheValue = jedis.get(cacheKey);
                log.info("第二次查询缓存数据为 -> [{}]", cacheValue);
                if (StringUtils.isNotBlank(cacheValue)) {
                    log.info("第二次查询缓存数据不为NULL., 直接返回...");
                    return gson.fromJson(cacheValue, Map.class);
                }
                log.info("第二次查询缓存数据为NULL, 开始查询数据库...");
                map = db(id);
                cacheValue = gson.toJson(map);
                log.info("查询数据库返回数据放进缓存...");
                jedis.set(cacheKey, cacheValue);
                jedis.expire(cacheKey, 3600);
                return map;
            }
        } catch (Exception e) {
            log.error("Exception is -> [{}]", e.getMessage());
        } finally {
            lock.unlock();
        }

        return null;
    }

    /**
     * Jedis
     */
    private static Jedis jedis() {
        JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 10000 );
        return jedisPool.getResource();
    }

    /**
     * 模拟数据库查询
     */
    private static Map<String, Object> db(Integer id) {
        return DATA.stream().filter(i -> id.equals(i.get("id"))).findAny().orElse(null);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() ->
                System.out.println(query(1))
            ).start();
        }

        //jedis().del("default:prefix:1");
    }


}


15:56:02.918 [Thread-0] INFO com.example.demo.App - 第一次查询缓存数据为 -> [null]
15:56:02.918 [Thread-4] INFO com.example.demo.App - 第一次查询缓存数据为 -> [null]
15:56:02.918 [Thread-3] INFO com.example.demo.App - 第一次查询缓存数据为 -> [null]
15:56:02.918 [Thread-1] INFO com.example.demo.App - 第一次查询缓存数据为 -> [null]
15:56:02.918 [Thread-2] INFO com.example.demo.App - 第一次查询缓存数据为 -> [null]
15:56:02.931 [Thread-0] INFO com.example.demo.App - 第二次查询缓存数据为 -> [null]
15:56:02.931 [Thread-0] INFO com.example.demo.App - 第二次查询缓存数据为NULL, 开始查询数据库...
15:56:02.941 [Thread-0] INFO com.example.demo.App - 查询数据库返回数据放进缓存...
{name=name1, id=1}
15:56:02.963 [Thread-4] INFO com.example.demo.App - 第二次查询缓存数据为 -> [{"name":"name1","id":1}]
15:56:02.963 [Thread-4] INFO com.example.demo.App - 第二次查询缓存数据不为NULL., 直接返回...
{name=name1, id=1}
15:56:02.976 [Thread-2] INFO com.example.demo.App - 第二次查询缓存数据为 -> [{"name":"name1","id":1}]
15:56:02.976 [Thread-2] INFO com.example.demo.App - 第二次查询缓存数据不为NULL., 直接返回...
{name=name1, id=1}
15:56:02.982 [Thread-1] INFO com.example.demo.App - 第二次查询缓存数据为 -> [{"name":"name1","id":1}]
15:56:02.982 [Thread-1] INFO com.example.demo.App - 第二次查询缓存数据不为NULL., 直接返回...
{name=name1, id=1}
15:56:02.988 [Thread-3] INFO com.example.demo.App - 第二次查询缓存数据为 -> [{"name":"name1","id":1}]
15:56:02.988 [Thread-3] INFO com.example.demo.App - 第二次查询缓存数据不为NULL., 直接返回...
{name=name1, id=1}

 

  • 缓存穿透

一般是出现这种情况是因为恶意频繁查询才会对系统造成很大的问题: key缓存并且数据库不存在,

所以每次查询都会查询数据库从而导致数据库崩溃。

解决思路:

从DB中查询出来数据为空,也进行空数据的缓存,避免DB数据为空也每次都进行数据库查询,过期时间设置短一些

使用布隆过滤器,但是会增加一定的复杂度及存在一定的误判率(判断不存在肯定是不存在,判断存在可能会不存在)

布隆过滤器 (guava库 BloomFilter ,RedisBloom  主要是写一份 redis实现布隆过滤器,另外一个百度很多)

# 因为端口号6379已经在使用,改了下端口映射
docker run -d -p 7001:6379 --name redis-redisbloom redislabs/rebloom:latest

<dependency>
    <groupId>com.redislabs</groupId>
    <artifactId>jrebloom</artifactId>
    <version>1.2.0</version>
</dependency>
import io.rebloom.client.Client;
import lombok.extern.slf4j.Slf4j;

/**
 * @ProjectName demo
 * @ClassName App2
 * @Description TODO
 * @Author WangDong
 * @Date 2020/6/3 4:26 下午
 * @Version 1.0.0
 **/
@Slf4j
public class App2 {

    /**
     * 自定义过滤器
     */
    private static final String NAME = "specialBloom";

    /**
     * 初始容量
     */
    private static final long INIT_CAPACITY = 10000;

    /**
     * 错判率
     */
    private static final double ERROR_RATE = 0.0001;

    public static void main(String[] args) {
        Client client = new Client("127.0.0.1", 7001);
        
        //client.delete(NAME);
        
        // 新建一个自定义过滤器
        client.createFilter(NAME, INIT_CAPACITY, ERROR_RATE);

        // 批量添加
        client.addMulti(NAME, "foo", "bar", "baz", "bat", "bag");

        String key = "foo";

        // 判断key是否存在过滤器中 存在 true 不存在false
        boolean result = client.exists(NAME, key);
        log.info("判断 [{}]NAME过滤器中,结果为 -> [{}]", key, result);
    }

}


17:48:00.131 [main] INFO com.example.demo.App2 - 判断key[foo]是否存在 [specialBloom] 过滤器中,结果为 -> [true]

 

  • 缓存雪崩

雪崩指的是多个key查询并且出现高并发,缓存中失效或者查不到,然后都去db查询,从而导致db压力突然飙升,

从而崩溃。出现原因: 1 key同时失效, 2 redis本身崩溃了

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(跟击穿的第一个方案类似,但是这样是避免不了其它key去查数据库,只能减少查询的次数)
  2. 实现redis高可用
  3. 不同的key,设置不同的过期时间,具体值可以根据业务决定,让缓存失效的时间点尽量均匀

 

 

Bloomfilter.js https://www.jasondavies.com/bloomfilter

Redis文档 https://oss.redislabs.com/redisbloom/

Java Client for RedisBloom https://github.com/RedisBloom/JRedisBloom

 

 

©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页