1. 概述

实际上就是一个用C语言开发的数据库,只不过和传统的数据库不同的是,这个Redis中的数据是存在内存中的,因此读写速度非常快,所以Redis被广泛地应用于缓存方向

2. Memcached [另一个缓存中间件]

  • 区别

    • Redis支持丰富的数据类型,而Memcached只支持最简单的K/V数据类型

    • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的适合可以再次加载到内存中使用,而Memecache没有数据持久化机制

    • Redis有灾难恢复机制,可以把内存中的数据持久化到磁盘上

    • Redis可以在服务器内存用完了之后,将不用的数据放到磁盘上,但是Memcached在服务器内存使用完了之后会抛出异常

    • Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据,但Redis原生支持集群模式的

    • Memcached是多线程的,非阻塞IO复用的网络模型,Redis使用单线程的多路IO复用模型(Redis 6.0引入了多线程IO)

    • Memcached过期数据的删除策略只用了惰性删除,而Redis同时使用了惰性删除和定期删除两种策略

  • 共同点

    • 都有过期策略

    • 两者的性能都非常高

    • 都是基于内存的数据库,一般用作缓存使用

3. 可执行工具

  • redis-server -> 启动Redis
  • redis-cli -> Redis命令行客户端
  • redis-benchmark -> Redis基准测试工具
  • redis-check-aof -> Redis AOF持久化文件检测和修复工具
  • redis-check-dump -> Redis RDB持久化文件检测和修复工具
  • redis-sentinel -> 启动Redis Sentinel

4. 常见的数据结构

img

a. string

最大:512MB

概述

简单的key-value类型,虽然Redis是C语言写的,但是Redis并没有使用C的字符串表示,而是自己构建了一种简单动态字符串「Simple Dynamic String,SDS」,相比于C的原生字符串,Redis的SDS不光可以保存文本数据,还可以保存二进制数据,并且获取字符串长度复杂度为O(1),而C语言字符串为O(n)。此外,Redis的SDS API是安全的,不会造成缓冲区溢出

常用命令

set,get,strlen,exists,decr,incr,setex

内部编码

  • int -> 8个字节的长整形
  • embstr -> 小于等于39个字节的字符串
  • raw -> 大于39个字节的字符串

应用场景

缓存、限流、计数器、分布式锁、分布式Session

b. list

概述

  • list实际上就是链表,特点是易于数据元素的插入和删除并且可以灵活地调整链表的长度,但是链表的随机访问较为困难,Redis中的list实际上是一个双向链表,支持反向查找和遍历

  • 用于存储多个有序的字符串,列表中的每个字符串称之为元素,一个列表最多可以存储2^32 - 1个元素

特点

  • 列表中的元素是有序的

  • 列表中的元素是可以重复的

常用命令

rpush,lpop,lpush,rpop,lrange,llen

img

内部编码

  • ziplist「压缩列表」

    • 当列表的元素个数小于list-max-ziplist-entries配置(默认是512个)时,同时列表中的每个元素的值都小于list-max-ziplist-value配置(默认64字节)时,Redis会选用ziplist来作为列表的内部实现来减少内存的使用

    • 单个ziplist节点最大能存储8kb,超过则会进行分裂,将数据存储在新的ziplist中

    • list-compress-depth 1 0表示所有节点都不进行压缩,1表示从头节点往后走一个,尾节点往前走一个不用压缩,其他都压缩

  • linkedlist「链表」

当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现

  • quicklist

    (Redis 3.2版本提供)

是以一个ziplist为节点的linkedlist,结合了ziplist和linkedlist两者的优势

应用场景

微博关注人时间轴列表、简单队列

c. hash

概述

类似于JDK1.8前的HashMap,内部实现主要是由数组+链表的形式,Redis的hash做了更多的优化,适合用于存储对象

常用命令

hset,hmset,hexists,hget,hgetall,hkeys,hvals

内部编码

  • ziplist「压缩列表」

    • 当哈希类型元素个数小于hash-max-ziplist-entries配置(默认为512个)时,同时所有的值都小于hash-max-ziplist-value配置(默认是64字节)时,Reids会使用ziplist作为hash的内部实现

    • ziplist使用的是更加紧凑的结构实现多个元素的连续存储,因此在节省内存方面比hashtable更加优秀

  • hashtable「哈希表」

    • 当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为hash的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)
    • 需要控制hash在该编码上的转换,因为hashtable会消耗更多的内存

