Redis《Redis核心技术与实战》重点

基础篇

总览

知识全景图:

image-20210413185129331

问题画像图:

image-20210413185229417

大体来说,一个键值数据库的实现包括以下四个模块

  • 访问框架
  • 操作模块
  • 索引模块
  • 存储模块

数据结构

Redis 采用哈希表作为 key-value 索引。

Redis的value类型,及对应的底层数据结构:

image-20220226171314433

根据底层数据结构,value的类型主要可以分为两类

  • String 类型:底层只有一种数据结构实现
  • 集合类型:即List、Hash、Set 和 Sorted Set 。底层都是两种数据结构实现
  • 集合类型的底层数据结构主要有 5 ·种:整数数组、双向链表、哈希表、跳表、压缩列表

Redis 用一个全局哈希表来保存所有键值对,来实现从键到值的快速访问:

image-20210413185538080

Redis数据组织形式:如上图。全局哈希表 -> entry -> key指针,value指针 -> 具体的key值、value值。只需要计算key的hash值,就能拿到哈希桶位置。

Redis 解决哈希冲突的方式:链式哈希。链表变得过长时,,进行rehash操作。此时可能会导致操作突然变慢

rehash操作:两个表来回拷贝。为了让rehash操作更高效,Redis 默认使用了两个全局哈希表。两个表轮流拷贝,并且索引引入了渐进式哈希,以避免同时大量copy导致卡顿。

访问:高性能IO模型

Redis单线程的概念:Redis 的单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的

网络IO由Socket提供,socket网络模型本身提供了非阻塞模式。所以速度快。具体处理时是单线程,避免了多线程开销。

OS提供了支持Socket 网络模型非阻塞模式的机制。这个机制就是Linux 的 IO 多路复用机制。其他OS也有,名字不同。Redis 网络框架调用 epoll 机制。

下图就是基于多路复用Redis IO 模型

image-20210413194434497

不同的OS,基本上都有对应的多路复用机制实现:Linux是select/epoll模式,FreeBSD 是 kqueue,Solaris 是 evport等等。

2020年5月,Redis6.0提出了多线程模型。

存储:AOF日志

AOF就是Append Only File,表示文件只能追加写

和MySQL的写前日志(Write Ahead Log, WAL)不同,AOF是写后日志。

AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。

合适的AOF日志写回策略以避免丢数据。因为可能操作执行了,日志没写成功。

AOF日志重写机制解决AOF日志变大问题。

AOF 日志由主线程写回,而重写过程是fork后由后台线程 bgrewriteaof 来完成的,所以不会阻塞主线程。重写过程:可以总结为“一个拷贝,两处日志”。

fork这个瞬间一定是会阻塞主线程的。

存储:RDB快照

内存快照:把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这个快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写。

为了提供所有数据的可靠性保证,内存快照执行的是全量快照

save会阻塞主进程。通过fork主线程数据,由 bgsave 命令来执行全量快照即可,这样就不会阻塞主线程。

Redis 借助了OS提供的写时复制技术(Copy-On-Write, COW),来实现在执行快照的同时,进行正常写操作处理。

AOF宕机恢复慢,频繁的RDB则阻塞主线程、磁盘压力大。所以可以综合使用RDB和AOF。

所有Redis相关配置在redis.conf文件。

从库宕机:主从库数据同步

Redis的主从库模式,主从库之间采用的是读写分离的方式,主从都能读,只有主库可以写。从而避免了多写的复杂度。

replicaof命令设置主从关系。

主从库第一次同步

  • 建立连接、协商同步。同步数据。
  • 生产RDB,发送RDB。
  • 新的写命令,发送 repl buffer(第二阶段复制时产生的新数据)。

主从级联:分担同步时主库全量复制压力。简单来说,就是从库可以和其他从库建立主从关系

主从库间网络断了怎么办?repl_backlog_buffer 缓冲区,缓存断开时数据。主从库断连后,主库此时收到的写操作命令,会写入 replication buffer,同时也会写入 repl_backlog_buffer 缓冲区

repl_backlog_buffer 构造:是一个环形区域。如果长期断网导致环形区写满,那么恢复时就进行全量复制。

image-20210414184623381

主从库间发送replconf ack命令心跳(包含了复制进度)进行同步。

解决主库压力过大:如果主库的写压力还是太大,缓冲空间容不下。那么我们可以考虑切片集群

主库宕机:哨兵机制选主

