清水泥沙

Redis(底层和应用)

参考

https://mp.weixin.qq.com/s/_W9ny6l3-JqQ_SWm9ACtqg

Redis的数据类型

图片

String

  • 用途
    1. 简单的key-value储存
    2. setnx key value 配合set ex 实现分布式锁
    3. 计数器(原子性)
    4. 分布式全局唯一ID
  • 底层
    • C语言中的String用char[]数组表示,源码中的用SDS封装char数组,这是Redis最少的存储单元,一个SDS可以最大存储512兆信息
    • Redis对SDS再次封装生成了RedisObject,主要有两个核心作用
      • 声明存储的是那种类型的数据
      • 存储指向SDS的指针
    • 当你执行set key value 时,其实redis会创建两个RedisObject 对象,一个是key的redisObject,一个是value的RedisObject,并且指定type为REDIS_STRING,而SDS分别存储key的值,和value的值。
    • Redis底层对SDS进行了优化
      • SDS修改后大小>1M时,系统会进行一个空间预分配
      • SDS是惰性释放空间的,不会马上释放内存,下次进行写操作时,会利用已开辟空间,不会重新申请

List

图片

list的底层是一个双向链表,最大长度为2^32-1。常用的组合有

  • lpush+lpop=stack 先进后出的栈
  • lpush+rpop=queue 先进先出的队列
  • lpush+ltrim =capped collection 有限集合
  • lpush+brpop = message queue 消息队列

一般可以用来做一些简单的消息队列,数据量小的的时候可以用独有的压缩队列来提升性能

Hash

hash适合将一些关联的聚合数据放在一起,比如用户信息,用户的购物车等一些数据。

hash的底层是一个字典集合,整体是层层封装的。从下到上的层级顺序为

  • dictEntry
    • 这是真正存储数据的节点,包含key-value和next节点,是一个链表节点
    • 图片
  • dictht
    • 一个dictEntry类型的数组
    • 数组的长度size
    • sizemask等于size-1
    • 当前数组中含多少个节点
    • 图片
  • dict
    • dictType 类型,包括一些自定义函数,这些函数使得 key 和 value 能够存储
    • rehashidx 其实是一个标志量,如果为-1说明当前没有扩容,如果不为 -1 则记录扩容位置;
    • dictht数组,两个Hash表;
    • iterators 记录了当前字典正在进行中的迭代器。
  • 整体结构图片
  • 渐进式扩容
    • dictht为何存在两个?

      • 目的是为了在不影响进行写操作的时候,慢慢从第一个数组转移到第二个数组,同时使用rehashindex来记录转移情况,当全部转移完成,将第二个数组转为第一个数组进行使用
      • rehashidx = -1 说明当前没有扩容,rehashidx != -1 则表示扩容到数组中的第几个了。
      • 扩容之后的数组大小为大于 used*2 的 2 的 n 次方的最小值,跟 HashMap 类似。然后挨个遍历数组同时调整 rehashidx 的值,对每个 dictEntry[i] 再挨个遍历链表将数据 Hash 后重新映射到 dictht[1]里面。并且 dictht[0].use 跟 dictht[1].use 是动态变化的
      • 整个过程的重点在于 rehashidx,其为第一个数组正在移动的下标位置,如果当前内存不够,或者操作系统繁忙,扩容的过程可以随时停止。
      • 停止之后如果对该对象进行操作,那是什么样子的呢?
      1. 如果是新增,则直接新增后第二个数组,因为如果新增到第一个数组,以后还是要移过来,没必要浪费时间
      2. 如果是删除,更新,查询,则先查找第一个数组,如果没找到,则再查询第二个数组。图片

Set

set是string的无序排列。

  • 应用场景
    • 特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;

ZSet

看底层 redis.h 后就会发现 Zset 用的就是可以跟二叉树媲美的跳跃表来实现有序。跳表就是多层链表的结合体,跳表分为许多层(level),每一层都可以看作是数据的索引,这些索引的意义就是加快跳表查找数据速度。

