🏳️🌈个人网站:code宝藏 👈,欢迎访问🎉🎉
🙏如果大家觉得博主写的还不错的话,可以点点关注,及时获取我的最新文章
🤝非常感谢大家的支持与点赞👍
📚这个是蒋德钧的《Redis核心技术与实战》的阅读笔记
文章目录
如何使用redis实现消息队列
消息队列的存取需求
-
消息保序
虽然消费者是异步处理消息,但是,消费者仍然需要按照生产者发送消息的顺序来处理消 息,避免后发送的消息被先处理了
-
重复消息处理
消费者从消息队列读取消息时,有时会因为网络堵塞而出现消息重传的情况。此时,消费 者可能会收到多条重复的消息。对于重复的消息,消费者如果多次处理的话,就可能造成 一个业务逻辑被多次执行
-
消息可靠性保证
消费者在处理消息的时候,还可能出现因为故障或宕机导致消息没有处理完成的情况。当消费者重启后,可以重新读取消息再次进行处理
解决方案
基于List的消息队列解决方案
**消息保序:**生产者可以使用 LPUSH 命令把要发送的消息依次写入 List,而消费者则可以使 用 RPOP 命令,从 List 的另一端按照消息的写入顺序,依次读取消息并进行处理。
因为List不会主动通知消费者有新消息写入,redis提供了BRPOP命令(阻塞式读取),客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列。
**重复消息处理:**我们可以为每一个消息提供全局唯一的ID号,List 本身是不会为每个消息生成 ID 号的,所以,消息的全局唯一 ID 号就需要生产者程序在发送消息前自行生成。生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。
**消息可靠性:**List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从 一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从 备份 List 中重新读取消息并进行处理了。
基于Streams的消息队列解决方案
Streams 是 Redis 专门为消息队列设计的数据类型,它提供了丰富的消息队列操作命令
-
XADD:插入消息,保证有序,可以自动生成全局唯一 ID;
-
XREAD:用于读取消息,可以按 ID 读取数据,可以设置block配置,实现阻塞读取操作。
-
XREADGROUP:按消费组形式读取消息;
-
XPENDING 和 XACK:XPENDING 命令可以用来查询每个消费组内所有消费者已读取 但尚未确认的消息,而 XACK 命令用于向消息队列确认消息处理已完成。
使用消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的
-
**消息保序:**XADD和XREAD命令
-
重复消息处理:消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消 费组内的其他消费者读取了
-
消息可靠性: Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到 消费者使用 XACK 命令通知 Streams“消息已经处理完成”。如果消费者没有成功处理消 息,它就不会给 Streams 发送 XACK 命令,消息仍然会留存。此时,消费者可以在重启 后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。
总结
如何解决缓存和数据库的数据不一致问题
读写缓存
要想保证缓存和数据库中的数据一致,就要采用同步直写策略,同时更新缓存和数据库。所以我们要在业务中使用事务机制,保证缓存和数据库的更新具有原子性。
同步直写策略:写缓存时,也同步写数据库,缓存和数据库中的数据一致;
异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。 使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么,此时,数据 库就没有最新的数据了。
只读缓存
1. 新增数据
直接写入到数据库中,缓存和数据库的数据是一致的。
2. 删改数据
根据删除缓存和更新数据库的顺序不同会有不同的问题:
无并发请求时
两种情况均适用
重试机制
可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消 息队列中重新读取这些值,然后再次进行删除或更新。
有并发请求时
情况一:先删除缓存,再更新数据库
问题:
解决办法:在线程A更新完数据库值后,可以让它sleep一小段时间,再进行一次缓存删除操作。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫 做**“延迟双删”**
之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存(读取到的是旧值),然后,线程 A 再进行删除。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。
情况二:先更新数据库,再删除缓存值
问题:
期间有不一致数据短暂存在,,对业务影响较小
总结
Redis如何应对并发访问
并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成 两步:
- 客户端先把数据读取到本地,在本地进行修改;
- 客户端修改完数据后,再写回 Redis。
我们把这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操 作)。
在并发情况下,这三个操作不具有互斥性,多个客户端值基于相同的初始值进行修改,而不是基于前一个客户端修改后的值进行修改。
所以要应对并发访问,我们就需要将这三个并发操作变成串行操作。
办法一
加锁
加锁可以将并发操作变成串行操作,但是会导致系统并发性能降低,且如果有多个客户端加锁时,需要使用分布式锁
办法二
为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:
-
把多个操作在 Redis 中实现成一个操作,也就是单命令操作;
INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作, Redis 在执行它们时,本身就具有互斥性。
-
把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而 保证了 Lua 脚本中操作的原子性。如果我们有多个操作要执行,但是又无法用 INCR/DECR 这种命令操作来实现,就可以把这些要执行的操作编写到一个 Lua 脚本中
如何使用Redis实现分布式锁?
分布式锁是由共享存储系统维护的变量,多个客户端可以向共享存储系统发送命令进行加 锁或释放锁操作。Redis 作为一个共享存储系统,可以用来实现分布式锁。
加锁
基于单个 Redis 节点实现分布式锁
对于加锁操作,我们需要满足三个条件
-
加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的 方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
-
锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所 以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
-
锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操 作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用 于标识客户端。
基于多个 Redis 节点实现高可靠的分布式锁
可以使用分布式锁算法Redlock
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户 端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布 式锁了,否则加锁失败。
这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它 实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
释放锁
释放锁包含了读取锁变量值、判断锁变量值和删除锁变量三个操作,我们无法使用单个命令来实现,所以,我们可以采用 Lua 脚本执行释放锁操作,通过 Redis 原子性地执行 Lua 脚本,来保证释放锁操作的原子性。