中间件--攻克Redis

前言

如今几乎所有的系统或多或少都使用着缓存,作为缓存界的king-redis;我们应该都很熟悉。这段时间阅读了不少关于redis的书籍,有做一些笔记也有一些自己的思考,分享出来一起交流思考。

Redis的学习思维导图

我基于自己对redis的理解,化了一个redis的学习思维导图。这个张图从redis的原理,数据类型,实际应用,面临的问题,集群的架构,数据的一致性方案,业务架构的演变几个方面来认识缓存Redis。

中间件--攻克Redis_第1张图片

概述

  • 使用C预研编写的,高性能非关系型的键值对数据库
  • 基于内存

数据结构

中间件--攻克Redis_第2张图片

 

 

Redis的命令

String

中间件--攻克Redis_第3张图片

  • set
  • get
  • setnx
  • incr:按1递增
  • incrby key n :按n递增

List

中间件--攻克Redis_第4张图片中间件--攻克Redis_第5张图片

  • rpush:右边推入
  • lpush:左边推入
  • lpop:左弹出元素
  • rpop:右弹出元素
  • lrang:范围取值
  • ltrim:Itrim key  n m   :只保留n到m区间的值
  • llen:获取lis存储值t的长度

Redis底层是一个快速列表,当数据少的时候分配一块连续内存存储,这个是ziplist;当数据比较多的是时候才会改成quicklist。quicklist其实是链表和ziplist结合起来组成的。

hash(字典)

中间件--攻克Redis_第6张图片

  • 相当于Java中的hashMap,是无序的;内部结构和Java中的HashMap是一样的,数组+链表
  • 区别在于redis中的字典的值只能是字符串,rehash扩容方式也不一样,使用的是渐进式式扩容
  • 中间件--攻克Redis_第7张图片
  • rehash会在扩容的时候,保留新旧两个hash结构;查询时会同时查询两个hash结构;
  • 然后再后续的定时任务中以及hash的子指令中,渐进式rehash会将旧的hash内容慢慢迁移到新的hash中
  • 当最后一个元素迁移后,旧的hash被删除。
  • hset map key "2111"
  • hset map key1 "21114"
  • hset map key2"21113"
  • hgetall map
  • hlen map
  • hmset :批量set值

Set(集合)

  • 类比Java中的HashSet, 内部的键值对式无序唯一的。
  • 内部是一个特殊的字典,所有的value都是一个NULL值
  • 当最后一个元素被删除后,数据结构自动删除,内存被回收
  • 可以存储中奖用户ID
  • sadd rq va
  • smembers rq   注意顺序,和插入的并不一致,因为 set 是无序的
  • sismember rq va  # 查询某个 value 是否存在,相当于 contains(o)

ZSet(有序列表)

中间件--攻克Redis_第8张图片

  • 类似Java的Sorted和HashMap的结合体
  • 一方面是一个set保证key的唯一性,另一方面给每个value赋值一个score,代表权重
  • 内部使用一种跳跃列表的数据结构
  • zset最后一个value被移除后,数据结构自动删除,内存被回收
  • zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间

跳跃表

  • zset 内部的排序功能是通过「跳跃列表」数据结构来实现的
  • image.png
  • 中间件--攻克Redis_第9张图片

 

「跳跃列表」之所以「跳跃」,是因为内部的元素可能「身兼数职」,比如上图中间的这个元素,同时处于 L0、L1 和 L2 层,可以快速在不同层次之间进行「跳跃」。定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。你也许会问,那新插入的元素如何才有机会「身兼数职」呢?跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。首先 L0 层肯定是 100% 了,L1 层只有 50% 的概率,L2 层只有 25% 的概率,L3 层只有 12.5% 的概率,一直随机到最顶层 L31 层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。

位图

  • 在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间
  • 位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理
  • 提供了位图统计指令 bitcount 和位图查找指令 bitpos,bitcount 用来统计指定位置范围内 1 的个数,bitpos 用来查找指定范围内出现的第一个 0 或 1。
  • 指定了范围参数[start, end], start 和 end 参数是字节索引,也就是说指定的位范围必须是 8 的倍数,而不能任意指定。

