Skip to content

Redis

1.Redis基础

1.1 Redis有哪些数据结构

  • String
  • Hash
  • List
  • Set
  • SortedSet(也称zset)

1.2 Redis数据结构详细说明

String

内部实现: 内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配

struct sdshdr {
long len;
long free;
char buf[];
};

使用SDS的优点:

  1. 获取字符串长度时间复杂度是O(1)
  2. 二进制安全

    什么是二进制安全?
    通俗地讲,C语言中,用’0’表示字符串的结束,如果字符串本身就有’0’字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

    SDS是怎么解决的?
    SDS使用len属性的值判断字符串是否结束,所以不会受’\0’的影响。

  3. 杜绝缓冲区溢出

    C字符串会溢出的原因
    字符串的拼接操作是使用十分频繁的,在C语言开发中使用char *strcat(char *dest,const char *src)方法将src字符串中的内容拼接到dest字符串的末尾。由于C字符串不记录自身的长度,所以strcat方法已经认为用户在执行此函数时已经为dest分配了足够多的内存,足以容纳src字符串中的所有内容,而一旦这个条件不成立就会产生缓冲区溢出,会把其他数据覆盖掉。 比如用恶意的字符覆盖一些内容,就容易被攻击。这个就是常见的缓冲区溢出攻击

    SDS是怎么解决的?
    与C字符串不同,SDS 的自动扩容机制完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查 SDS 的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题。

  4. 内存重分配次数优化

    空间预分配策略
    因为 SDS 的空间预分配策略, SDS字符串在增长过程中不会频繁的进行空间分配。通过这种分配策略,SDS 将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。

    惰性空间释放机制
    在SDS字符串缩短操作过程中, 多余出来的空间并不会直接释放,而是保留这部分空间,等待下次再用. 在更新字符串长度的过程中并没有涉及到内存的重分配策略,只是简单的修改sdshdr头中的len字段。

总结: SDS的结构还算是比较简单,Redis通过自己构建的SDS规避了传统C字符串潜在的性能问题,以及缓冲区溢出的风险,并且通过一系列策略以及数据结构的优化尽可能的节省了内存空间,此外,SDS为了和传统C字符串相兼容,在保存字符串的末尾也设置了空字符,使保存文本数据的SDS可以使用部分<string.h>库中的函数,而SDS自身也封装了一些修改字符串常见的操作,为Redis提供了简单可靠高性能的字符串操作API.

String字符串应用场景:

  1. 缓存功能
  2. 计数器。实际的业务使用:在ads平台投放搜索广告时,批次号的自增,算一个计数,使用的incr
  3. 分布式锁
  4. 共享用户Session。实际的业务使用:在spring容器中将session存储改为redis就行,就会自动使用
Hash

是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。(Hash可以只对某个字段修改)

List
  • 有序列表。通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西
  • 应用场景
    • 基于 Redis 实现简单的高性能分页。通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询
    • 简单的消息队列。Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过lpush命令从左边插入数据,多个数据消费者,可以使用Brpop命令阻塞的“抢”列表尾部的数据。基于异步消息队列List lpush-brpop(rpush-blpop)
      • 使用rpushlpush操作入队列,lpop和rpop操作出队列。List支持多个生产者和消费者并发进出消息,每个消费者拿到都是不同的列表元素。但是当队列为空时,lpop和rpop会一直空轮训,消耗资源;所以引入阻塞读blpop和brpop(b代表blocking),阻塞读在队列没有数据的时候进入休眠状态,一旦数据到来则立刻醒过来,消息延迟几乎为零。

        如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop和brpop或抛出异常, 所以在编写客户端消费者的时候要小心,如果捕获到异常,还要进行重试等操作处理异常。

      • 缺点
        • 做消费者确认ACK麻烦,不能保证消费者消费消息后是否成功处理(可能会有宕机或处理异常等),通常需要维护一个Pending列表,保证消息处理确认。
        • 不能重复消费,一旦消费就会被删除
        • 不支持分组消费
Set

无序集合,会自动去重的那种

应用场景:
基于Set可以做交集、并集、差集的操作,比如交集,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁。等等

Zset(也称SortedSet)

排序的Set,去重且可以排序,写进去的时候给一个分数,自动根据分数排序。

对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。

应用场景

  1. 可以实现延时队列(根据分数来实现延时队列)
  2. 排行榜

