构建一套可靠的分布式大型软件系统

https://icyfenix.cn/

我不服!这开源项目居然才888个星!?

先看一下我看了之后整理的思维导图吧:

我不服!这开源项目居然才888个星!?

看不清没关系,文末会给你领取链接的。

看完之后我个人的一个感受是:关于分布式架构方向,写的很多很全很系统。而且都是笔者一路走过来的经验之谈,浓缩在了文章里面。

对得起首页的这一句口号:构建可靠的大型分布式系统。

谁写的?

那么这个开源书的作者是谁呢?

周志明。

是的,就是你想到的那个凭借一己之力拉高了 Java 开发者内功水平,把 JVM 带到了初级面试题环节的男人。

我不服!这开源项目居然才888个星!?

其实你仔细看周佬写的自我介绍,很有小细节。

程序员、研究员、作者、布道师这四个职业,排在第一的是最没有噱头的“程序员”一职。

而在程序员里面,给自己的描述是:

一名兼职一些管理与研究工作的程序员。

其实关于这个点,我看过周佬的一些公开场合的自我介绍,都是说自己是一个“兼职一些管理工作的程序员”,给自己这样的人设标签。

他在自己写的《程序员之路》一文中解释过这个标签。

他想要透过这个标签表达的是对于一枚程序员,以后是想要发展为一个架构师还是研发管理者,都不要轻易地离开技术领域的一线前沿。

离开技术、放弃编码的决定很可能会像你高考之后放下的数学、生物、地理等知识那样,一旦放手,毕生就很难有机会再重新捡起来。

当你放下代码的时间越长,久而久之,你对代码、技术、产品状态与团队研发状态的理解,渐渐的会和团队成员产生了偏差错位,丧失了细节上给予指导的能力,丧失了专业问题上提出接地气解决方案的能力,只能在无法短期难以校验对错的大战略方向提意见。

在会议、流程及团队管理措施上下功夫,在职业经理人式的宣讲与汇报上寻找存在感。

此刻,你便从团队的导师变成了管理者,最终你与团队的关系,从携手并肩奋斗的伙伴,完全演变成只能靠公司制度与管理职位的权力来维系雇佣关系。

我能理解周佬说的现象,其实是一个非常普遍的现象。甚至有的朋友走上管理岗位的目标就是不再写代码了,基于当前的市场和行业现状,这样的选择是无可厚非的。

只是,有没有那么一点点可能,不要完全抛弃代码。这也需要是你和团队之间的最行之有效的纽带。

就像《代码整洁之道》一书中说的:

软件架构师本身就是最好的程序员,他们会一直编写代码,虽然可能不会像其他程序员输出的代码量那样多,但是只有持续地编程,才能确保他们遇见其他程序员所面对的问题,体会其他程序员心中的感受,因此如果不编程,他们亦将无法胜任软件架构这项工作。

这本《凤凰架构》,周佬自己对它的定位是这样的:

我不服!这开源项目居然才888个星!?

给开发人员整理的关于软件架构方面的技能地图,同时系统的梳理自己的知识,并配备了对应技术方案的演示程序。

真的是一件利人利己的事情。

我本来的想法是先带着你囫囵吞枣的走一圈这一本书,主要起到一个介绍的作用。

但是越写越不得劲的感觉,于是我把写好的大部分都删除了。因为即使我写的这么卖力认真,都没有体现出这本书的价值的千分之一。

你得自己去读,你才知道我没有骗你:这真的是个宝藏啊!

所以我决定换个思路,告诉你这里面有什么就行了,其实就是书中的探索起步一小节。

如果你对里面的某一部分感兴趣,自己主动打开网站去翻一下,那么我的目的就达到了。

探索起步

我不服!这开源项目居然才888个星!?

这是探索起步的更新日志部分,可以看到周佬对于该项目一直在进行维护新内容:

我不服!这开源项目居然才888个星!?

而对于已有的内容,其中的错别字、不通顺的地方、含义不清的地方,他也在抽时间修改,最近的一次修改就是 6 月 6 日,昨天,上周日:

我不服!这开源项目居然才888个星!?

所以,相比于其他的大部分网上的文章来说,会更加实时、系统、优质一点。

全书分为五大部分和两个篇外,而为了让你快速定位到合适自己的部分,周佬也细心的介绍了每一部分对应的读者类型。

  • 引导篇 探索起步这部分面向于准备对文档介绍的内容亲身实践的探索者。
  • 第一部分 演进中的架构:这部分适合所有开发者,但尤其推荐刚刚从单体架构向微服务架构转型的开发者去阅读。
  • 第二部分 架构师的视角:这部分讨论与风格无关的架构知识,适合所有技术架构师、系统设计、开发人员。
  • 第三部分 分布式的基石:这部分面向于使用分布式架构的开发人员。
  • 第四部分 不可变基础设施:这部分面向于基础设施运维人员、技术平台的开发者。
  • 第五部分 技术方法论:这部分面向于在企业中能对重要技术决策进行拍板的决策者。
  • 篇外 随笔文章:这部分无特定读者对象,内容是笔者日常文章的整理。
  • 篇外 附录:这部分面向刚开始接触云原生环境的设计者、开发者。

演进中的架构

其中第一部分我读完之后做的思维导图如下:

我不服!这开源项目居然才888个星!?

我曾经就完整的经历过一个服务从单体系统时代,再到 SOA 时代,最后步入微服务时代的全过程。

所以能比较深刻的理解到周老说的观点:

架构并不是被发明出来的,而是持续演进的结果。

可能你对于其中的后微服务时代和无服务时代稍微有点陌生,但是我换个英文名称,你就应该是非常熟悉了。

后微服务时代其实就是云原生时代,Cloud Native。

而无服务其实就是 Serverless。

但是需要注意的是,周佬把 Serverless 排在了 Cloud Native 之后,其实它们两者并没有继承替代关系。不要因为周老对于两者的书写顺序产生了“无服务就会比微服务更加先进”的错误想法。

周佬对于这两者之间的关系描述是这样的:

如果说微服务架构是分布式系统这条路当前所能做到的极致,那无服务架构,也许就是“不分布式”的云端系统这条路的起点。

架构师的视角

第二部分的思维导图如下:

我不服!这开源项目居然才888个星!?

这一部分主要聊了我们做分布式服务时,一定会涉及到的问题,比如:远程服务调用(RPC)、分布式事务的处理、多级分流、架构安全。

我个人认为这一部分是干货满满的。

其中访问远程服务,对 RPC 和 REST 从各自的起源开始进行了一个详尽的描述:

我不服!这开源项目居然才888个星!?

事务处理小节,你可以看看“共享事务”的概念,其实我发现有一部分号称是微服务架构的项目,走向了“共享事务”的路线。其实这是一种伪分布式,或者说是“阉割版”的分布式。

“共享事务”的提法和这里所列的两种处理方式在实际应用中并不值得提倡,鲜有采用这种方式的成功案例,能够查询到的资料几乎都发源于十余年前 Spring 的核心开发者Dave Syer撰写的文章《Distributed Transactions in Spring, with and without XA》。笔者把共享事务列为本章四种事务类型之一只是为了叙述逻辑的完备,尽管拆分微服务后仍然共享数据库的情况在现实中并不少见,但笔者个人不赞同将共享事务作为一种常规的解决方案来考量。

透明多级分流系统从客户端到网络再到服务端的拆析,这是一种上帝视角的描述,对于一次请求的进过脉络梳理,清晰的了解到系统的方方面面。而这一部分的主题就是“架构师的视角”。

架构师就应该是从这样的一个比较统筹规划的角度去看待系统,不必进入到具体系统的细枝末节中去:

我不服!这开源项目居然才888个星!?

分布式的基石

第三部分分布式的基石:

我不服!这开源项目居然才888个星!?

共识算法、服务发现、流量治理、网络通信、监控预警共同构成了分布式的基石。

可以说如果是一个分布式的服务,都能找到上面的这些关键词的影子。

有些是应用系统自己做的,有些是开源框架就帮你搞定了,你甚至不知道它们的存在。

但是我认为上面的诸如流量治理和监控预警(可观察性)并不是一个分布式服务一开始搭建时所必须的。

大多数情况下,刚刚搭建好的分布式都处于一个蛮荒状态。随着时间推进和业务的发展,会慢慢补充上流量治理和监控预警。

也就是说如果想要分布式服务发展的方向是可监控、可控制的,那么这些东西都是应该有的。

所以“基石”一词,用的像是手术刀一样精准。

接着来到了第四部分,不可变基础设施:

我不服!这开源项目居然才888个星!?

到这里我们就要从微服务走向云原生了。

在这一章,周佬以容器、编排系统和服务网格的发展为主线,介绍虚拟化容器与服务网格是如何模糊掉软件与硬件之间的界限,如何在基础设施与通讯层面上帮助微服务隐藏复杂性,解决原本只能由程序员通过软件编程来解决的分布式问题。

接下来的“技术方法论”属于微服务避坑指南,从目的、前提、边界、治理四个角度去阐述如何更好的使用微服务。

