Redis

发布时间 2023-08-11 15:36:57作者: 江南烟雨行舟

用来做缓存的,减少磁盘的 IO 读写操作,适合缓存不需要变动的值。

Redis 并不是多线程的,而是单线程 + 多路 IO 复用。

数据类型

  • string:二进制安全 的数据结构(可以存放例如图片这种二进制数据),默认情况下 最大为 512MB

  • list:有序可重复的双向链表,最大长度为 2^32 - 1(4,294,967,295)个元素。

  • set:无序不可重复集合,最大长度和 lists 一样。

  • hash:类型理解为 Java 中的 Map,里面保存类似于 web 请求的键值对参数。

  • zset:在 set 基础上增加了一个评分属性(score),通过这个属性排序成员。

事务

  1. 事务中的所有命令都是 按顺序执行 的,并且事务执行的时候不会处理其它客户端的请求
  2. 客户端在调用 EXEC 命令之前断开了连接,则不会执行任何操作;调用 EXEC 命令之后断开连接,会执行事务的所有操作。
  3. 不支持事务嵌套,抛出:(error) ERR MULTI calls can not be nested。
  4. 不支持事务回滚。

使用

使用 MULTI 创建一个 Redis 事务,客户端发送命令后并不会立即执行而是将它们排队,一旦调用 EXEC 后,所有的命令都会被执行,而 DISCARD 就是取消事务。

以下示例以原子方式递增 foo 和 bar 键:

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

从上面的会话中可以看出,EXEC 返回一个数组保存了命了的执行结果,执行结果和命令的执行顺序相同。

事务中的错误

一个事务被分为 排队阶段执行阶段

  1. 排队阶段:在调用 EXEC 方法之前出现错误,例如语法错误或内存限制等情况;出现这种情况,当前的事务将会被丢弃,不会执行任何命令。如果命令成功排队会返回 QUEUED,客户端通过判断这个值来主动丢弃当前事务。

    # 当排队阶段出现错误,执行 EXEC 命令会出现以下错误
    # 事务由于先前的错误而被丢弃
    # EXECABORT 由于先前的错误而放弃事务。
    (error) EXECABORT Transaction discarded because of previous errors.
    
  2. 执行阶段:在调用 EXEC 命令之后出现错误,例如字符串进行自增操作。出现这种情况,还是会继续执行其它命令;对于执行失败的命令,失败信息会放到 EXEC 返回的数组中。

    # incr k1 命令肯定会自增失败,所以第二条命了会返回错误
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379(TX)> 
    127.0.0.1:6379(TX)> set k1 a
    QUEUED
    127.0.0.1:6379(TX)> incr k1
    QUEUED
    127.0.0.1:6379(TX)> exec
    1) OK
    2) (error) ERR value is not an integer or out of range
    

缓存雪崩

比如说,内存淘汰策略,导致大量的缓存同时过期了,然后这个时候大量请求都会直接访问数据库。

缓存中为什么没有这些数据呢?

  1. 有一大堆缓存的数据过期了
  2. 被内存淘汰策略淘汰了
  3. Redis 服务宕机了
717343a0da7a1b05edab1d1cdf8f28e5.png

解决方案:

  1. 使用随机过期时间
  2. 延长过期时间
  3. 超热数据使用永久 key
  4. redis 集群
  5. 降流、限流: 当流量达到一定的阈值, 直接返回 "系统拥挤" 之类的提示
  6. 加锁: 限制同时访问数据库的请求数

缓存击穿

和缓存雪崩类似,它是因为有一个热点数据过期了,导致大量请求直接访问 Mysql 了。

缓存穿透

请求访问了缓存和数据库中都没有的数据

解决方案:

  1. 缓存空对象: 当数据库中也不存在数据的时候,在 Redis 中缓存一个空值;但是这样会造成额外的内存消耗和短期的数据不一致
  2. 布隆过滤: 在客户端和 Redis 之间增加了布隆过滤器;当布隆过滤中没有数据的时候,就会直接禁止流程继续执行(禁止访问 Redis 和数据库)
  3. 如果是自增主键的话,就保存最大值到缓存;不是的话就保存所有主键。
  4. 限制 IP 访问