1.3 Redis数据结构高级用法

  • HyperLogLog: 提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV
  • GEO: 可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。业务场景:用Redis来实现附近的人、计算最优地图路径
  • BitMap: 位图是支持按 bit 位来存储信息,Bitmap 类型非常适合二值状态统计的场景,例如可以用来实现 布隆过滤器(BloomFilter)

1.4 Redis其他知识

  • Pipeline: 可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
  • Lua: Redis 支持提交 Lua 脚本来执行一系列的功能,原子性。用户积分、送优惠券之类的原子操作,要成功都成功
  • 事务: Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去
  • Pub/Sub:作简单的消息队列
    • 优点:
      • 典型的广播模式,一个消息可以发布到多个消费者
      • 多信道订阅,消费者可以同时订阅多个信道,从而接收多类消息
      • 消息即时发送,消息不用等待消费者读取,消费者会自动接收到信道发布的消息
    • 缺点:
      • 消息一旦发布,作为发送端的任务就结束了,至于消费端怎么样发送端是不管的。换句话就是发布时若客户端不在线,则消息丢失,不能寻回
      • 不能保证每个消费者接收的时间是一致的
      • 若消费者客户端出现消息积压,到一定程度,会被强制断开,导致消息意外丢失。通常发生在消息的生产远大于消费速度时
    • 适用场景: Pub/Sub 模式不适合做消息存储,消息积压类的业务,而是擅长处理广播,即时通讯,即时反馈的业务。

2. 使用缓存可能会出现的问题

2.1 缓存雪崩

A系统每天高峰期可以承受每秒1w个请求,其中被缓存承接过去9900个,但是有一天缓存宕机了,1w个请求全部打到数据库或者其他下游系统,导致数据库或者下游系统一个都起不来,都崩溃了。如果系统没有预案,只能等这一波流量都过去之后,才能正常启动及恢复

解决方案: 在失效时间上加上随机时间,不让过多的缓存在同一个时间一起失效 或者 设置永不过期,有更新操作再更新就好了

2.2 缓存击穿(击:有目的性的)

缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就跳过缓存,直接请求数据库,就像在一个完好无损的桶上瞬间凿开了一个洞。

关键词: 失效的瞬间,大量的请求打到数据库

根据场景解决

  1. 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
  2. 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于Redis、Zookeeper等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
  3. 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

2.3 缓存穿透(绕过某些规则)

主要是一些恶意请求,比如说正常的业务Id是都是正数,但是有个黑客构造了恶意请求,全部使用负数来请求,那么这些负数在缓存中查询不到,就会穿透到数据库查询,给数据库造成了压力

关键词:
穿透,使用一些特殊的手段绕过缓存,直接查到数据库

避免缓存穿透的利器之Bloom Filter
BloomFilter实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难

这部分误识别率,对于系统不会造成压力,所以可以很好的处理缓存击穿

Bloom Filter原理
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

Bloom Filter跟单哈希函数BitMap不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率

Bloom Filter缺点

  1. 存在误判
  2. 删除困难:一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter

Bloom Filter场景

  1. 监控数据收集: 在收集监控数据的时候, 有的监控数据量会很大, 需要检查一个监控项的名字是否已经被记录到DB过了, 如果没有的话就需要写入DB.
  2. 爬虫URL: 爬虫过滤已抓取的url就不再抓取,可用Bloom Filter过滤
  3. 垃圾邮件过滤: 如果用哈希表,每存储一亿个 email地址,就需要 1.6GB的内存(用哈希表实现的具体办法是将每一个 email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB的内存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解决同样的问题。

3. Redis使用场景

  1. 热点数据缓存
    实际业务使用到的场景有:缓存订单不常用数据
  2. 轻量级队列
    异步日志,订单操作日志记录
  3. 分布式锁

4. Redis持久化

4.1 RDB做镜像全量持久化(周期性的持久化)

原理(命令: bgsave):
fork: fork是指redis通过创建子进程(该过程是阻塞的)来进行RDB操作
cow: cow指的是copy on write

子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来

简单的描述原理: RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;

场景: 适合冷备

优点:

  • RDB对Redis的性能影响非常小,因为是fork了一个子线程来做的
  • 数据恢复的时候速度比AOF来的快

缺点:

  • 因为是定时快照,数据丢失的多
  • 如果快照文件很大,客户端会暂停几毫秒或者几秒

4.2 AOF做增量持久化(append-only模式)

简单的描述原理: 以文本日志的形式记录 Redis 处理的每一个写入或删除操作。整体是通过一个后台的线程fsync来操作的。

场景: 适合热备

优点:

  • 数据完整性比RDB高,最多丢失1s的数据
  • append-only 追加写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。

缺点:

  • AOF文件比RDB还要大
  • AOF开启后,Redis支持写的QPS会比RDB支持写的要低(但是不影响Redis的高性能)

其他
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

4.3 RDB&AOF都开启时

RDB和AOF全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

5. Redis为什么快?

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
    1. SDS简单动态字符串
    2. 字典
    3. 跳跃表。跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

    最新的一版已经是多线程了,利用现代计算机多核优势。网络处理是多线程,但是命令执行过程还是单线程处理的

  4. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制(类似Java的VM),因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

    Redis 的 VM (虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。Redis 提高数据库容量的办法有两种:一种是可以将数据分割到多个 Redis Server上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。「需要特别注意的是 Redis 并没有使用 OS 提供的 Swap,而是自己实现。」

    Redis 为了保证查找的速度,只会将 value 交换出去,而在内存中保留所有的 Key。所以它非常适合 Key 很小,Value 很大的存储结构。如果 Key 很大,value 很小,那么vm可能还是无法满足需求。

  5. 合理的线程模型。使用多路I/O复用模型,非阻塞IO

    多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

  6. 合理的数据编码。Redis 支持多种数据类型,每种基本类型,可能对多种数据结构

6.Redis内存块满时,会走淘汰策略

背景

不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。

maxmemory

有哪些策略

  • FIFO: 淘汰最早数据
  • LRU(Least Recently Used): 剔除最近最少使用
    • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放
    • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • LFU(Least Frequently Used): 剔除最近使用频率最低的数据

    LFU的全称是”Least Frequently Used”,它是一种缓存淘汰策略,用于确定在缓存空间不足时应该移除哪些项目。LFU策略的核心思想是淘汰那些被访问次数最少的项目,以腾出空间来存储更频繁访问的项目。这是一种基于访问频率的缓存淘汰策略。

  • noeviction(不驱逐): noeviction是Redis中的一个配置选项,用于控制在内存不足时Redis服务器的行为。具体来说,noeviction选项决定了当Redis的内存使用达到了配置的最大内存限制时,服务器是否应该继续接受写入操作,还是应该拒绝写入操作以避免数据丢失
    • noeviction yes:当Redis的内存使用达到最大内存限制时,服务器将拒绝任何写入操作,以确保数据的持久性和安全性。这意味着Redis不会自动删除任何数据以腾出空间,而是让客户端应用程序处理内存不足的情况。
    • noeviction no:当Redis的内存使用达到最大内存限制时,服务器将尝试使用一种淘汰策略来删除一些数据,以腾出足够的空间来接受新的写入操作。Redis支持多种淘汰策略,例如LRU(Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不经常使用)等。使用这个选项,Redis将自动管理内存,但可能会删除一些数据以满足内存限制。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

7.Redis高可用

Redis Sentinal

Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务

它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。

哨兵节点主要负责三件事情:监控主节点、选主通知从节点和客户端。

为了减少误判的情况,哨兵在部署的时候不会只部署一个节点,而是用多个节点部署成哨兵集群(最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。

哨兵的主要功能
  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
哨兵部署节点数

哨兵必须用3️⃣个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。

经典的哨兵集群
- M1,S1
- R2,S2
- R3,S3
M: master主节点
S: 从节点
R: 另外的Redis节点,在同一网络中独立存在
选举新的master策略
  • 同等情况下,slave 复制的数据越多优先级越高
  • slave 的 priority 设置的越低,优先级越高
  • 相同的条件下 runid 越小越容易被选中
主从数据同步描述

你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。

master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命令都发给slave。

数据传输的时候断网了或者服务器挂了怎么办啊?

传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的(psync指令会补数)

Redis Cluster

Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储

Redis Cluster功能: 负载均衡故障切换主从复制

Redis Cluster 使用分片机制,在内部分为16384个slot插槽,分布在所有master节点上,每个master节点负责一部分slot。数据操作时计算该key的CRC16结果再模16834来计算在哪个slot,由哪个master进行处理。数据的冗余是通过slave节点来保障。

进一步解释: Redis Cluster包含多个主节点,去中心化,每个主节点负责管理一部分slot插槽。主节点处理客户端的写入请求,并负责数据的分发。

8.Redis双写一致性、并发竞争、线程模型

双写一致性

最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。不更新缓存的原因是你更新了可能很长一段时间没有被用到,所以白白浪费了系统资源,所以删掉最合适,等系统下一次用到就会触发计算然后缓存结果,这样可以节省系统资源,特别是对于那种需要经过大量计算才能得到结果的任务。反正无论如何保持一个原则,用到缓存才去算缓存,这也是懒加载的一个思想。CAP模式的lazy思想

    不知道你发现了没有,上面这种处理方式是有问题的(下面有解释)。先删缓存,再更新数据库就没有问题了

不一致的场景

场景

先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。

解决方案: 先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中

复杂的场景

数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了…

解决方案: jvm队列。其实从场景描述中就可以感觉到如果顺序(串行)的来处理就不会出现这个问题。为什么使用jvm队列呢?因为速度足够快,会最大限度的让用户无感知

  • 这个一定要根据真实的线上场景数据测试好,不然会导致大量的请求超时
  • 当然不能紧着一个队列来,多搞几个队列,减少请求超时的情况(得确保同一个业务id hash到相同的队列上哈,不然数据还是乱的)

并发竞争

这个也是线上非常常见的一个问题,就是多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。(这就是典型的并发问题,用解决并发的方法来解决就好)

解决方案: 使用zk分布式锁,Redis分布式锁等,谁获取到,谁操作

9.做好保障

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免数据库被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

10.为啥redis zset使用跳跃链表而不用红黑树实现

skiplist的复杂度和红黑树一样,而且实现起来更简单。

在并发环境下红黑树在插入和删除时需要rebalance,性能不如跳表。

11.Redis

  • 与 Memcache 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:
    • 一个是因为采用了非阻塞的异步事件处理机制;
    • 另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
  • Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库
  • 相比 Memcache,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
  • Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务

12.Key失效机制

Redis 的 key 可以设置过期时间,过期后 Redis 采用主动和被动结合的失效机制,一个是和 Memcache一样在访问时触发被动删除,另一种是定期的主动删除。

Redis过期策略

  • 定期删除: 定期好理解,默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了
  • 惰性删除: 见名知意,惰性嘛,我不主动删,我懒,我等你来查询了我看看你过期没,过期就删了还不给你返回,没过期该怎么样就怎么样

13.关于Redis锁相关问题

  • 死锁:设置过期时间
  • 过期时间评估不好,锁提前过期:守护线程(watch dog),自动续期
  • 使用分布式锁,如果锁内业务超时会怎么处理?使用守护线程自动处理, Redisson框架就实现了
  • 锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放
  • 如何解决主从切换后,锁失效问题?Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁)

14. 有哪些缓存类型

  1. 本地缓存
    • Jvm堆中的缓存,可以使用LRUMap来实现,例如LRUMap
    • Ehcache

    优缺点: 本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

  2. 分布式缓存

    优缺点: 可以解决本地缓存的问题,分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。

  3. 多级缓存

    为了平衡本地缓存和分布式缓存,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中

15. 相关问题

Redis大key如何处理?

大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。

一般而言,String 类型的值大于 10 KB、Hash、List、Set、ZSet 类型的元素的个数超过 5000个就是大key了

如果存了大的value数据会带来哪些问题?
  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。
如何找到大key?
  • redis-cli --bigkeys
  • 使用 SCAN 命令查找大 key。使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。对于 String 类型,可以直接使用 STRLEN 命令获取字符串的长度。对于集合类型,就获取集合类型的元素个数了
  • 第三方工具 RdbTools
删除大key时要分批删除

删除操作的本质是要释放键值对占用的内存空间,不要小瞧内存的释放过程。

释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。

所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。

  • 删除大的Hash,先使用hscan获取一批,再使用hdel一个一个删
  • 删除大的List,使用ltrim,每次删除少量元素
  • 删除大的Set,先使用sscan获取一批,再使用srem一个一个删
  • 删除大的ZSet,使用zremrangebyrank每次删除指定的top N个元素,这个N也是不要太大
大key异步删除

从 Redis 4.0 版本开始,可以采用异步删除法,用 unlink 命令代替 del 来删除。

这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。