应用场景

存储用户信息、用户主页访问量、组合查询

d. set

概述

类似于Java中的HashSet,Redis中的set类型是一种无序集合,集合中的元素没有先后顺序,主要用来存储不希望出现重复的数据,同时可以基于set轻易实现交集并集和差集的操作

常用命令

sadd,spop,smembers,sismember,scard,sinterstore,sunion

内部编码

  • intset「整数集合」

当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认为512个)时,Redis会选用intset作为集合的内部实现,从而减少内存的使用

  • hashtable「哈希表」

当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现

应用场景

  • 赞、踩、标签、好友关系
  • 点赞 -> SADD like:{消息ID} {用户ID}
  • 取消点赞 -> SREM like:{消息ID} {用户ID}
  • 检查用户是否点过赞 -> SISMEMBER like:{消息ID} {用户ID}
  • 获取点赞的用户列表 -> SMEMBERS like:{消息ID}
  • 获取点赞用户数 -> SCARD like:{消息ID}

e. sorted set

概述

和set相比,多了一个权重参数score,使得集合中的元素能够按score进行有序排列,可以通过score的范围来获取元素的列表

常见命令

zadd,zcard,zscore,zrange,zrevrange,zrem

内部编码

  • ziplist「压缩列表」

当有序集合的元素个数小于zset-max-ziplist-entries(默认128个)配置,同时每个元素的值都小于zset-max-ziplist-value(默认64字节)配置时,Redis会用ziplist来作为有序集合的内部实现

  • skiplist「跳跃表」

当ziplist条件不满足时,有序集合会使用skiplist来作为内部实现,因为此时的ziplist的读写效率会下降

应用场景

需要对数据根据某个权重进行排序的场景,比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜等

f. bitmap

概述

存储的是连续的二进制数字(0和1),通过bitmap,只需要一个bit位来表示某个元素对应的值或者状态,key就是对应元素本身,因此bitmap本身会节省较大的存储空间

常用命令

setbit,getbit,bitcount,bitop

应用场景

适合需要保存状态信息(比如是否签到,是否登录),并需要进一步对这些信息进行分析的场景,比如用户签到情况

5. 单线程如何监听大量客户端连接

通过IO多路复用程序来监听来自客户端的大量连接,它会将感兴趣的事件及类型(读,写)注册到内核中并监听每个事件是否发生img
查看最大连接客户端数量:CONFIG GET maxclients

6. Redis为什么不使用多线程

虽然说Redis是单线程模型,但是实际上它从4.0之后的版本开始就已经加入了对多线程的支持

7. Redis 6.0前不使用多线程的原因

  • 单线程更易于维护

  • Redis的瓶颈不是CPU,而是内存和网络

  • 多线程会出现死锁,线程上下文切换等问题,会影响性能

8. Redis 6.0后加入多线程的原因

  • 主要是为了提高网络IO读写性能

  • 执行命令仍然是单线程顺序执行

9. 单线程为什么还能这么快

因为所有的数据都位于内存中,读写都是内存级别的运算,而且单线程也避免了多线程的切换性能问题

10. Redis是单线程吗

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

11. 数据缓存的过期时间有什么作用

  • 有助于缓解内存的消耗

  • 避免手动多次判断数据是否过期

12. Redis如何判断数据是否过期的

Redis通过一个过期字典(可以看作是哈希表),来保存数据过期的时间,过期字典的键指向Redis数据库中的某个键,过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间

13. 如何保证缓存和数据库的一致性

旁路缓存模式「Cache Aside Pattern」: 实际上就是更新DB的同时,删除Redis中对应的缓存

  • 缓存删除失败

    • 缓存失效时间变短

    • 增加cache更新重试机制

14. 过期数据的删除策略

Redis中默认采用的是定期删除+惰性删除的结合方式

  • 惰性删除: 只会在取出key的时候对数据进行过期检查,这样对CPU最友好,但可能会造成太多过期key没被删除
  • 定期删除: 每隔一段时间就抽取一批key执行删除过期key的操作,并且Redis底层会通过限制删除操作执行的时长和频率来减少操作对CPU时间的影响

15. 内存淘汰机制「8种」