哨兵机制,解决问题:负责判断主库“客观下线”,以及负责主从切换

  1. 主库真的挂了吗
  2. 该选择哪个从库作为主库
  3. 怎么把新主库的相关信息通知给从库和客户端

哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。

哨兵的主要职责就是三个任务监控(主客观下线)、选主(筛选:延迟等,打分:优先级、进度、ID号)和通知

image-20210414184725156

哨兵宕机:哨兵集群通信选主切换

哨兵判断或执行切换时,哨兵自己挂了。怎么办?

哨兵的通信机制: Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。

哨兵如何获取其他哨兵地址:在主从集群中,哨兵通过“__sentinel__:hello”频道来获取其他哨兵的地址,并进行相互通信

哨兵如何获取从库地址:由哨兵向主库发送 INFO 命令

哨兵如何实现通知客户端从哨兵订阅消息即可实现哨兵的通知功能。其实哨兵就是一个运行在特定模式下的 Redis 实例,所以,哨兵也是可以提供 pub/sub 机制的

由哪个哨兵执行主从切换?第一阶段,投票客观下线第二阶段,投票Leader选举(先到先得)。

切片集群:应对数据增多

数据量多怎么办:

  • 纵向扩展:加内存。但是内存多了fork会有很大开销。
  • 横向扩展:cluster。有一定复杂性。

切片集群是一种通用机制*,可以有不同的实现方案:

  • Redis 3.0 之前:没有官方方案。主要有,基于客户端分区的 ShardedJedis,基于代理的 Codis、Twemproxy 等。后面会讲。
  • Redis 3.0 开始:官方引入了 Redis Cluster 方案。该方案中就规定了数据和实例的对应规则。

Redis Cluster具体方案如下:…

客户端如何定位数据?

  • 实例同步其他实例哈希槽:Redis 实例会把自己的哈希槽发给与它相连接的其它实例.
  • 实例把哈希槽发给客户端:客户端把哈希槽缓存到本地

变更的最新哈希槽信息怎么同步呢?

  • 实例之间:直接通过相互传递消息来同步。
  • 客户端:Redis Cluster重定向机制。MOVED 命令。

重定向机制:客户端给实例发送数据读写操作,若该实例上没有对应的数据,客户端需要再给另一个新实例发送操作命令

  • 数据全部迁移:

    • 实例A给客户端返回 MOVED 命令:该命令中就包含了新实例的访问地址。

    • 客户端重新访问新实例B:客户端根据返回的MOVED命令信息,访问新实例。同时本地缓存进行更新

  • 数据部分迁移:实例A给客户端返回 ASK 报错:该命令中包含了数据实际所在实例的访问地址。

    • 实例A给客户端返回 ASK 报错:该命令中包含了数据实际所在实例的访问地址。
    • 客户端重新访问实际实例:此时需要先向实例发送一个ASKING命令,,客户端本地的缓存不更新,下次访问时还是访问实例A。

主从库数据不一致怎么解决?没办法完全杜绝。

数据过期问题?参照Java全栈网站。

Redis-cluster没有使用一致性hash,而是引入了哈希槽的概念

实践篇:数据结构

String类型占空间

String 类型比较通用,甚至可以保存二进制字节流。但是String的底层结构,内存开销比较大。

SDS结构体额外开销:元数据比实际数据本身大时,就很耗空间。

image-20210507101241843

RedisObject 结构体额外开销,3种存放方式:

image-20210507103607730

全局哈希表额外开销:一个dictEntry 24个字节。

image-20210507104334871

jemalloc 分配库额外开销:Redis 使用的内存分配库 jemalloc,按2的幂次数分配。

用压缩列表更节省内存:List、Hash、SortedSet集合,底层在一定数据规模内,都是用压缩列表实现

image-20210507113217643

以 Redis Hash 集合为例,最大元素个数、单个元素的最大长度,以上两个阈值,有一个超过时,压缩列表就会转换为哈希表,且不会再转换回来

基于集合的各种统计

聚合统计:多个集合元素的聚合结果,比如求它们的交集、差集、并集。聚合计算适合用 Set 集合类型

排序统计:需要有序集合来存数据。在 Redis 常用的 4 个集合类型中(List、Hash、Set、Sorted Set),List 和 Sorted Set 属于有序集合。Sorted Set使用 ZRANGEBYSCORE命令。

二值状态统计:集合元素的取值就只有 0 和 1 两种。比如,打卡状态。此时每个数据用1个bit位存储就足够,非常省内存。Bitmap:Redis 提供的扩展数据类型。底层的数据结构是String类型,String类型保存为二进制的字节数组。底层实际上就是一个 bit 数组