每一层的数据都是有序的,上一层数据是下一层数据的子集,并且第一层(level 1)包含了全部的数据;层次越高,跳跃性越大,包含的数据越少。并且随便插入一个数据该数据是否会是跳表索引完全随机的跟玩骰子一样。

跳表包含一个表头,它查找数据时,是从上往下,从左往右进行查找。现在找出值为 37 的节点为例,来对比说明跳表和普遍的链表。

没有跳表查询 比如我查询数据 37,如果没有上面的索引时候路线如下图:

图片

有跳表查询 有跳表查询 37 的时候路线如下图:

图片

应用场景

  • 积分排行榜、时间排序新闻、延时队列

布隆过滤器

当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点(有效降低冲突概率),把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就知道集合中有没有它了:如果这些点有任何一个为 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的基本思想。

简单地说经过布隆过滤器检验的数据,如果不存在那他一定不存在,如果说他存在,他也有可能不存在。

可以参考下方文章

https://www.cnblogs.com/ysocean/p/12594982.html

发布订阅

redis具备发布订阅功能,订阅机制是指。生产者和消费者不进行直接通讯。而是通过某个频道进行生产,而消费者对频道进行订阅进行消费。

图片

持久化

redis数据都是存储在内存当中的,为了容灾,Redis提供了RDB和AOF两种持久化模式

RDB

RDB是一种对数据进行周期化的备份的持久机制

  • 优点
    • 压缩后的二进制文件适用于备份,全量复制,恢复速度比AOF更快
    • 如果对数据的一致和完整性要求不高的话,RDB更优于AOF
  • 缺点
    • 备份的数据完整性不高,恢复备份后可能存在数据丢失
    • 在执行备份时会拉起一个子进程,将数据写入到一个临时文件(占用内存暴涨),最后将临时文件替换成之前的备份文件。

手动执行备份时需要注意

  • SAVE会直接调用rdbSave会阻塞主进程,导致无法提供服务
  • BGSAVE会拉起一个子进程,子进程会调用rdbSave,在保存完成后对主进程发送完成信号,期间主进程仍然可用
  • Copy On Write 机制,备份的是开始那个时刻内存中的数据,只复制被修改内存页数据,不是全部内存数据。
  • Copy On Write 时如果父子进程大量写操作会导致分页错误。

AOF

AOF机制会讲每一个执行的指令以追加的方式写到日志文件当中,因为只是对日志进行追加所以写入速度很快。

  • 优点
    • AOF每秒通过一个后台线程对日志进行写入,数据不容易丢失
  • 缺点
    • 相对于RDB备份文件来说,AOF日志要大于RDB,恢复数据时较慢
    • 每秒都需要线程进行写入,相对运行效率来说低于RDB

AOF写入流程

  1. 命令首先追加到aof_buf然后再同步到IO磁盘,如果实时写入会影响整体性能
  2. 第二步是对 aof 文件的重写,目的是为了减少 AOF 文件的大小,可以自动触发或者手动触发(BGREWRITEAOF),是 Fork 出子进程操作,期间 Redis 服务仍可用。图片
  3. 在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;它依然会写入旧的 AOF 中,如果重写失败,能够保证数据不丢失。
  4. 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个 buf,防止新写的 file 丢失数据。
  5. 重写是直接把当前内存的数据生成对应命令,并不需要读取老的 AOF 文件进行分析、命令合并。
  6. 无论是 RDB 还是 AOF 都是先写入一个临时文件,然后通过 rename 完成文件的替换工作

备份建议

  • 降低fork频率,可以改为手动触发
  • 控制redis最大内存,避免备份数据过大,耗时过长
  • 在备份时尽量选择写入较少时段

恢复

重启Redis时,会检查AOF文件是否存在,如果不存在尝试加载RDB

图片

恢复建议

既然单独用 RDB 会丢失很多数据。单独用 AOF,数据恢复没 RDB 来的快,所以出现问题了第一时间用 RDB 恢复,然后 AOF 做数据补全才说王道。

redis为什么那么快

基于内存实现