Redis对于过期键有三种清除策略:

  • 被动删除:当读 / 写一个已经过期的Key时,会触发惰性删除策略,直接删除掉这个过期的Key
  • 主动删除:由于惰性删除策略无法保证冷数据被即时删掉,所以Redis会定期主动淘汰一批已过期的Key
  • 当前已用内存超过maxmemory限定时,触发主动清理策略

主动清理策略在Redis 4.0前一共有6种,在Redis 4.0之后,又一共新增了2种,目前共计8种,如下:

  • 针对设置了过期时间的key做处理

    • volatile-lru「least recently used」: 从已设置过期时间的数据集中挑选最近最少使用的数据进行淘汰
    • volatile-lfu「least frequently used」: 从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
    • volatile-ttl: 从已设置过期时间的数据集中挑选即将要过期的数据进行淘汰
    • volatile-random: 从已设置过期时间的数据集中任意选择数据进行淘汰
  • 针对所有的key做处理

    • allkeys-lru「least recently used」: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key

    • allkeys-lfu「least frequently used」: 当内存不足且写入数据时,会在所有的键空间中,删除最不经常使用的key

    • allkeys-random : 从数据集中任意选择数据淘汰

  • 不处理

    • no-eviction : 禁止驱逐数据,当内存满时写入数据,新写入操作将会报错
  • LRU算法 -> 最近最少使用:淘汰很久没有访问过的数据,以最近一次访问时间作为参考
  • LFU算法 -> 最不经常使用:淘汰最近一段时间被访问次数最少的数据,以次数作为参考

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重,这时候可能使用LFU会更好一点

需要根据自身的业务类型,配置好maxmemory-policy,默认为noeviction,推荐使用的是volatile-lru,如果不设置最大内存,当Redis内存超过物理内存限制时,内存的数据会开始和磁盘产生频繁的交互,让Redis的性能急剧下降,当Redis运行在主从模式下时,只有主节点才会执行过期删除策略,然后把删除操作同步给从节点

16. 持久化机制「2种」

RDB持久化「快照,snapshotting」

Redis可以通过创建快照来获得存储在内存中的数据在某个时间节点上的副本,Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在本地以便重启服务器的时候使用

默认配置

關閉RDB只需要將所有save保存策略註釋掉即可

  • save 900 1

    • 意思是每900秒,如果至少有一个key发生变化,那么Redis会自动触发指令创建快照
  • save 300 10

    • 在300秒时,如果至少有10个key发生变化,则会触发创建快照
  • save 60 10000

    • 在60秒时,如果至少有10000个key发生变化,则会触发创建快照

在默認情況下,Redis會將內存數據庫快照保存在名字為dump.rdb的二進制文件中,同時也可以手動執行命令生成RDB快照,進入Redis客戶端執行命令save或者bgsave可以生成dump.rdb文件,每次命令執行都會將所有Redis內存快照到一個新的RDB文件中,並覆蓋原有RDB快照文件

bgsave的寫時複製技術(Copy-On-Write,COW),在生成快照的同時,依然可以處理寫命令,簡單的說,bgsave子線程是由主線程fork生成的,可以共享主線程的所有內存數據,bgsave子線程運行後,開始讀取主線程的內存數據,並把它們寫入RDB文件,此時如果主線程對這些數據也都是讀操作,那麼主線程和bgsave子線程相互不影響 ,但是如果主線程要修改一塊數據,那麼這塊數據就會被複製一份,生成該數據的副本,然後,bgsave子線程會把這個副本數據寫入RDB文件,而在這個過程中,主線程仍然可以修改原來的數據img

AOF持久化「append-only file」

与快照持久化相比,AOF持久化的实时性会更好一些,因此已经成为了主流的持久化方案,默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly yes参数开启

开启AOF持久化之后每执行一条会更改Redis中数据的指令,Redis就会将该命令写入到硬盘中的AOF文件,AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof

從1.1版本開始,Redis增加了一種完全耐久的持久化方式:AOF持久化,將修改的每一條指令記錄進文件appendonly.aof中(先寫入os cache,每隔一段時間fsync到磁盤)

aof文件格式

RESP協議格式

  • *: 代表命令有多少個參數
  • $: 代表這個參數有幾個字符

