【八股】Redis篇

使用场景

🙂缓存:缓存穿透、击穿、雪崩、双写一致、持久化、数据过期、数据淘汰策略
🙂分布式锁:setnx、redisson
🙂消息队列、延迟队列、保存token:何种数据类型
🙂计数器

数据类型和它们底层的数据结构

🙂String(字符串):最基本的类型,可以存储任何数据,例如文本、图片、二进制数据等。用于缓存计数器、缓存数据、存储配置
【简单字符串SDS】
🙂List(列表):可以存储多个有序的元素,用于消息队列
【双向链表(短小)、压缩列表(长小)、快速列表(长大)】
🙂Set(集合):可以存储多个无序的元素,元素不能重复,用于去重、标签
【哈希表、整数集合】
🙂Hash(哈希):键值对存储,可以存储多个键值对,存储用户信息
【哈希表】
🙂Zset(有序集合):可以存储多个有序的键值对,排行榜、时间线和统计
【跳表或压缩列表】

  • Zset在 set 的基础上增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列
  • 排行榜:将用户的 ID 作为元素,用户的分数作为分数
  • 时间线:将发布的消息作为元素,消息的发布时间作为分数
  • 延时队列:将需要延时处理的任务作为元素,任务的执行时间作为分数
  • 压缩列表和跳表的区别:
    👉当 Zset 存储的元素数量小于zset-max-ziplist-entries 的值,且所有元素的最大长度小于 zset-max-ziplist-value 的值时,会选择使用压缩列表。占用的内存较少,但是在需要修改数据时,可能需要对整个压缩列表进行重写,性能较低。
    👉 当 Zset 存储的元素数量超过 zset-max-ziplist-entries 的值,或者任何元素的长度超过 zset-max-ziplist-value 的值时,会将底层结构从压缩列表转换为跳跃表。跳跃表的查找和修改数据的性能较高,但是占用的内存也较多。

缓存穿透现象是什么,怎么解决

🎶是指查询数据的时候,数据既不存在于redis中,也不存在于数据库中,导致查不到数据也写不到redis中,每次请求都会去请求数据库,导致数据库挂掉,这种情况大概率是遭受到了攻击

👉有两种解决方式,一个是返回空值,当我们查询不到数据的时候,也将这个null值写到redis中,还有一个方法是布隆过滤器,在查询数据的时候先查布隆过滤器中是否存在该数据,不存在则直接返回,存在再进入下一步查询redis

布隆过滤器

基于redisson实现的底层:布隆过滤器它是一个只存放二进制的数组, 通过对id值进行三次不同的哈希运算,得到三个哈希值,修改哈希值索引的数组元素为1。这样在每次查询id的时候,只需要查它对应的三个数组值是否为1,就能知道他是否存在了。但存在一个数据误判的情况,这时候我们可以扩大数组大小或者选择多个哈希函数来减少误判率,但这也是牺牲了空间换来的,一般我们设置误判率在5%左右即可

👉为什么不能用哈希表要用布隆过滤器?
哈希表考虑到负载因子的存在,对空间的利用率不高;而且哈希表有链表查询,在哈希冲突严重的情况下,会比纯数组查询的布隆慢

👉优点
存储空间和插入/查询时间都是常数;散列函数相互之间没有关系,方便并行实现;可以表示全集,不需要存储元素本身,在某些对保密要求非常严格的场合有优势

👉缺点
存在误算率,数据越多,误算率越高;一般情况下无法从过滤器中删除数据;二进制数组长度和 hash 函数个数确定过程复杂

缓存击穿

🎶是指key过期时刚好有大量的请求访问key,导致所有的请求都访问到数据库,增大了数据库的压力

👉解决方式:看我们的业务场景是需要数据强一致还是无需强一致。如果需要的话就使用互斥锁,不需要就为key设置逻辑过期时间

  • 互斥锁是一个线程在访问redis中的key发现过期的时候,用setnx设置一个互斥锁,当同步redis和数据库中的操作完成后,再释放锁资源,这样就算有另外的线程访问这个key,也会因为没有拿到锁资源而被阻塞。这样能保证数据的强一致性,但是性能不高。
  • 逻辑过期时间是指一个线程访问key发现过期时,开启一个新的线程进行数据同步,当前线程直接返回redis中的过期数据。而当新的线程同步完成后也会重新设置key的过期时间。这样会导致当前线程拿到的是一个过期的数据,无法保证数据强一致性,但是性能高。