随笔文章

最后一部分是“随笔文章”:

我不服!这开源项目居然才888个星!?

其中的《云原生时代,Java 的危与机》和《程序员之路》这两篇文章,建议你反复观看。

前者是技术方向的,后者是软技能方向的。

读完《Java 的危与机》,我感受到的是一场关于 Java 的自我革命已经悄然开始了。

Java 并不是一个优秀的开发语言,这一点我是非常承认且确定的。但是 Java 有一个庞大的用户群体和异常丰富的生态,这是它的护城河。所以短时间内还倒不下来。

但是大风起于青萍之末。虽然风雨欲来,而包括我在内的很多人都浑然不知。

在文章里面,周佬有这样的一段话:

Java 支持提前编译最大的困难在于它是一门动态链接的语言,它假设程序的代码空间是开放的(Open World),允许在程序的任何时候通过类加载器去加载新的类,作为程序的一部分运行。要进行提前编译,就必须放弃这部分动态性,假设程序的代码空间是封闭的(Closed World),所有要运行的代码都必须在编译期全部可知。这一点不仅仅影响到了类加载器的正常运作,除了无法再动态加载外,反射(通过反射可以调用在编译期不可知的方法)、动态代理、字节码生成库(如 CGLib)等一切会运行时产生新代码的功能都不再可用,如果将这些基础能力直接抽离掉,Helloworld 还是能跑起来,但 Spring 肯定跑不起来,Hibernate 也跑不起来,大部分的生产力工具都跑不起来,整个 Java 生态中绝大多数上层建筑都会轰然崩塌。

“整个 Java 生态中绝大多数上层建筑都会轰然崩塌。”

所以,Java 的这次变革属于釜底抽薪。

读完《Java 的危与机》之后,你再去看《Graal VM》一文,你就明白了:为什么说 Graal VM 的成功与否,与 Java 的前途命运息息相关。

其实这场变革已然悄悄开始,比如说一个小点:

大多数运行期对字节码的生成和修改操作,在 Graal VM 看来都是无法接受的。

但是比如 CGLIB 就是通过运行时产生字节码(生成代理类的子类)来做动态代理的。

这是目前的主流形式。

现在因为Graal VM 支持不了,所以必须由和框架一起来共同解决。

因此自 Spring Framework 5.2 起,@Configuration 注解中加入了一个新的 proxyBeanMethods 参数,设置为 false 则可避免 Spring 对与非接口类型的 Bean 进行代理。

同样地,对应在 Spring Boot 2.2 中,@SpringBootApplication 注解也增加了 proxyBeanMethods 参数,通常采用 Graal VM 去构建的 Spring Boot 本地应用都需要设置该参数。

可以看到的是为了推进 Java 生态向 Graal VM 兼容,Graal VM 主动拉拢了 Java 生态中最庞大的一个派系:Spring。

这即使壮士断腕,也是唇亡齿寒。

而我最喜欢的还是技术演示工程部分,并不是架空了去谈,而是直接把项目 Demo 都给你准备好了,开箱即用:

我不服!这开源项目居然才888个星!?

我发现周佬写的这个开源项目还有个特点,引用的部分会给出具体的官方的地址。严谨又权威,比如写到项目中用到的技术组件的时候:

我不服!这开源项目居然才888个星!?

真心推荐大家收藏起来,系统学习。

一个问答

在项目里面,我还发现了一个问答。

问题和回答都非常的好,搬运过来给你看看。

评论区:https://icyfenix.cn/methodology/forward-msa/prerequest.html

问题如下:

周大哥,看到了您说的马太效应。再联想到之前您讲的软件涅槃,而完善的微服务体系允许服务有涅槃的过程,有强大的容错能力。微服务发展又如此迅猛,觉得马太效应真的不远。

我不禁对最需要掌握的技能进行了思考,并产生了更强的焦虑感。

我是一名有七年工作经验的java开发工程师,28岁,目前在一家北京的传统信息软件技术公司,工资相对计算机行业偏低。

局限在java语言来说,jvm调优与并发编程等比较高阶的能力,是不是就很不关键了?

jvm我读了您写的《深入理解Java虚拟机:JVM高级特性与最佳实践》的第二版与第三版,由于工作中鲜有机会实践,只停留在一些理论理解,而缺失实践,理论知识也会淡忘。

并发编程读过《Java并发编程实战》,对并发编程有些了解,也有一些实践,一般水平。

微服务公司并没有用起来,实践经验也缺少。远程调用、分布式事务、注册中心、配置中心、熔断、限流等知识,通过看视频跟您的这个文档有一些了解。

java基础知识,经过这些年的磨练,是挺扎实了,spring能熟练使用。

常用设计模式有了解,也理解的比较到位。

我不想沦为螺丝钉。

我应该提升自己的哪些能力呢?

这些年只是做到了胜任分配给自己的工作。

现在发现自己缺少前瞻性思考,缺少对自己职业生涯的把控。

我现在想把握自己的职业生涯,请周大哥给一些指导。

我会通过招聘市场去挖掘市场需求,做整理,进行思考。

但是迫不及待的想跟您述说一下,请您不吝赐教,希望我的请求不是很唐突。

这个问题其实是很具有普遍性的:学了没地方实践,慢慢就忘记了。理论学了一大堆,聊起来可以谈笑风生,但是就是没有实际使用过。自己就是一颗螺丝钉。

周佬的回复如下:

写这文章不是为了贩卖焦虑,我也没有能力指导别人的职业生涯,但针对“应该提升自己的哪些能力”这类问题,我以前被问过很多次,这里可以重复一下。

我的建议就两个:

不要轻视不直接产生实践价值的知识;
不要对陷入已经被你熟练掌握的技术中不能自拔。

为了便于你理解,我做一个很土的比喻,把程序员提升自己类比成武侠中的练功,软件中的技能其实有很明显的“内力”和“武功”之分,譬如你提到的Java虚拟机,这类知识不仅是你在工作中鲜有机会实践,我也是差不多的。

大学计算机课程中,以“原理”二字结尾的课程,譬如计算机原理、操作系统原理、编译原理、数据库原理,等等,对绝大多数人而言,都不太会去设计处理器逻辑电路、设计程序语言和编译器,开发操作系统内核。

这些都有很经典的书:编译原理的龙书,计算机体系结构和程序运行的CSAPP,分布式与数据库原理的DDIA、操作系统原理的MOS,等等。这些书系统严谨全面,但可读性并不优秀,在B站/Coursera刷这些书作者们的公开课翻译视频也许是更好的方法。

这些技能能够辅助你去思考和分析问题,但是很难直接为你解决生产中的问题,以实践价值,就是以工作中是否有机会用到来衡量它们的作用是不合适的。

但这些课程之所以会是必修,是因为学习它们,能够为一名程序员的知识框架构筑好基础。

这话听起来很教条,可是当你一旦建立了相对完备稳固的知识框架,发现遇到的新知识、新技术,能够很自然地安放在已有知识体系的某个位置上,能够清楚感知到语言、技术、框架的设计意图和目标,甚至能共鸣到设计者当时所想,就会产生一种理所当然的感觉。

这样你接受新知识的认知负荷就会比别人更低,掌握起来更快速,理解起来更深刻。

我在这文档开篇中所说的,写这部文档是以整理自己的知识框架为目的,并非场面话,这点的确就是程序员如何学习新知识的关键,在知识快速迭代IT业界,这也是决定一名程序员能力上限有多高的根本因素。

相对的,那些具体的、用来解决生产中问题的技术和方法,譬如你提到的Spring、设计模式,我将其类比为“武功”。

这当然也是重要的,只有内力没有武功无法行走江湖,空有一身理论,但写不出代码来(包括那些只定大方向的架构师、设计者),我认为不肯定是合格的程序员,也很难指望能成为一名出色的技术领导(难以服众)。

但是具体的“武功”应该是能够快速捡起的,也能快速“忘掉”的,就是避免将一件事情做熟了,就一直陷在这件事情里面,避免拿到一把好的锤子,就看着一切问题都像是钉子。

很多程序员都抱怨,自己是CRUD Boy,自己在业务逻辑中打滚,没有机会接触底层的或者前瞻性的技术,所以自己技术难以提升。

这里当然有客观原因的存在,但往往也是受到了主观原因放大。

程序员其实与旧时代的手工技艺者差不多,骨子里就有天生的技术崇拜,你写的代码比别人的优雅健壮高性能,你杀BUG比别人快速干净利索,就会受到大家的认可。

很自然地,更多偏向技术偏向深层次的工作就会落到你这里,至少你会有话语权,有选择做哪些事情的权利,是否要一直在围绕着你最熟悉的业务去打滚是由你决定的。

学习武艺成为“武林高手”,是成为大BOSS之后才不必长期面对虾兵蟹将的纠缠。

学习一门具体的技术,也是为了用它解决好问题,然后把它忘掉,去掌握那些更深层次的、更前沿,而且自己还不会的技术。

最后还有个追问和回答如下:

我不服!这开源项目居然才888个星!?

