Redis篇
Redis篇口语回答
本篇记录了关于 Redis 的口语话回答
v1.1 2024/9/9 优化内容
2024/9/29 看完了小林coding版本的Redis面试题
v1.2 期待结合小林的Redis面试题做更新
1. Redis支持哪几种数据类型
Redis 的基础数据类型主要有五种:string、hash、list、set 和 zset。string 是最常用的类型,适合用于缓存内容、分布式锁等场景。hash 则常用于存储对象结构的数据,例如个人信息(姓名、年龄、头像等),相比于 string 需要序列化为 JSON,hash 可以直接使用更加方便。list 常用于存储有序的数据集合,适合消息队列等场景。set 是无序且唯一的集合,通常用于去重。zset(有序集合)常用来实现排行榜功能,利用其分数进行排序。这几种数据类型根据场景需求各有特点。
常用 API:
String:
SET key value
:设置指定 key 的值。GET key
:获取指定 key 的值。INCR key
:对 key 的值进行自增。APPEND key value
:在 key 的值后追加内容。
Hash:
HSET key field value
:为 hash 结构中的字段赋值。HGET key field
:获取 hash 中指定字段的值。HGETALL key
:获取 hash 的所有字段及其值。HDEL key field
:删除 hash 中指定字段。
List:
LPUSH key value
:从左侧向列表中添加元素。RPUSH key value
:从右侧向列表中添加元素。LPOP key
:从左侧弹出元素。LRANGE key start stop
:获取列表中指定范围的元素。
Set:
SADD key member
:向集合添加元素。SREM key member
:移除集合中的某个元素。SMEMBERS key
:返回集合中的所有元素。SISMEMBER key member
:检查某个元素是否存在于集合中。
ZSet:
ZADD key score member
:将元素添加到 zset 中,并指定分数。ZRANGE key start stop
:按分数排序后,获取指定排名范围的元素。ZSCORE key member
:获取指定元素的分数。ZREM key member
:移除 zset 中的某个元素。
2. Redis有哪些优点
Redis 是目前非常流行的缓存解决方案,最大的优势在于其高性能,速度非常快,所有操作基本都是毫秒级,具有极高的吞吐量,能够支持每秒数百万次的请求。Redis通过将数据存储在内存中,大大提高了读写速度。此外,它提供了多种丰富的数据类型,像 string、hash、zset 等,都是在日常开发中经常使用的,此外还有高级的数据类型,比如 geo 和 bitmap,这些可以应对特定的场景需求。Redis 的持久化机制也非常完善,提供了不同的策略来应对宕机等异常情况。再加上分布式集群的支持,Redis 的整体可用性非常高。
3. Redis常用类型的应用场景
Redis 的常用数据类型在多个场景下有广泛应用,最常见的场景就是缓存,尤其是 string 和 hash 类型用得较多。string 类型支持多种缓存方式,结合过期时间,使用起来非常方便,且其 value 可以存储 JSON 等格式的数据,具备很大的灵活性。另一个重要的应用场景是利用 Redis 实现分布式锁,SETNX
命令天然支持锁的机制,非常适合用于此类操作。对于一些对象数据的处理,hash 结构更加高效,避免了JSON 序列化带来的性能开销。此外,zset(sorted set)经常用于排行榜的实现,还可以用于延迟队列操作。set 类型常用来做黑名单管理,而list 可以用于实现简单的消息队列。
4. Redis是单线程的吗
Redis 通常被称为单线程,主要指的是从接收到客户端请求、进行内部操作再到返回结果的过程是由单线程执行的。即便如此,Redis 每秒可以处理约 10 万次请求(官方数据),因为操作都是在内存中进行,性能瓶颈更多出现在内存和带宽,而非CPU。因此,多线程反而可能带来线程安全、数据竞争以及上下文切换的开销。
但在执行命令之外,像持久化和内存释放等操作是由多线程处理的。自 Redis 6.0 起,为减轻网络瓶颈,还增加了 IO 多线程来处理请求,这大大优化了性能。因此,Redis 既在命令执行中是单线程,也通过多线程提升了其他操作的效率。
5. Redis过期策略有哪些
Redis 的过期策略包括主动和被动两种,其中主动策略又分为定期和定时,惰性清理是被动策略的一种。Redis 主要使用定期和惰性清理的结合方式。
定期清理:通过周期性执行的函数扫描即将过期的键,并立即删除。虽然这种方式能及时清理过期键,但会消耗较多的 CPU 资源。为了优化性能,Redis 采用了定期操作,每隔一段时间执行清理任务,减少 CPU 的消耗,同时较准时地删除过期键,从而实现了定时策略的优化。主要挑战是如何平衡执行频率和性能。
惰性清理:即使键已经过期,只有在下次访问时才会被删除。这种方式可以显著减少 CPU 的负担,但可能导致内存中存在过期的键,从而引发内存泄漏。遇到内存不足的情况,Redis 会触发内存淘汰策略来处理这些过期键。
6. 什么是缓存穿透
缓存穿透的核心问题在于,当高并发请求到来时,如果缓存中不存在某个 key,系统会查询数据库,如果数据库中也没有该 key,最终返回结果时这个key 不会被缓存。这意味着下一次相同的请求依然会查询数据库,从而造成数据库不断被请求,这就是缓存穿透。
解决缓存穿透的主要方法有两种:
缓存空值:当数据库查询不到数据时,将一个空值写入缓存。这样,后续相同的请求会直接命中缓存,从而避免重复查询数据库,减轻数据库的压力。
布隆过滤器:适用于预先过滤固定值的场景。布隆过滤器可以在请求到达数据库之前进行初步的检查,从而减少无效查询的概率,降低数据库压力。
7. 什么是缓存击穿
缓存击穿指的是在高并发情况下,某个热点 key 突然失效或未被缓存,导致大量请求直接穿透到后端数据库,从而造成数据库负载过高,甚至崩溃。这与缓存穿透不同,缓存击穿主要发生在单个key 上,而缓存穿透是针对不存在的 key。
解决缓存击穿的常见方法有两种:
互斥锁:在多个请求中,只有一个请求会去重建缓存,其他请求则等待。这样可以避免大量请求同时访问数据库。然而,需要注意的是,互斥锁可能带来死锁和请求阻塞的问题,需要仔细设计和处理。
逻辑过期时间:设置一个逻辑上的过期时间进行异步缓存更新,实际缓存永远不会过期。这样可以避免缓存击穿问题,但这种方法的复杂性和逻辑时间的设置要求较高,需要仔细设计。
一般情况下,互斥锁方案已足够应对大多数缓存击穿的问题。
8. 什么是缓存雪崩
缓存雪崩是指当大量缓存同时失效时,系统会在短时间内发起大量请求到数据库,从而对数据库造成极大压力,可能导致数据库崩溃或不可用。为了避免缓存雪崩,可以采取以下措施:首先,合理设置缓存过期时间,避免缓存失效集中在同一时间点,以分散失效压力;其次,在系统启动时进行缓存预热,防止大量请求在启动初期涌入数据库;最后,提升缓存架构的高可用性,减少因缓存服务故障导致的数据库压力。这些措施可以有效缓解缓存雪崩带来的负面影响。
9. Redis的 setnx 和 setex 的区别
setnx
和 setex
是对基础的 set
命令的扩展。setnx
的特点是只有在键不存在时才会设置值,这一特性常用于实现分布式锁。当 setnx
成功时,表示成功获取了锁;如果失败,则表示锁获取失败。setex
则在 set
命令的基础上增加了过期时间的概念,它允许设置一个键在指定时间后自动失效。例如,如果希望缓存值在 3 秒后自动过期,可以使用 setex
命令来实现。
10. Redis为什么这么快
Redis 的高性能主要得益于以下几点:首先,Redis 采用纯内存操作,相比磁盘存储,内存操作速度极快。其次,Redis 设计了合理的数据结构和数据编码,不同的数据类型使用不同的底层结构,确保了常数时间复杂度的高效数据访问。第三,Redis 是单线程的,由于操作都在内存中,性能瓶颈通常在网络和 CPU 上,单线程模式避免了上下文切换的开销。最后,Redis 使用了 I/O 多路复用技术,这减少了网络负载,提高了系统的吞吐量。以上这些因素共同促成了 Redis 的高效能。
11. Redis有哪些持久化方式
Redis 主要有两种持久化方式:RDB 和 AOF,同时也可以混合使用这两种方式。
RDB(Redis 数据库备份)是在指定时间间隔内生成数据集的快照,并将其保存为 RDB 格式的二进制文件。这种方式便于备份,恢复速度快,非常适合用于灾难恢复。
AOF(追加文件持久化)则记录每一个 写操作 到日志文件中。Redis 会将写操作以追加的方式写入 AOF 文件,每次恢复时,会通过重放这些操作来恢复数据。虽然AOF 文件通常较大,恢复速度也稍慢,但相较于 RDB,数据丢失的风险较小。
在实际使用中,通常会选择混合使用 RDB 和 AOF,以便兼顾备份的速度和数据的安全性。
12. 什么是Redis事务机制
Redis 的事务机制与 MySQL 等传统数据库的事务机制有所不同。Redis 事务主要是为了保证命令的原子性,但不提供回滚机制。在 Redis中,事务通过 将一系列命令放入一个队列 中来实现,这些命令会 按照顺序 一起执行。Redis 的事务机制主要由 MULTI
、EXEC
和 WATCH
命令配合使用。
具体使用方法是:首先使用 MULTI
命令标记事务的开始,然后将需要执行的命令放入事务队列中,最后通过 EXEC
命令执行这些命令。在此过程中,Redis 会保证这些命令的原子性,但如果出现异常,不会进行回滚操作。
13. Redis事务保证原子性吗,支持回滚吗
Redis 的事务机制与传统关系型数据库的事务机制有所不同。Redis 使用以下两个命令来实现事务:
MULTI
: 开启一个事务。之后的所有命令会被放入一个队列中,直到EXEC
命令执行。EXEC
: 执行之前在MULTI
命令和EXEC
命令之间放入队列中的所有命令。这些命令会按顺序一次性执行,Redis会将所有命令作为一个整体处理。
需要注意的是,Redis 事务保证的是单个命令的原子性,即每个命令在执行时是不可分割的。然而,Redis 事务并不具备传统关系型数据库的所有事务特性。如果在 EXEC
执行过程中某个命令失败(例如命令语法错误),该命令会被跳过,但其他命令仍会继续执行。这与关系型数据库不同,后者通常会在某个命令失败时回滚整个事务。
因此,Redis 事务没有回滚机制。如果事务中的某个命令执行失败,已经执行的命令不会被撤销。
14. 如果有大量的key需要设置同一时间过期,一般需要注意什么
缓存雪崩 发生在大量缓存键在同一时间过期,导致大量请求涌向数据库或后端服务,可能导致系统崩溃或性能严重下降。为了应对这种情况,可以采取以下解决方案:
过期时间随机化:
在设置缓存键的过期时间时,添加一个随机偏移量,使得不同键的过期时间略有不同,从而避免大量键在同一时刻失效。例如:
javaRandom random = new Random(); int baseExpiry = 3600; // 基础过期时间,单位为秒 int randomOffset = random.nextInt(300); // 随机偏移量,最大300秒 int finalExpiry = baseExpiry + randomOffset; redisClient.set(key, value, finalExpiry);
分散过期时间:
根据业务逻辑,将键的过期时间分散到不同时间段。例如,可以根据某些属性(如用户ID、商品ID等)设置不同的过期时间。
缓存预热:
在缓存失效前提前预热缓存,确保在高访问期时缓存中始终有数据。这可以通过定期更新缓存或在系统启动时加载数据到缓存来实现。
监控报警机制:
使用 Redis 自带的监控工具或第三方监控工具(如 Prometheus、Grafana 等)来监控缓存的命中率、延迟、内存使用等指标。设置报警规则,当发现缓存命中率下降或延迟增加时,及时发送报警通知,以便快速定位和解决问题。
15. Redis常见性能问题和解决方案
Redis 常见的性能问题包括内存空间不足、大键问题和阻塞操作等。针对这些问题,可以采取以下措施:
内存空间不足:
这种问题通常发生在数据量非常大的情况下。解决方法包括优化数据结构以减少内存占用,以及通过集群模式进行水平扩展,将数据分布到多个 Redis 实例中以增加总的可用内存。
大键问题:
大键问题通常出现在设计初期未能充分考虑,导致随着业务增长,某个小的键逐渐变成大键。大键会导致性能下降和延迟增加。解决这种问题需要从业务角度重新审视数据设计,进行合理的数据拆分和重构,以避免单个键的大小对性能造成影响。
阻塞操作:
一些操作,如
keys
命令,可能会导致 Redis 阻塞,影响系统性能。为了避免阻塞操作,生产环境中应尽量避免使用这些命令,特别是在大数据量的情况下。
16. jedis 与 redisson 对比有什么优缺点
Jedis 是一个轻量级的 Redis 客户端,易于集成和使用,适合简单的业务需求。它的主要优点是轻便和易用,但在一些方面如线程安全和分布式支持上,可能需要额外的配置和处理。
Redisson 是 Jedis 的升级版框架,提供了更丰富的功能和更好的分布式支持。主要区别包括:
- 线程安全: Redisson 在设计上天然支持线程安全,而 Jedis 需要额外的操作来保证线程安全。
- 分布式支持: Redisson 内部封装了许多分布式处理功能,原生支持分布式锁、限流等功能,无需从头编写。这使得 Redisson 更适合复杂的分布式场景。
- 集群配置: 使用 Jedis 配置 Redis 集群相对麻烦,需要进行较多的配置,而 Redisson 天然支持 Redis 集群,配置和使用更为简便。
因此,对于简单的业务需求,Jedis 足够使用;而对于复杂的大型项目,建议使用 Redisson 以充分利用其高级特性和简化配置。
17. Jedis和Lettuce一些关键的区别
Jedis 和 Lettuce 是两个常用的 Redis 客户端,它们各自有不同的特点和适用场景:
架构:
- Jedis: 采用直连模式,每个线程都需要创建自己的 Jedis 实例,这对于多线程环境是不安全的。为了避免资源浪费,可以使用 Jedis 连接池。
- Lettuce: 基于 Netty 框架,支持事件驱动的异步、同步和响应式编程。Lettuce 的 API 是线程安全的,多个线程可以安全地操作单个 Lettuce 连接。
功能:
- Jedis: 提供了全面的功能,支持多种数据类型(如 String、Hash、List、Set、Sorted Set)、事务、Lua 脚本、Pub/Sub、配置命令、批量操作等。
- Lettuce: 主要以同步、异步和响应式编程的方式操作 Redis。虽然功能全面,但某些特性可能不如 Jedis 丰富。
可扩展性:
- Jedis: 如果需要扩展功能,通常需要修改 Jedis 源码。
- Lettuce: 更容易进行扩展,支持更多的自定义和扩展。
稳定性:
- Jedis: 在高并发连接的场景下可能不如 Lettuce 稳定。
- Lettuce: 基于 Netty 框架,能够更好地管理连接和实现异步操作,通常在高并发环境下表现更稳定。
安全性:
- Jedis: 在安全性方面不如 Lettuce。
- Lettuce: 完全兼容 Redis 6.0,支持 Redis 的 ACL 规则,可以进行细粒度的权限控制。
同步与异步:
- Jedis: 主要支持同步操作,不支持异步操作。
- Lettuce: 支持异步操作,基于 Netty 的事件驱动架构提供异步调用。
线程安全性:
- Jedis: 客户端实例不是线程安全的,需要每个线程一个 Jedis 实例,通常通过连接池来管理。
- Lettuce: API 是线程安全的,多个线程可以共享一个 Lettuce 连接。
连接方式:
- Jedis: 如果不使用连接池,每次使用后需要关闭连接,避免资源泄露。
- Lettuce: 基于 Netty,可以在一个连接上并发处理多个命令。
总结:
- Jedis: 适合功能需求全面且不需要高并发处理的场景,可能需要较多的配置和管理。
- Lettuce: 更适合高并发、需要异步处理和线程安全的场景,易于扩展且稳定性较高。
选择哪个客户端取决于具体的需求和使用场景。
18. Redis key 的过期时间和永久有效分别怎么设置
设置键的过期时间
使用EXPIRE命令,EXPIRE命令用于设置键的过期时间,以秒为单位
EXPIRE key seconds
例如:
EXPIRE mykey 60 #设置 mykey 的过期时间为68秒
使用PEXPIRE命令,PEXPIRE命令用于设置键的过期时间,以秒为单位
PEXPIRE key milliseconds 例如:
PEXPIRE mykey 60000 #设置 mykey 的过期时间为60000毫秒(即60秒) 以上的两个区别就是秒级和毫秒级的区别
使用EXPIREAT命令,EXPIREAT命令用于设置键的过期时间为指定的 Unix 时间戳,以秒为单位。
EXPIREAT key timestamp 例如 :
EXPIREAT mykey 1672531199 #设置 mykey 的过期时间为指定的 Unix 时间戳
使用PEXPIREAT命令,PEXPIREAT命令用于设置键的过期时间为指定的 Unix 时间戳,以秒为单位。
PEXPIREAT key milliseconds-timestamp
例如:
PEXPIREAT mykey 1672531199000#设置 mykey 的过期时间为指定的 unix 时间戳(毫秒)
使用SET命令带选顶,SET命令可以在设置键值的同时指定过期时间。
SET key value EX seconds
SET key value PX milliseconds
例如: SET mykey"value"EX60 #设置 mykey 的值为"value"并使其在68秒后过期
SET mykey"value”Px68080 #设置 mykey 的值为"value”并使其在60008童秒(即68秒)后过期
设置键的过期时间
使用PERSIST命令,PERSIST命令用于移除键的过期时间,使其变为永久有效。
PERSIST key 例如:
PERSIST mykey #移除 mykey 的过期时间,使其变为永久有效
19. Redis内存用完了会发生什么
Redis 内存用完后的行为主要由配置的内存回收策略决定。默认的 noeviction
策略不会删除任何键,当内存不足时会报错,这通常不建议使用。常用的策略是 lru
,它会回收最近最少使用的键,特别是那些有过期时间的键。此外,还有 random
策略,它随机删除一些键,以及 ttl
策略,它根据键的过期时间来优先回收即将过期的键。选择合适的策略可以根据具体的业务需求来优化内存管理。
20. Redis如何实现延时队列
可以使用有序集合(Sorted Set)来实现延迟队列.有序集合中的每个元素有一个关联的分数,可以用来表示任务的执行时间戳.具体步骤如下:
添加任务到延迟队列 将任务添加到有序集合中去,使用任务的执行时间作为分数(score)
javaString queueName = "delay_queue"; String taskId = "task_1"; long delay = 5000; // 延迟时间(毫秒) long executionTime = System.currentTimeMillis() + delay; Jedis jedis = newJedis("localhost"); jedis.zadd(queueName, executionTime, taskId); jedis.close();
轮询延时队列并执行任务 定期检查有序集合中的任务,找到那些执行时间已经到达或者超过当前时间的任务, 并执行这些任务.
21. 看门狗机制的原理是什么
分布式锁通常会设置一个过期时间,以防止锁永远不被释放。如果业务处理时间超过锁的过期时间,就会出现锁过期的情况。为了解决这个问题,我们引入了看门狗机制。看门狗机制的主要作用是自动续约分布式锁,确保在业务逻辑处理完之前,锁不会过期。
具体来说,当客户端获取到锁时,会在 Redis 中设置一个锁键和一个过期时间(默认30秒)。同时,Redisson 会启动一个后台任务,这个任务会定期检查锁的状态。看门狗任务会每隔一段时间(默认是锁的过期时间的1/3,即10秒)检查锁的状态。如果锁仍然被持有,看门狗会将锁的过期时间重置为初始值。这样,锁的过期时间会不断延长,直到客户端明确释放锁或客户端挂掉。
22. Redis如何实现分布式锁
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。
Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。
Redis 的 SET 命令有个 NX
参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
- 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
基于 Redis 实现分布式锁时,对于加锁操作,我们需要满足三个条件。
- 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用SET 命令带上 NX 选项来实现加锁;
- 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET命令执行时加上 EX/PX选项,设置其过期时间:
- 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;
满足这三个条件的分布式命令如下:
SET lock_key unique_value NX PX 10000
lock_key
就是 key 键;
unique_value
是客户端生成的唯一的标识,区分 来自不同客户端的锁操作;
NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
PX 10000 表示设置 lock_key 的 过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要 先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作(即,先判断后删除),这时就 需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
//释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1])== ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
基于 Redis 实现分布式锁有什么优缺点?
分布式锁在实际使用中非常普遍。使用分布式锁时需要注意:在任何时刻,只能有一个客户端持有锁,并且在发生链接中断或异常时,锁应该能够被正确释放。实现分布式锁的方法主要有三种:基于数据库、Redis和 Zookeeper。
基于数据库的实现方案性能较低,且可能影响业务,因此不太常见。Redis 是一种较为常用的实现方式,通常利用 setnx
命令来实现锁的获取。如果 setnx
成功,表示锁获取成功;如果失败,则表示锁未能获取。业务执行完毕后,通过 del
命令释放锁。为了处理异常情况,Redis实现中常配合使用看门狗机制,以确保锁在业务处理期间能够自动续约。
基于 Redis 实现分布式锁的 优点:
- 性能高效(这是选择缓存实现分布式锁最核心的出发点)
- 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx方法,实现分布式锁很方便。
- 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)
基于 Redis 实现分布式锁的 缺点:
超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间自动失效,注意A线程没执行完,后续线程B又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了,
那么如何合理设置超时时间呢?
我们可以 基于续约的方式 设置超时时间(看门狗机制):先给锁设置一个超时时间,然后启动个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。
Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
Redis 如何解决集群情况下分布式锁的可靠性?
为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。
它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署5个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。
Redlock 算法加锁三个过程:
第一步是,客户端获取当前时间(t1)。
第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:
加锁操作使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。
如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间(不是对「锁」设置超时时间,而是对「加锁操作」设置超时时间),加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。
第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(
t2 - t1
)。如果t2 - t1 < 锁的过期时间
,此时,认为客户端加锁成功,否则认为加锁失败。
可以看到,加锁成功要同时满足两个条件( 简述:如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功 ):
条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
条件二:客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。
加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2 - t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
加锁失败后,客户端 向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。
23. Redis 的大 Key 问题如何解决
什么是大Key
大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。一般而言,下面这两种情况被称为大 key:
- String 类型的值大于 10 KB
- Hash、List、Set、ZSet 类型的元素的个数超过 5000个
大 key 会带来什么问题
大 key 会带来以下四种影响:
客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞Redis,从客户端这一视角看,就是很久很久都没有响应。
引发网络阻塞。每次获取大 key产生的网络流量较大,如果一个key的大小是1MB,每秒访问量为1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
阻塞工作线程。如果使用 del删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令内存分布不均。
集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis节点占用内存多,QPS 也会比较大。
如何找到大 key ?
redis-cli --bigkeys 查找大key
可以通过 redis-cli --bigkeys 命令查找大 key:
shellredis-cli -h 127.0.0.1 -p6379 -a"password"-- bigkeys
使用的时候注意事项:
最好选择在 从节点 上执行该命令。因为主节点上执行时,会阻塞主节点;
如果没有从节点,那么可以选择在 Redis 实例业务压力的低峰阶段进行扫描查询,以免影响到实例的正常运行;
或者可以使用 -i 参数控制扫描间隔,避免长时间扫描降低 Redis 实例的性能。
不足之处:
这个方法 只能返回每种类型中最大的那个 bigkey,无法得到大小 排在前 N 位 的 bigkey;
对于集合类型来说,这个方法只统计集合元素 个数的多少,而不是实际占用的 内存量。但是,一个集合中的元素个数多,并不一定占用的内存就多。因为,有可能每个元素占用的内存很小,这样的话,即使元素个数有很多,总内存开销也不大;
使用 SCAN 命令查找大 key
使用 SCAN 命令对数据库扫描,然后用
TYPE
命令获取返回的每一个 key 的类型。对于
String
类型,可以直接使用 STRLEN 命令 获取字符串的长度,也就是占用的内存空间字节数。对于 集合类型 来说,有两种方法可以获得它占用的内存大小:
如果能够预先从业务层知道集合元素的 平均大小,那么,可以使用下面的命令获取集合元素的个数,然后乘以集合元素的平均大小,这样就能获得集合占用的内存大小了。
List 类型:
LLEN
命令;Hash 类型:
HLEN
命令;Set类型:
SCARD
命令;SortedSet类型:
ZCARD
命令如果 不能提前知道写入集合的元素大小,可以使用
MEMORY USAGE
命令(需要 Redis 4.0 及以上版本),查询一个键值对占用的内存空间。
使用 RdbTools 工具查找大 key
使用 RdbTools 第三方开源工具,可以用来解析 Redis 快照(RDB)文件,找到其中的大 key。比如,下面这条命令,将大于 10 kb 的 key 输出到一个表格文件。
shellrdb dump.rdb-c memory--bytes 10240 -f redis.csv
如何删除大 key?
删除操作的本质是要释放键值对占用的内存空间,不要小瞧内存的释放过程。
释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
因此,删除大 key 这一个动作,我们要小心。具体要怎么做呢?这里给出两种方法:
- 分批次删除
- 异步删除(Redis 4.0版本以上)
下面详细介绍:
分批次删除
对于删除大 Hash,使用
hscan
命令,每次获取 100 个字段,再用hdel
命令,每次删除1个字段。对于删除大 List,通过
ltrim
命令,每次删除少量元素。对于删除大 Set,使用
sscan
命令,每次扫描集合中 100 个元素,再用srem
命令每次删除一个键。对于删除大 ZSet,使用
zremrangebyrank
命令,每次删除 top 100个元素。异步删除
从 Redis 4.0 版本开始,可以采用 异步删除法,用 unlink 命令代替 del 来删除。
这样 Redis 会将这个 key 放入到一个 异步线程 中进行删除,这样 不会阻塞主线程。
除了主动调用 unlink 命令实现异步删除之外,我们还可以通过配置参数,达到某些条件的时候自动进行异步删除。 主要有 4 种场景,默认都是关闭的:
shelllazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del noslave-lazy-flush no
它们代表的含义如下 lazyfree-lazy-eviction:表示当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除
lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除;
lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如.rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启lazy free 机制删除;
slave-lazy-flush:针对 slave (从节点)进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。
建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样就可以有效的提高主线程的执行效率。