侧边栏壁纸
博主头像
一定会去到彩虹海的麦当

说什么呢?约定好的事就一定要做到啊!

  • 累计撰写 63 篇文章
  • 累计创建 16 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

[redis]——实战应用

一定会去到彩虹海的麦当
2022-04-28 / 0 评论 / 0 点赞 / 47 阅读 / 3,166 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-17,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

这个是蒋德钧的《Redis核心技术与实战》的阅读笔记

如何使用redis实现消息队列

消息队列的存取需求

  1. 消息保序

    虽然消费者是异步处理消息,但是,消费者仍然需要按照生产者发送消息的顺序来处理消 息,避免后发送的消息被先处理了

  2. 重复消息处理

    消费者从消息队列读取消息时,有时会因为网络堵塞而出现消息重传的情况。此时,消费 者可能会收到多条重复的消息。对于重复的消息,消费者如果多次处理的话,就可能造成 一个业务逻辑被多次执行

  3. 消息可靠性保证

    消费者在处理消息的时候,还可能出现因为故障或宕机导致消息没有处理完成的情况。当消费者重启后,可以重新读取消息再次进行处理

解决方案

基于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 命令用于向消息队列确认消息处理已完成。

使用消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的

  1. **消息保序:**XADD和XREAD命令

  2. 重复消息处理:消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消 费组内的其他消费者读取了

  3. 消息可靠性: Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到 消费者使用 XACK 命令通知 Streams“消息已经处理完成”。如果消费者没有成功处理消 息,它就不会给 Streams 发送 XACK 命令,消息仍然会留存。此时,消费者可以在重启 后,用 XPENDING 命令查看已读取、但尚未确认处理完成的消息。

总结

image-20220424190724890

如何解决缓存和数据库的数据不一致问题

读写缓存

要想保证缓存和数据库中的数据一致,就要采用同步直写策略,同时更新缓存和数据库。所以我们要在业务中使用事务机制,保证缓存和数据库的更新具有原子性。

同步直写策略:写缓存时,也同步写数据库,缓存和数据库中的数据一致;

异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。 使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么,此时,数据 库就没有最新的数据了。

只读缓存

1. 新增数据

直接写入到数据库中,缓存和数据库的数据是一致的。

2. 删改数据

根据删除缓存和更新数据库的顺序不同会有不同的问题:

image-20220425071356565

无并发请求时

两种情况均适用

重试机制

可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消 息队列中重新读取这些值,然后再次进行删除或更新。

image-20220425072518318

有并发请求时

情况一:先删除缓存,再更新数据库

问题:

image-20220425071833360

解决办法:在线程A更新完数据库值后,可以让它sleep一小段时间,再进行一次缓存删除操作。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫 做**“延迟双删”**

之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存(读取到的是旧值),然后,线程 A 再进行删除。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。

情况二:先更新数据库,再删除缓存值

问题:

image-20220425072100255

期间有不一致数据短暂存在,,对业务影响较小

总结

image-20220425072731205

Redis如何应对并发访问

并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成 两步:

  1. 客户端先把数据读取到本地,在本地进行修改;
  2. 客户端修改完数据后,再写回 Redis。

我们把这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操 作)。

在并发情况下,这三个操作不具有互斥性,多个客户端值基于相同的初始值进行修改,而不是基于前一个客户端修改后的值进行修改。

所以要应对并发访问,我们就需要将这三个并发操作变成串行操作。

办法一

加锁

加锁可以将并发操作变成串行操作,但是会导致系统并发性能降低,且如果有多个客户端加锁时,需要使用分布式锁

image-20220425074253905

办法二

为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:

  1. 把多个操作在 Redis 中实现成一个操作,也就是单命令操作;

    INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作, Redis 在执行它们时,本身就具有互斥性。

  2. 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。

    Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而 保证了 Lua 脚本中操作的原子性。如果我们有多个操作要执行,但是又无法用 INCR/DECR 这种命令操作来实现,就可以把这些要执行的操作编写到一个 Lua 脚本中

如何使用Redis实现分布式锁?

分布式锁是由共享存储系统维护的变量,多个客户端可以向共享存储系统发送命令进行加 锁或释放锁操作。Redis 作为一个共享存储系统,可以用来实现分布式锁。

加锁

基于单个 Redis 节点实现分布式锁

对于加锁操作,我们需要满足三个条件

  1. 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的 方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;

  2. 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所 以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;

  3. 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操 作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用 于标识客户端。

基于多个 Redis 节点实现高可靠的分布式锁

可以使用分布式锁算法Redlock

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户 端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布 式锁了,否则加锁失败。

这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它 实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

释放锁

释放锁包含了读取锁变量值、判断锁变量值和删除锁变量三个操作,我们无法使用单个命令来实现,所以,我们可以采用 Lua 脚本执行释放锁操作,通过 Redis 原子性地执行 Lua 脚本,来保证释放锁操作的原子性。

0

评论区