• Redis缓存设计和问题处理


    工作中做的所有项目都用到了redis,对其设计思路和问题处理做个总结。

    key设计:可读性高,定义简洁,不包含特殊字符,一般使用:分隔,比如user:info:1000001,表示id为1000001的缓存key

    value设计:字符串不宜过长,字符串最大是512M,一般来说超过10k我们就认为他是bigkey,集合,有序集合,哈希,个数不宜太多,比如存储百万级别的数据。具体大小应该根据读写频率进行评估。bigkey太多容易造成redis阻塞和网络阻塞。过期删除时,如果没有redis4以上的异步删除机制,bigkey也容易造成阻塞。bigkey要注意拆解成多个key-value,如果不可避免,尽量不要用一次性全部取出的命令。key值要控制生命周期,避免存储过多无用的数据。

    命令:禁用keys,fslushall,flushdb等危险命令,适量使用批量命令可以提高效率,O(n)复杂度的命令要关注n的个数。部分遍历的需求可以使用scan相关的命令渐进式遍历。

    什么是缓存击穿?redis键值对集中失效,导致大量的请求到存储层,容易引起存储层负载过高导致故障。比如电商网站可能同时商家多个热卖商品,活动时间两小时,两小时之后就会集中失效。解决办法就是将key值基于某个基数随机设置过期时间,避免集中失效。类似下面的伪代码:

    public String get(String key) {
        // 从缓存中取数据
        String cacheData = cache.get(key);
        if (StringUtils.isNotBlank(cacheData)) {
             // 缓存非空,返回数据
             return cacheData;
        } 
        // 缓存为空,从存储层获取数据
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 随机设置过期时间
        int expireTime = new Random().nextInt(500)  + 500;
        return storageValue;     
    }

    什么是缓存穿透?一个不存在的数据,缓存和数据库都查不到,导致每次都会到数据库查询,失去了缓存的意义。这种一般是代码逻辑出问题或者被恶意攻击导致的。

    最直接的解决办法是缓存空数据且设置过期时间,这样还是可以在缓存层拦截下来,缺点时如果缓存了很多空对象,key-value会占用较多redis的空间。

    还有一种思路,使用布隆过滤器来解决,这是一种去重的思想,并不属于redis,redisson客户端进行比较好的实现,可以拿来使用。可以将布隆过滤器理解为一个非常大的bit数组,初始都为0,将key进行多次hash散列之后,放到指定的位置标记为1。具体判定方法为:如果过滤器中存在,这个key实际并不一定存在。如果过滤器中不存在,这个key一定不存在。缺点是需要提前构建过滤器,会有一定的误差率,不能删除,新增key时要在布隆过滤器中设置。

    什么是缓存雪崩?缓存层顶住了绝大多数请求,但是由于种种原因,比如宕机、缓存设计不够好、超大并发导致缓存扛不住等问题,大量的请求直接流向存储层,容易造成存储层故障。  

    1)缓存设计要合理,减少bigkey的出现,对业务和数据量提前进行预估。
    2)应该保证缓存层的高可用,现在更多的使用集群模式。
    3)限流、熔断、降级,并不是服务器能顶住一百万的请求,就随时让请求全部进来,限流处理还是很有必要的,要留有一定的空间。如果顶不住流量的压力对于非核心数据可以降级返回,对于核心数据仍然可以从缓存以及数据库中获取

    缓存和数据库不一致?并发条件下,读写缓存和读写数据库的执行过程是不可预估的,多个线程执行的顺序是外部很难看清楚,可能会出现缓存数据库不一致问题。比如A线程写入数据库,然后B线程此时写入数据库且写入缓存,由于各种原因,比如网络等,即使A线程先执行业务,也有可能会后写入缓存,这就可能出现不一致。

    如果问题并发量很小,就很难出现上述问题,或者缓存和数据库可以容忍出现不一致情况,我们给缓存设置过期时间就行,让缓存隔一段时间刷新一次数据。如果我们对于缓存和数据库的一致性有严格的要求,可以加读写锁来实现,读读相当于无锁,效率还是比较高的。引用外部的工具也可以处理,比如使用阿里开源的一款工具:canal,监听数据库binlog近实时更新缓存,且和我们的业务代码解耦,缺点就是系统引入了新的中间件,会增加系统复杂性。

    对热点key重建时的优化:在某些场景下,一些缓存的访问量极大,比如微博热搜、头条热点此类,缓存失效时,会涌入大量的请求尝试将数据写入缓存,建缓存可能设计很多复杂的逻辑,比如多次查询数据库、多次IO等,不能短时间内完成。如果每个请求都做这些操作,会给服务端应用造成压力。对于这种情况,我们可以使用互斥锁,可以参考redis分布式锁,同一时间只有一个线程重建缓存,其他线程只需要等待重建完成从缓存查询数据即可。看下面的伪代码:

    String get(String key) {
        // 从缓存中获取数据
        String cacheValue = redis.get(key);
        // 如果cacheValue为空, 重构缓存
        if (StringUtil.isBlank(cacheValue)) {
            // 推荐使用redisson的分布式锁
            if (redisson.tryLock(lockKey)) {
                cacheValue = storage.get(key);
                // 写入缓存
                redis.setex(key, timeout, cacheValue);
                // 释放锁
                redisson.unlock();
            }else {
           // 没有拿到所的线程,休眠一段时间直接再尝试从缓存中读取数据
                Thread.sleep(50);
                get(key);
            }
        }
        return cacheValue;
    }

    对于并发量很高的系统,而且对应用的稳定性有要求的系统,上面的很多情况可能都要思考在内。 

  • 相关阅读:
    字符串Hash 学习笔记
    P4315 月下“毛景树” 题解
    page
    Equation
    Graph
    配置UOJ数据的正确姿势
    luogu2261余数求和题解--整除分块
    luogu2858奶牛零食题解--区间DP
    luogu1005矩阵取数游戏题解--区间DP
    luogu4677山区建小学题解--区间DP
  • 原文地址:https://www.cnblogs.com/dlcode/p/14037960.html
Copyright © 2020-2023  润新知