基数统计:统计一个集合中不重复的元素个数。

  • Set天然支持去重。缺点是,太费内存
  • Hash和Set类似。也费内存。
  • HyperLogLog:用于统计基数的数据集合类型。它的最大优势就在于,它计算基数所需的空间总是固定的,和数据量无关。缺点:统计结果是有一定误差。

GEO,以及自定义数据类型

面对海量数据统计时,为了节省内存,Redis提供了额外的三种扩展类型

  • Bitmap
  • HyperLogLog
  • GEO

GEO 类型的底层数据结构就是用 Sorted Set 来实现的。

GeoHash 编码方法:让经纬度可以比较。基本原理就是“二分区间,区间编码

自定义数据类型:

  • RedisObject 的内部结构包括了 type、encoding,、lru 和 refcount 4 个元数据,以及 1 个*ptr指针
  • 我们在定义新的数据类型后,只需在 RedisObject 中设置好新类型的 type 和 encoding,再用*ptr指向新类型的实现即可。

由前可以,开发一个新的数据类型只需执行4个步骤即可:

  1. 定义新的类型,及其结构。
  2. 在RedisObject中增加新类型的 type、encoding定义。
  3. 实现创建、释放函数。
  4. 实现基本命令。

如果需要新的数据类型能被持久化保存,则还需要在 Redis 的 RDB 和 AOF 模块中增加对新数据类型进行持久化保存的代码

保存时间序列

RedisTimeSeries 是 Redis 的一个扩展模块,不是Redis原生的。用于进行时间序列数据的复杂聚合计算。

方案一:一条记录,分别作为两份存进Hash和SortedSet。

Redis实现简单事务:MULTI 和 EXEC 命令。后续会详细讲,现在只需要知道怎么用。

  • MULTI 命令:表示一系列原子性操作的开始。收到命令先存起来,放到内部命令队列。
  • EXEC 命令:表示一系列原子性操作的结束。内部命令队列的命令,一起提交执行。

方案二:RedisTimeSeries 是 Redis 的一个扩展模块,支持在 Redis 实例上直接对数据进行按时间范围的聚合计算。需要编译成dll库后加载

消息队列

MQ 的核心需求有三个:消息顺序性、重复消息处理、消息可靠性。此外还要解决消息堆积问题。

最先使用List处理。天然具有顺序性。重复消息要在客户端处理。可靠性用BRPOPLPUSH 命令从一个 List 中读取消息,会用另一个List留存。堆积没法处理,没法多个消费者消费。

Redis 从 5.0 版本开始提供的 Streams 数据类型,满足消息三大需求,且支持消费组形式的消息读取XREADGROUP:创建消费组之后,可以用 XREADGROUP 命令让消费组内的消费者读取消息。XACK 命令:消费者使用 XACK 命令后,消息才会被删除。

实践篇:性能和内存

本篇介绍影响 Redis 性能的 5 大类潜在因素

  • Redis 内部的阻塞式操作;
  • CPU 核和 NUMA 架构的影响;
  • Redis 关键系统配置;
  • Redis 内存碎片;
  • Redis 缓冲区。

Redis阻塞点

Redis 和不同的对象交互,就会有不同的阻塞点。这些交互对象有

  • 客户端:网络 IO,键值对增删改查操作,数据库操作;
  • 磁盘:生成 RDB 快照,记录 AOF 日志,AOF 日志重写;
  • 主从节点:主库生成、传输 RDB 文件,从库接收 RDB 文件、清空数据库、加载 RDB 文件;
  • 切片集群实例:向其他实例传输哈希槽信息,数据迁移。

如下图所示

image-20210516135845694

汇总前面提到的Redis主线程阻塞点:…

以下是各个阻塞点异步执行情况:

  • 阻塞点一:集合全量查询和聚合操作不能异步执行读操作是典型的关键路径操作
  • 阻塞点二:bigkey 删除。可以异步执行。删除操作不用等待返回结果。
  • 阻塞点三:清空数据库。可以异步执行。删除操作不用等待返回结果。
  • 阻塞点四:增改,AOF 日志同步写。可以异步执行。Redis 实例需要保证 AOF 日志中的操作记录已经落盘,该操作虽然需要实例等待,但无需返回具体的数据结果。
  • 阻塞点五:主从,从库加载RDB文件不能异步执行。从库要想对客户端提供数据存取服务,就必须把 RDB 文件加载完成。

