redis那点事7: 问题解决篇

231 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

让我们复习一个命令: setnx

     setnx: set一个值, 如果set的值已经存在则返回0, 不存在则会将值设置返回1

     如图:  ​    

需要准备的jar包:

    1. jedis-2.9.0.jar (操作 redis 的 jar包)

    2. commons-pool2-2.6.0.jar (redis 连接池的 jar包)

    3. guava-27.1-jre.jar (布隆过滤器的 jar包)

    这三个jar都能在maven中央仓库找到, 根据配置进行选择版本

    PS: 点击可以传送

 在处理之前先创建一个连接池以及通过setnx设计一个互斥锁

  连接池工具:  

public class JedisPoolTest {

    /**
     * 创建连接池
     */
    private static JedisPool pool = new JedisPool("192.168.146.240", 6379);

    /**
     * 获得jedis实例
     *
     * @return 实例
     */
    public static Jedis getJedis() {
        // 认证
        Jedis jedis = pool.getResource();
        jedis.auth("admin123");
        return jedis;
    }

}

互斥锁实现:

public class RedisLock {

    /*
     *  原理:redis 的setnx如果key存在就会做任何操作  不存在就会set 
     *  根据这一点可以实现一个简单的互斥锁
     *
     *  注意: 锁超时时不能直接del 如果在del之前有其他线程获得了锁, 那么可能造成锁的释放
     */

    /**
     * 锁超时过期时间  200方便看出测试效果
     */
    private static final long EXPIRED = 200;


    /**
     * 尝试获取分布式锁
     *
     * @param jedis jedis连接
     * @param lock  锁名称
     * @return true获得到锁
     */
    public static long tryLock(Jedis jedis, String lock) {
        // 设置锁的过期时间
        Long lockValue = System.currentTimeMillis() + EXPIRED + 1;
        // 尝试获取锁
        Long setnx = jedis.setnx(lock, String.valueOf(lockValue));
        // 判断是否得到锁
        if (setnx == 1) {
            return lockValue;
        } else {
            // 获取锁的值
            Long oldLockValue = Long.valueOf(jedis.get(lock));

            // 判断锁是否超时
            if (oldLockValue < System.currentTimeMillis()) {
                // 将锁赋值 并获取为改变的值
                String getOldLockValue = jedis.getSet(lock, String.valueOf(lockValue));
                // 再进行判断
                if (Long.valueOf(getOldLockValue).equals(oldLockValue)) {
                    // 锁设置成功
                    return lockValue;
                } else {
                    // 其他线程抢到了锁
                    return 0;
                }
            } else {
                return 0;
            }
        }
    }

    /**
     * 释放分布式锁
     *
     * @param jedis   jedis连接
     * @param lock    锁名称
     * @param timeOut 超时时间
     */
    public static void unLock(Jedis jedis, String lock, long timeOut) {
        // 先获取锁
        String lockValue = jedis.get(lock);
        if (lockValue == null) {
            return;
        } else if (Long.valueOf(lockValue).equals(timeOut)) {
            // 删除key
            jedis.del(lock);
        }
    }

}

回顾一下如何解决缓存雪崩?

    方案1: 设置互斥锁 mutex: 单机使用 lock  分布式使用setnx

    方案2: 建立备份缓存, A 和 B, 当 A 过期后, 读取缓存 B 并且查询数据库更新 A 和 B

    方案3: 设置缓存的时候加一个随机时间, 这样可以介绍计提时效的概率, 一定晨程度免了雪崩

    方案4: 使用阻塞队列查询

    这里使用 方案1 、方案3 和 方案4 进行解决

缓存雪崩解决方案 1 (互斥锁):

    优点: 思路简单,  数据一致

    缺点:  复杂度大, 容易死锁

    /**
     * 互斥锁 同缓存击穿策略 查找时策略
     * <p>
     * 优点: 思路简单 保证数据一致性
     * 缺点: 代码复杂度增大  存在死锁概率
     */
    private static String exam1(Jedis jedis, String key) throws InterruptedException {
        // 获取数据
        String s = jedis.get(key);
        if (s == null) {
            // 获得锁
            long lockValue = RedisLock.tryLock(jedis, "lock:snow1");
            if (lockValue != 0) {
                System.out.println("by$ BD");
                // 从数据库查找
                String valueByDB = getValueByDB(key);
                // 插入到redis
                jedis.set(key, valueByDB);
                // 释放锁
                RedisLock.unLock(jedis, "lock:snow1", lockValue);
                return valueByDB;
            } else {
                // 没获得到锁 休眠  已经有其他线程在查询数据了
                Thread.sleep(200);
                return exam2(jedis, key);
            }
        } else {
            System.out.println("by$ Cache");
            return s;
        }
    }