不知道你看到这个回答后的感受是怎么样的。

至少对我而言,振聋发聩。

特别是这两点:

不要轻视不直接产生实践价值的知识;
不要对陷入已经被你熟练掌握的技术中不能自拔。

已经放入手机标签中,时常提醒自己。

与君共勉。

最后说一句

最后再放一次开源项目的地址:

https://icyfenix.cn/

— EOF —

转自:https://mp.weixin.qq.com/s/8ASb7Aod6DAnIyW02TXfUQ

把 Redis 当作队列用,真的合适吗?

作者:Magic Kaito

来源:水滴与银弹

大家好,我是猫哥,今天分享一篇技术干货:

我经常听到很多人讨论,关于「把 Redis 当作队列来用是否合适」的问题。

有些人表示赞成,他们认为 Redis 很轻量,用作队列很方便。

也些人则反对,认为 Redis 会「丢」数据,最好还是用「专业」的队列中间件更稳妥。

究竟哪种方案更好呢?

这篇文章,我就和你聊一聊把 Redis 当作队列,究竟是否合适这个问题。

我会从简单到复杂,一步步带你梳理其中的细节,把这个问题真正的讲清楚。

看完这篇文章后,我希望你对这个问题你会有全新的认识。

在文章的最后,我还会告诉你关于「技术选型」的思路,文章有点长,希望你可以耐心读完。

把 Redis 当作队列用,真的合适吗?

从最简单的开始:List 队列

首先,我们先从最简单的场景开始讲起。

如果你的业务需求足够简单,想把 Redis 当作队列来使用,肯定最先想到的就是使用 List 这个数据类型。

因为 List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。

如果把 List 当作队列,你可以这么来用。

生产者使用 LPUSH 发布消息:

127.0.0.1:6379> LPUSH queue msg1
(integer) 1
127.0.0.1:6379> LPUSH queue msg2
(integer) 2

消费者这一侧,使用 RPOP 拉取消息:

127.0.0.1:6379> RPOP queue
"msg1"
127.0.0.1:6379> RPOP queue
"msg2"

这个模型非常简单,也很容易理解。

把 Redis 当作队列用,真的合适吗?

但这里有个小问题,当队列中已经没有消息了,消费者在执行 RPOP 时,会返回 NULL。

127.0.0.1:6379> RPOP queue
(nil)   // 没消息了

而我们在编写消费者逻辑时,一般是一个「死循环」,这个逻辑需要不断地从队列中拉取消息进行处理,伪代码一般会这么写:

while true:
    msg = redis.rpop("queue")
    // 没有消息,继续循环
    if msg == null:
        continue
    // 处理消息
    handle(msg)

如果此时队列为空,那消费者依旧会频繁拉取消息,这会造成「CPU 空转」,不仅浪费 CPU 资源,还会对 Redis 造成压力。

怎么解决这个问题呢?

也很简单,当队列为空时,我们可以「休眠」一会,再去尝试拉取消息。代码可以修改成这样:

while true:
    msg = redis.rpop("queue")
    // 没有消息,休眠2s
    if msg == null:
        sleep(2)
        continue
    // 处理消息        
    handle(msg)

这就解决了 CPU 空转问题。

这个问题虽然解决了,但又带来另外一个问题:当消费者在休眠等待时,有新消息来了,那消费者处理新消息就会存在「延迟」。

假设设置的休眠时间是 2s,那新消息最多存在 2s 的延迟。

要想缩短这个延迟,只能减小休眠的时间。但休眠时间越小,又有可能引发 CPU 空转问题。

鱼和熊掌不可兼得。

那如何做,既能及时处理新消息,还能避免 CPU 空转呢?

Redis 是否存在这样一种机制:如果队列为空,消费者在拉取消息时就「阻塞等待」,一旦有新消息过来,就通知我的消费者立即处理新消息呢?

幸运的是,Redis 确实提供了「阻塞式」拉取消息的命令:BRPOP / BLPOP,这里的 B 指的是阻塞(Block)。

把 Redis 当作队列用,真的合适吗?

现在,你可以这样来拉取消息了:

while true:
    // 没消息阻塞等待,0表示不设置超时时间
    msg = redis.brpop("queue"0)
    if msg == null:
        continue
    // 处理消息
    handle(msg)

使用 BRPOP 这种阻塞式方式拉取消息时,还支持传入一个「超时时间」,如果设置为 0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回 NULL。

这个方案不错,既兼顾了效率,还避免了 CPU 空转问题,一举两得。

注意:如果设置的超时时间太长,这个连接太久没有活跃过,可能会被 Redis Server 判定为无效连接,之后 Redis Server 会强制把这个客户端踢下线。所以,采用这种方案,客户端要有重连机制。

解决了消息处理不及时的问题,你可以再思考一下,这种队列模型,有什么缺点?

我们一起来分析一下:

  1. 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
  2. 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了

第一个问题是功能上的,使用 List 做消息队列,它仅仅支持最简单的,一组生产者对应一组消费者,不能满足多组生产者和消费者的业务场景。

第二个问题就比较棘手了,因为从 List 中 POP 一条消息出来后,这条消息就会立即从链表中删除了。也就是说,无论消费者是否处理成功,这条消息都没办法再次消费了。

这也意味着,如果消费者在处理消息时异常宕机,那这条消息就相当于丢失了。

针对这 2 个问题怎么解决呢?我们一个个来看。

发布/订阅模型:Pub/Sub

从名字就能看出来,这个模块是 Redis 专门是针对「发布/订阅」这种队列模型设计的。

它正好可以解决前面提到的第一个问题:重复消费。

即多组生产者、消费者的场景,我们来看它是如何做的。

Redis 提供了 PUBLISH / SUBSCRIBE 命令,来完成发布、订阅的操作。

把 Redis 当作队列用,真的合适吗?

假设你想开启 2 个消费者,同时消费同一批数据,就可以按照以下方式来实现。

首先,使用 SUBSCRIBE 命令,启动 2 个消费者,并「订阅」同一个队列。

// 2个消费者 都订阅一个队列
127.0.0.1:6379> SUBSCRIBE queue
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "queue"
3) (integer) 1

此时,2 个消费者都会被阻塞住,等待新消息的到来。

之后,再启动一个生产者,发布一条消息。

127.0.0.1:6379> PUBLISH queue msg1
(integer) 1

这时,2 个消费者就会解除阻塞,收到生产者发来的新消息。

127.0.0.1:6379> SUBSCRIBE queue
// 收到新消息
1) "message"
2) "queue"
3) "msg1"

看到了么,使用 Pub/Sub 这种方案,既支持阻塞式拉取消息,还很好地满足了多组消费者,消费同一批数据的业务需求。

除此之外,Pub/Sub 还提供了「匹配订阅」模式,允许消费者根据一定规则,订阅「多个」自己感兴趣的队列。

// 订阅符合规则的队列
127.0.0.1:6379> PSUBSCRIBE queue.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "queue.*"
3) (integer) 1

这里的消费者,订阅了 queue.* 相关的队列消息。

之后,生产者分别向 queue.p1 和 queue.p2 发布消息。

127.0.0.1:6379> PUBLISH queue.p1 msg1
(integer) 1
127.0.0.1:6379> PUBLISH queue.p2 msg2
(integer) 1

这时再看消费者,它就可以接收到这 2 个生产者的消息了。

127.0.0.1:6379> PSUBSCRIBE queue.*
Reading messages... (press Ctrl-C to quit)
...
// 来自queue.p1的消息
1) "pmessage"
2) "queue.*"
3) "queue.p1"
4) "msg1"

// 来自queue.p2的消息
1) "pmessage"
2) "queue.*"
3) "queue.p2"
4) "msg2"
把 Redis 当作队列用,真的合适吗?

我们可以看到,Pub/Sub 最大的优势就是,支持多组生产者、消费者处理消息。

讲完了它的优点,那它有什么缺点呢?

其实,Pub/Sub 最大问题是:丢数据

如果发生以下场景,就有可能导致数据丢失:

  1. 消费者下线
  2. Redis 宕机
  3. 消息堆积

究竟是怎么回事?

这其实与 Pub/Sub 的实现方式有很大关系。

Pub/Sub 在实现时非常简单,它没有基于任何数据类型,也没有做任何的数据存储,它只是单纯地为生产者、消费者建立「数据转发通道」,把符合规则的数据,从一端转发到另一端。

一个完整的发布、订阅消息处理流程是这样的:

  1. 消费者订阅指定队列,Redis 就会记录一个映射关系:队列->消费者
  2. 生产者向这个队列发布消息,那 Redis 就从映射关系中找出对应的消费者,把消息转发给它
把 Redis 当作队列用,真的合适吗?

看到了么,整个过程中,没有任何的数据存储,一切都是实时转发的。

这种设计方案,就导致了上面提到的那些问题。

例如,如果一个消费者异常挂掉了,它再重新上线后,只能接收新的消息,在下线期间生产者发布的消息,因为找不到消费者,都会被丢弃掉。

如果所有消费者都下线了,那生产者发布的消息,因为找不到任何一个消费者,也会全部「丢弃」。

