使用场景
🙂缓存:缓存穿透、击穿、雪崩、双写一致、持久化、数据过期、数据淘汰策略
🙂分布式锁: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的快照文件中恢复数据
👉怎么做
有两个命令,save
和bgsave
,save
会阻塞Redis服务器进程,直到RDB文件创建完成;bgsave
会fork
一个子进程来负责创建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.confi
g文件中配置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,每次不超过25msFAST
:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
👉定时过期:定时过期是指在设置键值对的时候,同时指定一个过期时间。一旦超过这个时间,键值对就会自动被删除。这种策略可以确保数据的实时性,但是它的效率并不是很高,因为Redis需要为每一个设置了过期时间的键维护一个定时器。
数据淘汰策略
🎶针对Redis内存不足时,仍然需要向Redis中添加策略的场景,此时需要按照特定规则来淘汰内存中的数据,将其删除掉
LRU和LFU
🎶LRU(Least Recently Used):最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高;
🎶LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
👉八种数据淘汰策略和使用建议
noeviction
【默认】- 不淘汰任何key,但是内存满时不允许写入新数据
- 内存用完了再添加新数据时会直接报错
volatile-ttl
- 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
allkeys-random
- 对全体key ,随机进行淘汰。
- 访问频率差别不大,没有明显冷热数据区分
volatile-random
- 对设置了TTL的key ,随机进行淘汰。
allkeys-lru
- 对全体key,基于LRU算法进行淘汰。
- 优先使用,特别是如果业务有明显的冷热数据区分
- 场景:数据库有1000万数据 ,Redis只能缓存20w数据, 保证Redis中数据都是热点数据
volatile-lru
- 对设置了TTL的key,基于LRU算法进行淘汰。
- 有置顶需求,置顶数据不设置过期时间
allkeys-lfu
- 对全体key,基于LFU算法进行淘汰。
- 短时高频访问
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
- 解决:加锁操作时给每个客户端设置一个唯一值(比如UUID),唯一值可以用来标识当前操作的客户端。在释放锁操作时,客户端判断当前锁变量的值是否和唯一标识相等,只有在相等的情况下,才能释放锁。(同一客户端线程中加锁、释放锁)
Redis如何保证操作的原子性
- 使用原子操作命令:如SET、HSET、SADD等。 Redis 是使用单线程串行处理客户端的请求来操作命令,这些命令在执行过程中不会被其他操作打断(相当于互斥)。
- 使用事务:Redis支持事务操作,即一系列原子操作被封装为一个事务。当事务开始时,Redis会锁住数据,防止其他进程或线程对其进行修改。当事务执行完毕,锁才会被释放。这就保证了在事务执行期间,其他进程无法修改数据。
- 锁机制:在多进程或多线程环境中,Redis通过使用锁机制来保证原子性。当一个进程或线程需要访问或修改数据时,它会先获取锁。只有当锁被成功获取,且没有其他进程或线程拥有锁时,该进程或线程才能执行数据操作。一旦操作完成,它就会释放锁,让其他进程或线程有机会获取。
- Lua脚本
【场景】两个客户端同时对[key1]执行自增操作,如何保证不会相互影响
👉使用单命令操作:比如用Redis的INCR
,DECR
,SETNX
命令,把RMW三个操作转变为一个原子操作
👉加锁: 调用SETNX
命令对某个键进行加锁(如果获取锁则执行后续RMW操作,否则直接返回未获取锁提示)-> 执行RMW业务操作 -> 调用DE
L命令删除锁
👉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节点的并发能力是有上限的,可以搭建主从集群,实现读写分离来提高并发能力,一般是一主多从,主节点负责写数据,从节点负责读数据
全量同步
从节点第一次与主节点建立连接的时候使用全量同步
- 从节点请求主节点同步数据:从节点会携带自己的
replication id
和offset
偏移量。 - 主节点判断是否是第一次请求,主要判断依据就是,主节点与从节点是否是同一个
replication id
,如果不是,就说明是第一次同步,那主节点就会把自己replication id
和offset
发送给从节点,让从节点与主节点的信息保持一致。 - 主节点执行
bgsave
指令生成rdb
文件,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb
文件。 - 在
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的客户端。
脑裂问题
🎶由于网络等原因,使得哨兵无法心跳感知到主节点,于是通过选举的方式产生了一个新的主节点,于是就有了两个主节点,这样会导致客户端在老主节点那更新数据,新的主节点无法同步更新数据,产生数据丢失。
👉解决方案,配置参数:一个主节点至少需要有一个从节点,才允许写入。或者缩短主从数据同步的延迟时间。
分片集群
用来解决高并发写和海量存储问题
👉原理
- 集群中有多个master,每个master保存不同数据
- 每个master可以有多个slave节点
- master之间通过ping检测彼此健康状态,就无需哨兵了
- 客户端可以访问任意节点,最终都会经过路由转发到正确节点
👉存储和读取数据的原理:16384个哈希槽分配到不同的master节点
根据key的有效部分计算哈希值,对16384取余【有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分】余数作为插槽,寻找插槽所在的节点
Redis中一致性需要注意的点
- 在我们用Redis主从复制或者集群模式的时候,需要确保主丛节点的同步,并考虑节点失效和故障恢复
- 读写操作的一致性
- 用Redis分片需要选择合适的分片策略,确保数据能够均匀分布,避免负载不均衡
- 使用Redis的事务和乐观锁保证原子性和一致性
- Redis用作缓存的时候要考虑与数据库的缓存一致