b7031182f770a7a5b3c82eaf749f53b0.png

布隆过滤器禁止流程继续执行,说明数据 100% 不存在;但是允许流程继续执行并不是 100% 准确的,所以还是有一定的穿透风险。

缓存预热

缓存预热就是防止服务器启动后立马宕机了

可以在服务启动前,将一些热点数据(日常统计)先添加到 Redis 中。

key 过期删除策略和内存淘汰策略

key 过期后不会立即删除,通过过期删除策略删除:

  • 定时删除: 给 key 设置过期时间的时候会创建一个关联的定时器,当时间到达时,由定时器自动执行 key 的删除操作;这样做的好处是过期的 key 可以被立即删除,但是会消耗 CPU 降低吞吐量。
  • 惰性删除: 访问 key 的时候判断是不是过期,过期就删除 key;这样做的好处是不会占用 CPU 资源,但是会导致过期后 key 还在缓存中。
  • 定期删除: 每隔一段时间,随机判断一些 key 是不是过期了,过期了就删除;这样做的好处是减少了 CPU 资源的占用,同时能够清理掉过期的 key,但是执行时常和频率无法确定,和算法又关系。

Redis 选择惰性删除 + 定期删除这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

在配置文件 redis.conf 中,可以通过参数 hz <每秒运行多少次> 来设定定期删除时间。

内存淘汰策略

在配置文件 redis.conf 中,可以通过参数 maxmemory <bytes> 来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存, 才会触发内存淘汰策略

不同位数的操作系统,maxmemory 的默认值是不同的:

  • 在 64 位操作系统中,maxmemory 的默认值是 0,表示没有内存大小限制,那么不管用户存放多少数据到 Redis 中,Redis 也不会对可用内存进行检查, 直到 Redis 实例因内存不足而崩溃也无作为。
  • 在 32 位操作系统中,maxmemory 的默认值是 3G,因为 32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位操作系统限制最大 3 GB 的可用内存是非常合理的,这样可以避免因为内存不足而导致 Redis 实例崩溃。

在配置文件 redis.conf 中,可以通过参数 maxmemory-policy 来设定内存淘汰策略:

  • volatile-random: 随机淘汰一些设置了超时时间的 key
  • volatile-ttl: 优先淘汰一些即将过期的 key
  • volatile-lru: 淘汰设置了过期时间,但偶尔使用的 key
  • volatile-lfu: 淘汰设置了过期时间,但已经不怎么用了的 key
  • allkeys-random: 随机淘汰一些 key
  • allkeys-lru: 淘汰偶尔使用的 key
  • allkeys-lfu: 淘汰已经不怎么使用的 key
  • noeviction: 关闭内存淘汰策略,如果关闭会抛出内存溢出

LRU(Least Recently Used): 最少使用的, 可以理解为这个 key 偶尔被使用
LFU(Least Frequently Used): 最不常用的, 可以理解为这个 key 已经不怎么用了
random: 随机选择一些 key
ttl: 优先淘汰那些即将过期的 key
volatile: 只会淘汰设置了过期时间的 key
allkeys: 会淘汰所有的 key

如果使用了 volatile-lru、volatile-lfu、volatile-random 和 volatile-ttl 策略, 当没有匹配到任何 key 的时候, 它们不会移除任何 key 类似于 noeviction.

不能使用简单的过期策略

  1. 如果设置时间太短,会造成穿透和雪崩,而且时间太短也没有必要使用缓存了。
  2. 设置时间太长,导致总是读取到脏数据。

数据一致性

缓存过期、数据添加、删除、更新后才需要更新缓存,保证 MySQL 和 Redis 中的数据是一致的(保证最终一致性):

  • 并发量不是很大的情况下可以加分布式锁做双写;
    例如: 在并发场景下请求 A 将 c 的值修改为 1, 同时请求 B 将 c 的值修改为 2, 这样就导致数据库和缓存中的值有可能不一样, 这个时候就可以加锁, 让 MySQL 和 Redis 是原子操作.
  • 通过 MQ 但是要保证消息的顺序一致性
  • 通过阿里巴巴的 canal

分布式锁

就是和 Java 中锁的作用是一样的,用来互斥共享资源、保证数据一致、防止数据重复。