所以,当你在使用 Pub/Sub 时,一定要注意:消费者必须先订阅队列,生产者才能发布消息,否则消息会丢失。

这也是前面讲例子时,我们让消费者先订阅队列,之后才让生产者发布消息的原因。

另外,因为 Pub/Sub 没有基于任何数据类型实现,所以它也不具备「数据持久化」的能力。

也就是说,Pub/Sub 的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,Pub/Sub 的数据也会全部丢失。

最后,我们来看 Pub/Sub 在处理「消息积压」时,为什么也会丢数据?

当消费者的速度,跟不上生产者时,就会导致数据积压的情况发生。

如果采用 List 当作队列,消息积压时,会导致这个链表很长,最直接的影响就是,Redis 内存会持续增长,直到消费者把所有数据都从链表中取出。

但 Pub/Sub 的处理方式却不一样,当消息积压时,有可能会导致消费失败和消息丢失

这是怎么回事?

还是回到 Pub/Sub 的实现细节上来说。

每个消费者订阅一个队列时,Redis 都会在 Server 上给这个消费者在分配一个「缓冲区」,这个缓冲区其实就是一块内存。

当生产者发布消息时,Redis 先把消息写到对应消费者的缓冲区中。

之后,消费者不断地从缓冲区读取消息,处理消息。

把 Redis 当作队列用,真的合适吗?

但是,问题就出在这个缓冲区上。

因为这个缓冲区其实是有「上限」的(可配置),如果消费者拉取消息很慢,就会造成生产者发布到缓冲区的消息开始积压,缓冲区内存持续增长。

如果超过了缓冲区配置的上限,此时,Redis 就会「强制」把这个消费者踢下线。

这时消费者就会消费失败,也会丢失数据。

如果你有看过 Redis 的配置文件,可以看到这个缓冲区的默认配置:client-output-buffer-limit pubsub 32mb 8mb 60。

它的参数含义如下:

  • 32mb:缓冲区一旦超过 32MB,Redis 直接强制把消费者踢下线
  • 8mb + 60:缓冲区超过 8MB,并且持续 60 秒,Redis 也会把消费者踢下线

Pub/Sub 的这一点特点,是与 List 作队列差异比较大的。

从这里你应该可以看出,List 其实是属于「拉」模型,而 Pub/Sub 其实属于「推」模型

List 中的数据可以一直积压在内存中,消费者什么时候来「拉」都可以。

但 Pub/Sub 是把消息先「推」到消费者在 Redis Server 上的缓冲区中,然后等消费者再来取。

当生产、消费速度不匹配时,就会导致缓冲区的内存开始膨胀,Redis 为了控制缓冲区的上限,所以就有了上面讲到的,强制把消费者踢下线的机制。

好了,现在我们总结一下 Pub/Sub 的优缺点:

  1. 支持发布 / 订阅,支持多组生产者、消费者处理消息
  2. 消费者下线,数据会丢失
  3. 不支持数据持久化,Redis 宕机,数据也会丢失
  4. 消息堆积,缓冲区溢出,消费者会被强制踢下线,数据也会丢失

有没有发现,除了第一个是优点之外,剩下的都是缺点。

所以,很多人看到 Pub/Sub 的特点后,觉得这个功能很「鸡肋」。

也正是以上原因,Pub/Sub 在实际的应用场景中用得并不多。

目前只有哨兵集群和 Redis 实例通信时,采用了 Pub/Sub 的方案,因为哨兵正好符合即时通讯的业务场景。

我们再来看一下,Pub/Sub 有没有解决,消息处理时异常宕机,无法再次消费的问题呢?

其实也不行,Pub/Sub 从缓冲区取走数据之后,数据就从 Redis 缓冲区删除了,消费者发生异常,自然也无法再次重新消费。

好,现在我们重新梳理一下,我们在使用消息队列时的需求。

当我们在使用一个消息队列时,希望它的功能如下:

  • 支持阻塞等待拉取消息
  • 支持发布 / 订阅模式
  • 消费失败,可重新消费,消息不丢失
  • 实例宕机,消息不丢失,数据可持久化
  • 消息可堆积

Redis 除了 List 和 Pub/Sub 之外,还有符合这些要求的数据类型吗?

其实,Redis 的作者也看到了以上这些问题,也一直在朝着这些方向努力着。

Redis 作者在开发 Redis 期间,还另外开发了一个开源项目 disque。

这个项目的定位,就是一个基于内存的分布式消息队列中间件。

但由于种种原因,这个项目一直不温不火。

终于,在 Redis 5.0 版本,作者把 disque 功能移植到了 Redis 中,并给它定义了一个新的数据类型:Stream

下面我们就来看看,它能符合上面提到的这些要求吗?

趋于成熟的队列:Stream

我们来看 Stream 是如何解决上面这些问题的。

我们依旧从简单到复杂,依次来看 Stream 在做消息队列时,是如何处理的?

首先,Stream 通过 XADD 和 XREAD 完成最简单的生产、消费模型:

  • XADD:发布消息
  • XREAD:读取消息

生产者发布 2 条消息:

// *表示让Redis自动生成消息ID
127.0.0.1:6379> XADD queue * name zhangsan
"1618469123380-0"
127.0.0.1:6379> XADD queue * name lisi
"1618469127777-0"

使用 XADD 命令发布消息,其中的「*」表示让 Redis 自动生成唯一的消息 ID。

这个消息 ID 的格式是「时间戳-自增序号」。

消费者拉取消息:

// 从开头读取5条消息,0-0表示从开头读取
127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 0-0
1) 1) "queue"
   2) 1) 1) "1618469123380-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618469127777-0"
         2) 1) "name"
            2) "lisi"

如果想继续拉取消息,需要传入上一条消息的 ID:

127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 1618469127777-0
(nil)

没有消息,Redis 会返回 NULL。

把 Redis 当作队列用,真的合适吗?

以上就是 Stream 最简单的生产、消费。

这里不再重点介绍 Stream 命令的各种参数,我在例子中演示时,凡是大写的单词都是「固定」参数,凡是小写的单词,都是可以自己定义的,例如队列名、消息长度等等,下面的例子规则也是一样,为了方便你理解,这里有必要提醒一下。

下面我们来看,针对前面提到的消息队列要求,Stream 都是如何解决的?

1) Stream 是否支持「阻塞式」拉取消息?

可以的,在读取消息时,只需要增加 BLOCK 参数即可。

// BLOCK 0 表示阻塞等待,不设置超时时间
127.0.0.1:6379> XREAD COUNT 5 BLOCK 0 STREAMS queue 1618469127777-0

这时,消费者就会阻塞等待,直到生产者发布新的消息才会返回。

2) Stream 是否支持发布 / 订阅模式?

也没问题,Stream 通过以下命令完成发布订阅:

  • XGROUP:创建消费者组
  • XREADGROUP:在指定消费组下,开启消费者拉取消息

下面我们来看具体如何做?

首先,生产者依旧发布 2 条消息:

127.0.0.1:6379> XADD queue * name zhangsan
"1618470740565-0"
127.0.0.1:6379> XADD queue * name lisi
"1618470743793-0"

之后,我们想要开启 2 组消费者处理同一批数据,就需要创建 2 个消费者组:

// 创建消费者组1,0-0表示从头拉取消息
127.0.0.1:6379> XGROUP CREATE queue group1 0-0
OK
// 创建消费者组2,0-0表示从头拉取消息
127.0.0.1:6379> XGROUP CREATE queue group2 0-0
OK

消费者组创建好之后,我们可以给每个「消费者组」下面挂一个「消费者」,让它们分别处理同一批数据。

第一个消费组开始消费:

// group1的consumer开始消费,>表示拉取最新数据
127.0.0.1:6379> XREADGROUP GROUP group1 consumer COUNT 5 STREAMS queue >
1) 1) "queue"
   2) 1) 1) "1618470740565-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618470743793-0"
         2) 1) "name"
            2) "lisi"

同样地,第二个消费组开始消费:

// group2的consumer开始消费,>表示拉取最新数据
127.0.0.1:6379> XREADGROUP GROUP group2 consumer COUNT 5 STREAMS queue >
1) 1) "queue"
   2) 1) 1) "1618470740565-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618470743793-0"
         2) 1) "name"
            2) "lisi"

我们可以看到,这 2 组消费者,都可以获取同一批数据进行处理了。

这样一来,就达到了多组消费者「订阅」消费的目的。

把 Redis 当作队列用,真的合适吗?

3) 消息处理时异常,Stream 能否保证消息不丢失,重新消费?

除了上面拉取消息时用到了消息 ID,这里为了保证重新消费,也要用到这个消息 ID。

当一组消费者处理完消息后,需要执行 XACK 命令告知 Redis,这时 Redis 就会把这条消息标记为「处理完成」。

// group1下的 1618472043089-0 消息已处理完成
127.0.0.1:6379> XACK queue group1 1618472043089-0
把 Redis 当作队列用,真的合适吗?

