惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 【当耐特】
L
Lohrmann on Cybersecurity
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
P
Proofpoint News Feed
H
Heimdal Security Blog
云风的 BLOG
云风的 BLOG
H
Hacker News: Front Page
量子位
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LINUX DO - 最新话题
F
Full Disclosure
小众软件
小众软件
Martin Fowler
Martin Fowler
Security Latest
Security Latest
The Cloudflare Blog
Hacker News: Ask HN
Hacker News: Ask HN
C
Check Point Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
I
InfoQ
AI
AI
Blog — PlanetScale
Blog — PlanetScale
I
Intezer
H
Hackread – Cybersecurity News, Data Breaches, AI and More
M
MIT News - Artificial intelligence
V
Vulnerabilities – Threatpost
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
V2EX
N
News and Events Feed by Topic
C
Cybersecurity and Infrastructure Security Agency CISA
T
Troy Hunt's Blog
大猫的无限游戏
大猫的无限游戏
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Register - Security
The Register - Security
Forbes - Security
Forbes - Security
Recent Announcements
Recent Announcements
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cisco Talos Blog
Cisco Talos Blog
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
S
SegmentFault 最新的问题
S
Securelist
Cloudbric
Cloudbric
T
Tenable Blog
D
Docker

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
Redis 缓存技术学习系列之发布订阅 - 元视角
飞鸿踏雪 · 2017-04-16 · via 元视角

各位朋友,大家好,我是 Payne,欢迎大家关注我的博客,我的博客地址是http://qinyuanpei.com。最近这段时间的天气可谓是变幻莫测,常常是周一到周五像夏天般热烈,而周六和周天像秋天般冷清。你不知道它到底会在何时下雨,即使你可以一直带着伞等雨落下来。但是对于没有伞的我来说,学会努力奔跑以至于不那么狼狈,或许是在这个世界上我唯一可以去做的事情。可是你知道一个人孤独的时候,即使是下雨这种再平常不过的事情,他都可以从雨声里听出孤独的感觉来,所以这个周末我决定继续研究 Redis 缓存技术,而今天我想和大家讨论的话题是 Redis 中的发布-订阅(Pub-Sub),希望大家喜欢!

从观察者模式说起

  如果你熟悉常见的设计模式,就应该会知道在 24 种设计模式中,有一种称为观察者模式的设计模式,该模式又被称为发布-订阅模式。在正式讨论 Redis 中的发布-订阅特性前,我想先花点时间来为大家讲解下这种设计模式。观察者模式定义了一种一对多的依赖关系,让多个观察者同时监听同一个主题对象,当该主题对象在状态发生变化时,会通知所有观察者对象并使其自动更新自己。下面是该模式的 UML 类图:

设计者模式的UML类图 设计者模式的UML类图

  通常我们提到设计模式的时候,都认为实际模式是非常抽象而晦涩的概念,事实上设计模式是一种经过反复验证的编程经验。我们每天面对这个世界对其进行抽象并认识它,所以设计模式本质上是根植自生活的一种编程思想。以观察者模式为例,我们或许会在微信里订阅各种各样感兴趣的公众号,当这些公众号的内容发生更新时,就会主动向我们推送新的内容。在这里,我们订阅的公众号称为"主题",而我们则称为"观察者"或者"订阅者",而这正是观察者模式又被称为"发布-订阅模式"的原因所在,这种定义了一种一对多的依赖关系,让多个观察者同时监听同一个主题对象,当该主题对象在状态发生变化时,会通知所有观察者对象并使其自动更新自己的设计模式就被称为"观察者模式"。而通过这张图我们可以了解到,观察者模式试图解决的问题是,在不同的实例对象间相互协作的时候,如果在降低其各自耦合度的同时,维持这些示例对象间的一致性。在该模式中,主要存在四种角色,即:

  • 抽象主题(Subject):抽象主题将所有观察者对象的引用保存到一个集合里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加(Attach 方法)和删除(Detach 方法)观察者对象。
  • 具体主题(ConcreteSubject):具体主题将在其内部定义相关状态,并将相关状态存入具体观察者对象。在具体主题内部状态发生变化时,通知所有注册过的观察者发出通知,即 UML 类图中定义的 Notify()方法。
  • 抽象观察者(Observer):抽象观察者将为所有具体的观察者定义一个接口,在获得主题更新通知时更新自己,即 UML 类图中定义的 Update()方法,执行该方法后观察者与主题的状态实现同步。
  • 具体观察者(ConcreteObserver):具体观察者将实现抽象观察者所定义的更新接口,来使得观察者自身的状态与主题状态协调,即具体观察者需要重写 Update()方法并维护其内部状态同主题保持一致。

  至此我们就从思想上理解了观察者模式,观察者模式本质上是在维护一种一对多的依赖关系,因为观察者与主题都是依赖于抽象而非具体,两者分别属于两个不同层次上的抽象,因此观察者和主题两者间是解耦的。可是当你去实现一个具体的主题或者具体的观察者的时候,你会发现这两者间依然存在一定的依赖,因为观察者和主题在接口设计上需要协调,因为两者分别作为消息的"接收方"和"发送方"存在。观察者模式虽然在解耦上效果显著,可这并不代表它就是完美的。事实上,当观察者数目特别多的时候,为了通知所有的观察者将花费大量的时间;其次,当观察者间存在依赖关系时,观察者模式将导致这些观察者出现循环调用;再者,当主题通过异步的方式来通知观察者时,需要考虑通知本身是以自洽的方式进行的;最后,观察者模式可以确保观察者捕捉到主题的变化,可是观察者模式机制本身不具备知晓主题如何变化的能力。好了,下面我们来讲解如何实现一个基本的观察者模式。

