Skip to content

Latest commit

 

History

History
199 lines (180 loc) · 17.3 KB

redis.md

File metadata and controls

199 lines (180 loc) · 17.3 KB

结构图


  • 单线程Reactor模型:因此需要额外注意调用命令的操作复杂度,避免阻塞Redis

基本数据结构

构成Redis的底层数据结构

  • 字符串:类似于Java中的StringBuilder,内部存放着一个char数组,当前使用量,因此插入前会先扩容处理。
    int lenint freechar buf[]​​

  • 链表:类似于Java中的LinkedList,为双向链表
    每一个节点如下​​listNode *prevlistNode *nextvoid *value​​

  • 字典:实际上是hash表的实现,类似于Java中的HashMap,hash冲突也是使用链地址法解决。
    dictEntry **tablelong sizelong usedlong sizemask​​​

  • 跳跃表:redis是单线程处理请求,因此并发性是不需要考虑的,跳跃表作为有序集合的实现是很容易实现范围查找的。

  • 整数集合:适用于值都是整数,并且元素数量不多的情况下的集合实现方式
    整数集合就是一个数组+长度​int8_t ​contents[]uint32_t lengthuint32_t encoding​​

  • 压缩列表:一般作为列表与hash键的实现方式,节省内存使用。

  • 对外对象:由基本数据结构构成对外使用的对象,使用redisObject进行包裹。

    • 字符串对象(string):使用SDS实现
    • 列表对象(list):使用链表或者压缩列表实现
    • 哈希对象(hash):使用压缩列表或者字典实现
    • 集合对象(set)
      • 整数集合:底层是数组
      • 普通集合:使用压缩列表或者字典实现
    • 有序集合对象(zset):使用压缩列表或者跳跃表实现
    • 位图(Bitmaps):本身是字符串,可以实现位操作,一般理解为一个只存储0和1的数组。
    • HyperLogLog:底层也是字符串,其存在一定误差率,但是极大的节省了内存,如果只为计算独立总数,不需要获取单条数据则可以使用
    • 地理信息定位(GEO):
    • 结构图


  • Key迁移

    • move:针对Redis内部数据迁移,从一个DB转移到另一个DB,不支持多个key
    • dump+restore:先序列化成RDB,然后使用Restore进行RDB复原,整个过程分两个步骤,非原子性,不支持多个key
    • migrate(推荐使用):类似于dump+restore,不过整个流程是原子性操作,支持多个key
  • Key遍历

    • keys:全量遍历,该命令在键值对很多的情况下会造成Redis阻塞。
    • scan:渐进式遍历,每次返回新游标方式遍历Redis,不会造成阻塞
      • hscan:遍历hash
      • sscan:遍历set
      • zscan:遍历zset
  • 扩容:redis本身是哈希表的实现,因此必然会出现扩容的情况

    • rehash:新建一个哈希表h1,把旧的哈希表h0全部迁移到对应的h1当中,在使用h1替换h0
    • 渐进式rehash:当key非常多的时候不能一次性迁移完,每次当操作key时会顺便完成迁移任务,解决了集中式rehash带来的庞大计算量。
      渐进式情况下会出现两个哈希表,因此查需要查两次,h0中查不到则再去h1中查找。另外新添加的只会添加到h1中。​
    • 问题
      • rehash会增大内存,可能触发满容淘汰机制,大量key被淘汰,然后主从同步,导致redis抖动。
        • 美团解决方案:修改源代码,当剩余内存不足时,不触发rehash操作
        • 美团解决方案:做好容量规划,提前分配足够内存。
      • 集群状态下某一个分片可能因为key多,触发rehash,导致当前分片内存分配不均匀。
  • 附加功能

    • 订阅/发布
      在RedisServer中存放的两个属性​​pubsub_channels(订阅字典)pubsub_patterns(模式匹配链表)​当消息触发时先去 订阅字典中找到对应的订阅链,像Client逐一推送,然后再遍历模式匹配链表,像匹配的Client依次推送。​​
    • 事务与Lua:原生事务不支持回滚操作,一般都使用lua脚本代替,lua脚本是原子性执行。
      • eval:eval 脚本内容 key个数 key列表 参数列表
      • evalsha:Redis先预加载lua脚本,然后只要使用对应的sha1签名定位即可。
    • Pipeline:将命令缓存起来,然后一次性发送给RedisServer执行,减少网络耗时。Pipiline非原子性,需要客户端支持。
    • 慢查询:Redis的慢查询记录只是命令的执行时间,不包括排队时间。
      • slowlog-log-slower-than:预设阈值,搞OPS场景下建议1毫秒
      • slowlog-max-len:记录日志最大长度,线上可设置1000以上
    • key过期策略与内存回收
      • 惰性删除:当调用读写命令时回去RedisDb实例中的expires中判断是否已过期,过期则清理key
      • 定期删除:定时任务会全局的遍历RedisDb中的expires,从中随机选择key,判断是否需要清除。
        每次检查会从RedisDb[0] -> RedisDb[16]依次轮询定期删除是使用了ServerCron这一周期性事件循环机制,默认100ms一次。
      • 持久化策略的影响
        • RDB
          生成RDB文件时会检查对应的key,过期的key不会被持久化到RDB文件中。恢复模式下主节点再入RDB文件时也会主动剔除过期的key,从节点会全部载入。
        • AOF
          写入时清除过期key会向AOF文件中添加一条DEL指令。AOF文件重写时会主动过滤过期的key。
        • 主从同步
          复制模式下以主节点为主,只有主节点的操作才会触发删除,从节点的读操作不会删除对应的key过期数据。(3.2版本之后会删除该key),
      • 内存回收:key过期被删除并不代表内存被回收,内存只有在引用计数的值为0时才会被回收。
        • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
        • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
        • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
        • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
        • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
        • no-enviction(驱逐):禁止驱逐数据
  • 持久化

    • RDB:RDB是每隔指定时间生成一次内存快照。
      • 命令:SAVE(阻塞) BGSAVE(后台fork进程进行),当BGSAVE时会拒绝其他的BGSAVE指令。
      • 快照:后台生成rdb之后再原子性的替换当前rdb文件。
      • 载入:Redis启动时自动载入,如果服务器开启了AOF则优先载入AOF文件,因为一般情况下AOF比RDB更精确。
        载入过程中服务器是一直处于阻塞状态
      • 配置:
        • save:save 900 1 在900ms中变化一次。
      • 优点
        • 最大化Redis的性能,备份时再子进程中进行,因此对处理请求的进程没有影响
        • 全量备份,适合容灾,不像AOF记录每一条指令,因此恢复速度快
      • 缺点
        • 因为是按照时间线来保存数据的,因此数据丢失会损失该时间段之内的数据。
        • 本身fork()在数据集比较大的情况下可能耗时一般几毫秒,如果CPU吃紧的话,那么可能长达1秒,该操作会使得当前处理请求的线程阻塞。
    • AOF:AOF是每次针对写命令记录到缓冲区aof_buf,然后再同步到AOF文件中。
      • 命令:
      • 载入:Redis启动时读取AOF文件,执行一遍指令即可
      • 重写:AOF文件随着日积月累必然很大,因此Redis提供重写功能。
        类似RDB,但是把已有的内容以命令形式写入到新的AOF文件中,重写在后台进行,因此Redis还需要提供一个重写时接收的命令缓冲区,当新的AOF文件写入完毕后缓冲区的再次写入,最后用新的AOF文件原子性替换旧的AOF文件​所使用的命令为:​​BGREWEITEAOF
      • 优点
        • 每次记录写指令,在默认情况下最多丢失1秒数据
        • 自动重写机制,在AOF过大时,Redis可以自动重写,再原子性的替换
      • 缺点
        • 体积大,恢复慢
        • 性能略弱与RDB
      • 配置
        • appendfsync
          同步操作是由于操作系统写入缓存的存在。
          • always:将aof_buf中所有的内容写入并同步到AOF文件中。
          • everysec(默认):每一秒将aof_buf中所有的内容写入并同步到AOF文件中。同步操作由一个线程来执行。最多丢失2秒数据。
          • no:将aof_buf的所有内容写入到AOF文件中,但是不同步,交由操作系统同步。
        • redis-check-aof :当aof文件出错,使用该工具修复 redis-check-aof --fix
    • 持久化相关问题
      • fork操作:fork操作对于操作系统来说是一个重量级操作,尤其是针对大内存Redis,高并发下fork操作可能会导致数万条Redis命令延迟。
        • 耗时定位:info stats中latest_fork_usec获取最近一次fork耗时
        • 改善操作
          • 1. 优先使用物理机或者高效支持fork操作的虚拟化技术。
          • 2. 控制Redis实例最大可用内存,fork耗时与内存量成正比。
          • 3. 降低fork操作的频率,放款AOF自动触发时机。
  • 主从
    主从复制建议用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...,当master挂了只要升级第一个Salve1为master即可,其他不要动

    • 复制:当从服务器启动时像主服务器发送sync信号,主服务器开始RDB后台备份,之后主服务器将rdb文件发送给从服务器。
      • 从节点默认只读:对从节点的修改主节点无法感知,因此默认是只读模式。
    • 全量复制:一般用于初次复制场景,会把主节点全部数据一次性发送给从节点。
      全量复制有个过程是发送RDB文件,如果RDB过大导致网络IO被打满,并且Redis默认超时是60秒,就会导致复制失败
    • 增量复制:由于网络抖动引起的主从断开等场景,主节点会补发丢失数据给从节点,使用复制游标控制。
    • 复制计划缓冲区:初次复制时主节点会使用缓冲区缓存进来的写命令,等到复制完成之后再发送给从节点。
    • 复制风暴问题:大量从节点短时间内对同一个主节点发起全量复制,主要是主节点重启时容易发生。
      • 树状复制结构图解决
      • 主节点不要分不到一台机器上
      • 响应时间加长,避免带宽不足导致RDB发送超时。
  • 哨兵 Sentinel

    • 监控:Sentinel定期检测Redis数据节点以及其他Sentinel节点是否可达。
    • 通知:Sentinel节点将故障转移结果通知到应用方
    • 转移:Sentinel将从节点晋升为主节点并维护后续正确的主从关系。
  • 集群:提供分布式数据库方案,提供数据共享,复制,故障转移等功能。
    目前一台机器上通常跑多个实例,然后这些实例的管理就是集群所要做的。

    • Redis cluster:官方提供的集群方案
      • 原理:一致性hash算法
        对于cluster来说,其管理的redis集群分为16384个槽slot,其中每一个集群中的节点node可以选择负责一定范围内的槽。这是一种服务器Sharding技术,当一个key-value请求到集群后,会先计算出其落在哪个槽,然后去改槽中进行操作。​集群中每一个node都是相互知道其他node的,因此客户端呢连接任意一个node都是可以达到访问整个集群的效果。​​​因此客户端连接时往往指定全部节点的ip,然后客户端负责容错处理,对已经下线的节点进行标记,后续请求不请求该节点。​​
      • 动态扩容
        集群增加机器会使得slot重新分配,分配过程中涉及到之前键值对的拷贝。
      • 故障处理
        其推荐每一个node都配置为主从结构,当主master挂掉之后,对应的salve会自动提升为master,也就具备了故障转移功能。
    • Codis
      • 原理
        codis属于一个代理层,可以理解为其背后是一个容量无限制的redis的实例,客户端访问都是通过code_proxy访问,虽然redis没什么问题,但是该proxy本身要做到高可用

      • codis_proxy高可用
        一般起多个,客户端可以配置多个proxy地址,做到高可用。​或者中间增加一层ha​

