Redis(3):应用

  • 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。

  • 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。

  • 消息队列:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。

  • 延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。

  • 分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。

  • 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。

1 分布式锁

  • 锁是否可重入(唯一标识)

  • 锁失效机制(过期时间,看门狗续期)

  • 未释放锁

  • 获取锁后未同步到从服务器(红锁)

1.1 分布式锁实现

1.1.1 基于数据库的实现方式

基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:

1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;

2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;

3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

1.1.2 基于Redis的实现方式

SETNX 是 Redis 中的一个命令,用于设置键的值,但仅当键不存在时才设置成功。在分布式环境中,可以利用 SETNX 命令来实现分布式锁。具体步骤如下:

  1. 客户端通过 SETNX 命令尝试将一个特定的键作为锁的标识,并设置一个唯一的值作为锁的持有者标识。

  2. 如果 SETNX 命令成功执行(返回值为 1),表示当前客户端成功获取了锁,可以执行后续操作。

  3. 如果 SETNX 命令执行失败(返回值为 0),表示当前锁已被其他客户端持有,当前客户端未获取到锁,需要等待一段时间后重新尝试获取锁。

虽然 SETNX 命令在某些情况下可以用来实现简单的分布式锁,但是它也存在一些问题:

  1. 无法设置过期时间SETNX 命令本身不支持设置键的过期时间,因此当持有锁的客户端发生异常或程序出现问题时,可能导致锁无法被释放,造成死锁或锁泄露问题。

  2. 非原子性操作:尽管 SETNX 命令本身是原子性的,但是获取锁和释放锁通常需要多个命令的组合,例如获取锁时需要执行 SETNX,释放锁时需要执行 DEL。这种组合操作不是原子性的,可能会导致锁的不一致性问题。

  3. Redis集群,设置锁到主节点,主节点同步锁时挂掉

为了解决这些问题,可以采用以下方法:

  1. 配合 EXPIRE 命令设置过期时间:在获取锁成功后,使用 EXPIRE 命令为锁设置一个合理的过期时间,确保即使持有锁的客户端发生异常,锁也能在一定时间后自动释放。

  2. 使用 Lua 脚本确保原子性:将获取锁和释放锁的操作封装在 Lua 脚本中执行,Lua 脚本可以在 Redis 中以原子性的方式执行多个命令,确保获取锁和释放锁的操作是原子性的,避免了竞态条件的发生。

  3. 考虑使用Redlock红锁算法等更复杂的分布式锁方案:如果应用场景要求更高的分布式锁安全性和可靠性,可以考虑使用 Redlock 算法等更复杂的分布式锁方案,这些方案通常基于多个 Redis 实例,并结合超时机制和复制机制来保证分布式锁的安全性和可靠性。

1.1.3 基于ZooKeeper的实现方式(临时有序节点)

[!note]
每个客户端对某个方法加锁时,在zookeeper上的 与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有 序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁 无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

1.1.4 Redisson实现分布式锁

业务处理时间>锁自动释放时间看门狗续期:延迟任务

1.2 分布式锁挂掉(无法获取锁)

  1. 检查Redis服务器状态:首先需要确保Redis服务器正常工作。可以尝试通过telnet命令连接Redis服务器,并使用PING命令检查连接是否正常。如果无法连接或者连接异常,可以尝试重启Redis服务器来恢复正常工作。

  2. 检查网络连接:如果Redis服务器正常工作,但是无法获取到锁,可能是网络连接存在问题。可以检查网络连接是否正常,包括网络延迟、带宽等。可以使用ping命令或者traceroute命令来检查网络连接是否正常。

  3. 重启Redis:如果发现Redis分布式锁挂了,我们可以尝试重启Redis服务来解决问题。在重启之前,需要先确保已备份好重要的数据,并通知系统管理员进行操作。

  4. 采用备份机制:在分布式系统中,可以考虑使用主从复制或者集群模式来提供高可用性和容错能力。当主节点出现故障时,从节点可以接管工作,避免分布式锁挂掉导致系统不可用。

  5. 重试机制:在获取分布式锁时,可以设置重试机制,即当无法获取到锁时,进行一定次数的重试。这样可以增加获取到锁的机会。可以设置一个重试次数和重试间隔,当达到重试次数后,可以进行其他的处理逻辑。

  6. 监控和告警:为了及时发现Redis分布式锁挂了的情况,可以设置监控和告警机制。可以监控Redis服务器的状态,包括连接数、内存使用情况等,并设置告警规则,在异常情况下及时通知管理员。

  7. 锁失效机制