所有数据存储在内存当中,比IO操作快百倍。

高效的数据结构

Redis底层多种数据结构支持不同的数据类型

丰富合理的编码

Redis底层会自动适配数据类型的编码和长度

  • string :自动存储int类型,非int使用raw编码
  • list:字符串长度小于一定个数会使用ziplist编码,否则转为linkedlist
  • hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对。
  • Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码。
  • Zset:保存的元素个数小于定值且成员长度小于定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。

合理的线程模型

使用多路IO复用模型进行客户端监听,单线程,不需要切换上下文,没有锁开销

图片

缓存雪崩

缓存雪崩是指,同同一个时间内,redis的部分键过期失效,所有用户请求直接打到了数据库上,数据库承受不了流量压力导致程序崩溃

解决方案

  • 分散数据缓存的失效时间,防止同一时间大面积失效
  • 如果缓存数据库是分布式,将热点数据分布在不同的缓存数据中
  • 设置热点数据永不过时,惰性更新

缓存穿透

缓存穿透是指,用户对不存在的数据进行访问,直接绕过了缓存层到达数据库,这样的请求过多导致数据库压力过大。

解决方案

  • 接口增加检验,对用户进行鉴权或者参数校验
  • 使用nginx对IP进行频率限制,超过阈值对IP进行拉黑
  • 在读取数据库找不到数据时,缓存一个null并且过期失效
  • 使用布隆过滤器

缓存击穿

大并发对某一个热点key进行访问,当这个key在失效的巡检,涌进大量的请求到达数据库

解决方法

  • 讲热点数据设置为永不过时,在流量降低时惰性更新。

双写一致性

  • 双写:缓存和数据库均更新数据,如何保持数据一致性?

    • 先更新数据库,再更新缓存
      • 安全问题:线程 A 更新数据库->线程 B 更新数据库->线程 B 更新缓存->线程 A 更新缓存。导致脏读。
      • 做成一个原子操作,数据库事务执行对更新数据进行加锁,更新缓存后,提交事务
    • 业务场景
      • 读多写少场景,频繁更新数据库而缓存根本没用。更何况如果缓存是叠加计算后结果更浪费性能。
  • 先删缓存,再更新数据库

    *

    A 请求写来更新缓存。

    B 发现缓存不在去数据查询旧值后写入缓存。

    A 将数据写入数据库,此时缓存跟数据库不一致。

    因此 FackBook 提出了 Cache Aside Pattern

    失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

    命中:应用程序从 cache 中取数据,取到后返回。

    更新:先把数据存到数据库中,成功后,再让缓存失效。

脑裂

脑裂是在集群中,master节点和slave节点因为通信问题断开连接。集群自动将slave某节点上升为master节点,这时候集群中就同时存在了两个master节点。

  • 造成问题
    • 当集群通讯恢复后,失去通信后上升为master节点的slave降级后,原来的master节点升级同步数据时,就会丢失这一段时间内写入的数据。
  • 解决方法
    • Redis 处理方案是 redis 的配置文件中存在两个参数
    • min-replicas-to-write 3 表示连接到master的最少slave数量
    • min-replicas-max-lag 10 表示slave连接到master的最大延迟时间
    • 如果master的salve数量小于第一个参数并且ping延迟时间<=第二个参数,那么master就会拒绝写入,从而减少数据的丢失

事务

Redis中的事务只分三个步骤

图片

  • redis的事务只是一次性,顺序性,排他性滴执行命令队列的一系列命令
  • redis事务不具备隔离概念
  • redis事务不具备原子性,不可以执行回滚,只会检测命令队列是否有语法错误
  • 可以使用乐观锁机制,检测某些key,如果在事务过程中发生改变,不执行命令队列

redis分布式锁

redis原理比较简单,redis是一个单线程处理器,具备互斥特性,通过setNx,exist 等命令就可以完成简单的分布式锁,处理好超时释放锁的逻辑即可。

SETNX

SETNX 是SET if Not eXists的简写,日常指令是SETNX key value,如果 key 不存在则set成功返回 1,如果这个key已经存在了返回0。