常见的分布式锁实现方案:

  • Redis
  • MySQL
  • ZooKeeper

通过 SET 后面加 NX、PX 参数(保证原子)获取锁:

SET lockKey requestId NX PX 30000
  • lockKey: 键就是锁的名字。
  • requestId: 值就是客户端生成的一个随机字符串,它要保证在足够长的一段时间内,这个值是唯一的。
  • NX: 保证 lockKey 不存在的时候才能获取锁。
  • PX: 设置过期时间(毫秒),也可以通过 EX 设置过期时间(秒);到了过期时间后,会自动删除 lockKey,这是为了防止服务出现问题后锁没有被释放

在 Java 中使用 jedis 包的调用方法是:

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime)

锁超时问题

客户端 1 获取了锁后,但是业务处理时长超过了锁的过期时间, 导致锁被自动释放了,然后客户端 2 获取到锁后,客户端1执行完业务后会释放锁,这个时候其实释放的就是客户端 2 的锁。

18fa845b2de47de1840374c608333a70.png

通过使用锁续期解决锁超时问题。

Redisson 开源组件实现了锁续期的功能,叫做看门狗;锁的过期时间默认是 30 秒,每隔 10 秒会将锁的过期时间重新设置为 30 秒;当获取锁的服务崩溃后,30 秒后也会释放锁。

最好不要增加锁的释放时间:

  1. 没办法完全确定业务的处理时间。
  2. 如果获取锁的服务崩溃了,会一段时间空等待。

释放锁

释放锁的时候通过 lua 脚本保证原子性:

if redis.call("get",KEYS[1]) == ARGV[1] then
      return redis.call("del",KEYS[1])
  else
      return 0
  end

这段 Lua 脚本在执行的时候要把前面的 requestId 作为 ARGV[1] 的值传进去,把 lockKey 作为 KEYS[1] 的值传进去。

Redis 主从架构数据复制导致分布式锁出现问题

通常使用 Redis Cluster 或者 哨兵模式 这两种方式实现 Redis 的高可用,而这两种方式都是基于主从架构数据复制实现的,这就导致主从发生重选,分布式锁出现问题:

e6fcf4fb0db884b5388925c2e27c39bd.png

同步复制和异步复制都会发生这个问题,即使使用了 WAIT 命令也一样;因为从节点还没同步完就被选为了主节点。

还有网络分区也会导致分布式锁出现问题。

Redlock 算法

Redlock 算法解决主节点故障,从节点变为主节点后分布式锁出现问题

通过这种方式解决问题要求比较高,必须要有 >=5 个 Redis 节点(必须是奇数),这些节点完全互相独立,不存在主从复制或者其它集群协调机制

获取锁的大概过程:

  1. 获取当前 Unix 时间戳
  2. 遍历 5 个实例获取锁,保证获取了一半以上的锁
  3. 然后再用获取锁的时间减去第一步获取的时间
  4. 如果获取锁的时间大于了锁的过期时间,就会获取失败。
a488f9e1641b5a1ba576144b18454b78.png

高并发情况下,指定一个随机的获取超时时间,否则会出现没有一个线程获取锁的情况:

e6ee5fe2e5a6ebe7257c313254938770.png

一些问题

问题 1:为什么要在多个实例上加锁?

本质上为了容错,部分实例异常宕机,剩余实例只要超过 N/2+1 依旧可用。

问题 2:为什么步骤 3 加锁成功之后, 还要计算加锁的累计耗时?

因为加锁操作的针对的是分布式中的多个节点,所以耗时肯定是比单个实例耗时更久,至少需要 N/2+1 个网络来回,还要考虑网络延迟、丢包、超时等情况发生,网络请求次数越多,异常的概率越大,所以即使 N/2+1 个节点加锁成功,但如果加锁的累计耗时已经超过了锁的过期时间,那么此时的锁已经没有意义了。

问题 3:为什么释放锁,要操作所有节点,对所有节点都释放锁?

因为当对某一个 Redis 节点加锁时,可能因为网络原因导致加锁“失败”。