缓存雪崩

🎶是指很多的key同时到期了,导致访问这些key的请求都到达了数据库端。
👉解决方式:尽量不要设置相同的key过期时间,而是采取随机值。

双写一致性

🎶是指数据库中的数据应该与Redis中的数据保持一致。如何保证双写一致性也分为强一致业务和允许延时一致的业务

👉允许延时一致的业务场景:使用延迟双删,即缓存中删除数据后,再到数据库中修改数据,然后延迟一段时间再到缓存中删除数据。【why延迟?】因为数据库是有两章主从分离的表,从表更新主表的数据也是需要时间的

👉需要强一致性的场景:使用读写锁排他锁,读的时候上读写锁,这样其他线程来只能读不能写。写的时候上排他锁,其他线程都被阻塞

【场景题】有多个 Redis 节点,当 MySQL 发生更新时,需要确保更新各个节点的缓存,其中一个节点下线的情况下如何保持系统的正常运行。

  • 事务:在 MySQL 更新操作之前,开启 Redis 事务。在事务中执行更新各个 Redis 节点的缓存操作,包括写入新的数据、删除旧的数据等。提交事务以确保所有操作原子性
  • 管道:使用 Redis 管道可以将多个命令一次性发送到 Redis 服务器,并在一次通信中获取所有命令的执行结果,从而减少通信开销和延迟。在 MySQL 更新之前,创建一个 Redis 管道。将更新各个节点的缓存操作添加到管道中。执行管道以一次性提交所有操作

持久化

RDB(Redis DataBase)

🎶数据快照,把内存中的所有数据记录到磁盘,当Redis宕机恢复数据的时候,从RDB的快照文件中恢复数据

👉怎么做
有两个命令,savebgsavesave会阻塞Redis服务器进程,直到RDB文件创建完成;bgsavefork一个子进程来负责创建RDB文件,只有fork的时候会阻塞,创建RDB的时候父进程可以继续处理命令请求,所以一般用bgsave。在redis.config文件中配置Redis内部触发RDB的机制,比如save 900 1 表示900s内,如果至少一个key被修改,执行bgsave命令

👉执行原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程中的内存数据。fork采取的是copy-on-write技术:当主进程执行读操作时,访问共享内存,当主进程执行写操作时,则会拷贝一份内存数据的副本,在副本中执行写操作,这样就不会出现脏读现象
【扩展:怎么共享】Redis读写数据的时候不能直接处理物理内存,而是处理虚拟内存,通过页表来找到虚拟地址和物理地址之间的映射关系实现的。因此子进程只需要fork一份主进程的页表,即可完成内存共享。

👉优缺点:RDB是二进制压缩文件,占用空间小,便于传输,恢复数据速度较快。两次RDB期间有空档期,此期间若Redis宕机了可能会造成数据的丢失。

AOF(Append Only File)

🎶当redis操作写命令的时候,都会将命令存储在追加文件AOF中,当redis实例宕机恢复数据的时候,会从AOF中再次执行一遍命令来恢复数据。

👉怎么做

  • AOF默认是关闭的,在redis.config文件中配置appendonly yes开启,记录的频率通过appendfsync always/everysec/no修改,这三种指令分别代表了 同步刷盘/每秒刷盘/操作系统控制刷盘,数据完整性由好到查,速度由慢到快,一般选用everysec
  • 使用bgrerwriteaof命令,让AOF文件执行重写功能,比如说一个key执行了多次写操作,但只有最后一次写操作有用,开启这个命令就可以只记录最后一次写操作。
  • 自动重写AOF:auto-aof-rewrite-percentage 100/ auto-aof-rewrit-min-size 64mb
    AOF文件比上次文件增长超过多少100%则触发重写/AOF文件体积到达64mb触发重写

👉优缺点:数据的完整性较高,文件较大,恢复速度较慢。

RDB-AOF混合持久化

该模式会将生成相应的RDB数据,写入AOF文件中,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。这样用户可以同时获得RDB持久化和AOF持久化的优点。

数据过期策略

🎶Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉,Redis的删除策略是惰性删除 + 定期删除两种策略进行配合使用

👉惰性删除:访问key的时候判断是否过期,如果过期则删除。对CPU友好但是对内存不友好

👉定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key。可以通过限制删除操作的执行频率和时长来减少对CPU的影响。但是难以确定合适的频率和时长

  • SLOW:定时任务,执行频率10hz,每次不超过25ms
  • FAST:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