如果消费者异常宕机,肯定不会发送 XACK,那么 Redis 就会依旧保留这条消息。

待这组消费者重新上线后,Redis 就会把之前没有处理成功的数据,重新发给这个消费者。这样一来,即使消费者异常,也不会丢失数据了。

// 消费者重新上线,0-0表示重新拉取未ACK的消息
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS queue 0-0
// 之前没消费成功的数据,依旧可以重新消费
1) 1) "queue"
   2) 1) 1) "1618472043089-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618472045158-0"
         2) 1) "name"
            2) "lisi"

4) Stream 数据会写入到 RDB 和 AOF 做持久化吗?

Stream 是新增加的数据类型,它与其它数据类型一样,每个写操作,也都会写入到 RDB 和 AOF 中。

我们只需要配置好持久化策略,这样的话,就算 Redis 宕机重启,Stream 中的数据也可以从 RDB 或 AOF 中恢复回来。

5) 消息堆积时,Stream 是怎么处理的?

其实,当消息队列发生消息堆积时,一般只有 2 个解决方案:

  1. 生产者限流:避免消费者处理不及时,导致持续积压
  2. 丢弃消息:中间件丢弃旧消息,只保留固定长度的新消息

而 Redis 在实现 Stream 时,采用了第 2 个方案。

在发布消息时,你可以指定队列的最大长度,防止队列积压导致内存爆炸。

// 队列长度最大10000
127.0.0.1:6379> XADD queue MAXLEN 10000 * name zhangsan
"1618473015018-0"

当队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。

这么来看,Stream 在消息积压时,如果指定了最大长度,还是有可能丢失消息的。

除了以上介绍到的命令,Stream 还支持查看消息长度(XLEN)、查看消费者状态(XINFO)等命令,使用也比较简单,你可以查询官方文档了解一下,这里就不过多介绍了。

好了,通过以上介绍,我们可以看到,Redis 的 Stream 几乎覆盖到了消息队列的各种场景,是不是觉得很完美?

既然它的功能这么强大,这是不是意味着,Redis 真的可以作为专业的消息队列中间件来使用呢?

但是还「差一点」,就算 Redis 能做到以上这些,也只是「趋近于」专业的消息队列。

原因在于 Redis 本身的一些问题,如果把其定位成消息队列,还是有些欠缺的。

到这里,就不得不把 Redis 与专业的队列中间件做对比了。

下面我们就来看一下,Redis 在作队列时,到底还有哪些欠缺?

与专业的消息队列对比

其实,一个专业的消息队列,必须要做到两大块:

  1. 消息不丢
  2. 消息可堆积

前面我们讨论的重点,很大篇幅围绕的是第一点展开的。

这里我们换个角度,从一个消息队列的「使用模型」来分析一下,怎么做,才能保证数据不丢?

使用一个消息队列,其实就分为三大块:生产者、队列中间件、消费者

把 Redis 当作队列用,真的合适吗?

消息是否会发生丢失,其重点也就在于以下 3 个环节:

  1. 生产者会不会丢消息?
  2. 消费者会不会丢消息?
  3. 队列中间件会不会丢消息?

1) 生产者会不会丢消息?

当生产者在发布消息时,可能发生以下异常情况:

  1. 消息没发出去:网络故障或其它问题导致发布失败,中间件直接返回失败
  2. 不确定是否发布成功:网络问题导致发布超时,可能数据已发送成功,但读取响应结果超时了

如果是情况 1,消息根本没发出去,那么重新发一次就好了。

如果是情况 2,生产者没办法知道消息到底有没有发成功?所以,为了避免消息丢失,它也只能继续重试,直到发布成功为止。

生产者一般会设定一个最大重试次数,超过上限依旧失败,需要记录日志报警处理。

也就是说,生产者为了避免消息丢失,只能采用失败重试的方式来处理。

但发现没有?这也意味着消息可能会重复发送。

是的,在使用消息队列时,要保证消息不丢,宁可重发,也不能丢弃。

那消费者这边,就需要多做一些逻辑了。

对于敏感业务,当消费者收到重复数据数据时,要设计幂等逻辑,保证业务的正确性。

从这个角度来看,生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。

所以,无论是 Redis 还是专业的队列中间件,生产者在这一点上都是可以保证消息不丢的。

2) 消费者会不会丢消息?

这种情况就是我们前面提到的,消费者拿到消息后,还没处理完成,就异常宕机了,那消费者还能否重新消费失败的消息?

要解决这个问题,消费者在处理完消息后,必须「告知」队列中间件,队列中间件才会把标记已处理,否则仍旧把这些数据发给消费者。

这种方案需要消费者和中间件互相配合,才能保证消费者这一侧的消息不丢。

无论是 Redis 的 Stream,还是专业的队列中间件,例如 RabbitMQ、Kafka,其实都是这么做的。

所以,从这个角度来看,Redis 也是合格的。

3) 队列中间件会不会丢消息?

前面 2 个问题都比较好处理,只要客户端和服务端配合好,就能保证生产端、消费端都不丢消息。

但是,如果队列中间件本身就不可靠呢?

毕竟生产者和消费这都依赖它,如果它不可靠,那么生产者和消费者无论怎么做,都无法保证数据不丢。

在这个方面,Redis 其实没有达到要求。

Redis 在以下 2 个场景下,都会导致数据丢失。

  1. AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能
  2. 主从复制也是异步的,主从切换时,也存在丢失数据的可能(从库还未同步完成主库发来的数据,就被提成主库)

基于以上原因我们可以看到,Redis 本身的无法保证严格的数据完整性

所以,如果把 Redis 当做消息队列,在这方面是有可能导致数据丢失的。

再来看那些专业的消息队列中间件是如何解决这个问题的?

像 RabbitMQ 或 Kafka 这类专业的队列中间件,在使用时,一般是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,以此保证消息的完整性。这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。

也正因为如此,RabbitMQ、Kafka在设计时也更复杂。毕竟,它们是专门针对队列场景设计的。

但 Redis 的定位则不同,它的定位更多是当作缓存来用,它们两者在这个方面肯定是存在差异的。

最后,我们来看消息积压怎么办?

4) 消息积压怎么办?

因为 Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。

所以,Redis 的 Stream 提供了可以指定队列最大长度的功能,就是为了避免这种情况发生。

但 Kafka、RabbitMQ 这类消息队列就不一样了,它们的数据都会存储在磁盘上,磁盘的成本要比内存小得多,当消息积压时,无非就是多占用一些磁盘空间,相比于内存,在面对积压时也会更加「坦然」。

综上,我们可以看到,把 Redis 当作队列来使用时,始终面临的 2 个问题:

  1. Redis 本身可能会丢数据
  2. 面对消息积压,Redis 内存资源紧张

到这里,Redis 是否可以用作队列,我想这个答案你应该会比较清晰了。

如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。

而且,Redis 相比于 Kafka、RabbitMQ,部署和运维也更加轻量。

如果你的业务场景对于数据丢失非常敏感,而且写入量非常大,消息积压时会占用很多的机器资源,那么我建议你使用专业的消息队列中间件。

总结

好了,总结一下。这篇文章我们从「Redis 能否用作队列」这个角度出发,介绍了 List、Pub/Sub、Stream 在做队列的使用方式,以及它们各自的优劣。

之后又把 Redis 和专业的消息队列中间件做对比,发现 Redis 的不足之处。

最后,我们得出 Redis 做队列的合适场景。

这里我也列了一个表格,总结了它们各自的优缺点。

把 Redis 当作队列用,真的合适吗?

后记

最后,我想和你再聊一聊关于「技术方案选型」的问题。

你应该也看到了,这篇文章虽然始于 Redis,但并不止于 Redis。

我们在分析 Redis 细节时,一直在提出问题,然后寻找更好的解决方案,在文章最后,又聊到一个专业的消息队列应该怎么做。

其实,我们在讨论技术选型时,就是一个关于如何取舍的问题。

而这里我想传达给你的信息是,在面对技术选型时,不要不经过思考就觉得哪个方案好,哪个方案不好

你需要根据具体场景具体分析,这里我把这个分析过程分为 2 个层面:

  1. 业务功能角度
  2. 技术资源角度

这篇文章所讲到的内容,都是以业务功能角度出发做决策的。

但这里的第二点,从技术资源角度出发,其实也很重要。

技术资源的角度是说,你所处的公司环境、技术资源能否匹配这些技术方案

这个怎么解释呢?

简单来讲,就是你所在的公司、团队,是否有匹配的资源能 hold 住这些技术方案。

我们都知道 Kafka、RabbitMQ 是非常专业的消息中间件,但它们的部署和运维,相比于 Redis 来说,也会更复杂一些。

如果你在一个大公司,公司本身就有优秀的运维团队,那么使用这些中间件肯定没问题,因为有足够优秀的人能 hold 住这些中间件,公司也会投入人力和时间在这个方向上。

但如果你是在一个初创公司,业务正处在快速发展期,暂时没有能 hold 住这些中间件的团队和人,如果贸然使用这些组件,当发生故障时,排查问题也会变得很困难,甚至会阻碍业务的发展。

