拜读了极客时间蒋德钧老师的《 Redis 核心技术与实战》专栏,觉得蒋老师讲解 Redis 的思路特别清晰,一口气读完后写了这篇读书笔记,对照常见的 Redis 面试题发现已经涵盖了十之八九。
思维导图
该专栏先通过下图中的两大维度和三个主线带领我们详细学习 Redis 的设计和各种技术,随后分析了一些在实战中可能遇到的一些问题,例如:缓存雪崩、击穿、穿透以及脑裂等经典问题。摘要记录
Redis如何设计数据结构?
键和值是如何组织的?
使用全局哈希表来组织,达到O(1)时间复杂度。但随着键变多,会发生哈希碰撞,此时同一个哈希桶中的键会用指针链接,导致查找效率变低。因此引入rehash,渐进式地将数据复制到另一张哈希表中,把一次数据拷贝的开销分摊到多次请求过程中,避免耗时和阻塞。
集合数据类型的操作效率是怎样保证的?
集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈希表、压缩列表和跳表。压缩列表和数组差不多,查询复杂度O(N),在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
跳表是在链表的基础上加了多级索引,加速数据的定位,查询复杂度O(logN)
集合中不同操作的复杂度不同
- 单元素操作是基础,取决于数据结构的复杂度
- 范围操作非常耗时;统计操作通常高效。底层的数据结构专门记录了统计相关,因此高效。范围操作通常遍历,需要规避,可以使用SCAN系列操作。
- 例外情况只有几个。压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。
字符串类型如何设计?
-
如果使用它保存64位整数时,直接保存为8字节的Long类型整数,这叫int编码
-
如果包含字符,就会使用简单动态字符串(Simple Dynamic String, SDS)结构体保存:buf(实际数据)、len(buf长度)、alloc(实际分配长度)
- Redis本身需要元数据RedisObject来记录信息
为什么说String类型内存开销大?
1个10位数的字符串,使用两个Long类型共16字节,元数据却达到了48字节,一共需要64字节。如果使用底层结构为压缩列表的数据类型,比如List,Hash等,只需要1+4+1+8=16字节
面向LBS应用的GEO类型
以叫车服务为例,来分析下 LBS 应用中经纬度的存取特点。- 每一辆网约车都有一个编号(例如 33),网约车需要将自己的经度信息(例如 116.034579)和纬度信息(例如 39.000452 )发给叫车应用。
- 用户在叫车的时候,叫车应用会根据用户的经纬度位置(例如经度 116.054579,纬度 39.030452),查找用户的附近车辆,并进行匹配。
- 等把位置相近的用户和车辆匹配上以后,叫车应用就会根据车辆的编号,获取车辆的信息,并返回给用户。
如果使用Hash来存储车ID和经纬度位置的信息,无法做到有序的范围查询,使用Sorted Set是更好的选择,GEO底层也正是使用 Sorted Set 实现的。其中的元素是车ID,元素的权重分数是经纬度信息。但权重分数是float类型,因此需要对经纬度进行GeoHash编码
GeoHash编码就是先对经度和纬度分别编码,再放在一块编码。对经度和纬度编码就是不断二分区间,落在左区间取0,右区间取1,等做完N次二分,就得到一个N位的编码。经度范围是 [-180, 180] ,纬度是 [-90, 90]。进行最终编码时,偶数位是经度编码,奇数位是纬度编码,组合成最终编码。
使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用“搜索附近的人或物”的功能了。
Redis如何设计高性能IO模型?
Redis单线程指的是网络IO与键值读写是一个线程完成,持久化、集群同步、异步删除等是额外线程。Redis如何使用单线程实现十万级别处理能力?
高效数据结构+内存完成操作+多路复用机制Redis的单线程需要处理网络IO+KV读写,即:bind/listen, accept(建立连接), recv(读取请求), parse, get(KV读写), send(返回结果)
其中accept与recv阶段容易发生阻塞,因此需要使用非阻塞模式,同时使用Linux中的IO多路复用机制保证不会阻塞的同时,也有机制继续监听。
Linux中的IO多路复用机制是指一个线程处理多个IO流,即select/epoll机制。通过基于事件的回调机制,不同事件调用不同处理函数。事件入队,Redis单线程进行消费。
Redis如何设计持久化机制?
AOF(Append File Only)
AOF记的是命令,执行成功后才记录,规避检查开销,并且不会阻塞当前的写操作。风险:没来得及写入就宕机导致丢失、可能阻塞下一个操作
三种写回策略
-
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
-
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
-
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
日志文件太大怎么办?
采用重写机制减少大小,只记录该KV对的最新状态。重写过程由后台子进程完成,不阻塞主线程。
重写过程归纳为“一个拷贝,两处日志”:
- 执行重写时主线程fork出重写子进程,把内存拷贝给它
- 在重写期间,主线程继续把到来的写操作写入原AOF缓冲区,防止宕机丢失
- 同时,该操作也会被写到重写AOF的缓冲区,保证最新状态
使用AOF时,主线程逐条顺序读命令重放,过程缓慢,因此引出RDB快照
RDB(Redis DataBase)内存快照
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。- save:在主线程中执行,会导致阻塞;
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
在使用bgsave生成快照时理论上不能写入,因为和主线程共用一份数据,一旦主线程写入会让快照的完整性被破坏。此时Redis借助OS提供的CopyOnWrite技术,把要写/更新的数据生成副本后修改,不影响子进程写入RDB。
RDB的问题在于:
- 不好控制生成快照的频率,频率太低会导致数据丢失;频率太高会增加写磁盘的频率,可能上一次还没结束,下次已经开始,恶性循环。
- bgsave子进程是fork主线程出来的,fork操作本身会阻塞主线程一段时间
- 如果使用增量快照,需要很多元数据来记录哪里产生更新,浪费内存资源
混合AOF与RDB(4.0提出)
用RDB做全量快照,在两次RDB之间用AOF记录更新,等到下次RDB结束后,就可以把AOF清空,继续记录这次快照开始的更新。Redis如何减少服务中断?
主从复制
做法
读写分离的设置,主库读写都接受,从库只接受读,主库将写操作同步给从库在从库配置:
replicaof ip port
RDB文件中不包含生成RDB文件后产生的新请求记录,因此有第三阶段。在同步过程中,主库需要fork出子进程来生成RDB快照,如果从库过多,主库会频繁fork造成阻塞,且传输RDB文件需要占用主库网络带宽,因此有主从从的级联模式。
主从网络断了如何解决?
使用增量复制的方法,主库在一个环形缓冲区 repl_backlog_buffer 写入断网期间的更新,并记下偏移量。从库回归后也读这个缓冲区,也有读偏移量,从开始读到主库的偏移量结束。然而环形区有被覆盖的风险,需要精心设计大小:repl_backlog_size = 缓冲空间大小 * 2哨兵机制
哨兵是运行在特殊模式下的Redis进程,主要负责:监控、选主、通知周期性给所有库发送心跳,不响应就标记为下线,如果是主库就启动自动切换主库流程。选择新主库后,将其信息通知其他从库,执行replicaof命令并进行数据复制。同时,将新主库告知客户端,写操作发到新主库。
“客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”,这样能减少误判。
选定新主库时,首先要筛选符合条件的从库,其次根据规则给这些从库打分,分数最高的当选。
筛选条件:在线;网络连接状态好,根据 down-after-milliseconds 判断断联次数。
规则:优先级>同步程度>ID号小
哨兵集群的pub/sub机制
哨兵们通过在主库上发布消息和订阅消息与其他哨兵建立网络连接,频道名为“sentinel:hello”,因此需要与主库建立连接。哨兵也会和从库建立连接,通过向主库发送INFO命令获得从库信息。
此外,哨兵还需要把新主库的信息告知客户端,因此需要与客户端建立连接。
执行主从切换
一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”。这个所需的赞成票数是通过哨兵配置文件中的 quorum 配置项设定的。此时,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为“Leader 选举”。
在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:
- 拿到半数以上的赞成票;
- 拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds,不然可能导致哨兵集群对故障主库的判定不一致。
切片集群/分片集群
解决问题:单实例在fork子进程的时候随着数据量增大,时间会变得很长,导致Redis的响应变慢。搭建集群可以把数据划分多份数据切片和实例的对应分布关系
Redis Cluster采用哈希槽来处理数据和实例的映射关系:- 一共16384个slot,每个KV进来都根据key算出一个模数(0-16383)
- 搭建集群时,Redis给每个实例分配16384/N个slot
- 如果想手动分配,使用 cluster addslots 命令,但注意分配完16384个slot,否则集群不工作。
客户端如何定位数据
-
刚分配完时,每个实例会把slot分配信息发给其他实例,完成信息扩散。
-
客户端收到后会把信息缓存在本地,请求时计算出 slot 并发给对应实例
-
如果信息有变化,使用重定向机制,实例会把最新的位置信息返回给客户端,客户端再次给新实例发送命令
主从同步中的坑
四种常用的集合统计模式
聚合统计
当需要对多个集合进行聚合计算时,Set 类型会是一个非常不错的选择。但Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。所以,可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了。
排序统计
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,优先考虑使用 Sorted Set。二值状态统计
在统计 1 亿个用户连续 10 天的签到情况时,可以把每天的日期作为 key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit 对应一个用户当天的签到情况。接下来,对 10 个 Bitmap 做“与”操作,得到的结果也是一个 Bitmap。在这个 Bitmap 中,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,可以用 BITCOUNT 统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。现在,我们可以计算一下记录了 10 天签到情况后的内存开销。每天使用 1 个 1 亿位的 Bitmap,大约占 12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约为 120MB,内存压力不算太大。不过,在实际应用时,最好对 Bitmap 设置过期时间,让 Redis 自动删除不再需要的签到记录,以节省内存开销。
所以,如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。在记录海量数据时,Bitmap 能够有效地节省内存空间。
基数统计
HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。如何避免Redis单线程模型的阻塞?
Redis实例的阻塞点
-
客户端:网络 IO,键值对增删改查操作,数据库操作;
-
磁盘:生成 RDB 快照,记录 AOF 日志,AOF 日志重写;
-
主从节点:主库生成、传输 RDB 文件,从库接收 RDB 文件、清空数据库、加载 RDB 文件;
-
切片集群实例:向其他实例传输哈希槽信息,数据迁移。
客户端交互阻塞点
-
注意涉及操作复杂度为 O(N) 的集合操作,例如**集合全量查询和聚合操作。**
-
删除包含大量数据的集合,也称为 BigKey 删除
-
清空数据库(例如 FLUSHDB 和 FLUSHALL 操作)必然也是一个潜在的阻塞风险,因为它涉及到删除和释放所有的键值对。
磁盘交互阻塞点
AOF日志同步写:一个同步写磁盘的操作的耗时大约是 1~2ms,如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程了。主从节点交互阻塞点
-
对于从库来说,它在接收了 RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库,正如上面提到的。
-
从库在清空当前数据库后,还需要把 RDB 文件加载到内存,这个过程的快慢和 RDB 文件的大小密切相关,RDB 文件越大,加载过程越慢,所以,加载 RDB 文件成为阻塞点。
切片集群实例交互阻塞点
Redis Cluster方案迁移 Big Key 会造成主线程阻塞以上提到的所有阻塞点除了“集合全量查询和聚合操作”和“从库加载 RDB 文件”,都可以通过异步子进程机制来规避。
CPU结构如何影响Redis性能?
一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,速度非常快。但是,这些 L1 和 L2 缓存的大小一般只有 KB 级别。如果 L1、L2 缓存中没有所需的数据,应用程序就需要访问内存来获取数据。而应用程序的访存延迟一般在百纳秒级别,是访问 L1、L2 缓存的延迟的近 10 倍。
所以,不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)。L3 缓存能够使用的存储资源比较多,所以一般比较大,能达到几 MB 到几十 MB,这就能让应用程序缓存更多的数据。当 L1、L2 缓存中没有数据缓存时,可以访问 L3,尽可能避免访问内存。另外,现在主流的 CPU 处理器中,每个物理核通常都会运行两个超线程,也叫作逻辑核。同一个物理核的逻辑核会共享使用 L1、L2 缓存。
在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。
如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。
在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)。
CPU 多核对 Redis 性能的影响
在多核 CPU 的场景下,一旦应用程序需要在一个新的 CPU 核上运行,那么,运行时信息就需要重新加载到新的 CPU 核上。而且,新的 CPU 核的 L1、L2 缓存也需要重新加载数据和指令,这会导致程序的运行时间增加。执行下面的命令,就把 Redis 实例绑在了 0 号核上,其中,“-c”选项用于设置要绑定的核编号。taskset -c 0 ./redis-server
如何应对变慢的Redis?
查看Redis的基线性能
redis-cli 命令提供了–intrinsic-latency 选项,可以用来监测和统计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。其中,测试时长可以用–intrinsic-latency 选项的参数来指定。一般情况下,运行 120 秒就足够监测到最大延迟了,所以,我们可以把参数设置为 120。Redis自身操作特性的影响
-
慢查询命令。复杂度为 O(N) 的命令要慎用,如果有的话可以用其他高效命令代替或者在客户端完成操作而不是Redis中。
-
过期Key操作。过期 Key 的删除机制是Redis用来回收内存空间的机制,会引起 Redis 操作阻塞,导致性能变慢,算法如下:
- 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个 Key,并删除其中过期的 Key
- 如果超过 25% 的 Key 过期了,则重复删除,直到比例降至 25% 以下。删除操作是阻塞的(4.0后可以异步删除)。如果频繁使用带有相同时间参数过期命令设置 Key,就可能导致。
文件系统:AOF 的影响
写AOF时,如果使用 everysec 策略,Redis允许丢失一秒的记录,主线程并不需要确保每条日志都写回磁盘,因此Redis 使用后台子进程异步完成 fsync 操作。而对于 always 策略,需要确保写入后返回,使用主线程。在进行AOF重写机制时,Redis 使用后台子进程异步完成,但重写动作本身会对磁盘进行大量 IO 操作,同时,fsync 操作需要等到数据写到磁盘后才能返回。因此,当 AOF 重写压力比较大时,就会导致 fsync 被阻塞。虽然使用了后台子进程,但如果下一次主线程想继续写新的 AOF 时,发现上一次写未完成,就会阻塞。
如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite 设置为 yes,如下所示:
no-appendfsync-on-rewrite yes
这个配置项设置为 yes 时,表示在 AOF 重写时,不进行 fsync 操作。也就是说,Redis 实例把写命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回了。当然,如果此时实例发生宕机,就会导致数据丢失。
操作系统:swap 的影响
内存 swap 是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写,所以,一旦触发 swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。Redis 是内存数据库,内存使用量大,如果没有控制好内存的使用量,或者和其他内存需求大的应用一起运行了,就可能受到 swap 的影响,而导致性能变慢。通常触发 swap 的原因是物理机器内存不足,对于 Redis 而言:
- Redis 实例自身使用大量内存,导致物理机器可用内存不足
- 和 Redis 实例在同台机器上其他进程进行大量文件读写,占用系统内存,导致分配给 Redis 实例的内存量减少,进而触发 Redis 发生 swap
操作系统本身会在后台记录每个进程的 swap 使用情况,即有多少数据量发生了 swap。你可以先通过下面的命令查看 Redis 的进程号,这里是 5332。
$ redis-cli info | grep process_id
process_id: 5332
然后,进入 Redis 所在机器的 /proc 目录下的该进程目录中:
$ cd /proc/5332
最后,运行下面的命令,查看该 Redis 进程的使用情况。在这儿,我只截取了部分结果:
$cat smaps | egrep '^(Swap|Size)'
Size: 584 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 4 kB
Swap: 0 kB
Size: 462044 kB
Swap: 462008 kB
Size: 21392 kB
Swap: 0 kB
每一行 Size 表示的是 Redis 实例所用的一块内存大小,而 Size 下方的 Swap 和它相对应,表示这块 Size 大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完全被换出到磁盘了。当出现百 MB,甚至 GB 级别的 swap 大小时,就表明,此时,Redis 实例的内存压力很大,很有可能会变慢。所以,swap 的大小是排查 Redis 性能变慢是否由 swap 引起的重要指标。
操作系统:内存大页
由于持久化过程中 Redis 使用 CopyOnWrite 机制来避免阻塞客户端写请求,一旦有数据修改,Redis 会直接复制一份再进行修改。如果采用了内存大页机制,OS 支持 2MB 大小的内存页分配而不是常规的 4KB 粒度。那么,即使客户端请求只修改 100B 数据,Redis 也需要拷贝 2MB 的大页,因此会导致性能变慢。
首先,我们要先排查下内存大页。方法是:在 Redis 实例运行的机器上执行如下命令:
cat /sys/kernel/mm/transparent_hugepage/enabled
如果执行结果是 always,就表明内存大页机制被启动了;如果是 never,就表示,内存大页机制被禁止。在实际生产环境中部署时,建议不要使用内存大页机制:
echo never /sys/kernel/mm/transparent_hugepage/enabled
Redis 的内存空间存储效率?
内存碎片
虽然OS的剩余内存空间总量足够,但应用申请的是一块连续地址空间,但在剩余的内存空间中没有这么大的连续空间,这些剩余空间就是内存碎片。内因:Redis 划分内存是按照一系列固定大小划分的,8字节、16字节、32字节。但如果 Redis 每次向分配器申请内存不一样就有形成碎片的风险。
外因:键值对空间的要求大小不一,以及对首次分配后的修改。
如何判断是否有碎片?
INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86
这里有一个 mem_fragmentation_ratio 的指标
mem_fragmentation_ratio = used_memory_rss/ used_memory
used_memory_rss 是操作系统实际分配给 Redis 的物理内存空间,里面就包含了碎片;而 used_memory 是 Redis 为了保存数据实际申请使用的空间。那么,知道了这个指标,我们该如何使用呢?在这儿,我提供一些经验阈值:
- mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。
- mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了
如何清理碎片?
首先,Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes,命令如下:config set activedefrag yes
这个命令只是启用了自动清理功能,但是,具体什么时候清理,会受到下面这两个参数的控制。这两个参数分别设置了触发内存清理的一个条件,如果同时满足这两个条件,就开始清理。在清理的过程中,只要有一个条件不满足了,就停止自动清理。
- active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;
- active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。
为了尽可能减少碎片清理对 Redis 正常请求处理的影响,自动内存碎片清理功能在执行时,还会监控清理操作占用的 CPU 时间,而且还设置了两个参数,分别用于控制清理操作占用的 CPU 时间比例的上、下限,既保证清理工作能正常进行,又避免了降低 Redis 性能。这两个参数具体如下:
- active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;
- active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。
Redis 的缓冲区溢出问题?
输入缓冲区
主要缓存客户端发来的命令溢出主要情况是:
- 写入了 bigkey,比如一下子写入了多个百万级别的集合类型数据;
- 服务器端处理请求的速度过慢,例如,Redis 主线程出现了间歇性阻塞,无法及时处理正常发送的请求,导致客户端发送的请求在缓冲区越积越多。
使用 client list 命令可以查看输入缓冲区:qbuf 代表输入缓冲区已使用的大小;qbuf-free 指的是尚未使用的大小。如果前者很大后者很小就要注意了。
很遗憾无法调整输入缓冲区的上限(最大 1GB ),再大会影响 Redis 实例。因此只能避免客户端写入 BigKey 以及避免主线程阻塞。
输出缓冲区
主要缓存主线程要返回给客户端的数据。Redis 为每个客户端设置的输出缓冲区也包括两部分:一部分,是一个大小为 16KB 的固定缓冲空间,用来暂存 OK 响应和出错信息;另一部分,是一个可以动态增加的缓冲空间,用来暂存大小可变的响应结果。溢出主要情况是:
- 服务器端返回 bigkey 的大量结果;
- 执行了 MONITOR 命令;(线上不要持续用)
- 缓冲区大小设置得不合理。
通过 client-output-buffer-limit 配置项,可以设置输出缓冲区的大小。具体设置的内容包括两方面:
- 设置缓冲区大小的上限阈值;
- 设置输出缓冲区持续写入数据的数量上限阈值,和持续写入数据的时间的上限阈值。
在具体使用 client-output-buffer-limit 来设置缓冲区大小的时候,我们需要先区分下客户端的类型。对于和 Redis 实例进行交互的应用程序来说,主要使用两类客户端和 Redis 服务器端交互,分别是常规和 Redis 服务器端进行读写命令交互的普通客户端,以及订阅了 Redis 频道的订阅客户端。
给普通客户端设置缓冲区大小时,通常:
client-output-buffer-limit normal 0 0 0
其中,normal 表示当前设置的是普通客户端,第 1 个 0 设置的是缓冲区大小限制,第 2 个 0 和第 3 个 0 分别表示缓冲区持续写入量限制和持续写入时间限制。
因为普通客户端发送请求后会等待结果返回再发下一个,因此这种阻塞式发送一般不会被阻塞导致溢出,因此不做任何限制。
对于订阅式客户端来说,一旦订阅的 Redis 频道有消息了,服务器端都会通过输出缓冲区把消息发给客户端。所以,订阅客户端和服务器间的消息发送方式,不属于阻塞式发送。如果频道消息较多的话,也会占用较多的输出缓冲区空间。因此会做如下配置:
client-output-buffer-limit pubsub 8mb 2mb 60
其中,pubsub 参数表示当前是对订阅客户端进行设置;8mb 表示输出缓冲区的大小上限为 8MB,一旦实际占用的缓冲区大小要超过 8MB,服务器端就会直接关闭客户端的连接;2mb 和 60 表示,如果连续 60 秒内对输出缓冲区的写入量超过 2MB 的话,服务器端也会关闭客户端连接。
主从集群中的缓冲区
复制缓冲区的溢出
在全量复制过程中,主节点在向从节点传输RDB文件时,会继续接受客户端发送的写命令,并保存在复制缓冲区中,等RDB传输结束后,再发送给从节点执行。如果在全量复制时,从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多,最终导致溢出。如何避免呢?
- 控制主节点保存的数据量大小(2-4GB),保证全量复制执行速度。
- 设置合理的复制缓冲区大小
config set client-output-buffer-limit slave 512mb 128mb 60
其中,slave 参数表明该配置项是针对复制缓冲区的。512mb 代表将缓冲区大小的上限设置为 512MB;128mb 和 60 代表的设置是,如果连续 60 秒内的写入量超过 128MB 的话,也会触发缓冲区溢出。
复制积压缓冲区的溢出
主节点在把接受到的写命令同步给从节点时,会将他们写入该缓冲区,一旦从节点发生问题,再次恢复后,从节点就会从该缓冲区中,读取中断期间主节点接受到的写命令,进行增量同步。其实就是 repl_backlog_buffer ,是个环形的缓冲区。Redis作为旁路缓存
只读缓存
当应用读取数据时,先查询 Redis 中数据是否存在。所有的数据写请求会直接发往数据库中增删改并删除掉 Redis 中过时的缓存数据。读写缓存
写请求也会发送到缓存,在缓存中增删改。但最新数据落在 Redis 这种内存数据库一旦发生宕机,会有最新数据丢失的风险。所以推演出**同步直写**和**异步写回**两种策略。同步直写
写请求发给缓存的同时,也发给数据库处理,缓存和数据库都写完后才给客户端返回。提供了数据可靠性保证。但这种策略会降低缓存访问性能,缓存中写请求处理很快,数据库却很慢,等待数据库处理后才返回会增加响应延迟。异步写回
所有请求先在缓存中处理,等这些增改的数据要被从缓存中淘汰出来时写回数据库,保证了响应延迟。但一旦没来得及写回数据库,就有丢失的风险。如果需要对写请求加速,选择读写缓存策略;如果写请求很少,或者只需提升读请求响应速度,选择只读缓存。
在商品大促的场景中,商品的库存信息会一直被修改。如果每次修改都需到数据库中处理,就会拖慢整个应用,此时,我们通常会选择读写缓存的模式。而在短视频 App 的场景中,虽然视频的属性有很多,但是,一般确定后,修改并不频繁,此时,在数据库中进行修改对缓存影响不大,所以只读缓存模式是一个合适的选择。
Redis的淘汰策略
一般来说,建议把缓存容量设置为总数据量的15%-30%,兼顾访问性能和内存空间开销。设置缓存最大容量:CONFIG SET maxmemory 4gb
淘汰策略
LRU算法
Least Recently Used该算法把所有数据组织成一个链表,头和尾分别代表MRU和LRU,如果数据被访问,就会被移到MRU端。如果新数据写入链表时没有空余位置了,那么将新数据放到MRU端,从LRU端删除一项。使用链表管理数据会带来额外的空间开销;而且大量数据被访问时带来频繁的链表移动操作,耗时进而降低性能。
因此,Redis 对其做了简化。默认记录每个数据最近访问的时间戳(RedisObject中的lru字段),在决定淘汰时选出一个候选集合,比较集合中数据的lru字段,淘汰值最小的数据。
CONFIG SET maxmemory-samples 100
当需要再次淘汰时,挑选比候选集合中最小 lru 还小的数据进入集合,集合达到设置的大小时,就继续淘汰数据。
使用建议
-
优先使用 allkeys-lru 策略。这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。
-
如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
-
如果业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
如何解决缓存和数据库数据不一致问题?
如果是读写缓存,那么采用同步直写策略,在业务应用中使用事务同时更新缓存和数据库。只读缓存
增操作直接操作数据库,删改操作直接删除缓存,读操作如果缓存中没有的话就读数据库然后写到缓存中。新增数据
直接写入数据库,此时,缓存中本身就没有新增数据,而数据库中是最新值,所以,此时缓存和数据库的数据是一致的。删改数据
如果先删除缓存,再更新数据库,删除成功而更新失败,会导致其他请求读到旧值。如果先更新数据库,再删除缓存,更新成功而删除失败,也会导致其他请求读到缓存中的旧值。
重试机制:
把要删除或者更新的数据库值暂存到MQ中,如果应用没能成功删除缓存或更新数据库,可以从MQ中重新读取这些值,再次进行删除或更新。如果成功,就从MQ中去除。否则,再次重试,超过规定次数则向业务层发送报错信息。
即使两个操作都没有出错,在有大量并发请求时,应用也有可能读到不一致的数据。
情况一:先删除缓存,再更新数据库
线程A删除缓存后,还没更新数据库,线程B开始读取数据,发现缺失缓存,只能去数据库读取,这会带来两个问题:
- 线程B读到旧值
- 把旧值重新写入缓存
延迟双删解决方案:
在线程A更新数据库后,让其 sleep 一小段时间,再进行一次缓存删除操作。
这段时间是为了让线程B读数据库、写入缓存后,线程A再进行删除。所以这段时间应该大于线程B读数据再写缓存的时间。建议在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。
情况二:先更新数据库,再删除缓存
如果线程A先更新数据库,还没来得及删缓存,线程B读数据会直接读到缓存的旧值。但这种情况很少,对业务影响较小。
如何解决缓存雪崩、击穿、穿透难题?
缓存雪崩
缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。第一个原因:缓存中大量数据同时过期,导致大量请求无法在内存中处理。
解决:
- 避免大量数据设置相同过期时间,增加较小的随机数。
- 服务降级,非核心数据暂停查询,直接返回预定义信息。
第二个原因:Redis 实例宕机
建议:
- 业务系统中实现服务熔断或请求限流机制。如果发现 Redis 实例宕机,数据库所在机器负载飙升,可以启动服务熔断机制,暂停其访问。或者,请求入口控制每秒进入系统的请求数。
- 事前预防。构建 Redis 高可靠集群。
缓存击穿
缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。解决:对于访问特别频繁的热点数据不设置过期时间
缓存穿透
缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力。一般有两种情况:- 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
- 恶意攻击:专门访问数据库中没有的数据。
方案一:缓存空值或缺省值
针对查询的数据,在 Redis 中缓存空值或协商缺省值。应用后续请求再查询时,直接从 Redis 返回空值或缺省值。
方案二:布隆过滤器快速判断数据是否存在,避免从数据库查询,减轻压力
过滤器使用 Redis 实现,能承担很大并发访问压力
方案三:对业务请求进行合法性监测,过滤恶意请求。
如何解决缓存污染问题?
在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。当缓存污染不严重时,只有少量数据占据缓存空间,此时,对缓存系统的影响不大。但是,缓存污染一旦变得严重后,就会有大量不再访问的数据滞留在缓存中。如果这时数据占满了缓存空间,我们再往缓存中写入新数据时,就需要先把这些数据逐步淘汰出缓存,这就会引入额外的操作时间开销,进而会影响应用的性能。
LRU算法只看数据的访问时间,使用 LRU 策略在处理扫描式单次查询操作时,无法解决缓存污染。虽然数据只被访问一次,且后续不会再用,但其lru时间戳非常大,会一直被放在缓存中。
LFU 策略
算法实现和计数策略
在 LRU 基础上,给每个数据增加一个计数器,统计访问次数。当使用 LFU 淘汰数据时,首先根据访问次数筛选,淘汰访问次数最低的数据。如果访问次数相同,再比较访问时效性。具体实现时,把 RedisObject 中的 lru 字段(24 bit)拆分成两部分:
- ldt 值:占 16 bit,表示访问时间戳
- counter 值:占 8 bit,表示访问次数
但 8 bit 最多表示 255 次,封顶后只会比较时间戳,失去了 LFU 的意义,因此采用了优化的计数规则,而不是访问一次就给 counter 加 1。
LFU 策略实现的计数规则是:每当数据被访问一次时,首先,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。
通过控制 lfu_log_factor 大小,可以控制 counter 达到 255 的速度。非线性递增,lfu_log_factor 越大,区分越明显,一般设置为 10
衰减机制
Redis 实现时还设计了 counter 的衰减机制。使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。 lfu_decay_time 越大,衰减的越快,对于短时高频访问数据,可以设置为 1 ,尽快淘汰出去。总结
在实际业务应用中,LRU 和 LFU 两个策略都有应用。LRU 和 LFU 两个策略关注的数据访问特征各有侧重,LRU 策略更加关注数据的时效性,而 LFU 策略更加关注数据的访问频次。通常情况下,实际应用的负载具有较好的时间局部性,所以 LRU 策略的应用会更加广泛。但是,在扫描式查询的应用场景中,LFU 策略就可以很好地应对缓存污染问题了,建议优先使用。此外,如果业务应用中有短时高频访问的数据,除了 LFU 策略本身会对数据的访问次数进行自动衰减以外,建议:优先使用 volatile-lfu 策略,并根据这些数据的访问时限设置它们的过期时间,以免它们留存在缓存中造成污染。
Redis 如何应对并发访问?
Redis 的两种原子操作方法
单命令操作
在实际应用中,数据修改时可能包含多个操作,至少包括读数据、数据增减、写回数据三个操作,Redis 提供了 INCR/DECR 命令,把这三个操作转变为一个原子操作。但如果不是简单地增减数据,而是有复杂的逻辑和其他操作,单命令操作无法完成。Lua 脚本
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。使用 Redis 实现分布式锁
分布式锁的两个要求:- 锁操作具备原子性
- 锁变量所在的共享存储系统的可靠性需要保证
基于单个 Redis 节点实现分布式锁
可以使用 SETNX 和 DEL 命令来实现加锁和释放锁操作。但有两个风险:- 如果一直没释放锁,会影响其他客户端拿锁。可以通过设置锁变量的过期时间,过期自动释放锁。
- 如果客户端A加锁后,客户端B释放了锁,此时客户端A的锁就被误释放了。因此需要区分来自不同客户端的锁操作。使用 unique_value 作为值,在释放锁时需要判断该操作是否来自加锁的客户端。
基于多个 Redis 节点实现高可靠的分布式锁
为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。步骤如下:
- 客户端获取当前时间
- 客户端按顺序依次向 Redis 实例执行加锁操作。如果某个 Redis 实例发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给加锁操作设置一个超时时间。如果客户端在和一个 Redis 实例请求加锁时,一直到超时都没有成功,那么此时,客户端会和下一个 Redis 实例继续请求加锁。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。
- 一旦客户端完成了所有 Redis 实例的加锁操作,客户端计算整个过程的总耗时。需要同时满足两个条件,才认为加锁成功。
- 客户端从超过半数的实例上成功获得锁
- 总耗时没有超过锁的有效时间
满足条件后重新计算锁剩余的有效时间,如果来不及完成共享数据的操作了,可以释放锁,以免出现没完成数据操作,锁就过期了的情况。
Redis 事务如何保证 ACID?
原子性
-
Redis 的事务,正确操作下可以保证原子性。出现语法错误时也会停止执行。
-
但如果事务操作入队时,命令和操作的数据类型不匹配,但 Redis 没有检查出错误,实际执行时会对错误命令报错,执行正确的命令,原子性无法保证。
-
在执行事务的 EXEC 命令时,Redis 实例发生了故障,导致事务执行失败。如果开启了 AOF 日志,只有部分事务操作会被记录,需要使用 redis-check-aof 工具检查 AOF 日志文件,把未完成的事务操作从 AOF 文件中去除。这样一来,使用 AOF 恢复实例后,事务操作不会再被执行,从而保证了原子性。如果没开启 AOF 那无法恢复实例,更谈不上原子性了。
一致性
-
命令入队时报错,事务会放弃执行,保证一致性
-
命令实际执行报错,错误命令不执行,正确命令执行,保证一致性
-
EXEC 命令执行时实例故障:
- 如果没有开启 RDB 和 AOF,实例重启后数据没有了,数据库还是一致的。
- 如果使用了 RDB,事务执行时不会执行快照,因此也是一致的。
- 如果使用 AOF,部分操作被记录,需要使用工具清除事务的部分操作,恢复后也是一致的。
隔离性
事务的隔离性保证,会受到和事务一起执行的并发操作的影响。而事务执行又可以分成命令入队(EXEC 命令执行前)和命令实际执行(EXEC 命令执行后)两个阶段:- 并发操作在 EXEC 命令前执行,此时,隔离性的保证需要使用 WATCH 机制实现,否则无法保证
- 并发操作在 EXEC 命令后执行,此时,可以保证,因为是单线程。
WATCH 机制的作用是,在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证。WATCH 机制的具体实现是由 WATCH 命令实现的。
持久性
如果 Redis 没有使用 RDB 或 AOF,那么事务的持久化属性肯定得不到保证。如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。如果 Redis 采用了 AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况,所以,事务的持久性属性也还是得不到保证。
所以,不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的。
Redis 主从集群的脑裂问题
-
当使用哨兵机制进行主从切换,超过预设数量的哨兵实例和主库心跳超时,主库此时被判断为客观下线。
-
但是,原主库并没有真的故障,被判断下线后又重新开始处理客户端的请求。此时,哨兵还没有完成主从切换,客户端仍可以和原主库通信,客户端发送的写操作在原主库写入数据。
-
主从切换后,原主库执行 slaveof 命令,和新主库进行全量同步,原主库最后清空本地数据,加载新主库的 RDB 文件,导致之前写入数据丢失。
解决方案
通过两个配置项限制主库的请求处理:- min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
- min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。
这两个配置项搭配使用后的要求是:如果在 min-slaves-max-lag 时间内,少于 min-slaves-to-write 个从库实例与主库进行数据复制发送 ACK 消息,主库就不能再接受客户端的请求。这样一来,原主库在假故障期间,因为无法与从库进行同步,也无法接受客户端的请求了。
Redis 如何支撑秒杀场景?
秒杀场景的负载特征对支撑系统的要求
-
瞬时并发访问量非常高。使用 Redis 拦截大部分请求,避免全打数据库
-
读多写少。使用 Redis 完成查询
Redis 在秒杀场景哪些环节发挥作用
-
活动前,用户不断刷新页面,此时应该尽量把详情页面元素静态化,走 CDN 或者浏览器缓存,减轻服务端压力,无需 Redis 参与。
-
活动开始,大量用户点击秒杀按钮,依次发生:查询库存、库存扣减、订单处理。因为每个秒杀请求都会查询库存,该操作承担最大并发压力。同时,库存扣减也要放在 Redis 中完成且与库存查询保持原子性。
-
活动结束后,压力骤减,无需讨论。
因此,真正需要 Redis 的环节就是查询库存和库存扣减。
Redis 使用哪些方法支撑这两个环节
首先是高并发要求,Redis 本身就有该特性,并且也可以使用切片集群,用不同的实例保存不同商品的库存。其次是保证查询库存和库存扣减的原子性执行。
- 使用原子操作无法进行这两个环节。
- 使用 Lua 脚本来进行。
- 使用分布式锁来支撑。先让客户端向 Redis 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减。这样一来,大量的秒杀请求就会在争夺分布式锁时被过滤掉。而且,库存查验和扣减也不用使用原子操作了,因为多个并发客户端只有一个客户端能够拿到锁,已经保证了客户端并发访问的互斥性。可以使用切片集群中的不同实例来分别保存分布式锁和商品库存信息。使用这种保存方式后,秒杀请求会首先访问保存分布式锁的实例。如果客户端没有拿到锁,这些客户端就不会查询商品库存,这就可以减轻保存库存信息的实例的压力了。
如何应对数据倾斜?
数据倾斜有两类:- 数据量倾斜。某个实例上的数据特别多
- 数据访问倾斜。某个实例上的数据被访问得特别频繁
数据量倾斜
bigkey 导致倾斜
某个实例上正好保存了 bigkey。bigkey 的 value 值很大(String 类型),或者是 bigkey 保存了大量集合元素(集合类型),会导致这个实例的数据量增加,内存资源消耗也相应增加。为了避免 bigkey 造成的数据倾斜,一个根本的应对方法是,我们在业务层生成数据时,要尽量避免把过多的数据保存在同一个键值对中。
此外,如果 bigkey 正好是集合类型,我们还有一个方法,就是把 bigkey 拆分成很多个小的集合类型数据,分散保存在不同的实例上。
Slot 分配不均衡导致倾斜、
如果集群运维人员没有均衡地分配 Slot,就会有大量的数据被分配到同一个 Slot 中,而同一个 Slot 只会在一个实例上分布,这就会导致,大量数据被集中到一个实例上,造成数据倾斜。在分配之前,就要避免把过多的 Slot 分配到同一个实例。如果是已经分配好 Slot 的集群,可以先查看 Slot 和实例的具体分配关系,从而判断是否有过多的 Slot 集中到了同一个实例。如果有的话,就将部分 Slot 迁移到其它实例,从而避免数据倾斜。
Hash Tag 导致倾斜
Hash Tag 是指加在键值对 key 中的一对花括号{}。这对括号会把 key 的一部分括起来,客户端在计算 key 的 CRC16 值时,只对 Hash Tag 花括号中的 key 内容进行计算。如果没用 Hash Tag 的话,客户端计算整个 key 的 CRC16 的值。Hash Tag 主要是用在 Redis Cluster 和 Codis 中,支持事务操作和范围查询。因为 Redis Cluster 和 Codis 本身并不支持跨实例的事务操作和范围查询,当业务应用有这些需求时,就只能先把这些数据读取到业务层进行事务处理,或者是逐个查询每个实例,得到范围查询的结果。这样操作起来非常麻烦,所以,使用 Hash Tag 把要执行事务操作或是范围查询的数据映射到同一个实例上,这样就能很轻松地实现事务或范围查询了。
如果使用 Hash Tag 进行切片的数据会带来较大的访问压力,就优先考虑避免数据倾斜,最好不要使用 Hash Tag 进行数据切片。因为事务和范围查询都还可以放在客户端来执行,而数据倾斜会导致实例不稳定,造成服务不可用。
数据访问倾斜
发生数据访问倾斜的根本原因,就是实例上存在热点数据(比如新闻应用中的热点新闻内容、电商促销活动中的热门商品信息,等等)。通常来说,热点数据以服务读操作为主,在这种情况下,我们可以采用热点数据多副本的方法来应对:
把热点数据复制多份,在每一个数据副本的 key 中增加一个随机前缀,让它和其它副本数据不会被映射到同一个 Slot 中。这样一来,热点数据既有多个副本可以同时服务请求,同时,这些副本数据的 key 又不一样,会被映射到不同的 Slot 中。在给这些 Slot 分配实例时,注意把它们分配到不同的实例上,那么,热点数据的访问压力就被分散到不同的实例上了。
注意:热点数据多副本方法只能针对只读的热点数据。如果热点数据是有读有写的话,就不适合采用多副本方法了,因为要保证多副本间的数据一致性,会带来额外的开销。对于有读有写的热点数据,我们就要给实例本身增加资源了,例如使用配置更高的机器,来应对大量的访问压力。
Redis 6.0 新增特性
Redis INFO 命令
首先,无论是运行单实例或是集群,重点关注一下 stat、commandstat、cpu 和 memory 这四个参数的返回结果,这里面包含了命令的执行情况(比如命令的执行次数和执行时间、命令使用的 CPU 资源),内存资源的使用情况(比如内存已使用量、内存碎片率),CPU 资源使用情况等,这可以帮助判断实例的运行状态和资源消耗情况。
另外,当启用 RDB 或 AOF 功能时,需要重点关注下 persistence 参数的返回结果,通过它查看到 RDB 或者 AOF 的执行情况。
如果在使用主从集群,就要重点关注下 replication 参数的返回结果,这里面包含了主从同步的实时状态。