👉定时过期:定时过期是指在设置键值对的时候,同时指定一个过期时间。一旦超过这个时间,键值对就会自动被删除。这种策略可以确保数据的实时性,但是它的效率并不是很高,因为Redis需要为每一个设置了过期时间的键维护一个定时器。

数据淘汰策略

🎶针对Redis内存不足时,仍然需要向Redis中添加策略的场景,此时需要按照特定规则来淘汰内存中的数据,将其删除掉

LRU和LFU

🎶LRU(Least Recently Used):最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高;
🎶LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

👉八种数据淘汰策略和使用建议

  1. noeviction【默认】
    • 不淘汰任何key,但是内存满时不允许写入新数据
    • 内存用完了再添加新数据时会直接报错
  2. volatile-ttl
    • 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
  3. allkeys-random
    • 对全体key ,随机进行淘汰。
    • 访问频率差别不大,没有明显冷热数据区分
  4. volatile-random
    • 对设置了TTL的key ,随机进行淘汰。
  5. allkeys-lru
    • 对全体key,基于LRU算法进行淘汰。
    • 优先使用,特别是如果业务有明显的冷热数据区分
    • 场景:数据库有1000万数据 ,Redis只能缓存20w数据, 保证Redis中数据都是热点数据
  6. volatile-lru
    • 对设置了TTL的key,基于LRU算法进行淘汰。
    • 有置顶需求,置顶数据不设置过期时间
  7. allkeys-lfu
    • 对全体key,基于LFU算法进行淘汰。
    • 短时高频访问
  8. volatile-lfu
    • 对设置了TTL的key,基于LFU算法进行淘汰。
    • 短时高频访问

分布式锁

集群架构下,用分布式锁解决线程之间的互斥性,有两种实现:setnx和Redisson

setnx

SET lock val NX EX 10 NX表示互斥:set if not exists,EX表示设置超时时间
DEL key 释放锁:

Redisson(基于setnx和lua)

RLock lock = redissonClient.getlock("锁名");
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
if (islock){
  try{ 线程要执行的具体业务 } finally{ lock.unlock(); } 
}

👉控制锁时间的合理性:提供了一个watch dog机制来合理控制锁的有效时长,一个线程获得锁成功后,watch dog会给持有锁的线程续期【默认10s】

👉流程:一个线程来尝试加锁,成功后可以操作Redis,同时另开了一个线程进行监控,也就是watch dog,它会不断监听持有锁的线程,每隔(releaseTime / 3)的时间做一次续期,增加锁的使用时间,手动释放锁后,还需要通知watch dog不再监听。如果此时又有另外一个线程来尝试加锁,它会循环等待持有锁的线程释放锁,在高并发情况下增加了性能,但是等待时间超过阈值了以后也会停止

👉可重入吗?
可以,用hash结构记录线程id和重入次数【key是锁名,值是线程id和重入次数】

👉能解决主从一致性吗?
不能,但是可以用redisson提供的红锁,但不推荐,如果非要保证强一致性可以用zookeeper实现的分布式锁。

👉 执行了SETNX命令加锁后的风险和解决思路

  • 假如某个客户端在执行了SETNX命令加锁之后,在后面操作业务逻辑时发生了异常,没有执行 DEL 命令释放锁。该锁就会一直被这个客户端持有,其它客户端无法拿到锁,导致其它客户端无法执行后续操作。
    • 解决:给锁变量设置一个过期时间,到期自动释放锁 SET key value [EX seconds | PX milliseconds] [NX]
  • 如果客户端 A 执行了 SETNX 命令加锁后,客户端 B 执行 DEL 命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,则可以成功获得锁。
    • 解决:加锁操作时给每个客户端设置一个唯一值(比如UUID),唯一值可以用来标识当前操作的客户端。在释放锁操作时,客户端判断当前锁变量的值是否和唯一标识相等,只有在相等的情况下,才能释放锁。(同一客户端线程中加锁、释放锁)SET lock_key unique_value NX PX 10000

