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
命令来实现分布式锁。具体步骤如下:
-
客户端通过
SETNX
命令尝试将一个特定的键作为锁的标识,并设置一个唯一的值作为锁的持有者标识。 -
如果
SETNX
命令成功执行(返回值为 1),表示当前客户端成功获取了锁,可以执行后续操作。 -
如果
SETNX
命令执行失败(返回值为 0),表示当前锁已被其他客户端持有,当前客户端未获取到锁,需要等待一段时间后重新尝试获取锁。
虽然 SETNX
命令在某些情况下可以用来实现简单的分布式锁,但是它也存在一些问题:
-
无法设置过期时间:
SETNX
命令本身不支持设置键的过期时间,因此当持有锁的客户端发生异常或程序出现问题时,可能导致锁无法被释放,造成死锁或锁泄露问题。 -
非原子性操作:尽管
SETNX
命令本身是原子性的,但是获取锁和释放锁通常需要多个命令的组合,例如获取锁时需要执行SETNX
,释放锁时需要执行DEL
。这种组合操作不是原子性的,可能会导致锁的不一致性问题。 -
Redis集群,设置锁到主节点,主节点同步锁时挂掉
为了解决这些问题,可以采用以下方法:
-
配合
EXPIRE
命令设置过期时间:在获取锁成功后,使用EXPIRE
命令为锁设置一个合理的过期时间,确保即使持有锁的客户端发生异常,锁也能在一定时间后自动释放。 -
使用 Lua 脚本确保原子性:将获取锁和释放锁的操作封装在 Lua 脚本中执行,Lua 脚本可以在 Redis 中以原子性的方式执行多个命令,确保获取锁和释放锁的操作是原子性的,避免了竞态条件的发生。
-
考虑使用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 分布式锁挂掉(无法获取锁)
-
检查Redis服务器状态:首先需要确保Redis服务器正常工作。可以尝试通过telnet命令连接Redis服务器,并使用PING命令检查连接是否正常。如果无法连接或者连接异常,可以尝试重启Redis服务器来恢复正常工作。
-
检查网络连接:如果Redis服务器正常工作,但是无法获取到锁,可能是网络连接存在问题。可以检查网络连接是否正常,包括网络延迟、带宽等。可以使用ping命令或者traceroute命令来检查网络连接是否正常。
-
重启Redis:如果发现Redis分布式锁挂了,我们可以尝试重启Redis服务来解决问题。在重启之前,需要先确保已备份好重要的数据,并通知系统管理员进行操作。
-
采用备份机制:在分布式系统中,可以考虑使用主从复制或者集群模式来提供高可用性和容错能力。当主节点出现故障时,从节点可以接管工作,避免分布式锁挂掉导致系统不可用。
-
重试机制:在获取分布式锁时,可以设置重试机制,即当无法获取到锁时,进行一定次数的重试。这样可以增加获取到锁的机会。可以设置一个重试次数和重试间隔,当达到重试次数后,可以进行其他的处理逻辑。
-
监控和告警:为了及时发现Redis分布式锁挂了的情况,可以设置监控和告警机制。可以监控Redis服务器的状态,包括连接数、内存使用情况等,并设置告警规则,在异常情况下及时通知管理员。
-
锁失效机制
1.3 存在问题
持有锁未释放导致死锁
-
锁过期时间
锁提前过期 -
守护进程,自动续期
释放其他机器申请的锁 -
加上唯一标识
主从切换,从库未同步主库的锁
-
Redlock,红锁,在多个Redis实例上加锁
Redis 常见面试题 | 小林coding (xiaolincoding.com)
什么是分布式锁?实现分布式锁的三种方式 - 刘清政 - 博客园 (cnblogs.com)
Java分布式锁(6种实现方法)_分布式锁 实现方案-CSDN博客
怎样实现redis分布式锁? - 知乎 (zhihu.com)
2 消息队列
-
List
-
Stream:发布/订阅
2.1 Pub/sub
发布订阅机制存在以下缺点,都是跟丢失数据有关:
-
发布/订阅机制没有基于任何数据类型实现,所以不具备「数据持久化」的能力,也就是发布/订阅机制的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,发布/订阅机制的数据也会全部丢失。
-
发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。
-
当消费端有一定的消息积压时,也就是生产者发送的消息,消费者消费不过来时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是
client-output-buffer-limit pubsub 32mb 8mb 60
。 -
丢消息
所以,发布/订阅机制只适合即时通讯的场景,比如构建哨兵集群 (opens new window)的场景采用了发布/订阅机制。
Redis 消息队列的三种方案(List、Streams、Pub/Sub) - 知乎 (zhihu.com)
3 搜索引擎
-
RediSearch:基于 Redis 的搜索引擎模块
3.1 RediSearch vs Elasticsearch
4 延时任务(延迟队列)
场景:订单超时取消、红包超时退回
-
Redis 过期事件监听
-
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 的消息,进而实现了延时任务功能。
-
时效性差:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。
-
丢消息:Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。
-
多服务实例下消息重复消费: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 |