分布式缓存(Redis)连杀

  1. 为啥要用缓存
    当然是为了提高性能和并发啊!结合项目中的业务场景回答即可.
    eg.
    出单系统中保存了保单信息.这样接口请求就不用每次去查数据库,减轻了数据库的压力,提高了系统的并发。
    接下来就可能会根据这个情况问你们怎么处理数据不一致等问题了。

  2. 缓存和数据库数据不一致问题
    涉及到双写问题,就会有数据不一致的情况。需要保存数据一致需要牺牲性能,不过实际场景中一般也不要求这么高的一致性。要求严格一致的话,可以将读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。

    1. 缓存模式: Cache Aside Pattern(先淘汰缓存,再写数据库)

      读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应读的时候
      写的时候,先删除缓存,然后再更新数据库

    2. 不一致场景,高并发的情况下出现数据不一致的情况,场景如下:

      1. 有个写请求,此时删除了缓存中的数据,但是还没来得及写数据库;这个时候来了一个读请求,又把数据库中的旧数据load到缓存中去了,然后上一个请求的写数据库操作完成了。这个时候,数据库中的是最新的数据,缓存中的却是旧数据了。
      2. 如果是数据库是主从结构,写主库,读从库,那么由于数据库的数据同延时,也是会造成缓存和数据库的数据不一致的问题。
    3. 解决思路:

      针对第一种情况,可以将数据库与缓存更新与读取操作进行异步串行化;将相同id的读写请求hash到同一台服务处理,服务中使用队列一个一个的执行。如果发现队列中有对应资源的写请求,那么就等待其执行结束后再去取值返回(这里需要考虑等待时间过长的问题,还有该方案影响较大,较复杂)。
      针对第二种场景,我们用补偿的思维来处理,既然是延迟导致的数据不一致,那么根据数据库实际的延迟时间,我们使用定时任务或者消息触发,如果有写请求结束后,我们在指定的时间之后再删除一次该缓存值,这样即使有不一致的脏数据,那也只会出现在延迟的这一段时间中。

    拓展阅读: 缓存架构设计细节二三事

  3. 缓存穿透和雪崩问题
    缓存穿透和雪崩就是缓存不可用之后,请求对数据的请求压力都落在了数据库上,导致数据库无法承受如此高的并发,从而导致数据库不可用影响服务的正常运行.
    出现该问题需要从多个方面处理:

    1. Redis缓存自身的高可用问题,需要架设多主多从模式,避免全面崩溃;开启RDB和AOF备份,在Redis服务出问题时能快速根据备份文件恢复缓存数据
    2. 应用服务自身高可用问题,使用hystrix限流&降级保证服务不被拖垮;同时可以增加一层内存缓存,减少对Redis的依赖。
    3. 还是应用服务层面的,可以过滤部分明显不合理服务的数据请求(eg. 遭遇攻击,所有请求的资源ID都是负数,该资源实际不存在,那么肯定不会命中缓存,就会去数据库查,给数据库造成压力。此时则可以在程序端直接过滤这种情况,或者将其缓存在Redis,只存一个空数据即可)
  4. 缓存并发竞争问题
    问题: 这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了。
    处理思路: 类似JAVA并发处理ABA的问题,给分配一个递增的序列号之类的,序列号在当前值之前的写操作就不执行.

  5. Redis为啥这么快(为啥redis单线程模型也能效率这么高)
    Redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器,是单线程的,redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件,为啥快呢:
    1)纯内存操作
    2)核心是基于非阻塞的IO多路复用机制
    3)单线程反而避免了多线程的频繁上下文切换问题(这个感觉就emm…)
    4)内部数据结构设计

  6. Redis持久化方式及其区别
    RDB: 通过fork一个子进程保存当前内存的一个快照实现备份. 适合大规模的数据恢复,但是数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。另外备份时会占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件.

    # save <指定时间间隔> <执行指定次数更新操作>,满足条件就将内存中的数据同步到硬盘中
    save <seconds> <changes>
    
    # 指定本地数据库文件名,一般采用默认的 dump.rdb
    dbfilename dump.rdb
    
    # 默认开启数据压缩
    rdbcompression yes
    

    AOF: 通过对每条写入命令以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。 AOF对数据数据的完整性和一致性支持更好,但是其备份日志文件一般会比RDB方式的备份文件更大,恢复也更慢,同时由于fsync的频率方式,会影响Redis的性能。

    # 打开aof
    appendonly yes
     
    # 日志文件
    appendfilename "appendonly.aof"
    
    # 更新条件
    # appendfsync always
    appendfsync everysec
    # appendfsync no
    
    # 触发重写的配置
    # 时间长了日志会特别大,此时需要触发重写
    # 重写的原理:Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中。最后替换旧的aof文件。
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    
  7. Redis基本类型及其内在数据结构
    主要有String、List、Hash、Set、zSet五种
    其内在数据结构定义如下:
    redis数据结构

  8. Redis如何实现分布式和高可用(eg. 主从模式的数据复制、hash分槽等)
    Redis Cluster实现了分布式.
    主从结构和数据持久化机制保证了高可用.
    Redis Cluster原理流程:
    hash分槽,
    //todo: 流程实现

    Redis主从模式原理:
    Redis 2.8之前主从模式,主从之间的数据复制只有全复制机制,通过执行命令SLAVEOF ip port,使得其成为从服务器。全数据复制流程如下:
    一. 数据复制

    1. 从服务器向主服务器发送SYNC命令
    2. 主服务器收到SYNC请求后,执行BGSAVE命令在后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的写命令。
    3. 主服务器将生成的RDB文件发送给从服务器,从服务器根据RDB文件更新状态
    4. 主服务器将缓冲区中的写命令发送给从服务器,从服务器执行这些命令,最终和主服务器达到一致的状态

    二. 命令传播
    6. 数据复制完成后,为了保持一致的状态,主服务的写命令都需要传播给其从服务器。

    不足: 上面的方式存在不足,即如果从服务器是和主服务器断开后,又立马重新连接上后。那么此时如果还执行全同步的话,就十分浪费。因为全同步非常占用CPU,IO和带宽等。

    所以Redis 2.8之后,支持了PSYNC,即部分重同步机制

    部分重同步机制主要有依靠:
    主从服务器的复制偏移量: 用于记录主从服务器的状态是否一致,以及数据复制时的偏移量
    主服务器的复制积压缓冲区:固定大小的(默认1M)FIFO队列,用于记录主服务器的写命令
    主服务器的ID: 用于判断,从服务器断开连接后重新连接到的主服务器还是不是原来那个

    部分重同步流程如下:
    主从服务器都有一个复制偏移量,当执行了写命令时,复制偏移量对应会增加。

    1. 从服务器请求同步,带上当前的offset和之前的主服务器ID
    2. 如果请求中的主服务ID和当前的主服务器ID不一致,或者其offset的值已经不再复制积压缓冲区内,那么需要执行全同步,流程同上;
    3. 否则,执行部分同步,主服务器将复制积压缓冲区中offset之后的命令发送给从服务器;
    4. 从服务器执行对应写命令,并修改offset,达到和主服务器状态一致

  9. Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现?
    设置了定时过期的会自动过期,没有设置的如果内存不够则按照最少使用来删除。Redis删除策略如下:

    定期删除: 默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除
    惰性删除:在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西

    但是仅仅这样的话,会有如下问题:
    定期删除时,部分key没有被删除。而且,这部分key也没有访问 。那么这部分数据岂不是就一直在占用内存。这个时候就会走内存删除策略,有如下几种:

    noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
    allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
    allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
    volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
    volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
    volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

    java实现LRU的话:

     public class LRUCache<K, V> extends LinkedHashMap<K, V> {
         private final int         maxSize;
         public LRUCache(int maxSize){
             this(maxSize, 16, 0.75f, false);
         }
    
         public LRUCache(int maxSize, int initialCapacity, float loadFactor, boolean accessOrder){
             super(initialCapacity, loadFactor, accessOrder);
              this.maxSize = maxSize;
         }
          // 最重要就是重写该方法咯,就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
         @Override
         protected boolean removeEldestEntry(Map.Entry eldest) {
             return this.size() >this.maxSize; 
         }
     }
    
  10. 使用Redis遇到过什么问题
    这个问题就需要根据自己项目来实际说一个.比如这边遇到过一个热点问题。一般处理方式:

    • 增加内存缓存,减轻Redis的压力

    • 分散热点数据

    问题发现和处理:老一套方案,首先定位性能瓶颈: CPU/内存/IO等

    1. ps aux | grep appname 查看进程ID

    2. top -H pid查看cpu和内存的使用情况(未发现应用服务器的cpu和内存有较高的使用率)
      cpu繁忙一般可能的情况如下:

      • 线程中的不合理循环
      • 发生了频繁的full gc
      • 多线程的上下文频繁切换
    3. 通过Arthas工具查看接口执行过程中每个方法的耗时情况

      $trace xxx #cost
      方法内部调用路径,并输出方法路径上的每个节点上耗时, trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。trace 能方便的帮助你定位和发现因 RT 高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路

      结果如下:

      +---[0.001162ms] com.paic.ibcsapi.service.dto.ApplyResultDTO:setApplyPolicyNo()
              +---[0.511972ms] com.xxx.service.apply.save.ApplySaveService:fillApplyPolicyNo()
              +---[19.709352ms] com.xxx.service.apply.save.ApplySaveService:saveContractDTOInfo()
              +---[1.313593ms] com.xxx.service.apply.save.ApplySaveService:saveApplyParameterInfo()
              +---[min=4.35E-4ms,max=9.48E-4ms,total=0.002326ms,count=3] java.lang.StringBuilder:<init>()
              +---[min=5.1E-4ms,max=0.00146ms,total=0.004705ms,count=6] java.lang.StringBuilder:append()
              +---[0.001361ms] com.paic.icore.acss.base.dto.BaseInfoDTO:getTransactionNo()
              +---[min=4.86E-4ms,max=6.61E-4ms,total=0.001777ms,count=3] java.lang.StringBuilder:toString()
              +---[523.544075ms] com.xxx.common.service.redis.RedisService:stringSet()
              +---[9.91E-4ms] com.xxx.common.util.StringUtils:isNotBlank()
              +---[min=6.07E-4ms,max=7.75E-4ms,total=0.001382ms,count=2] java.lang.String:equals()
              +---[min=5.03E-4ms,max=6.95E-4ms,total=0.001198ms,count=2] java.lang.Integer:intValue()
              +---[min=592.763104ms,max=868.853641ms,total=1461.616745ms,count=2] com.xxx.common.service.redis.RedisService:ObjectSet()
      
    4. 通过trace 定位到是redis耗时后,定位redis问题

      1. 查看redis服务器状态,发现压测时有台服务器的 查看redis服务器状态,发现压测时有台服务器的CPU达到70%左右
      2. slowlog get 命令获取redis慢查询定位到是某个key的读取导致, 到此发现是redis数据的热点问题
    5. 解决方案

      1. 出现该问题是有工具类滥用redis的问题,每次获取数据都从redis获取。首先,调整代码,增加内存缓存。redis缓存刷新时发布事件动态刷新内存缓存; 然后,优化存储的dto,只缓存必要的数据。减小数据量;最后,替换序列化工具,将jackson序列化替换为fastjson.
      2. 流程中部分耗时的IO操作线程异步后台处理
  11. Redis热点数据问题
    热点数据分类:

    1. 用户消费的数据远大于生产的数据(热卖商品、热点新闻、热点评论、明星直播)。
    2. 请求分片集中,超过单 Server 的性能极限。

    热点数据危害:

    1. 流量集中,达到物理网卡上限。
    2. 请求过多,缓存分片服务被打垮。
    3. DB 击穿,引起业务雪崩。

    解决方案:

    1. 读写分离
    2. 服务内存缓存

    摘抄自热点 Key 问题的发现与解决

推荐阅读《redis设计与实现》

待续

发布了37 篇原创文章 · 获赞 58 · 访问量 16万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览