aof重寫

  • AOF會定期根據內存的最新數據生成AOF文件
    減少中間不必要的操作過程語句,直接生成最終結果語句,好處是節省存儲空間

  • 控制AOF自動重寫頻率

    • auto-aof-rewrite-min-size 64mb

      • aof文件至少要達到64M才會自動重寫,文件太小恢復速度本來就很快,因此重寫的意義不大
    • auto-aof-rewrite-percentage

      • aof文件自上次重寫後文件大小增長了100%則會再次觸發重寫
  • 三种方式

    • appendfsync always

      • 每次有数据修改发生时都会写入AOF文件
    • appendfsync everysec

      • 每秒钟同步一次,显示地将多个命令同步到硬盘中
    • appendfsync no

      • 让操作系统决定何时进行同步

混合持久化

Redis 4.0加入aof-use-rdb-peramble yes用以解決啟動花費時間大

重啟Redis時,我們使用RDB來恢復內存狀態,因為會丟失大量數據,因此我們通常使用AOF日誌重放,但是重放AOF日誌性能相對RDB來說要慢很多,這樣Redis實例很大的情況下,啟動就需要花費很長的時間

如果開啟了混合持久化,AOF在重寫的時候,不再是單純地將內存數據轉換為RESP命令寫入AOF文件,而是將重寫這一刻之前的內存做RDB快照處理,並且將RDB快照內存和增量的AOF修改內存數據的命令存在一起,都寫入新的AOF文件,新的文件一開始不叫appendonly.aof,等到重寫完新的AOF文件才會進行改名,覆蓋原有的AOF文件,完成新舊兩個AOF文件的替換

於是在Redis重啟的時候,可以先加載RDB的內存,然後再重放增量AOF日誌就可以完全替代之前的AOF全量文件重放,因此重啟效率大幅得到提升

img

17. 事务

  • 可以通过MUTIL,EXEC,DISCARD,WATCH等命令来实现事务「transaction」功能

  • Redis不支持roll back,因此不满足原子性(也不满足持久性)

18. 主從工作原理

如果為master配置了一個slave,不管這個slave是否第一次連接上master都會發送一個PSYNC給master請求複製數據,master收到PSYNC命令後,會在後台進行數據持久化通過bgsave生成最新的rdb快照文件,持久化期間,master會繼續接收客戶端的請求,它會把這些可能修改數據集的請求緩存在內存中,當持久化完成後,master會把這份rdb文件數據發送給slave,slave收到後進行持久化生成rdb,然後加載至內存中,隨後master再將緩存在內存中的命令發給slave做最後的同步


當master與slave之間的連結由於某些原因斷開時,slave能夠自動重新連接master,如果master收到了多個slave並發連接請求,它只會進行一次持久化,而不是一個連接一次,然後再把這一份持久化的數據發送給多個並發連接的slave

img


當master和slave斷開重連後,一般都會對整份數據進行複製,但從Redis2.8版本開始,Redis改用可以支持部分數據複製的命令PSYNC去master同步數據,slave與master能夠在網絡連接斷開重連後只進行部分的數據複製(也就是斷點續傳)

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始,如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列中,则会重新进行一次全量数据的复制

img

客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完成后再一次性读取服务的响应,可以极大地降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。Redis必须在处理完所有命令前先缓存起所有命令的处理结果,打包的命令越多,缓存消耗内存也越多
pipeline前面命令的失败并不会影响后续命令的执行

19. 常用命令

  • keys *: 打印所有的键 时间复杂度O(n)
  • dbsize: 键总数
  • exists key: 检查键是否存在
  • del key: 删除键
  • type key: 查看键类型
  • scan cursor: 提供了3个参数,第一个是cursor整数值(hash桶子的索引值),第二个key是正则表达式,第三个参数则是一次遍历的数量(参考值,底层可能不按照这个数量走 ),且第三个参数并不是符合条件的结果数量,第1次遍历时,cursor的值为0,然后将返回结果中第一个整数值作为下一次遍历的cursor,一直遍历到返回的cursor值为0时结束
    如果在scan时遇到有键的变化,则遍历效果可能会出现重复的键
  • info: 用于查看Redis信息
    • instantaneous_ops_per_sec: 每秒执行多少次指令
    • used_memory: Redis分配的内存总量,包含了进程内部开销&数据占用内存
    • used_memory_human: Redis分配的内存总量
    • used_memory_rss_human: 像操作系统申请的内存大小
    • used_memory_peak: Redis内存消耗峰值
    • maxmemory: 配置最大可使用的内存值,默认为0则表示不限制

