Redis(1):数据结构、持久化、缓存
1 Redis
Redis(REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 是一种基于内存的数据库(内存数据库,支持持久化),对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。Redis 存储的是 KV 键值对数据。
Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片集群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
redis为什么快
-
内存存储:Redis将数据存储在内存中,相比于传统的磁盘存储,内存的读写速度更快,可以达到微秒级的响应时间。(高性能)
-
单线程模型:Redis采用单线程模型,避免了多线程之间的竞争和锁的开销,避免了频繁的上下文切换
-
非阻塞IO:Redis使用了异步的非阻塞IO模型,通过IO多路复用技术(如epoll、kqueue等)实现高效的网络通信,提高了并发处理能力。基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用。(高并发)
-
简单的数据结构:Redis支持多种简单的数据结构,如字符串、哈希表、列表、集合和有序集合等,这些数据结构的实现都经过了优化,使得Redis在处理这些数据结构时更加高效。
2 Redis单线程/多线程
2.1 Redis线程模型
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
Redis针对大键值对异步删除。
-
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
-
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
[!note] 事件
- 文件事件(file event) : Redis 服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象
- 时间事件(time event ) : Redis 服务器中的一些操作(比如serverCron 函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
Redis基于Reactor模式开发了文件事件处理器,文件事件处理器以单线程方式运行,使用I/O多路复用(multiplexing)程序监听多个套接字。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
2.2 Redis6.0引入多线程
Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的
这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。 所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理。
2.3 IO多路复用select/poll/epoll
-
select: select允许程序同时监控多个文件描述符的读写状态,但受限于位图大小,且每次调用都需从用户空间向内核空间复制位图,性能开销大。
-
poll: poll改进了select,使用数组存储文件描述符,无位图大小限制,可处理更多文件描述符。但同样存在每次调用时的用户空间到内核空间的复制开销。
-
epoll: epoll是Linux特有的高效IO多路复用机制,基于事件驱动,无需轮询,通过注册感兴趣的事件并在事件发生时通知应用程序,适合处理大量并发连接,性能优越且资源消耗低。
3 数据结构
3.1 SDS
简单动态字符串(simple dynamic string, SDS)
-
O(1)时间获取字符串长度
-
C字符串不记录自身长度可能导致缓冲区溢出
-
减少修改字符串带来的内存重分配次数,每次增长或者缩短一个C字符串,程序都要对保存这个C字符串的数组进行一次内存重分配操作。空间预分配、惰性空间释放(未使用空间)
-
二进制安全,C字符串除了末尾之外,字符串里面不能有空字符,否则被认为字符串末尾。这些限制使得C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据
3.2 链表
-
链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。
-
每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
-
每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
-
因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
-
通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。
3.3 dict(字典)
-
字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
-
Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
-
Redis使用MurmurHash2算法来计算键的哈希值。
-
哈希表使用链地址法来解决键冲突。
使用两个哈希表:渐进性rehash
为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]里面的所有键值对全部 rehash 到 ht[1], 而是分多次、渐进式地将 ht[0]里面的键值对慢慢地 rehash 到 ht[1]。
3.4 skiplist(跳跃表)
跳跃表(skiplist) 是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。跳表在原始链表基础上,建立多级索引,通过多级索引检索定位将增删改查的时间复杂度变为O(log n)。
3.5 intset(整数集合)
3.6 ziplist(压缩列表)
每个压缩列表节点可以保存一个字节数组或者一个整数值
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
4 数据类型应用场景
-
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
-
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。基于 list 实现分页查询,实现高性能分页。
-
Hash 类型:缓存对象、购物车等。
-
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
-
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
-
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
-
HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
-
GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
-
Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
5 数据类型
字符串对象、列表对象、哈希对象、集合对象、有序集合
Redis使用对象来表示数据库中的键和值, 每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值( 值对象)。
对于Redis 数据库保存的键值对来说,键总是个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。
5.1 String(字符串)
字符串对象的编码可以是int、raw 或者embstr 。
String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是512M
。
String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
5.1.1 应用场景
需要存储常规数据的场景
-
举例:缓存 Session、Token、图片地址、序列化后的对象(相比较于Hash存储更节省内存)、缓存对象JSON数据
-
相关命令:
SET
、GET
。
需要计数的场景
-
举例:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。计算访问次数、点赞、转发、库存数量等等。
-
相关命令:
SET
、GET
、INCR
、DECR
。
分布式锁
利用 SETNX key value
命令可以实现一个最简易的分布式锁。
-
如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
-
如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
SETNX key value |
-
key:锁
-
value:客户端生成的唯一标识
共享session
通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
例如用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题,问题在于分布式系统每次会把请求随机分配到不同的服务器。
5.1.2 String 还是 Hash 存储对象数据更好呢?
-
String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
-
String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
5.2 List(列表对象)
列表对象的编码可以是ziplist或者linkedlist
5.2.1 应用场景
信息流展示
-
举例:最新文章、最新动态。
-
相关命令:
LPUSH
、LRANGE
。
消息队列
List
可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
5.3 hash(哈希对象)
哈希对象的编码可以是ziplist或者hashtable 。
5.3.1 应用场景
对象数据存储场景
-
举例:用户信息、商品信息、文章信息、购物车信息。
-
相关命令:
HSET
(设置单个字段的值)、HMSET
(设置多个字段的值)、HGET
(获取单个字段的值)、HMGET
(获取多个字段的值)。
5.4 Set(集合对象)
集合对象的编码可以是intset或者hashtable 。
5.4.1 应用场景
需要存放的数据不能重复的场景
-
举例:网站 UV独立访客 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等场景。 -
相关命令:
SCARD
(获取集合数量) 。
需要获取多个数据源交集、并集和差集的场景 -
举例:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。
-
相关命令:
SINTER
(交集)、SINTERSTORE
(交集)、SUNION
(并集)、SUNIONSTORE
(并集)、SDIFF
(差集)、SDIFFSTORE
(差集)。
需要随机获取数据源中的元素的场景 -
举例:抽奖系统、随机点名等场景。
-
相关命令:
SPOP
(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER
(随机获取集合中的元素,适合允许重复中奖的场景)。
动态点赞
Set 类型可以保证一个用户只能点一个赞,key 是文章id,value 是用户id。
5.5 Zset(Sorted Set,有序集合对象)
有序集合的编码可以是ziplist或者skiplist。和 Set 相比,Sorted Set 增加了一个权重参数
score
,使得集合中的元素能够按score
进行有序排列,还可以通过score
的范围来获取元素的列表,获取指定有序集合中指定元素的 score 值。有点像是 Java 中HashMap
和TreeSet
的结合体。
5.5.1 应用场景
需要随机获取数据源中的元素根据某个权重进行排序的场景
-
举例:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
-
相关命令:
ZRANGE
(从小到大排序)、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
需要存储的数据有优先级或者重要程度的场景 比如优先级任务队列。
-
举例:优先级任务队列。
-
相关命令:
ZRANGE
(从小到大排序)、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
5.5.2 有序集合为什么不用平衡树、红黑树、B+树实现
-
平衡树:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但平衡树需要保证整棵树左右节点的绝对平衡,旋转操作比较费时
-
红黑树:跳表的实现更简单,红黑树是弱平衡树,需要通过旋转和染色来保证黑平衡。红黑树区间查找效率不高。
-
B+树:B+树是数据库和文件系统的索引结构,目的是减少IO次数定位到更多的数据。Redis是内存数据库,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,节约内存。 B+树那样插入时发现失衡时还需要对节点分裂与合并。
B+树
-
多叉树结构:它是一棵多叉树,每个节点可以包含多个子节点,减小了树的高度,查询效率高。
-
存储效率高:其中非叶子节点存储多个 key,叶子节点存储 value,使得每个节点更够存储更多的键,根据索引进行范围查询时查询效率更高。
-
平衡性:它是绝对的平衡,即树的各个分支高度相差不大,确保查询和插入时间复杂度为O(log n)。
-
顺序访问:叶子节点间通过链表指针相连,范围查询表现出色。
-
数据均匀分布:B+树插入时可能会导致数据重新分布,使得数据在整棵树分布更加均匀,保证范围查询和删除效率。
5.6 Bitmap(位图)
通过一个bit位来表示某个元素对应的值或者状态
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1
的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
5.6.1 应用场景
需要保存状态信息(0/1 即可表示)的场景
-
举例:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
-
相关命令:
SETBIT
、GETBIT
、BITCOUNT
、BITOP
。
签到统计
判断用户登录状态
5.7 HyperLogLog(基数统计)
HyperLogLog是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数
,基数计数概率算法。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
所以,简单来说 HyperLogLog 提供不精确的去重计数。
5.7.1 应用场景
数量量巨大(百万、千万级别以上)的计数场景
-
举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、
-
相关命令:
PFADD
、PFCOUNT
。
uv的全称是Unique Visitor,译为独立访问用户数,访问网站的一台电脑客户端为一个访客。
pv的全称是Page View,译为页面浏览量或点击量,通常是衡量一个网站甚至一条网络新闻的指标。
百万级网页UV计数
5.8 Geospatial (地理位置)
Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。
需要管理使用地理空间数据的场景
-
举例:附近的人。
-
相关命令:
GEOADD
、GEORADIUS
、GEORADIUSBYMEMBER
。
5.9 Stream
消息队列
Redis 常见数据类型和应用场景 | 小林coding (xiaolincoding.com)
6 持久化
6.1 快照(snapshotting,RDB)
RDB 文件用于保存和还原Redis 服务器所有数据库中的所有键值对数据。
-
save
: 同步保存操作,会阻塞 Redis 主线程; -
bgsave
: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
6.2 只追加文件(append-only file, AOF)
实时性能更好
与RDB 持久化通过保存数据库中的键值对来记录数据库状态不同, AOF 持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,
Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
6.2.1 写回策略
-
命令追加
-
AOF文件的写入和同步(写入指写入缓冲区,同步将缓冲区的内容写回硬盘):命令会先写入AOF缓冲区,再定期写入和同步到AOF文件
-
Redis执行完写操作命令后,会将命令追加到
server.aof_buf
AOF缓冲区; -
然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
-
根据持久化方式(
fsync
策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。
6.2.2 AOF持久化方式
-
appendfsync always 每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
-
appendfsync everysec 每秒钟调用fsync函数同步一次AOF文件
-
appendfsync no 让操作系统决定何时进行同步,一般为30秒一次
6.2.3 为什么先执行命令,再把数据写入日志呢?
Reids 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处。
-
避免额外的检查开销:因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。
-
不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
当然,这样做也会带来风险:
-
数据可能会丢失: 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
-
可能阻塞其他操作: 由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
6.2.4 AOF重写
通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。重写后的AOF文件只包含还原当前数据库状态所需要的命令,新的AFO文件更节省空间。
AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
比如:多次向列表中添加删除元素,可以优化为向列表中添加剩下的元素
在AOF重写通过子进程进行,在重写过程中,服务器接收客户端请求可能改变服务器状态:
服务器进程:
-
执行客户端发来的命令
-
将执行后的写命令追加到AOF缓冲区
-
将执行后的写命令追加到AOF重写缓冲区。
Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的:
-
子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
-
子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生写时复制(Copy On Write),于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
6.3 RDB 和 AOF 的混合持久化(Redis 4.0 新增)
混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
混合持久化优点:
-
混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
混合持久化缺点:
-
AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
-
兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。
7 Redis内存管理
7.1 过期键删除策略
内存容量有限,需要设置过期时间来释放内存。每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
-
定时删除:在设置键的过期时间的同时,创建一个定时器( timer ), 让定时器在键的过期时间来临时,立即执行对键的删除操作。
-
惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
-
定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
-
延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
删除策略 | CPU | 内存 |
---|---|---|
定时删除 | 不友好 | 友好 |
惰性删除 | 友好 | 不友好 |
定期删除 | 取决于执行时长和频率 | 取决于执行时长和频率 |
通过过期字典判断数据是否过期: | ||
![]() |
仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
7.1.1 Redis 持久化时,对过期键会如何处理的?
RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
-
RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
-
RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
- 如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
- 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。
-
AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
-
AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。
7.1.2 Redis 主从模式中,对过期键会如何处理?
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
7.2 内存淘汰策略
refcount: Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
lru: 该属性记录了对象最后一次被命令程序访问的时间:
typedef struct redisObject{ |
7.2.1 内存淘汰机制
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存
淘汰范围:设置过期时间的数据集、键空间淘汰策略:随机、最近最少使用、最不经常使用
-
no-eviction:禁止淘汰数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
-
volatile-ttl(time to live):从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰。 -
volatile-lru(least recently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最近最少使用的数据淘汰。 -
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
-
volatile-random:从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰。 -
allkeys-random:从数据集(
server.db[i].dict
)中任意选择数据淘汰。 -
volatile-lfu(least frequently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最不经常使用的数据淘汰。 -
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
7.2.2 LRU和LFU
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据,根据访问时间淘汰数据。传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
LFU 全称是 Least Frequently Used 翻译为最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
7.3 Redis内存碎片
1、Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
2、频繁修改 Redis 中的数据也会产生内存碎片。 当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。
-
内存分配策略,分配空间大于所需空间
-
数据删除后没有释放内存
[!question] 假如MySQL有1000万数据,采用Redis作为中间缓存,取其中的10万,如何保证Redis中的数据都是热点数据?
- 过期键删除策略
- 内存淘汰策略
8 Redis缓存问题
8.1 缓存穿透
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
8.1.1 解决方法
-
缓存无效key,设置较短的过期时间(表名:列名:主键名:主键值)
-
对查询的key进行参数合法性校验,比如长度、格式等
-
布隆过滤器:判断给定的数据是否存在,二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。(存放在布隆过滤器中的数据不容易删除,判断数据存在,实际不一定存在)
-
接口限流:根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。
8.2 缓存击穿
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
8.2.1 解决方法
-
设置热点数据永不过期或者过期时间比较长。
-
针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
-
请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。 使用互斥锁,当缓存失效的时候,不是立即去load db,而是先使用缓存工具的某个机制,比如Redis的SETNX去设置一个锁,当操作返回成功时,再去load db并放入缓存;否则,就重试获取缓存值。
8.3 缓存雪崩
1.缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。
2.Redis故障宕机
8.3.1 解决方法
针对 Redis 服务不可用的情况:
-
采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
-
请求限流,避免同时处理大量的请求。只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务
-
服务熔断,,暂停业务应用对缓存服务的访问,直接返回错误
-
多级缓存,例如本地缓存+Redis 缓存的组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。限流、降级、熔断
针对热点缓存失效的情况:
-
设置不同的失效时间比如随机设置缓存的失效时间。
-
设置缓存不过期:通过后台服务来更新缓存数据。
-
缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中
-
使用互斥锁,当缓存失效的时候,不是立即去load db,而是先使用缓存工具的某个机制,比如Redis的SETNX去设置一个锁,当操作返回成功时,再去load db并放入缓存;否则,就重试获取缓存值。
-
后台更新缓存,业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
8.4 缓存预热如何实现?
常见的缓存预热方式有两种:
-
使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
-
使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。
9 Redis vs Memcached
共同点:
-
都是基于内存的数据库,一般都用来当做缓存使用。
-
都有过期策略。
-
两者的性能都非常高。
区别:
-
Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
-
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
-
Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
-
Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
-
Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
-
Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
-
Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
-
Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
[!question] Redis存在数据安全性、数据一致性问题吗?
两个用户读取数据状态,之后分别更新
面试官问:Redis 是并发安全的吗?怎么做到的?-阿里云开发者社区 (aliyun.com)