缓存雪崩解决方案 2 (设置随机时间):

    优点: 简单方便

    缺点: 时间不宜维护

  /**
     * 随机时间  添加时策略
     * <p>
     * 优点: 简单方便
     * 缺点: 时间不宜维护
     */
    private static void exam2(Jedis jedis, String key, String value, int seconds) {
        // 设置值
        jedis.set(key, value);
        // 设置过期时间 随机多分配几分钟这样可以防止同时失效
        jedis.expire(key, seconds + (new Random().nextInt(5) * 60));
        // 打印过期时间
        System.out.println("ttl: #" + jedis.ttl(key));
    }

缓存雪崩解决方案 3 (阻塞队列):

    优点: 思路简单

    缺点: 代码复杂度大, 队列处理可能处理不及时

   public static void main(String[] args) {
        // 从连接池获取链接
        Jedis jedis = JedisPoolTest.getJedis();

        // 启动监听MQ
        new Thread(() -> {
            try {
                execMQ();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    exam3(JedisPoolTest.getJedis(), "snow:exam3:" + new Random().nextInt(2));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }



    /**
     * 创建阻塞队列
     */
    private static LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(200);

    /**
     * 阻塞队列的方式  查找时策略
     * 优点: 思路简单
     * 缺点: 阻塞队列 代码复杂度增大
     */
    private static String exam3(Jedis jedis, String key) throws InterruptedException {
        // 获取值
        String value = jedis.get(key);
        // 如果值过期
        if (value == null) {
            queue.put(key);
            Thread.sleep(50);
            return exam3(jedis, key);
        } else {
            System.out.println("by Cache");
            return value;
        }
    }

    /**
     * 监听处理MQ
     */
    private static void execMQ() throws InterruptedException {
        // 从连接池获取jedis实例
        Jedis jedis = JedisPoolTest.getJedis();
        // 监听队列
        for (; ; ) {
            // 获得队列的key
            String key = queue.take();
            // 查看redis是否有数据
            String value = jedis.get(key);
            // 如果有数据证明队列头已经处理完毕
            if (value == null) {
                // 没有数据查询数据库
                jedis.set(key, getValueByDB(key));
                jedis.expire(key, 20);
                System.out.println("queue DB");
            } else {
                System.out.println("queue Cache");
            }
        }
    }

回顾一下如何解决缓存穿透?

    方案1: 使用布隆过滤器, 储存 key 的 bitmap, 如果没有查询到key, 则直接被过滤

    方案2: 当查询数据库为空时, 将空数据也写入redis, 并且设置较短的过期时间

缓存穿透解决策略 1(布隆过滤器): 

    优点: 思路简单, 保证一致性, 性能强

    缺点: 代码复杂度大, 需要维护一个集合来放缓存的key, bloom不支持删操作

    public static void main(String[] args) {
        // 从连接池获取jedis实例
        Jedis jedis = JedisPoolTest.getJedis();

        // 初始化bloom
        bloomInit(jedis);

        exam1(jedis, "a");
        exam1(jedis, "b");
        exam1(jedis, "c");
        exam1(jedis, "d");
        exam1(jedis, "7");
       
    }

    /**
     * 创建bloom过滤器
     */
    private static BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 2000);

    /**
     * 初始化bloom过滤器
     */
    private static void bloomInit(Jedis jedis) {
        // 遍历所有key将所有的数据添加bloom
        ScanParams scanParams = new ScanParams().count(100);
        // 指针
        String cursor = "0";
        do {
            // 遍历
            ScanResult<String> scan = jedis.scan(cursor, scanParams);
            // 获得所有key
            List<String> result = scan.getResult();
            result.forEach(s -> {
                filter.put(s);
            });
            cursor = scan.getStringCursor();
        } while (!"0".equals(cursor));
        System.out.println("boolmFilterCount#: " + filter.approximateElementCount());
    }

    /**
     * 布隆过滤器的巨大用处就是, 能够迅速判断一个元素是否在一个集合中
     * <p>
     * 优点: 思路简单, 保证一致性, 性能强
     * 缺点: 代码复杂度大, 需要维护一个集合来放缓存的key, bloom不支持删操作
     */
    private static String exam1(Jedis jedis, String key) {
        String value = jedis.get(key);
        if (value == null) {
            // 如果不存在查看bloom是否存在
            if (!filter.mightContain(key)) {
                System.out.println("none");
                return "";
            } else {
                // 查询数据库
                String valueByDB = getValueByDB(key, false);
                System.out.println("by DB");
                jedis.set(key, valueByDB);
                return valueByDB;
            }
        } else {
            System.out.println("by Cache");
            return value;
        }
    }

缓存穿透解决策略 2(设置空值):

    优点: 思路简单

    缺点: 需要维护新的key

   /**
     * 设置空值, 如果查询数据库, 没有查到的话设置一个会过期的空值
     * <p>
     * 优点: 思路简单
     * 缺点: 需要维护一个新的key
     */
    private static String exam2(Jedis jedis) {
        // 查询缓存
        String value = jedis.get("null:key1");
        // 缓存没有查询数据库
        if (value == null) {
            // 查询数据库
            String valueByDB = getValueByDB("null:key1");
            System.out.println("by DB");
            // 如果数据库为空
            if (valueByDB == null) {
                valueByDB = "";
            }
            // set一个空值
            jedis.set("null:key1", valueByDB);
            jedis.expire("null:key1", 60 * 2);
            return "";
        } else {
            System.out.println("by Cache");
            return value;
        }
    }

回顾一下如何解决缓存击穿?

    方案1:  设置互斥锁 mutex: 单机使用 lock  分布式使用setnx

    方案2:  异步构建缓存

缓存击穿解决策略 1(互斥锁):

    优点: 思路简单, 保证数据一致性

    缺点: 代码复杂度大, 存在死锁概率


    /**
     * 使用互斥锁
     * 该方法是比较普遍的做法, 在根据key获得的value值为空时, 先锁上, 再从数据库加载, 加载完毕, 释放锁, 若其他线程发现获取锁失败, 则睡眠后重试
     * 至于锁的类型, 单机环境用并发包的Lock类型就行, 集群环境则使用分布式锁 (redis的setnx)
     * <p>
     * 优点: 思路简单 保证数据一致性
     * 缺点: 代码复杂度增大  存在死锁概率
     */
    private static String exam1(Jedis jedis, String key) throws InterruptedException {
        // 获取数据
        String s = jedis.get(key);
        if (s == null) {
            // 获得锁
            long lockValue = RedisLock.tryLock(jedis, "lock:break1");
            if (lockValue != 0) {
                System.out.println("by BD");
                // 从数据库查找
                String valueByDB = getValueByDB(key);
                // 插入到redis
                jedis.set(key, valueByDB);
                // 释放锁
                RedisLock.unLock(jedis, "lock:break1", lockValue);
                return valueByDB;
            } else {
                // 没获得到锁 休眠  已经有其他线程在查询数据了
                Thread.sleep(200);
                return exam1(jedis, key);
            }
        } else {
            System.out.println("by Cache");
            return s;
        }
    }

缓存击穿解决策略 2(构建异步缓存):

    优点: 体验最佳, 无需用户等待

    缺点: 无法保证一致性

    /**
     * 构建线程池
     */
    private static ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    /**
     * 构建一个队列
     */
    private static LinkedBlockingQueue MQqueue = new LinkedBlockingQueue(200);

    /**
     * 处理消息的队列
     */
    private static void execMQ() {
        Jedis jedis = JedisPoolTest.getJedis();
        for (; ; ) {
            try {
                // 无限处理队列, 如果没有阻塞
                String key = (String) MQqueue.take();
                Map<String, String> map = jedis.hgetAll(key);
                if (map.isEmpty() || Long.valueOf(map.get("timeout")) <= System.currentTimeMillis()) {
                    System.out.println("by MQ DB");
                    jedis.hset(key, "value", getValueByDB(key));
                    jedis.hset(key, "timeout", String.valueOf(System.currentTimeMillis() + 2000));
                } else {
                    System.out.println("by Cache");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 异步构建缓存
     * 构建缓存采取异步策略, 会从线程池中取线程来异步构建缓存, 从而不会让所有的请求直接怼到数据库上
     * 该方案redis自己维护一个timeout, 当timeout小于System.currentTimeMillis()时, 则进行缓存更新,否则直接返回value值
     * <p>
     * 优点: 体验最佳, 用户无需等待
     * 缺点: 无法保证缓存一致性
     */
    private static String exam2(Jedis jedis, String key) throws InterruptedException {
        // 获取数据
        Map<String, String> map = jedis.hgetAll(key);
        if (Long.valueOf(map.get("timeout")) <= System.currentTimeMillis()) {
            // 需要更新数据 异步后台执行
            threadPool.execute(() -> {
                MQqueue.add(key);
            });
        } else {
            // 无需更新数据
            System.out.println("by Cache");
        }
        return map.get("value");
    }

结束

  这就是我对redis问题解决篇的总结, 这里没用测试缓存预热和缓存更新, 代码都经过了并发测试, 而且没问题

  如果你们在测试过程中发现有bug 请评论, 相互学习共同进步