20. Redis Lua脚本

允许开发者使用Lua语言编写脚本传到Redis中执行

  • 减少网络开销: 本来5次网络请求,可以用一个请求完成,原先5次请求的逻辑放在Redis服务器上完成,使用脚本,减少了网络往返时延
  • 原子操作: Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,管道不是原子的,不过Redis的批量操作命令是原子的
  • 替代redis的事务功能: Redis自带的事务功能很鸡肋,报错不支持回滚,而Redis的lua脚本几乎实现了常规的事务功能,支持报错回滚操作,官方推荐如果要使用Redis的事务功能可以用Redis Lua替代

不要在Lua脚本中出现死循环和耗时的运算,否则Redis会阻塞,将不接收其他命令,所以使用时要注意不能出现死循环、耗时的运算,Redis是单进程、单线程执行脚本,管道不会阻塞Redis

21. Sentinel哨兵模式

  • 哨兵模式是特殊的Redis服务,不提供读写服务,主要用来监控Redis实例节点,哨兵架构下client端第一次找出Redis的主节点,后续就直接访问Redis的主节点,不会每次都通过Sentinel代理访问Redis的主节点,当Redis的主节点发生变化,哨兵会第一时间感知到,并且将新的Redis主节点通知给client端(这里的Redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

  • 在Redis 3.0以前的版本实现集群一般是借助哨兵Sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率

22. 高可用集群模式

概述

Redis集群是由一个或多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性,Redis集群不需要sentinel哨兵,也能完成节点移除和故障转移的功能,需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,可线性扩展至上万个节点(官方推荐不超过1000个节点)

原理分析

Redis Cluster将所有数据划分为16384个槽位(slots),每个节点负责其中一部分槽位,槽位的信息存储于每个节点中,当Redis Cluster的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地,这样当客户端要查找某个key时,可以直接定位到目标节点

槽位定位算法

  • 集群默认会对key值使用CRC16算法进行hash得到一个整数值,然后用这个整数值对16384进行取模来得到具体槽位
    HASH_SLOT = CRC16(key) mod 16384

跳转重定位

  • 当客户端向一个错误的节点发出了指令,该节点会发现指令的key所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据,客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存

集群节点间的通信机制

  • Redis Cluster节点间采取的是gossip协议进行通信

  • 维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据)的两种方式:集中式 or gossip

    • 集中式
      • 优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以感知到,不足在于所有的元数据的更新压力都全部集中在一个地方,可能导致元数据的存储压力,很多中间件都会借助zookeeper集中式存储元数据
    • gossip
      • 包含多种消息,包括pong,ping,meet,fail等,优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力,但信息有点滞后
      • 每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口,每个节点每隔一段时间都会往另外几个节点发送ping消息,痛失其他节点接收到ping消息后会回复pong消息
        • meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他的节点进行通信
        • ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据(类似感知到集群节点增加 和 移除,hash slot信息等)
        • pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新
        • fail: 某个节点判断另一个节点fail后,就发送fail给其他节点,通知其他节点,指定的节点宕机了

选举原理

  • 当slave发现自己的master变成了FAIL状态时

    • slave发现自己的master变为了FAIL

    • 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST

    • 其他的节点收到了该信息,但只有master做响应,判断请求者的合法性,并在确认后发送一个FAILOVER_AUTH_ACK,对每一个epoch只发送一个ACK

    • 尝试FAILOVER对slave收集master返回的FAILOVER_AUTH_ACK

    • slave收到超过半数master的ack后变成新的master (所以这里就是为什么集群至少需要3个主节点,如果只有2个,当其中一个挂了,只剩一个主节点是不能选举成功的)

    • slave广播pong消息通知其他集群节点

  • 从节点并不是当master一进入FAIL状态时就立马发起选举,而是有一定的延迟,确保FAIL状态在集群中传播
    延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
    SLAVE_RANK表示的是此slave已经从master复制数据的总量的rank,rank越小则表示数据越新
    这种计算公式下,持有最新数据的slave将会首先发起选举

