介绍一下Redis
Redis 是一个用 C 语言开发的数据,并且 Redis 的数据是存储在内存中的,读写速度非常快,几乎不受 CPU 和磁盘 I/O 的影响。 Redis 被广泛应用在缓存方面,它支持多种数据类型如字符串、列表、集合、有序集合、哈希等;还支持事务,且操作遵循原子性;并且拥有其他强大的功能如持久化、主从复制、集群等。
Redis 可以用来做计数器、缓存、查找表(如 DNS 记录可用 Redis 进行存储)、消息队列( List 是一个双向队列,可以通过 lpush 和 rpop 写入和读取消息)、会话缓存( Redis 存储多台应用服务器的会话信息)、分布式锁(可以使用 Redis 自带的 SETNX 命令实现分布式锁)。
Redis和Memcached的区别
二者都是非关系型数据库,主要有以下不同:
- 数据类型: Redis 支持五种不同的数据类型,而 Memcached 仅支持字符串类型。
- 数据持久化: Redis 支持两种持久化策略,分别是 RDB 快照和 AOF 日志,而 Memcached 不支持数据持久化。
- 分布式: Redis Cluster 实现了分布式支持,而 Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
- 内存管理机制:在 Redis 中,并不是所有数据都一直存储在内存中,可将一些很久没用的数据交换到磁盘。而 Memcached 的数据则会一直存储在内存中,内存用完后会报异常。为了完全解决内存碎片问题, Memcached 会将内存分割成特定长度的块来存储数据,但这样会造成内存的利用率不高。
Redis常见数据结构
-
String: String 是 Redis 中最基本的数据类型,以
key-value的形式存在,它是二进制安全的,即可正确存储二进制数据。 Redis 基于 C 实现了 SDS ,即简单动态字符串,它不仅能保存字符串数据,还能够保存二进制数据,并且获取字符串长度为 O(1) ,相比于 C 语言的 O(n) 性能提升了不少。并且 SDS 不会有缓冲区溢出的问题。应用场景:各种计数场景如统计用户访问次数,统计文章点赞、转发数量等。
-
Hash: String 元素组成的字典,适用于存储对象。它是基于压缩列表和字典实现的。
应用场景:系统中对象数据的存储。
-
List:列表,按照 String 元素的插入顺序排序。它是基于压缩列表和链表实现的。
应用场景:消息队列
-
Set: String 元素组成的无序集合,通过哈希表实现,不允许重复。
应用场景:需要存放不能重复的数据以及需要求集合运算的场景,例如: QQ 共同好友、微博共同关注。
-
zSet:有序集合,基于压缩列表和跳跃表进行实现,通过分数 (score) 来为集合中的成员进行从小到大排序(分数越小排越前面)。
应用场景:排行榜。
-
补充:用于计数的 HyperLogLog ,用于支持存储地理位置信息的 Geo 。
Redis单线程模型
Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。
Redis 的文件事件处理器( file event handler )是单线程的,但采用了 IO 多路复用机制同时监听多个 socket ,根据 socket 上的事件来选择对应的事件处理器进行处理。
关于 IO 多路复用模型可查看我的这篇文章:https://www.yeliheng.com/articles/a9a016c7
文件事件处理器的结构包含4个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(包括连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket ,会将 socket 放入一个队列中排队,然后每次从队列中取出一个 socket 给事件分派器,事件分派器再把 socket 分派给对应的事件处理器去处理。当一个 socket 的事件被处理完成后,才会到队列中取下一个 socket 交给事件分派器。
为什么Redis单线程模型效率也这么高?
答: Redis 是纯内存操作,而且采用了非阻塞的 IO 多路复用机制,单线程也避免了多线程竞争和上下文频繁切换等问题。
Redis 6之前为什么一直不使用多线程?
答:因为单线程复杂度相对于多线程要低,易于编码和维护; Redis 的性能瓶颈不是 CPU ,主要是内存和网络;多线程存在上下文切换问题,并且加锁解锁操作可能导致死锁。
那Redis 6之后为什么引入了多线程?
Redis6 之所以引入多线程主要是为了提高网络IO的读写性能。但 Redis 的多线程只是应用在网络数据的读写上,执行命令依然是单线程顺序执行。
具体的工作流程如下:
- 主线程接收连接请求,获取客户端 socket 并放入等待读处理队列;
- 主线程处理完读事件之后,将等待队列中的 socket 分配给 IO 线程组;
- 主线程阻塞等待 IO 线程读取 socket 并解析请求;
- socket 读取完毕后,主线程通过单线程的方式执行所有请求命令,读取并解析请求数据;
- 主线程阻塞等待 IO 线程将数据回写到 socket;
- 解除绑定,清空等待队列;
总结: IO 线程组要么同时在读 socket ,要么同时在写 socket ;并且 IO 线程组只负责读写 socket ,主线程才负责命令处理。
Redis 6开启多线程
Redis 6 的多线程是默认禁用的,开启需要将 redis.conf 的 io-threads-do-reads 设置成 yes ;开启多线程后要设置线程数,不然不会生效,直接设置 io-threads 的数量。官方建议 4 核的机器建议设置为 2 或 3 个线程, 8 核的建议设置为 6 个线程,线程数一定要小于机器核数。
Redis 持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到磁盘上。
Redis持久化方式及其区别
Redis 提供两种持久化机制,分别是快照持久化和AOF持久化。
快照持久化
Redis 可以通过创建快照来获取某时刻内存中数据的副本,并写入一个临时文件,持久化结束后,用这个临时文件替换上次的持久化文件,达到数据恢复的目的。其内部是通过 BGSAVE 指令创建快照, BGSAVE 会在后台子线程执行保存操作。在 Redis 配置文件中可以指定触发快照的条件,具体如下:
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1
save 300 10
save 60 10000
三条语句的意思分别是:在 15 分钟后,若至少有 1 个 key 发生变化,则创建快照;在 5 分钟后,若至少 10 个 key 发生变化,则创建快照;在 1 分钟后,若至少 10000 个 key 发生变化,则创建快照。
快照持久化的优缺点:
优点:
- 只有一个文件
dump.rdb,方便持久化。 - 性能最大化, fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。
- 相对于数据集大时,比 AOF 的启动效率更高。
缺点:
- 数据安全性低。 RDB 是间隔一段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。
AOF(Append-only file)持久化
AOF 方式的持久化会将所有的命令行记录仪Redis命令格式完全存储在磁盘上,保存为 .aof 文件,数据量大将占用很大的磁盘空间。 Redis 默认没有使用 AOF 的方式,需要在配置文件中将 appendonly 设置为 yes。
配置文件中有 3 种不同的 AOF 持久化方式:
-
每次有数据修改发生时都会写入 AOF 文件中,会严重降低性能。
-
每秒同步一次,显式地将多个命令同步到硬盘。
-
让操作系统决定何时进行同步。
AOF 持久化的优缺点:
优点:
- 数据安全, AOF 持久化可以配置
appendfsync属性为always,每进行一次命令操作就记录到 AOF 文件中一次。 - 通过
append模式写文件,即使中途服务器宕机,也可以通过redis-check-aof工具解决数据一致性问题。
缺点:
- AOF 文件体积相比于快照要大得多,并且恢复速度较慢。
- 数据集大时,启动的效率低。
混合持久化
AOF rewrite :先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录该键值对的多个命令。就可以使得新的 AOF 文件和旧的 AOF 文件保存的数据状态是一样的,但是体积更小。
Redis 4.0 开始支持快照和 AOF 混合持久化。即:在 AOF 重写的时候直接把快照内容写到 AOF 文件开头,可以使得快速加载的同时又能够避免丢失过多的数据。这是 Redis 4.0 后默认的持久化方式。
补充:BGSAVE原理
原理图如下:

Redis内部如何判断数据是否过期
RedisDB 结构中的 expires 字典保存了数据库中所有键的过期时间,被称为过期字典。过期字典中的键就是一个指针,指向键空间中某个键对象;过期字典的值是一个 long long 类型的整数,保存了对应键的过期时间。
Redis过期数据删除策略
定时删除
定时删除即过期就马上删除,对内存友好,但会占用太多 CPU 时间,影响服务器的响应时间和吞吐量。
惰性删除
过期不立即删除,被访问才删除,对 CPU 最友好,但很可能导致很多的 key 没有被删除。
定期删除
每隔一段时间抽取一批 key,然后删除过期的 key。 Redis 底层会通过限定删除操作执行的时长和频率来减少对 CPU 的影响。
Redis实际使用的是惰性删除和定期删除这两种策略。但惰性删除和定期删除也可能漏掉很多过期 key 的情况,如果大量过期的 key 堆积在内存里,就可能会导致 out of memory。 Redis 用内存淘汰机制来解决这个问题。
Redis的数据淘汰策略
- volatile-lru:从已设置过期时间的数据集中,挑选最近最少使用的数据被淘汰;
- volatile-ttl:从已设置过期时间的数据集中,挑选将要过期的数据被淘汰;
- volatile-random:从已设置过期时间的数据集中,随机挑选数据被淘汰。
- allkeys-lru:在键空间中,挑选最近最少使用的数据被淘汰;
- allkeys-random:在键空间中,随机挑选数据被淘汰;
- no-eviction:禁止淘汰数据,当内存不足以容纳新写入的数据时,会直接保存。
在 Redis4.0 之后,还增加了两种策略:
- volatile-lfu:从已设置过期时间的数据集中,挑选最不经常使用的数据被淘汰;
- allkeys-lfu:从键空间中,挑选最不经常使用的数据被淘汰;
LRU和LFU的区别
LRU:最近最少使用,如果数据最近被访问过,将来被访问的可能性也很高。实现:新数据插入链表头部;每当缓存命中,则将命中数据移到链表头部;当链表满时,将链表尾部数据丢弃。
LFU:最不经常使用,如果数据在过去被访问次数最多,那么将来被访问的可能性也很高。实现:新加的数据插入队列尾部;队列中的数据被访问后,引用计数加1,队列重新排序;需要淘汰数据时,删除队列最后的数据。
使用Pipeline有什么好处,为什么要用Pipeline
Redis 是基于请求/响应模型,单个请求处理需要一一应答。在我们需要大批量向 Redis 中写入数据时,如果不使用 Pipeline,则 Redis 需要对每条指令一一返回操作结果,会耗费大量时间和系统 I/O 以及网络请求。为了提升效率,我们可以使用 Pipeline 批量执行指令,将多次 IO 往返的时间缩短为 1 次,并且无需等待上一条指令执行的结果就可以继续执行下一条指令。但要注意,Pipeline 中执行的指令需要没有依赖关系。有依赖关系的指令建议按顺序分批进行发送。
Redis事务
Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位。每个事务都是一个单独的隔离操作,事务中的所有命令都会序列化并且按顺序执行。服务端在执行事务的过程中,不会被其他客户端发送来的指令中断。Redis 的事务与 MySQL 不同,Redis 事务是不支持回滚的。若执行的指令存在语法错误,Redis 会抛出失败异常,可在应用层进行捕获。但若出现其他问题,Redis 依然会继续执行剩下的指令。这样做的原因是:回滚需要增加许多额外的工作,不支持回滚可以保持简单,快速的特性。符合 Redis 设计的初衷。
Redis 可以通过以下命令来实现事务:
- MULTI:标记一个事务块的开始。
- DISCARD:放弃执行事务块内的所有指令。
- EXEC:执行事务块内的所有指令。
- WATCH:监视一个或多个 key,在事务执行之前如果 key 被其他命令修改过,那么事务将会被打断。
缓存穿透、缓存击穿、缓存雪崩
缓存穿透
缓存穿透是指查询一个在缓存中不存在,在数据库中也不存在的数据,由于缓存不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个根本不存在的数据每次请求都要到数据库中去查询,进而给数据库带来很大压力。最常见的解决方法是使用布隆过滤器。
布隆过滤器
布隆过滤器是(Bloom Filter)1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
向布隆过滤器中添加元素时布隆过滤器会进行如下操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有N个哈希函数就得到N个哈希值);
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
判断一个元素是否存在布隆过滤器中的操作:
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
但布隆过滤器存在误判的情况,可能一个原先不存在的数据判断出来确实存在的。这是因为不同数据计算出来的哈希值可能是相同的,所以布隆过滤器判断出元素存在,那该元素不一定存在;判断出元素不存在,那该元素一定不存在。误判率的大小跟给定的数组长度和哈希函数的个数有关。我们可以通过增加数组长度和增加哈希函数的数量来减少误判率,但这会造成性能的开销。
缓存击穿
缓存击穿的概念请注意不要与缓存穿透混淆。缓存击穿是指存在一个热点数据,大量用户集中对这一个热点进行访问,这个数据的 key 在缓存中一旦失效,大量请求就会同时涌入数据库,会造成数据库瞬间压力过大。
解决方案:
- 可以设置热点数据永不过期;
- 加互斥锁,使用分布式锁。保证对于每个 key 只有一个线程去查询数据库,其他线程等待,将高并发压力从数据库转移到分布式锁上。
缓存雪崩
缓存雪崩是指缓存在同一时间大面积失效,使得请求都直接落在数据库上,造成数据库短时间内承受大量请求。
解决方案:
- 针对 Redis 服务不可用的情况,可以采用 Redis 集群,避免单机出现问题整个缓存服务都不可用的情况;
- 针对热点缓存失效的情况,可以设置不同的失效时间或者设置热点缓存永不失效。
补充
-
缓存预热:缓存预热值系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,请求会直接到达数据库,然后再将数据缓存起来的问题。
实现方案:
- 数据量不大的情况下可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
- 手动刷新缓存。
-
缓存更新:除了缓存服务器自带的缓存失效策略之外( Redis 有 6 种策略可供选择),我们还可以根据具体的业务需求进行自定义缓存淘汰策略。
常见的策略有两种:(1)定时去清理过期的缓存;(2)当有用户请求时,再判断这个请求所用到的缓存是否过期,过期的话就去请求新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂。具体用哪种方案,大家可以根据自己的应用场景来权衡。
-
缓存降级:当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 降级的最终目的是保证核心服务可用,即使是有损的。
降级方案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在 95~100% 之间),可以自动降级或人工降级,并发送报警信息;
- 重要警告:比如可用率低于 90%、数据库连接池已满、访问量突然猛增到系统能承受的最大阈值,此时可以根据情况自动降级或者人工降级;
- 严重警告:因特殊原因数据错误,此时需要紧急人工降级。服务降级的目的是为了防止 Redis 服务故障,导致数据库请求过多跟着一起发生崩溃的问题。因此,对于不重要的缓存数据,可以采取服务降级策略。例如对于一些非热点数据或冷数据可直接绕过缓存去请求数据库,以保证核心服务的可达性。
Redis同步机制
主从同步