常见问题

  1. Redis为什么那么快?
    两个原因:1.纯内存操作,每一个命令执行都很快 2.使用Reactor模型,异步非阻塞IO处理,可以使用相当少的资源管理大量请求排队。​​Redis的因为是直接对内存操作,因此每一个命令都是非常快的,并且Redis的单进程实例,其只会用一个线程去处理客户端的请求,那么怎么应对客户端的并发请求呢?答案是串行线程封闭模式,该模式下客户端的并发请求会进去Redis的请求队列中,Redis从队列中依次取出每一个请求,执行完毕后返回客户端。​
  2. flush all之后的处理
    不小心执行了清空数据命令,应该立即执行 shutdown nosave,避免AOF文件重写。​2. 修改AOF文件,删掉flushall指令,重启redis,让redis自行恢复。
  • 分布式锁
    新版的set指令支持原子性的设置key,以及超时时间。
  • MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中都是热点数据? 
    估算20w数据量所占用的内存,然后设置Redis的内存过期淘汰策略为LRU,当Redis内存满了后会自动淘汰对应的数据。
  • Redis内存突然增长的原因?
    https://mp.weixin.qq.com/s/eXKkfhdG8VyS9OmKZOkeEw其中的一种情况是Redis在rehash,rehash过程中Redis会使用数组中的第二个槽,然后rehash结束后替换,因此在这个过程中会新建很多指针引用,这些会占用大量内存,一个6000w规模的key-value可能会多产生2G内存。​​
  • 大量key同时过期为什么可能造成cpu的卡顿?
    定期删除策略是每一轮循环扫描出过期的key大于25%,则再次继续下一轮扫描,那么大量过期key存在的话,该任务会不停的执行,因此就可能造成cpu短期内被占用,也就是卡顿现象。解决方案是使得key的过期时间分布均匀,可以加上随机时间戳。​本质原因Redis是单进程单线程结构,使用事件机制做的多任务处理。​
  • 缓存穿透解决方案:缓存穿透意思是缓存没命中,同时数据库也没命中,因此每次都需要去DB层查询。
    • 缓存空对象:数据库查询不到则生成个空对象放入缓存中,下次查询则走缓存,启到保护DB的作用。
    • 布隆过滤器拦截:其解决的问题是如何判断一个key是否在大量数据的池子中,可以使用redis bitmap实现
  • 缓存雪崩解决方案:指缓存层挂掉,然后请求全部打到了DB层,导致数据库挂掉。
    • 缓存服务做高可用
    • 重要组件限流与隔离:缓慢访问远远好于服务挂掉。
  • 热点缓存重建解决方案:缓存失效后需要重建,但可能由于重建很复杂,导致大量请求打到数据库层。
    热点缓存比如排行榜,推荐榜等数据,并发访问很高,当缓存失效的一瞬间,很可能多个线程同时重建缓存,导致后端服务压力剧增。
    • 互斥锁:缓存重建时加锁,保证只有一个线程在重建缓存。
    • 永不过期:所谓永不过期是缓存层面上不设置过期时间,应用层面上设置过期时间,当线程发现缓存过期之后,启动异步线程去更新数据,然后该线程先返回老数据。
      该方案会导致数据短时间内的不一致性,取决于业务所能容忍的上限。该方案的优势就是能杜绝热点key所带来的问题。
  • Redis CPU饱和怎么处理?
    Redis是单线程架构,因此CPU饱和导致客户端的操作大量超时,Redis几乎不可用状态。此时使用Redis-cli​​ --stat查看该Redis状态,定位到具体原因,如果是qps太大则考虑集群水平分担压力,如果是复制导致则考虑更改一些配置优化。使用 info commandstats可以查看命令执行详细,考虑是不是命令复杂度太高导致。​
  • 处理BigKey?
    bigKey可能造成Redis的阻塞,因为单条命令耗时过长,并且在传输过程中可能造成网络的拥堵。使用redis-cli --bigkeys可以统计bigkey的分布。​使用scan扫描监控​
  • 优秀文章与书籍

参考