集群脑裂数据丢失问题

  • Redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失

  • 规避方法可以在Redis配置中加入参数,这种方法不能百分百避免数据丢失
    min-replicas-to-write 1 表示写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2

  • 配置参数再一定程度上回影响集群的可用性,比如slave如果少于1个,即便leader是正常的,也无法写入数据

集群是否完整才能对外提供服务

可通过cluster-require-full-coverage参数做配置,当负责一个插槽的主库下线且没有响应的从库进行故障恢复时,集群仍然可用,如果为yes则集群不可用
默认情况下,一台master挂了且没有slave,则集群会显示为不可用状态

Redis集群为什么至少需要3个master节点,并且推荐节点数为技术

  • 因为新的master选举需要大于半数集群的master同意才能选举成功,如果只有2个master则达不成选举成功的条件

  • 奇数个master节点和偶数个master节点带来的效益是一样的,因此奇数个master节点主要是从节省机器资源的角度出发的

集群对批量操作命令的支持

为了防止批处理命令的情况下不同的key散落在不同的分区,可以在key对前面加{xx},这样数据分片hash计算的时候算的就只会是大括号里头的值,能够确保不同的key能够落在同一个slot中

23. 慢查询分析

客户端命令生命周期:发送命令 - 命令排队 - 命令执行 - 返回结果

Redis提供了slowlog-log-slower-than用于处理识别慢查询的预设阈值(单位为微秒,默认值为10,000,其中1秒=1000毫秒=1,000,000微秒),即如果超过该阈值,则会被记录为是慢查询,如果该值为0,则会记录下所有的命令,如果该值小于0,则不会记录任何命令

Redis同时也提供了slow-log-max-len配置用于说明慢查询日志最多可以存储多少条,实际上Redis采用了内置的列表用于存储慢查询日志,因此如果达到该配置的最大记录数量,则会将一开始加入的日志移除

可以通过以下命令进行配置

1
2
3
4
config set slowlog-log-slower-than 20000
config set slowlog-max-len 1000
# 需要该命令用于将修改的配置存储至配置文件中
config rewrite
  • 获取慢查询日志:slowlog get [n]

  • 获取慢查询日志列表当前长度: slowlog len

  • 慢查询日志清理: slowlog reset

24. Redis 6.x新特性

a. 多线程

Redis 6.0开始提供了多线程的支持,Redis 6.0以前实际上也可以称之为是多线程版本,但是多线程指的是:删除大Key,持久化等操作,而用户命令请求依然是单线程模型,然而在Redis 6.0开始提供了多线程的读写IO,但是最终执行用户命令的线程依然是单线程,如此这般,就不会有多线程数据的竞争关系

Redis 6.0前的命令执行模式: read -> execute command -> write > next read -> next execute command -> next write

以上执行模式限定为单线程模式,然而从Redis 6.0起线程模型可以通过以下参数配置应用多线程模型

1
2
# 4个IO线程中 其中1个为main线程,main线程负责IO读写和命令执行操作
io-threads 4

根据如上配置,有3个IO线程只执行网络IO中的写操作,也就是说具体的READ和命令执行都是由main线程来执行,最后多线程将数据写回至socket发回至客户端,整体如下

image-20221126202626497

也可以通过io-threads-do-reads yes参数配置让IO线程支持读操作,达成以下效果

image-20221126202727837

图片来源:后端研究所

b. 客户端缓存

当客户端访问某个KEY时,服务端将记录key和client,客户端拿到数据后进行缓存,当key再次被访问时,key将直接被返回,避免与Redis的再次交互,节省服务端资源,当数据被其他请求修改时,Redis也将通知客户端做key失效的动作,之后客户端再关于对这个key请求即可获取到最新数据

c. ACL

支持对命令访问和执行进行用户级别的权限控制,默认情况下还是可以执行任意的命令,具体可参考https://redis.io/docs/management/security/acl/

25. 缓存穿透

a. 概念

缓存穿透指的是查询一个根本就不存在的数据,因此缓存层 & 存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层,然而当缓存层查询不到时,这个请求会继而往下发放至业务层做查询,此时将导致每次都如果进行数据库查询,此时缓存并起不到保护后端存储的意义,上述称之为缓存穿透

b. 原因

  • 自身业务代码 or 数据出现问题
  • 一些恶意攻击,爬虫导致大量空命中