而这种情形下,如果公司的技术人员对于 Redis 都很熟,综合评估来看,Redis 也基本可以满足业务 90% 的需求,那当下选择 Redis 未必不是一个好的决策。

所以,做技术选型不只是技术问题,还与人、团队、管理、组织结构有关

也正是因为这些原因,当你在和别人讨论技术选型问题时,你会发现每个公司的做法都不相同。

毕竟每个公司所处的环境和文化不一样,做出的决策当然就会各有差异。

如果你不了解这其中的逻辑,那在做技术选型时,只会趋于表面现象,无法深入到问题根源。

而一旦你理解了这个逻辑,那么你在看待这个问题时,不仅对于技术会有更加深刻认识,对技术资源和人的把握,也会更加清晰。

希望你以后在做技术选型时,能够把这些因素也考虑在内,这对你的技术成长之路也是非常有帮助的。

转自:https://mp.weixin.qq.com/s/l1dMnu6laOLm375mMxy3WQ

数据中台为什么要建标签体系,分类它不香吗?

来源:谈数据,作者:石秀峰
全文共3678个字,建议阅读6分钟
数据中台为什么要建标签体系,分类它不香吗?
在众多的数据中台的解决方案中,一个叫做“标签中心”或“标签体系”的应用,几乎成了数据中台的“标配”。
乍一看,标签体系就像一个树状的分类。
因此,有专家吐槽:“现在讲啥数据标签,数据类目,跟SAP Classification比真是弱爆了”。
数据中台为什么要建标签体系,分类它不香吗?

注:SAP Classification为SAP产品中的一个分类系统,它支持以有序的结构将特定对象(例如,文章和站点)组合在一起。它的基本功能是为各种不同类型对象的所有属性特征提供存储库。然后将相似特征的对象分组到类中(分类对象),以便系统可以更容易地在事务中找到它们。

那数据标签到底是什么?它与数据分类体系相比,是“真的香”,还是“弱爆了”?
 

01. 概念的澄清

无论是在数据中台,还是传统产品设计中,绕不开的几个概念:分类、属性、标签。
1、什么是分类
分类,就是指按照种类、等级、性质或特征的归类。也就是把相同属性或特征的“对象”归集在一起,形成不同的类别,方便人们通过类别来对“对象”进行的查询、识别、管理和使用。“对象”可以是人、产品、物料或其他实体,例如:人可以分为男人、女人,也可以分为老年人、中年人、青少年。
2、什么是属性
属性是事物所具有的性质或特征,重点强调的是事物本身,例如:人的性别、身高、胖瘦、年龄、性格等都是人的属性。可以将某个事物的属性抽象出来作为事物的分类,如我们上边举的例子,男人、女人是按照性别属性对人的分类;老年人、中年人、青少年是按照年龄属性对人的分类。事物可以按照属性来分类,分类也是事物的属性之一。
3、什么是标签

标签,原意是标明物品的品名、重量、体积、用途等信息的简要标牌,例如:商品标签、图书标签、车检标签、文件标签、服装吊牌等。从这个概念衍生到网络标签,是人工或系统自动或用户自发的,通过相关性很强的关键字对事物或内容进行描述,帮助人们分类内容,以便于检索和分享。例如:我们也可以给“人”这个对象打上男人或女人,老人或青年的标签。可见,标签也有维度或分类,而属性也是一种标签。

 

02. 分类VS标签,到底有啥区别?

前边的例子中,男人、女人,老人、青年,这既可以是对人的分类,也可以是人的标签。分类和标签有时候没有明确的分界线,分类可以作为一个标签,标签也可以抽象出分类。那么,分类和标签到底有什么不同?
关于这个问题,知乎上网友的一个回答挺有意思的:
分类由于只能隶属于一个,所以往往带有武断和不恰当的色彩,它是一个“is a”的问题,属于本质论的范畴,而对事物的“本质”的认定,严格来说,这事只有上帝才能做,换句话说,谁做都不合适。标签则不同,它是一个has a”的问题,说某个东西有某种属性,要求就没那么严格了。
笔者理解:分类是一种严谨的数据组织方式,一般按照一个或多个维度自上而下、从整体到明细的穷举,遵循“相互独立,完全穷举” 的原则。而标签是一种灵活的数据组织方式,放弃大而全的框架,基于业务场景自下而上地倒推标签需求。
除此之外,分类和标签还存在如下不同点:
1、分类一般是面向团队或组织的,注重标准化;而标签可以面向组织,也可以是面向个人,注重的是个性化。
2、分类具有排他性,分类之间是独立的、不能交叉;而标签允许交叉,标签之间可以相互关联、相互依赖。
3、分类体系需要事先规划,在标准化的框架下进行使用;而标签可以静态的,也可以是动态的,允许随时添加。
4、分类注重结构化,具有层级控制,是一个树状结构;而标签的结构是松散、灵活、开放的,整体看是一个网状结构。
数据中台为什么要建标签体系,分类它不香吗?

来源:PMCAFF产品社区《知识管理——如何使用印象笔记的多级目录标签做知识分类管理?》

 

03. 分类VS标签,哪个“香”?

在数字化应用中,分类和标签其实根本无法分出个伯仲。标签体系也需要一定的分类,而基于某个维度的分类也可以作为一种标签,只是他们支持的应用场景或有不同。
分类一直存在,它源于人的认知,可以追溯到人类的起源。
在几百万年前的原始部落,人们将打回来的猎物、采集回来的种子进行分类存放,以便存储和管理。分类的作用自然不必多说,它可以让我们条理清晰,层次清晰,一目了然的识别和管理事物。
即使在当今由互联网、计算机构成的数字化世界中,分类体系也一直是我们组织数据、处理数据、查询数据、管理数据的一个重要的手段。
数据中台为什么要建标签体系,分类它不香吗?
在传统企业的信息化建设中,使用更多的是分类,例如:ERP中的物料分类、人员分类、客商分类等。一套科学、严谨的分类体系是企业的“人、财、物、进、销、存”等业务流程有效管理的基础,在企业管理中发挥着重要的作用。一家企业的“物料分类”科不科学、规不规范,在很大程度上能够反映这家企业的管理的规范性情况和精细化程度。上文我们提到的SAP Classification就是这样一个可以根据事物属性特征进行多维分类体系的系统。不仅SAP,像Oracle、用友、金蝶等ERP大厂,同样也有类似的多维数据分类体系。
标签(网络标签)是随着互联网发展产生的,最早用在博客、文章的内容分类中,方便用户管理和聚合内容。随着大数据的发展,标签体系的作用也越来越大,被互联网企业广泛使用,通过特征集合并关联打标签的对象,对分析对象生成画像,挖掘对象的价值。例如:各大互联网APP(淘宝、今日头条、抖音等)都有一个基于标签体系的推荐引擎模块,通过用户静态属性和行为属性给用户打标签,形成360度用户画像,然后根据用户的偏好将信息或产品推送给用户。
因此,分类VS标签,根本无法区别那个更“强”或哪个更“弱”,根据不同的应用场景选择不同的体系就好了。

在企业的数字化建设中,需要分类和标签结合使用,才能最大化发挥数据效能、挖掘数据价值。

 

04. 数据中台为什么需要标签体系

上文中我们说过,传统企业的信息化使用更多的是分类体系,而标签体系是互联网企业的“致胜法宝”。
随着数字化时代的到来,掀起了传统企业数字化转型的浪潮。从技术侧来看,转型就是拥抱互联网,战略性的使用数据和IT。在这场数字化的变革中,数据中台被认为是赋能企业数字化转型,实现降本增效的新引擎。
数据中台为什么要建标签体系,分类它不香吗?
来源:36氪研究院《2020年中国服装行业数据中台研究报告》
关于数据中台的文章,笔者也写了很多了,我们再回顾一下它的概念:数据中台是数据+技术+产品+组织的有机组合,是快、准、全、统、通的智能大数据体系。与数据仓库等传统数据工具相比,数据中台是一种新的理念,以“技术+业务”为双驱动,是企业开展新型运营的一个中枢系统。
因此,如果你将数据中台定位成一个存数据、管数据的技术平台,那或许有“分类体系”就可以了。如果你的数据中台定位的是企业数字化转型的运营中枢系统,要实现对前端业务的支持和赋能,那“标签体系”就是数据中台一个标配。原因前边介绍过,分类是自上而下的规划,侧重标准化,标签是自下而上的倒推,注重业务场景。

“数字转型,场景为王”,在“技术+业务”双驱模式的数据中台中,标签体系、数据萃取将助力企业运营转型升级。

 

05. 数据中台生产数据标签的四个步骤