注意这个“失败”,指的是 Redis 节点实际已经加锁成功了,但是返回的结果因为网络延迟并没有传到加锁的线程,被加锁线程丢弃了,加锁线程误以为没有成功,于是加锁线程去尝试下一个节点了。

崩溃恢复(AOF 持久化)对 Redlock 算法影响

8c0f13367af46b2ad5314bb7123ab1a3.png

为了保证 Redlock 算法的安全性,有如下两种手段:

  1. 持久化配置中设置 fsync=always,性能大大降低。
  2. 恰当的运维,把崩溃节点进行延迟重启,超过崩溃前所有锁的 TTL 时间之后才加入 Redlock 节点组。

导致 Redis 阻塞的命令

  1. BRPOP、BLPOP、BRPOPLPUSH 命令:这些命令会阻塞 Redis 进程,直到有列表非空,或者达到超时时间。
  2. SCAN 命令:这个命令可能会阻塞 Redis 进程一段时间,尤其是在数据集非常大的情况下。
  3. SORT 命令:这个命令可能会阻塞 Redis 进程一段时间,尤其是在需要进行大量排序的情况下。
  4. FLUSHALL、FLUSHDB 命令:这些命令会立即清空所有数据库或者当前数据库的所有数据,直到清空操作完成前,Redis 进程会被阻塞。

KEYS 和 SCAN

使用 KEYS 命令时,Redis 主进程会被阻塞或内存不足;推荐使用 SCAN 命令。

SCAN 命令将数据遍历操作分成了好几个小块,在遍历的时候会处理其它客户端的请求。

SCAN 命令使用游标机制避免内存不足,但是会出现数据重复的情况。

Redis 6 多线程

在 Redis 6 中,多线程 I/O 仅用于处理网络 I/O 操作,这种方式可以提高 Redis 在高并发情况下的性能表现,并减少因 I/O 操作而导致的延迟。

但是,指令的执行还是单线程的。

Redis6多线程.png

Jedis、lettuce 和 Redisson 对比

Redis 官方推荐的 Java 客户端有Jedis、lettuce 和 Redisson。

Jedis

是老牌的 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持,其官方网址是:http://tool.oschina.net/uploads/apidocs/redis/clients/jedis/Jedis.html。

优点:

  • 支持全面的 Redis 操作特性(可以理解为API比较全面)。

缺点:

  • 使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步;
  • Jedis 客户端实例不是线程安全的,所以需要通过连接池来使用 Jedis。

lettuce

是一种可扩展的线程安全的 Redis 客户端,支持异步模式。如果避免阻塞和事务操作,如 BLPOP 和 MULTI/EXEC,多个线程就可以共享一个连接。lettuce 底层基于 Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。lettuce 的官网地址是:https://lettuce.io/

优点:

  • 支持同步异步通信模式;
  • Lettuce 的 API 是线程安全的,如果不是执行阻塞和事务操作,如 BLPOP 和 MULTI/EXEC,多个线程就可以共享一个连接。

Redisson

是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。其中包括( BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson 提供了使用Redis 的最简单和最便捷的方法。Redisson 的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。Redisson的官方网址是:https://redisson.org/

优点:

  • 使用者对 Redis 的关注分离,可以类比 Spring 框架,这些框架搭建了应用程序的基础框架和功能,提升开发效率,让开发者有更多的时间来关注业务逻辑;
  • 提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过 Redis 支持延迟队列等。

缺点:

  • Redisson 对字符串的操作支持比较差。

参考资料

https://redis.io/docs/manual/patterns/distributed-locks/

https://pdai.tech/md/arch/arch-z-lock.html

https://blog.csdn.net/ByteDanceTech/article/details/125814670

https://www.infoq.cn/article/dvaaj71f4fbqsxmgvdce

https://blog.51cto.com/u_15127579/2722185

https://developpaper.com/deep-understanding-of-rediss-simple-dynamic-string/

https://scalegrid.io/blog/top-redis-use-cases-by-core-data-structure-types/

https://gitee.com/mrjackiechan/class-documents/blob/master/Part_04/

https://yunpengn.github.io/blog/2019/05/04/consistent-redis-sql/

Redis面试题.pdf

Redis面试题(二).pdf

Redis面试题(含答案).pdf

Redis实战.pdf