c. 解决方案

  • 可以将查询不到的空对象也一并加入到缓存中,并设置一个过期时间
  • 可以使用布隆过滤器过滤掉一些不存在的数据

d. 布隆过滤器

Java中可以引入org.redisson.redisson

对于布隆过滤器来说,某个值存在时,这个值可能不存在,当它说不存在时,对应的值是一定不存在

image-20221129090359760

布隆过滤器实际上就是一个大型的位数组 & 几个hash函数组成

当像布隆过滤器添加KEY时,会采用多个hash算法对这个KEY进行hash运算,得到几个hash索引(具体几个取决于采用了多少个hash算法),随后在位数组对应的下标将该槽位置为1

当像布隆过滤器查询KEY时,也会采用多个hash算法对这个KEY进行hash运算,得到相对应的hash索引,随后前往位数组判断对应的索引下标值是否都为1,如果存在一个索引下标的值不为1,说明这个KEY一定不存在

布隆过滤器比较适用于数据命中不高,数据相对固定,实时性低(通常是数据集过大)的应用场景,代码维护较为复杂,但是呢!缓存空间占用很小

需要注意的是要把所有的缓存数据提前放入到布隆过滤器中,并且增加数据时也要同步更新,同时布隆过滤器无法删除数据,如果需要删除就得重新初始化数据

26. 缓存失效 / 击穿

由于大批量缓存在同一时间失效可能会导致大量请求同时穿透缓存直达数据库,会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间,也就是可以在设置一个常规过期时间后再额外追加一个random范围内的时间,这样不至于同一时间点缓存都失效掉给数据库造成太大压力

27. 缓存雪崩

a. 概述

缓存雪崩指的是缓存层支撑不住 or 宕掉后,流量会直接打向后端存储层

由于缓存层承载着大量请求,有效地保护了存储层,但如果某些原因直接导致了缓存成不能提供服务,则大量请求会直接落入到存储层,导致级联宕机的情况

b. 预防

  • 保证缓存层服务的高可用性,比如使用Redis Sentinel 或者 Redis Cluster
  • 依赖隔离组件为后端限流熔断并降级,使用Sentinel或者Hystrix限流降级组件,服务降级,可以针对不同的数据采取不同的处理方式
    • 当业务应用访问的是非核心数据,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息,空值或错误提示信息
    • 当业务应用访问的是核心数据,仍然允许查询缓存,如果缓存缺失,可以继续通过数据库读取
  • 提前预演缓存层宕机后,应用以及后端的负载情况以及可能出现的问题,并在此基础上做一些预案设定

28. 热点缓存Key重建优化

当一个Key为热点Key时,并发量非常大,并且重建缓存无法在短时间内完成,可能是一个复杂计算,那么在这个Key缓存失效的瞬间,会有大量线程来重建缓存,造成后端负载过大,有可能直接打穿应用,解决该问题的核心是在于避免大量的线程同时重建缓存,可以利用互斥锁来解决,只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完成后再进行数据查询

29. 缓存 & 数据库双写不一致

在大并发下,同时操作数据库与缓存会存在不一致问题,如下

a. 双写不一致情况

image-20221129230528254

b. 读写并发不一致

image-20221129230552182

c. 解决方案

  • 对于并发几率很小的数据,不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可,即便是并发很高,如果业务上可以容忍时间的缓存数据不一致,则缓存加上过期时间依然可以解决大部分业务对于缓存的要求
  • 如果无法容忍数据不一致,可以通过读写锁来保证并发读写或者写写的时候按照顺序排好队执行,读读的时候其实相当于无锁
  • 可以利用三方开源中间件达到数据库 - 缓存一致性,比如阿里开源的canal通过监听数据库的binlog日志实时地修改缓存,但是增加了系统的复杂度
  • 如果是写多读多的情况同时又无法容忍缓存数据不一致,则没有必要增加缓存,可以直接操作数据库,放入缓存的数据应该是对实时性 & 一致性要求不是很高的数据

30. 开发规范 & 性能优化

a. key名设计

  • 可读性和可管理性:以业务名为前缀(防止Key冲突),用冒号分隔,如trade:order:1
  • 间接性:保证语义的前提下,需要控制Key的长度,当Key较多时,内存占用也会随之变大,如user:{uid}:friends:messages:{mid}可以简化为u:{uid}:fr:m{mid}
  • 不要包含特殊字符:特殊字符指的是空格、换行、单双引号以及其他的转义字符