根据CPU优化Redis

本篇主题:基于 CPU 多核架构和多 CPU 架构优化 Redis 性能的方法。

一级缓存和二级缓存,缓存空间只能被当前物理核使用。不同物理核共享一个三级缓存L3。

要想Redis速度快,就要尽量在同一个物理核操作。物理核内缓存的,是访问内存的10倍多。所以,充分利用 L1、L2 缓存。

image-20210516140104506

image-20210516140121421

性能优化:taskset 命令绑核。

最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上,这样Redis 实例就可以直接从本地内存读取网络数据,如下图所示:

image-20210516140255898

绑定物理核:我们不要把一个实例和一个逻辑核绑定,而是和一个物理核绑定。以避免各种后台子进程和主进程竞争。

排查Redis变慢问题(上)

接下来两篇,来看下如何系统性的解决Redis变慢问题,即问题认定系统性排查应对方案

问题认定

  • 看 Redis 延绝对值。比如突然出现几秒到十几秒的响应延迟,那肯定就是Redis变慢了。
  • 基线性能*:从 2.8.7 版本开始,redis-cli 命令提供了–intrinsic-latency 选项,可以用来监测和统计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。
  • 网络对基线性能的影响:可以iPerf 这样的工具,测量从 Redis 客户端到服务器端的网络延迟。如果延迟有几十毫秒甚至是几百毫秒,那么就是网络带宽被其他程序占用了。

一般认为,观察到的 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了。

系统性排查:影响 Redis 性能的三大要素Redis 自身的操作特性文件系统操作系统

Redis 自身操作特性:重点介绍两类关键操作:慢查询命令过期 key 操作

  • 慢查询:可以通过 Redis 日志,或者是 latency monitor 工具,查询变慢的请求。如全量、聚合、KEYS命令。
  • 过期key:Redis 默认每 100 毫秒会删除一些过期 key,,当有大量key同时过期时,就会一直删除。删除在4.0之前是阻塞的。

排查Redis变慢问题(下)

文件系统:Redis需要持久化。文件系统将数据写回磁盘的机制,会直接影响到 Redis 持久化的效率。AOF阻塞

操作系统:Redis 内存操作非常频繁。OS的内存机制会直接影响到 Redis 的处理效率,例如内存不够时OS会启动Swap机制、内存大页。可能阻塞。

回顾上上一篇的Redis架构图:注意红色部分:

image-20210513110632241

AOF写回:no、everysec、aways,重写,都可能阻塞。

image-20210516140423793

解决方案:调整策略。或换固态硬盘,提升10倍性能。

操作系统swap:内存 swap:OS将内存数据在内存磁盘间来回换入换出的机制,涉及到磁盘的读写。内存不够时触发。

排查方法OS会在后台记录每个进程的 swap 使用情况,即有多少数据量发生了 swap。可proc目录查看相应进程的smaps信息。当出现百 MB,甚至 GB 级别的 swap 大小时,就表明此时Redis 实例的内存压力很大。

操作系统:内存大页。

内存大页机制(Transparent Huge Page, THP):内存的分页粒度增大。Linux 内核从 2.6.38 开始支持 2MB 大小的内存页分配,常规分配粒度是4KB。

Redis是需要持久化的,当它的子线程在后台执行持久化时,如果客户端请求修改到了正在持久化的数据,就会触发写时复制。即,一旦有数据要被修改,不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改。

排查方法:直接执行命令查询配置文件即可:

解决方法:直接关闭内存大页。生产中建议关闭内存大页。

下面是Redis性能变慢时的9个checklist,逐一检查即可……

Redis删除数据后内存未能及时释放

本节主题:Redis 的内存空间存储效率问题。数据已经删了,空出来的空间却没法用(内存碎片),怎么排查和解决。

内存碎片的形成原因,主要有有内因和外因。简单来说:

  • 内因:操作系统的内存分配机制,内存分配器按固定大小分配内存
  • 外因: Redis 的负载特征,数据键值对大小不一,以及数据键值对随时在删改

如何判断是否有内存碎片?

  • Redis 自身提供了 INFO 命令,可以用来查询内存使用的详细信息
  • 内存碎片率:重点关注 mem_fragmentation_ratio 指标。大于 1.5 :内存碎片量过多*。表明内存碎片率已经超过了 50%。