在经历过“追捧”和“质疑”等种种考验后,数据中台在基于业务驱动的标签体系助力下,正在用事实验证其价值。关于数据中台标签体系的建设,可以关注公众号(谈数据),阅读《数据中台:基于标签体系的360°用户画像》一文。
下面我们简单介绍下数据中台如何通过加工和生产数据标签,从而助力企业实现数字化转型的,这个过程大致可分为四个步骤:
第一,需要从业务视角对企业数据进行梳理,并将各业务域、各渠道、各类型的数据进行采集和汇聚。
第二,通过对数据进行分类处理,从中提炼出可复用的行为元素(业务线、实体对象、实体属性、动作等),通过沉淀行为元素,可以更好地规范来源数据。这里业务线是在不同业务运营线,例如:某造纸公司有生活纸运营线,文化纸运营线、工业纸运营线、特种纸运营线等。实体对象是指操作和被操作的各商业主体,例如:用户、产品等。实体属性是指实体对象的属性特征信息,例如:用户的年龄、性别、喜好等。动作就是主体发出的操作,例如:询价、购买、浏览等。
第三,根据对象的行为元素给对象打上相应的“标签”,以支持信息查询、信息推送等应用。与传统博客、CMS(文章管理系统)的手动给内容打标签不同,数据中台是根据对象的行为规则自动给对象打标签,并且可以设置行为数据的时间衰减算法,为不同标签分配不同的权重,形成全面的“用户画像”,做到“比用户自己还了解他自己”。
第四,各相关应用直接调用数据中台的标签体系、画像服务,支持企业的精准营销、个性推荐、渠道优化、产品创新等应用场景。
其中,执行第二、第三个步骤的前提就是要做好标签类目体系的规划。也就是说,标签体系也是具有一定的分类结构的。例如:用户标签可以按照标签的控制深度划分为基本属性标签、行为属性标签、商业属性标签等;也可以按照数据中台的数据分层结构,分为事实标签、模型标签、预测标签等。请参考《数据中台:基于标签体系的360°用户画像》

数据中台为什么要建标签体系,分类它不香吗?

写在最后的话

分类体系和标签体系虽各有各的适用场景和侧重,但它们都是企业数据中台理念落地的关键要素,帮助企业沉淀数据资产化,优化数据服务,助力企业落地数字化转型。分类体系能够帮助企业科学、有效的组织数据,规划标签类目;标签体系能够萃取和精炼数据服务,并对数据分类进行反向优化。在企业数据的管理和应用中,它们相互配合、互为支撑,不仅能够让企业有数可查,有数可用,而且能够让企业的数据用的更好。

转自:https://mp.weixin.qq.com/s/ZbFnpgymVyAvtwzmAvSk3A

你管这破玩意儿叫负载均衡?

相信大家都听过这样的一道经典面试题:「请说出在淘宝网输入一个关键词到最终展示网页的整个流程,越详细越好」

这个问题很难,涉及到 HTTP,TCP,网关,LVS 等一系列相关的概念及诸多协议的工作机制,如果你能掌握到这其中的每个知识点,那将极大地点亮你的技能树,对于网络是如何运作也会了然于胸,即便不能完全掌握,但知道流量怎么流转的对你排查定位问题会大有帮助,我之前就利用这些知识定位到不少问题,为了弄清楚整个流程,我查阅了很多资料,相信应该可以把这个问题讲明白,不过写着写着发现篇幅实在太长,所以分为上下两篇来分别介绍一下,本篇先介绍流量在后端的的整体架构图,下一篇会深入剖析各个细节点,如 LVS ,NAT 的工作细节等,这其中会涉及到交换机,路由器的工作机制等知识点,相信大家看了肯定有帮助

李大牛创业了,由于前期没啥流量,所以他只部署了一台 tomcat server,让客户端将请求直接打到这台 server 上

你管这破玩意儿叫负载均衡?

这样部署一开始也没啥问题,因为业务量不是很大,单机足以扛住,但后来李大牛的业务踩中了风口,业务迅猛发展,于是单机的性能逐渐遇到了瓶颈,而且由于只部署了一台机器,这台机器挂掉了业务也就跌零了,这可不行,所以为了避免单机性能瓶颈与解决单点故障的隐患,李大牛决定多部署几台机器(假设为三台),这样可以让 client 随机打向其中的一台机器,这样就算其中一台机器挂了,另外的机器还存活,让 client 打向其它没有宕机的机器即可

你管这破玩意儿叫负载均衡?

现在问题来了,client 到底该打向这三台机器的哪一台呢,如果让 client 来选择肯定不合适,因为如果让 client 来选择具体的 server,那么它必须知道有哪几台 server,然后再用轮询等方式随机连接其中一台机器,但如果其中某台 server 宕机了,client 是无法提前感知到的,那么很可能 client 会连接到这台挂掉的 server 上,所以选择哪台机器来连接的工作最好放在 server 中,具体怎么做呢,在架构设计中有个经典的共识:没有什么是加一层解决不了的,如果有那就再加一层,所以我们在 server 端再加一层,将其命名为 LB(Load Balance,负载均衡),由 LB 统一接收 client 的请求,然后再由它来决定具体与哪一个 server 通信,一般业界普遍使用 Nginx 作为 LB

你管这破玩意儿叫负载均衡?

采用这样的架构设计总算支撑了业务的快速增长,但随后不久李大牛发现这样的架构有点问题:所有的流量都能打到  server 上,这显然是有问题的,不太安全,那能不能在流量打到 server 前再做一层鉴权操作呢,鉴权通过了我们才让它打到 server 上,我们把这一层叫做网关(为了避免单点故障,网关也要以集群的形式存在)

你管这破玩意儿叫负载均衡?

这样的话所有的流量在打到  server 前都要经过网关这一层,鉴权通过后才把流量转发到 server 中,否则就向 client 返回报错信息,除了鉴权外,网关还起到风控(防止羊毛党),协议转换(比如将 HTTP 转换成 Dubbo),流量控制等功能,以最大程度地保证转发给 server 的流量是安全的,可控的。

这样的设计持续了很长一段时间,但是后来李大牛发现这样的设计其实还是有问题,不管是动态请求,还是静态资源(如 js,css文件)请求都打到 tomcat 了,这样在流量大时会造成 tomcat 承受极大的压力,其实对于静态资源的处理 tomcat 不如 Nginx,tomcat 每次都要从磁盘加载文件比较影响性能,而 Nginx 有 proxy  cache 等功能可以极大提升对静态资源的处理能力。

画外音:所谓的 proxy cache 是指 nginx 从静态资源服务器上获取资源后会缓存在本地的内存+磁盘中,下次请求如果命中缓存就从 Nginx 本机的 Cache 中直接返回了

所以李大牛又作了如下优化:如果是动态请求,则经过 gateway 打到 tomcat,如果是 Nginx,则打到静态资源服务器上

你管这破玩意儿叫负载均衡?

这就是我们所说的动静分离,将静态请求与动态请求分开,这样 tomcat 就可以专注于处理其擅长的动态请求,而静态资源由于利用到了 Nginx 的  proxy cache 等功能,后端的处理能力又上了一个台阶。

另外需要注意的是并不是所有的动态请求都需要经过网关,像我们的运营中心后台由于是内部员工使用的,所以它的鉴权与网关的 api 鉴权并不相同,所以我们直接部署了两台运营中心的 server ,直接让 Nginx 将运营中心的请求打到了这两台 server 上,绕过了网关。

你管这破玩意儿叫负载均衡?

当然为了避免单点故障 Nginx 也需要部署至少两台机器,于是我们的架构变成了下面这样,Nginx 部署两台,以主备的形式存在,备 Nginx 会通过 keepalived 机制(发送心跳包) 来及时感知到主 Nginx 的存活,发现宕机自己就顶上充当主 Nginx 的角色

你管这破玩意儿叫负载均衡?

看起来这样的架构确实不错,但要注意的是 Nginx 是七层(即应用 层)负载均衡器 ,这意味着如果它要转发流量首先得和 client 建立一个 TCP 连接,并且转发的时候也要与转发到的上游 server 建立一个 TCP 连接,而我们知道建立 TCP 连接其实是需要耗费内存(TCP Socket,接收/发送缓存区等需要占用内存)的,客户端和上游服务器要发送数据都需要先发送暂存到到 Nginx 再经由另一端的 TCP 连接传给对方。

你管这破玩意儿叫负载均衡?

所以 Nginx 的负载能力受限于机器I/O,CPU内存等一系列配置,一旦连接很多(比如达到百万)的话,Nginx 抗负载能力就会急遽下降。

经过分析可知 Nginx 的负载能力较差主要是因为它是七层负载均衡器必须要在上下游分别建立两个 TCP 所致,那么是否能设计一个类似路由器那样的只负载转发包但不需要建立连接的负载均衡器呢,这样由于不需要建立连接,只负责转发包,不需要维护额外的 TCP 连接,它的负载能力必然大大提升,于是四层负载均衡器 LVS 就诞生了,简单对比下两者的区别

你管这破玩意儿叫负载均衡?

可以看到  LVS 只是单纯地转发包,不需要和上下游建立连接即可转发包,相比于 Nginx 它的抗负载能力强、性能高(能达到 F5 硬件的 60%),对内存和cpu资源消耗比较低

那么四层负载均衡器是如何工作的呢