HyperLogLog

  • 不精确统计,比如统计一个网站的UV

Stream

  • Redis5.0提出的
  • Stream的消费模型借鉴了kafka的消费分组的概念,弥补了redis pub/sub不能支持持久化消息的缺陷
  • 中间件--攻克Redis_第10张图片

Redis的事务

  • 使用MULTI和EXEC命令,来保证一个请求在不被其他请求打断的情况下执行多个命令
  • 被MULTI和EXEC包围的命令会一个接一个的执行,直到所有命令执行完毕

Redis的持久化

RDB(快照持久化)

  • 创建快照获得存储在内存里面数据在某个时间点上的副本
  • 在创建快照后,可以对快照进行备份
  • 根据配置文件;快照会被写入dbfilename选项的指定文件里面,并存储在dir选项上的路径上
  • 系统发生崩溃,会丢失最近一次生成快照之后更改的所有数据
  • 只适合即使丢失一部分数据也不会造成问题的应用程序
  • Redis存储数据量大的时候,BGSAVE的执行会导致系统长时间的停顿,致使系统崩溃

AOF(持久化)

  • 将redis写的命令写到AOF的文件末尾,记录数据发生的变化
  • redis只要从头到尾执行一次AOF文件包含的所有写命令,就可以恢复数据
  • 配置appendfsync:
  • 中间件--攻克Redis_第11张图片
  • 文件同步:file.write()  file.fush  sync

重写/压缩AOF文件

  • AOF可以将丢失数据的窗口降低到1秒,甚至不会丢失数据,但是Redis的运行会是AOF的文件体积不断增长,还原的时间也会非常的长
  • BGREWRITEAO命令,移除冗余的写命令来重写AOF文件,减小体积

复制

  • 让其他服务器或者节点拥有一个不断更新的数据副本
  • 主从复制,主节点写操作,从节点读操作,主节点会把更新操作更新给从节点
  • 扩展节点的时候,会接收到主节点的数据初始化副本,之后同上,主节点每次更新都会实时更新到从节点
  •  

分布式锁

锁发展的过程和最终形态

  • setnx的命令,只允许一个客户端访问,用完执行del指令释放,但是如果执行发生异常,会导致del没有执行,导致死锁
  • 基于上面的问题可以给锁加上一个5s,保证中间异常,也可以保证5s后锁自动释放,如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。这个问题在于setnx 和 expire 是两条指令而不是原子指令
  • 在redis2.8后 扩展了set指令参数:set key value ex[seconds] px[millions] nx    这个指令就是setnx 和 expire组合在一起的原子指令

超时问题

  • Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。
  • 这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行
  • Redis 分布式锁不要用于较长时间的任务

可重入性

  • 可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。
  • redis不建议使用可重入锁。如果要实现重入锁的话,客户端要对set命令做计数处理

在集群存在的问题

  • 比如在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生
  • Redlock 算法
  1. 在加锁的时候,会向过半的节点发送 set(key, value, nx=True, ex=xxx) 指令;只要过半节点set成功,就认为枷锁成功
  2. 释放锁的时候 需要向所有节点发送 del 指令

消息队列

适用场景

  • 对于那些只有一组消费者的消息队列,使用 Redis就可以非常轻松的搞定
  • Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

异步消息队列

  1. 实现
  • 使用List来实现
  • 使用rpush/lpush来做入队操作
  • 使用brpop/blpop来做出队操作,b代表blocking阻塞操作,在队列没有数据的时候,会立即进入休眠状态。一旦有数据,就会被唤醒。
  1. 空连接
  • 如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常来。所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。...
  1. 锁冲突
  • 到客户端在处理请求时加锁没加成功怎么办。一般有 3 种策略来处理加锁失败

1、直接抛出异常,通知用户稍后重试;

2、sleep 一会再重试;

3、将请求转移至延时队列,过一会再试;