如何清理内存碎片?两个思路

  • 直接重启Redis实例
  • 4.0-RC3 版本后,Redis提供了自动清理的配置。类似JVM的内存整理算法。碎片清理的影响,这些都会带来延迟,导致性能降低。

内存碎片设置:

  • 启用:把 activedefrag 配置项设置为 yes:config set activedefrag yes
  • 触发大小:相对比例、或绝对大小。
  • 占用CPU时间限制:上限、下限。

缓冲区不够用咋办

本篇主题:Redis 中缓冲区的用法。服务器端和客户端、主从集群间的缓冲区溢出问题。以及应对方案

客户端输入缓冲区和输出缓冲区:服务器端给每个连接的客户端都设置了一个输入缓冲区和输出缓冲区,以解决请求和处理速度的不匹配。

image-20210516140717543

输入缓冲区溢出:

  • 查看:使用 CLIENT LIST 命令:可查看服务端为每个客户端分配的输入缓冲区使用情况。
  • 排查:写入太快:写入了 bigkey处理太慢:服务器端处理请求的速度过慢
  • 处理:单个缓冲区溢出直接关闭该客户端连接。多个缓冲区溢出,超过配置项,引发数据淘汰超过总内存,导致Redis崩溃
  • 预防:控制生产:避免客户端写入 bigkey。控制消费:避免 Redis 主线程阻塞。缓冲区已写死每个客户端1G,没法调大。

输出缓冲区溢出:

输出缓冲区设计:为了提高效率,Redis的输出缓冲区分为了两部分

  • 一个大小固定为 16KB 的缓冲空间:暂存 OK 响应和出错信息;
  • 一个可动态增加的缓冲空间:暂存大小可变的响应结果。

其中,以下三种溢出情况如下

  • 服务器端返回 bigkey 的大量结果。
  • 执行了 MONITOR 命令
  • 缓冲区大小设置不合理

主从集群中的缓冲区:

全量复制缓冲区:主节点在向从节点传输 RDB 文件,待 RDB 文件传输完成后,再发送给从节点执行。主节点上会为每个从节点都维护一个复制缓冲区,如下图所示:

image-20210516153728205

主节点接收到大量写命令,那么就可能溢出。

复制缓冲区一旦发生溢出,主节点就会直接关闭和从节点进行复制操作的连接,导致全量复制失败。

如何避免

  • 控制主节点保存的数据量大小。主节点的数据量经验值是 2~4GB。
  • 使用 client-output-buffer-limit 配置项设置合理的复制缓冲区大小
  • 控制从节点的数量:避免主节点中复制缓冲区占用过多内存的问题。

增量复制:称为复制积压缓冲区。其实就是我们前面第6篇学到的 repl_backlog_buffer。一个大小有限的环形缓冲区。溢出后就进行全量复制。

实践篇:缓存

本篇阐述以下4个关键问题

  • Redis 作为缓存具体如何工作
  • Redis 缓存如果满了怎么办
  • Redis 异常如何处理?即缓存一致性、缓存穿透、缓存雪崩、缓存击穿如何应对?
  • Redis 缓存如何扩展?Redis的内存毕竟有限,如果用快速的固态硬盘来保存数据,可以增加缓存的数据量,那么,Redis 缓存可以使用快速固态硬盘吗?

Redis工作模式

本节主题:了解下缓存的特征、 Redis 适用于缓存的天然优势、 Redis 缓存的具体工作机制。

Redis 称为旁路缓存,也就是说,读取缓存、读取数据库和更新缓存的操作都需要在应用程序中完成,应用程序调用Redis来执行操作

两种用法:

  • 只读缓存:读操作在Redis处理,懒加载。写操作在DB处理,同时删除Redis旧数据。最新的数据在DB中。
  • 读写缓存:读写操作都在Redis处理,最新的数据在Redis中。写操作使用同步直写,或异步写回

在商品大促的场景中,商品的库存信息会一直被修改。如果每次修改都需到数据库中处理,就会拖慢整个应用,此时,我们通常会选择读写缓存的模式。

缓存满了怎么办

建议把缓存容量设置**为总数据量的 15% 到 30%**,兼顾访问性能和内存空间开销。

Redis 缓存有哪些淘汰策略?总共8种淘汰策略

image-20210516193657660

Redis 中的 LRU 算法简化,简单来说就是增加了一个随机选取候选集合的操作

如何处理被淘汰的数据?淘汰数据,分情况处理:

  • 干净数据:直接删除。
  • 脏数据:写回数据库。

缓存异常:缓存和DB数据不一致

如何发生?

