《八股文》20 道 Redis 面试题
《八股文》20 道 Redis 面试题
以下文章来源于爱笑的架构师,作者雷架
爱笑的架构师
死磕技术,热爱生活!
强烈推荐👉项目消息推送平台Austin(10K+ stars),可以用作毕业设计,可以用作校招,可以看看生产环境是怎么推送消息的。
1、什么是 Redis,Redis 有哪些特点?
Redis 全称为:Remote Dictionary Server(远程字典服务),Redis 是一种支持 key-value 等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
特点 1:丰富的数据类型
我们知道很多数据库只能处理一种数据结构:
- 传统 SQL 数据库处理二维关系数据;
- MemCached 数据库,键和值都是字符串;
- 文档数据库(MongoDB)是由 Json/Bson 组成的文档。
当然不是他们这些数据库不好,而是一旦数据库提供数据结构不适合去做某件事情的话,程序写起来就非常麻烦和不自然。
Redis 虽然也是键值对数据库,但是和 Memcached 不同的是:Redis 的值不仅可以是字符串,它还可以是其他五中数据机构中的任意一种。通过选用不同的数据结构,用户可以使用 Redis 解决各种各样的问题,使用 Redis,你碰到一个问题,首先会想到是选用那种数据结构把哪些功能问题解决掉,有了多样的数据结构,方便你解决问题。
特点 2:内存存储
数据库有两种:一种是硬盘数据库,一种是内存数据库。
硬盘数据库是把值存储在硬盘上,在内存中就存储一下索引,当硬盘数据库想访问硬盘的值时,它先在内存里找到索引,然后再找值。问题在于,在读取和写入硬盘的时候,如果读写比较多的时候,它会把硬盘的 IO 功能堵死。
内存存储是讲所有的数据都存储在内存里面,数据读取和写入速度非常快。
特点 3:持久化功能
将数据存储在内存里面的数据保存到硬盘中,保证数据安全,方便进行数据备份和恢复。
2、Redis 有哪些数据结构?
Redis 是 key-value 数据库,key 的类型只能是 String,但是 value 的数据类型就比较丰富了,主要包括五种:
- String
- Hash
- List
- Set
- Sorted Set
其实远不止这几种,具体请看 Redis支持的数据结构
(1)String 字符串
语法
SET KEY_NAME VALUE
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化的对象。string 类型是 Redis 最基本的数据类型,一个键最大能存储 512MB。
(2)Hash 哈希
语法
HSET KEY_NAME FIELD VALUE
Redis hash 是一个键值 (key=>value) 对集合。Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
(3)List 列表
语法
//在 key 对应 list 的头部添加字符串元素
LPUSH KEY_NAME VALUE1.. VALUEN
//在 key 对应 list 的尾部添加字符串元素
RPUSH KEY_NAME VALUE1..VALUEN
//对应 list 中删除 count 个和 value 相同的元素
LREM KEY_NAME COUNT VALUE
//返回 key 对应 list 的长度
LLEN KEY_NAME
Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)
(4)Set 集合
语法
SADD KEY_NAME VALUE1...VALUEn
Redis 的 Set 是 string 类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
(5)Sorted Set 有序集合
语法
ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN
Redis zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。
redis 正是通过分数来为集合中的成员进行从小到大的排序。
zset 的成员是唯一的,但分数 (score) 却可以重复。
ZADD
向有序集合中添加元素时,如果多个客户端同时向集合中添加元素,Redis 会确保每次操作的原子性。这意味着即使多个客户端并发执行 ZADD
,每个命令都会独立执行,不会被中途打断。
例如:
ZADD myzset 1 "element1" ZADD myzset 2 "element2"
即使有多个并发请求,只要 Redis 进程处理这些命令时会确保每个操作是独立、顺序执行的
有时你可以通过设置合理的分数(score)来控制竞争,比如用时间戳或递增的计数器作为分数,这样即使多个客户端同时向集合中插入元素,也能通过分数确保元素在集合中的顺序。
例如,使用时间戳作为分数可以避免冲突:
ZADD myzset 1674000000 "element1" ZADD myzset 1674000001 "element2"
3、一个字符串类型的值能存储最大容量是多少?
查询官方文档(https://redis.io/topics/data-types)可以看到 String 类型的 value 值最多支持的长度为 512M,所以正确的答案是 512M。
4、能说一下 Redis 每种数据结构的使用场景吗?
(1)String 的使用场景
字符串类型的使用场景:信息缓存、计数器、分布式锁等等。
常用命令:get/set/del/incr/decr/incrby/decrby
实战场景 1:记录每一个用户的访问次数,或者记录每一个商品的浏览次数
方案:
常用键名:userid:pageview 或者 pageview:userid,如果一个用户的 id 为 123,那对应的 redis key 就为 pageview:123,value 就为用户的访问次数,增加次数可以使用命令:incr。
使用理由:每一个用户访问次数或者商品浏览次数的修改是很频繁的,如果使用 mysql 这种文件系统频繁修改会造成 mysql 压力,效率也低。而使用 redis 的好处有二:使用内存,很快;单线程,所以无竞争,数据不会被改乱。
实战场景 2:缓存频繁读取,但是不常修改的信息,如用户信息,视频信息
方案:
业务逻辑上:先从 redis 读取,有值就从 redis 读取,没有则从 mysql 读取,并写一份到 redis 中作为缓存,注意要设置过期时间。
键值设计上:
直接将用户一条 mysql 记录做序列化 (通常序列化为 json) 作为值,userInfo:userid 作为 key,键名如:userInfo:123,value 存储对应用户信息的 json 串。如 key 为:"user:id:name:1", value 为 "{"name":"leijia","age":18}"。
实战场景 3:限定某个 ip 特定时间内的访问次数
方案:
用 key 记录 IP,value 记录访问次数,同时 key 的过期时间设置为 60 秒,如果 key 过期了则重新设置,否则进行判断,当一分钟内访问超过 100 次,则禁止访问。令牌桶的思路
实战场景 4: 分布式 session
我们知道 session 是以文件的形式保存在服务器中的;如果你的应用做了负载均衡,将网站的项目放在多个服务器上,当用户在服务器 A 上进行登陆,session 文件会写在 A 服务器;当用户跳转页面时,请求被分配到 B 服务器上的时候,就找不到这个 session 文件,用户就要重新登陆。
如果想要多个服务器共享一个 session,可以将 session 存放在 redis 中,redis 可以独立于所有负载均衡服务器,也可以放在其中一台负载均衡服务器上;但是所有应用所在的服务器连接的都是同一个 redis 服务器。
当我们用一台专门的服务器来保存集群中的 session 的时候,我们实际上就得到了一个无状态集群,集群可以无痛添加机器而不用担心状态问题。
要使用 Redis 保存用户的会话数据,可以使用 Redis 的 键值对 存储模型,通常会选择 哈希(hash) 或 字符串(string) 来存储会话信息。使用字符串的时候,会将用户信息,比如用户名,角色,登陆时间,上次操作时间之类的信息放到一个 JSON 字符串中进行保存,不过我们也可以用一个 Hash 类型的值来保存,这样可以我们就可以单独更新用户信息中的某一个字段
如何设置 session 过期呢?我们可以为 Web 应用做一个拦截器,只要有用户访问了服务,就延长用户的 session 的过期时间。
SpringBoot 也提供了 @EnableRedisHttpSession
注解来将 Redis 作为会话管理器。
话说回来,使用 JWT 也可以实现集群的去中心化的身份认证。不过我感觉没有 Redis 好用,原因是 JWT 无法做到快速踢用户下线。具体请看 JWT基本知识
(2)Hash 的使用场景
以购物车为例子,用户 id 设置为 key,那么购物车里所有的商品就是用户 key 对应的值了,每个商品有 id 和购买数量,对应 hash 的结构就是商品 id 为 field,商品数量为 value。如图所示:
如果将商品 id 和商品数量序列化成 json 字符串,那么也可以用上面讲的 string 类型存储。下面对比一下这两种数据结构:
对比项 | string(json) | hash |
---|---|---|
效率 | 很高 | 高 |
容量 | 低 | 低 |
灵活性 | 低 | 高 |
序列化 | 简单 | 复杂 |
总结一下: | ||
当对象的某个属性需要频繁修改时,不适合用 string+json,因为它不够灵活,每次修改都需要重新将整个对象序列化并赋值;如果使用 hash 类型,则可以针对某个属性单独修改,没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评价数等可能经常发生变化的属性,就适合存储在 hash 类型里。 | ||
(3)List 的使用场景 | ||
列表本质是一个有序的,元素可重复的队列。 | ||
实战场景:定时排行榜 | ||
list 类型的 lrange 命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在 list 类型中,如 QQ 音乐内地排行榜,每周计算一次存储在 list 类型中,访问接口时通过 page 和 size 分页转化成 lrange 命令获取排行榜数据。 | ||
![]() |
||
但是,并不是所有的排行榜都能用 list 类型实现,只有定时计算的排行榜才适合使用 list 类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list 类型不能支持实时计算的排行榜,下面介绍有序集合 sorted set 的应用场景时会详细介绍实时计算的排行榜的实现。 |
限流器
滑动窗口日志算法 (Sliding Window Log)。
将 IP 的记录请求时间戳记录到 redis 中,一个 IP 一个 key,然后如果每次来请求,都根据当前请求时间戳往前固定时间作为起点,将队列中窗口起始时间早于这个起点的都删掉,并统计窗口内的请求个数,如果超过指定个数,就报错,
(4)Set 的使用场景
集合的特点是无序性和确定性(不重复)。
实战场景:收藏夹
例如 QQ 音乐中如果你喜欢一首歌,点个『喜欢』就会将歌曲放到个人收藏夹中,每一个用户做一个收藏的集合,每个收藏的集合存放用户收藏过的歌曲 id。
key 为用户 id,value 为歌曲 id 的集合。
(5)Sorted Set 的使用场景
有序集合的特点是有序,无重复值。与 set 不同的是 sorted set 每个元素都会关联一个 score 属性,redis 正是通过 score 来为集合中的成员进行从小到大的排序。
实战场景:实时排行榜
QQ 音乐中有多种实时榜单,比如飙升榜、热歌榜、新歌榜,可以用 redis key 存储榜单类型,score 为点击量,value 为歌曲 id,用户每点击一首歌曲会更新 redis 数据,sorted set 会依据 score 即点击量将歌曲 id 排序。
5、Redis 如何做持久化的?能说一下 RDB 和 AOF 的实现原理吗?
什么是持久化?
持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML 数据文件中等等。
还可以从如下两个层面简单的理解持久化 :
- 应用层:如果关闭 (shutdown) 你的应用然后重新启动则先前的数据依然存在。
- 系统层:如果关闭 (shutdown) 你的系统(电脑)然后重新启动则先前的数据依然存在。
Redis 为什么要持久化?
Redis 是内存数据库,为了保证效率所有的操作都是在内存中完成。数据都是缓存在内存中,当你重启系统或者关闭系统,之前缓存在内存中的数据都会丢失再也不能找回。因此为了避免这种情况,Redis 需要实现持久化将内存中的数据存储起来。
Redis 如何实现持久化?
Redis 官方提供了不同级别的持久化方式: - RDB 持久化:能够在指定的时间间隔能对你的数据进行快照存储。
- AOF 持久化:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾。Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
- 不使用持久化:如果你只希望你的数据在服务器运行的时候存在,你也可以选择不使用任何持久化方式。
- 同时开启 RDB 和 AOF:你也可以同时开启两种持久化方式,在这种情况下当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
这么多持久化方式我们应该怎么选?在选择之前我们需要搞清楚每种持久化方式的区别以及各自的优劣势。
RDB 持久化
RDB(Redis Database) 持久化是把当前内存数据生成快照保存到硬盘的过程,触发 RDB 持久化过程分为手动触发和自动触发。
(1)手动触发
手动触发对应 save 命令,会阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
(2)自动触发
自动触发对应 bgsave 命令,Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。
在 redis.conf 配置文件中可以配置:
save <seconds> <changes>
表示 xx 秒内数据修改 xx 次时自动触发 bgsave。如果想关闭自动触发,可以在 save 命令后面加一个空串,即:
save ""
还有其他常见可以触发 bgsave,如:
- 如果从节点执行全量复制操作,主节点自动执行 bgsave 生成 RDB 文件并发送给从节点。
- 默认情况下执行 shutdown 命令时,如果没有开启 AOF 持久化功能则 自动执行 bgsave。
bgsave 工作机制
(1)执行 bgsave 命令,Redis 父进程判断当前是否存在正在执行的子进 程,如 RDB/AOF 子进程,如果存在,bgsave 命令直接返回。
(2)父进程执行 fork 操作创建子进程,fork 操作过程中父进程会阻塞,通 过 info stats 命令查看 latest_fork_usec 选项,可以获取最近一个 fork 操作的耗时,单位为微秒
(3)父进程 fork 完成后,bgsave 命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
(4)子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行 lastsave 命令可以获取最后一次生成 RDB 的 时间,对应 info 统计的 rdb_last_save_time 选项。
(5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence 下的 rdb_* 相关选项。
-- RDB 持久化完 --
AOF 持久化
AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
AOF 持久化工作机制
开启 AOF 功能需要配置:appendonly yes,默认不开启。
AOF 文件名 通过 appendfilename 配置设置,默认文件名是 appendonly.aof。保存路径同 RDB 持久化方式一致,通过 dir 配置指定。
AOF 的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)。
(1)所有的写入命令会追加到 aof_buf(缓冲区)中。
(2)AOF 缓冲区根据对应的策略向硬盘做同步操作。
AOF 为什么把命令追加到 aof_buf 中?Redis 使用单线程响应命令,如果每次写 AOF 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区 aof_buf 中,还有另一个好处,Redis 可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。
(3)随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
(4)当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。
AOF 重写(rewrite)机制
重写的目的:
- 减小 AOF 文件占用空间;
- 更小的 AOF 文件可以更快地被 Redis 加载恢复。
AOF 重写可以分为手动触发和自动触发: - 手动触发:直接调用 bgrewriteaof 命令。
- 自动触发:根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定自动触发时机。
auto-aof-rewrite-min-size:表示运行 AOF 重写时文件最小体积,默认 为 64MB。
auto-aof-rewrite-percentage:代表当前 AOF 文件空间 (aof_current_size)和上一次重写后 AOF 文件空间(aof_base_size)的比值。
自动触发时机
当 aof_current_size>auto-aof-rewrite-minsize 并且(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentage。
其中 aof_current_size 和 aof_base_size 可以在 info Persistence 统计信息中查看。
AOF 文件重写后为什么会变小?
(1)旧的 AOF 文件含有无效的命令,如:del key1, hdel key2 等。重写只保留最终数据的写入命令。
(2)多条命令可以合并,如 lpush list a,lpush list b,lpush list c 可以直接转化为 lpush list a b c。
AOF 文件数据恢复
数据恢复流程说明:
(1)AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件。
(2)AOF 关闭或者 AOF 文件不存在时,加载 RDB 文件。
(3)加载 AOF/RDB 文件成功后,Redis 启动成功。
(4)AOF/RDB 文件存在错误时,Redis 启动失败并打印错误信息。
-- AOF 持久化完 --
RDB 和 AOF 的优缺点
RDB 优点
- RDB 是一个非常紧凑的文件,它保存了某个时间点的数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去 24 小时内的数据,同时每天保存过去 30 天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。
- RDB 是一个紧凑的单一文件,很方便传送到另一个远端数据中心,非常适用于灾难恢复。
- RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 Redis 的性能。
- 与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些。
AOF 优点 - 你可以使用不同的 fsync 策略:无 fsync、每秒 fsync 、每次写的时候 fsync .使用默认的每秒 fsync 策略, Redis 的性能依然很好 ( fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失 1 秒的数据。
- AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因 (磁盘空间已满,写的过程中宕机等等) 未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。导出(export) AOF 文件也非常简单:举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
RDB 缺点 - Redis 要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔 5 分钟或者更久做一次完整的保存,万一在 Redis 意外宕机,你可能会丢失几分钟的数据。
- RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候, fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。
AOF 缺点 - 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 数据恢复(load)时 AOF 比 RDB 慢,通常 RDB 可以提供更有保证的最大延迟时间。
RDB 和 AOF 简单对比总结
RDB 优点: - RDB 是紧凑的二进制文件,比较适合备份,全量复制等场景
- RDB 恢复数据远快于 AOF
RDB 缺点: - RDB 无法实现实时或者秒级持久化;
- 新老版本无法兼容 RDB 格式。
AOF 优点: - 可以更好地保护数据不丢失;
- appen-only 模式写入性能比较高;
- 适合做灾难性的误删除紧急恢复。
AOF 缺点: - 对于同一份文件,AOF 文件要比 RDB 快照大;
- AOF 开启后,会对写的 QPS 有所影响,相对于 RDB 来说 写 QPS 要下降;
- 数据库恢复比较慢, 不合适做冷备。
6、讲解一下 Redis 的线程模型?
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
如果面试官继续追问为啥 redis 单线程模型也能效率这么高?
- 纯内存操作
- 核心是基于非阻塞的 IO 多路复用机制
- 单线程反而避免了多线程的频繁上下文切换问题
7、缓存雪崩、缓存穿透、缓存预热、缓存击穿、缓存降级的区别是什么?
在实际生产环境中有时会遇到缓存穿透、缓存击穿、缓存雪崩等异常场景,为了避免异常带来巨大损失,我们需要了解每种异常发生的原因以及解决方案,帮助提升系统可靠性和高可用。
(1)缓存穿透
什么是缓存穿透?
缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。
如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至击垮数据库系统。
缓存穿透常用的解决方案
(1)布隆过滤器(推荐)
布隆过滤器(Bloom Filter,简称 BF)由 Burton Howard Bloom 在 1970 年提出,是一种空间效率高的概率型数据结构。
布隆过滤器专门用来检测集合中是否存在特定的元素。
如果在平时我们要判断一个元素是否在一个集合中,通常会采用查找比较的方法,下面分析不同的数据结构查找效率:
- 采用线性表存储,查找时间复杂度为
- 采用平衡二叉排序树(AVL、红黑树)存储,查找时间复杂度为
- 采用哈希表存储,考虑到哈希碰撞,整体时间复杂度也要
当需要判断一个元素是否存在于海量数据集合中,不仅查找时间慢,还会占用大量存储空间。接下来看一下布隆过滤器如何解决这个问题。
布隆过滤器设计思想
布隆过滤器由一个长度为 m 比特的位数组(bit array)与 k 个哈希函数(hash function)组成的数据结构。位数组初始化均为 0,所有的哈希函数都可以分别把输入数据尽量均匀地散列。
当要向布隆过滤器中插入一个元素时,该元素经过 k 个哈希函数计算产生 k 个哈希值,以哈希值作为位数组中的下标,将所有 k 个对应的比特值由 0 置为 1。
当要查询一个元素时,同样将其经过哈希函数计算产生哈希值,然后检查对应的 k 个比特值:如果有任意一个比特为 0,表明该元素一定不在集合中;如果所有比特均为 1,表明该集合有可能性在集合中。为什么不是一定在集合中呢?因为不同的元素计算的哈希值有可能一样,会出现哈希碰撞,导致一个不存在的元素有可能对应的比特位为 1,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在 BF 中是绝不会出现的。
总结一下:布隆过滤器认为不在的,一定不会在集合中;布隆过滤器认为在的,可能在也可能不在集合中。
举个例子:下图是一个布隆过滤器,共有 18 个比特位,3 个哈希函数。集合中三个元素 x,y,z 通过三个哈希函数散列到不同的比特位,并将比特位置为 1。当查询元素 w 时,通过三个哈希函数计算,发现有一个比特位的值为 0,可以肯定认为该元素不在集合中。
布隆过滤器优缺点
优点: - 节省空间:不需要存储数据本身,只需要存储数据对应 hash 比特位
- 时间复杂度低:插入和查找的时间复杂度都为 O(k),k 为哈希函数的个数
缺点: - 存在假阳性:布隆过滤器判断存在,可能出现元素不在集合中;判断准确率取决于哈希函数的个数
- 不能删除元素:如果一个元素被删除,但是却不能从布隆过滤器中删除,这也是造成假阳性的原因了
布隆过滤器适用场景 - 爬虫系统 url 去重
- 垃圾邮件过滤
- 黑名单
(2)返回空对象
当缓存未命中,查询持久层也为空,可以将返回的空对象写到缓存中,这样下次请求该 key 时直接从缓存中查询返回空对象,请求不会落到持久层数据库。为了避免存储过多空对象,通常会给空对象设置一个过期时间。
这种方法会存在两个问题: - 如果有大量的 key 穿透,缓存空对象会占用宝贵的内存空间。
- 空对象的 key 设置了过期时间,在这段时间可能会存在缓存和持久层数据不一致的场景。
(2)缓存击穿
什么是缓存击穿?
缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
缓存击穿危害
数据库瞬时压力骤增,造成大量请求阻塞。
如何解决?
方案一:使用互斥锁(mutex key)
这种思路比较简单,就是让一个线程回写缓存,其他线程等待回写缓存线程执行完,重新读缓存即可。
同一时间只有一个线程读数据库然后回写缓存,其他线程都处于阻塞状态。如果是高并发场景,大量线程阻塞势必会降低吞吐量。这种情况如何解决?大家可以在留言区讨论。
如果是分布式应用就需要使用分布式锁。
方案二:热点数据永不过期
永不过期实际包含两层意思:
- 物理不过期,针对热点 key 不设置过期时间
- 逻辑过期,把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
从实战看这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程 (非构建缓存的线程) 可能访问的是老数据,对于不追求严格强一致性的系统是可以接受的。
(3)缓存雪崩
什么是缓存雪崩?
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
缓存雪崩解决方案
常用的解决方案有:
- 均匀过期
- 加互斥锁
- 缓存永不过期
- 双层缓存策略
(1)均匀过期
设置不同的过期时间,让缓存失效的时间点尽量均匀。通常可以为有效期增加随机值或者统一规划有效期。
(2)加互斥锁
跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队。
(3)缓存永不过期
跟缓存击穿解决思路一致,缓存在物理上永远不过期,用一个异步的线程更新缓存。
(4)双层缓存策略
使用主备两层缓存:
主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值。
备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存。
(4)缓存预热
什么是缓存预热?
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。
如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热的操作方法
- 数据量不大的时候,工程启动的时候进行加载缓存动作;
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
- 数据量太大的时候,优先保证热点数据进行提前加载到缓存。
(5)缓存降级
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
在项目实战中通常会将部分热点数据缓存到服务的内存中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。
降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
8、Redis 的内存淘汰机制
Redis 内存淘汰策略是指当缓存内存不足时,通过淘汰旧数据处理新加入数据选择的策略。
如何配置最大内存?
(1)通过配置文件配置
修改 redis.conf 配置文件
maxmemory 1024mb //设置Redis最大占用内存大小为1024M
注意:maxmemory 默认配置为 0,在 64 位操作系统下 redis 最大内存为操作系统剩余内存,在 32 位操作系统下 redis 最大内存为 3GB。
(2)通过动态命令配置
Redis 支持运行时通过命令动态修改内存大小:
127.0.0.1:6379> config set maxmemory 200mb //设置Redis最大占用内存大小为200M
127.0.0.1:6379> config get maxmemory //获取设置的Redis能使用的最大内存大小
1) "maxmemory"
2) "209715200"
淘汰策略的分类
Redis 最大占用内存用完之后,如果继续添加数据,如何处理这种情况呢?实际上 Redis 官方已经定义了八种策略来处理这种情况:
noeviction
默认策略,对于写请求直接返回错误,不进行淘汰。
allkeys-lru
lru(less recently used), 最近最少使用。从所有的 key 中使用近似 LRU 算法进行淘汰。
volatile-lru
lru(less recently used), 最近最少使用。从设置了过期时间的 key 中使用近似 LRU 算法进行淘汰。
allkeys-random
从所有的 key 中随机淘汰。
volatile-random
从设置了过期时间的 key 中随机淘汰。
volatile-ttl
ttl(time to live),在设置了过期时间的 key 中根据 key 的过期时间进行淘汰,越早过期的越优先被淘汰。
allkeys-lfu
lfu(Least Frequently Used),最少使用频率。从所有的 key 中使用近似 LFU 算法进行淘汰。从 Redis4.0 开始支持。
volatile-lfu
lfu(Least Frequently Used),最少使用频率。从设置了过期时间的 key 中使用近似 LFU 算法进行淘汰。从 Redis4.0 开始支持。
注意:当使用 volatile-lru、volatile-random、volatile-ttl 这三种策略时,如果没有设置过期的 key 可以被淘汰,则和 noeviction 一样返回错误。
LRU 算法
LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间用来存储新的数据。这个时候就可以使用 LRU 算法了。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。
LRU 在 Redis 中的实现
Redis 使用的是近似 LRU 算法,它跟常规的 LRU 算法还不太一样。近似 LRU 算法通过随机采样法淘汰数据,每次随机出 5 个(默认)key,从里面淘汰掉最近最少使用的 key。
可以通过 maxmemory-samples 参数修改采样数量, 如:maxmemory-samples 10
maxmenory-samples 配置的越大,淘汰的结果越接近于严格的 LRU 算法,但因此耗费的 CPU 也很高。
Redis 为了实现近似 LRU 算法,给每个 key 增加了一个额外增加了一个 24bit 的字段,用来存储该 key 最后一次被访问的时间。
Redis3.0 对近似 LRU 的优化
Redis3.0 对近似 LRU 算法进行了一些优化。新算法会维护一个候选池(大小为 16),池中的数据根据访问时间进行排序,第一次随机选取的 key 都会放入池中,随后每次随机选取的 key 只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。当放满后,如果有新的 key 需要放入,则将池中最后访问时间最大(最近被访问)的移除。
当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的 key 淘汰掉就行。
LFU 算法
LFU(Least Frequently Used),是 Redis4.0 新加的一种淘汰策略,它的核心思想是根据 key 的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。
LFU 算法能更好的表示一个 key 被访问的热度。假如你使用的是 LRU 算法,一个 key 很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些 key 将来是很有可能被访问到的则被淘汰了。如果使用 LFU 算法则不会出现这种情况,因为使用一次并不会使一个 key 成为热点数据。
9、Redis 有事务机制吗?
有事务机制。Redis 事务生命周期:
- 开启事务:使用 MULTI 开启一个事务
- 命令入队列:每次操作的命令都会加入到一个队列中,但命令此时不会真正被执行
- 提交事务:使用 EXEC 命令提交事务,开始顺序执行队列中的命令
10、Redis 事务到底是不是原子性的?
先看关系型数据库 ACID 中关于原子性的定义:
原子性: 一个事务 (transaction) 中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复 (Rollback) 到事务开始前的状态,就像这个事务从来没有执行过一样。
官方文档对事务的定义:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。EXEC 命令负责触发并执行事务中的所有命令:如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。
官方认为 Redis 事务是一个原子操作,这是站在执行与否的角度考虑的。但是从 ACID 原子性定义来看,严格意义上讲 Redis 事务是非原子型的,因为在命令顺序执行过程中,一旦发生命令执行错误 Redis 是不会停止执行然后回滚数据。
11、Redis 为什么不支持回滚(roll back)?
在事务运行期间虽然 Redis 命令可能会执行失败,但是 Redis 依然会执行事务内剩余的命令而不会执行回滚操作。如果你熟悉 mysql 关系型数据库事务,你会对此非常疑惑,Redis 官方的理由如下:
只有当被调用的 Redis 命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis 能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致 Redis 命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。支持事务回滚能力会导致设计复杂,这与 Redis 的初衷相违背,Redis 的设计目标是功能简化及确保更快的运行速度。
对于官方的这种理由有一个普遍的反对观点:程序有 bug 怎么办?但其实回归不能解决程序的 bug,比如某位粗心的程序员计划更新键 A,实际上最后更新了键 B,回滚机制是没法解决这种人为错误的。正因为这种人为的错误不太可能进入生产系统,所以官方在设计 Redis 时选用更加简单和快速的方法,没有实现回滚的机制。
12、Redis 事务相关的命令有哪几个?
(1)WATCH
可以为 Redis 事务提供 check-and-set (CAS)行为。被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回 nil-reply 来表示事务已经失败。
(2)MULTI
用于开启一个事务,它总是返回 OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时, 所有队列中的命令才会被执行。
(3)UNWATCH
取消 WATCH 命令对所有 key 的监视,一般用于 DISCARD 和 EXEC 命令之前。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。
(4)DISCARD
当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空,并且客户端会从事务状态中退出。
(5)EXEC
负责触发并执行事务中的所有命令:
如果客户端成功开启事务后执行 EXEC,那么事务中的所有命令都会被执行。
如果客户端在使用 MULTI 开启了事务后,却因为断线而没有成功执行 EXEC,那么事务中的所有命令都不会被执行。需要特别注意的是:即使事务中有某条/某些命令执行失败了,事务队列中的其他命令仍然会继续执行,Redis 不会停止执行事务中的命令,而不会像我们通常使用的关系型数据库一样进行回滚。
13、什么是 Redis 主从复制?
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点 (master),后者称为从节点 (slave);数据的复制是单向的,只能由主节点到从节点。
主从复制的作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- 高可用基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
主从复制实现原理
主从复制过程主要可以分为 3 个阶段:连接建立阶段、数据同步阶段、命令传播阶段。
连接建立阶段
该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。
步骤 1:保存主节点信息
slaveof 命令是异步的,在从节点上执行 slaveof 命令,从节点立即向客户端返回 ok,从节点服务器内部维护了两个字段,即 masterhost 和 masterport 字段,用于存储主节点的 ip 和 port 信息。
步骤 2:建立 socket 连接
从节点每秒 1 次调用复制定时函数 replicationCron(),如果发现了有主节点可以连接,便会根据主节点的 ip 和 port,创建 socket 连接。
从节点为该 socket 建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收 RDB 文件、接收命令传播等。
主节点接收到从节点的 socket 连接后(即 accept 之后),为该 socket 创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。
步骤 3:发送 ping 命令
从节点成为主节点的客户端之后,发送 ping 命令进行首次请求,目的是:检查 socket 连接是否可用,以及主节点当前是否能够处理请求。
从节点发送 ping 命令后,可能出现 3 种情况:
(1)返回 pong:说明 socket 连接正常,且主节点当前可以处理请求,复制过程继续。
(2)超时:一定时间后从节点仍未收到主节点的回复,说明 socket 连接不可用,则从节点断开 socket 连接,并重连。
(3)返回 pong 以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开 socket 连接,并重连。
步骤 4:身份验证
如果从节点中设置了 masterauth 选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送 auth 命令进行的,auth 命令的参数即为配置文件中的 masterauth 的值。
如果主节点设置密码的状态,与从节点 masterauth 的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开 socket 连接,并重连。
步骤 5:发送从节点端口信息
身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为 6380),主节点将该信息保存到该从节点对应的客户端的 slave_listening_port 字段中;该端口信息除了在主节点中执行 info Replication 时显示以外,没有其他作用。
数据同步阶段
主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送 psync 命令(Redis2.8 以前是 sync 命令),开始同步。
数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制,后面再讲解这两种复制方式以及 psync 命令的执行过程,这里不再详述。
命令传播阶段
数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
需要注意的是,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的 repl-disable-tcp-nodelay 配置等有关。
14、Sentinel(哨兵模式)的原理你能讲一下吗?
Redis 的主从复制模式下,一旦主节点由于故障不能提供服务,需要手动将从节点晋升为主节点,同时还要通知客户端更新主节点地址,这种故障处理方式从一定程度上是无法接受的。
Redis 2.8 以后提供了 Redis Sentinel 哨兵机制来解决这个问题。
Redis Sentinel 是 Redis 高可用的实现方案。Sentinel 是一个管理多个 Redis 实例的工具,它可以实现对 Redis 的监控、通知、自动故障转移。
Redis Sentinel 架构图如下:
哨兵模式的原理
哨兵模式的主要作用在于它能够自动完成故障发现和故障转移,并通知客户端,从而实现高可用。哨兵模式通常由一组 Sentinel 节点和一组(或多组)主从复制节点组成。
心跳机制
(1)Sentinel 与 Redis Node
Redis Sentinel 是一个特殊的 Redis 节点。在哨兵模式创建时,需要通过配置指定 Sentinel 与 Redis Master Node 之间的关系,然后 Sentinel 会从主节点上获取所有从节点的信息,之后 Sentinel 会定时向主节点和从节点发送 info 命令获取其拓扑结构和状态信息。
(2)Sentinel 与 Sentinel
基于 Redis 的订阅发布功能, 每个 Sentinel 节点会向主节点的 sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息 ,同时每个 Sentinel 节点也会订阅该频道, 来获取其他 Sentinel 节点的信息以及它们对主节点的判断。
通过以上两步所有的 Sentinel 节点以及它们与所有的 Redis 节点之间都已经彼此感知到,之后每个 Sentinel 节点会向主节点、从节点、以及其余 Sentinel 节点定时发送 ping 命令作为心跳检测, 来确认这些节点是否可达。
故障转移
每个 Sentinel 都会定时进行心跳检查,当发现主节点出现心跳检测超时的情况时,此时认为该主节点已经不可用,这种判定称为主观下线。
之后该 Sentinel 节点会通过 sentinel ismaster-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断, 当 quorum(法定人数) 个 Sentinel 节点都认为该节点故障时,则执行客观下线,即认为该节点已经不可用。这也同时解释了为什么必须需要一组 Sentinel 节点,因为单个 Sentinel 节点很容易对故障状态做出误判。
这里 quorum 的值是我们在哨兵模式搭建时指定的,后文会有说明,通常为 Sentinel 节点总数/2+1,即半数以上节点做出主观下线判断就可以执行客观下线。
因为故障转移的工作只需要一个 Sentinel 节点来完成,所以 Sentinel 节点之间会再做一次选举工作, 基于 Raft 算法选出一个 Sentinel 领导者来进行故障转移的工作。
被选举出的 Sentinel 领导者进行故障转移的具体步骤如下:
(1)在从节点列表中选出一个节点作为新的主节点
- 过滤不健康或者不满足要求的节点;
- 选择 slave-priority(优先级)最高的从节点, 如果存在则返回, 不存在则继续;
- 选择复制偏移量最大的从节点 , 如果存在则返回, 不存在则继续;
- 选择 runid 最小的从节点。
(2)Sentinel 领导者节点会对选出来的从节点执行 slaveof no one 命令让其成为主节点。
(3)Sentinel 领导者节点会向剩余的从节点发送命令,让他们从新的主节点上复制数据。
(4)Sentinel 领导者会将原来的主节点更新为从节点, 并对其进行监控, 当其恢复后命令它去复制新的主节点。
15、Cluster(集群)的原理你能讲一下吗?
引入 Cluster 模式的原因:
不管是主从模式还是哨兵模式都只能由一个 master 在写数据,在海量数据高并发场景,一个节点写数据容易出现瓶颈,引入 Cluster 模式可以实现多个节点同时写数据。
Redis-Cluster 采用无中心结构,每个节点都保存数据,节点之间互相连接从而知道整个集群状态。
如图所示 Cluster 模式其实就是多个主从复制的结构组合起来的,每一个主从复制结构可以看成一个节点,那么上面的 Cluster 集群中就有三个节点。
16、Memcache 与 Redis 的区别都有哪些?
存储方式
Memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
Redis 有部份存在硬盘上,这样能保证数据的持久性。
数据支持类型
Memcache 对数据类型支持相对简单。
Redis 有丰富的数据类型。
使用底层模型不同
它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。
Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
17、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用 keys 指令可以扫出指定模式的 key 列表:keys pre*,这个时候面试官会追问该命令对线上业务有什么影响,直接看下一个问题。
18、如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?
redis 的单线程的。keys 指令会导致线 程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时 候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间 会比直接用 keys 指令长。
19、如果有大量的 key 需要设置同一时间过期,一般需要注意什么?
如果大量的 key 过期时间设置的过于集中,到过期的那个时间点,Redis 可能会出现短暂的卡顿现象 (因为 redis 是单线程的)。严重的话可能会导致服务器雪崩,所以我们一般在过期时间上加一个随机值,让过期时间尽量分散。
20、Redis 常用的客户端有哪些?
Jedis:是老牌的 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持。
Redisson:实现了分布式和可扩展的 Java 数据结构。
Lettuce:高级 Redis 客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
优点:
Jedis:比较全面的提供了 Redis 的操作特性。
Redisson:促使使用者对 Redis 的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过 Redis 支持延迟队列。
Lettuce:基于 Netty 框架的事件驱动的通信层,其方法调用是异步的。Lettuce 的 API 是线程安全的,所以可以操 作单个 Lettuce 连接来完成各种操作。
END