SETEX

SETEX key seconds value 表达的意思是 将值 value 关联到 key ,并将 key 的生存时间设为多少秒。如果 key 已经存在,setex 命令将覆写旧值。并且 setex 是一个原子性(atomic)操作。

加锁:

一般就是用一个标识唯一性的字符串比如 UUID 配合 SETNX 实现加锁。

解锁:

这里用到了 LUA 脚本,LUA 可以保证是原子性的,思路就是判断一下 Key 和入参是否相等,是的话就删除,返回成功 1,0 就是失败。

缺点:

这个锁是无法重入的,且自己实心的话各种边边角角都要考虑到,所以了解个大致思路流程即可,工程化还是用开源工具包就行。

Redis过期策略

定时过期

给设置了过期时间的key设置一定定时器,到期后立即对key进行删除

  • 优点
    • 节省内存空间
  • 缺点
    • 需要占用大量cpu资源处理过期数据

惰性过期

只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。

  • 优点
    • 可以最大化地节省 CPU 资源。
  • 缺点
    • 却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。

定时删除

每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中 key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。

内存淘汰策略

Redis 的内存淘汰策略是指在 Redis 的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

  • volatile-ttk:从设置了过期时间的数据集合中挑选即将过期的淘汰
  • volatile-random从设置了过期时间的数据集合随机淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据,不删除的意思。

redis集群高可用

单机问题有机器故障、容量瓶颈、QPS 瓶颈。在实际应用中,Redis 的多机部署时候会涉及到 redis 主从复制、Sentinel 哨兵模式、Redis Cluster。

模式 优点 缺点
单机版 架构简单,部署方便 机器故障、容量瓶颈、QPS 瓶颈
主从复制 高可靠性,读写分离 故障恢复复杂,主库的写跟存受单机限制
Sentinel 哨兵 集群部署简单,HA 原理繁琐,slave 存在资源浪费,不能解决读写分离问题
Redis Cluster 数据动态存储solt,可扩展,高可用 客户端动态感知后端变更,批量操作支持查

主从复制

该模式下 具有高可用性且读写分离, 会采用 增量同步全量同步 两种机制。

全量同步

图片

Redis 全量复制一般发生在 Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份

  • slave 连接 master,发送 psync 命令。
  • master 接收到 psync 命名后,开始执行 bgsave 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令。
  • master 发送快照文件到 slave,并在发送期间继续记录被执行的写命令。
  • slave 收到快照文件后丢弃所有旧数据,载入收到的快照。
  • master 快照发送完毕后开始向slave发送缓冲区中的写命令。
  • slave 完成对快照的载入,开始接收命令请求,并执行来自 master 缓冲区的写命令。

增量同步

也叫指令同步,就是从库重放在主库中进行的指令。Redis 会把指令存放在一个环形队列当中,因为内存容量有限,如果备机一直起不来,不可能把所有的内存都去存指令,也就是说,如果备机一直未同步,指令可能会被覆盖掉。

Redis 增量复制是指 Slave 初始化后开始正常工作时 master 发生的写操作同步到 slave 的过程。增量复制的过程主要是 master 每执行一个写命令就会向 slave 发送相同的写命令。

Redis 主从同步策略:

  • 主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
  • slave 在同步 master 数据时候,如果 slave 丢失连接不用怕,slave 在重新连接之后丢失重补。
  • 般通过主从来实现读写分离,但是如果 master 挂掉后如何保证 Redis 的 HA 呢?引入 Sentinel 进行 master 的选择。

高可用之哨兵模式

图片

Redis-sentinel 本身是一个独立运行的进程,一般 sentinel 集群节点数至少三个且奇数个。它能监控多个 master-slave 集群,sentinel 节点发现 master 宕机后能进行自动切换。Sentinel 可以监视任意多个主服务器以及主服务器属下的从服务器,并在被监视的主服务器下线时,自动执行故障转移操作。这里需注意 sentinel 也有 single-point-of-failure 问题。