对于读写缓存:如果要保证数据一致,就要用同步直写策略。此时需要要在业务应用中使用事务机制,来保证缓存和 DB 的更新具有原子性。禁止使用异步写回(缓存中的数据要淘汰时才更新到DB)。

对于只读缓存:删改数据:删改DB,同时删除缓存。需要保证两个步骤的原子性,数据才一致。

如何解决单线程删除失败导致一致?

使用重试机制操作方法:把要删除的缓存数据、或是要删改的数据库值,放到MQ。再进行删改操作,成功则移除MQ中的消息,失败则从MQ读取值,再次进行删改操作。

如何解决多线程并发读到旧数据?A删除了缓存,B又读取DB旧的数据加载到缓存?

  • 先删缓存,再更新DB:解决方案延迟双删,相当于是把其他线程更新的缓存旧值再次删掉。即在线程 A 更新完数据库值以后,让它先 sleep 一小段时间,再进行一次缓存删除操作。(其实还是没完全删除)。
  • 先更新DB,再删缓存:会有短暂不一致,但是时长较短。

对于只读缓存来说,我们既可以先删除缓存值再更新DB,也可以先更新DB再删除缓存。建议优先使用先更新数据库再删除缓存,原因主要有两个:

  • 删除缓存后DB压力大:先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力;
  • 延迟双删时间不好设置:如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

缓存异常:雪崩、击穿、穿透

除了数据不一致问题,我们常常还会面临缓存异常的三个问题,分别是:

  • 缓存雪崩大量数据同时失效,大量请求打到DB。原因:数据大量过期,或Redis挂掉。事前预防:随机微调过期时间(1-3min)。Redis高可用。事后弥补:发送雪崩时,服务降级。不重要的数据直接返回预定义的空值或错误,不走缓存和DB。服务熔断请求限流机制。
  • 缓存击穿少量热点数据失效,大量请求打到DB。原因:一般是热点数据过期失效。热点数据不设置过期时间即可。
  • 缓存穿透数据本来就不存在,大量请求打到DB。原因:数据误删除。恶意攻击。前端进行请求检测,过滤恶意请求。布隆过滤器快速判断数据是否存在,避免到DB查询。缓存空值或默认值。

image-20210517202605091

缓存异常:被污染

缓存污染即缓存了很少用到的数据。占满缓存空间,此时需要淘汰数据,就增加了时间消耗,降低了Redis性能。

解决方案把很少用到的数据筛选出来淘汰掉

8种淘汰算法分析:

  • LRU的问题:由于只看数据的访问时间,使用 LRU 策略在处理扫描式单次查询操作时,无法解决缓存污染
  • LFU 策略:从两个维度来筛选并淘汰数据:一是,数据访问的时效性(访问时间离当前时间的远近);二是,数据的被访问次数

LFU 缓存策略算法:在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。先根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,再根据访问时效性筛选,将距离上一次访问时间更久的数据淘汰出缓存。

与LRU相比,仅仅是把原来 24bit 大小的 lru 字段,进一步拆分成了两部分

  • ldt 值:lru 字段的前 16bit,表示数据的访问时间戳

  • counter 值:lru 字段的后 8bit,表示数据的访问次数。改进1:访问次数非线性递增。乘以系数,加1,取倒,和随机值比较,大于才加1。改进2:访问次数自动衰减。N分钟没被访问,就衰减 (差值 / lfu_decay_time) 次

实践篇:锁

Redis如何应对并发访问:无锁的原子操作

业务场景:使用Redis时,多个业务对Redis数据并发写,可能导致数据改错。需要保证并发访问的正确性。

Redis提供了两种方法来保证并发访问的正确性加锁原子性操作。本节主要介绍原子性操作。下节介绍加锁操作。

需要控制什么?主要是控制多个客户端读写同一份数据的过程。保证任何一个客户端发送的操作在 Redis 实例上执行时具有互斥性。当客户端需要写数据时,基本流程分为两步:

  1. 客户端读取数据到本地,在本地修改数据。
  2. 客户端把修改完的数据,写回 Redis。

使用原子性操作来加锁:Redis 的原子操作有两种方法

  • 单命令操作INCR / DECR 命令,把多个操作在 Redis 中实现成一个操作;
  • Lua脚本Lua 脚本,把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。用 Redis 的 EVAL 命令来执行 Lua 脚本

Redis如何应对并发访问:分布式锁

RedLock实现原理。

与ZK分布式锁比较?

Redis事务机制:ACID