延时队列的实现

 

  • 通过zset(有序列表)来实现,消息序列化成一个字符串作为zset的value,消息的到期时间作为score
  • 用多个线程轮询zset获取到期的任务

 

Redis集群---主从复制

最终一致性

  • Redis保证最终一致性,最终从节点的状态会和主节点的状态保持一致
  • 如果网络断开,主从节点的数据会出现大量的不一致,一旦网络恢复,从节点会采用多种策略追赶主节点的数据。

主从同步   (快照同步+增量同步(指令同步))

  • 支持主从同步,从从同步
  • 中间件--攻克Redis_第12张图片
  • Redis同步的是指令流,主节点会将那些对自己状态产生修改的指令记录到本地的内存buffer中;然后异步的将buffer中的指令同步到从节点,从节点一边执行同步指令,一边向主节点反馈自己同步到哪里(偏移量)
  • 因为内存的buffer是有限的,所以主节点不能记录所有的指令daobuffer------->Redis的复制内存buffer是一个定长的环型数组,如果数组内容满了,就会从头开始覆盖旧的指令数据

快照同步

  • 首先在主库上进行一次bgsave,将当前的内存数据全部快照到磁盘文件,然后将快照文件的内容全部传送到从节点
  • 从节点接收到全部的快照文件后,执行一次全量加载,加载之前将当前的内存数据清空。加载后通知主节点继续进行同步增量
  • 在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环
  • 中间件--攻克Redis_第13张图片

增加从节点

当从节点刚刚加入到集群时,它必须先要进行一次快照同步,同步完成后再继续进行增量同步

无盘复制

  • 主节点在进行快照同步时,会进行很重的文件 IO 操作,特别是对于非 SSD 磁盘存储时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果发生快照,fsync 将会被推迟执行,这就会严重影响主节点的服务效率。
  • 所谓无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载

wait指令

Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一致性 (不严格)。wait 指令是 Redis3.0 版本以后才出现的。

 

Redis集群---哨兵

哨兵过程

  • 解决的问题:发生故障的时候可以自动的进行主从切换
  • 中间件--攻克Redis_第14张图片
  • 哨兵负责监控主从节点的健康,当主节点挂了后,自动选择一个最优的从节点切换为主节点
  • 哨兵模式下,客户端会连接一个哨兵,通过哨兵来查询主节点的地址返回给客户端
  • 当主节点挂了后,原来的主从复制也断开,其他的从节点会重新和新的主节点建立复制关系
  • 哨兵监控原来的主节点,原来主节点恢复后,会成为新的从节点和现在的主节点进行关联

哨兵的问题

  • 哨兵本身的作用是为了redis集群的高可用,在用户无感知,运维无需介入的情况下,自动完成主从节点的切换
  • 没有解决master写的压力
  • 主从模式,切换需要时间,过程可能会丢失数据
  • Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以限制主从延迟过大。

min-slaves-to-write 1

min-slaves-max-lag 10

第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没有给反馈

Redis集群-RedisCluster(去中心化)

  • 去中心化
  • 至少有三个节点组成
  • 每个节点父祖娥整个集群数据的一部分数据
  • 节点之间通过一种特殊的二进制协议相互交互集群的信息
  • 中间件--攻克Redis_第15张图片
  •  
  • Redis Cluster 将所有数据划分为 16384 的 slots,每个节点负责其中一部分槽位
  • 即使使用哨兵,redis每个实例也是全量存储,每个redis存储的内容都是完整的数据,浪费内存且有木桶效应。为了最大化利用内存,可以采用cluster群集,就是分布式存储。即每台redis存储不同的内容。采用redis-cluster架构正是满足这种分布式存储要求的集群的一种体现。redis-cluster架构中,被设计成共有16384个hash slot。每个master分得一部分slot,其算法为:hash_slot = crc16(key) mod 16384 ,这就找到对应slot。采用hash slot的算法,实际上是解决了redis-cluster架构下,有多个master节点的时候,数据如何分布到这些节点上去。key是可用key,如果有{}则取{}内的作为可用key,否则整个可以是可用key。群集至少需要3主3从,且每个实例使用不同的配置文件。