观察者模式的实现

  现在,我们已然了解到在观察者模式中主要有四类角色,即抽象主题、抽象观察者、具体主题和具体观察者。因此,要实现观察者模式,实际上就是要实现这四种不同的角色。回到我们最初讨论过的场景,即微信用户订阅公众号,假设博主希望在博客更新的时候,以邮件或者公众号的形式来通知读者朋友博客更新的内容,这是一个典型的一对多的依赖关系维护的问题,显然此时观察者模式是一个最佳的设计思路。在这个设计中,邮件和公众号是两个具体的观察者,而博客是一个具体的主题。参照观察者模式的 UML 类图,我们应该首先提取出来两个抽象类,即 Subject 和 Observer。

  对 Subject 类而言,首先它需要提供一个订阅(Subscribe 的方法和取消订阅(Unsubscribe)方法,这和我们在日常生活中订阅报纸是完全一样的;其次,它需要有一个更新(Update)的方法,该方法负责向所有的订阅者广播消息。为什么叫做广播呢?因为所有的订阅者都会收到这条消息,这种订阅者被动接受主题推送消息的方式我们称为"推送模式",即在 Update 的时候,主题会主动推送"参数"给订阅者;而订阅者主动拉取主题消息的方式我们称为"拉取模式",即在 Update 的时候,主题并不主动推送"参数"给订阅者,而是由订阅者通过注入的主题来获取消息。这两种方式我们都可以称之为观察者模式,在这里我们选择"推送模式",代码实现如下:

public abstract class Subject
{
   private IList<Observer> observers = new List<Observer>();

   public void Attach(Observer observer)
   {
      observers.Add(observer);
   }

   public void Deatch(Observer observer)
   {
      observers.Remove(observer);
   }

   public void Notify(string message)
   {
      observers.ToList().ForEach(o => o.Update(message));
   }
}

对 Observer 而言,它在观察者模式中承担着消息接收者的角色,所以我们需要为其定义好接收消息的接口,需要注意的是该接口必须与具体主题保持一致,这便是我在文章中提到的,主题和观察者存在一定程度依赖的问题。考虑到不同的观察者所做的事情是完全不同的,例如邮件和公众号采取两种不同的方式来推送消息,因此 Update 方法应该被声明为虚方法,以为不同的观察者提供重写的扩展能力。它的代码实现如下:

public abstract class Observer
{
   public virtual void Update(string message)
   {
   
   }
}

对具体观察者而言,我们需要做的就是继承 Observer 类然后重写 Update 方法,在这里我们需要实现两个不同的类 EmailObserver 和 WechatObserver,它们分别来实现邮件和公众号接收到主题推送消息以后的逻辑,这里以 EmailObserver 为例,代码实现如下:

public class EmailObserver:Observer
{
   public override void Update(string message)
   {
      Console.WriteLine("邮箱接收到订阅消息:{0}", message);
   }
}

对具体主题而言,我们不再关心如何向所有的观察者发送消息,该功能在 Subject 父类中已然完成。我们可以为新的主题类添加更多的属性来描述其内部发生变化时的状态,例如文章数目、评论数目或者是内容更改等等。在这个例子中我们选择最简单的方式,即简单通知这两个观察者,因此我们直接继承 Subject 类即可。此时,完整的调用代码如下:

BlogSubject blog = new BlogSubject();
blog.Attach(new EmailObserver());
blog.Attach(new WechatObserver());
blog.Notify("Payne更新了Redis缓存技术学习系列文章");

好了,现在通过下面的截图,我们就可以看到两个观察者 EmailObserver 和 WechatObserver,都接收到了来自主题 Blog 的消息推送。这就是观察者模式啦,看起来是不是非常简单。可是相信大家使用公众号以后就会发现一个问题,随着你订阅的内容越来越多,你的微信消息列表里出现的消息推送就越来越多,这个时候如果你不想接收消息推送该怎么办呢?答案好像只有一个,那就是取消订阅。这个场景可以看出"推送模式"让订阅者饱受消息骚扰,而为了解决这个问题,我们就有了"拉取模式",此时主题仅仅是告诉观察者博客内容有更新,而更新的内容需要观察者自己去处理,这种模式大同小异,大家可以参照"推送模式"来自己实现。

观察者模式基本实例 观察者模式基本实例

这些就是观察者模式的核心内容啦,观察者模式的优点是它解除了主题和观察者间的耦合,并且使得这两者各自都依赖于抽象而非具体,观察者模式适用的场景是当一个对象的改变需要给变其它对象时,而且它不知道具体有多少个对象有待改变时。在 C#中我们可以通过委托、事件以及 Observable 接口这三种方式来更好、更快的实现观察者模式,自然这些都是后话啦,如果以后有机会我们可以继续进行探讨。

Hey Redis Pub-Sub

好了,了解完观察者模式即发布-订阅模式以后,我们现在就可以开始学习 Redis 中的发布-订阅模式啦。为什么我们要在开始学习 Redis 中的发布-订阅模式前,了解设计模式相关的概念呢?这是因为 Redis 中的发布-订阅模式和 Gof 设计模式一脉相承,譬如事件机制、消息机制等概念其实都是观察者模式的一种实际应用,一旦我们掌握了观察者模式的核心思想,即使这个世界充满了套路,可是这对你我而言又有什么不同呢?我们学习设计模式不是为了记住这些类图,而是能在最恰当的场景中合理使用这些模式来解决问题,这是我们学习的最终目的。

Redis 中的发布-订阅模式是一种消息通信模式,即发布者发布消息,订阅者接收消息。在 Redis 中客户端可以订阅任意个频道,当该频道内接收到一个新消息时,所有订阅该频道的客户端都会收到这条新消息。我们可以这样理解这种消息通信模式,我们每个微信账号都是一个客户端,每个客户端都可以订阅任意个微信公众号,当微信的后台服务上接收到某个微信公众号的请求消息时,所有订阅了该微信公众号的客户端都会收到该推送。一个简单的图示如下:

Redis中的消息模式 Redis中的消息模式

   我们可以注意到这和我们在文章中提到的"观察者模式"非常相似,在这个通信模式下,客户端作为消息的订阅者,即观察者。而频道作为消息的发布者,即主题。在 Redis 中频道是一个字符串类型的值,你可以将其理解为一个 Id。虽然我们在这篇文章中花费大量时间来讲观察者模式,事实上 Redis 中的发布-订阅是非常轻量并且强大的,下面是常见的命令:

  • PSUBSCRIBE:该命令用于订阅一个或者多个符合模式匹配的频道

  • PUBSUB:该命令用于返回由活跃频道组成的列表,即可以查询订阅与发布系统的状态

  • PUBLISH:该命令用于发送消息到指定的频道

  • PUNSUBSCRIBE:该命令用于退订所有符合模式匹配的频道

  • SUBSCRIBE:该命令用于订阅一个或多个频道

  • UNSUBSCRIBE:该命令用于退订一个或多个频道

以上这些就是和发布-订阅相关的命令啦,从整体上而言它是相当简洁和紧凑的。在这篇文章中我们通篇都在说观察者模式,事实上 Redis 的发布-订阅从本质上来讲还是观察者模式,Redis 内部会维护一个频道的字典,首先它会从频道字典中查找所有的客户端,如果字典中不存在该频道,则将订阅该频道的客户端列表添加到字典中,否则它会返回字典中已经存在的客户端列表。在获取到所有客户端列表以后,Redis 将会遍历客户端列表中的客户端,然后给每个客户端发送消息,这部分代码的解读可以参考这篇文章:15 天玩转 redis —— 第九篇 发布/订阅模式。好了,这篇文章暂时就是这样子啦,为什么感觉最近学习 Redis 没有动力了呢?这篇文章没有实际的命令演示,这是因为我是在 Windows 系统下写完的这篇文章,深夜啦,睡吧!

现在我们一起来看一个简单的示例,在这个示例中我们让两个客户端 A 和 B,订阅同一个频道 News,然后由客户端 C 来向这个频道 News 广播一条消息,理论上客户端 A 和客户端 B 都将会收到这条消息,需要注意此时服务端是开启的。首先,对于客户端 A 和客户端 B,我们在两个不同的终端窗口中打开 redis-cli,然后输入命令:

在按下回车后,我们可以看到下面的信息:

1) "subscribe"
2) "News"
3) (integer) 1

好了,现在我们在客户端 C 中来广播一条消息:

> PUBLISH News "This is a message sent by 127.0.0.1:6379"

此时我们可以看到下图中所示的结果:

Redis中发布订阅模式 Redis中发布订阅模式