Redis-学习总结
Redis-学习总结
xukun1.Redis中常见的数据类型有哪些?
String:可以存储任何类型的数据,最大长度为512MB
- 使用场景:缓存 Session、Token、图片地址、序列化后的对象、页面单位时间的访问数、分布式锁等
Set:存储无序且不重复的字符串集合,使用哈希表实现可以基于
Set
轻易实现交集、并集、差集的操作,支持快速查找和去重操作- 使用场景:文章点赞、共同好友(交集)、共同粉丝(交集)等
zSet:存储有序的字符串集合,类似于set但是增加了权重参数
score
,使得集合中的元素能够按score
进行有序排列,还可以通过score
的范围来获取元素的列表- 使用场景:各种排行榜、优先级任务队列等
Hash:存储键值对,适合用于存储对象
- 使用场景:用户信息、商品信息、文章信息、购物车信息等
List:集合,使用双向链表实现,可以支持
反向查找和遍历
,更方便操作,不过带来了部分额外的内存开销。- 使用场景:最新文章、最新动态、消息队列等
2.Redis为什么这么快
主要有以下几个方面原因:
- 基于内存存储:Redis大部分数据都存储在内存中,相较于传统的基于磁盘的数据库系统,内存操作的读写速度要快得多,开启持久化功能(RDB 或 AOF)后,主线程仍然在内存中操作
- 单线程模型:Redis 采用单线程( 6.0 版本开始也可以开启多线程),避免了多线程上下文切换和锁竞争所带来的开销,简化了并发场景下的处理流程
- 高效的 I/O 多路复用:Redis使用单线程结合高效的 i/o 多路复用机制,在大规模并发情况下也能保持高吞吐量
- 高效的数据结构:Redis提供多种高效的数据结构(如String、Set、List、Hash等),这些结构经过优化,能够快速完成各种操作,Redis 会根据实际存储规模自动选择最合适的底层数据结构,节省内存和提升访问效率。
3.为什么 Redis 设计为单线程?6.0 版本为何引入多线程?
设计为单线程的主要原因是:
- 避免并发复杂度:多线程在共享数据结构时往往需要加锁或其他同步机制,容易带来锁竞争、线程切换等开销,从而降低整体性能,而单线程在处理请求时不需要考虑线程同步带来的额外开销,避免了并发场景下可能发生的各种复杂问题
- 使用I/O 多路复用:Redis 使用多路复用机制,在使用单线程的同时,一条线程就能够同时处理大量的连接请求
- 基于内存的数据操作:Redis 最核心的操作都在内存中完成,内存读写本身就极快,再加上内置多种优化数据结构的支持,很大程度上降低了单线程的性能瓶颈,同时单线程可以有效地利用 CPU 缓存,提高效率
6.0版本引入多线程的原因是:
- 随着数据规模的增长、请求量的增多,Redis 6.0 引入了可选的网络 I/O 多线程,用来进一步缓解网络I/O的压力,但核心的数据操作和命令执行仍然是单线程进行
4.Redis的Hash是什么
Redis 中,Hash是一种用于存储键值对的数据结构,和传统的HashTable类似,同时,Redis 的Hash能够管理一些关联的数据,比较灵活
Hash 的特点如下:
- 节省内存:
当 Redis存储小数据,Hash 中的字段数量较少的时侯,Redis 会采用比如zip list来存储,节省内存 - 支持快速读写:
Redis支持快速地对Hash内部的字段进行增删改查的操作,适合存储对象的属性
Hash有以下几个常用的命令:
写操作
设置 Hash 中的指定字段的值:
1
HSET user:1001 name "ikun" age "23"
一次设置多个字段的值:
1
HMSET user:1001 name "ikun" age "23" city "hangzhou"
读操作
获取 Hash 中指定字段的值:
1
HGET user:1001 name
一次获取多个字段的值:
1
HMGET user:1001 name age
获取 Hash 中所有字段和值:
1
HGETALL user:1001
获取 Hash 中所有字段名:
1
HKEYS user:1001
获取 Hash 中所有字段的值:
1
HVALS user:1001
删除操作
删除一个或多个字段:
1
HDEL user:1001 field
其他常用命令
返回 Hash 中字段的数量:
1
HLEN user:1001
判断指定字段是否存在:
1
HEXISTS user:1001 field
为 hash 中的字段加上一个整数值:
1
HINCRBY user:1001 age 1
实现Hash有两种方式:
- 在Redis 6和之前底层是zip list+HashTable实现的
- 在Redis 7之后是Listpack(紧凑列表)+HashTable实现的
我的Redis版本为5.0,所以只有ziplist
hash-max-ziplist-entries
表示建的默认最大个数为512
hash-max-ziplist-value
表示每个字段值默认最大长度为64
当Hash的值小于以上两个值后5.0版本会用ziplist存储,7.0后用用ziplist或者listpack存储,大于这两个值会使用HashTable并且不会转化成ziplist或者listpack
我们可以使用config set
命令,设置这两个默认的值大小。
5.Zset 的实现原理
Zset是有序集合,Sorted Set,简称 Zset,由跳表和哈希表组成,Zset结合了set的特性和排序功能,是一种能够存储具有唯一性的成员并按照分数(score)进行排序的集合结构,在保持元素唯一的同时按分数排序并支持高效的增删改查
Zset内部实现主要由两个核心数据结构组成:
- 跳表:提供了基于排序的高效范围操作
- 哈希表:提供了对成员分数的快速索引
由于这两种数据结构,Zset天然的实现了排行榜、延时队列等需要有序和精准查找的场景!
当Zset的元素数量较少时,Redis会使用ziplist来节省内存
zset-max-ziplist-entries
表示元素的默认最大个数为128zset-max-ziplist-value
表示元素成员名和分值默认最大长度为64
如果需要修改可以使用config set
命令
如果超过这两个值的任意一个阈值,Zset 将使用跳表+HashTable作为底层实现
同时使用跳表+HashTable是因为:
- 跳表与HashTable都要同时保存每个成员的分数,保证了在跳表与HashTable之间进行相互印证和快速操作,在插入(ZADD)、删除(ZREM)、查找(ZSCORE)、范围查询(ZRANGE、ZREVRANGE、ZRANGEBYSCORE)操作时都会进行相应更新,从而保持数据同步的一致性
6.Redis中跳表的实现原理
Redis 中的跳表(Skiplist)是通过多层有序链表+随机层高实现的,跳表的最底层的链表保存了所有元素,这一点与B+树类似,是Zset内部实现的两个核心数据结构之一,支持有序数据的快速范围查询和快速精确定位 ,可以用在实时排名、定时调度等业务场景
跳表的结构如下所示:
最顶层链表: [10] ───────────────> [40] ───────────────> [70] ─> nil
中间层链表: [10] ─────> [30] ───> [40] ─────> [70] ──────────────> nil
最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ─> [70] ─────────> nil
- 最底层是完整的有序链表,存放所有节点
- 最底层之上,会随机生成若干个索引层,用于跳过部分节点
- 每向上一层,节点数通常会减少一半左右,通过前/后指针把这些节点连成一条索引链
插入操作:
插入节点:
- 每插入一个新节点时,会使用随机函数决定插入节点在哪一层(一般限制在1-32之间),假如随机结果允许节点具有更高层,则会在上层加一个指针,用来增加跳跃的范围,比如插入一个64的节点,假设在最顶层
- 最顶层链表: [10] ────────────> [40]─────> [64] ──────> [70] ─> nil
中间层链表: [10] ─────> [30] ───> [40] ─────> [70] ──────────> nil
最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ─────> [70] ─> nil
更新索引层
- 在插入点所在层以上的每一层,如果新节点层高足够,则会把它插入到相应层级的链表中,并更新前驱节点和新节点之间的forward指针(前进指针)、span(该层 forward 指针跨越的节点数量)等信息,对上面插入的64进行更新就变成
- 最顶层链表: [10] ────────────> [40]─────> [64] ──────> [70] ───> nil
中间层链表: [10] ─────> [30] ───> [40] ─────> [64] ──────> [70] ──> nil
最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ──> [64] ───> [70] ─> nil
更新 backward-后退指针(zskiplistNode的结构体数组)
- 在最底层链表,设置新节点的后退指针指向前驱节点,如果新节点不是尾节点,也要更新后继节点的后退指针
zskiplistNode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14typedef struct zskiplistNode {
//成员
sds ele;
//后退指针(双向链表的一部分)
struct zskiplistNode *backward;
//不同层级上的前进指针和跨度
struct zskiplistLevel {
struct zskiplistNode *forward;
//该层 forward 指针跨越的节点数量
unsigned long span;
} level[];
} zskiplistNode;
查询操作:
- 从最高层链表开始查询:
- 当我们进行查询操作时,从最顶层链表快速跳过大量节点,从头结点依次向前查找,当下一节点的值
<=
目标值时,就向前移动,一旦发现不能继续前进时,就向下降到下一层更精确地查找
- 当我们进行查询操作时,从最顶层链表快速跳过大量节点,从头结点依次向前查找,当下一节点的值
- 降到下一层链表继续查询:
- 这一层会重复最高层链表查询时的比较-前进过程,继续向前查询
- 到底层精确定位:
- 直到降到最底层,就能精确地找到目标节点或确定目标节点不存在
这个过程类似在多层高速公路上寻找出口:先在最上层快速跳,接近目标区域后逐层下切换到普通道路,再进行精确查找
比如上面的例子:
- 最顶层链表: [10] ───────────────> [40] ───────────────> [70] ─> nil
中间层链表: [10] ─────> [30] ───> [40] ─────> [70] ──────────────> nil
最底层链表: [10] ─> [20] ─> [30] ─> [40] ─> [50] ─> [60] ─> [70] ─────────> nil
我们要查找20这个节点,首先从最顶层链表的10这个节点开始,因为20<40那么移动到中间层的10这个节点,中间层再比较,20<30(下一节点的值<=目标值),下降到下一层到达最底层链表,接着继续比较,20==20,查找成功。
总共跳转的次数为[10]->[10] + [10]->[10] + [10]->[20] 花费三次,如果通过传统链表(最底层),只需要[10]->[20] 一次,所以跳表的查询速度并不是一定比传统链表快
删除操作:
- 定位删除节点
- 和查询过程相同,先在各层索引中找到目标节点的前驱节点
- 移除指针
- 从最高层到最底层,依次把前驱节点的forward指针改为跳过该节点,指向它的后继节点
- 更新backward
- 在最底层更新前驱和后继节点的backward指针
- 释放节点
- 释放被删除节点对应的空间,如果节点层高大于 1,需要将多层结构也清理掉
7.Redis 中如何保证缓存与数据库的数据一致性?
保证缓存和数据库的数据一致性我们可以通过以下几个方式:
先更新缓存,再更新数据库:
具体方式:先把新数据写到缓存,然后再写数据库
这种方式大多数情况下不推荐使用,很容易出现不一致或写失败的问题,比如说
如果在缓存更新成功后、数据库尚未更新完成时,其他线程就来读取,会读到新缓存的值、旧数据库中的值的不一致情况
或者数据库更新失败,导致缓存和数据库一直不一致,还有一点就是在高并发场景下,不同线程对同一条数据做更新时,执行顺序很难控制
先更新数据库,再更新缓存:
具体方式:先更新数据库,再把新值写入缓存
这种方式在简单场景可以使用,但不推荐,因为并发场景下也会有各种竞态问题,比如说
如果先更新数据库但缓存里还是旧数据,且读操作在缓存更新之前到达,则会读到旧值
假如另一个线程也在修改并更新缓存,可能出现后写覆盖先写的情况,需要引入锁或版本号来避免
先删除缓存,再更新数据库:
具体方式:在写操作时,先删除缓存中的对应数据,然后再更新数据库,如果后面发现有读操作时,缓存没命中的话,再从数据库读取并回填缓存
这种方式在没有并发或并发量很小的场景下能用,但对高并发场景不太友好,所以也不推荐,会产生下面的问题:
在删除缓存后,数据库还未完成更新,就有线程来读数据,可能会把旧的数据库数据重新写回缓存,解决这个问题需要引入延时删除、分布式锁等策略
先更新数据库,再删除缓存:
具体方式:先更新数据库,再删除缓存中的旧数据,后续有读请求时,发现缓存未命中,就到数据库查最新数据并回填,这也是最常见的一种做法,一般情况业务上都会使用,因为只要保证数据库更新完毕后,能及时删除对应缓存,就不会保留过期数据
这种方式在并发场景下也可能出现脏数据,比如说:
数据库更新完、缓存还没删的瞬间,被其他线程读到然后又回填
解决办法:延时双删、分布式锁
缓存双删策略:
- 具体方式:更新数据库之前,先删除一次缓存,更新完数据库之后,等一个短暂的时间,再删除一次缓存,能够解决数据库更新期间可能出现的竞态问题,第一次删除是为了让后续读操作强制去数据库拿最新数据,而第二次延时删除是为了防止在第一次删除后、第二次删除前的时间窗口里,有其他线程刚好把旧数据又放回缓存
- 这种方式是高并发的写场景下的常见做法,能显著减小并发场景下短暂的不一致概率,但是需要设置合理的延时时间以及分布式锁等措施,时间过长或过短都可能出问题
使用 Binlog 异步更新缓存:
具体方式:Mysql会产生Binlog日志,通过监听器监听数据库更新操作,然后通过消息队列异步地去更新或删除 Redis 中对应的缓存,保证对数据库的所有操作,都能将变动同步到缓存
这种方式也会产生一些问题,比如说:
binlog 解析并同步到缓存会有一定延迟
如果 binlog 丢失、或者监听器出现故障,可能造成缓存与数据库再次不同步
8.Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
1.缓存击穿:
- 定义:是指当缓存中的某个热点数据过期,导致大量请求访问了该热点数据,无法从缓存中读取,直接访问数据库,导致数据库宕机
- 解决方案:
- 热点数据设置永不过期策略
- 使用互斥锁,保证同一时间只有一个线程更新缓存
2.缓存穿透:
- 定义:是指当用户访问的数据是缓存和数据库中都没有的数据时,这个数据在缓存中没有记录,就都去访问数据库,有大量这样的请求到来时,造成数据库的压力骤增
- 解决方案:
- 使用布隆过滤器(bloomfilter),快速判断数据是否存在,类似hashset,让请求访问的时候先判断这个数据存不存在,存在则放行,否则直接返回
- 对查询的结果进行缓存,即使不存在也缓存一个标识
- 在API接口入口出进行非法请求的限制,判断请求参数是否含有非法值、请求字段是否存在
3.缓存雪崩:
- 定义:是指多个缓存在同一时刻过期或者 Redis 故障宕机时,导致大量请求无法在 Redis 中处理,而去同时访问数据库,造成数据库瞬间压力骤增或者宕机
- 解决方案:
- 设置合理的过期时间
- 使用双缓存策略
- 使用互斥锁
9.Redis string 类型的底层实现是什么?
Redis中string类型底层基于SDS实现,SDS 是一种动态字符串结构,能够在保持c语言兼容字符串用法的同时,提供更高效的内存管理和使用体验
有以下几种编码方式:
- int:
- 用于存储整数类型的字符串,内存消耗小
- embstr:
- 通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS
- 占用64Bytes的空间,存储44Bytes的数据
- raw:
- 通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS
- 存储大于44Bytes的数据
10.Redis 中如何实现分布式锁?
最常用的方式是使用 SET key uniqueValue EX seconds NX
原子地获取锁,并用 Lua 脚本保证解锁操作是解的自己加的锁
具体方式:
利用 Redis 提供的
SET key uniqueValue NX EX seconds
命令:NX:互斥,确保只能有一个线程获取到锁
EX:设置过期时间,确保锁不会被某个客户端一直占有,避免死锁
如果返回
OK
,说明加锁成功;如果返回nil
,说明加锁失败,锁已被其他客户端持有例如:
1
2
3
4#key为user:1001
#value为uniqueId(唯一标识,这里随便写了一个)
#过期时间为 30 秒
SET user:1001 uniqueId EX 30 NX
通过给锁的value设置一个唯一标识(如 UUID、雪花算法或随机字符串),确保是自己加的锁之后才可以解锁
确认好后使用 Lua 脚本来删除锁
在释放锁时,需要先比较 key 的 value 与自己持有的标识是否一致,如果一致则删除 key:
1 | if redis.call("GET", KEYS[1]) == ARGV[1] then |
- KEYS[1]:锁的 key
- ARGV[1]:加锁时存储D唯一标识
还可以使用RedLock 实现分布式锁,具体方式:
准备一组相互独立的 Redis 节点(一般为 5 个),客户端依次向这些节点尝试加锁(用
SET NX EX
)。如果成功在
n/2+1及以上
节点上拿到锁且在过期时间内,则认为加锁成功解锁时也需要依次向各节点发起解锁操作
11.Redis 的 Red Lock 是什么?
RedLock红锁,是一个利用多节点独立部署Redis实例来实现分布式锁的方案,比起传统的不需要使用从库和哨兵机制,
原理:在大多数节点上(> N/2)加锁成功 + 在过期时间时间内完成加锁操作,才判定加锁成功
具体方式:
准备一组相互独立的 Redis 节点(一般至少为 5 个),客户端依次向这些节点尝试加锁(用
SET NX EX
)注意必须是没有设置过期时间的情况下才能加锁`
如果成功在
n/2+1及以上
节点上拿到锁且在过期时间内,则认为加锁成功解锁时也需要依次向各节点发起解锁操作,一般使用Lua脚本
总结:使用RedLock需要部署多台Redis实例,成本高,性能来说也,比不上单实例的redis,而且加锁需要依次向所有节点加锁,时间成本也高,但是可以很好的解决单点故障、数据一致性、安全性问题,一般业务还是推荐经典是主从+烧饼机制
12.Redis 实现分布式锁时可能遇到的问题有哪些?
业务执行异常导致的锁无法自动释放
比如:业务还没执行完,锁已经过期了,无法自动释放锁
解决办法:
使用Redission的看门狗机制
设置合理的过期时间
解锁时误释放他人锁
比如:客户端 A 获取锁后,本来应该在执行任务结束后删除锁,但如果发生网络延迟或阻塞,导致锁已过期并被客户端 B 成功获取,此时客户端 A 依旧执行删除操作,错误地删除了客户端 B 的锁
解决办法:
增加唯一标识,解锁时判断一下是不是自己的锁
使用Lua 脚本配合操作,原子地判断并删除锁,避免竞争导致的错误删除
各种故障问题
比如:因为故障(单点故障、网络err、主从不同步等)导致没有正确解锁,或者设置过期时间不当,会让锁一直存在下去,出现死锁
解决办法:
避免不带过期时间的锁
设置合理的过期时间
对于执行时间不可预估的长任务,可以使用续约机制
时钟漂移
比如:部署了多台Redis实例,不同实例因为各种原因导致实例的系统时间不一致,影响了锁的过期时间
解决办法:
所有节点上的系统时间采用NTP服务同步
锁的可重入性
比如:Redis的分布式锁默认是不可重入的,同一个客户端在持有锁的情况下再次请求锁会失败
解决办法:
给锁添加计数器,在获取锁时,如果发现计数器不为零,说明当前线程已经获取到了锁,此时可以直接增加计数器并返回 true,即表明已经获取到了锁,无需再次获取,在释放锁时,需要将计数器-1,如果计数器为零,才释放锁
增加唯一标识,每次获取锁时检查这个标识,如果请求锁的客户端已经持有了这个锁,则只更新锁的过期时间而不需要重新获取
13.Redis的持久化机制有哪些?
主要有3种:
RDB:通过在特定的时间间隔或者满足特定条件时,将当前内存的数据生成快照文件(默认为
dump.rdb
)来实现持久化,Redis重启,可以通过加载快照文件直接恢复到生成快照时的数据状态数据量不大、备份与迁移方便,但有可能丢失最近一次快照之后的数据
实现方式:
手动命令:
- save :在主线程中执行数据快照,阻塞 Redis 进程,直到快照完成
- bgsave:Redis使用rdbSaveBackground异步执行快照,主线程可继续处理请求
自动触发:
通过配置文件redis.conf设置save
比如
save 60 1
表示60秒内至少有1次写操作,则触发一次 bgsave
AOF:通过将每个写操作记录到日志文件(默认为
appendonly.aof
)实现持久化,支持将所有写操作记录下来,只要在Redis重启后重新执行AOF文件中的写命令即可将数据恢复到内存中记录所有写命令,数据丢失极少,但文件更大、恢复更慢、IO 开销更高
- 实现方式:
- 三种回写策略:
- always:每执行一条写操作后都立刻执行一次fsync,最安全但最慢,影响吞吐量
- everysec(默认策略):每秒执行一次 fsync,性能和安全性较为均衡(最多丢失1秒的数据)
- no:由操作系统自行决定何时进行 fsync,最快但安全性低,在Redis崩溃时数据丢失
- 重写机制:随着记录的操作增多aof文件会变大,所以Redis采用重写机制,定期对aof 文件进行压缩,把内存中的数据用最精简的写命令格式重新写入新的aof文件来瘦身
- 三种回写策略:
- 实现方式:
RDB+AOF混合:Redis 4.0+版本新增的混合持久化机制,结合RDB与AOF优势的持久化方案,当执行AOF重写时,Redis 会先写入一段RDB格式的内容,再写剩余命令日志
进一步提升数据安全和重启效率,在加快恢复速度的同时,保持AOF的高可用性
在Redis同时存在RDB和AOF文件时,默认优先加载 AOF 文件,确保数据完整性
14.Redis 主从复制的实现原理是什么?
主从复制是一种非阻塞的异步数据复制机制,通过将数据复制到一个或多个从节点,在多台服务器之间保持数据一致,主从复制主要有两种数据同步方式,全量同步和增量同步
优点:
- 读写分离:主从复制能实现读写分离,写操作请求主节点,读操作请求从节点,提高系统吞吐量
- 高可用性:当主节点宕机时,可手动或通过哨兵等机制进行主从切换,实现故障转移
缺点:
- 数据延迟:从节点接收写命令是异步的,可能会有延迟
- 数据丢失:主节点宕机时,没发送到从节点的增量数据可能会丢失
具体流程:
从节点发送同步请求,开始同步:
从节点启动后,向主节点发送一个PSYNC命令,尝试与主节点进行复制同步PSYNC是Redis 2.8版本引入的,从节点发送
PSYNC replid offset
给主节点,主节点判断能否继续进行部分同步,如果无法部分同步,则进行全量同步runid(复制ID):主节点的runID,当主节点发生故障切换后,新主节点会生成新的复制ID
offset(复制进度):由于记录上次同步的最后偏移位置,第一次值为
-1
主节点响应全量同步或增量同步:
- 全量同步:如果是第一次复制(从节点发送
PYSNC ? -1
),或者主节点runid为空时,会进行全量同步 - 增量同步:全量同步后,主节点通过增量同步保持持久连接,将后续写入数据库的操作同步到从节点,保持数据一致
- 全量同步:如果是第一次复制(从节点发送
15.Redis 数据过期后的删除策略是什么?
Redis中,给键置了过期时间之后,不会在过期时间到达的那一刻立刻将过期的数据删除,而是采用了惰性删除和定期删除相结合的方式来清理过期数据
惰性删除:当客户端访问某个键时,Redis 会先检查该键是否过期,如果过期则会将其立即删除并返回空结果或者报错,但是如果一个过期键一直没有被访问,就不会被惰性删除,这会占用额外的内存空间
定期删除:Redis 会周期性地(默认100ms,具体可配置)随机抽样检查过期键并主动删除,通过定期,避免内存中堆积过多已过期但未被访问的键,防止内存膨胀
内存回收机制:当 Redis 开启了maxmemory配置并且达到设置的上限时,如果还有新的写操作,需要提供空间存储新数据,就会触发内存淘汰策略
- 主要有以下几种策略:
- noeviction(默认策略):不删除建,新的写操作直接拒绝
- volatile-lru:只在设置了过期时间的键里挑选最近最少使用的键删除
- allkeys-lru:在所有键中挑选最近最少使用的键删除
- allkeys-random:随机删除任意键
- volatile-random:随机删除设了过期时间的键
- volatile-ttl:在设了过期时间的键中最先过期的键优先删除
- allkeys-lfu:基于访问频率在所有键中选择最不常用的键删除
- volatile-lfu:基于访问频率在有过期时间的键中选择最不常用的键删除
内存回收机制主要是为了应对内存超限的情况,而过期键的删除则是一个常规的过期清理过程
- 主要有以下几种策略:
16.如何解决 Redis 中的热点 key 问题?
在某些业务场景下,大多数请求会集中读写少数几个key,比如抢购、秒杀场景,访问量都会集中到特定商品id所对应的库存key。因为Redis 的核心是单线程执行命令,如果某个key在同一时刻被大量访问,就容易造成阻塞,如果所有数据被过度集中在一个key中,读写压力也会集中在单节点
解决Redis中的热点key问题可以有以下几个方法:
热点key拆分:拆分大 key,将热点数据分散,减轻单点压力
分散存储:对于高并发的计数场景,比如点赞数、浏览量等,可以采用多哥key+异步的方式
eg:将一个全局计数器拆分成多个key,每个请求随机写入某个key,然后再定时汇总
读写分离:通过Redis主从复制,增加从节点或多级缓存
对于读多写少的业务,使用主从复制,主节点负责写,从节点负责读,这样读请求可以分散到多个从节点,减轻主节点压力
对于热点数据,还可以在应用内部(JVM、本地 Cache)或者CDN再加一层缓存,减少对Redis的访问次数
限流降级:在业务层面进行流量管控,减少请求
限流:在业务层对请求进行限速或漏桶/令牌桶控制,避免短时间内大量的请求都打到Redis,减轻峰值压力,比如Nginx限流、分布式限流中间件等
降级:当某个热点key访问过高时,可以对部分非关键功能进行降级,或者访问到热点key时直接返回固定值、或者缓存的旧值
17.Redis 集群的实现原理是什么?
Redis集群是一种分布式架构,用于在多个Redis实例下进行数据分开存储,每个实例可以有主从节点,核心是通过哈希槽(Hash Slot)机制和Gossip协议来实现的,适用于大规模、高并发的业务场景
哈希槽:将键空间划分为16384个固定槽,每个实例负责一定范围(或多个范围)的哈希槽
例如节点A负责槽0 ~ 5000,节点B负责槽5001 ~ 10000,节点C负责槽10001 ~ 16383
每个key都会先通过哈希函数计算哈希值,然后对1638 取模,得到其所属的槽编号slot,再定位到对应的节点
Gossip 协议:用于节点之间交换消息和进行故障检测的协议
节点随机挑选其它节点进行消息交换,使信息在整个集群中扩散,最后保持一致
Gossip协议下能够自动感知并进行故障转移,无需人工干预
18.Redis 中的 Big Key 问题是什么?如何解决?
Big Key指的是某key下存储了非常大的数据量,占用空间比较大,一个超大的字符串等等,Big Key容易导致一系列性能和可用性问题,比如大数据量的读写操作阻塞、内存占用过高、查询效率变慢、客户端请求超时等等
解决办法
因为问题本质是数据过度集中在单个 key 内,所以可以从以下几个方面解决:
- 大key拆分:拆分大 key,将热点数据分散,减轻单点压力
- 优化数据结构:使用合适的数据结构就行优化,或者定期清理无用数据
- 使用Redis集群:采用集群的方式,把大Key拆分到不同服务器上
容易产生Big Key的场景
- 消息列表:Redis中用List存储用户的消息列表,时间久了之后单个List可能存储大量的数据
- 计数器:大量用户的计数都存到单个Hash里,数据也会变得非常庞大
- 排行榜:排行榜用户数骤增的情况下,Sorted Set里的元素数也会变成Big Key
怎么查找Big Key?
可以通过redis自带的命令查找
1
redis-cli --bigkeys
使用内置命令
1
MEMORY USAGE key
使用 scan/ hscan/zscan等命令