为什么使用 Redis
- 速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O1)
- 支持丰富数据类型,支持 string,list,set,Zset,hash 等
- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除
Redis的常用数据类型有哪些
Redis是一个基于内存的键值对数据库,它的键都是字符串类型,而值的部分支持5种数据类型,每种类型特点不一样
- string:字符串类型,可以存储普通字符串、JSON字符串,也可以存储对象系列化之后的字符串
- hash:哈希类型,类似于Java中的HashMap,比较适合存储对象
- list:列表类型,底层是一个顺序链表,可以从两端添加或移除元素,元素是有序的,可重复的
- set:无序集合,没有重复元素
- zset:有序集合,没有重复元素,且集合中每个元素关联一个分数,可以根据分数进行排序
跳表你了解吗
跳表(SkipList)首先是链表,但与传统的链表相比有几点差异:
- 跳表结合了链表和二分查找的思想
- 元素按照升序排列存储
- 节点可能包含多个指针,指针跨度不同
- 查找时从顶层向下,不断缩小搜索范围
- 整个查询的复杂度为 O ( log n )
Redis数据类型Sorted Set使用了跳表
Redis 可以用来做什么
- 缓存
- 排行榜
- 分布式计数器
- 分布式锁
- 消息队列
- 分布式 token
- 限流
Redis 为什么快
- (内存操作)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
- (单线程,省去线程切换、锁竞争的开销)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- (NIO 的 IO 多路复用模型)使用多路 I/O 复用模型,非阻塞 IO;这里"多路"指的是多个网络连接,"复用"指的是复用同一个线程
解释一下I/O多路复用模型
嗯~~
- I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
- 其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
- 在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
Redis 过期删除策略
常用的过期数据的删除策略就两个:
- 惰性删除:当客户端尝试访问一个键时,Redis会在访问时检查键是否过期,如果过期则执行删除操作。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且Redis底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影 响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是定期删除+惰性/懒汉式删除。但是仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的 情况。这样就导致大量过期 key 堆积在内存里,然后就 Outofmemory 了。
怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
Redis 内存淘汰策略
Redis 提供6 种数据淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中移除最不经常使用的key
Redis 持久化机制 RDB 和 AOF 区别?
- RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
- AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据
在恢复速度上:
- RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中采用RDB+AOF
- 这段按照项目来说.....也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令。
你们的项目中的持久化是如何配置选择的?
RDB+AOF
Redis 如何选择合适的持久化方式
- 如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化。
- 如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用 RDB。
- 如果是用做内存数据库,要使用 Redis 的持久化,建议是 RDB 和 AOF 都开启,或者定期执行 bgsave 做快照备份,RDB 方式更适合做数据的备份,AOF可以保证数据的不丢失
Redis4.0 相对与3.X 版本其中一个比较大的变化是4.0 添加了新的混合持久化方式
简单的说:新的 AOF 文件前半段是 RDB 格式的全量数据后半段是 AOF 格式的增量数据
优势:
- 混合持久化结合了 RDB 持久化和 AOF 持久化的优点,由于绝大部分都 是 RDB 格式,加载速度快,同时结合 AOF,增量的数据以 AOF 方式保存 了,数据更少的丢失。!
劣势:
- 兼容性差,一旦开启了混合持久化,在4.0 之前版本都不识别该 aof 文件,同时由于前部分是 RDB 格式,阅读性较差。
在生成 RDB 期间,Redis 可以同时处理写请求么
Redis在进行RDB期间是可以同时处理写请求的,这是因为Redis使用操作系统的多进程写时复制技术来实现快照持久化
具体说,就是Redis在持久化时会产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求
但当主线程执行写指令修改某个数据时,这个数据会被复制出一份副本,而子进程写入RDB 文件的内存数据只能是原本的内存数据。此时这次修改的数据只能交由下一次bgsave快照进行备份。
但RDB快照文件体积小,在恢复速度来说比AOF文件要快。
Redis集群有哪些方案, 知道嘛
在Redis中提供的集群方案总共有三种
- 主从复制
-
- 保证高可用性
- 实现故障转移需要手动实现
- 无法实现海量数据存储
-
- 哨兵模式
-
- 保证高可用性
- 可以实现自动化的故障转移
- 无法实现海量数据存储
-
- Redis分片集群
-
- 保证高可用性
- 可以实现自动
- 可以实现海量
-
什么是 Redis 主从同步
- 主从第一次同步是全量同步
- 后期数据变化后,则执行增量同步
全量同步的流程
- 从节点执行replicaof命令,发送自己的replid和offset给主节点
- 主节点判断从节点的replid与自己的是否一致,
- 如果不一致说明是第一次来,需要做全量同步,主节点返回自己的replid给从节点
- 主节点开始执行bgsave,生成rdb文件
- 主节点发送rdb文件给从节点,再发送的过程中
- 从节点接收rdb文件,清空本地数据,加载rdb文件中的数据
- 同步过程中,主节点接收到的新命令写入从节点的写缓冲区(repl_buffer)
- 从节点接收到缓冲区数据后写入本地,并记录最新数据对应的offset后期采用增量同步
增量同步的流程
- 主节点会不断把自己接收到的命令记录在repl_baklog中,并修改offset
- 从节点向主节点发送psync命令,发送自己的offset和replid
- 主节点判断replid和offset与从节点是否一致
- 如果replid一致,说明是增量同步。然后判断offset是否一致
- 如果从节点offset小于主节点offset,并且在repl_baklog中能找到对应数据则将offset之间相差的数据发送给从节点
- 从节点接收到数据后写入本地,修改自己的offset与主节点一致
如何从repl_baklog中获取offset(偏移量)后的数据
repl_baklog本质上是一个数组,但它是一个环形数组。
如果master未超出环的上限,slave就可以通过向master发生offset做增量同步。
如果master超出环的上限,就会从原点,也就是起点开始覆盖原有的数据,此时无法通过offset做增量同步。但可以从内存中找数据做全量同步
增量同步的风险
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。
repl_baklog可以在配置文件中进行修改存储大小
Redis分片集群中数据是怎么存储和读取的
Redis 集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,取到每个key的hash值对 16384取余来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
Redis 集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分hash 槽。
Redis 主从架构数据会丢失吗,为什么?
有两种数据丢失的情况:
- 异步复制导致的数据丢失:因为 master -> slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这些部分数据就丢失了。
- 脑裂导致的数据丢失:某个 master 因为网络故障,导致和其他slave不能连接,但是实际上 master 还运行着,此时哨兵可能就会认为master 宕机了,然后开启选举,将其他 slave 切换成了 master。此时,集群里就会存在两个 master,也就是所谓的脑裂。这时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续写向旧 master 的数据可能也丢失了。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master复制数据。数据就会丢失。
如何保存 Redis 数据与 DB 一致
方案1:同步双写,即更新完 DB 后立即同步更新 redis
方案2:异步监听,即通过 Canal 监听 MySQL 变化的表,同步更新数据到Redis
方案3:MQ 异步,即更新完 DB 后生产消息到 MQ,MQ 消费者更新数据到Redis
什么是Redis的缓存预热
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中,对数据库造成流量的压力。
缓存预热解决方案:
- 数据量不大的时候,工程启动的时候进行加载缓存动作;
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
- 数据量太大的时候,优先保证热点数据进行提前加载到缓存。
什么是缓存降级
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
什么是缓存击穿?如何解决?
- 给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求,这些请求肯会瞬间把DB击垮
解决方案
- 使用互斥锁:当检测到某个热点数据的缓存失效时,不立即去查询数据库,而是先用Redis的setnx去设置一个互斥锁,当互斥锁添加成功之后,再进行数据库的查询和回设缓存,否则重试(get缓存的方法)获取缓存。避免大量请求同时访问数据库,减轻数据库的压力,可以保证数据的强一致性,但性能差
- 逻辑过期策略:对热点的key不设置过期时间,在存储数据的时候新增一个过期时间字段,在新增数据到缓存的时候维护这个字段值。当查询缓存时,会从redis取出数据,判断过期时间字段是否过期,如果过期,就添加一个互斥锁,并打开一个新线程,进行缓存重建,当前的线程无需等待,直接返回过期数据,同一时间内来查询缓存的线程,会获取互斥锁失败,直接返回的过期数据,只有新线程缓存重建完成,写入最新缓存并释放锁之后,查询到的缓存才是最新数据。高可用,性能好,但无法保证数据的绝对一致。
什么是缓存穿透?如何解决?
缓存穿透
- 查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
解决方法
- 缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存。实现简单,消耗内存,可能发生不一致问题
- 布隆过滤器:使用布隆过滤器来预先过滤掉一些明显不存在的数据,从而减少对数据库的查询请求。如果布隆过滤器认为某个数据不存在,就可以直接返回空值,而不需要再次查询数据库。内存占用少,没有多余的key,实现复杂,存在误判
- 热点数据预热:将热点数据提前加载到缓存中,这样可以减少对数据库的请求,提高缓存命中率。
- 异常数据处理:针对异常情况,比如非法输入或恶意攻击,可以在应用层进行合法性校验,避免将非法请求传递到缓存层和数据库层。
什么是缓存雪崩?如何解决?
- 同一时段大量的缓存key同时失效或者Redis宕机,导致大量请求到达数据库,就会导致雪崩
- 解决方案
-
- key同时失效:给不同的key的设置不同的过期时间(给key的TTL添加随机值)
- Redis宕机:搭建Redis的高可用集群,提高服务的可用性。例如哨兵模式,集群模式
- 给缓存业务设置降级限流策略。在nginx或springCloud gateway设置
- 给业务添加多级缓存。例如Guava或者Caffeine来实现一级缓存,Redis为二级缓存
缓存穿透、缓存击穿、缓存雪崩通用的解决方案
- 限流和降级:对请求进行限流和降级处理,当检测到某个请求频率异常高时,可以暂时拒绝服务或返回默认值,避免对数据库造成过大压力。
Redis分布式锁如何实现
Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写。
上面这几个命令就是最基本的用来完成分布式锁的命令
加锁:使用 setnx key value
命令,如果key不存在,设置value(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。
解锁:使用 del
命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过setnx
命令进行加锁。
Redis分布式锁的有效时长
有效时间设置多长,假如我的业务操作比有效时间长?我的业务代码还没执行完就自动给我解锁了,不就完蛋了吗。
解决方案:
- 第一种:程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。
- 第二种:给锁续期。锁续期实现思路:当加锁成功后,同时开启守护线程,默认有效期是用户所设置的,然后每隔10秒就会给锁续期到用户所设置的有效期,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁
上述的第二种解决方案可以使用redis官方所提供的Redisson进行实现
Redisson分布式锁原理
Redisson是一个基于Redis的分布式锁框架,它的原理如下:
- 获取锁:
-
- 客户端使用Redisson获取分布式锁时,会向Redis服务器发送一个SET命令,将锁的key设置为一个唯一标识符,并设置一个过期时间。
- 如果锁的key在Redis中不存在,即锁未被其他客户端持有,那么客户端成功获取锁,并设置一个过期时间,表示锁的持有时间。
- 如果锁的key在Redis中已经存在,表示锁已经被其他客户端持有,客户端获取锁失败,可以选择等待或进行重试。
- 保持锁的持有:
-
- 客户端成功获取锁后,可以通过续期机制来保持锁的持有。续期机制是通过定时向Redis服务器发送一个命令来更新锁的过期时间,确保锁不会过期。
- Redisson使用了Lua脚本来保证续期操作的原子性,避免了续期过程中锁被其他客户端获取的问题。
- 释放锁:
-
- 当客户端不再需要持有锁时,可以向Redis服务器发送一个DEL命令,将锁的key从Redis中删除,释放锁资源。
- 如果客户端持有的锁过期了,Redis会自动将该锁删除,避免锁的永久持有。
需要注意的是,Redisson还提供了一些额外的特性,如可重入锁、公平锁、红锁等,以满足不同的分布式锁需求。
总的来说,Redisson通过利用Redis的原子性操作和过期时间特性,实现了一个可靠的分布式锁机制,使得在分布式环境下可以安全地进行锁的获取、持有和释放。