Redis 通过 MULTI、EXEC、DISCARD 和 WATCH 四个命令来支持事务机制:

image-20220629102735558

Redis 的事务机制可以保证一致性隔离性,但是无法保证持久性

原子性的情况比较复杂,当事务中使用的命令语法有误时,原子性无法保证。在其它情况下,事务都可以原子性执行。

实践篇:集群

Redis问题:主从数据不一致、过期数据、服务挂掉

本节介绍主从机制三个常见的坑

  • 主从数据不一致:查看主从库复制进度产生原因主从库间的命令复制是异步进行的。。可以使用 Redis 的 INFO replication 命令。差值太大的从库直接断开与客户端连接。
  • 读到过期数据:惰性删除策略:数据到期不立即删除。Redis 3.2及之后,从库会进行过期数据判断,过期时返回空值。过期时间设置,有相对时间和绝对时间。建议在业务应用中使用 EXPIREAT/PEXPIREAT 命令,在指定时间点删除数据,此时需要主从节点时钟保持一致。
  • 配置项不合理导致服务挂掉:
    • 哨兵保护模式导致无法相互通信protected-mode 被设置为 yes时,哨兵无法判断主库下线,也无法进行主从切换,最终 Redis 服务不可用。
    • 设置 Redis Cluster 中实例响应心跳消息的超时时间:建议将 cluster-node-timeout 调大些(例如 10 到 20 秒)。

Redis问题:数据丢失

业务场景:主从集群有 1 个主库、5 个从库和 3 个哨兵实例,在使用的过程中,我们发现客户端发送的一些数据丢失了,这直接影响到了业务层的数据可靠性。

问题排查:脑裂:在主从集群中,同时有两个主节点,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。严重时,就导致数据丢失了。

主从集群中数据丢失最常见的原因

  1. 主库的数据还没有同步到从库,此时主库发生故障
  2. 从库升级为主库,未同步的数据丢失

脑裂导致数据丢失:(1)主库由于某些原因无法处理请求,也没有响应哨兵的心跳,被哨兵错误地判断为客观下线。(2)开始主从切换。(3)哨兵还没有完成主从切换,原主库又重新开始处理请求了,此时客户端仍然可以和原主库通信,客户端发送的写操作在原主库上写入了数据。(4)主从切换完成,数据全量同步,原主库新写数据丢失

如何应对脑裂问题?

主要的想法是,限制主库的接收请求,防止主库被占满。

Redis 提供了两个配置项来限制主库的请求处理

  • min-slaves-to-write:主库能进行数据同步的最少从库数量。
  • min-slaves-max-lag:主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

Codis

Redis Cluster发布之前,Redis的集群解决方案:Codis。由豌豆荚开发。

image-20220227144914094

image-20220227150202218

Codis 和 Redis Cluster两种方案区别如下图所示:

image-20220227150307211

Redis支撑秒杀场景的关键技术和实践

秒杀场景的业务特点是限时限量,即用户只能在某个时间点买,且库存量非常有限。业务系统要处理瞬时的大量高并发请求。

不过,秒杀场景包含了多个环节,可以分成秒杀前、秒杀中和秒杀后三个阶段,每个阶段的请求处理需求并不相同,Redis 并不能支撑秒杀场景的每一个环节

本节主题:Redis 在秒杀场景中具体是在哪个环节起到支撑作用?又是如何支持?

三个阶段:

秒杀前:

  • 页面刷新。使用CDM和前端静态页面抗。

秒杀中,大量点击秒杀按钮:

  1. 库存查验最大的并发压力在该步骤上
  2. 库存扣减:有库存就扣减库存。
  3. 订单处理:扣减库存成功后创建订单。

具体处理流程:直接在 Redis 中进行库存查验库存扣减。库存查验完成后,一旦库存有余量,就立即在 Redis 中扣减库存。而且为了避免请求查询到旧的库存值,库存查验库存扣减这两个操作需要保证原子性

订单处理环节:可以在DB执行。虽然操作复杂,还会涉及多张表,但是请求压力已经不大,而且需要保证多张表的事务性

秒杀后:未买到的用户少量查询。已下单用户查询订单详情。

  • 服务端能够支撑。

综上,在秒杀场景中需要 Redis 参与的两个环节如下图所示:

image-20220227151327324

分布式锁实现思路:客户端向 Redis 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减。没拿到锁的客户端直接返回秒杀失败即可,此时大量的请求就被抢锁这个操作过滤掉了。切片集群中分布式锁商品库存信息不要放到同一个实例,缓解单个实例的压力。