b. value设计

  • 避免bigkey:在Redis中,一个字符串最大可存储512M,一个二级数据结构最大可以存储2^32^ - 1个元素,但实际上如果为以下状况,则可以判定为是bigkey,bigkey的删除不能使用del直接删除,而应该使用hscanf, sscan, zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个几百万的zset设置过期时间为1小时,则会触发del操作,造成阻塞)
    • 字符串类型:单个value值很大,一般超过10kb则认为是bigkey
    • 非字符串类型:hash, list, zset, set的big体现在元素个数过多,一般超过5000个则认为是bigkey

c. 命令使用

  • O(N)命令应该关注N的数量:如hgetall, lrange, smembers, zrange等并非不能使用,但是需要明确N的值,有遍历的需求可以使用hscan, sscan, zscan代替

  • 禁用命令:禁止在生产环境中使用keys, flushall, flushdb等操作,可以通过redis的rename机制禁用命令,或者使用scan的方式渐进式处理

  • 合理使用select:redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰

  • 使用批量操作提高效率 (需要控制批量的个数,一般保持在500个内)

    • 原生命令:如mget, mset,是原子操作
    • 非原生命令:可以使用pipeline提高效率,非原子操作,同时需要客户端 & 服务端同时支持
  • Redis的事务功能较弱,不建议过多使用,应该使用Lua语言代替

d. 客户端使用

  • 避免多个应用使用一个Redis实例
  • 使用带有连接池的组件,可以有效控制连接,同时提高效率

image-20221130083240381

  • maxTotal: 最大连接数,早期为maxActive,需要考虑的因素比较多,如下
    • 业务希望Redis的并发量
    • 客户端执行命令的时间
    • Redis资源状况nodes * maxTotal 不能超过Redis的最大连接数maxClient
    • 资源开销:希望控制住空闲连接(指的是连接池可以立刻使用的连接),但不希望因为连接池的频繁释放 & 创建连接导致不必要的开销
    • 如果一个命令时间平均耗时为1ms,一个连接的QPS大约为1000,业务期望的QPS为50000,则理论上需要的资源池大小为 50000 / 1000 = 50,但还需要额外预留,则该maxTotal值可以相对大一些
    • maxTotal的值并不是越大越好,一方面会占用太多客户端 & 服务端的资源,另一方面对于Redis这种高QPS的服务器,一个大命令导致的阻塞即便有很大的连接池也没啥作用
  • maxIdle & minIdle: maxIdle实际上是业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置过小,否则会有新连接的开销
    • 连接池最佳的性能是maxTotal == maxIdle ,这样就避免了连接池伸缩带来的性能干扰,但是如果并发量不大或者maxTotal设置过高,会导致不必要的连接资源浪费,一般推荐maxIdle可以设置为上面业务期望QPS计算出来的理论连接数,而maxTotal则可以再放大一倍
    • maxIdle: 当超过的连接执行完业务后会慢慢被移出连接池释放掉
    • minIdle: 意思是至少需要保持的空闲连接数,在使用连接的过程中,如果连接数超过了minIdle,那么就继续建立连接,如果超过了maxIdle,则当超过的连接执行完业务后会慢慢地被移出连接池释放掉
  • 高并发下建议客户端添加熔断功能
  • 设置合理的密码,如果有必要的话可以使用SSL加密访问

31. bigkey

主要体现在于key对应的value所占的内存空间比较大

a. 危害

  • 内存空间不均匀:例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均
  • 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大
  • 网络拥塞:每次获取bigkey产生的网络流量较大,假设一个bigkey为1MB,每秒访问量为1000,则每秒产生1000MB的流量

b. 如何发现

redis-cli --bigkeys可以命令统计bigkey的分布,手动判断一个key是否为bigkey,可以通过debug object key来查看serializedlength属性,其中serializedlength不代表真实的字节大小,它返回对象使用的是RDB编码序列化后的长度,值会偏小,但是对于排查bigkey有一定辅助作用

  • 被动收集:抛出异常时打印出所操作的key,方便排查bigkey问题
  • 主动检测scan + debug object,如果怀疑存在bigkey,则可以使用scan命令渐进地扫描出所有的key,分别计算出每个key的serializedlength,找到对应bigkey进行相应的处理和报警