负载均衡设备在接收到第一个来自客户端的SYN 请求时,即通过负载均衡算法选择一个最佳的服务器,并对报文中目标IP地址进行修改(改为后端服务器 IP ),直接转发给该服务器。TCP 的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下,为保证服务器回包可以正确返回给负载均衡设备,在转发报文的同时可能还会对报文原来的源地址进行修改。

综上所述,我们在 Nginx 上再加了一层 LVS,以让它来承接我们的所有流量,当然为了保证 LVS 的可用性,我们也采用主备的方式部署 LVS,另外采用这种架构如果 Nginx 容量不够我们可以很方便地进行水平扩容,于是我们的架构改进如下:

你管这破玩意儿叫负载均衡?

当然只有一台 LVS 的话在流量很大的情况下也是扛不住的,怎么办,多加几台啊,使用 DNS 负载均衡,在 DNS 服务器解析域名的时候随机打到其中一台 LVS 不就行了

你管这破玩意儿叫负载均衡?

通过这样的方式终于可以让流量稳定流转了,有个点可能一些朋友会有疑问,下面我们一起来看看

既然 LVS 可以采用部署多台的形式来避免单点故障,那 Nginx 也可以啊,而且 Nginx 在 1.9 之后也开始支持四层负载均衡了,所以貌似 LVS 不是很有必要?

如果不用 LVS 则架构图是这样的

你管这破玩意儿叫负载均衡?

通过部署多台 Nginx 的方式在流量不是那么大的时候确实是可行,但 LVS 是 Linux 的内核模块,工作在内核态,而 Nginx 工作在用户态,也相对比较重,所以在性能和稳定性上 Nginx 是不如 LVS 的,这就是为什么我们要采用 LVS + Nginx 的部署方式。

另外相信大家也注意到了,如果流量很大时,静态资源应该部署在 CDN 上, CDN 会自动选择离用户最近的节点返回给用户,所以我们最终的架构改进如下

你管这破玩意儿叫负载均衡?

总结

架构一定要结合业务的实际情况来设计,脱离业务谈架构其实是耍流氓,可以看到上文每一个架构的衍化都与我们的业务发展息息相关,对于中小型流量没有那么大的公司,其实用 Nginx 作为负载均衡足够,在流量迅猛增长后则考虑使用 lvs+nginx,当然像美团这样的巨量流量(数十 Gbps的流量、上千万的并发连接),lvs 也不管用了(实测虽然使用了 lvs 但依然出现了不少丢包的现象)所以它们开发出了自己的一套四层负载均衡器 MGW

另外看了本文相信大家对分层的概念应该有更透彻的理解,没有什么是分层解决不了的事,如果有,那就再多加一层,分层使每个模块各司其职,功能解藕,而且方便扩展,大家很熟悉的 TCP/IP 就是个很好的例子,每层只管负责自己的事,至于下层是什么实现的上层是不 care 的

以上就是本文的全部内容,希望大家看了有收获

– EOF –

一步步实现 Redis 搜索引擎

场景

大家如果是做后端开发的,想必都实现过列表查询的接口,当然有的查询条件很简单,一条 SQL 就搞定了,但有的查询条件极其复杂,再加上库表中设计的各种不合理,导致查询接口特别难写,然后加班什么的就不用说了(不知各位有没有这种感受呢~)。

下面以一个例子开始,这是某购物网站的搜索条件,如果让你实现这样的一个搜索接口,你会如何实现?(当然你说借助搜索引擎,像 Elasticsearch 之类的,你完全可以实现。但我这里想说的是,如果要你自己实现呢?

一步步实现 Redis 搜索引擎

从上图中可以看出,搜索总共分为6大类,每大类中又分了各个子类。这中间,各大类条件之间是取的交集,各子类中有单选、多选、以及自定义的情况,最终输出符合条件的结果集。

好了,既然需求很明确了,我们就开始来实现。

 

实现1

率先登场是小A同学,他是写 SQL 方面的“专家”。小A信心满满的说:“不就是一个查询接口吗?看着条件很多,但凭着我丰富的 SQL 经验,这点还是难不倒我的。”

于是乎就写出了下面这段代码(这里以 MYSQL 为例):

select ... from table_1left join table_2left join table_3left join (select ... from table_x where ...) tmp_1...where ...order by ...limit m,n

代码在测试环境跑了一把,结果好像都匹配上了,于是准备上预发。这一上预发,问题就开始暴露出来。预发为了尽可能的逼真线上环境,所以数据量自然而然要比测试大的多。所以这么一个复杂的 SQL,它的执行效率可想而知。测试同学果断把小A的代码给打了回来。

 

实现2

总结了小A失败的教训,小B开始对SQL进行了优化,先是通过了explain关键字进行SQL性能分析,对该加索引的地方都加上了索引。同时将一条复杂SQL拆分成了多条SQL,计算结果在程序内存中进行计算。

伪代码如下:

$result_1 = query('select ... from table_1 where ...');$result_2 = query('select ... from table_2 where ...');$result_3 = query('select ... from table_3 where ...');...
$result = array_intersect($result_1, $result_2, $result_3, ...);

这种方案从性能上明显比第一种要好很多,可是在功能验收的时候,产品经理还是觉得查询速度不够快。小B自己也知道,每次查询都会向数据库查询多次,而且有些历史原因,部分条件是做不到单表查询的,所以查询等待的时间是避免不了的。

 

实现3

小C从上面的方案中看到了优化的空间。他发现小B在思路上是没问题的,将复杂条件拆分,计算各个子维度的结果集,最后将所有的子结果集进行一个汇总合并,得到最终想要的结果。

于是他突发奇想,能否事先将各个子维度的结果集给缓存起来,这要查询的时候直接去取想要的子集,而不用每次去查库计算。

这里小C采用 Redis 来存储缓存数据,用它的主要原因是,它提供了多种数据结构,并且在 Redis 中进行集合的交并集操作是一件很容易的事情。

具体方案,如图所示:

一步步实现 Redis 搜索引擎

这里每个条件都事先将计算好的结果集ID存入对应的key中,选用的数据结构是集合(Set)。查询操作包括:

  • 子类单选:直接根据条件 key,获取对应结果集;
  • 子类多选:根据多个条件 Key,进行并集操作,获取对应结果集;
  • 最终结果:将获取的所有子类结果集进行交集操作,得到最终结果;

这其实就是所谓的反向索引。

这里会发现,漏了一个价格的条件。从需求中可知,价格条件是个区间,并且是无穷举的。所以上述的这种穷举条件的 Key-Value 方式是做不到的。这里我们采用 Redis 的另一种数据结构进行实现,有序集合(Sorted Set):

一步步实现 Redis 搜索引擎

将所有商品加入 Key 为价格的有序集合中,值为商品ID,每个值对应的分数为商品价格的数值。这样在 Redis 的有序集合中就可以通过ZRANGEBYSCORE命令,根据分数(价格)区间,获取相应结果集。

至此,方案三的优化已全部结束,将数据的查询与计算通过缓存的手段,进行了分离。在每次查找时,只需要简单的查找 Redis 几次就能得出结果。查询速度上符合了验收的要求。

 

扩展

  • 分页

这里你或许发现了一个严重的功能缺陷,列表查询怎么能没有分页。是的,我们马上来看 Redis 是如何实现分页的。

分页主要涉及排序,这里简单起见,就以创建时间为例。

如图所示:

一步步实现 Redis 搜索引擎

图中蓝色部分是以创建时间为分值的商品有序集合,蓝色下方的结果集即为条件计算而得的结果,通过ZINTERSTORE命令,赋结果集权重为0,商品时间结果为1,取交集而得的结果集赋予创建时间分值的新有序集合。对新结果集的操作即能得到分页所需的各个数据:

  • 页面总数为:ZCOUNT命令
  • 当前页内容:ZRANGE命令
  • 若以倒序排列:ZREVRANGE命令
  • 数据更新

关于索引数据更新的问题,有两种方式来进行。一种是通过商品数据的修改,来即时触发更新操作,一种是通过定时脚本来进行批量更新。这里要注意的是,关于索引内容的更新,如果暴力的删除 Key,再重新设置 Key。因为 Redis 中两个操作不会是原子性进行的,所以中间可能存在空白间隙,建议采用仅移除集合中失效元素,添加新元素的方式进行。

 

性能优化

Redis 是内存级操作,所以单次的查询会很快。但是如果我们的实现中会进行多次的 Redis 操作,Redis 的多次连接时间可能是不必要时间消耗。通过使用MULTI命令,开启一个事务,将 Redis 的多次操作放在一个事务中,最后通过EXEC来进行原子性执行(注意:这里所谓的事务,只是将多个操作在一次连接中执行,如果执行过程中遇到失败,是不会回滚的)。

 

总结

这里只是一个采用 Redis 优化查询搜索的一个简单 Demo,和现有的开源搜索引擎相比,它更轻量,学习成本页相应低些。其次,它的一些思想与开源搜索引擎是类似的,如果再加上词语解析,也可以实现类似全文检索的功能。

·END·


作者:jasonGeng88

来源:https://github.com/jasonGeng88/blog/blob/master/201706/redis-search.md