1.3 存在问题

持有锁未释放导致死锁

  • 锁过期时间
    锁提前过期

  • 守护进程,自动续期
    释放其他机器申请的锁

  • 加上唯一标识

主从切换,从库未同步主库的锁

  • Redlock,红锁,在多个Redis实例上加锁

Redis 常见面试题 | 小林coding (xiaolincoding.com)

什么是分布式锁?实现分布式锁的三种方式 - 刘清政 - 博客园 (cnblogs.com)

Java分布式锁(6种实现方法)_分布式锁 实现方案-CSDN博客

怎样实现redis分布式锁? - 知乎 (zhihu.com)

2 消息队列

  • List

  • Stream:发布/订阅

2.1 Pub/sub

发布订阅机制存在以下缺点,都是跟丢失数据有关:

  1. 发布/订阅机制没有基于任何数据类型实现,所以不具备「数据持久化」的能力,也就是发布/订阅机制的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,发布/订阅机制的数据也会全部丢失。

  2. 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。

  3. 当消费端有一定的消息积压时,也就是生产者发送的消息,消费者消费不过来时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是 client-output-buffer-limit pubsub 32mb 8mb 60

  4. 丢消息

所以,发布/订阅机制只适合即时通讯的场景,比如构建哨兵集群 (opens new window)的场景采用了发布/订阅机制。

Redis 消息队列的三种方案(List、Streams、Pub/Sub) - 知乎 (zhihu.com)

3 搜索引擎

  • RediSearch:基于 Redis 的搜索引擎模块

3.1   RediSearch vs Elasticsearch

4 延时任务(延迟队列)

场景:订单超时取消、红包超时退回

  1. Redis 过期事件监听

  2. Redisson 内置的延时队列

延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:

  • 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消;

  • 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单;

  • 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单;

在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

4.1 Redis过期事件监听

Redis 2.0 引入了发布订阅 (pub/sub) 功能。在 pub/sub 中,引入了一个叫做 channel(频道) 的概念,有点类似于消息队列中的 topic(主题)

pub/sub 涉及发布者(publisher)和订阅者(subscriber,也叫消费者)两个角色:

  • 发布者通过 PUBLISH 投递消息给指定 channel。

  • 订阅者通过SUBSCRIBE订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。

在 pub/sub 模式下,生产者需要指定消息发送到哪个 channel 中,而消费者则订阅对应的 channel 以获取消息。

Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它们发送消息的,而不是我们自己编写的代码。其中,__keyevent@0__:expired 就是一个默认的 channel,负责监听 key 的过期事件。也就是说,当一个 key 过期之后,Redis 会发布一个 key 过期的事件到__keyevent@<db>__:expired这个 channel 中。

我们只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延时任务功能。

  1. 时效性差:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。

  2. 丢消息:Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。

  3. 多服务实例下消息重复消费:Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。

4.2 Redisson延迟队列

Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。

Redisson 使用 zrangebyscore 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。

  • 减少了丢消息的可能性

  • 消息不存在重复消费问题

5 频繁点赞操作

采用redis的集合(set),通过用户ID判断点赞唯一性,利用Redis作为缓存更新,异步写入数据库;异步通知点赞人,就将点击当成事件放到一个队列中 统一处理即可。

redis-cli
setnx mylock uuid