数据分布优化:如何应对数据倾斜?

数据倾斜

  • 数据量的倾斜:在某些情况下,一部分实例上的数据很多。
  • 数据访问倾斜:虽然每个实例数据量差不多,但某个实例上的数据是热点数据,访问量很大。

数据量倾斜主要是三个原因

  • 某个实例保存了 bigkey:避免产生bigkey。也可拆分bigkey集合。
  • Slot 分配不均衡:查看slot分布,避免过多的 Slot 集中到了同一个实例,否则进行Slot迁移。
  • Hash Tag:是指加在键值对 key 中的一对花括号{}。这对括号会把 key 的一部分括起来,客户端在计算 key 的 CRC16 值时,只对 Hash Tag 花括号中的 key 内容进行计算。如果没用 Hash Tag 的话,客户端计算整个 key 的 CRC16 的值。例如,key为user:profile:{3231},则计算CRC16时只按3231计算。 如果Hash Tag 内容都一样,那么,这些 key 对应的数据会被映射到同一个 Slot 中。

为什么要用HashTag:用在 Redis Cluster 和 Codis 中,支持事务操作和范围查询。直接使用HashTag把数据都映射到同一个实例,就避免了跨实例访问的问题了。

最好是不要用 Hash Tag。

应对方案:热点数据以服务读操作为主,我们可以采用热点数据多副本的方法来应对。

通信开销:限制Redis Cluster规模的关键因素

Gossip 协议:为了让集群中的每个实例都知道其它所有实例的状态信息,实例之间会按照一定的规则进行通信。这个规则就是 Gossip 协议。

Gossip 协议的工作原理,可概括为两点:

  1. 定期发送检测消息:每个实例之间会按照一定的频率,从集群中随机挑选一些实例,把 PING 消息发送给挑选出来的实例,用来检测这些实例是否在线,并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息部分其它实例的状态信息,以及 Slot 映射表
  2. 回复检测消息:一个实例在接收到 PING 消息后,会给发送 PING 消息的实例,返回一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。

Gossip消息的大小:一次ping-pong大概24K。虽然24KB不大,但是假如客户端的访问正常也就是几KB,那么显然PING-PONG的24KB是比较大的。

Gossip消息频率:定时随机选取发送 + 定期扫描检查发送。

建议你把 Redis Cluster 的规模控制在 400~500 个实例

未来篇

Redis 6.0的新特性:多线程、客户端缓存与安全

Redis在2020年5月推出了 6.0 版本,增加了很多新特性。来了解一下。

几个关键新特性

  • 面向网络处理的多 IO 线程:提升Redis性能。提高网络请求处理的速度
  • 客户端缓存:提升Redis性能。让应用可以直接在客户端本地读取数据
  • 细粒度的权限控制: 加强 Redis 安全保护,可以按照命令粒度控制不同用户的访问权限。
  • RESP 3 协议的使用:增强客户端的功能,让应用更加方便地使用 Redis 的不同数据类型。

遇到的问题:随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。表现是吞吐量没法继续提升,但CPU负荷不高。

多线程主要是用来读取解析Socket请求、结果回写Socket

解决思路有两个

  • 用用户态网络协议栈(例如 DPDK)取代内核网络协议栈:让网络请求的处理不用在内核里执行,直接在用户态完成处理就行。
  • 用多个 IO 线程来处理网络请求,提高网络请求处理的并行度:6.0使用了该方案。

Redis 6.0 的新特性总结如下图:使用建议:Redis6.0刚出来,还需踩坑

image-20220227155039713

Redis的下一步:基于NVM内存的实践

NVM:新型非易失存储(Non-Volatile Memory,NVM),容量大、性能快、能持久化保存数据。由于NVM 器件像 DRAM 一样,可以让软件以字节粒度进行寻址访问,所以,在实际应用中,NVM 可以作为内存来使用,我们称为 NVM 内存。

如果我们使用持久化内存,就可以充分利用 PM 快速持久化的特点,来避免 RDB 和 AOF 的操作。我们就可以把 Redis 直接运行在 PM 上。同时,数据本身就可以在 PM 上持久化保存了,就无需额外的 RDB 或 AOF 日志机制来保证数据可靠性。

有了持久化内存后,还需要 Redis 主从集群吗?肯定还是需要主从集群的。持久化内存只能解决存储容量和数据恢复问题,关注点在于单个实例……


转载请注明来源