Redis 虽然读取和写入的速度都特别快,但也会存在读压力特别大的情况。为了分担读压力,Redis 支持主从复制,Redis 的主从结构可以采用一主多从或者级联结构,上图为级联结构。
Redis 主从复制可以分为全量同步和增量同步。
全量同步过程
- Slave 连接 Master,并发送
SYNC命令; - Master 接收到
SYNC命令后,开始执行BGSAVE命令生成快照。在快照生成的期间会使用缓冲区将接收到的写命令全部缓存起来; - Master 的快照保存完后,会向所有 Slave 发送快照文件,并在发送期间继续记录被执行的写命令;
- Slave 收到快照文件后丢弃所有旧数据,载入收到的快照;
- Master 在快照发送完毕后开始向 Slave 发送缓冲区中的写命令;
- Slave 完成对快照的载入,开始接收命令请求,并执行来自 Master 缓冲区的写命令。
全量同步完成后,为了提升性能,我们会让读操作都交给 Slave 节点,Master 只负责写操作。所以在有新的写入,Master 就要及时地将其传播到所有 Slave,以便各节点数据保持一致,这就需要进行增量同步。
增量同步过程
- Master 接收到用户的操作指令,判断是否需要传播到 Slave;
- 将操作记录追加到 AOF 文件中;
- 将操作传播到其他 Slave:
- 对齐主从库:即确保 Slave 是该操作所对应的数据库;
- 往响应缓存中写入指令
- 将缓存中的数据发送给 Slave。
主从同步的弊端:不具备高可用性。当 Master 服务挂掉之后,Redis 将不能对外提供写入操作,因此 Redis Sentinel 应运而生。
Redis Sentinel-哨兵模式
Redis Sentinel 即 Redis 哨兵,是 Redis 官方提供的集群管理工具,其本身也是一个独立运行的进程,它能监控多个 Master-Slave 集群,发现 Master 宕机后能够自动进行切换。
Redis Sentinel 功能:
- 监控:检查主从服务器是否运行正常。
- 通知:当被监控的 Redis 服务器出现问题时,Redis 哨兵可以通过 API 向管路员或其他应用程序发送故障通知。
- 自动故障迁移:当一个服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作。它会将一个失效 Master 下的其中一个 Slave 升级为新的 Master,并让之前的 Slave 复制新的 Master 的数据。
Redis集群
常见的Redis集群架构
- 主从复制集群:见上文。
- Redis Sentinel:哨兵模式,项目体量较小时可选用。见上文。
- Redis Cluster:Redis Cluster 即 Redis 集群模式,是 Redis 官方提供的集群化方案。哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题。但哨兵模式还是存在难以在线扩容,Redis 容量受限于单机配置的问题。Cluster 模式实现了 Redis 的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。体量较大时,选择 Redis Cluster。
- Twemprox:Twemprox 是 Twitter 开源的一个 Redis 和 Memcached 代理服务器,主要用于管理 Redis 和Memcached 集群,减少与缓存服务器直接连接的数量。
- Codis:Codis 是一个代理中间件,当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 来执行,并将结果返回给客户端。一个 Codis 实例可以连接多个 Redis 实例,也可以启动多个 Codis 实例来支撑,每个 Codis 节点都是对等的,这样可以增加系统整体的 QPS 需求,还能具备容灾能力。
- 客户端分片:在Redis Cluster 还没出现之前使用较多,现在基本很少使用了,在业务代码层实现,运行几个毫无关联的 Redis 实例,在代码层,对 key 进行 hash 计算,然后去对应的 Redis 实例操作数据。这种方式对 hash 层代码要求比较高,需要考虑节点失效后的替代算法方案、数据震荡后的自动脚本恢复、实例的监控等。
是否用过Redis Cluster?它的工作流程是怎样的?
Redis集群中内置了 16384 个哈希槽,在 Redis 的每个节点上,都有一个插槽(slot),取值范围为 0-16383 。当我们存取 key 的时候,Redis 会根据 CRC16 算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
为了保证高可用,Cluster 模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么就认为主节点 A 宕机了。如果主节点 A 和它的从节点都宕机了,那么该集群就无法再提供服务了。Cluster 模式集群节点最小配置 6 个节点( 3 主 3 从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster集群方案什么情况下会导致整个集群不可用?
Redis 没有使用哈希一致性算法,而是使用哈希槽。Redis 中的哈希槽一共有 16384 个,计算给定密钥的哈希槽,我们只需要对密钥的 CRC16 取摸 16384。假设集群中有 A、B、C 三个集群节点,不存在复制模式下,每个集群的节点包含的哈希槽如下:
-
节点 A 包含从 0 到 5500 的哈希槽;
-
节点 B 包含从 5501 到 11000 的哈希槽;
-
节点 C 包含从 11001 到 16383 的哈希槽;
这时,如果节点 B 出现故障,整个集群就会出现缺少 5501 到 11000 的哈希槽范围而不可用。
Redis Cluster哈希槽
Redis 集群并没有使用一致性 hash,而是引入了哈希槽的概念。Redis 集群有 16384(2^14)个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
场景题
如何从1亿条数据中查询某一固定前缀的key?
在面试中被问到这类题目,如果面试官没有指定数据规模,那么面试者应询问数据规模,摸清是 100 条, 1000 条还是一亿条。不同数据规模有不同的解决方法。在数据规模小的时候,我们可以直接使用 keys <pattern> 指令直接进行查找,使用 keys 指令会扫描出所有的数据,这样做十分方便,但在数据规模庞大时可能会造成Redis忙碌导致其他查询无法正常到达,在生产环境中是不可取的。所以在本题的1亿条数据我们要使用另外一种方式,即 SCAN 指令。
指令格式:SCAN cursor [MATCH pattern] [COUNT count]
使用 SCAN 不会一次性查出所有数据,而会根据我们给定的 count 以及 pattern 进行匹配。此处要注意,即使指定了 count, SCAN 一次返回的数量是不可控的,只能是大概率符合 count 给定的数值。因此 SCAN 不保证每次执行都返回某个给定数量的元素。
cursor 即游标。SCAN 是基于游标的迭代器,cursor 指定为多少,下一次就会基于这个指定的 cursor 延续之前的迭代过程,直到 cursor 为0时则视为一次完整的迭代结束。此处需注意:SCAN 返回的 cursor 并不一定是从上次迭代的最后一个元素开始,可能会是最后一个元素之前的元素,这就可能造成重复迭代。所以在实际的代码场景中,我们需要对其进行去重操作,可以使用集合的数据结构来进行去重。
如何通过Redis实现分布式锁?
分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性,这就需要使用到共享的中间件例如 Redis、Zookeeper 等。
在 Redis 中,我们用两种方式来实现分布式锁:
SETNX + EXPIRE(错误示例)
SETNX 是 SET IF NOT EXISTS 的简写。日常命令格式是 SETNX <key> <value>,如果 key 不存在,则 SETNX 成功返回1,如果这个 key 已经存在了,则返回 0。我们可以先使用 SETNX 指令来抢占锁,然后使用 EXPIRE 指令为其设置一个过期时间,释放该锁。
这样做问题很明显,一个操作被分成了两条指令执行,若执行第一条指令时宕机,则第二条指令永远不会被执行,其他线程也就永远无法获取到该锁了。这样做违背了操作的原子性。
所以我们要引入第二种方案,即使用 SET 一条指令来上锁,释放锁。这样就遵循了操作的原子性。
SET方式
这种方案的指令如下:SET <key> <value> [EX seconds] [PX milliseconds] [NX|XX]
- EX seconds:设置键的过期时间为 seconds 秒
- PX milliseconds:设置键的过期时间为 milliseconds 毫秒
- NX:只在键不存在时,才对键进行设置操作
- XX:只在键已经存在时,才对键进行设置操作
- SET操作成功完成时,返回 OK,否则返回 (nil)
这样,一条指令就能够实现加锁解锁,保证操作的原子性。此处需要注意:若同一时间大量 key 过期,可能也会导致 Redis 服务暂时不可用,所以我们可以使用随机数将过期时间分散化,避免同一时刻key大量过期消耗大量性能的问题。
如何使用Redis实现一个异步消息队列?
我们可以使用list结构作为队列,rpush 作为生产者生产消息,lpop 作为消费者消费消息。这种模式有个问题,在队列中没有消息时,lpop 并不会阻塞等待,而是会返回 (nil)。所以要实现异步消息队列还需要在代码中对 lpop 进行适当的 sleep 以等待消息。
面试官可能会问:可不可以不用 sleep 呢?答案是肯定的。我们可以使用 Redis 自带的指令:blpop,在没有消息的时候,blpop 会阻塞直到消息到来。
面试官可能接着问:能不能生产一次供多个消费者使用呢?答:可以的,使用 pub/sub,即发布-订阅模式。消费者订阅一个频道,生产者在该频道发送消息,所有订阅该频道的消费者都会接收到。具体关于发布-订阅模式的话题,请查看我的设计模式的文章:https://www.yeliheng.com/articles/8ce8924f
面试官还会继续问:使用发布订阅模式有什么缺点?答:此处 Redis 实现的发布订阅模式无法保证消息的正常送达,也就是在消费者下线的情况下,生产的消息会丢失。要解决这个问题应使用专业的消息队列,如 RabbitMQ、Kalfka。
进阶问题:面试官可能还问:如何实用Redis实现延时队列?答:使用 zSet,以时间戳作为 score,消息内容作为 key,然后调用 zadd来生产消息,消费者用 zrangebyscore 指令来获取N秒之前的数据轮询进行处理。