槽位定位算法

  • Cluster 默认会对 key 值使用 crc32 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。

跳转

  • 当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据

迁移

  • 迁移工具:redis-trib

容错

  • Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其中某个从节点提升为主节点
  • 如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。

Redis的过期策略

过期的key集合

  • redis将每个设置过期时间的key放入到一个独立的字典中(Hash)
  • 之后就是遍历这个字典来删除到期的key

定时扫描策略

  • Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
  • 1、从过期字典中随机 20 个 key; 2、删除这 20 个 key 中已经过期的 key; 3、如果过期的 key 比率超过 1/4,那就重复步骤 1;

从库的过期策略

  • 从库不会进行过期扫描,从库对过期的处理是被动的
  • 主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的key

Redis Ready

缓存穿透(不存在key)

  • 缓存中不存在访问的Key,请求转向了数据库,此时大量的这种请求访问时,会把所有的压力落到数据库(黑客攻击)
  • 解决方法:1.布隆过滤器:在缓存前设立一个校验层,我们可以把在缓存没有查询的数据,数据库也没有数据的key存储在布隆过滤器中,避免对数据库直接访问造成的压力;2.存储空对象:把数据库查不到数据的key存储空对象在redis层,并设置过期时间;但是这样会造成空间浪费,过期时间到了 问题依然会存在。

缓存击穿(热key过期)

  • 缓存穿透是指redis不存这个key,大量的请求压力直接到数据库
  • 缓存击穿通常是指;一个很热的key有效期失效的瞬间,大量的请求直接访问数据库,造成很大的压力
  • 解决方法:
  1. 设置热key永不过期,不够灵活
  2. 使用分布式锁,保证对于每个key的请求只有一个线程去查询后端服务

缓存雪崩(缓存key集体过期,redis宕机)

  • 缓存雪崩,是指在某一个时间段,缓存集中过期失效。(key过期时间相同了,或者redis宕机),大量数据进入存储层,造成存储层压力过大甚至也宕机的情况
  • 解决办法:
  1. 事前:保证redis的集群的高可用,(异地多活,无中心化);把key的时间尽可能的随机设置,避免同时失效,可以进行数据预热,对能够预知的数据在程序启动的时候,先把数据加载到缓存。
  2. 事中:限流降级,在缓存失效后,通过分时锁控制读取数据库的线程数
  3. 事后:利用redis的持久化机制保持的数据恢复到缓存

热key问题

  • 原因:在瞬间大量的请求去访问redis的某个固定的key,造成流量过于集中,达到物理网卡的上线,导致redis宕机
  • 如何发现热key:
  1. 根据业务场景的经验
  2. 在客户端统计key的访问次数
  3. 在redis代理层做key的收集
  4. redis自带的命令,monitor抓取redis的命令,统计热key
  • 解决方法:
  1. 利用二级缓存:比如在应用层使用本地缓存,当发现热key后直接从本地缓存中读取,不需要访问redis
  2. 备份热Key:不要让这个热Key的数据都在同一台redis节点上

小结:处理热key的问题,无非是两步:1.是监控热key   2.通知系统处理;

大key问题

  • 场景:
  1. 热门话题下的评论
  2. 大V的粉丝列表
  • 造成的问题:
  1. Redis主线程是单线程模式,在集群模式下slot分配均匀的情况下,会出现数据和查询倾斜的情况,部分大key的redis节点占用内存多,QPS高
  2. 大key的相关删除和自动过期,会出现QPS突升和突降的情况,极端情况下,会造成主从复制异常,redis服务阻塞无法响应请求
  • 处理方法:
  1. memory usage:通过这个指令统计分析,对大key合理的设置阈值和预警机制
  2. lazy free :在删除的时候只进行逻辑删除,把key的释放操作放到lazyfree的bio线程,避免主线程的阻塞

因为是使用语雀做的笔记迁移过来格式会有一些问题,后面有时间会逐个调整

 

 

 

 

 

 

 

你可能感兴趣的:(中间件,#,应用类,#,思想理论类,分布式,redis,数据库,队列)