redis可以满足很多的应用场景,而且因为将所有数据都放到内存中,所以它的读写性能很好,很多公司都在使用redis。redis给我们带来便利的同时,使用过程中会存在什么问题呢,本文将简单加以总结。

  • 阻塞问题
    redis使用了单线程来处理请求,为什么单线程可以支持如此高的并发呢?主要有如下几点:
  1. 纯内存访问:将所有数据都放到内存中,内存响应时间为100纳秒,是redis达到每秒万级别访问的重要基础
  2. 非阻塞IO:redis使用epoll作为I/O多路复用技术,redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多时间
  3. 单线程:避免了线程切换和竞态产生的消耗,简化了数据结构和算法的实现
    因此如果某个命令执行时间过长,会造成其他命令阻塞,对redis来说是致命的
产生阻塞的场景:
A. API或数据结构使用不合理
   a. 避免使用某些易造成阻塞的命令如:keys sort hgetall smembers
      执行showlog get [n] 可以获取最近n条执行慢的记录,对于执行超过一定时间
      (默认10ms,线上建议设置为1ms)的命令都会记录到一个定长队列(默认128,可调整)中。              
   b. 防止一次操作获取过多数据:缩减大对象或者把大对象拆分为多个小对象
      发现大对象的命令:redis-cli -h{ip} -p{port} bigkeys
      内部原理:采用分段进行scan操作,把历史扫描过的大对象统计出来
   c. 防止大量key同时过期:如果有很多key在同一秒内过期,超过了所有key的25%,redis主线程就会阻塞直到过期key比例下降到25%以内,
      因此要避免同一时间过期大量key,过期时间可做散列处理。
      redis4.0引入的lazyfree机制可以避免del、flushdb、flushall、rename等命令引起的redis-server阻塞,提高服务稳定性。
     
B. CPU饱和
   单线程的redis处理命令时只能使用一个CPU,CPU饱和是指redis把单核的CPU使用率跑到接近100%。
   首先要确定redis的并发量是否达到极限,通过redis-cli-h{ip} -p{port}--stat 获取redis当前使用情况。
   如果达到每秒6w+左右的qps,说明单台已跑到极限,需要水平扩展。
   如果qps只有几百或者几千CPU就已经饱和,可能使用了高算法复杂度的命令或者是对内存的过度优化
   (如放宽了ziplist的使用条件,虽然使用的内存会变少,但是更耗CPU)。
      
C. 持久化操作
   持久化引起主线程的阻塞操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞
   a. fork阻塞
      发生在RDB和AOF重写时,redis主线程调用fork操作产生共享内存的子线程,由子线程完成持久化文件的重写工作,若fork操作耗时过长会引起阻塞。
      避免使用内存过大的实例。              
   b. AOF刷盘阻塞
      开启AOF持久化功能时,一般会采用1次/s的刷盘方式,后台线程每秒对AOF文件做fsync操作,当硬盘压力过大时fsync操作需要等待直到写入完成。
      如果主线程距离上一次的fsync成功超过2s,为了数据安全会阻塞直到后台线程执行完fsync完成。这种阻塞是由于磁盘压力引起。
      尽量独立部署
   c. HugePage写操作阻塞
      子进程在执行重写期间利用linux的copyonwrite机制,会拖慢写操作的执行时间,导致大量写操作慢查询。
      优化linux配置
  • 缓存穿透
    缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不命中,且不将空结果写到缓存中。
    会导致后端存储负载变大,造成后端存储宕机等问题。可以在程序中分别统计总调用数、缓存命中数、存储命中数,若有大量存储层空命中,可能是出现了缓存穿透。
    产生原因:1.自身代码或数据出现问题 2.恶意攻击,爬虫造成空命中
    如何解决:
  1. 缓存空对象
    存储层不命中,扔将空对象保存到缓存层。
    适用场景:数据频繁变化、实时性高
    带来问题:
    a.缓存了空值,会占用内存空间;可以设置较短过期时间,自动剔除。
    b.数据不一致,若存储层添加了此数据,有短暂不一致;可主动清除掉缓存的空对象。
  2. 布隆过滤器
    在访问缓存层和数据层之前将存在的key用布隆过滤器提前保存起来,做第一层拦截。
    适用场景:大用户集,实时性要求较低的场景,如有几亿的数据集,每隔一段时间会新增用户进去,在更新之前新用户的访问会存在缓存穿透问题。
    缺点:代码维护复杂  
     

redis使用中存在的问题及如何避免(一)阐述了redis的阻塞问题及缓存穿透问题,本文将继续总结redis在使用中的问题及方案。

  • 无底洞问题
    随着数据量和访问量的增长,需要增加更多的节点做水平扩容,键值会分布到更多的节点上,若客户端进行批量操作则通常会从不同的节点上获取数据,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络交互。
    随着节点数的增多,客户端一次批量操作涉及的网络交互耗时也会不断增大;网络连接数增多,对节点性能也有一定影响。
    更多的节点不代表更高的性能,这就是无底洞问题。
  • 雪崩问题
    由于缓存层承载着大量请求,有效的保护了存储层,但如果缓存层由于某些原因不能提供服务,所有请求都会压到存储层,存储层流量暴增,导致存储层也会级联宕机。

    1. 保证缓存层服务高可用性
      Redis Sentinel或者Redis Cluster都实现了高可用
    2. 隔离限流降级
      对重要的资源Redis、Mysql、外部接口调用都进行隔离,机器、进程、线程等层面都可做隔离。
      可使用漏桶、令牌桶等方式进行限流操作,将流量挡在应用上层。
      对出现问题的数据或功能做降级处理,友好的展示给用户。
    3. 提前演练测试
  • 热点key重建优化
    缓存+过期时间策略即可以加速数据读写,又保证数据的定期更新,若出现如下两个问题,可能会对应用产生致命危害:

    1. 当前key是一个热点key,并发量非常大
    2. 重建缓存不能在短时间内完成,如:复杂的sql、多次IO、多个依赖等。
      在缓存失效的瞬间,有大量的线程来创建缓存,造成后端负载加大,甚至导致系统崩溃。
      方案:

      a.互斥锁
        只允许有一个线程去重建数据,其他线程等待构建完缓存,重新从缓存中获取数据。
      b.永远不过期
        设置逻辑过期时间,判断逻辑时间和当前时间大小,然后异步去构建数据覆盖老数据。
        
      a方案思路简单,能保证一致性;但代码复杂度增大,存在死锁风险,存在线程池阻塞风险。
      b方案基本可以杜绝热点key问题;但不保证一致性,逻辑过期时间增加代码维护成本。