大致罗列下哨兵用途:

集群监控:循环监控 master 跟 slave 节点。

消息通知:当它发现有 redis 实例有故障的话,就会发送消息给管理员

故障转移:这里分为主观下线(单独一个哨兵发现 master 故障了)。客观下线(多个哨兵进行抉择发现达到 quorum 数时候开始进行切换)。

配置中心:如果发生了故障转移,它会通知将 master 的新地址写在配置中心告诉客户端。

Redis Cluster

RedisCluster 是 Redis 的分布式解决方案,在 3.0 版本后推出的方案,有效地解决了 Redis 分布式的需求。

图片

分区规则

图片

常见的分区规则

  • 节点取余:hash(key) % N
  • 一致性哈希:一致性哈希环
  • 虚拟槽哈希:CRC16[key] & 16383

RedisCluster 采用了虚拟槽分区方式,具题的实现细节如下:

  • 采用去中心化的思想,它使用虚拟槽 solt 分区覆盖到所有节点上,取数据一样的流程,节点之间使用轻量协议通信 Gossip 来减少带宽占用所以性能很高,
  • 自动实现负载均衡与高可用,自动实现 failover 并且支持动态扩展,官方已经玩到可以 1000 个节点 实现的复杂度低。
  • 每个 Master 也需要配置主从,并且内部也是采用哨兵模式,如果有半数节点发现某个异常节点会共同决定更改异常节点的状态。
  • 如果集群中的 master 没有 slave 节点,则 master 挂掉后整个集群就会进入 fail 状态,因为集群的 slot 映射不完整。如果集群超过半数以上的 master 挂掉,集群都会进入 fail 状态。
  • 官方推荐 集群部署至少要 3 台以上的 master 节点。

redis限流

经常乘坐北京西二旗地铁或者在北京西站乘坐的时候经常会遇到一种情况就是如果人很多,地铁的工作人员拿个小牌前面一档让你等会儿再检票,这就是实际生活应对人流量巨大的措施。

在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。那么何为限流呢?顾名思义,限流就是限制流量,就像你宽带包了 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 qps,从而达到保护系统的目的。

基于redis的setnx,zset

setnx

比如我们需要在 10 秒内限定 20 个请求,那么我们在 setnx 的时候可以设置过期时间 10,当请求的 setnx 数量达到 20 时候即达到了限流效果。

缺点:比如当统计 1-10 秒的时候,无法统计 2-11 秒之内,如果需要统计 N 秒内的 M 个请求,那么我们的 Redis 中需要保持 N 个 key 等等问题。

zset

其实限流涉及的最主要的就是滑动窗口,上面也提到 1-10 怎么变成 2-11。其实也就是起始值和末端值都各+1 即可。我们可以将请求打造成一个 zset 数组,当每一次请求进来的时候,value 保持唯一,可以用 UUID 生成,而 score 可以用当前时间戳表示,因为 score 我们可以用来计算当前时间戳之内有多少的请求数量。而 zset 数据结构也提供了 range 方法让我们可以很轻易的获取到 2 个时间戳内有多少请求,

缺点:就是 zset 的数据结构会越来越大。

常见知识点

  1. 字符串模糊查询时用 Keys 可能导致线程阻塞,尽量用 scan 指令进行无阻塞的取出数据然后去重下即可。
  2. 多个操作的情况下记得用 pipeLine 把所有的命令一次发过去,避免频繁的发送、接收带来的网络开销,提升性能。
  3. bigkeys 可以扫描 redis 中的大 key,底层是使用 scan 命令去遍历所有的键,对每个键根据其类型执行 STRLEN、LLEN、SCARD、HLEN、ZCARD 这些命令获取其长度或者元素个数。缺陷是线上试用并且个数多不一定空间大,
  4. 线上应用记得开启 Redis 慢查询日志哦,基本思路跟 MySQL 类似。
  5. Redis 中因为内存分配策略跟增删数据是会导致内存碎片,你可以重启服务也可以执行activedefrag yes 进行内存重新整理来解决此问题。