应用场景 #
-
热点数据缓存
redis访问速度块、支持的数据类型比较丰富,所以 redis 很适合用来存储热点数据,另外结合 expire,我们可以设置过期时间然后再进行缓存更新操作
-
限时业务的运用
redis 中可以使用 expire 命令设置一个键的生存时间,到时间后 redis 会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
-
计数器
incrby 命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
-
排行榜
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助 redis 的 SortedSet 进行热点数据的排序。
-
分布式锁
-
点赞、好友等相互关系的存储
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。 又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
IO 模型 #
单线程 #
Redis 的网络 IO 和键值对读写是由一个线程来完成,这也是对外提供键值存储服务的主要流程。
其他功能,比如持久化、异步删除、集群数据同步,有额外的线程执行
数据结构 #
skip list #

删除一个节点,直接把某个层级中对应的改节点删掉,插入节点时,新节点以指数递减的概率往上层链表插入即可。 比如L0中100%插入,L1中以1/2的概率插入,如果L1中插入了,L2中又以1/2的概率插入
在 redis 中,zslRandomLevel 是以25%的概率决定是否将单个节点放置到下一层,而不是50%。

Redis 为什么使用 skiplist 而不是平衡树
- skiplist 并不是特别耗内存,只需要调整下节点到更高 level 的概率,就可以做到比 B 树更少的内存消耗。
- sorted set 可能会面对大量的 zrange 和 zreverange 操作,跳表作为单链表遍历的实现性能不亚于其他的平衡树。在查找区间内所有元素时,跳表只需要定位到两个区间端点所在的最低层的位置,然后按顺序遍历即可。而红黑树只能定位到断点后,再从首位置开始每次都要查找后继节点,相对耗时。
- 实现和调试起来比较简单。 例如,实现 O(log(N)) 时间复杂度的 ZRANK 只需要简单修改下代码即可。
对象 #
Redis 的每个对象都由一个 redisObject 结构表示,该结构中保存和数据有关的三个属性,分别是 type 属性、 encoding 属性和 ptr 属性。
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
// 最后一次被命令访问时间
int refcount; // 引用计数
void *ptr; // 指向底层实现数据结构
} robj;
字符串对象 #
-
int
-
raw
创建字符串需要两次分配内存
-
embstr
只读:任何修改命令都将其转为 raw
调用一次内存分配函数创建 redisObject 结构和 sdshdr 结构,只需一次内存分配次数和一次内存释放次数。
列表对象–LIST #
redis3.2之后使用 quicklist 代替了 ziplist 和 linkedlist
哈希对象 #
-
ziplist
同一键值对的两个节点紧挨在一起,键在前,值在后,不同键值对按添加顺序排列
当哈希对象的键或值太长时,或者是键值对的数量太多,编码都会从 ziplist 转换为 hashtable。
-
hashtable
对象的每个键值对都是用一个字典键值对来保存,每个键和每个值都是一个字符串对象
集合对象–set #
只存储不重复的元素,无序方式;而列表对象可存储重复元素,先后顺序
==set 存放的是不重复值的集合,所以可以做全局去重的功能==
==利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。==
-
intset
当集合中的元素都是整数且元素个数小于 set-maxintset-entries 配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
-
hashtable
字典的每个键都是一个字符串对象,每个字符串对象包含一个集合元素,而字典的值则全部被设置为 NULL
有序集合–zset #
相比于集合类型多了一个排序属性 score(分值),集合元素可以按 score 进行排列,可以==做排行榜应用,取 TOP N 操作==
-
ziplist
使用压缩列表作为底层实现。每个集合元素使用两个紧挨在一起的压缩列表来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。 压缩列表内的集合元素按照分支从小到大进行排序,分值较小的元素被放置在靠近表头的位置,分值较大的元素则被放置在靠近表尾的位置。
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64个字节
-
skiplist
同时使用跳跃表和字典实现,==字典根据键找分值快,跳跃表根据分值找键快==
skiplist 编码的有序集合对象使用 zset 结构作为底层实现。 zset 结构中的 zsl 跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点保存了一个集合元素:跳跃表节点的 object 属性保存了元素的成员;而跳跃表节点的 score 属性保存了分数的高低。
过期键 #
过期键删除策略 #
-
定时删除:
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即对执行对键的删除操作。 定时删除策略对内存较为友好,但是对CPU不友好。
-
惰性删除:
放任键过期不管,每次从键空间中取得键时,都检查取得的键是否过期,如果过期就删除该键,没有过期就返回该键。
对CPU较为友好,对内存较为不友好。如果数据库中有很多的过期键,但是却永远不会被访问到,那么这可以看做是一种内存泄露。比如日志文件
-
定期删除:
每隔一段时间对数据库进行一次检查,删除过期键。折中
==Redis 采用惰性删除 + 定期删除==。采用定期删除和惰性删除,仍然会有部分过期键没有被清除,所以需要用到内存淘汰机制。
内存淘汰机制 #
-
LRU
-
LFU
-
random
-
TTL
从已设置过期时间的数据集中挑选将要过期的数据淘汰
redis 持久化 #
RDB 持久化 #
记录的是某一时刻的数据,而不是操作
子线程写入 + 启动时自动载入
- 将某个时间点的所有数据都存放到硬盘上。
- 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
- 如果系统发生故障,将会丢失最后一次创建快照之后的数据。
- 如果数据量很大,保存快照的时间会很长。
SAVE
命令与 BGSAVE
命令用于生成 RDB 文件
AOF 日志 #
写后日志,把数据写入内存再记录日志。追加写
好处:
- 记录日志的时候不会先对命令进行语法检查,避免了检查开销
- 命令执行后才记录,不会阻塞当前写操作
写回策略:
- Always:同步写回,每个写命令执行完立马同步将日志写回磁盘
- Everysec:每秒写回,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
- No:操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