Redis如何保证操作的原子性

  • 使用原子操作命令:如SET、HSET、SADD等。 Redis 是使用单线程串行处理客户端的请求来操作命令,这些命令在执行过程中不会被其他操作打断(相当于互斥)。
  • 使用事务:Redis支持事务操作,即一系列原子操作被封装为一个事务。当事务开始时,Redis会锁住数据,防止其他进程或线程对其进行修改。当事务执行完毕,锁才会被释放。这就保证了在事务执行期间,其他进程无法修改数据。
  • 锁机制:在多进程或多线程环境中,Redis通过使用锁机制来保证原子性。当一个进程或线程需要访问或修改数据时,它会先获取锁。只有当锁被成功获取,且没有其他进程或线程拥有锁时,该进程或线程才能执行数据操作。一旦操作完成,它就会释放锁,让其他进程或线程有机会获取。
  • Lua脚本
    【场景】两个客户端同时对[key1]执行自增操作,如何保证不会相互影响
    👉使用单命令操作:比如用Redis的INCRDECRSETNX 命令,把RMW三个操作转变为一个原子操作
    👉加锁: 调用SETNX命令对某个键进行加锁(如果获取锁则执行后续RMW操作,否则直接返回未获取锁提示)-> 执行RMW业务操作 -> 调用DEL命令删除锁
    👉Lua脚本:多个操作写到一个 Lua 脚本中(Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性),限制所有客户端在一定时间范围内对某个方法(键)的访问次数。客户端 IP 作为 key,某个方法(键)的访问次数作为 value
local current current = redis.call("incr",KEYS[1])  //从Redis中获取名为 KEYS[1] 的键的当前值,并将其递增。
if tonumber(current) == 1  // 如果递增后的值为1,则设置该键的过期时间为60秒。
then redis.call("expire",KEYS[1],60) 
end

然后调用执行:redis-cli --eval lua.script keys , args

主从复制

单个Redis节点的并发能力是有上限的,可以搭建主从集群,实现读写分离来提高并发能力,一般是一主多从,主节点负责写数据,从节点负责读数据

全量同步

从节点第一次与主节点建立连接的时候使用全量同步

  1. 从节点请求主节点同步数据:从节点会携带自己的replication idoffset偏移量。
  2. 主节点判断是否是第一次请求,主要判断依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己replication idoffset发送给从节点,让从节点与主节点的信息保持一致。
  3. 主节点执行bgsave指令生成rdb文件,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件。
  4. rdb生成的期间,主节点会以命令的方式记录到缓冲区(一个日志文件repl_baklog),会把这个日志文件也发送到主节点进行同步。

增量同步

slave重启或后期数据变化使用增量同步

  • 从节点请求主节点同步数据,主节点还是判断是不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志repl_baklog中获取offset值之后的数据,发送给从节点进行数据同步。

哨兵sentinel

实现主从集群的自动故障恢复

作用:监测/选主/通知

  • 监测:Sentinel 会基于心跳机制不断检查master和slave是否按预期工作。【主观下线】如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。【客观下线】若超过一半的sentinel都认为该实例主观下线,则该实例客观下线。
  • 选主:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。首先判断主与从节点断开时间长短,如断开时间太长则不选举该从节点。然后判断从节点的slave-priority值,越小优先级越高。如果优先值相等,则判断从节点的offset值,越大优先级越高
  • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端。

脑裂问题

🎶由于网络等原因,使得哨兵无法心跳感知到主节点,于是通过选举的方式产生了一个新的主节点,于是就有了两个主节点,这样会导致客户端在老主节点那更新数据,新的主节点无法同步更新数据,产生数据丢失。
👉解决方案,配置参数:一个主节点至少需要有一个从节点,才允许写入。或者缩短主从数据同步的延迟时间。

分片集群

用来解决高并发写和海量存储问题

👉原理

  1. 集群中有多个master,每个master保存不同数据
  2. 每个master可以有多个slave节点
  3. master之间通过ping检测彼此健康状态,就无需哨兵了
  4. 客户端可以访问任意节点,最终都会经过路由转发到正确节点

👉存储和读取数据的原理:16384个哈希槽分配到不同的master节点

根据key的有效部分计算哈希值,对16384取余【有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分】余数作为插槽,寻找插槽所在的节点

Redis中一致性需要注意的点

  1. 在我们用Redis主从复制或者集群模式的时候,需要确保主丛节点的同步,并考虑节点失效和故障恢复
  2. 读写操作的一致性
  3. 用Redis分片需要选择合适的分片策略,确保数据能够均匀分布,避免负载不均衡
  4. 使用Redis的事务和乐观锁保证原子性和一致性
  5. Redis用作缓存的时候要考虑与数据库的缓存一致

大key问题

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值