AOF 重写机制
Redis 根据数据库现状创建一个新的 AOF 文件。旧日志文件的多条命令在重写后的新日志中变成了一条命令。
每次重写时,先执行一个内存拷贝用于重写,两个日志保证在重写过程中新写入的数据不会丢失。采用额外的线程进行,不会阻塞主线程。
缓存穿透、击穿、雪崩 #
缓存穿透 #
缓存和数据库中都没有的数据,用户不断发起请求,导致这个不存在的数据每次请求都要到存储层查询,失去了缓存的意义
解决方案:
- 接口层增加校验,如用户鉴权校验、ID 做基础校验、id <= 0 的直接拦截
- 用缓存娶不到的数据,在数据库中也没有渠道,这是可以将 key-value 写为 key-null,缓存有效时间可以设置短一些,防止攻击用户反复用同一 id 暴力攻击
- 布隆过滤器,使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库
缓存击穿 #
缓存中没有单数据库中有的数据(一般指缓存时间到期),由于并发用户较多,同时读缓存没有读到数据,又到数据库去取数据,引起数据库压力瞬间增大
解决方案:
- 设施热点数据永远不过期
- 接口限流与熔断,降级。重要的接口做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中某些服务不可用的时候,进行熔断,失败快速返回机制
- 加互斥锁。
缓存雪崩 #
大量热点 key 设置了相同的过期时间,导致在缓存的同一时刻全部失效,造成瞬时数据库的请求量大,压力骤增,引起雪崩,甚至导致数据库被打挂
解决方案
- 过期时间打散。给缓存的过期时间加上一个随机值时间,使得每个 key 的过期时间分不开来,不集中在同一时刻失效
- 热点数据不过期
- 加互斥锁。按 key 维度加锁,对同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存。
接口限流、服务降级、熔断 #
接口限流 #
限流原因:
- 用户增长过快
- 热点事件(微博热搜)
- 爬虫
- 恶意刷单
- 对内的 RPC 服务来说,一个接口可能被多个服务调用,一个服务突发流量把接口挂掉,导致其他服务也停止
单机限流算法
- 计数器算法
- 令牌桶
- 漏桶
计数器 #
限制一秒钟能通过的请求数,每来一个请求将计数加一,达到阈值则后续请求全部拒绝
弊端:突刺现象,可能前 10ms 通过阈值的请求,后面 990ms 只能把所有请求拒绝
漏桶 #
**原理:**算法内部有一个容器,类似漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。如果容器满了,那么新进来的请求就丢弃。
**实现:**准备一个队列保存请求,通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行
弊端:无法应对短时间突发流量。
令牌桶 #
**原理:**存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌,如果桶中令牌数达到上限,就丢弃令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。

漏桶算法 能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,所以它适合于具有突发特性的流量。
服务降级 #
系统将某些服务或者接口的功能降低
- 双十一,订单暂不允许修改收货地址
- 论坛只能看帖子不能发帖子
- App 日志上传接口,停掉一段时间,不能上传日志
熔断 #
目的是应对依赖的外部系统故障的情况
- A 服务的 X 功能依赖 B 服务的某个接口,当 B 服务的接口相应很慢时,A 服务的 X 功能相应被拖慢,导致 A 服务的线程被卡在 X 功能处理上
- 加入熔断机制,A 服务不再请求 B 服务的这个接口,A 服务内部只要发现是请求 B 服务的这个接口就立即返回错误,避免 A 服务整个被拖慢
实现
- 统一的 API 调用层,由 API 调用层来进行采样或统计,如果接口调用散落在代码各处则无法进行统一处理
- 阈值设计,如1 分钟内 30% 请求响应时间超过 1s 接熔断