jsyzchen

jsyzchen 查看完整档案

北京编辑南京信息工程大学  |  计算机系 编辑百度  |  高级研发工程师 编辑 www.jsyzchen.com 编辑
编辑

人都是被逼出来的,哪有压迫哪就有动力。

个人动态

jsyzchen 收藏了文章 · 10月9日

架构制图:工具与方法论

简介: 软件工程也是工程,因此传统工程制图的一些基本理论,在软件行业同样适用。但另一方面,软件与实体制造业之间还是有着本质区别,所以在制图方面的需求和方式也大相径庭,无法直接套用。作为软件行业的从业者,你可以完全不懂工程制图,但你不得不懂架构制图 —— 这是任何程序员职业生涯的的必修课。

1.png

作者 | 楚衡

前言

“架构制图”这词乍一听似乎有些晦涩,但如果提起“工程制图”,相信绝大部分工科背景的程序员们都不会陌生,甚至还能共同感慨下那些年一起伏在宿舍左手圆规,右手直尺,徒手作图到深夜的日子。

软件工程也是工程,因此传统工程制图的一些基本理论,在软件行业同样适用。但另一方面,软件与实体制造业之间还是有着本质区别,所以在制图方面的需求和方式也大相径庭,无法直接套用。作为软件行业的从业者,你可以完全不懂工程制图,但你不得不懂架构制图 —— 这是任何程序员职业生涯的的必修课。

本文在后半段将介绍如何用图去描述(describe)和传达(communicate)你的架构设计。值得强调的是,本文并不会侧重于单一的方法和工具,而是更希望关注那些优秀方法背后的通用方法论,即架构制图的本质共性最佳实践。希望本文能起到引子作用,激发大家对自己日常工作中关于架构和制图部分的关注、审视与思考;如果还真能帮助大家提升一点点制图效率和效果,那就更好不过了。

2.png

什么是软件架构?

  1. 软件架构定义

3.png

IEEE 给出的定义:架构是环境中该系统的一组基础概念(concepts)属性(properties),具体表现就是它的元素(elements)关系(relationships),以及设计与演进的基本原则(principles)

CMU 软件工程研究院的定义:架构是用于推演出该系统的一组结构(structures),具体是由软件元素(elements)、元素之间的关系(relationships),以及各自的属性(properties)共同组成。

Uncle Bob 在 Clean Architecture 一书中给出的定义:架构是创建者给予该系统的形态(shape)。这个形态的具体形式来源于对系统组件(components)划分排列,以及这些组件之间互相通讯的方式。

  1. 架构核心要素

4.png

综合上述各种权威定义,软件系统的架构通常需要包含如下四类核心要素:

  • 元素(elements):将系统拆分为一组元素 - 模块、组件、结构体、子系统;
  • 关系(relationships):不同元素之间的关系 - 交互、依赖 、继承、组合、聚合;
  • 属性(properties):每个元素具备的属性 - 名称、职责、接口、实现限制等;
  • 原理(principles):为什么这么设计 - 拆分依据、设计原则、决策原因等。

为什么架构很重要?

  1. 架构是系统实现的蓝图

5.png

最近有部很火的网剧叫《摩天大楼》,讲述了一段匪夷所思的悬疑故事。为什么扯这个呢?因为我想借用这个剧的标题来问个问题:摩天大楼是由谁建起来的?也许你心里会默念:废话,不就是建筑工人们一砖一瓦堆起来的嘛。仔细再想想?背后是不是还有一堆操碎了心的建筑设计师(比如剧中帅气的林大森)和土木工程师们?他们虽然不搬砖也不扛水泥,但如果没有他们产出的那些繁琐严谨的设计图纸,摩天大楼是是不可能像农村自建房一样仅凭工人们各自的经验与想象力就能快速平稳地竖立起来的。

正是靠着这些图纸所描绘出来的工程蓝图(blueprints),才让成百上千工人们的分工合作和验收标准有了依据:大家只需要照着蓝图,按部就班地把自己所负责的那些砖瓦添上去就行了;只要蓝图正确,且施工过程也没有偏差,最终顺利完工只是个时间问题。

与建筑、汽车或者任何其他工程行业一样,软件在落地实现(编码)之前也需要先有蓝图;而其中最重要的一份蓝图,就是架构设计。没有架构,仅凭程序员自己脑子里的模糊设想,也许你可以像传统手艺人一样独自创造出一些美好有用的小东西(比如 Linux 0.01 版本),但不太可能以工程的方式协同一个团队共同建造起一个与摩天大楼规模类似的复杂软件系统(比如现代的 Linux 系统)。一方面,人类的思维能力终归有限,必须依靠架构这种高度抽象和简化的蓝图,才能让复杂系统的创造、理解、分析和治理变得可行;另一方面,量级达到一定程度的大型系统,也只能依靠多人分工合作才能完成,而架构也正是多人沟通协作的重要基础。

  1. 架构是沟通协作的基础

6.png
7.png

软件项目的最终价值产出就是软件系统,而架构作为软件系统的灵魂和骨架,可以起到如下作用:

  • 理解对齐:所有软件系统的目的都是为了实现用户需求,但实现的途径有无限种可能性(相比传统工程行业,软件的灵活性更大、知识迭代更快)。架构设计就是去选择其中一条最合适的实现途径,因此其中会涉及非常多关键的选路决策(为什么要这么拆分?为什么选择 A 技术而不是 B?)。这些重要的技术决策需要通过架构描述这种形式被记录和同步,才能让项目组所有成员对整个系统的理解对齐,形成共识
  • 工作量化:项目管理最重要的步骤之一就是工时评估,它是确定项目排期和里程碑的直接依据。显然,只通过 PRD / 交互图是无法科学量化出项目工作量的,因为很难直观判断出一句简短需求或一个简单页面背后,究竟要写多少代码、实现起来难度有多大。有了清晰明确的架构之后,理论上绝大部分开发工作都能做到可见可预测可拆解,自然而然也就能够被更准确地量化。当然,精准的工作量评估在 IT 行业内也一直是个未解之谜,实际的工期会受太多未知因素影响,包括程序员的技能熟练度、心情好不好、有没有吃饱等。
  • 标准术语:编程作为一种具有创造力的工作,从某种角度看跟写科幻小说是类似的。好的科幻小说都喜欢造概念,比如三体中的智子,如果没看过小说肯定不知道这是个啥玩意儿。软件系统在造概念这一点上,相比科幻小说只有过之而无不及,毕竟小说里的世界通常还是以现实为背景,而软件中的世界就全凭造物者(程序员)的想象(建模)了。稍微复杂一点的软件系统,都会引入一些领域特定甚至全新创作的概念。为了避免在项目过程中出现鸡同鸭讲的沟通障碍和理解歧义,就必须对描述这些概念的术语进行统一。而架构的一个重要目的,就是定义和解释清楚系统中涉及的所有关键概念,并在整个架构设计和描述过程中使用标准和一致的术语,真正做到让大家的沟通都在一个频道上。
  • 言之有物:就跟讨论产品交互时需要对着原型图、讨论代码细节时需要直接看代码一样,架构是在讨论一些较高维技术问题时的必要实物(具体的实物化形式就是所谓架构描述)。否则,要么一堆人对着空气谈(纸上谈兵都说不上),要么每次沟通时都重新找块白板画一画(费时费力且容易遗落信息,显然不是长久之计)。
  • 知识沉淀 & 新人培训:架构应该被作为与代码同等重要的文档资产持续沉淀和维护,同时也是项目新人快速理解和上手系统的重要依据。不要让你的系统跟公司内某些祖传遗留系统一样 —— 只有代码遗留了下来,架构文档却没有;只能靠一些口口相传的残留设计记忆,苦苦维系着项目的生命延续。
  1. 架构决定了产品质量

8.png

如何衡量一个软件产品的质量?上图是 ISO/IEC 25010 标准定义的软件产品质量模型,包括以下 8 个大类:

  • 功能适合性:功能完整度、功能正确性和功能恰当性;
  • 性能效率:时间表现(e.g. 响应时间)、资源利用和容量;
  • 兼容性:共存能力(e.g. 多版本组件共存)和互操作性;
  • 可用性:可学习性、可运维性、用户错误保护(e.g. 自动纠错)、UI 美观度、可访问性;
  • 可靠性:成熟度、可用性、容错性、可恢复性;
  • 安全性:机密性、完整性、不可伪造性、权威性和可审计;
  • 可维护性:模块度、可复用性、可分析性、可修改性、可测试性;
  • 可移植性:可适配性、可安装性、可替代性。

上述质量模型中列出的所有点,都是架构设计需要着重考虑的。其中除了功能适合性以外,其他所有点都属于非功能需求的范畴,这也是区分架构好坏的真正分水岭 —— 好的架构设计,不会停留在仅满足功能需求这一最基本的需求层次上(最坏的架构设计也同样能做到),更重要且更难以应对的是其他众多的非功能需求。

9.png

当然,鱼与熊掌不可兼得。架构与人生一样,也是一场权衡的游戏,弄不好就跟第八季的龙母一样的下场:既要又要还要,最后反而什么都得不到。好的架构师更应该像雪诺同志学习,表面上“know nothing”,实际上“know everthing”:清楚系统所有利益相关者(stakeholders),努力挖掘各方的主要述求(concerns),相应平衡自己的架构决策(decisions),最终实现你好我好大家好的终极架构目标。

  1. 我还能说出更多理由

10.png

要不是篇幅所限,这一页 PPT 显然不够装:

  • 架构包含系统所有最重要的早期决策,这些决策会进而影响后续所有大大小小的技术决策。因此,早期的架构设计需要非常严谨和慎重,要尽可能“一次做对”(虽然很难),否则越往后纠错的成本越高;
  • 架构在组织内具有非常高的复用价值,因为同一组织内的产品之间一定会具备很多共性(需求、限制、环境等),很适合在架构层面进行最大化复用,避免重复解决相似的问题;
  • 康威定律指出,软件架构反映了组织结构。这个结论反过来也成立:好的架构也会让组织结构变得更高效
  • 越庞大和复杂的系统,架构越重要,因为只有好的架构才能有效控制、管理和降低系统复杂度
  • 是不是越听越糊涂,仿佛架构有无数种诠释和意义?不必过于纠结,按照GoF的设计模式所述:Architecture is about the important stuff. Whatever that is. 对,管它是啥,记住架构很重要就够了。

如何设计一个好的架构?

理解了架构的概念和重要性后,真正的架构师修炼之路才刚刚开始。如何设计一个好的架构?这显然是一个非常博大精深的主题,但并不是本文的重点,因此这里只简单列举了一些基本思想(原则)和经典套路(模式)。当然,架构设计更接近一门经验学科,仅停留在能脱口而出一些玄乎而高大上的理论概念肯定是不够的,需要结合实际工作内容和业务场景多多实践和揣摩才行,否则只能算是徘徊在架构的门外,连入门都谈不。

  1. 架构原则(principles)

11.png

SOLID 原则是一套比较经典且流行的架构原则(主要还是名字起得好):

  • 单一职责:与 Unix 哲学所倡导的“Do one thing and do it well”不谋而合;
  • 开闭原则:用新增(扩展)来取代修改(破坏现有封装),这与函数式的 immutable 思想也有异曲同工之妙;
  • 里式替换:父类能够出现的地方子类一定能够出现,这样它们之间才算是具备继承的“Is-A”关系;
  • 接口隔离:不要让一个类依赖另一个类中用不到的接口,简单说就是最小化组件之间的接口依赖和耦合;
  • 依赖反转:依赖抽象类与接口,而不是具体实现;让低层次模块依赖高层次模块的稳定抽象,实现解耦。

此外,我们做架构设计时也会尽量遵循如下一些原则(与上述 SOLID 原则在本质上也是相通的):

  • 正交性:架构同一层次拆分出的各组件之间,应该尽量保持正交,即彼此职责独立,边界清晰,没有重叠;
  • 高内聚:同一组件内部应该是高度内聚的(cohesive),像是一个不可分割的整体(否则就应该拆开);
  • 低耦合:不同组件之间应该尽量减少耦合(coupling),既降低相互的变化影响,也能增强组件可复用性;
  • 隔离变化:许多架构原则与模式的本质都是在隔离变化 —— 将预期可能变化的部分都隔离到一块,减少发生变化时受影响(需要修改代码、重新测试或产生故障隐患)的其他稳定部分。
  1. 架构模式(patterns)

12.png

架构模式(architectural patterns)与我们常讨论的设计模式(design patterns)并不是一码事,但如果仅从“模式”这个角度去解读,两者的理念都是一致的:针对给定上下文中经常出现的问题的通用、可复用的解决方案。最主要的区别在于,架构模式会更高维抽象和偏全局整体(毕竟是运用在架构设计层面)。

常见的架构模式,既包括一些传统模式(e.g. 分层、C/S、MVC、事件驱动),也包括一些新兴玩法(e.g. 云原生、微服务、Serverless)。不同模式有不同的适用场景,没有哪一种模式能通杀所有需求。成熟的架构师应该像一个冷静到冒得感情的杀手,永远只会客观地评估和选择最适合当下的解决手段,即使那么做会显得简单乏味;相反,不成熟的架构师,一心总想着搞事情(e.g. 强行套用微服务架构),而不是真正搞定问题。

怎么描述你的架构设计?

有了良好的架构设计,万里长征之路就已经走了一大半。就像是青年导演第一次遇上好剧本,心潮澎湃两眼放光,仿佛已经预见了电影上映后的票房盛况。当然,剩下的一小半路,并不会如想象中那么平坦 —— 同样的剧本,不同导演拍出来会有质一样的区别。好的“最佳导演”,即使面对不是“最佳剧本”的剧本,也有能力拍出“最佳影片”。同样,好的架构师,也应该有能力描述好一个不错的架构设计;即使做不到为精彩的内容加分,也不应该因为形式上没描述好而丢分,否则就会像高考作文丢了卷面分一样憋屈和心酸。

  1. 架构描述的意义

13.png

为什么要描述架构?让它只存在我深深的脑海里不行吗?西方人有句谚语:好记性不如烂笔头。任何没有持久化的东西都是易失的(volatile),就跟内存一样。另一方面,就如前文所述,架构是沟通协作的基础,不通过架构描述(Architecture Description)沉淀下来让所有项目干系人都能看到,那就失去了沟通和传播的唯一载体。

根据个人观察,大家对“架构需要描述”这一点都没异议,所以绝大部分项目都或多或少会产出一些有模有样的架构描述文档。但“有架构描述”和“有好的架构描述”,这之间的鸿沟是巨大的,甚至比“没有”和“有”之间的差别还大。如果你也跟我一样,饱经沧桑阅尽无数架构文档,曾拍手叫好心怀感激过,也曾拍着大腿愤怒不已过,应该也能感同身受。

  1. 架构描述的方式

14.png

对于同一件事物,作家会选择用文字来叙述,而画家却会用图画。尽管两者想要传达的信息是一致的,但描述方式的不同也会带来效果上的巨大差异。架构描述也分文字(Text)图(Diagram)两种形式,两者各有千秋:

  • 文字的背后是由一套严谨和完备的语言作为支撑,因此其描述可以做到非常精准详尽,而且编写起来也很方便,随便打开个记事本软件都能写;此外,就跟写代码一样,文字很易于做版本管理,借助简单的文本 diff 工具就能一目了然地对比出不同版本之间的细节差异;
  • 相比而言,图并不具备以上文字所独有的特点,但也有自己的独特优势:图是直观形象的,顺应了人类与生俱来的视觉识别本能;图的表达能力更强,很多时候一小张图所能传达出的信息(比如空间位置关系、颜色分类、图标形状),也许用一千行字也不足以完整准确地描述出来,即所谓“一图胜千言”。

聪明的你冷笑了一声:哼,又不是小孩子非得做选择题,难道不可以文字与图都要吗?当然可以,理想的架构描述一定是图文并茂的。但现实世界显然比理想残酷,实际软件项目中很难给你留足时间先憋出一篇完美的架构文档。如果以成年人的思维去考虑投入产出比(ROI),那么你一定会优先选择画图。

  1. 为什么你应该优先画图?

15.png

敏捷软件开发宣言中提到:相比详尽的文档,可运作的软件更加重要(Working software over comprehensive documentation)。这么说当然不代表就不用写文档了,只是提倡没必要写过于详尽的文档。为什么?因为详尽的文档需要耗费大量的编写和维护成本,不符合敏捷开发的小步迭代和快速响应变化等原则。

那么,在如今这个全面敏捷开发的时代,如何也顺应潮流更加敏捷地编写架构文档呢?ROI is your friend —— 不求多,但求精,尽量用最少的笔墨表达出最核心的内容。从内容上来说,ROI 高的部分一般是偏顶层的整体架构或最核心的关键链路,这点在后文的 C4 模型理念中也有体现。而从形式上来说,图在文字面前具有无与伦比的表达力优势,显然是 ROI 更高的选择。

  1. 为什么你需要学习画图?

16.png

多画图是没错,但有必要专门学习吗?又不是素描彩笔水墨画,只是画一堆条条框框而已,稍微有点工程常识的都能上。画的有点丑?那没关系,顶多再动用点与生俱来的艺术美感,把这几条线对对齐那几个框摆摆正,再整点五彩斑斓的背景色啥的,不就显得很专业了嘛?

看到这里,屏幕前的你又轻蔑一笑:哼,显然没这么简单。确实,道理说出来大家都懂,架构制图与工程制图一样,都是一件需要下功夫认真严谨对待的事情。但现实中大部分人还真没这工夫去下那功夫,比如上面贴的两幅很常见的架构图。第一张图不用多说,这种草图自己涂涂抹抹挺好,但拿出来见人就是你的不对了。那第二张图呢,看上去似乎还挺像那么回事的?并不是,如果你更仔细地去揣摩,就能发现这张图底下所隐藏的很多模糊和不严谨之处(可参考这张图的来源文章:The Art of Crafting Architectural Diagrams)。

所以,能画图并不代表能画好图;要想制得一手既漂亮又可读的好图,还是需要经过持续学习与刻意练习的,很难仅凭直觉和悟性就能掌握其中的关键要领。此外,错误的图往往比没有图还要糟糕,即使你只是抱着“有图就行,差不多那个意思得了”的心态,也至少应该理解一些科学制图的关键要素,避免给本来就已经很复杂难做的项目又蒙上一层模糊滤镜,甚至起到混淆和误导的反作用。

  1. 架构制图的目标

17.png

讨论具体的制图方法和工具前,我们需要先竖立清晰的制图目标。工具是人类进化的阶梯,但如果理解和利用不当,则很容易反过来被工具所限制甚至奴役,忘了最初发明和使用工具的初心。对于架构制图而言,已经有那么多形形色色的方法与工具,使用它们的初心是什么呢?我认为本质上都是想把制图这个过程从一门自由的手艺变成一项科学的工程:系统、严谨、完整、标准化,同时能做到可重复、可持续和高效。

P.S:当时做 PPT 太赶,所以从这个章节开始的配图,只能被迫走极简路线了,还请见谅。。。

架构制图方法与工具

经过前面几个章节的“简短”铺垫,相信大家对架构制图的背景知识都已经产生了足够的认知。本章节将会具体列举和描述一些典型的架构制图方法与工具,其中有常见的也有罕见的,重点是希望能通过各种方法的横向对比,加深大家对制图方法本质的理解。

  1. 方法一:UML

18.png

UML 应该是大部分人最熟悉的制图方法了,最新的 UML 2.x 版本由以下两大类图组成:

  • 结构图(Structural Diagrams):通过对象、属性、操作和关系等方式,强调系统的静态结构,其中最常见的类型包括类图(Class Diagram)、组件图(Component Diagram)和部署图(Deployment Diagram);
  • 行为图(Behavioral Diagrams):通过展示对象之间的协作关系以及对象内部的状态改变,强调系统的动态行为,其中最常见的类型包括用例图(Use Case Diagram)、活动图(Activity Diagram)、时序图(Sequence Diagram)和状态机图(State Machine Diagram)。

作为通用的“统一建模语言”,UML 总共包含了 14 种不同类型的图,可以全面覆盖软件设计领域各种制图需求,当然也包括了架构制图。同时,也正是因为 UML 把自己当成了一门语言,因此其各种记号(notion)和语义(sematics)都有非常严谨的定义,不会出现模糊或者歧义问题。最后,UML 经过几十年的发展和推广,也早已成为世界范围内广泛使用标准规范,其所带来的的隐性价值就是:在团队内使用 UML 进行沟通的成本是比较低的,因为可以假定绝大部分技术人员都能理解UML的含义和用法。

然而,UML 也非万能(虽然历史上曾一度把它当成软件设计的银弹),它最被人诟病的缺点就是过于复杂。这也不能怪 UML,毕竟它就是要被设计为足够通用、严谨和强大的,这些目标都与“简单”背道而驰,并让它一步步演化到了今天这个复杂刻板的庞然大物模样。虽然上面我们自信地假定了技术人员大多都懂 UML,但这个“懂”如果带上一个程度量词,我觉得平均能到 20% 就不错了 —— 绝大部分也就能认识几个常见的类图、时序图,估计都很难准确说出类图中各种箭头的含义。

无论怎么说,UML依然应该是每个程序员的制图工具箱中最常用和必备的工具之一。当然,也不应该是唯一,因为下面也还有些不能错过的好东西。

  1. 方法二:4+1 View Model

19.png

“4+1”是啥?不知道没关系,听过“6+1”吗?对,就是那个小时候常看的“非常6+1”节目。它跟“4+1”之间的关系,就跟它们与邵佳一、张嘉译和沈佳宜之间的关系一样,除了赶巧共用了同一个后缀发音以外,八竿子打不着。

所以,“4+1”到底是指什么?让我们来 Wiki 一下:“4+1”是一种视图模型(view model),可以通过多种共存的视图描述软件密集型系统的架构。这些视图基于不同项目干系人(利益相关者)的视点(viewpoint),例如:终端用户、开发者、系统工程师和项目经理。“4+1”由 4 种基础视图和一些经过挑选的用例或场景(即额外的“+1”视图)组成,各自的具体含义如下:

  • 逻辑视图(Logical view):描述系统为终端用户提供的功能,一般会通过UML中的类图和状态图来表示;
  • 过程视图(Process view):描述系统的动态行为,包括流程和交互等,一般会通过 UML 中的时序图、活动图和通讯图来表示;
  • 开发视图(Development view):从程序员的视角来阐述系统,也被称为“实现视图”,一般会通过 UML 中的组件图和包图来表示;
  • 物理视图(Physical view):从系统工程师的角度来描述系统,包括系统组件的物理拓扑、各组件之间的物理连接,也被称为“部署视图”,一般会通过 UML 中的部署图来表示;
  • 场景(Scenarios):通过一小组用例或场景来描述架构,包括系统中各种对象和进程之间的交互时序,也被称为“用例视图”。这些场景会被用于识别架构元素(architectural elements)以及阐述和验证整个架构设计,也可以被作为架构原型的测试起点。

虽然上面提到“4+1”的各种视图一般都是用UML图来表示,但实际上“4+1”本身是一种通用的视图模型,并没有限制绘图的记号和工具。对于工程师而言,这种偏学院派的方法可能这辈子都不会直接用到,但其中蕴含的一个关键架构制图思想非常有价值:架构需要通过多种视图来描述,而这些视图是来源于不同项目干系人的视点(角度);只有这样才能产生一整套全面、立体且客观的架构描述。

  1. 方法三:C4 Model

C4 模型是一种“抽象优先”(abstraction-first)的架构制图方法,它也是受前面的 UML 和“4+1”视图模型所启发,但相对而言要更加简单和轻量,只包含少量的一组抽象和图表,很易于学习和使用。

1)定义、理念与关键思想

20.png

C4 模型通过容器、组件、代码以及人这几个抽象来描述一个软件系统的静态结构,它的核心理念是希望像 Google Map 一样,通过不同层次的细节,为代码建立一种可以放大缩小的导览图。它最关键的思想就是自顶向下对系统的静态结构进行逐级拆分,依次描述各层次对象的职责、关系和外部依赖。除了核心的层次化静态结构视图,它还可以包含动态视图、部署视图等补充视图。

21.png

上面的左图展示了 C4 模型中各层次抽象之间的映射关系:1 个软件系统由 1~N 个容器组成,1 个容器由 1~N 个组件组成,1 个组件由 1~N 个代码结构组成。右图是以简单的 Spring PetClinic 项目为例,演示了一个真实软件系统在 C4 模型下的层次结构:最上层就是 PetClinic 软件系统,它可以拆分为数据库、Web 应用等几个容器;Web 应用又可以进一步拆分出 ClinicService 这个组件,而这个组件下又包含了 ClinicService 接口类、ClinicServiceImple 实现类、Owner / Pet / Visit 等领域对象类。

使用 C4 模型进行架构制图,本质上就是对上述几种抽象进行可视化。具体的做法是依次建立如下几类从粗到细的结构图:Context、Container、Component 和 Code(可选),这也是 C4 模型名称的来历。

2)Level 1:System Context diagram

22.png

系统上下文图作为第一级(L1),提供了一个展示系统全貌的顶层大图(big picture)视角,包括最中心的软件系统、周边的用户以及其他有交互的系统。其中最关键的两个概念分别是:

  • (Person):即使用软件系统的用户,例如一个在线商城系统的消费者、运营小二、系统管理员等;
  • 软件系统(Software System):作为最高层次抽象,描述了给用户创造价值的软件制品;既包括当前正在设计的软件系统,也包括该系统所依赖(或被依赖)的其他软件系统。一个软件系统通常是由单个软件开发团队所负责。

在绘制系统上下文图时,不需要关心诸如技术栈、协议等任何底层细节。这类图的受众是最广的,因为任何人都可以理解并从中获取到足够的信息,包括技术人员和非技术人员,也包括团队内成员和团队外成员。

3)Level 2:Container diagram

23.png

通过 L1 的上下文图理解了系统在整个 IT 环境中的定位后,下一步就是把系统这个框框放大,详细看下其中包含了哪些“容器”(Container,注意不要跟 Docker 容器搞混了噢!)。C4 模型中的容器是指单个应用或数据存储,通常可以独立部署和运行(有独立的进程空间,通过 IPC 机制互相通讯),例如:SpringBoot 微服务、React SPA、移动 App、数据库、Serverlss 函数、Shell 脚本。

L2 的容器图不仅展示了系统的进一步职责拆分,还包括了主要的技术选型、容器之间的通讯方式等关键架构信息。这类图可以面向全部的技术人员,既包括架构师、开发者,也包括运维人员、技术支持等。

4)Level 3:Component diagram

24.png

继续前面的套路,下一步就是把系统中各个容器再分别进行局部放大,将每个容器进一步拆分成多个组件(Component)。在 C4 模型中,组件是指一组通过良好接口定义封装在一起的相关功能(通常运行在同一个进程空间内),例如:Spring 里的一个Controller(不只包括定义了 REST 接口的 Controller 主类,也包括背后所有相关联的实现类,如 Service/Repository 等)。

与容器图类似,L3 的组件图也不只包含了容器的组件划分,还包括各个组件的职责定义、技术与实现细节等。随着层次的下沉和细节的增多,组件图的受众范围进一步缩窄,一般只适用于软件架构师和开发者(其他角色没必要理解,一般也理解不了)。

5)Level 4:Code(可选)

25.png

再继续对组件进行放大,所能看到的最底层和细节的信息,就是 L4 的代码(Code)了。当然,这里所谓的“代码”还是以图的形式(e.g. UML 类图、数据库 E/R 图)展示类或文件粒度的代码结构,并不是真正的代码本身。即便如此,代码图在 99% 的架构描述场景下也依然过于详尽,一方面数量庞大,绘制成本很高;另一方面易于变化,维护成本也非常高。因此,一般只有非常重要和复杂的组件才需要用到这一层级进行描述。如果确实需要绘制,也应该优先考虑自动化的方式,比如很多 IDE 就支持自动生成 UML 类图。

6)补充图:Landscape / Dynamic / Deployment Diagram

26.png

除了上述各个层次的静态结构图,C4 模型还提出了一系列的补充图(Supplementary diagrams),包括:

  • 系统全景图(System Landscape diagram):全景图与系统上下文图的绘制方法类似,区别在于它是从企业或组织角度全景地展示出所有软件系统(包括与当前系统没有直接关联的)以及相关的用户和系统交互,即进一步放大架构图的 scope
  • 动态图(Dynamic diagram):由于结构图天生只能描述出系统的静态结构属性,因此 C4 模型中推荐使用 UML 中的通讯图、时序图等,对系统中关键链路的动态行为进行补充描述,即“动静结合”;
  • 部署图(Deployment diagram):除了缺失动态属性,上述结构图还有一个局限性:只描述了系统的抽象逻辑架构,并没有描述出系统实际部署时的具体物理架构。因此,C4 模型推荐再使用 UML 的部署图,对系统逻辑节点(一般是 L2 的“容器”粒度)与物理节点(e.g. 物理机 / 虚拟机 / Docker 容器 / 应用 Runtime)之间的映射关系进行补充描述,即“虚实结合”。

结合了这些补充图后的 C4 模型,才是可以全面与立体地描述出软件架构方方面面的完全体架构制图方法。

  1. 方法四:arc42

27.png

严格来说,arc42 并不是一种架构制图方法,而是一个架构文档模板。虽然如前文所说,在架构描述中“图”是比“文字”更高优的选择,但实际项目过程中你终究还是需要产出一份相对完整、有图有文字的架构文档。arc42 就是专门用于帮助大家更好地编写架构文档;而作为架构文档中最重要的架构图,显然 arc42 也不会放过 —— 其中多个核心章节都与架构图有关,且详细描述了相应的制图方法。这里不会详细展开介绍 arc42(不能抢了下一篇文章的饭碗),只会简单介绍下 arc42 中制图方法与 C4 模型的异同。

伟大的思想都是相似的,arc42 也不例外。上方左图的右侧部分,概括了 arc42 模板中与制图相关的几个核心章节,分别是:

  • 第 3 章 - Context:该章节用于介绍系统的背景和上下文,因此其制图思路几乎等同于 C4 模型中的 L1(系统上下文图);
  • 第 5 章 - Building block view:该章节用于介绍系统的基本构成要素,按照官方指导思想也与 C4 模型中的自顶向下层次化拆分思想无异,唯一区是 arc42 并没有规定拆分的具体层次,只要有需要可以按照“黑盒 -> 白盒”的套路一直拆到底;
  • 第 6 章 - Runtime view:看名字就无需解释了,就等同于 C4 模型中补充的运行时视图;
  • 第 7 章 - Deployment view:同样地,这里也等同于 C4 模型中补充的部署视图;但有一点,arc42 强调部署视图也可以类似结构视图一样做自顶向下的层次化拆分(对于较为复杂的部署架构,层次化确实很有必要)。

因此,本质上 arc42 中提倡的制图方法与C4模型是等价和兼容的,完全可以配合使用:以 arc42 作为架构文档框架,其中的架构制图采用更具体的 C4 模型。这也是目前我们项目中实际采用的方法。

  1. 其他方法 & 制图工具

28.png

除了上述几种方法以外,在软件行业蓬勃发展的数十年间也涌现出过很多其他的优秀架构制图方法,其中既包括一些通用方法,如:SysMLAADLArchiMate,也包括一些领域特定方法,比如在企业中后台业务建模场景中很常见的 BPMN。再详细地展开描述各个方法,显然会让本文又臭又长(虽然写到这里时似乎就已经注定了),有兴趣的读者可以自行检索和探索。

到这里为止,本章节介绍的都是架构制图的各种方法;而实际从方法到落地的过程中,还有一个绕不开的环节:选用什么样的工具去制图?总不能真的跟写工程制图作业一样用纸和笔吧?作为数字化改革的推动者,程序员们当然要全面拥抱数字化工具;大家日常工作中必然也已经积累了很多顺手的画图工具,因此这里我只推荐两个自己用得比较多的:

  • draw.io:这是一个开源的在线绘图软件,相信很多人都有用过。考虑到数据安全问题,推荐大家用完全离线的桌面版。作为一个程序员友好的绘图工具,draw.io 的最大优点就是支持三方插件,比如这个开源的 c4-draw.io 插件,可以帮助你更方便地在 draw.io 中绘制 C4 模型架构图;
  • PlantUML:作为文本制图的代表性工具,PlantUML 可以用于绘制各种类型的UML图,以及其他一些适用于文本制图场景的图(比如这个开源的 C4-PlantUML 扩展)。在这些场景下,文本制图具有可视化制图所无法比拟的优势:轻量、高效、版本化、自动化、一致性、易于复用等。虽然文本制图工具诞生已久(比如应用广泛的 [Graphviz](),最早发行于 1991 年),但相信随着现代各种 XXX as Code 的意识觉醒,这类 Diagram as Code 工具也会获得更多青睐(btw,语雀文档早已支持内嵌 PlantUML 制图)。

架构制图方法论总结

古有云:授人以鱼,不如授人以渔。推而广之:授人以方法,也不如授人以方法论。什么是方法论?虽然这个词在公司里已经用烂了,但确实有它的价值和意义:方法论(methodology)是对方法的更高维度抽象,由它可以推导出解决问题的具体方法(method)。理解了方法论,才能融会贯通,掌握解决问题的本质要点;你也不会再受限于单一的具体方法,因为使用任何方法都能快速上手和灵活运用,并得到差不多的同等效果。

因此,本文最后这一章节将对各种架构制图方法进行归纳总结,并尝试提炼出一个通用的架构制图方法论,期望能帮助大家更好地理解架构制图背后的原理和思想。即便现在所熟知的各种方法与工具终会过时,也依然能风轻云淡地看待它们的新老交替:过去是 UML,现在是 C4,未来是什么呢?这并不关键,因为即使方法过时了,背后的方法论也不会过时。

所以,那些茫茫多的方法背后,究竟是什么样的核心方法论在支撑着呢?经过作者呕心沥血冥思苦想了近 15 秒钟,终于总结出了如下这套经典方法论(p.s:就是凑数的,不要太当真~ )。由于其中包含了 5 个环环相扣的要点,我们姑且称它为:五环理论

29.png

  1. 理解制图目标

30.png

架构制图的第一要点,是需要先深刻理解制图目标。正所谓“以始为终”,有了目标我们才能清晰地前行;否则漫无目的地乱窜,往往会多走不少弯路,甚至南辕北辙。架构制图的目标是什么?其实前文已经提到过很多,这里再简单总结下:

  • 准确(accurate):错的图比没有图还糟糕;即使一开始是准确的,后面也需要定期更新校对;
  • 完整(complete):需要覆盖架构的核心要素和关键信息,为受众呈现一个没有残缺的完整架构设计;
  • 清晰(clear):制图时最好带上图例(形状、颜色、线型、箭头);用图描述不清的地方,还可以加上文字标注做补充;
  • 一致(consistent):比如同一类型的图,最好使用相同的记号风格,以降低受众的理解成本;不一致往往还会带来混淆;
  • 简洁(consise):在满足以上 4 点基础之上,还需要让图更加简洁,一方面是更容易被人接受(没人读 = 没写),另一方面更新维护成本也更低。
  1. 找准受众和关注点

31.png

架构制图的第二要点,是要找准你制图的受众(audience)以及他们各自的关注点(concern)。找不准的话,要么效果大打折扣(不是他们想听的),要么犹如对牛弹琴(他们根本就听不懂)。常见的一些受众和关注点可包括:

  • 研发:一般会关注很多实现相关细节,比如技术选型、实现可行性、可维护性等,毕竟他们是架构的最直接消费者;
  • 运维:不太关心应用内的具体技术实现(当成黑盒),但很关心各个应用实例的物理部署方式、网络连通性、可运维性等;
  • 安全:只关注系统是否有安全风险,例如是否可能被注入恶意代码、是否有权限漏洞等;如果经历过安全评审,应该很有体感;
  • 产品:大部分情况下只关心项目能否按期上线,其他方面...可能表面上表示些许关心,实际上要么并不在乎要么真的不懂。

3. 自顶向下逐层描述

32.png

架构制图的第三要点,是合理运用层次化(hierarchical)的套路,自顶向下逐层描述。无论是 C4 模型还是 arc42 模板,背后都深刻运用并显著强调了这一点。为什么一定要这么做?其中蕴含了两个普适的原理:

  • 分而治之:软件领域中,分而治之是控制和应对复杂系统的最有效方法。而层次化拆分,本质上就是一种分而治之手段:将系统按照从粗到细的粒度,一级一级地拆分成多个相对独立和低耦合的元素(子系统、应用、组件等);
  • 金字塔原理:这本书的核心观点就是,按照自顶向下的方式,先抛出主观点再依次用各个子观点去论证。这样的沟通方式更符合人类的思维逻辑,也更容易让读者接受。简单来说,就是要“先说重点”,帮助读者做归纳总结和划重点,而不是先抛出一大堆细枝末节的零散东西让读者自己去消化和推演。
  1. 使用多种架构视图

33.png

架构制图的第四要点,是在向传统的工程制图方法论致敬:使用多种架构视图来描述你的架构。在工程制图的世界里,任何立体的制品,大到机床小到零件,都至少需要通过三种视图(主视图、俯视图、左视图)来描述。作为现实世界的映射,软件系统也是多维和立体的,只用单一视图不可能覆盖所有关键的架构信息;即使强行把这些信息都塞在一张图里,那也一定会复杂到让人无法理解。

在架构设计领域,架构视图(architectural view)有专门的定义:针对系统架构某一个方面(aspect)的一种描述;每个视图都会覆盖项目干系人的一种或多种关注点。从上述定义可以看出来,不同的架构视图会有不同的侧重点,同时在描述自己所专注的方面时也会略去与当前视图无关的其他细节 —— 这其实也是一种与层次化拆分类似的分而治之思想,只不过这里是针对完整系统的维度分解,而层次化则是针对某一具体视图再做自顶向下的垂直下钻(drill-down);两者是正交且可以相互配合的,例如前面说到的结构视图、部署视图甚至动态视图,都可以分别再进行层次化拆分。

  1. 遵循规范和最佳实践

34.png

架构制图的第五要点,其实只是一句正确的废话:遵循规范和最佳实践。这一点已经不限于架构制图,而是上升到了工程实践领域的通用方法论层面。正如前面章节所说,“学习架构制图的目标,就是要把它从一门手艺变成一项工程”,因此架构制图的“施工”过程也理所应当符合工程化思维

  • 一方面,制图需要遵循明确的规范,在理论层面进行约束和指引,确保过程和产物的高质量与标准化;
  • 另一方面,制图还需要遵循业界最佳实践,在实践层面持续吸取优秀经验,不断精进自己和团队的制图技能。

附:架构描述标准化概念模型

国际上对架构描述其实建立了专门的标准(ISO  / IEC / IEEE 42010:2011),其中的很多概念词汇在本文中都有提到(e.g. Stakeholder、Concern、View、Viewpoint),有兴趣的同学可以进一步研究下。

35.png

结语

如果你从头到尾耐着性子看到了这里,那么不用怀疑,你一定就是我们团队要找的那种能成大事儿的人:

原文链接
本文为阿里云原创内容,未经允许不得转载。

查看原文

jsyzchen 关注了专栏 · 10月9日

阿里云栖号

汇集阿里技术精粹-yq.aliyun.com

关注 11802

jsyzchen 收藏了文章 · 5月2日

史上最全、最新的Redis面试题(2020最新版)!

作者:ThinkWon
原文:https://thinkwon.blog.csdn.ne...

技术人面试,无论是开发、运维、测试、还是架构师,Redis技术是必问的点。公众号后台、群里,一直也有人问有没有相关的面试题汇总,有时候想看看查漏补缺一下,面试也有可能用的上。所以,今天,民工哥给大家带这篇汇总文章。

什么是Redis

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。

Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。

与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

Redis有哪些优缺点

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

为什么要用 Redis /为什么要用缓存

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

为什么要用 Redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路 I/O 复用模型,非阻塞 IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

数据类型

Redis有哪些数据类型

Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求

**Redis的应用场景
**

总结一

计数器

可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

缓存

将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

查找表

例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

消息队列(发布/订阅功能)

List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。

分布式锁实现

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。

总结二

Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。

数据类型说明string字符串,最简单的k-v存储hashhash格式,value为field和value,适合ID-Detail这样的场景。list简单的list,顺序列表,支持首位或者末尾插入数据set无序list,查找速度快,适合交集、并集、差集处理sorted set有序的set

其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。

string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就用这种类型来存储。

hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情,个人信息详情,新闻详情等。

list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。

set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人共同的好友等。

Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。

如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利用Redis的性能。

持久化

什么是Redis持久化?

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis DataBase缩写快照

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

优点:

1、只有一个文件 dump.rdb,方便持久化。

2、容灾性好,一个文件可以保存到安全的磁盘。

3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能

4.相对于数据集大时,比 AOF 的启动效率更高。

缺点:

1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)

2、AOF(Append-only file)持久化方式:是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。

AOF:持久化

AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

优点:

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。

2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。

3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点:

1、AOF 文件比 RDB 文件大,且恢复速度慢。

2、数据集大的时候,比 rdb 启动效率低。

优缺点是什么?

  • AOF文件比RDB更新频率高,优先使用AOF还原数据。
  • AOF比RDB更安全也更大
  • RDB性能比AOF好
  • 如果两个都配了优先加载AOF

如何选择合适的持久化方式

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
  • 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

Redis持久化数据和缓存怎么做扩容?

  • 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
  • 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

过期键的删除策略

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis key的过期时间和永久有效分别怎么设置?

EXPIRE和PERSIST命令。

我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  • 定时去清理过期的缓存;
  • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

内存相关

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis主要消耗什么物理资源?

内存。

Redis的内存用完了会发生什么?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

线程模型

Redis线程模型

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。

事务

什么是事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis事务的概念

Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务的三个阶段

  • 事务开始 MULTI
  • 命令入队
  • 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

  • redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  • 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
  • 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
  • MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • UNWATCH命令可以取消watch对所有key的监控。

事务管理(ACID)概述

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)

事务前后数据的完整性必须保持一致。

隔离性(Isolation)

多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

Redis事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

集群方案

哨兵模式

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的核心知识

  • 哨兵至少需要 3 个实例,来保证自己的健壮性。
  • 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
  • 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

官方Redis Cluster 方案(服务端路由查询)

redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?

简介

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行

方案说明

  • 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
  • 每份数据分片会存储在多个互为主从的多节点上
  • 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  • 同一分片多个节点间的数据不保持一致性
  • 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  • 扩容时时需要需要把旧节点的数据迁移一部分到新节点

在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制

基本通信原理

集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。

分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • redis cluster 的 hash slot 算法

优点

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel的监控和自动Failover(故障转移)能力
  • 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • 高性能,客户端直连redis服务,免去了proxy代理的损耗

缺点

  • 运维也很复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作(pipeline管道操作)
  • 分布式逻辑和存储模块耦合等

基于客户端分配

简介

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool

优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。

客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化

基于代理服务器分片

简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端

特征

  • 透明接入,业务程序不用关心后端Redis实例,切换成本低
  • Proxy 的逻辑和存储的逻辑是隔离的
  • 代理层多了一次转发,性能有所损耗

业界开源方案

  • Twtter开源的Twemproxy
  • 豌豆荚开源的Codis

Redis 主从架构

单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。

redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发

redis replication 的核心机制

  • redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
  • 一个 master node 是可以配置多个 slave node 的;
  • slave node 也可以连接其他的 slave node;
  • slave node 做复制的时候,不会 block master node 的正常工作;
  • slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
  • slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

注意,如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。

另外,master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的,即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。

redis 主从复制的核心原理

当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,

同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

过程原理

  • 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
  • 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
  • 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
  • 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
  • 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

缺点

所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决

Redis集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品

生产环境中的 redis 是怎么部署的?

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。

5 台机器对外提供读写,一共有 50g 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

说说Redis哈希槽的概念?

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

Redis集群之间是如何复制的?

异步复制

Redis集群最大节点个数是多少?

16384个

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

分区

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

为什么要做Redis分区?

分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

你知道有哪些Redis分区实现方案?

  • 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
  • 代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
  • 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

Redis分区有什么缺点?

  • 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
  • 同时操作多个key,则不能使用Redis事务.
  • 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set)
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
  • 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

分布式问题

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

使用SETNX完成同步锁的流程及事项如下:

使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间。

释放锁,使用DEL命令将锁数据删除。

如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

什么是 RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  • 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

缓存异常

缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

附加

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。

Bitmap:典型的就是哈希表

缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐)

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。

它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。

Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。

Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  • 设置热点数据永远不过期。
  • 加互斥锁,互斥锁

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  • 直接写个缓存刷新页面,上线时手工操作一下;
  • 数据量不大,可以在项目启动的时候自动进行加载;
  • 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

常用工具

Redis支持的Java客户端都有哪些?官方推荐用哪个?

Redisson、Jedis、lettuce等等,官方推荐使用Redisson。

Redis和Redisson有什么关系?

Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

Jedis与Redisson对比有什么优缺点?

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

其他问题

Redis与Memcached的区别

两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

Redis常见性能问题和解决方案?

  • Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
  • 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
  • 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
  • 尽量避免在压力较大的主库上增加从库
  • Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
  • 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。

Redis官方为什么不提供Windows版本?

因为目前Linux版本已经相当稳定,而且用户量很大,无需开发windows版本,反而会带来兼容性等问题。

一个字符串类型的值能存储最大容量是多少?

512M

Redis如何做大量数据插入?

Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的?

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis如何实现延时队列?

使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

Redis回收进程如何工作的?

一个客户端运行了新的命令,添加了新的数据。

Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。

一个新的命令被执行,等等。

所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

Redis回收使用的是什么算法?

LRU算法

查看原文

jsyzchen 赞了文章 · 5月2日

史上最全、最新的Redis面试题(2020最新版)!

作者:ThinkWon
原文:https://thinkwon.blog.csdn.ne...

技术人面试,无论是开发、运维、测试、还是架构师,Redis技术是必问的点。公众号后台、群里,一直也有人问有没有相关的面试题汇总,有时候想看看查漏补缺一下,面试也有可能用的上。所以,今天,民工哥给大家带这篇汇总文章。

什么是Redis

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。

Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。

与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

Redis有哪些优缺点

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

为什么要用 Redis /为什么要用缓存

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

为什么要用 Redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路 I/O 复用模型,非阻塞 IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

数据类型

Redis有哪些数据类型

Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分的使用要求

**Redis的应用场景
**

总结一

计数器

可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

缓存

将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

全页缓存(FPC)

除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

查找表

例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

消息队列(发布/订阅功能)

List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。

分布式锁实现

在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

其它

Set 可以实现交集、并集等操作,从而实现共同好友等功能。ZSet 可以实现有序性操作,从而实现排行榜等功能。

总结二

Redis相比其他缓存,有一个非常大的优势,就是支持多种数据类型。

数据类型说明string字符串,最简单的k-v存储hashhash格式,value为field和value,适合ID-Detail这样的场景。list简单的list,顺序列表,支持首位或者末尾插入数据set无序list,查找速度快,适合交集、并集、差集处理sorted set有序的set

其实,通过上面的数据类型的特性,基本就能想到合适的应用场景了。

string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就用这种类型来存储。

hash——一般key为ID或者唯一标示,value对应的就是详情了。如商品详情,个人信息详情,新闻详情等。

list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的***,消息队列等。

set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:查找两个人共同的好友等。

Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据。

如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,比一般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利用Redis的性能。

持久化

什么是Redis持久化?

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis DataBase缩写快照

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

优点:

1、只有一个文件 dump.rdb,方便持久化。

2、容灾性好,一个文件可以保存到安全的磁盘。

3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能

4.相对于数据集大时,比 AOF 的启动效率更高。

缺点:

1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)

2、AOF(Append-only file)持久化方式:是指所有的命令行记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof 文件。

AOF:持久化

AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

优点:

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。

2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。

3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点:

1、AOF 文件比 RDB 文件大,且恢复速度慢。

2、数据集大的时候,比 rdb 启动效率低。

优缺点是什么?

  • AOF文件比RDB更新频率高,优先使用AOF还原数据。
  • AOF比RDB更安全也更大
  • RDB性能比AOF好
  • 如果两个都配了优先加载AOF

如何选择合适的持久化方式

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
  • 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

Redis持久化数据和缓存怎么做扩容?

  • 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
  • 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。

过期键的删除策略

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis key的过期时间和永久有效分别怎么设置?

EXPIRE和PERSIST命令。

我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  • 定时去清理过期的缓存;
  • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

内存相关

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis主要消耗什么物理资源?

内存。

Redis的内存用完了会发生什么?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

线程模型

Redis线程模型

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。

事务

什么是事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis事务的概念

Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务的三个阶段

  • 事务开始 MULTI
  • 命令入队
  • 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

  • redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  • 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
  • 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
  • MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • UNWATCH命令可以取消watch对所有key的监控。

事务管理(ACID)概述

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)

事务前后数据的完整性必须保持一致。

隔离性(Isolation)

多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

Redis事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

集群方案

哨兵模式

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的核心知识

  • 哨兵至少需要 3 个实例,来保证自己的健壮性。
  • 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
  • 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

官方Redis Cluster 方案(服务端路由查询)

redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?

简介

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行

方案说明

  • 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
  • 每份数据分片会存储在多个互为主从的多节点上
  • 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  • 同一分片多个节点间的数据不保持一致性
  • 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  • 扩容时时需要需要把旧节点的数据迁移一部分到新节点

在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制

基本通信原理

集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。

分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • redis cluster 的 hash slot 算法

优点

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel的监控和自动Failover(故障转移)能力
  • 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • 高性能,客户端直连redis服务,免去了proxy代理的损耗

缺点

  • 运维也很复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作(pipeline管道操作)
  • 分布式逻辑和存储模块耦合等

基于客户端分配

简介

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool

优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。

客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化

基于代理服务器分片

简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端

特征

  • 透明接入,业务程序不用关心后端Redis实例,切换成本低
  • Proxy 的逻辑和存储的逻辑是隔离的
  • 代理层多了一次转发,性能有所损耗

业界开源方案

  • Twtter开源的Twemproxy
  • 豌豆荚开源的Codis

Redis 主从架构

单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。

redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发

redis replication 的核心机制

  • redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
  • 一个 master node 是可以配置多个 slave node 的;
  • slave node 也可以连接其他的 slave node;
  • slave node 做复制的时候,不会 block master node 的正常工作;
  • slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
  • slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

注意,如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。

另外,master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的,即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。

redis 主从复制的核心原理

当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,

同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

过程原理

  • 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
  • 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
  • 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
  • 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
  • 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

缺点

所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决

Redis集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品

生产环境中的 redis 是怎么部署的?

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。

5 台机器对外提供读写,一共有 50g 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

说说Redis哈希槽的概念?

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

Redis集群之间是如何复制的?

异步复制

Redis集群最大节点个数是多少?

16384个

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

分区

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

为什么要做Redis分区?

分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

你知道有哪些Redis分区实现方案?

  • 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
  • 代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
  • 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

Redis分区有什么缺点?

  • 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
  • 同时操作多个key,则不能使用Redis事务.
  • 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set)
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
  • 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

分布式问题

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

使用SETNX完成同步锁的流程及事项如下:

使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间。

释放锁,使用DEL命令将锁数据删除。

如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

什么是 RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  • 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

缓存异常

缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

附加

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。

Bitmap:典型的就是哈希表

缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐)

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。

它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。

Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。

Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  • 设置热点数据永远不过期。
  • 加互斥锁,互斥锁

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  • 直接写个缓存刷新页面,上线时手工操作一下;
  • 数据量不大,可以在项目启动的时候自动进行加载;
  • 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

常用工具

Redis支持的Java客户端都有哪些?官方推荐用哪个?

Redisson、Jedis、lettuce等等,官方推荐使用Redisson。

Redis和Redisson有什么关系?

Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

Jedis与Redisson对比有什么优缺点?

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

其他问题

Redis与Memcached的区别

两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

Redis常见性能问题和解决方案?

  • Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
  • 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
  • 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
  • 尽量避免在压力较大的主库上增加从库
  • Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
  • 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。

Redis官方为什么不提供Windows版本?

因为目前Linux版本已经相当稳定,而且用户量很大,无需开发windows版本,反而会带来兼容性等问题。

一个字符串类型的值能存储最大容量是多少?

512M

Redis如何做大量数据插入?

Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的?

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis如何实现延时队列?

使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

Redis回收进程如何工作的?

一个客户端运行了新的命令,添加了新的数据。

Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。

一个新的命令被执行,等等。

所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

Redis回收使用的是什么算法?

LRU算法

查看原文

赞 23 收藏 19 评论 0

jsyzchen 赞了文章 · 4月14日

14道关于计算机网络的面试题,助你查漏补缺

photo-1562887245-f2d5024f6134.jpeg

最近在整理计算机网络的时候发现遇到了很多面试中常见的面试题,本部分主要原作者在 Github 等各大论坛收录的计算机网络相关知识和一些相关面试题时所做的笔记,分享这份总结给大家,对大家对计算机网络的可以来一次全方位的检漏和排查,感谢原作者 CavsZhouyou 的付出,原文链接放在文章最下方,如果出现错误,希望大家共同指出!

应用层

应用层协议定义了应用进程间的交互和通信规则,不同主机的应用进程间如何相互传递报文,比如传递的报文的类型、格式、
有哪些字段等等。

HTTP 协议概况

HTTP 是超文本传输协议,它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传
输层协议,保证了数据传输的可靠性。

HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息。

HTTP 有两种连接模式,一种是持续连接,一种非持续连接。非持续连接指的是服务器必须为每一个请求的对象建立和维护
一个全新的连接。持续连接下,TCP 连接默认不关闭,可以被多个请求复用。采用持续连接的好处是可以避免每次建立 TCP
连接三次握手时所花费的时间。在 HTTP1.0 以前使用的非持续的连接,但是可以在请求时,加上 Connection: keep-a
live 来要求服务器不要关闭 TCP 连接。HTTP1.1 以后默认采用的是持续的连接。目前对于同一个域,大多数浏览器支持
同时建立 6 个持久连接。

HTTP 请求报文

HTTP 报文有两种,一种是请求报文,一种是响应报文。

HTTP 请求报文的格式如下:


GET / HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

HTTP 请求报文的第一行叫做请求行,后面的行叫做首部行,首部行后还可以跟一个实体主体。请求首部之后有一个空行,这
个空行不能省略,它用来划分首部与实体。

请求行包含三个字段:方法字段、URL 字段和 HTTP 版本字段。

方法字段可以取几种不同的值,一般有 GET、POST、HEAD、PUT 和 DELETE。一般 GET 方法只被用于向服务器获取数据。
POST 方法用于将实体提交到指定的资源,通常会造成服务器资源的修改。HEAD 方法与 GET 方法类似,但是在返回的响应
中,不包含请求对象。PUT 方法用于上传文件到服务器,DELETE 方法用于删除服务器上的对象。虽然请求的方法很多,但
更多表达的是一种语义上的区别,并不是说 POST 能做的事情,GET 就不能做了,主要看我们如何选择。更多的方法可以参
文档

HTTP 响应报文

HTTP 报文有两种,一种是请求报文,一种是响应报文。

HTTP 响应报文的格式如下:

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>
  <body>Hello World</body>
</html>

HTTP 响应报文的第一行叫做状态行,后面的行是首部行,最后是实体主体。

状态行包含了三个字段:协议版本字段、状态码和相应的状态信息。

实体部分是报文的主要部分,它包含了所请求的对象。

常见的状态有

200-请求成功、202-服务器端已经收到请求消息,但是尚未进行处理
301-永久移动、302-临时移动、304-所请求的资源未修改、
400-客户端请求的语法错误、404-请求的资源不存在
500-服务器内部错误。

一般 1XX 代表服务器接收到请求、2XX 代表成功、3XX 代表重定向、4XX 代表客户端错误、5XX 代表服务器端错误。

更多关于状态码的可以查看:

《HTTP 状态码》

首部行

首部可以分为四种首部,请求首部、响应首部、通用首部和实体首部。通用首部和实体首部在请求报文和响应报文中都可以设
置,区别在于请求首部和响应首部。

常见的请求首部有 Accept 可接收媒体资源的类型、Accept-Charset 可接收的字符集、Host 请求的主机名。

常见的响应首部有 ETag 资源的匹配信息,Location 客户端重定向的 URI。

常见的通用首部有 Cache-Control 控制缓存策略、Connection 管理持久连接。

常见的实体首部有 Content-Length 实体主体的大小、Expires 实体主体的过期时间、Last-Modified 资源的最后修
改时间。

更多关于首部的资料可以查看:

《HTTP 首部字段详细介绍》

《图解 HTTP》

HTTP/1.1 协议缺点

HTTP/1.1 默认使用了持久连接,多个请求可以复用同一个 TCP 连接,但是在同一个 TCP 连接里面,数据请求的通信次序
是固定的。服务器只有处理完一个请求的响应后,才会进行下一个请求的处理,如果前面请求的响应特别慢的话,就会造成许
多请求排队等待的情况,这种情况被称为“队头堵塞”。队头阻塞会导致持久连接在达到最大数量时,剩余的资源需要等待其他
资源请求完成后才能发起请求。

为了避免这个问题,一个是减少请求数,一个是同时打开多个持久连接。这就是我们对网站优化时,使用雪碧图、合并脚本的
原因。

HTTP/2 协议

2009 年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。这个协议在 Chrome 浏览器上证明
可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。2015 年,HTTP/2 发布。

HTTP/2 主要有以下新的特性:

二进制协议

HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是
二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。
帧的概念是它实现多路复用的基础。

多路复用

HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回
应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"的问题。

数据流

HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的
请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每
个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流 ID ,用来区分它属于哪个数据流。

头信息压缩

HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是
重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,
客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引
号,这样就能提高速度了。

服务器推送

HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送,提前给客户端推送必要的资源
,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用
SSE 等方式向客户端发送即时数据的推送是不同的。

详细的资料可以参考:
《HTTP 协议入门》
《HTTP/2 服务器推送(Server Push)教程》

HTTP/2 协议缺点

因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。由于多个数据流使用同一个 TCP 连接,遵
守同一个流量状态控制和拥塞控制。只要一个数据流遭遇到拥塞,剩下的数据流就没法发出去,这样就导致了后面的所有数据都
会被阻塞。HTTP/2 出现的这个问题是由于其使用 TCP 协议的问题,与它本身的实现其实并没有多大关系。

HTTP/3 协议

由于 TCP 本身存在的一些限制,Google 就开发了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上。 QUIC
协议在 UDP 协议上实现了多路复用、有序交付、重传等等功能

详细资料可以参考:
《如何看待 HTTP/3 ?》

HTTPS 协议

HTTP 存在的问题

  1. HTTP 报文使用明文方式发送,可能被第三方窃听。
  2. HTTP 报文可能被第三方截取后修改通信内容,接收方没有办法发现报文内容的修改。
  3. HTTP 还存在认证的问题,第三方可以冒充他人参与通信。

HTTPS 简介

HTTPS 指的是超文本传输安全协议,HTTPS 是基于 HTTP 协议的,不过它会使用 TLS/SSL 来对数据加密。使用 TLS/
SSL 协议,所有的信息都是加密的,第三方没有办法窃听。并且它提供了一种校验机制,信息一旦被篡改,通信的双方会立
刻发现。它还配备了身份证书,防止身份被冒充的情况出现。

TLS 握手过程

  1. 第一步,客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。
  2. 第二步,服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。
  3. 第三步,客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服
    务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。
  4. 第四步,服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。
  5. 第五步,客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥
    来加密信息。

实现原理

TLS 的握手过程主要用到了三个方法来保证传输的安全。

首先是对称加密的方法,对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就
是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。
这就要用到非对称加密的方法。

非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只
有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户,
都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加
密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。

因此我们可以使用对称加密和非对称加密结合的方式,因为对称加密的方式的缺点是无法保证秘钥的安全传输,因此我们可以
非对称加密的方式来对对称加密的秘钥进行传输,然后以后的通信使用对称加密的方式来加密,这样就解决了两个方法各自存
在的问题。

但是现在的方法也不一定是安全的,因为我们没有办法确定我们得到的公钥就一定是安全的公钥。可能存在一个中间人,截取
了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥
解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而我们自己还不知道。

为了解决这样的问题,我们可以使用数字证书的方式,首先我们使用一种 Hash 算法来对我们的公钥和其他信息进行加密生成
一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合
在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证
处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和我们生成的摘要进行对比,就能发现我们得到的信息是否被更改
了。这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有
这样我们才能保证数据的安全。

详细资料可以参考:
《一个故事讲完 https》
《SSL/TLS 协议运行机制的概述》
《图解 SSL/TLS 协议》
《RSA 算法原理(一)》
《RSA 算法原理(二)》
《分分钟让你理解 HTTPS》

DNS 协议

概况

DNS 协议提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。它是一个由分层的 DNS 服务器组成的分
布式数据库,是定义了主机如何查询这个分布式数据库的方式的应用层协议。DNS 协议运行在 UDP 协议之上,使用 53 号
端口。

域名的层级结构

域名的层级结构可以如下

主机名.次级域名.顶级域名.根域名

# 即

host.sld.tld.root

根据域名的层级结构,管理不同层级域名的服务器,可以分为根域名服务器、顶级域名服务器和权威域名服务器。

查询过程

DNS 的查询过程一般为,我们首先将 DNS 请求发送到本地 DNS 服务器,由本地 DNS 服务器来代为请求。

  1. 从"根域名服务器"查到"顶级域名服务器"的 NS 记录和 A 记录( IP 地址)。
  2. 从"顶级域名服务器"查到"次级域名服务器"的 NS 记录和 A 记录( IP 地址)。
  3. 从"次级域名服务器"查出"主机名"的 IP 地址。

比如我们如果想要查询 www.baidu.com 的 IP 地址,我们首先会将请求发送到本地的 DNS 服务器中,本地 DNS 服务
器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名
服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com
的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服
务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。

DNS 记录和报文

DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为

(Name,Value,Type,TTL)

其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。

常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同。

  1. 如果 Type = A,则 Name 是主机名,Value 是主机名对应的 IP 地址。因此一条记录为 A 的资源记录,提供了标
    准的主机名到 IP 地址的映射。
  2. 如果 Type = NS,则 Name 是个域名,Value 是负责该域名的 DNS 服务器的主机名。这个记录主要用于 DNS 链式
    查询时,返回下一级需要查询的 DNS 服务器的信息。
  3. 如果 Type = CNAME,则 Name 为别名,Value 为该主机的规范主机名。该条记录用于向查询的主机返回一个主机名
    对应的规范主机名,从而告诉查询主机去查询这个主机名的 IP 地址。主机别名主要是为了通过给一些复杂的主机名提供
    一个便于记忆的简单的别名。
  4. 如果 Type = MX,则 Name 为一个邮件服务器的别名,Value 为邮件服务器的规范主机名。它的作用和 CNAME 是一
    样的,都是为了解决规范主机名不利于记忆的缺点。

递归查询和迭代查询

递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归
查询,用户只需要发出一次查询请求。

迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出
多次的查询请求。

一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我
们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次
查询的结果,下一级的查询由本地 DNS 服务器自己进行。

DNS 缓存

DNS 缓存的原理非常简单,在一个请求链中,当某个 DNS 服务器接收到一个 DNS 回答后,它能够将回答中的信息缓存在本
地存储器中。返回的资源记录中的 TTL 代表了该条记录的缓存的时间。

DNS 实现负载平衡

DNS 可以用于在冗余的服务器上实现负载平衡。因为现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应
多个服务器地址。当用户发起网站域名的 DNS 请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在
每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不
同的服务器上,这样来实现负载均衡。

详细资料可以参考:
《DNS 原理入门》
《根域名的知识》

传输层

传输层协议主要是为不同主机上的不同进程间提供了逻辑通信的功能。传输层只工作在端系统中。

多路复用与多路分解

将传输层报文段中的数据交付到正确的套接字的工作被称为多路分解。

在源主机上从不同的套接字中收集数据,封装头信息生成报文段后,将报文段传递到网络层,这个过程被称为多路复用。

无连接的多路复用和多路分解指的是 UDP 套接字的分配过程,一个 UDP 套接字由一个二元组来标识,这个二元组包含了一
个目的地址和一个目的端口号。因此不同源地址和端口号的 UDP 报文段到达主机后,如果它们拥有相同的目的地址和目的端
口号,那么不同的报文段将会转交到同一个 UDP 套接字中。

面向连接的多路复用和多路分解指的是 TCP 套接字的分配过程,一个 TCP 套接字由一个四元组来标识,这个四元组包含了
源 IP 地址、源端口号、目的地址和目的端口号。因此,一个 TCP 报文段从网络中到达一台主机上时,该主机使用全部 4 个
值来将报文段定向到相应的套接字。

UDP 协议

UDP 是一种无连接的,不可靠的传输层协议。它只提供了传输层需要实现的最低限度的功能,除了复用/分解功能和少量的差
错检测外,它几乎没有对 IP 增加其他的东西。UDP 协议适用于对实时性要求高的应用场景。

特点:

  1. 使用 UDP 时,在发送报文段之前,通信双方没有握手的过程,因此 UDP 被称为是无连接的传输层协议。因为没有握手
    过程,相对于 TCP 来说,没有建立连接的时延。因为没有连接,所以不需要在端系统中保存连接的状态。
  2. UDP 提供尽力而为的交付服务,也就是说 UDP 协议不保证数据的可靠交付。
  3. UDP 没有拥塞控制和流量控制的机制,所以 UDP 报文段的发送速率没有限制。
  4. 因为一个 UDP 套接字只使用目的地址和目的端口来标识,所以 UDP 可以支持一对一、一对多、多对一和多对多的交互
    通信。
  5. UDP 首部小,只有 8 个字节。

UDP 报文段结构

UDP 报文段由首部和应用数据组成。报文段首部包含四个字段,分别是源端口号、目的端口号、长度和检验和,每个字段的长
度为两个字节。长度字段指的是整个报文段的长度,包含了首部和应用数据的大小。校验和是 UDP 提供的一种差错校验机制。
虽然提供了差错校验的机制,但是 UDP 对于差错的恢复无能为力。

UDP 报文段结构

TCP 协议

TCP 协议是面向连接的,提供可靠数据传输服务的传输层协议。

特点:

  1. TCP 协议是面向连接的,在通信双方进行通信前,需要通过三次握手建立连接。它需要在端系统中维护双方连接的状态信息。
  2. TCP 协议通过序号、确认号、定时重传、检验和等机制,来提供可靠的数据传输服务。
  3. TCP 协议提供的是点对点的服务,即它是在单个发送方和单个接收方之间的连接。
  4. TCP 协议提供的是全双工的服务,也就是说连接的双方的能够向对方发送和接收数据。
  5. TCP 提供了拥塞控制机制,在网络拥塞的时候会控制发送数据的速率,有助于减少数据包的丢失和减轻网络中的拥塞程度。
  6. TCP 提供了流量控制机制,保证了通信双方的发送和接收速率相同。如果接收方可接收的缓存很小时,发送方会降低发送
    速率,避免因为缓存填满而造成的数据包的丢失。

TCP 报文段结构

TCP 报文段由首部和数据组成,它的首部一般为 20 个字节。

源端口和目的端口号用于报文段的多路复用和分解。

32 比特的序号和 32 比特的确认号,用与实现可靠数据运输服务。

16 比特的接收窗口字段用于实现流量控制,该字段表示接收方愿意接收的字节的数量。

4 比特的首部长度字段,该字段指示了以 32 比特的字为单位的 TCP 首部的长度。

6 比特的标志字段,ACK 字段用于指示确认序号的值是有效的,RST、SYN 和 FIN 比特用于连接建立和拆除。设置 PSH 字
段指示接收方应该立即将数据交给上层,URG 字段用来指示报文段里存在紧急的数据。

校验和提供了对数据的差错检测。

TCP 报文段结构

TCP 三次握手的过程

第一次握手,客户端向服务器发送一个 SYN 连接请求报文段,报文段的首部中 SYN 标志位置为 1,序号字段是一个任选的
随机数。它代表的是客户端数据的初始序号。

第二次握手,服务器端接收到客户端发送的 SYN 连接请求报文段后,服务器首先会为该连接分配 TCP 缓存和变量,然后向
客户端发送 SYN ACK 报文段,报文段的首部中 SYN 和 ACK 标志位都被置为 1,代表这是一个对 SYN 连接请求的确认,
同时序号字段是服务器端产生的一个任选的随机数,它代表的是服务器端数据的初始序号。确认号字段为客户端发送的序号加
一。

第三次握手,客户端接收到服务器的肯定应答后,它也会为这次 TCP 连接分配缓存和变量,同时向服务器端发送一个对服务
器端的报文段的确认。第三次握手可以在报文段中携带数据。

在我看来,TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉对方,什么样序号的报文段能够被正确接收。
第三次握手的作用是客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服务器就没有办法知道自己的序号是否
已被确认。同时这样也是为了防止失效的请求报文段被服务器接收,而出现错误的情况。

详细资料可以参考:
《TCP 为什么是三次握手,而不是两次或四次?》
《TCP 的三次握手与四次挥手》

TCP 四次挥手的过程

因为 TCP 连接是全双工的,也就是说通信的双方都可以向对方发送和接收消息,所以断开连接需要双方的确认。

第一次挥手,客户端认为没有数据要再发送给服务器端,它就向服务器发送一个 FIN 报文段,申请断开客户端到服务器端的
连接。发送后客户端进入 FIN_WAIT_1 状态。

第二次挥手,服务器端接收到客户端释放连接的请求后,向客户端发送一个确认报文段,表示已经接收到了客户端释放连接的
请求,以后不再接收客户端发送过来的数据。但是因为连接是全双工的,所以此时,服务器端还可以向客户端发送数据。服务
器端进入 CLOSE_WAIT 状态。客户端收到确认后,进入 FIN_WAIT_2 状态。

第三次挥手,服务器端发送完所有数据后,向客户端发送 FIN 报文段,申请断开服务器端到客户端的连接。发送后进入 LAS
T_ACK 状态。

第四次挥手,客户端接收到 FIN 请求后,向服务器端发送一个确认应答,并进入 TIME_WAIT 阶段。该阶段会持续一段时间,
这个时间为报文段在网络中的最大生存时间,如果该时间内服务端没有重发请求的话,客户端进入 CLOSED 的状态。如果收到
服务器的重发请求就重新发送确认报文段。服务器端收到客户端的确认报文段后就进入 CLOSED 状态,这样全双工的连接就被
释放了。

TCP 使用四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代
表不能再向对方发送数据,连接处于的是半释放的状态。

最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止发送给服务器的确认报文段丢失或者出错,从而导致服务器
端不能正常关闭。

详细资料可以参考:

《前端面试之道》

状态转化图

客户端状态图

服务端状态图

ARQ 协议

ARQ 协议指的是自动重传请求,它通过超时和重传来保证数据的可靠交付,它是 TCP 协议实现可靠数据传输的一个很重要的
机制。

它分为停止等待 ARQ 协议和连续 ARQ 协议。

一、停止等待 ARQ 协议

停止等待 ARQ 协议的基本原理是,对于发送方来说发送方每发送一个分组,就为这个分组设置一个定时器。当发送分组的确认
回答返回了,则清除定时器,发送下一个分组。如果在规定的时间内没有收到已发送分组的肯定回答,则重新发送上一个分组。

对于接受方来说,每次接受到一个分组,就返回对这个分组的肯定应答,当收到冗余的分组时,就直接丢弃,并返回一个对冗余
分组的确认。当收到分组损坏的情况的时候,直接丢弃。

使用停止等待 ARQ 协议的缺点是每次发送分组必须等到分组确认后才能发送下一个分组,这样会造成信道的利用率过低。

二、连续 ARQ 协议

连续 ARQ 协议是为了解决停止等待 ARQ 协议对于信道的利用率过低的问题。它通过连续发送一组分组,然后再等待对分组的
确认回答,对于如何处理分组中可能出现的差错恢复情况,一般可以使用滑动窗口协议和选择重传协议来实现。

  1. 滑动窗口协议

使用滑动窗口协议,在发送方维持了一个发送窗口,发送窗口以前的分组是已经发送并确认了的分组,发送窗口中包含了已经发
送但未确认的分组和允许发送但还未发送的分组,发送窗口以后的分组是缓存中还不允许发送的分组。当发送方向接收方发送分
组时,会依次发送窗口内的所有分组,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的分组。如果在定
时器的时间内收到某一个分组的确认回答,则滑动窗口,将窗口的首部移动到确认分组的后一个位置,此时如果还有已发送但没
有确认的分组,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送但还未收到确认的分
组。

接收方使用的是累计确认的机制,对于所有按序到达的分组,接收方返回一个分组的肯定回答。如果收到了一个乱序的分组,那
么接方会直接丢弃,并返回一个最近的按序到达的分组的肯定回答。使用累计确认保证了确认号以前的分组都已经按序到达了,
所以发送窗口可以移动到已确认分组的后面。

滑动窗口协议的缺点是因为使用了累计确认的机制,如果出现了只是窗口中的第一个分组丢失,而后面的分组都按序到达的情况
的话,那么滑动窗口协议会重新发送所有的分组,这样就造成了大量不必要分组的丢弃和重传。

  1. 选择重传协议

因为滑动窗口使用累计确认的方式,所以会造成很多不必要分组的重传。使用选择重传协议可以解决这个问题。

选择重传协议在发送方维护了一个发送窗口。发送窗口的以前是已经发送并确认的分组,窗口内包含了已发送但未被确认的分组,
已确认的乱序分组,和允许发送但还未发送的分组,发送窗口以后的是缓存中还不允许发送的分组。选择重传协议与滑动窗口协
议最大的不同是,发送方发送分组时,为一个分组都创建了一个定时器。当发送方接受到一个分组的确认应答后,取消该分组的
定时器,并判断接受该分组后,是否存在由窗口首部为首的连续的确认分组,如果有则向后移动窗口的位置,如果没有则将该分
组标识为已接收的乱序分组。当某一个分组定时器到时后,则重新传递这个分组。

在接收方,它会确认每一个正确接收的分组,不管这个分组是按序的还是乱序的,乱序的分组将被缓存下来,直到所有的乱序分
组都到达形成一个有序序列后,再将这一段分组交付给上层。对于不能被正确接收的分组,接收方直接忽略该分组。

详细资料可以参考:
《TCP 连续 ARQ 协议和滑动窗口协议》

TCP 的可靠运输机制

TCP 的可靠运输机制是基于连续 ARQ 协议和滑动窗口协议的。

TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但
未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。当发送方向接收方发
送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。
如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如
果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送
但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示,
说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发
送但确认的报文段。

接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文
段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都
已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。

发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文
段的发送速率。

但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重
传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。

TCP 的流量控制机制

TCP 提供了流量控制的服务,这个服务的主要目的是控制发送方的发送速率,保证接收方来得及接收。因为一旦发送的速率大
于接收方所能接收的速率,就会造成报文段的丢失。接收方主要是通过接收窗口来告诉发送方自己所能接收的大小,发送方根据
接收方的接收窗口的大小来调整发送窗口的大小,以此来达到控制发送速率的目的。

TCP 的拥塞控制机制

TCP 的拥塞控制主要是根据网络中的拥塞情况来控制发送方数据的发送速率,如果网络处于拥塞的状态,发送方就减小发送的
速率,这样一方面是为了避免继续增加网络中的拥塞程度,另一方面也是为了避免网络拥塞可能造成的报文段丢失。

TCP 的拥塞控制主要使用了四个机制,分别是慢启动、拥塞避免、快速重传和快速恢复。

慢启动的基本思想是,因为在发送方刚开始发送数据的时候,并不知道网络中的拥塞程度,所以先以较低的速率发送,进行试探
,每次收到一个确认报文,就将发动窗口的长度加一,这样每个 RTT 时间后,发送窗口的长度就会加倍。当发送窗口的大小达
到一个阈值的时候就进入拥塞避免算法。

拥塞避免算法是为了避免可能发生的拥塞,将发送窗口的大小由每过一个 RTT 增长一倍,变为每过一个 RTT ,长度只加一。
这样将窗口的增长速率由指数增长,变为加法线性增长。

快速重传指的是,当发送方收到三个冗余的确认应答时,因为 TCP 使用的是累计确认的机制,所以很有可能是发生了报文段的
丢失,因此采用立即重传的机制,在定时器结束前发送所有已发送但还未接收到确认应答的报文段。

快速恢复是对快速重传的后续处理,因为网络中可能已经出现了拥塞情况,所以会将慢启动的阀值减小为原来的一半,然后将拥
塞窗口的值置为减半后的阀值,然后开始执行拥塞避免算法,使得拥塞窗口缓慢地加性增大。简单来理解就是,乘性减,加性增。

TCP 认为网络拥塞的主要依据是报文段的重传次数,它会根据网络中的拥塞程度,通过调整慢启动的阀值,然后交替使用上面四
种机制来达到拥塞控制的目的。

详细资料可以参考:
《TCP 的拥塞控制机制》
《网络基本功:TCP 拥塞控制机制》

网络层

网络层协议主要实现了不同主机间的逻辑通信功能。网络层协议一共包含两个主要的组件,一个 IP 网际协议,一个是路由选
择协议。

IP 网际协议规定了网络层的编址和转发方式,比如说我们接入网络的主机都会被分配一个 IP 地址,常用的比如 IPV4 使用
32 位来分配地址,还有 IPv6 使用 128 位来分配地址。

路由选择协议决定了数据报从源到目的地所流经的路径,常见的比如距离向量路由选择算法等。

数据链路层

数据链路层提供的服务是如何将数据报通过单一通信链路从一个结点移动到相邻节点。每一台主机都有一个唯一的 MAC 地址,
这是由网络适配器决定的,在全世界都是独一无二的。

物理层

物理层提供的服务是尽可能的屏蔽掉组成网络的物理设备和传输介质间的差异,使数据链路层不需要考虑网络的具体传输介质
是什么。

详细资料可以参考:
《搞定计算机网络面试,看这篇就够了(补充版)》
《互联网协议入门(一)》
《互联网协议入门(二)》

常考面试题

1. Post 和 Get 的区别?

Post 和 Get 是 HTTP 请求的两种方法。

(1)从应用场景上来说,GET 请求是一个幂等的请求,一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网
页。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景。比如注册用户这一类的操作。

(2)因为不同的应用场景,所以浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。

(3)从发送的报文格式来说,Get 请求的报文中实体部分为空,Post 请求的报文中实体部分一般为向服务器发送的数据。

(4)但是 Get 请求也可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说,一个方面是不太安全,
因为请求的 url 会被保留在历史记录中。并且浏览器由于对 url 有一个长度上的限制,所以会影响 get 请求发送数据时
的长度。这个限制是浏览器规定的,并不是 RFC 规定的。还有就是 post 的参数传递支持更多的数据类型。

2. TLS/SSL 中什么一定要用三个随机数,来生成"会话密钥"?

客户端和服务器都需要生成随机数,以此来保证每次生成的秘钥都不相同。使用三个随机数,是因为 SSL 的协议默认不信任每个主
机都能产生完全随机的数,如果只使用一个伪随机的数来生成秘钥,就很容易被破解。通过使用三个随机数的方式,增加了自由度,
一个伪随机可能被破解,但是三个伪随机就很接近于随机了,因此可以使用这种方法来保持生成秘钥的随机性和安全性。

3. SSL 连接断开后如何恢复?

一共有两种方法来恢复断开的 SSL 连接,一种是使用 session ID,一种是 session ticket。

使用 session ID 的方式,每一次的会话都有一个编号,当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务器
如果有这个编号的记录,那么双方就可以继续使用以前的秘钥,而不用重新生成一把。目前所有的浏览器都支持这一种方法。但是
这种方法有一个缺点是,session ID 只能够存在一台服务器上,如果我们的请求通过负载平衡被转移到了其他的服务器上,那
么就无法恢复对话。

另一种方式是 session ticket 的方式,session ticket 是服务器在上一次对话中发送给客户的,这个 ticket 是加密的
,只有服务器能够解密,里面包含了本次会话的信息,比如对话秘钥和加密方法等。这样不管我们的请求是否转移到其他的服务器
上,当服务器将 ticket 解密以后,就能够获取上次对话的信息,就不用重新生成对话秘钥了。

4. RSA 算法的安全性保障?

对极大整数做因数分解的难度决定了 RSA 算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA 算法愈可靠。现在102
4位的 RSA 密钥基本安全,2048位的密钥极其安全。

5. DNS 为什么使用 UDP 协议作为传输层协议?

DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议时造成的连接时延。因为为了得到一个域名的 IP 地
址,往往会向多个域名服务器查询,如果使用 TCP 协议,那么每次请求都会存在连接时延,这样使 DNS 服务变得很慢,因为大
多数的地址查询请求,都是浏览器请求页面时发出的,这样会造成网页的等待时间过长。

使用 UDP 协议作为 DNS 协议会有一个问题,由于历史原因,物理链路的最小MTU = 576,所以为了限制报文长度不超过576,
UDP 的报文段的长度被限制在 512 个字节以内,这样一旦 DNS 的查询或者应答报文,超过了 512 字节,那么基于 UDP 的
DNS 协议就会被截断为 512 字节,那么有可能用户得到的 DNS 应答就是不完整的。这里 DNS 报文的长度一旦超过限制,并不
会像 TCP 协议那样被拆分成多个报文段传输,因为 UDP 协议不会维护连接状态,所以我们没有办法确定那几个报文段属于同一
个数据,UDP 只会将多余的数据给截取掉。为了解决这个问题,我们可以使用 TCP 协议去请求报文。

DNS 还存在的一个问题是安全问题,就是我们没有办法确定我们得到的应答,一定是一个安全的应答,因为应答可以被他人伪造,
所以现在有了 DNS over HTTPS 来解决这个问题。

详细资料可以参考:
《为什么 DNS 使用 UDP 而不是 TCP?》

6. 当你在浏览器中输入 Google.com 并且按下回车之后发生了什么?

(1)首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,
将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字
符,则对非法字符进行转义后再进行下一过程。

(2)浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新
的请求。

(3)下一步我们首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果
有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域
名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地
址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用
户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。

(4)当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源
端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给
数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,我们本机的 MAC 地址作为源 MAC 地址,目的 MAC 地
址需要分情况处理,通过将 IP 地址与我们本机的子网掩码相与,我们可以判断我们是否与请求主机在同一个子网里,如果
在同一个子网里,我们可以使用 APR 协议获取到目的主机的 MAC 地址,如果我们不在一个子网里,那么我们的请求应该
转发给我们的网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应
该为网关的地址。

(5)下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接
收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的
确认应答后,进入连接建立的状态,同时向服务器也发送一个 ACK 确认报文段,服务器端接收到确认后,也进入连接建立
状态,此时双方的连接就建立起来了。

(6)如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版
本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证
书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后
发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解
密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加
密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。

(7)当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行
解析,开始页面的渲染过程。

(8)浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端
是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建
立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页
面进行绘制。这个时候整个页面就显示出来了。

(9)最后一步是 TCP 断开连接的四次挥手过程。

详细资料可以参考:
《当你在浏览器中输入 Google.com 并且按下回车之后发生了什么?》

7. 谈谈 CDN 服务?

CDN 是一个内容分发网络,通过对源网站资源的缓存,利用本身多台位于不同地域、不同运营商的服务器,向用户提供资就近访问的
功能。也就是说,用户的请求并不是直接发送给源网站,而是发送给 CDN 服务器,由 CND 服务器将请求定位到最近的含有该资源
的服务器上去请求。这样有利于提高网站的访问速度,同时通过这种方式也减轻了源服务器的访问压力。

详细资料可以参考:
《CDN 是什么?使用 CDN 有什么优势?》

8. 什么是正向代理和反向代理?

我们常说的代理也就是指正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的
服务都被代理服务器代替来请求。

反向代理隐藏了真实的服务端,当我们请求一个网站的时候,背后可能有成千上万台服务器为我们服务,但具体是哪一台,我们不知
道,也不需要知道,我们只需要知道反向代理服务器是谁就好了,反向代理服务器会帮我们把请求转发到真实的服务器那里去。反向
代理器一般用来实现负载平衡。

详细资料可以参考:
《正向代理与反向代理有什么区别》
《webpack 配置 proxy 反向代理的原理是什么?》

9. 负载平衡的两种实现方式?

一种是使用反向代理的方式,用户的请求都发送到反向代理服务上,然后由反向代理服务器来转发请求到真实的服务器上,以此来实
现集群的负载平衡。

另一种是 DNS 的方式,DNS 可以用于在冗余的服务器上实现负载平衡。因为现在一般的大型网站使用多台服务器提供服务,因此一
个域名可能会对应多个服务器地址。当用户向网站域名请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在
每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不同的服
务器上,这样来实现负载均衡。这种方式有一个缺点就是,由于 DNS 服务器中存在缓存,所以有可能一个服务器出现故障后,域名解
析仍然返回的是那个 IP 地址,就会造成访问的问题。

详细资料可以参考:
《负载均衡的原理》

10. http 请求方法 options 方法有什么用?

 OPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能。这个方法会请求服务器返回该资源所支持的所有 HTTP 请
 求方法,该方法会用'*'来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常。JS 的 XMLHttpRequest
 对象进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。

相关资料可以参考:
《HTTP 请求方法》

11. http1.1 和 http1.0 之间有哪些区别?

http1.1 相对于 http1.0 有这样几个区别:
(1)连接方面的区别,http1.1 默认使用持久连接,而 http1.0 默认使用非持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。
(2)资源请求方面的区别,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

(3)缓存方面的区别,在 http1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。

(4)http1.1 中还新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,就可以将请求发往同一台服务器上的不同网站。

(5)http1.1 相对于 http1.0 还新增了很多方法,如 PUT、HEAD、OPTIONS 等。

详细资料可以参考:
《HTTP1.0、HTTP1.1 和 HTTP2.0 的区别》
《HTTP 协议入门》
《网络---一篇文章详解请求头 Host 的概念》

12. 网站域名加 www 与不加 www 的区别?

详细资料可以参考:
《为什么域名前要加 www 前缀 www 是什么意思?》
《为什么越来越多的网站域名不加「www」前缀?》
《域名有 www 与没有 www 有什么区别?》

13. 即时通讯的实现,短轮询、长轮询、SSE 和 WebSocket 间的区别?

短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。

短轮询的基本思路就是浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行
响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客
户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 ht
tp 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。

长轮询的基本思路是,首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将
这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。
客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的
优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。

SSE 的基本思想是,服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通
方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断
地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机
制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不
需要建立过多的 http 请求,相比之下节约了资源。

上面三种方式本质上都是基于 http 协议的,我们还可以使用 WebSocket 协议来实现。WebSocket 是 Html5 定义的一个新协
议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置
比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能
由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。

详细资料可以参考:
《轮询、长轮询、长连接、websocket》
《Server-Sent Events 教程》
《WebSocket 教程》

14. 怎么实现多个网站之间共享登录状态

在多个网站之间共享登录状态指的就是单点登录。多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

我认为单点登录可以这样来实现,首先将用户信息的验证中心独立出来,作为一个单独的认证中心,该认证中心的作用是判断客户端发
送的账号密码的正确性,然后向客户端返回对应的用户信息,并且返回一个由服务器端秘钥加密的登录信息的 token 给客户端,该
token 具有一定的有效时限。当一个应用系统跳转到另一个应用系统时,通过 url 参数的方式来传递 token,然后转移到的应用站
点发送给认证中心,认证中心对 token 进行解密后验证,如果用户信息没有失效,则向客户端返回对应的用户信息,如果失效了则将
页面重定向会单点登录页面。

详细资料可以参考:
《HTTP 是个无状态协议,怎么保持登录状态?》

推荐

笔者再次墙裂推荐收藏这篇原文,转载于CavsZhouyou - 🐜 前端面试复习笔记,这个仓库是原作者校招时的前端复习笔记,主要总结一些比较重要的知识点和前端面试问题,希望对大家有所帮助。

最后如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力😁

查看原文

赞 40 收藏 34 评论 0

jsyzchen 收藏了文章 · 4月11日

Beanstalkd 使用记录

一、安装

1、官网

https://kr.github.io/beanstalkd/

2、安装

yum install beanstalkd --enablerepo=epel

3、启动

/usr/bin/beanstalkd -l 0.0.0.0 -p 11300 -b /var/lib/beanstalkd/binlog -F

-b 开启binlog,断电后重启会自动恢复任务。

4、配置文件

/etc/sysconfig/beanstalkd

二、基本概念

1、Beanstalkd设计里面的核心概念:

  • job:一个需要异步处理的任务,是 Beanstalkd 中的基本单元,需要放在一个 tube 中。

  • tube:一个有名的任务队列,用来存储统一类型的 job,是 producer 和 consumer 操作的对象。

  • producer:Job 的生产者,通过 put 命令来将一个 job 放到一个 tube 中。

  • consumer:Job的消费者,通过 reserve/release/bury/delete 命令来获取 job 或改变 job 的状态。

2、job 的生命周期

图片描述

当producer直接put一个job时,job就处于READY状态,等待consumer来处理,如果选择延迟put,job就先到DELAYED状态,等待时间过后才迁移到READY状态。consumer获取了当前READY的job后,该job的状态就迁移到RESERVED,这样其他的consumer就不能再操作该job。当consumer完成该job后,可以选择delete, release或者bury操作;delete之后,job从系统消亡,之后不能再获取;release操作可以重新把该job状态迁移回READY(也可以延迟该状态迁移操作),使其他的consumer可以继续获取和执行该job;有意思的是bury操作,可以把该job休眠,等到需要的时候,再将休眠的job kick回READY状态,也可以delete BURIED状态的job。正是有这些有趣的操作和状态,才可以基于此做出很多意思的应用,比如要实现一个循环队列,就可以将RESERVED状态的job休眠掉,等没有READY状态的job时再将BURIED状态的job一次性kick回READY状态。

  • READY - 需要立即处理的任务,当延时 (DELAYED) 任务到期后会自动成为当前任务;

  • DELAYED - 延迟执行的任务, 当消费者处理任务后, 可以用将消息再次放回 DELAYED 队列延迟执行;

  • RESERVED - 已经被消费者获取, 正在执行的任务。Beanstalkd 负责检查任务是否在 TTR(time-to-run) 内完成;

  • BURIED - 保留的任务: 任务不会被执行,也不会消失,除非有人把它 "踢" 回队列;

  • DELETED - 消息被彻底删除。Beanstalkd 不再维持这些消息。

3、一些特性

优先级

任务 (job) 可以有 0~2^32 个优先级, 0 代表最高优先级,默认优先级为1024。

持久化

可以通过binlog将job及其状态记录到文件里面,在Beanstalkd下次启动时可以通过读取binlog来恢复之前的job及状态。

分布式容错

分布式设计和Memcached类似,beanstalkd各个server之间并不知道彼此的存在,都是通过client来实现分布式以及根据tube名称去特定server获取job。

超时控制

为了防止某个consumer长时间占用任务但不能处理的情况,Beanstalkd为reserve操作设置了timeout时间,如果该consumer不能在指定时间内完成job,job将被迁移回READY状态,供其他consumer执行。

三、Client Libraries For PHP

项目地址:https://github.com/pda/pheanstalk/

1、Producer 示例:向队列中添加job

$pheanstalk = new Pheanstalk_Pheanstalk('127.0.0.1');

$pheanstalk ->useTube('tubeName') ->put($jobData);

2、Consumer 示例:从队列中取出job

$job = $pheanstalk ->watch('tubeName') ->ignore('default') ->reserve();

echo $job->getData();

$pheanstalk->delete($job);

3、检查服务状态

$isAlive = $pheanstalk->getConnection()->isServiceListening(); //返回 true 或 false

4、获取某一 tube 的数据

try{
    $tubeStatus = $pheanstalk->statsTube('tubeName');
} catch (Exception $e){
    if($e->getMessage()=='Server reported NOT_FOUND'){     //tube 不存在
        $current_jobs_ready = 0;
    }
}

四、其他

1、PHP 版控制台

https://github.com/ptrofimov/beanstalk_console

2、Chrome 插件

https://chrome.google.com/webstore/detail/beanstalkd-dashboard/dakkekjnlffnecpmdiamebeooimjnipm

五、参考资料

  1. https://github.com/kr/beanstalkd/wiki/faq

  2. http://csrd.aliapp.com/?p=1201

查看原文

jsyzchen 收藏了文章 · 4月11日

微服务实战:从架构到发布(一)

引言:“微服务”是当前软件架构领域非常热门的词汇,能找到很多关于微服务的定义、准则,以及如何从微服务中获益的文章,在企业的实践中去应用“微服务”的资源却很少。本篇文章中,会介绍微服务架构(Microservices Architecture)的基础概念,以及如何在实践中具体应用。

单体架构(Monolithic Architecture )

企业级的应用一般都会面临各种各样的业务需求,而常见的方式是把大量功能堆积到同一个单体架构中去。比如:常见的ERP、CRM等系统都以单体架构的方式运行,同时由于提供了大量的业务功能,随着功能的升级,整个研发、发布、定位问题,扩展,升级这样一个“怪物”系统会变得越来越困难。

单体架构的初期效率很高,应用会随着时间推移逐渐变大。在每次的迭代中,开发团队都会面对新功能,然后开发许多新代码,随着时间推移,这个简单的应用会变成了一个巨大的怪物。

1.png
图1:单体架构

大部分企业通过SOA来解决上述问题,SOA的思路是把应用中相近的功能聚合到一起,以服务的形式提供出去。因此基于SOA架构的应用可以理解为一批服务的组合。SOA带来的问题是,引入了大量的服务、消息格式定义和规范。

多数情况下,SOA的服务直接相互独立,但是部署在同一个运行环境中(类似于一个Tomcat实例下,运行了很多web应用)。和单体架构类似,随着业务功能的增多SOA的服务会变得越来越复杂,本质上看没有因为使用SOA而变的更好。图1,是一个包含多种服务的在线零售网站,所有的服务部署在一个运行环境中,是一个典型的单体架构。

单体架构的应用一般有以下特点:

  • 设计、开发、部署为一个单独的单元。

  • 会变得越来越复杂,最后导致维护、升级、新增功能变得异常困难

  • 很难以敏捷研发模式进行开发和发布

  • 部分更新,都需要重新部署整个应用

  • 水平扩展:必须以应用为单位进行扩展,在资源需求有冲突时扩展变得比较困难(部分服务需要更多的计算资源,部分需要更多内存资源)

  • 可用性:一个服务的不稳定会导致整个应用出问题

  • 创新困难:很难引入新的技术和框架,所有的功能都构建在同质的框架之上

微服务架构(Microservices Architecture)

微服务架构的核心思想是,一个应用是由多个小的、相互独立的、微服务组成,这些服务运行在自己的进程中,开发和发布都没有依赖。

多数人对于微服务的定义是,把本来运行在单体架构中的服务拆分成相互独立的服务,并运行在各自的进程中。在我看来,不仅如此。最关键的地方在于,不同的服务能依据不同的业务需求,构建的不同的技术架构之上,并且聚焦在有限的业务功能之上。

因此,在线零售网站可以用图2的微服务架构来简单概括。基于业务需求,需要增加一个账户服务微服务,因此构建微服务绝不是在单体架构中把服务拆分开这么简单。

2.png
图2:微服务架构

微服务设计:规模、范围、业务功能

你可能从零开始用微服务来构建应用,也可能重构现有系统,确定微服务的规模,范围和功能都特别重要。让我们讨论一些有关微服务设计的关键问题和对它的误解:

  • “微”很容易被误解:很多开发者会倾向于把服务往尽量小的颗粒度去做

  • 在SOA方式下,服务都还是以单体架构在运行,用于支持不同的功能。如果依旧采用SAO类似的服务,仅仅是名义上叫做微服务,并不能带来任何微服务的优势。

那我们在微服务中应该怎样设计呢。以下是微服务的设计指南:

  • 职责单一原则(Single Responsibility
    Principle):把某一个微服务的功能聚焦在特定业务或者有限的范围内会有助于敏捷开发和服务的发布。

  • 设计阶段就需要把业务范围进行界定。

  • 需要关心微服务的业务范围,而不是服务的数量和规模尽量小。数量和规模需要依照业务功能而定。

  • 于SOA不同,某个微服务的功能、操作和消息协议尽量简单。

  • 项目初期把服务的范围制定相对宽泛,随着深入,进一步重构服务,细分微服务是个很好的做法。

微服务消息

在单体架构中,不同功能之间通信通过方法调用,或者跨语言通信。SOA降低了这种语言直接的耦合度,采用基于SOAP协议的web服务。这种web服务的功能和消息体定义都十分复杂,微服务需要更轻量的机制。

同步消息 – REST, Thrift

同步消息就是客户端需要保持等待,直到服务器返回应答。REST是微服务中默认的同步消息方式,它提供了基于HTTP协议和资源API风格的简单消息格式,多数微服务都采用这种方式(每个功能代表了一个资源和对应的操作)。

Thrift是另外一个可选的方案。它采用接口描述语言定义并创建服务,支持可扩展的跨语言服务开发,所包含的代码生成引擎可以在多种语言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等创建高效的、无缝的服务,其传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发、大数据量和多语言的环境更有优势。

3.png
图3:REST接口,对外微服务

异步消息 – AMQP, STOMP, MQTT

异步消息就是客户端不需要一直等待服务应答,有应到后会得到通知。某些微服务需要用到异步消息,一般采用AMQP, STOMP, MQTT。

消息格式 – JSON, XML, Thrift, ProtoBuf, Avro

消息格式是微服务中另外一个很重要的因素。SOA的web服务一般采用文本消息,基于复杂的消息格式(SOAP)和消息定义(xsd)。微服务采用简单的文本协议JSON和XML,基于HTTP的资源API风格。如果需要二进制,通过用到Thrift, ProtoBuf, Avro。

服务约定 – 定义接口 – Swagger, RAML, Thrift IDL

如果把功能实现为服务,并发布,需要定义一套约定。单体架构中,SOA采用WSDL,WSDL过于复杂并且和SOAP紧耦合,不适合微服务。

REST设计的微服务,通常采用Swagger和RAML定义约定。

对于不是基于REST设计的微服务,比如Thrift,通常采用IDL(Interface Definition Languages),比如Thrift IDL。

微服务集成 (服务间通信)

微服务架构下,应用的服务直接相互独立。在一个具体的商业应用中,需要有些机制支持微服务之间通信。因此服务间的通信机制特别重要。

SOA体系下,服务之间通过企业服务总线(Enterprise Service Bus)通信,许多业务逻辑在中间层(消息的路由、转换和组织)。微服务架构倾向于降低中心消息总线(类似于ESB)的依赖,将业务逻辑分布在每个具体的服务终端。

大部分微服务基于HTTP、JSON这样的标准协议,集成不同标准和格式变的不再重要。另外一个选择是采用轻量级的消息总线或者网关,有路由功能,没有复杂的业务逻辑。下面就介绍几种常见的架构方式。

点对点方式 – 直接调用服务

点对点方式中,服务之间直接用。每个微服务都开放REST API,并且调用其它微服务的接口。

1-444.png
图4:通过点对点方式通信

很明显,在比较简单的微服务应用场景下,这种方式还可行,随着应用复杂度的提升,会变得越来越不可维护。这点有些类似SOA的ESB,尽量不采用点对点的集成方式。

点对点有下面几个缺点:

  • 非功能的需求,比如用户授权、限制、监控,需要在每个微服务中进行实现

  • 随着功能的演进,服务会变得越来越复杂。

  • 不同的服务直接,客户端和服务直接没有控制功能(监控、跟踪、过滤)

  • 直接通信在大型系统设计中,一般是反面典型。

因此,如果设计一个大型的微服务系统,尽量避免点对点的通信方式,也不能像ESB这样重量级的总线。而是一个轻量级的总线,能够提供非业务功能的抽象。这就是API网关方式。

API-网关方式

API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能个。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。

1-5.png
图5:通过API-网关暴露微服务

用我们网上商店的例子,在图5中,所有的业务接口通过API网关暴露,是所有客户端接口的唯一入口。微服务之间的通信也通过API网关。

采用网关方式有如下优势:

  • 有能力为微服务接口提供网关层次的抽象。比如:微服务的接口可以各种各样,在网关层,可以对外暴露统一的规范接口。

  • 轻量的消息路由、格式转换。

  • 统一控制安全、监控、限流等非业务功能。

  • 每个微服务会变得更加轻量,非业务功能个都在网关层统一处理,微服务只需要关注业务逻辑

目前,API网关方式应该是微服务架构中应用最广泛的设计模式。

消息代理方式

微服务也可以集成在异步的场景下,通过队列和订阅主题,实现消息的发布和订阅。一个微服务可以是消息的发布者,把消息通过异步的方式发送到队列或者订阅主题下。作为消费者的微服务可以从队列或者主题共获取消息。通过消息中间件把服务之间的直接调用解耦。

1-6
图6:异步通信方式

通常异步的生产者/消费者模式,通过AMQP、MQTT等异步消息规范。

数据的去中心化

单体架构中,不同功能的服务模块都把数据存储在某个中心数据库中。

1-7.png
图7:单体架构,用一个数据库存储所有数据

微服务方式,多个服务之间的设计相互独立,数据也应该相互独立(比如,某个微服务的数据库结构定义方式改变,可能会中断其它服务)。因此,每个微服务都应该有自己的数据库。

1_8.png
图8:每个微服务有自己私有的数据库,其它微服务不能直接访问。

数据去中心话的核心要点:

  • 每个微服务有自己私有的数据库持久化业务数据

  • 每个微服务只能访问自己的数据库,而不能访问其它服务的数据库

  • 某些业务场景下,需要在一个事务中更新多个数据库。这种情况也不能直接访问其它微服务的数据库,而是通过对于微服务进行操作。

数据的去中心化,进一步降低了微服务之间的耦合度,不同服务可以采用不同的数据库技术(SQL、NoSQL等)。在复杂的业务场景下,如果包含多个微服务,通常在客户端或者中间层(网关)处理。

下篇文章会介绍微服务实战的其它内容:管理去中心化、服务的注册和发现、安全、事务、失败的设计、其它。


原文作者:Kasun Indrasiri,软件架构师,WSO2
原文链接:https://dzone.com/articles/mi...
翻译系_力谱宿云LeapCloud团队_云服务研发成员:Frank Qin
中文首发自: 微服务实战:从架构到发布(一)
欢迎关注微信公众号:MaxLeap_yidongyanfa

关于MaxLeap
MaxLeap移动云服务平台为企业提供一站式的移动研发和运营云服务,帮助企业快速研发和上线移动应用,平台提供数据云存储,云引擎,支付管理,IM,数据分析和营销自动化等服务。
官网链接:https://maxleap.cn

查看原文

jsyzchen 收藏了文章 · 4月11日

使用git和github管理自己的项目---基础操作学习

我是通过看廖雪峰的git教程学习的,真的是极好的,以下是我学习过程中的总结,记录下来,方便自己参考以熟悉git和github的使用。

除了廖雪峰的教程,还比较推荐在看完廖雪峰的教程之后,再看这样一篇文章:使用git和github进行协同开发流程,和我的另一篇git的笔记:使用git和github管理自己的项目---真实开发环境的策略 ,深入了解github和git怎么在具体的项目开发中管理项目、有什么好的规范!

我这里记录git的学习笔记,方便随时参考,具体的git的原理github的原理集中式和分布式版本控制的区别版本控制的概念和原理Git和其他版本控制工具比如SVN的区别、以及更多的相关概念讲解,请自己参见廖雪峰的git教程

我强烈推荐先看廖雪峰的git教程来系统化认识和学习git,然后自己整理一份具体的步骤方便平时开发时快速参考,或者直接参考我的。

先说一个比较好笑的事:廖雪峰的git教程中,刚开始的部分的浏览量有38万,但是最后一页教程的网页的浏览量是1万多。

以前因为觉得git、github比较难,所以一直拖着不学,现在去尝试学习,一晚上就OK了

  • 重点之一是找到全面的、详细的、生动的、高质量的教程。廖雪峰的git教程是我目前找到的最好的一个。之前找到的讲解git用法的书《pro git》现在看来是没有必要再看了!

  • 重点之二就是自己立即去学,其实没什么难的,不要被貌似难的表象、概念唬住。

  • 重点之三是最好能自己做简单的、系统的整理,方便自己随时再回来查看、复习、参考,不至于出现这样的情况:学完以后过一段时间之后再用,但是这时候发现忘了差不多了!

  • 重点之四就是立即动手去用,否则学完很快就忘了。

有钱的买mac,没钱的用ubuntu--或者其他的linux发行版、被逼无奈的用Windows--但是被逼之余的自主时间一定要远离Windows。

现在我的测试环境是ubuntu。

以对一个文件的管理为例,简单说明git的使用。另外需要说明的是下面的实验过程主要是只针对一个文件、并且修改的次数往往只有一次,而在真正的项目中,往往有大量的文件,也可能多次修改后才合并,合并时候的冲突可能也不只一两个,等等。不过原理都是一样的,我想说的是,不要局限在这个教程的示例上,请自己通过教程掌握基本的远原理之后,自己推广、去大量的实践,最重要的是要制定一个好的版本控制的策略(合理分工、安排,还是尽可能的避免冲突为好),这个可以参考:使用git和github进行协同开发流程以及我的学习笔记使用git和github管理自己的项目---真实开发环境的策略

1.创建版本库

  • sudo apt-get install git 先安装git

  • 先创建目录,作为仓库

  • git init 初始化仓库,可以发现当前目录下多了一个.git的目录,这个目录是Git来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把Git仓库给破坏了

  • vim readme.txt 新建一个文本文件,比如往里面添加简单的一行字符串

  • git add readme.txt 添加一个文件,比如readme.txt,如果目录里面的所有文件都要添加,可以git add *

  • git commit-m "添加一个readme.txt文件" 将文件提交到仓库,并加上说明(这时候是版本1)

  • 如果是第一次使用git,那么git commit可能报错,所以需要你配置一些个人信息

  • git config --global user.email "you@example.com" 配置邮件

  • git config --global user.name "Your Name" 配置用户名

  • 必须配置,否则后面的commit、push到远程库都会失败

  • 然后再次git commit -m "添加一个readme.txt文件" 才会成功

2.提交修改

  • 假如此时第一次修改了readme.txt文件

  • git status 让我们时刻掌握仓库当前的状态。这时告诉我们,readme.txt被修改过了,但还没有准备提交的修改。

  • git diff readme.txt 查看对readme.txt做了什么修改

  • git add readme.txt 提交修改和提交新文件是一样,先git add

  • git status 可以再用git status查看仓库的当前状态,告诉我们,将要被提交的修改包括readme.txt

  • git commit-m "第一次修改" 然后再git commit,并添加修改的描述(这时候是版本2)

  • git status 可以再执行git status看仓库状态,因为所有的都提交了,Git告诉我们当前没有需要提交的修改,而且,工作目录是干净(working directory clean)的。

3.版本回退

  • 你可以像上面所说的那样不停的提交新的文件、提交对文件的修改

  • 这时候第二次修改readme.txt文件

  • git add readme.txt 先git add

  • git commit -m "第二次修改" 提交第二次修改(这时候是版本3)

  • git log 显示从最近到最远的提交日志,具体显示的内容自己试一试看看

  • git log --pretty=oneline 如果嫌输出信息太多,看得眼花缭乱,试试加上--pretty=oneline参数

  • 这篇教程去理解为什么Git的版本号要这么长,Git的版本号类似:3628164fb26d48395383f8f31179f24e0882e1e0 这样的特别长的十六进制数。

  • git reset --hard HEAD^ 会回退到上一个版本,也就是从版本3回退到版本2

  • 在Git中,用HEAD表示当前版本,也就是最新的提交3628164...882e1e0(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100

  • vim readme.txt 可以看到此时的readme.txt文件就是版本2时候的内容,回退成功!

  • git log 此时看到版本3的信息没有了

  • git reset --hard 3628164 通过命令行上的历史信息(假如你没清屏的话),找到版本3 的版本号,不一定要全部的版本号,就像这个命令的例子,只要前面的约7、8位这样就可以指定回到版本3

  • vim readme.txt 看到的是第三版本的readme.txt文件的内容,所以又回来了

  • Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向你要回退的那个版本

  • git reflog 记录你的每一次命令,最先显示的是这个命令执行之后的版本的版本号的前七位,这样就算你清屏了或者重启了,也能找到某个版本的版本号,就可以轻松回退到那个版本

4.工作区、版本库和暂存区

工作区:就是你在电脑里能看到的目录,比如我的learngit文件夹就是一个工作区。

版本库:工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。

暂存区:Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。

前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:

  1. 第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;

  2. 第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。

因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。

你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。

详细知识见这篇教程。必须理解暂存区、工作区、版本库。这些都是是Git非常重要的概念,弄明白了这些概念,就弄明白了Git的很多操作到底干了什么。没弄明白的话,请反复看!!

5.管理修改

为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。

什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。

通过实例讲解什么叫跟踪修改,要想理解,请参考原文结合暂存区的知识理解:

  • vim readme.txt 编辑文件,比如添加新的一行

  • git add readme.txt 添加,但是不提交

  • vim readme.txt 再编辑文件,比如再添加一行

  • git commit -m "修改两次,添一次,提交一次" 提交

  • git status 看到的效果是:只提交了第一次的修改,第二次的修改没有提交

那怎么提交第二次修改呢?你可以继续git addgit commit,也可以别着急提交第一次修改,先git add第二次修改,再git commit,也就是第一次修改 -> git add -> 第二次修改 -> git add -> git commit,就相当于把两次修改合并后一块提交了。

6.撤销修改

第一种情况

  • 修改了readme.txt文件,还没有git add 和git commit

  • 但是在你提交之前发现这次修改有问题。既然错误发现得很及时,就可以很容易地纠正它。你可以手动把文件恢复到上一个版本的状态。

  • git checkout -- readme.txt 也可以通过命令撤销修改,这条命令的意思就是,把readme.txt文件在工作区的修改全部撤销

  • 无论是文件修改后值存在于工作区还没有放到暂存区,还是已经添加到暂存区,总之这个命令就是让这个文件回到最近一次git commit或git add时的状态。

  • 查看文件,内容果然复原了。git checkout -- file命令中的--很重要,没有--,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout命令。

第二种情况

  • 修改了readme.txt文件,而且执行了git add readme.txt

  • 庆幸的是你在 git commit 之前发现了这个问题

  • git status 查看一下,修改只是添加到了暂存区,还没有提交

  • git reset HEAD readme.txt 可以把暂存区的修改撤销掉,重新放回工作区。git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。

  • git status 查看一下,现在暂存区是干净的,工作区有修改

  • git checkout -- readme.txt 还记得第一种情况中如何丢弃工作区的修改吧

第三种情况

现在,假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得版本回退一节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。

还记得Git是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把错误的修改(如果是影响很大的错误)提交推送到远程版本库,你就真的惨了……

区别对待本地版本库和远程版本库!

7.删除文件

在Git中,删除也是一个修改操作

  • 添加一个新的文件 test.txt

  • git add test.txt

  • git commit test.txt -m "再次新增一个文件"

  • 一般情况下,你通常会在文件管理器中把没用的文件删除,或者直接rm test.txt

  • git status 这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了

  • 现在你有两个选择,一是确实从版本库中删除该文件,那就git rm test.txt,然后git commit 文件就从版本库中删除了

  • 另一种情况是删除错了,因为版本库里还有,所以可以轻松地将误删除的文件恢复到最新版本git checkout -- test.txt git checkout其实使用版本库中的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”

8.添加远程库

要想学习这部分的知识,请先参考下面的:a.配置连接远程仓库Github

  • 假如现在你已经配置好github,并且在github上添加了learngit仓库。

  • git remote add origin git@github.com:michaelliao/learngit.git 这个命令是在本地的learngit仓库下执行的,前面通过learngit仓库为例我们已经讲过在本地创建和操作git仓库。这两个地方的仓库名不需要相同,因为会通过在本地的仓库目录下执行这条命令(命令中包含远程库的名字)已经将两者建立了联系

  • 请千万注意,把上面的michaelliao替换成你自己的GitHub账户名,否则,你在本地关联的就是我的远程库,关联没有问题,但是你以后推送是推不上去的,因为你的SSH Key公钥不在我的账户列表中。

  • git push -u origin master 把本地库的所有内容推送到远程库上。把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。

  • 然后去Github对应的远程库看看,都已经推送上去了。

  • 此后,每次本地提交后,只要有必要,就可以使用命令git push origin master推送最新修改。

这样你就可以在Github上托管你的项目代码、vim的配置文件和插件、重要的文档……

现在我的vim的配置文件和插件已经同步到Github上了:https://github.com/xumenger/m...

另外推荐我的关于vim配置的文章::http://segmentfault.com/a/119...

9.从远程库克隆

  • 假设我的github上面有一个远程库,但是本地没有,需要克隆到本地,远程库的名字叫'gitskills'

  • git clone git@github.com:michaelliao/gitskills.git 克隆一个本地库

  • cd gitskills 进入克隆下来的本地库,默认的名字是和github上的一样的

  • ls -al 可以看到本地的克隆库里面是和远程库里面的一样的

  • 如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。

你也许还注意到,GitHub给出的地址不止一个,还可以用https://github.com/michaellia...这样的地址。实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。

使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https。

10.分支管理

分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。

但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

11.创建和合并分支

首先教程中会详细讲解分支的原理(分支、指针、工作区……),一定要好好看!!看完之后你才能对你的创建分支和合并分支的操作不只是会用,更能在用的时候没有任何疑惑!反正能学到更多的知识,何乐而不为!

另外推荐这样的博客:使用git和github进行协同开发流程以及我的学习笔记使用git和github管理自己的项目---真实开发环境的策略

版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,我们练习的learngit,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。

开始实战:

  • git checkout -b dev 创建一个新的分支:dev,并且会切换到dev分支。所以这条命令有两个作用。git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:git branch devgit checkout dev

  • 补充:所有的git管理的项目刚开始时候默认有一条分支:master

  • git branch 查看当前所在的分支。git branch命令会列出所有分支,当前分支前面会标一个*号。

  • 因为切换到dev分支,所以我们现在可以在dev分支上正常提交,比如对readme.txt做一个修改

  • git add readme.txt

  • git commit -m "提交到dev分支"

  • git checkout master 现在,dev分支的工作完成,我们就可以切换回master分支

  • *注意:*切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变

  • git merge dev 这是在master分支上执行的命令,作用是:把dev分支上的工作成果合并到master分支上

  • git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward,我们后面会将其他方式的合并。

  • git branch -d dev 合并完成之后,可以放心的删除dev分支了

  • git branch 删除后,查看branch,只剩下master了

12.解决冲突

教程中有详细的图文说明,很形象,很好!一定要参考!

人生不如意之事十之八九,合并分支往往也不是一帆风顺的。

  • git checkout -b feature1 创建新的分支feature1,并且换到这个分支,进行新的实验

  • 在feature1分支下,假如将readme.txt的最后一行由"test branch" 改为"test feature1"

  • git add readme.txt

  • git commit -m "在feature1上修改readme.txt的最后一行" 在feature1分支上提交

  • git checkout master 切换到master分支。Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。

  • 在master分支下,假如将readme.txt的最后一行由"test branch" 改为"test master"因为上面的是在feature1上进行的修改,所以切换回master之后,看到的文件并不是在feature1上修改后的文件

  • git add readme.txt

  • git commit -m "又在master上修改了readme.txt文件" 在master上也提交修改

  • 现在,master分支和feature1分支各自都分别有新的提交

  • git merge feature1 在master分支上执行该命令,与feature1分支合并。这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,果然冲突了!Git告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交

  • git status git status也可以告诉我们冲突的文件

  • 这时候使用vim等编辑器打开readme.txt文件可以看到已经在readme.txt文件中将冲突的信息已经添加到里面了,Git用<<<<<<<=======>>>>>>>标记出不同分支的内容

  • 然后我们编辑readme.txt文件,处理冲突,将内容改成我们想要的样子

  • git add readme.txt

  • git commit -m "解决冲突" 在master上提交

  • git log --graph --pretty=oneline --abbrev-commit 用带参数的git log可以看到分支的合并情况。用git log --graph命令可以看到分支合并图。

  • git branch -d feature1 最后删除feature分支,完成工作。

2015.09.09 今天就学到这里,实在太晚了,赶紧睡觉,明天还得工作!什么都没有身体重要!
明天继续:分支管理策略

a.配置连接远程仓库Github

首先看这篇文章了解git和SVN的区别,毕竟现在必须在工作中使用的就是SVN,所以还是弄清楚两者的区别。

Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。

实际情况往往是这样,找一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。

在继续阅读后续内容前,请自行注册GitHub账号。由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:

  1. 创建SSH Key。在用户目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsaid_rsa.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key,输入命令ssh-keygen -t rsa -C "youremail@example.com",你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有id_rsaid_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。

  2. 登陆GitHub,打开“Account settings”,“SSH Keys”页面.然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容:
    图片描述

  3. 点“Add Key”,你就应该看到已经添加的Key:
    图片描述

注意现在的Github的页面的布局可能和图片中显示有细小的差别,不过相信你能找到对应的操作!

为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。

当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。

最后友情提示,在GitHub上免费托管的Git仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。

如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。另一个办法是自己动手,搭一个Git服务器,因为是你自己的Git服务器,所以别人也是看不见的。这个方法我们后面会讲到的,相当简单,公司内部开发必备。

现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。具体可以见教程

首先,登陆GitHub,然后,在右上角找到“Create a new repo”按钮,创建一个新的仓库:
图片描述

在Repository name填入learngit,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库:

图片描述

目前,在GitHub上的这个learngit仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。

查看原文

jsyzchen 收藏了文章 · 4月11日

JavaScript MVC 框架技术选型

你很喜欢Gmail和Trello之类的单页面应用,但是不太确定该从何开始。也许你的JavaScript代码是如此的杂乱无章,以致于你很想在下一个项目上尝试下JavaScript MVC库和框架,却苦于没有头绪?我正在撰写一本单页面应用的书,所以我阅读了大量网上的相关资料。在这里我尝试提供一些看法,希望可以帮助你下决定。

简介

JavaScript MVC frameworks

这里讨论的是时下最热的框架,AngularJS、Backbone、Ember和Knockout。同时提到了Batman、CANjs、Meteor和Spine,但是没有详细展开。

我们从多个不同的角度考察每个项目,包括社区、领导、成熟度、大小、依赖、互操作性、启发、理念和特性。

社区

社区是一个衡量任何开源项目健康程度的重要指数。以下表格显示了GitHub上每个项目的关注者数量。

GitHub watchers

你当然不该仅仅根据这些数据做决定,但是它们确实为你提供了关于这些框架的一些感性认识:

最主流:

  • Backbone.js
  • AngularJS

正在高速成长的:

  • AngularJS
  • Meteor
  • Ember
  • Knockout

关注总量较低但是增长迅猛的:

  • CANjs

增长性

特别值得注意的是AngularJS 13个月以来的惊人增长(379%)。在你做决定的时候要考虑上这一点。下面的图表比较了13个月以来GitHub关注者的增长速度,可以看出某个项目的社区成长的速度。考虑到原先的社区大小,Meteor(130%)、Ember(104%)、Knockout(76%)和Backbone(64%)的增长速度也很惊人。

Growth

领导

了解项目的核心开发者的背景,他们创建框架时尝试解决的问题,有助于你欣赏他们在设计上的决策和动机。例如,David Heinemeier Hansson,流行的Ruby on Rails框架的缔造者,是37signals的签约开发者,从事项目设计,每周只有10小时能花在开发框架上。Ruby on Rails事实上是从他和37signals的签约工作中提取出来的。这一背景有助于你理解为什么这个框架需要将开发效率提升到极限——这意味这使用大量的约定(已经做出的决定)和支架程序(生成的代码)。下面,我将介绍JavaScript MVC 框架的缔造者,也许也能激起你对他们的工作的欣赏。

Backbone

Jeremy Ashkenas和DocumentCloud

Jeremy Ashkenas

Jeremy Ashkenas是CoffeeScript编程语言、Backbone.js JavaScript框架和Underscore.js JavaScript工具库的创立者。根据维基百科,他现在在NYTimes/DocumentCloud从事互动新闻方面的开发。

图片来自The Canadian University Software Engineering Conference

AngularJS

iško Hevery和Adam Abrons

AngularJS最初由Google的Miško Hevery和Adam Abrons于2009年开发,当时是一个在线JSON存储服务的一部分。Abrons后来离开了这个项目,但是在Google工作的Hevery继续开发,和Google的员工Igor Minár、Vojta Jína一起维护这个库。

图片来自Devoxx 2012

Knockout

Steve Sanderson

Steve Sanderson是Knockout的原作者。Steve Sanderson现在为微软工作,他所在的团队开发ASP.NET、IIS和其他web项目。他以前以外包开发者或顾问身份为Bristol周边的客户开发.NET下的软件,他还为Apress写了一些书,包括《Pro ASP.NET MVC框架》。

图片来自Steve Sanderson的博客

Ember

Ember核心成员中最知名的公众人物是Yehuda Katz和Tom Dale。

Yehuda Katz是Ember.js、Ruby on Rails和jQuery的核心开发者,他白天的时间花在他创办的创业公司,Tilde Inc.。Yehuda是畅销书《jQuery in Action》和《Rails 3 in Action》的作者之一。

Tom Dale原先在SproutCore团队工作。他以前是苹果软件工程师,在开发MobileMe和iCloud应用时精通了前端JavaScript技能。

YehudaTom Dale

图片来自Ember Team

Meteor

Meteor的开发团队刚刚得到了1.12千万美金,所以他们可以全职开发。他们的团队有12名开发者,每个开发者的简历都让人印象深刻。这个团队拥有雄心勃勃的计划,超越了大多数专注于组织客户端代码和状态的JavaScript MVC框架。Meteor是一个全端框架,包括服务器架构和数据库。

成熟度

评估框架的成熟度可以帮助你理解在项目中使用新技术的风险。未经考验的新框架在文档、扩展性、稳定性(API改变)和支持(寻找了解该框架的开发者来维护代码)方面可能存在问题,这些问题可能导致出乎意料的结果,即使从其他方面看起来决策很明智。需要考虑的项目包括:多少生产环境下的应用使用这些框架?这些应用有多少用户?文档是否良好?例子和教程是否充足?例子是否过时?API是否稳定?其他开发者了解或正打算了解这门技术么?

  • Backbone(最成熟)

    • 大量3年以上的生产环境应用,包括GroupOn、 FourSquare、USAToday、DocumentCloud。
    • 良好的文档。
    • 良好的示例代码,但是很多都过时了。
    • API非常稳定。
    • GitHub上有大量关注者。
  • AngularJS(成熟)

    • Google将其用于生产环境,但是不像其他项目一样拥有长久的历史记录。
    • 良好且持续改进的文档。
    • 大量的示例代码。
    • GitHub上有大量关注者。
  • Knockout(成熟)

    • 用于生产环境至今已有2年。
    • 良好的文档,内含类似jsfiddle的代码样例。
    • API很稳定。
    • 大量的示例代码。
    • GitHub上有大量关注者。
  • Ember.js

    • 在两年的开发之后,第一个可用于生产的发布(1.0)在2013年8月30日放出。
    • 文档正在改善。
    • API在1.0之前都不稳定(这是有意的)。
    • 大量示例代码,由于1.0前的API变动,有些已经过时。
    • GitHub上有大量关注者。
  • Meteor

    • 处于开发的早期阶段,较多地用于示例应用
    • 文档不错,然而尚不完整
    • API仍处于变动之中。
    • 一些示例代码。
    • GitHub上有大量关注者。

大小

了解每个框架的大小,它会在你的应用中增加多少分量是很重要的。大小会影响性能,不过它同时也会暗示你这个框架的雄心有多大,你学习它可能需要花多少时间,以及它提供多少帮助你构建应用的方式(即特性和鲁棒性)。一个框架的野心越大,特性越多,通常也就意味更难在应用的页面上将它与其他部件组合。轻量的框架更像一个库,将他集成到你的项目中所耗费的精力也相对较小。

size

包括Backbone和Spine在内的一些项目为自己的轻量而自豪,这些项目更多地将自己视作库,而不是框架。通常这些小型的框架留下了空间,你可以使用你自己的库来实现特定功能,例如模板和路由。我在讨论这些框架的特性的时候将继续讨论这个话题。

包括Ember和AngularJS在内的其他项目雄心勃勃,更适合叫做框架。它们通常拥有更多内建的特性,更少依赖外部库。

下面的列表显示了我更倾向于将哪些项目归入库或框架:

框架
BackboneEmber
KnockoutAngularJS
SpineBatman
CanJSMeteor

依赖性

使用这些项目构建真实世界的应用的时候还需要哪些库?以下的图表展示了为了保证开发效率每个库所需的依赖,以及这些依赖的大小。

通过从cdnjs下载库,我们收集了这些数据。在实践中,大多数的项目会使用jQuery配合这些框架处理DOM,因为需要动画和AJAX。在移动应用中,使用Zepto.js来代替jQuery处理DOM不是什么稀奇的事。Zepto.js是一个比jQuery轻量得多的库。虽然Zepto.js不支持Internet Explorer,但是移动应用通常不需要为此操心。AngularJS包含了一个jQuery 的缩减版jQLite。但是如果你在项目中使用了jQuery的话,它会被覆盖。AngularJS团队鼓励开发者,如非必要,不要添加完整的jQuery库。为了帮助你做出正确的选择,下面的表格同时显示了移动版(假定使用Zepto.js)和web版(假定使用jQuery)。

size of javascript mvc frameworks w dependencies web app

size of javascript mvc frameworks w dependencies mobile app

互操作性

这一部分讨论是否框架设计为控制整个页面或者它可以被用于现存页面的一个部分——你可能想渐渐将新技术引入现有的项目。前面的库和框架的讨论基本可以体现每个项目的互操作性,库更倾向于很容易地集成到现存的项目,而框架为你做更多的事,但是不容易和其他项目配合。

AngularJS

AngularJS可以和其他库很好地配合,但是它鼓励开发者们考虑是否可以不用jQuery和jQueryUI.事实上Angular内置了一个jQuery的子集jqLite。遵循这一实践的理由是让单元测试更容易,因为很多依赖库和插件设计的时候没有考虑单元测试,相应地更难和单元测试配合。在实践中,大多数的开发者最终还是因为某些特性使用了jQuery.

Backbone

由于Backbone的小尺寸和无预设的架构,将其包含在众多流行的客户端库和服务器端技术中很容易。

Ember.js

被设计为在运行时控制整个页面,所以不太适合用于页面的部分。

Knockout.js

可以在项目中作为小组件使用,不控制整个页面。

启发

记者采访音乐家时最爱问的问题是“你在成长的时候听哪些艺术家的音乐,或者说,谁启发了你?”这个问题常常使得读者能够预期音乐家的声乐。这些框架中大部分的观念都不是全新的,而是来自于创造者以前工作的项目中喜欢的部分。这一部分总结了我从框架创造者的访谈中收集到的关于启发的信息。

AngularJS

HTML类的声明性的语言,Adobe和Flex、微软的WPF\Silverligt等RIA技术给AngularJS的影响很深。这些声明性技术没有”主体“方法,仅仅表达需要发生什么,而不指定具体实现。视图和模型中的数据双向绑定是这一声明式编程风格在绝佳例子。此外,在Google的服务器端Java代码中大量使用的依赖注入和IOC容器(特别是Juice)也启发了AngularJS的创造者。他们重视单元测试,需要框架被设计允许依赖注入,这样测试就可以从其他应用层剥离出来,运行起来也会更快。

Ember

Tom Dale在Quora上谈了Ember受到的影响

Ember.js刚开始开发的时候,我们从Cocoa等本地应用程序框架引入一些概念,不过后来我们觉得这些概念弊大于利,或者说它们和Web应用程序格格不入。因此,我们开始从Ruby on Rails和Backbone.js 等开源项目中寻找灵感。因此,Ember.js结合了本地应用的强大和现代web的轻量。

此外,Ember.js是SproutCore JavaScript库的进化版,SproutCore停止仿效Cocoa而更多地借鉴jQuery的时候,Ember诞生了,理解这一点很重要。

Knockout

hanselminutes的播客提供了Steve Sanderson受到哪些方面启发的背景信息。总结一下,MVVM设计模式和微软的WPF、Sliverlight等声明性技术是最大的启发者。你可能会发现Knockout的最佳特性——声明性的数据双向绑定——和Anjular相似,因为两者的启发者是相似的。

理念

报纸在报道新闻的时候努力保持中立。唯一的例外是编者案,鼓励表达观点,作者通常在问题上占据一个强烈的立场。但是大多数情况,这两者既不是严格的中立报道,也不是强烈的意见表达,而是位于两者之间的连续统。技术框架也有类似的划分,即是否强主张。例如,Ruby on Rails推崇约定优于配置,并且为开发者做了大量决定,包括文件结构和数据访问。相应地,它被认为是强主张的。其他Sinatra类的服务器端框架更轻量,并不预设文件结构和数据访问。相应的,被看成是无主张的。服务端框架有理念,客户端JavaScript MVC框架同样有,我们讨论的框架也可以被置于强主张和无主张的连续统中考察。让我们看看每个项目,然后讨论他们的理念。

Backbone: 无主张

Backbone是最开明的框架,极度无主张,允许开发者做出自己的决定,有时这会导致代码差异过大而难以维护。唯一的例外是Backbone假定服务器端有一个REST服务,我会在特性部分详细讨论这一点。这一假定可以通过覆盖模型的sync方法来绕过。

AngularJS: 强主张

AngularJS有较强的主张,特别是它强调可测试性和依赖注入。此外,HTML类的声明性编程很棒的理念也在框架中广泛体现。

Ember: 极度强主张

Ember力求开发者仅对应用的特有部分作决定,其余全部交给约定和支架。这个理念和Ruby on Rails和jQuery很接近。这一理念最好的表达出现在emberjs.com的网站上:

不要浪费时间做那些无关紧要的选择。Ember.js吸纳了常见的惯用法,因此你可以专注于应用的特殊部分,而不是重新发明轮子。

Ember标准化了文件和url结构,当然,有必要的时候也允许你覆盖这些设定。你可以预期的是大量的代码会为你生成,大量的类似文件结构的约定。相应地,你需要做出的常规选择更少,因为框架已经为你选定了合理的默认值,你可以着手构建你的应用的特殊部分。

Knockout: 无主张

路由和数据存储留给开发者决定。不预设文件或URL结构。甚至允许用基于字符串的模板替换声明性的基于DOM的模板。

特性

我们可以将这些JavaScript MVC框架看成是帮助开发者构建单页应用的常用特性集合。每个框架实现这些特性的方式,或者不实现这些特性的方式(通过其他库来补全框架的功能)是值得留心的。

JavaScript MVC框架的主要特性是什么?

  • HTML和客户端JavaScript对象模型的双向绑定
  • 视图模板
  • 数据存储(本地存储或者通过服务器存储到数据库)
  • URL路由(确保后退按钮和搜索引擎正常工作)

除此以外,一些框架提供常用的语言层面的服务,例如通用的pub/sub事件模型和面向对象的继承支持。

数据绑定

这是最受吹捧的特性。你通过HTML input修改了数据,绑定到input的JavaScript对象马上更新,其他绑定的用户接口元素也随之更新。在很多框架中,反之亦然。如果你修改了JavaScript对象,html会自动刷新。这是一个web上的双向的数据绑定,我们在Flex、Windows Forms、WPF等富客户端应用框架中见过这类绑定。以下的表格显示了哪些框架支持数据绑定。

data binding

有些人可能持有异议,因为Backbone和Spine部分支持数据绑定。但是我觉得大量工作需要留给开发者,保险起见,不如说这些库不支持这些特性。

视图模板

客户端的JavaScript数据模型需要穿插在HTML中,这些框架采用两种方式解决问题。

基于字符串的模板(目前最流行的是handlebars.js),将字符串、文本模板中的动态部分替换为模型中的数据。字符串常被提到也饱受争议的特性之一是性能。其缺点是调试控制语句类的逻辑很困难。

基于DOM的模板拥抱标记语言的声明式本性,这是开了挂的html,通过html中的附加属性来描述需要的绑定和事件。这些库需要的代码大大减少,为开发者做了很多事情。

view templates

模型(可观察的:跟踪改动)

一些框架(Backbone、Spine)更专注于模型,要求开发者在base模型的基础上扩展JavaScript类,通过.get().set()访问属性,这样就能跟踪改动,模型的变动也会触发事件。KnockoutJS让开发者在原始的JavaScript对象上应用可察封装,然后通过object.propertyName()访问属性(小心,别丢了括号)。

其他库(AngularJS)对页面上的所有绑定的DOM元素作脏检查,因为没有标准的get和set访问器。因此将这些库用于大页面时将导致性能问题。这些库不仅需要更少的刷新模板的代码,也不需要你使用特定的get和set访问器来修改模型中的数据,所以你可以使用原始的JavaScript对象。这大大提升了开发效率,这一点在框架的初学者身上体现得尤为明显。

数据存储

这些框架通过以下方式将数据存储到服务器

  • 与REST服务自动同步
  • 需要开发者自行实现ajax调用,调用针对返回json的web服务
  • 同时允许以上两种方式
REST

一些框架默认预设后端有非常整洁的REST JSON服务,并且,至少在默认的情况下,前端与后端频繁交互,在后台异步更新数据,而用户界面响应流畅。这些框架内部使用jQuery或Zepto发送合适的AJAX请求给服务器。用户界面的HTML DOM元素监听应用的JavaScript对象模型的改动,同步机制得到模型属性的更改提醒,将改动发送给REST服务,确保模型和服务器同步。

在线和离线

Backbone默认在客户端保存数据之前发送请求,这样服务器端和客户端同步就很容易。和Backbone非常类似的Spine框架,采用了不同的方式,在异步发送请求到服务器前,先在客户端存储记录,这提供了更好的用户界面响应,在移动应用中常发生的离线状态下也能工作。如果你的项目需要支持离线,需要了解清楚框架对该特性的支持

DIY

这些框架要求开发者使用$.ajax(jQuery)来调用服务,或者添加兼容的开源库处理数据存储需求。

数据存储特性

Meteor之类精心制作的框架拥有更完整的数据存储方案,但是要求服务器端有MongoDB数据库。这类方案试图提供一个扩展性优异的默认解决方案,提供从头到尾的JavaScript开发体验。

以下的表格总结了每个框架是如何处理数据存储的。

路由

将URL路由映射到JavaScript函数,可以支持浏览器的后退按钮。单页应用最大的缺点之一是由于页面无刷新,浏览器历史不会添加条目,所以后退按钮通常无法将用户带回页面的前一状态,除非开发者在主要的状态改变时做一些额外的工作,通过在URL后附加井号,或者,使用现代浏览器的push和pop状态,实现状态跟踪机制。总之,大多数项目都提供基本、初步而有用的功能。Knockout的做法很简单,允许你使用其他的第三方开源库。

routing

苹果对苹果

考察各个框架的特性之后,我发现我并不是在做一个“苹果对苹果”的比较。一个更为公平的比较也许是将AngularJS和EmberJS之类全面的框架与配合第三方库使用的Backbone和KnockoutJS之类的MV*框架进行比较。具体来说,下面的比较会更有意义:

  • AngularJS
  • EmberJS
  • Backbone 搭配 Marionette
  • KnockoutJS 搭配 DurandalJS、HistoryJS或SammayJS。

以后的博客中我会继续深入。

告诉我更多

为项目选择JavaScript MVC框架时有大量需要考虑的事项,我希望本文是一个良好的开始。请在评论中分享你使用这些框架的体验,包括它们的优异之处,也包括它们使用上的陷阱。


原文 Choosing a JavaScript MVC Framework
翻译 SegmentFault

查看原文

jsyzchen 收藏了文章 · 4月11日

基于Redis实现的延迟队列

delay-queue

基于Redis实现的延迟队列, 参考有赞延迟队列设计实现

项目地址

应用场景

  • 订单超过30分钟未支付,自动关闭

  • 订单完成后, 如果用户一直未评价, 5天后自动好评

  • 会员到期前15天, 到期前3天分别发送短信提醒

猜想

支付宝异步通知时间间隔是如何实现的(通知的间隔频率一般是:2m,10m,10m,1h,2h,6h,15h)

订单支付成功后, 生成通知任务, 放入消息队列中.
任务内容包含Array{0,0,2m,10m,10m,1h,2h,6h,15h}和通知到第几次N(这里N=1, 即第1次).
消费者从队列中取出任务, 根据N取得对应的时间间隔为0, 立即发送通知.

第1次通知失败, N += 1 => 2
从Array中取得间隔时间为2m, 添加一个延迟时间为2m的任务到延迟队列, 任务内容仍包含Array和N

第2次通知失败, N += 1 => 3, 取出对应的间隔时间10m, 添加一个任务到延迟队列, 同上
......
第7次通知失败, N += 1 => 8, 取出对应的间隔时间15h, 添加一个任务到延迟队列, 同上
第8次通知失败, N += 1 => 9, 取不到间隔时间, 结束通知

实现原理

利用Redis的有序集合,member为JobId, score为任务执行的时间戳,

每秒扫描一次集合,取出执行时间小于等于当前时间的任务.

依赖

  • Redis

运行

./delay-queue -c delay-queue.conf

HTTP Server监听0.0.0.0:9277, Redis连接地址127.0.0.1:6379, 数据库编号1

HTTP接口

  • 请求方法 POST

  • 请求Body及返回值均为json

返回值

{
  "code": 0,
  "message": "添加成功",
  "data": null
}
参数名类型含义备注
codeint状态码0: 成功 非0: 失败
messagestring状态描述信息
dataobject, null附加信息

添加任务

URL地址 /push

{
  "topic": "order",
  "id": "15702398321",
  "delay": 3600,
  "ttr": 120,
  "body": "{\"uid\": 10829378,\"created\": 1498657365 }"
}
参数名类型含义备注
topicstringJob类型
idstringJob唯一标识需确保JobID唯一
delayintJob需要延迟的时间, 单位:秒
ttrintJob执行超时时间, 单位:秒
bodystringJob的内容,供消费者做具体的业务处理,如果是json格式需转义

轮询队列获取任务

服务端会Hold住连接, 直到队列中有任务或180秒后超时返回,
任务执行完成后需调用finish接口删除任务, 否则任务会重复投递, 消费端需能处理同一任务的多次投递

URL地址 /pop

{
  "topic": "order"
}
参数名类型含义备注
topicstringJob类型

队列中有任务返回值

{
  "code": 0,
  "message": "操作成功",
  "data": {
    "id": "15702398321",
    "body": "{\"uid\": 10829378,\"created\": 1498657365 }"
  }
}

队列为空返回值

{
  "code": 0,
  "message": "操作成功",
  "data": null
}

删除任务

URL地址 /delete

{
  "id": "15702398321"
}
参数名类型含义备注
idstringJob唯一标识

完成任务

URL地址 /finish

{
  "id": "15702398321"
}
参数名类型含义备注
idstringJob唯一标识
查看原文

jsyzchen 收藏了文章 · 4月11日

Sentry的使用

公司项目里需要加入,于是自己倒腾了下,就总结了下入门使用手法,哈哈,欢迎指点,希望能帮助大家~~~

Sentry 自我理解,可以更便捷的了解到非必现的bug、无法通过日志追踪的异常等。

一、Sentry介绍:

sentry 是一个实时事件日志记录和聚合平台。它专门用于监视错误和提取执行适当的事后操作所需的所有信息, 而无需使用标准用户反馈循环的任何麻烦。
Sentry 是一个日志平台,分为客户端和服务端,客户端(目前客户端有Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web节目方便查看。Sentry由python编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest等

二、Sentry的注册和使用

公司已有现成的服务了,所以我们只需要用公司邮箱进行注册即可。

下面介绍一下简单的使用方法:

2.1 首先注册账号: 分为UAT 和线上(ONLINE)

online: https://sentry-fe... // 这里是正式线上链接

uat: http://sentry... // 这里是uat环境链接

(公司邮箱注册)

TLYhFvRNXW.gif

新注册的账号可能无法New Project 创建,这时候需要点击左侧栏目,点击 Project & Teams ,需要加入Teams,这是刷新页面,则可以在Select project 下面看到你的项目,如果无项目请联系管理员;

YqwsIuXTtX.gif

进入后进行简单配置,然后右上角可以点击 New Project 创建,选择需要项目类型,根据提示进行配置

c2CHwXF4ct.gif

选择相对应的技术

izBwzIPLN3.gif

下面这个生成的链接也就是日后项目实时监听bug的地址(项目中的sentry.ts文件中需要配置的这个)

847I23cYbJ.gif

2.2 sentry在项目中的配置

a. 首先在项目中下载依赖
$ yarn add raven-js -D
b. sentry配置文件

(目前有一套配置文件,可直接饮用,也可对里面的错误警报规则做修改)
如:

Fs88JMk64A.gif

c. sentry 在项目中的引用:
import * as Raven from "raven-js"; // 首先引入
import { ravenOptions, DSN_ONLINE, DSN_UAT } from "config/sentry"; // 上面对sentry的配置文件
import { isLocalEnv, isUatEnv } from "utils/url";  // 封装的页面链接获取

componentDidMount() {
    if (!isLocalEnv) {
            const dsn = isUatEnv ? DSN_UAT : DSN_ONLINE;
            Raven.config(
                dsn,
                Object.assign({}, ravenOptions, {
                    release: __webpack_global__.SENTRY_RELEASE
// 这里注意了,__webpack_global__相当于是全局的,需要在tsconfig.json里进行配置,深层的原理和关联可以追踪看ezpack库
                })
            ).install();
        }
}

//  componentDidCatch 错误捕获
componentDidCatch(error, errorInfo) {
        const group = errorInfo ? location.href : "default";
        Raven.captureException(error, { extra: errorInfo, fingerprint: [group] });
    }

<br/>

Xa9IoSkx10.gif
07LOdoo0p4.gif

以上是在项目中添加sentry最简单的配置使用,当完成到这里的时候,可以进行测试 比如在项目中添加
throw new Error("test error");
发布在环境上进行测试,在自己的账号项目中检查是否监听到错误。
以上只是简单地配置在项目中了,当然,我们可以将它运用的更友好,比如在请求的时候加上:
const captureException = (err, option) => {
    Raven.captureException(err, {
        fingerprint: ["API", option.url],
        message: err,
        extra: option
    });
};
//  在请求时,发生错误处理方法中可以加入此配置,在监听到错误时,详细信息会包括fingerprint: 类型, message: 错误信息, extra: 请求链接的信息以及连接等, 都将在 sentry监听到的详情里展现出来。
还有一些其他用法,待研究后更新。。。(^__^) 嘻嘻……

推荐参考资料:

【参考资料1】
【参考资料2】

查看原文

jsyzchen 收藏了文章 · 4月11日

工作中经常用到github上优秀、实用、轻量级、无依赖的插件和库

原文收录在我的 GitHub博客 ( https://github.com/jawil/blog ) ,喜欢的可以关注最新动态,大家一起多交流学习,共同进步,以学习者的身份写博客,记录点滴。

按照格式推荐好用的插件有福利哦,说不定会送1024论坛邀请码,好自为之,你懂的,嘿嘿嘿。

由于github的issues没有TOC菜单栏导航,所以这里方便大家查看,先安利一款Chrome浏览器的插件,感谢github用户@BBcaptain 推荐。点击我呀,进入商店,自备梯子,如果不会翻墙,赶紧转行。。。

效果预览,是不是很方便,图片较多,建议等待一会或者多刷新几下:

Echo.js – 简单易用的图片延迟加载插件

github:https://github.com/toddmotto/...

官方网站:https://toddmotto.com/echo-js...

star:3k+

Install:

npm:npm install echo-js
bower:bower install echojs

大小:2KB

功能介绍:
  Echo.js 是一个独立的延迟加载图片的 JavaScript 插件。Echo.js 不依赖第三方库,压缩后不到1KB大小。 延迟加载是提高网页首屏显示速度的一种很有效的方法,当图片元素进入窗口可视区域的时候,它就会改变图像的 src 属性,从服务端加载所需的图片,这也是一个异步的过程。
  
Demo:
效果预览地址:https://jawil.github.io/demo/...
Demo源码:https://github.com/jawil/jawi...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Lazyr.js – 延迟加载图片(Lazy Loading)

github:https://github.com/callmecavs...

官方网站:http://callmecavs.com/layzr.js/

star:5k+

Install:

npm:npm install layzr.js --save

大小:2.75 KB

功能介绍:
  Lazyr.js 是一个小的、快速的、现代的、相互间无依赖的图片延迟加载库。通过延迟加载图片,让图片出现在(或接近))视窗才加载来提高页面打开速度。这个库通过保持最少选项并最大化速度。

Demo:
跟上面的Echo.js用法类似,喜欢的可以自行去尝试,这里就不再演示了,我一般用Echo.js。






better-scroll.js – 小巧,灵活的 JavaScript 模拟滚动条的插件

github:https://github.com/ustbhuangy...

官方网站:https://ustbhuangyi.github.io...

star:300+

Install:

npm install better-scroll --save-dev
import BScroll from 'better-scroll';

大小:24 KB

功能介绍:
  better-scroll 是一个只有24.8KB的 JavaScript 模拟浏览器自带滚动条的插件,是在iscroll开源的基础上进行优化的一款插件,简单好用,轻巧高性能,功能强大,API通俗易懂,是一款优秀的scroll插件,抛弃原生滚动条,从现在做起。

Demo:
效果预览地址:https://jawil.github.io/demo/... (PC端切换到移动模式)
Demo源码:https://github.com/jawil/webp...
注:在ustbhuangyi的源码下改进了一下,做成多页面,技术栈:webpack2+vue.js2+sass+axios

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






better-picker – 一款轻量级IOS风格的JavaScript选择器

github:https://github.com/ustbhuangy...

官方网站:http://ustbhuangyi.github.io/...

star:200+

Install:

npm: npm install better-picker --save-dev
import Picker from 'better-picker'

大小:46.5 KB

功能介绍:
  移动端最好用的的筛选器组件,高仿 ios 的 UIPickerView ,非常流畅的体验,原生 JS 实现,不依赖任何插件和第三方库。
Demo:
效果预览地址:http://ustbhuangyi.github.io/...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Sortable – 一款用于实现元素拖拽排序的功能的插件

github:https://github.com/RubaXa/Sor...

官方网站:http://rubaxa.github.io/Sorta...

star:9k+

Install:

Bower: bower install sortablejs --save
npm: npm install sortablejs 

大小:5 KB

功能介绍:
 Sortable:现代浏览器上用于实现元素拖拽排序的功能,支持 Meteor, AngularJS, React,不依赖 jQuery这玩意。

Demo:
效果预览地址:http://rubaxa.github.io/Sorta...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







slick – 功能异常强大的一个图片滑动切换效果库

github:https://github.com/kenwheeler...

官方网站:http://kenwheeler.github.io/s...

star:17k+

Install:

Bower: bower install slick-carousel --save
npm: npm install slick-carousel
CDNs:
https://cdnjs.com/libraries/slick-carousel
https://www.jsdelivr.com/projects/jquery.slick

大小:40 KB

功能介绍:
 slick 是一个功能异常强大的一个图片滑动切换效果库,接口丰富,支持各种动画和各种样式的切换滑动,唯一的缺点就是基于jQuery,基本废了,现在没人喜欢用jQuery,该淘汰了。。。支持 RequireJS 以及 Bower 安装。

Demo:
效果预览地址:http://kenwheeler.github.io/s...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







swipe – 非常轻量级的一个图片滑动切换效果库

github:https://github.com/lyfeyaj/Swipe

官方网站:http://lyfeyaj.github.io/swipe/

star:200+

Install:

Bower: bower install swipe-js  --save
npm: npm install swipe-js 

大小:5 KB

功能介绍:
 swipe:非常轻量级的一个图片滑动切换效果库, 性能良好, 尤其是对手机的支持, 压缩后的大小约 5kb。可以结合 jQuery、RequireJS 使用。

Demo:
效果预览地址:http://lyfeyaj.github.io/swipe/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Slideout.js – 触摸滑出式 Web App 导航菜单

github:https://github.com/mango/slid...

官方网站:https://slideout.js.org/

star:6k+

Install:

npm:npm install slideout
spm:spm install slideout
bower:bower install slideout.js
component:component install mango/slideout
<script data-original="https://cdnjs.cloudflare.com/ajax/libs/slideout/1.0.1/slideout.min.js"></script>

大小:4 KB

功能介绍:
  Slideout.js 是为您的移动 Web 应用开发的触摸滑出式的导航菜单。它没有依赖,自由搭配简单的标记,支持原生的滚动,您可以轻松地定制它。它支持不同的 CSS3 转换和过渡。最重要的是,它只是4KB 。

Demo:
效果预览地址:https://slideout.js.org/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






SweetAlert – 替代 Alert 的漂亮的提示效果

github:https://github.com/t4t5/sweet...

官方网站:http://t4t5.github.io/sweetal...

star:15k+

Install:

bower:bower install sweetalert
npm:npm install sweetalert
<script data-original="dist/sweetalert.min.js"></script>
<link rel="stylesheet" type="text/css" href="dist/sweetalert.css">

大小:16 KB

功能介绍:
  Sweet Alert 是一个替代传统的 JavaScript Alert 的漂亮提示效果。SweetAlert 自动居中对齐在页面中央,不管您使用的是台式电脑,手机或平板电脑看起来效果都很棒。另外提供了丰富的自定义配置选择,可以灵活控制。

Demo:
效果预览地址:http://t4t5.github.io/sweetal...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

类似插件:limonte/sweetalert2,好像这个最近还在更新,这个感觉更漂亮,大同小异,这里不多做介绍。

github:https://github.com/limonte/sw...

官方网站: https://limonte.github.io/swe...






Awesomplete.js - 比datalist更强大更实用,零依赖的简单自动补全插件

github:https://github.com/leaverou/a...

官方网站:http://leaverou.github.io/awe...

star:5k+

Install:
`npm: npm install awesomplete
`

大小:5 KB

功能介绍:
 Awesomplete 是一款超轻量级的,可定制的,简单的自动完成插件,零依赖,使用现代化标准构建。你可以简单地添加 awesomplete 样式,让它自动处理(你仍然可以通过指定 HTML 属性配置更多选项),您可以用几行 JS 代码,提供更多的自定义。

Demo:
效果预览地址:http://leaverou.github.io/awe...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

Cleave.js – 自动格式化表单输入框的文本内容

github:https://github.com/nosir/clea...

官方网站:http://nosir.github.io/cleave...

star:6k+

Install:

npm:npm install --save cleave.js
bower:bower install --save cleave.js

大小:11.1 KB

功能介绍:
  Cleave.js 有一个简单的目的:帮助你自动格式输入的文本内容。 这个想法是提供一个简单的方法来格式化您的输入数据以增加输入字段的可读性。通过使用这个库,您不需要编写任何正则表达式来控制输入文本的格式。然而,这并不意味着取代任何验证或掩码库,你仍应在后端验证数据。它支持信用卡号码、电话号码格式(支持各个国家)、日期格式、数字格式、自定义分隔符,前缀和块模式等,提供 CommonJS/AMD 模式以及ReactJS 组件端口。

Demo:
效果预览地址:http://nosir.github.io/cleave...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。
输入201748自动格式化成2017-04-08,是不是很方便





Immutable.js – JavaScript 不可变数据集合(Facebook出品)

github:https://github.com/facebook/i...

官方网站:http://facebook.github.io/imm...

star:18k+

Install:

npm install immutable --S-D

大小:60 KB

功能介绍:
  不可变数据是指一旦创建就不能被修改的数据,使得应用开发更简单,允许使用函数式编程技术,比如惰性评估。Immutable JS 提供一个惰性 Sequence,允许高效的队列方法链,类似 map 和 filter ,不用创建中间代表。Immutable.js 提供持久化的列表、堆栈、Map, 以及 OrderedMap 等,最大限度地减少需要复制或缓存数据。

Demo:

<script data-original="immutable.min.js"></script>
<script>
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
</script>

更多信息和探讨请移步,这里不多做介绍:facebook immutable.js 意义何在,使用场景?






Popmotion.js – 小巧,灵活的 JavaScript 运动引擎

github:https://github.com/Popmotion/...

官方网站:https://popmotion.io/

star:3k+

Install:

npm install --save popmotion
import { tween } from 'popmotion';

大小:12 KB

功能介绍:
  Popmotion 是一个只有12KB的 JavaScript 运动引擎,可以用来实现动画,物理效果和输入跟踪。原生的DOM支持:CSS,SVG,SVG路径和DOM属性的支持,开箱即用。Popmotion 网站上有很多很赞的效果,赶紧去体验一下。

Demo:
效果预览地址:http://codepen.io/popmotion/p...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。







Dynamics.js - 创建逼真的物理动画的 JS 库

github:https://github.com/michaelvil...

官方网站:http://dynamicsjs.com/

star:6k+

Install:

npm: npm install dynamics.js
bower: bower install dynamics.js

大小:20 KB

功能介绍:
  Popmotion 是一个只有12KB的 JavaScript 运动引擎,可以用来实现动画,物理效果和输入跟踪。原生的DOM支持:CSS,SVG,SVG路径和DOM属性的支持,开箱即用。Popmotion 网站上有很多很赞的效果,赶紧去体验一下。

Demo:
效果预览地址:http://dynamicsjs.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Rainyday.js – 使用 JavaScript 实现雨滴效果

github:https://github.com/maroslaw/r...

官方网站:http://maroslaw.github.io/rai...

star:5k+

Install:

在github的dist目录下载rainyday.min.js

大小:10 KB

功能介绍:
 Rainyday.js 背后的想法是创建一个 JavaScript 库,利用 HTML5 Canvas 渲染一个雨滴落在玻璃表面的动画。Rainyday.js 有功能可扩展的 API,例如碰撞检测和易于扩展自己的不同的动画组件的实现。它是一个使用 HTML5 特性纯 JavaScript 库,支持大部分现代浏览器。

Demo:
效果预览地址:http://maroslaw.github.io/rai...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。

Swiper – 经典的移动触摸滑块轮播插件

github:https://github.com/nolimits4w...

官方网站:http://idangero.us/swiper/

star:11.8k+

Install:

在github上下载

大小:暂不确定,按需引入

功能介绍:
 Swiper 是移动 Web 开发中最常用的滑块插件,是一款免费的,最现代化的移动触摸滑块,支持硬件加速的转换和惊人的原生表现。它的目的是在移动网站,移动 Web 应用程序和 Hygrid 混合应用程序中使用。最初的设计主要是为 iOS,但同时也支持最新的 Android,Windows Phone 8 和现代的桌面浏览器。

Demo:
效果预览地址:http://idangero.us/swiper/#.W...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Snabbt.js – 极简的 JavaScript 动画库

github:https://github.com/daniel-lun...

官方网站:http://daniel-lundin.github.i...

star:5k+

Install:

bower:bower install snabbt.js
npm:npm install snabbt.js
CDNs:
https://cdnjs.com/libraries/snabbt.js
http://www.jsdelivr.com/#!snabbt

大小:16 KB

功能介绍:
 Snabbt.js 是一个简约的 JavaScript 动画库。它会平移,旋转,缩放,倾斜和调整你的元素。通过矩阵乘法运算,变换等可以任何你想要的方式进行组合。最终的结果通过 CSS3 变换矩阵设置。

Demo:
效果预览地址:http://daniel-lundin.github.i...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






imagesLoaded – 检测网页中的图片是否加载完成

github:https://github.com/desandro/i...

官方网站:http://imagesloaded.desandro....

star:6k+

Install:

Bower: bower install imagesloaded --save
npm: npm install imagesloaded
CDNs:
<script data-original="https://unpkg.com/imagesloaded@4.1/imagesloaded.pkgd.min.js"></script>
<script data-original="https://unpkg.com/imagesloaded@4.1/imagesloaded.pkgd.js"></script>

大小:5 KB

功能介绍:
 imagesLoaded 是一个用于来检测网页中的图片是否载入完成的 JavaScript 工具库。支持回调的获取图片加载的进度,还可以绑定自定义事件。可以结合 jQuery、RequireJS 使用。

Demo:
效果预览地址:http://codepen.io/desandro/fu...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Fort.js – 时尚、现代的表单填写进度提示效果

github:https://github.com/idriskhenc...

官方网站:https://github.com/idriskhenc...

star:800+

Install:

CDN:
css:
https://cdnjs.cloudflare.com/ajax/libs/Fort.js/2.0.0/fort.min.css
js:
https://cdnjs.cloudflare.com/ajax/libs/Fort.js/2.0.0/fort.min.js

大小:6 KB

功能介绍:
  Fort.js 是一款用于时尚、现代的表单填写进度提示效果的 JavaScript 库,你需要做的就是添加表单,剩下的任务就交给 Fort.js 算法了,使用非常简单。提供了Default、Gradient、Sections 以及 Flash 四种效果,满足开发的各种场合需要。

Demo:
效果预览地址:http://idriskhenchil.github.i...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






MagicSuggest – Bootstrap 主题的多选组合框

github:https://github.com/nicolasbiz...

官方网站:http://nicolasbize.com/magics...

star:1k+

Install:

github自行进行下载

大小:21.8 KB

功能介绍:
  MagicSuggest 是专为 Bootstrap 主题开发的多选组合框。它支持自定义呈现,数据通过 Ajax 异步获取,使用组件自动过滤。它允许空间免费项目,也有动态加载固定的建议。

Demo:
效果预览地址:http://nicolasbize.com/magics...

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Numeral.js – 格式化和操作数字的 JavaScript 库

github:https://github.com/adamwdrape...

官方网站:http://numeraljs.com/

star:4k+

Install:

npm: npm install numeral
CDNs:
<script data-original="//cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js"></script>

大小:10 KB

功能介绍:
   Numeral.js 是一个用于格式化和操作数字的 JavaScript 库。数字可以格式化为货币,百分比,时间,甚至是小数,千位,和缩写格式,功能十分强大。支持包括中文在内的17种语言。

Demo:

var myNumeral = numeral(1000);

var value = myNumeral.value();
// 1000

var myNumeral2 = numeral('1,000');

var value2 = myNumeral2.value();
// 1000






Draggabilly – 轻松实现拖放功能(Drag & Drop)

github:https://github.com/desandro/d...

官方网站:http://draggabilly.desandro.com/

star:2k+

Install:

Bower: bower install draggabilly --save
npm: npm install draggabilly
CDNs:
<script data-original="https://npmcdn.com/draggabilly@2.1/dist/draggabilly.pkgd.min.js"></script>
<script data-original="https://npmcdn.com/draggabilly@2.1/dist/draggabilly.pkgd.js"></script>

大小:5 KB

功能介绍:
 Draggabilly 是一个很小的 JavaScript 库,专注于拖放功能。只需要简单的设置参数就可以在你的网站用添加拖放功能。兼容 IE8+ 浏览器,支持多点触摸。可以灵活绑定事件,支持 RequireJS 以及 Bower 安装。

Demo:
效果预览地址:http://draggabilly.desandro.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






Quill – 可以灵活自定义的开源的富文本编辑器

github:https://github.com/quilljs/qu...

官方网站:https://quilljs.com

star:12k+

Install:

npm: npm install quill
CDNs:
<!-- Main Quill library -->
<script data-original="//cdn.quilljs.com/1.0.0/quill.js" type="text/javascript"></script>
<script data-original="//cdn.quilljs.com/1.0.0/quill.min.js" type="text/javascript"></script>

<!-- Theme included stylesheets -->
<link href="//cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet">
<link href="//cdn.quilljs.com/1.0.0/quill.bubble.css" rel="stylesheet">

<!-- Core build with no theme, formatting, non-essential modules -->
<link href="//cdn.quilljs.com/1.0.0/quill.core.css" rel="stylesheet">
<script data-original="//cdn.quilljs.com/1.0.0/quill.core.js" type="text/javascript"></script>

大小:需求不同,大小不同

功能介绍:
  Quill 的建立是为了解决现有的所见即所得(WYSIWYG)的编辑器本身就是所见即所得(指不能再扩张)的问题。如果编辑器不正是你想要的方式,这是很难或不可能对其进行自定义以满足您的需求。

  Quill 旨在通过把自身组织成模块,并提供了强大的 API 来构建额外的模块来解决这个问题。它也并没有规定你用样式来定义编辑器皮肤。Quill 还提供了所有你希望富文本编辑器说用于的功能,包括轻量级封装,众多的格式化选项,以及广泛的跨平台支持。

Demo:
效果预览地址:https://quilljs.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






basket.js – 基于 LocalStorage 的资源加载器

github:https://github.com/addyosmani...

官方网站:https://addyosmani.com/basket...

star:2k+

Install:

Bower: bower install basket.js --save
npm: npm install basket.js

大小:4 KB

功能介绍:
 basket.js是一款基于 LocalStorage 的资源加载器,可以用来缓存 script 和 css, 手机端使用速度快于浏览器直接缓存。
 
Demo:
效果预览地址:https://addyosmani.com/basket...
更多示例请查看官方文档






scrollReveal.js – 使元素以非常酷帅的方式进入画布 (Viewpoint)

github:https://github.com/jlmakes/sc...

官方网站:https://scrollrevealjs.org/

star:12k+

Install:

Bower: bower install scrollreveal --save
npm: npm install scrollreveal
CDNs:
<script data-original="https://unpkg.com/scrollreveal/dist/scrollreveal.min.js"></script>

大小:8 KB

功能介绍:
 scrollReveal.js 是一个用于使元素以非常酷帅的方式进入画布的 JavaScript 工具库。轻量级,高性能,无依赖的一个小巧酷炫的库。
 
Demo:
效果预览地址:https://scrollrevealjs.org/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






moment.js – 一款轻量级的Javascript日期处理类库

github:https://github.com/moment/mom...

官方网站:http://momentjs.com/

star:30k+

Install:

bower install moment --save # bower
npm install moment --save   # npm
yarn add moment             # Yarn
Install-Package Moment.js   # NuGet
spm install moment --save   # spm
meteor add momentjs:moment  # meteor

大小:16.6 KB

功能介绍:
  moment.js是一个轻量级的JavaScript库日期解析、验证操作,格式化日期的库。

Demo:
效果预览地址:http://momentjs.com/

Demo效果预览:
这是一个GIF动图,不信,你看第一行的日期,时间在走?。






infinite-scroll – 一款滚动加载按需加载的轻量级插件

github:https://github.com/infinite-s...

官方网站:http://www.infinite-scroll.com/

star:4k+

Install:

github自行下载

大小:20 KB

功能介绍:
 infinite-scroll是一款滚动加载,滚动到最下到自动加载的轻量级JavaScript插件,简单实用,按需加载提高用户体验,非常适合移动端使用,配合上面的图片懒加载如虎添翼。

Demo:
效果预览地址:http://www.dazeddigital.com/

Demo效果预览:
图片有点大,稍等片刻。建议上面Demo效果预览地址进行预览。






欢迎大家按照格式补充,持续更新,有什么好用的轮子赶紧滚起来吧!

推荐有福利,送1024论坛邀请码,嘿嘿嘿。


查看原文

jsyzchen 收藏了文章 · 4月11日

MySQL大表优化方案

当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化:

单表优化

除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑、部署、运维的各种复杂度,一般以整型值为主的表在千万级以下,字符串为主的表在五百万以下是没有太大问题的。而事实上很多时候MySQL单表的性能依然有不少优化空间,甚至能正常支撑千万级以上的数据量:

字段

  • 尽量使用TINYINTSMALLINTMEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED

  • VARCHAR的长度只分配真正需要的空间

  • 使用枚举或整数代替字符串类型

  • 尽量使用TIMESTAMP而非DATETIME

  • 单表不要有太多字段,建议在20以内

  • 避免使用NULL字段,很难查询优化且占用额外索引空间

  • 用整型来存IP

索引

  • 索引并不是越多越好,要根据查询有针对性的创建,考虑在WHEREORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描

  • 应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描

  • 值分布很稀少的字段不适合建索引,例如"性别"这种只有两三个值的字段

  • 字符字段只建前缀索引

  • 字符字段最好不要做主键

  • 不用外键,由程序保证约束

  • 尽量不用UNIQUE,由程序保证约束

  • 使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引

查询SQL

  • 可通过开启慢查询日志来找出较慢的SQL

  • 不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边

  • sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库

  • 不用SELECT *

  • OR改写成INOR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内

  • 不用函数和触发器,在应用程序实现

  • 避免%xxx式查询

  • 少用JOIN

  • 使用同类型进行比较,比如用'123''123'比,123123

  • 尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描

  • 对于连续数值,使用BETWEEN不用INSELECT id FROM t WHERE num BETWEEN 1 AND 5

  • 列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大

引擎

目前广泛使用的是MyISAM和InnoDB两种引擎:

MyISAM

MyISAM引擎是MySQL 5.1及之前版本的默认引擎,它的特点是:

  • 不支持行锁,读取时对需要读到的所有表加锁,写入时则对表加排它锁

  • 不支持事务

  • 不支持外键

  • 不支持崩溃后的安全恢复

  • 在表有读取查询的同时,支持往表中插入新纪录

  • 支持BLOBTEXT的前500个字符索引,支持全文索引

  • 支持延迟更新索引,极大提升写入性能

  • 对于不会进行修改的表,支持压缩表,极大减少磁盘空间占用

InnoDB

InnoDB在MySQL 5.5后成为默认索引,它的特点是:

  • 支持行锁,采用MVCC来支持高并发

  • 支持事务

  • 支持外键

  • 支持崩溃后的安全恢复

  • 不支持全文索引

总体来讲,MyISAM适合SELECT密集型的表,而InnoDB适合INSERTUPDATE密集型的表

系统调优参数

可以使用下面几个工具来做基准测试:

  • sysbench:一个模块化,跨平台以及多线程的性能测试工具

  • iibench-mysql:基于 Java 的 MySQL/Percona/MariaDB 索引进行插入性能测试工具

  • tpcc-mysql:Percona开发的TPC-C测试工具

具体的调优参数内容较多,具体可参考官方文档,这里介绍一些比较重要的参数:

  • back_log:back_log值指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。也就是说,如果MySql的连接数据达到max_connections时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数量超过back_log,将不被授予连接资源。可以从默认的50升至500

  • wait_timeout:数据库连接闲置时间,闲置连接会占用内存资源。可以从默认的8小时减到半小时

  • max_user_connection: 最大连接数,默认为0无上限,最好设一个合理上限

  • thread_concurrency:并发线程数,设为CPU核数的两倍

  • skip_name_resolve:禁止对外部连接进行DNS解析,消除DNS解析时间,但需要所有远程主机用IP访问

  • key_buffer_size:索引块的缓存大小,增加会提升索引处理速度,对MyISAM表性能影响最大。对于内存4G左右,可设为256M或384M,通过查询show status like 'key_read%',保证key_reads / key_read_requests在0.1%以下最好

  • innodb_buffer_pool_size:缓存数据块和索引块,对InnoDB表性能影响最大。通过查询show status like 'Innodb_buffer_pool_read%',保证 (Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests越高越好

  • innodb_additional_mem_pool_size:InnoDB存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小,当数据库对象非常多的时候,适当调整该参数的大小以确保所有数据都能存放在内存中提高访问效率,当过小的时候,MySQL会记录Warning信息到数据库的错误日志中,这时就需要该调整这个参数大小

  • innodb_log_buffer_size:InnoDB存储引擎的事务日志所使用的缓冲区,一般来说不建议超过32MB

  • query_cache_size:缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以仅仅只能针对select语句。当某个表的数据有任何任何变化,都会导致所有引用了该表的select语句在Query Cache中的缓存数据失效。所以,当我们的数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。根据命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))进行调整,一般不建议太大,256MB可能已经差不多了,大型的配置型静态数据可适当调大.
    可以通过命令show status like 'Qcache_%'查看目前系统Query catch使用大小

  • read_buffer_size:MySql读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySql会为它分配一段内存缓冲区。如果对表的顺序扫描请求非常频繁,可以通过增加该变量值以及内存缓冲区大小提高其性能

  • sort_buffer_size:MySql执行排序使用的缓冲大小。如果想要增加ORDER BY的速度,首先看是否可以让MySQL使用索引而不是额外的排序阶段。如果不能,可以尝试增加sort_buffer_size变量的大小

  • read_rnd_buffer_size:MySql的随机读缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySql会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。

  • record_buffer:每个进行一个顺序扫描的线程为其扫描的每张表分配这个大小的一个缓冲区。如果你做很多顺序扫描,可能想要增加该值

  • thread_cache_size:保存当前没有与连接关联但是准备为后面新的连接服务的线程,可以快速响应连接的线程请求而无需创建新的

  • table_cache:类似于thread_cache_size,但用来缓存表文件,对InnoDB效果不大,主要用于MyISAM

升级硬件

Scale up,这个不多说了,根据MySQL是CPU密集型还是I/O密集型,通过提升CPU和内存、使用SSD,都能显著提升MySQL性能

读写分离

也是目前常用的优化,从库读主库写,一般不要采用双主或多主引入很多复杂性,尽量采用文中的其他方案来提高性能。同时目前很多拆分的解决方案同时也兼顾考虑了读写分离

缓存

缓存可以发生在这些层次:

  • MySQL内部:在系统调优参数介绍了相关设置

  • 数据访问层:比如MyBatis针对SQL语句做缓存,而Hibernate可以精确到单个记录,这里缓存的对象主要是持久化对象Persistence Object

  • 应用服务层:这里可以通过编程手段对缓存做到更精准的控制和更多的实现策略,这里缓存的对象是数据传输对象Data Transfer Object

  • Web层:针对web页面做缓存

  • 浏览器客户端:用户端的缓存

可以根据实际情况在一个层次或多个层次结合加入缓存。这里重点介绍下服务层的缓存实现,目前主要有两种方式:

  • 直写式(Write Through):在数据写入数据库后,同时更新缓存,维持数据库与缓存的一致性。这也是当前大多数应用缓存框架如Spring Cache的工作方式。这种实现非常简单,同步好,但效率一般。

  • 回写式(Write Back):当有数据要写入数据库时,只会更新缓存,然后异步批量的将缓存数据同步到数据库上。这种实现比较复杂,需要较多的应用逻辑,同时可能会产生数据库与缓存的不同步,但效率非常高。

表分区

MySQL在5.1版引入的分区是一种简单的水平拆分,用户需要在建表的时候加上分区参数,对应用是透明的无需修改代码

对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成,实现分区的代码实际上是通过对一组底层表的对象封装,但对SQL层来说是一个完全封装底层的黑盒子。MySQL实现分区的方式也意味着索引也是按照分区的子表定义,没有全局索引

Alt text

用户的SQL语句是需要针对分区表做优化,SQL条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区,可以通过EXPLAIN PARTITIONS来查看某条SQL语句会落在那些分区上,从而进行SQL优化,如下图5条记录落在两个分区上:

mysql> explain partitions select count(1) from user_partition where id in (1,2,3,4,5);
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table          | partitions | type  | possible_keys | key     | key_len | ref  | rows | Extra                    |
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
|  1 | SIMPLE      | user_partition | p1,p4      | range | PRIMARY       | PRIMARY | 8       | NULL |    5 | Using where; Using index |
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
1 row in set (0.00 sec)

分区的好处是:

  • 可以让单表存储更多的数据

  • 分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作

  • 部分查询能够从查询条件确定只落在少数分区上,速度会很快

  • 分区表的数据还可以分布在不同的物理设备上,从而搞笑利用多个硬件设备

  • 可以使用分区表赖避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争

  • 可以备份和恢复单个分区

分区的限制和缺点:

  • 一个表最多只能有1024个分区

  • 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来

  • 分区表无法使用外键约束

  • NULL值会使分区过滤无效

  • 所有分区必须使用相同的存储引擎

分区的类型:

  • RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区

  • LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择

  • HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式

  • KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值

分区适合的场景有:

  • 最适合的场景数据的时间序列性比较强,则可以按时间来分区,如下所示:

CREATE TABLE members (
    firstname VARCHAR(25) NOT NULL,
    lastname VARCHAR(25) NOT NULL,
    username VARCHAR(16) NOT NULL,
    email VARCHAR(35),
    joined DATE NOT NULL
)
PARTITION BY RANGE( YEAR(joined) ) (
    PARTITION p0 VALUES LESS THAN (1960),
    PARTITION p1 VALUES LESS THAN (1970),
    PARTITION p2 VALUES LESS THAN (1980),
    PARTITION p3 VALUES LESS THAN (1990),
    PARTITION p4 VALUES LESS THAN MAXVALUE
);

查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容的批量删除。

  • 如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存

另外MySQL有一种早期的简单的分区实现 - 合并表(merge table),限制较多且缺乏优化,不建议使用,应该用新的分区机制来替代

垂直拆分

垂直分库是根据数据库里面的数据表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以把用户数据放到用户库、把订单数据放到订单库。垂直分表是对数据表进行垂直拆分的一种方式,常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联

比如原始的用户表是:

Alt text

垂直拆分后是:

Alt text

垂直拆分的优点是:

  • 可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)

  • 可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起

  • 数据维护简单

缺点是:

  • 主键出现冗余,需要管理冗余列

  • 会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力

  • 依然存在单表数据量过大的问题(需要水平拆分)

  • 事务处理复杂

水平拆分

概述

水平拆分是通过某种策略将数据分片来存储,分库内分表和分库两部分,每片数据会分散到不同的MySQL表或库,达到分布式的效果,能够支持非常大的数据量。前面的表分区本质上也是一种特殊的库内分表

库内分表,仅仅是单纯的解决了单一表数据过大的问题,由于没有把表的数据分布到不同的机器上,因此对于减轻MySQL服务器的压力来说,并没有太大的作用,大家还是竞争同一个物理机上的IO、CPU、网络,这个就要通过分库来解决

前面垂直拆分的用户表如果进行水平拆分,结果是:

Alt text

实际情况中往往会是垂直拆分和水平拆分的结合,即将Users_A_MUsers_N_Z再拆成UsersUserExtras,这样一共四张表

水平拆分的优点是:

  • 不存在单库大数据和高并发的性能瓶颈

  • 应用端改造较少

  • 提高了系统的稳定性和负载能力

缺点是:

  • 分片事务一致性难以解决

  • 跨节点Join性能差,逻辑复杂

  • 数据多次扩展难度跟维护量极大

分片原则

  • 能不分就不分,参考单表优化

  • 分片数量尽量少,分片尽量均匀分布在多个数据结点上,因为一个查询SQL跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,只在必要的时候进行扩容,增加分片数量

  • 分片规则需要慎重选择做好提前规划,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容

  • 尽量不要在一个事务中的SQL跨越多个分片,分布式事务一直是个不好处理的问题

  • 查询条件尽量优化,尽量避免Select * 的方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。

  • 通过数据冗余和表分区赖降低跨库Join的可能

这里特别强调一下分片规则的选择问题,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤,比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。

总体上来说,分片的选择是取决于最频繁的查询SQL的条件,因为不带任何Where语句的查询SQL,会遍历所有的分片,性能相对最差,因此这种SQL越多,对系统的影响越大,所以我们要尽量避免这种SQL的产生。

解决方案

由于水平拆分牵涉的逻辑比较复杂,当前也有了不少比较成熟的解决方案。这些方案分为两大类:客户端架构和代理架构。

客户端架构

通过修改数据访问层,如JDBC、Data Source、MyBatis,通过配置来管理多个数据源,直连数据库,并在模块内完成数据的分片整合,一般以Jar包的方式呈现

这是一个客户端架构的例子:

Alt text

可以看到分片的实现是和应用服务器在一起的,通过修改Spring JDBC层来实现

客户端架构的优点是:

  • 应用直连数据库,降低外围系统依赖所带来的宕机风险

  • 集成成本低,无需额外运维的组件

缺点是:

  • 限于只能在数据库访问层上做文章,扩展性一般,对于比较复杂的系统可能会力不从心

  • 将分片逻辑的压力放在应用服务器上,造成额外风险

代理架构

通过独立的中间件来统一管理所有数据源和数据分片整合,后端数据库集群对前端应用程序透明,需要独立部署和运维代理组件

这是一个代理架构的例子:

Alt text

代理组件为了分流和防止单点,一般以集群形式存在,同时可能需要Zookeeper之类的服务组件来管理

代理架构的优点是:

  • 能够处理非常复杂的需求,不受数据库访问层原来实现的限制,扩展性强

  • 对于应用服务器透明且没有增加任何额外负载

缺点是:

  • 需部署和运维独立的代理中间件,成本高

  • 应用需经过代理来连接数据库,网络上多了一跳,性能有损失且有额外风险

各方案比较
出品方架构模型支持数据库分库分表读写分离外部依赖是否开源实现语言支持语言最后更新Github星数
MySQL FabricMySQL官方代理架构MySQLpython无限制4个月前35
Cobar阿里巴巴代理架构MySQLJava无限制两年前1287
Cobar Client阿里巴巴客户端架构MySQLJavaJava三年前344
TDDL淘宝客户端架构无限制Diamond只开源部分JavaJava未知519
Atlas奇虎360代理架构MySQLC无限制10个月前1941
Heisenberg百度熊照代理架构MySQLJava无限制2个月前197
TribeDB个人代理架构MySQLNodeJS无限制3个月前126
ShardingJDBC当当客户端架构MySQLJavaJava当天1144
Shark个人客户端架构MySQLJavaJava两天前84
KingShard个人代理架构MySQLGolang无限制两天前1836
OneProxy平民软件代理架构MySQL未知无限制未知未知
MyCat社区代理架构MySQLJava无限制两天前1270
VitessYoutube代理架构MySQLGolang无限制当天3636
Mixer个人代理架构MySQLGolang无限制9个月前472
JetPantsTumblr客户端架构MySQLRubyRuby10个月前957
HibernateShardHibernate客户端架构无限制JavaJava4年前57
MybatisShardMakerSoft客户端架构无限制JavaJava11个月前119
GizzardTwitter代理架构无限制Java无限制3年前2087

如此多的方案,如何进行选择?可以按以下思路来考虑:

  1. 确定是使用代理架构还是客户端架构。中小型规模或是比较简单的场景倾向于选择客户端架构,复杂场景或大规模系统倾向选择代理架构

  2. 具体功能是否满足,比如需要跨节点ORDER BY,那么支持该功能的优先考虑

  3. 不考虑一年内没有更新的产品,说明开发停滞,甚至无人维护和技术支持

  4. 最好按大公司->社区->小公司->个人这样的出品方顺序来选择

  5. 选择口碑较好的,比如github星数、使用者数量质量和使用者反馈

  6. 开源的优先,往往项目有特殊需求可能需要改动源代码

按照上述思路,推荐以下选择:

  • 客户端架构:ShardingJDBC

  • 代理架构:MyCat或者Atlas

兼容MySQL且可水平扩展的数据库

目前也有一些开源数据库兼容MySQL协议,如:

但其工业品质和MySQL尚有差距,且需要较大的运维投入,如果想将原始的MySQL迁移到可水平扩展的新数据库中,可以考虑一些云数据库:

NoSQL

在MySQL上做Sharding是一种戴着镣铐的跳舞,事实上很多大表本身对MySQL这种RDBMS的需求并不大,并不要求ACID,可以考虑将这些表迁移到NoSQL,彻底解决水平扩展问题,例如:

  • 日志类、监控类、统计类数据

  • 非结构化或弱结构化数据

  • 对事务要求不强,且无太多关联操作的数据

查看原文

jsyzchen 收藏了问题 · 4月11日

如何转义emoji表情,让它可以存入utf8的数据库?

unicode emoji是4个字节的,存不进MySQL里,找到一个转义的库http://code.iamcal.com/php/emoji/,但是转为Unicode之后,还是4个字节,一样存不进,应该说根本没转。转为其他格式的emoji又怕以后新增了表情不好做,你们在不改数据库编码的前提下,是怎么弄的?

方法1:base_encode64

这种方法是可以,但是旧数据没有经过encode操作,取数据的时候如果统一进行decode的话,旧数据会丢失的。

方法2:urlencode

这个似乎可以,对没有经过encode的数据进行decode也不会有影响,而且多次decode似乎也不会有影响。你们说这个方法有缺陷吗?

=======================
一个发现,微信获取用户基本信息的时候,笑哭那个表情print_r出的是\ud83d\ude02,而我存储的时候,报错说这个 \xF0\x9F\x98\x82 值不能存储,请问这是怎么回事,自动转码了,转成的这是什么?是微信转码过了吗?

=======================

方法3:最后采用了下面采纳的那个方法,因为我觉得它有下面几个优点:

1、那个方法只转换表情,不会转换中文,所以数据还是直接可读的
数据库中存储起来是这样的,clipboard.png 后面的\ud83d\udca5可以随意复制粘贴,而显示出来是这样的, clipboard.png

2、不会把表情转换为其它标准,只有一个简单的,固定的转换算法,也就是说不需要一个表情库来对照着转换,所以以后其它人要使用这个数据的时候,也很容易知道每个表情是对应的哪个。就算苹果大爷又增加了表情,也不需要做什么额外的修改。

3、可以无限decode输出的都是正确的内容。因为有的时候可能需要在一次请求中的两个地方做decode,其它decode多次会把正确的数据改成其它数据,这个不会。

缺点:
1、看了下面的代码就知道,这个是强制修改字符编码中,指定区间内的编码,也就说有可能误杀,也有可能有超出这个区间的emoji没杀到。不过仅仅是在字符前加反斜杠,即使误杀了,发现之后也很容易改回来。
数据库中发现有这样的clipboard.png ,是漏杀了,但是不知道为什么,这个可以直接存数据库。


/**
  把用户输入的文本转义(主要针对特殊符号和emoji表情)
 */
function userTextEncode($str){
    if(!is_string($str))return $str;
    if(!$str || $str=='undefined')return '';

    $text = json_encode($str); //暴露出unicode
    $text = preg_replace_callback("/(\\\u[ed][0-9a-f]{3})/i",function($str){
        return addslashes($str[0]);
    },$text); //将emoji的unicode留下,其他不动,这里的正则比原答案增加了d,因为我发现我很多emoji实际上是\ud开头的,反而暂时没发现有\ue开头。
    return json_decode($text);
}
/**
  解码上面的转义
 */
function userTextDecode($str){
    $text = json_encode($str); //暴露出unicode
    $text = preg_replace_callback('/\\\\\\\\/i',function($str){
        return '\\';
    },$text); //将两条斜杠变成一条,其他不动
    return json_decode($text);
}

jsyzchen 赞了文章 · 4月11日

[Skr-Shop]购物车之架构设计

来还债了,希望大家在疫情中都是平安的,回来的时候公司也还在!


skr shop是一群底层码农,由于被工作中的项目折磨的精神失常,加之由于程序员的自傲:别人设计的系统都是一坨shit,我的设计才是宇宙最牛逼,于是乎决定要做一个只设计不编码的电商设计手册。

在上一篇文章 购物车设计之需求分析 描述了购物车的通用需求。本文重点则在如何实现上进行架构上的设计(业务+系统架构)。

说明

架构设计可以分为三个层面:

  • 业务架构
  • 系统架构
  • 技术架构

快速简单的说明下三个架构的意思;当我们拿到购物车需求时,我们说用Golang来实现,存储用Redis;这描述的是技术架构;我们对购物车代码项目进行代码分层,设计规范,以及依赖系统的规划这叫系统架构;

那业务架构是什么呢?业务架构本质上是对系统架构的文字语言描述;什么意思?我们拿到一个需求首先要跟需求方进行沟通,建立统一的认知。比如:规范名词(购物车中说的商品与商品系统中商品的含义是不同的);建立大家都能明白的模型,购物车、用户、商品、订单这些实体之间的互动,以及各自具备什么功能。

在业务架构分析上有很多方法论,比如:领域驱动设计,但是它并不是唯一的业务架构分析方法,也并不是说最好的。适合你的就是最好的。我们常用的实体关系图、UML图也属于业务架构领域;

这里需要强点一点的是,不管你用什么方式来建模设计,有设计总比没设计强,其次一定要将建模的内容体现到你的代码中去。

本文在业务架构上的分析借助了 DDD (领域驱动设计)思想;还是那句话适合的就是最好的

业务架构

通过前面的需求分析,我们已经明确我们的购物车要干什么了。先来看一下一个典型的用户操作购物车过程。

用户旅程

在这个过程中,用户使用购物车这个载体完成了商品的购买流程;不断流动的数据是商品,购物车这个载体是稳定的。这是我们系统中的稳定点与变化点。

商品的流动方式可能多种多样,比如从不同地方加入购物车,不同方式加入购物车,生命周期在购物车中也不一样;但是这个流程是稳定的,一定是先让购物车中存在商品,然后才能去结算产生订单。

商品在购物车中的生命周期如下:

过程

按照这个过程,我们来看一下每个阶段对应的操作。

过程对应的操作

这里注意一点,加车前这个操作其实我们可以放到购物车的添加操作中,但是由于这部分是非常不稳定且多变的。我们将其独立出来,方便后续进行扩展而不影响相对比较稳定的购物车阶段。

上面这三个阶段,按照DDD中的概念,应该叫做实体,他们整体构成了购物车这个域;今天我们先不讲这些概念,就先略过,后面有机会单独发文讲解。

加车前

通过流程分析,我们总结出了系统需要具备的操作接口,以及这些接口对应的实体,现在我们先来看加车前主要要做些什么;

加车前其实主要就是对准备加入的购物车商品进行各个纬度的校验,检查是否满足要求。

在让用户加车前,我们首先解决的是用户从哪里卖,然后进行验证?因为同一个商品从不同渠道购买是存在不同情况的,比如:小米手机,我们是通过秒杀买,还是通过好友众筹买,或者商城直接购买,价格存在差异,但是实际上他是同一个商品;

第二个问题是是否具备购买资格,还是上面说的,秒杀、众筹这个加车操作,不是谁都可以添加的,得现有资格。那么资格的检查也是放到这里;

第三个问题是对这个购买的商品进行商品属性上的验证,如是否上下架,有库存,限购数量等等。

而且大家会发现,这里的验证条件可能是非常多变的。如何构建一个方便扩展的代码呢?

加车的验证

整个加车过程,重要的就是根据来源来区分不同的验证。我们有两种选择方式。

方式一:通过策略模式+门面模式的方式来搞定。策略就是根据不同的加车来源进行不同的验证,门面就是根据不同的来源封装一个个策略;

方式二:通过责任链模式,但是这里需要有一个变化,这个链在执行过程中,可以选择跳过某些节点,比如:秒杀不需要库存、也不需要众筹的验证;

通过综合的分析我选择了责任链的模式。贴一下核心代码

// 每个验证逻辑要实现的接口
type Handler interface {
    Skipped(in interface{}) bool // 这里判断是否跳过
    HandleRequest(in interface{}) error // 这里进行各种验证
}

// 责任链的节点
type RequestChain struct {
    Handler
    Next *RequestChain
}

// 设置handler
func (h *RequestChain) SetNextHandler(in *RequestChain) *RequestChain {
    h.Next = in
    return in
}

关于设计模式,大家可以看我小伙伴的github:https://github.com/TIGERB/eas...

购物车

说完了加车前,现在来看购物车这一部分。我们在之前曾讨论过,购物车可能会有多种形态的,比如:存储多个商品一起结算,某个商品立即结算等。因此购物车一定会根据渠道来进行购物车类型的选择。

这部分的操作相对是比较稳定的。我们挑几个比较重要的操作来讲一下思路即可。

加入购物车

通过把条件验证的前置,会发现在进行加车操作时,这部分逻辑已经变得非常的轻量了。要做的主要是下面几个部分的逻辑。

加入购物车

这里有几个取巧的地方,首先是获取商品的逻辑,由于在前面验证的时候也会用到,因此这里前面获取后会通过参数的方式继续往后传递,因此这里不需要在读库或者调用服务来获取;

其次这里需要把当前用户现有购物车数据获取到,然后将添加的这个商品添加进来。这是一个类似合并操作,原来这个商品是存在,相当于数量加一;需要注意这个商品跟现存的商品有没有父子关系,有没有可能加入后改变了某个活动规则,比如:原来买了2个送1个赠品,现在再添加了一个变成3个,送2个赠品;

注意:这里的添加并不是在购物车直接改数量,可能就是在列表、详情页直接添加添加。

通过将合并后的购物车数据,通过营销活动检查确认ok后,直接回写到存储中。

合并购物车

为什么会有合并购物车这个操作?因为一般电商都是准许游客身份进行操作的,因此当用户登录后需要将二者进行合并。

这里的合并很多部分的逻辑是可以与加入购物车复用的逻辑。比如:合并后的数据都需要检查是否合法,然后覆写回存储中。因此大家可以看到这里的关联性。设计的方法在某种程度上要通用。

购物车列表

购物车列表这是一个非常重要的接口,原则上购物车接口会提供两种类型,一种简版,一种完全版本;

简版的列表接口主要是用在类似PC首页右上角之类获取简单信息;完全版本就是在购物车列表中会用到。

在实际实现中,购物车绝不仅仅是一个读取接口那么简单。因为我们都知道不管是商品信息、活动信息都是在不断的发生变化。因此每次的读取接口必然需要检查当前购物车中数据的合法性,然后发现不一致后需要覆写原存储的数据。

购物车列表

也有一些做法会在每个接口都去检查数据的合法性,我建议为了性能考虑,部分接口可以适当放宽检查,在获取列表时再进行完整的检查。比如添加接口,我只会检测我添加的商品的合法性,绝不会对整个购物车进行检查。因为该操作之后一般都会调用列表操作,那么此时还会进行校验,二者重复操作,因此只取后者。

结算

结算包括两部分,结算页的详情信息与提交订单。结算页可以说是在购物车列表上的一个包装,因为结算页与列表页最大的不同是需要用户选择配送地址(虚拟商品另说),此时会产生更明确的价格信息,其他基本一致。因此在设计购物车列表接口的时候,一定要考虑充分的通用性。

这里另外一个需要注意的是:立即购买,我们也会通过结算页接口来实现,但是内部其实还是会调用添加接口,将商品添加到购物车中;有三个需要注意的地方,首先是这个添加操作是服务内部完成的,对于服务调用方是不需要感知这个加入操作的存在;其次是这个购物车在Redis中的Key是独立于普通购物车的,否则二者的商品耦合在一起非常难于操作处理;最后立即购买的购物车要考虑账号多终端登录的时候,彼此数据不能互相影响,这里可以用每个端的uuid来作为购物车的标记避免这种情况。

购物车的最后一步是生成订单,这一步最要紧的是需要给购物车加锁,避免提交过程中数据被篡改,多说一句,很多人写的Redis分布式锁代码都存在缺陷,大家一定要注意原子性的问题,这类文章网络上很多不再赘述。

加锁成功之后,我们这里有多种做法,一种是按照DB涉及组织数据开始写表,这适用于业务量要求不大,比如订单每秒下单量不超过2000K的;那如果你的系统并发要求非常高怎么办?

其实也很简单,高性能的三大法宝之一:异步;我们提交的时候直接将数据快照写入MQ中,然后通过异步的方式进行消费处理,可以通过通过控制消费者的数量来提升处理能力。这种方法虽然性能提升,但是复杂度也会上升,大家需要根据自己的实际情况来选择。

关于业务架构的设计,到此告一段落,接下来我们来看系统架构。

系统架构

系统结构主要包含,如何将业务架构映射过来,以及输出对应输入参数、输出参数的说明。由于输入、输出针对各自业务来确定的,而且没有什么难度,我们这里就只说如何将业务架构映射到系统架构,以及系统架构中最核心的Redis数据结构选择以及存储的数据结构设计。

代码结构

下面的代码目录是按照 Golang 来进行设计的。我们来看看如何将上面的业务架构映射到代码层面来。

├── addproducts.go
├── cartlist.go
├── mergecart.go
├── entity
│   ├── cart
│   │   ├── add.go
│   │   ├── cart.go
│   │   └── list.go
│   ├── order
│   │   ├── checkout.go
│   │   ├── order.go
│   │   └── submit.go
│   └── precart
├── event
│   └── sendorder.go
├── facade
│   ├── activity.go
│   └── product.go
└── repo

外层有 entityeventfacaderepo这四个目录,职责如下:

entity: 存放的是我们前面分析的购物领域的三个实体;所有主要的操作都在这三个实体上;

event: 这是用来处理产生的事件,比如刚刚说的如果我们提交订单采用异步的方式,那么该目录就该完成的是如何把数据发送到MQ中去;

facade: 这儿目录是干嘛的呢?这主要是因为我们的服务还需要依赖像商品、营销活动这些服务,那么我们不应该在实体中直接调用它,因为第三方可能存在变动,或者有增加、减少,我们在这里进行以下简单的封装(设计模式中的门面模式);

repo: 这个目录从某种程度上可以理解为 Model层,在整个领域服务中,如果与持久化打交道,都通过它来完成。

最后外层的几个文件,就是我们所提供的领域服务,供应用层来进行调用的。

为了保证内容的紧凑,我这里放弃了对整个微服务的目录介绍,只单独介绍了领域服务,后续会单独成文介绍下微服务的整个系统架构。

通过上面的划分,我们完成了两件事情:

  1. 业务架构分析的结构在系统代码中都有映射,他们彼此体现。这样最大的好处是,保证设计与代码的一致性,看了文档你就知道对应的代码在哪里;
  2. 每个目录各自的关注点都进行了分离,更内聚,更容易开发与维护。

Redis存储

现在来看,我们选择Redis作为购物商品数据的存储,我们要解决两个问题,一是我们需要存哪些数据?二是我们用什么结构来存?

网络上很多写购物车的都是只保存一个商品id,真实场景是很难满足需求的。你想想,一个商品id如何记住用户选择的赠品?用户上次选择的活动?以及购买的商品渠道?

综合比较通用的场景,我给出一个参考结构:

// 购物车数据
type ShoppingData struct {
    Item       []*Item `json:"item"`
    UpdateTime int64   `json:"update_time"`
    Version    int32   `json:"version"`
}

// 单个商品item元素
type Item struct {
    ItemId       string          `json:"item_id"`
    ParentItemId string          `json:"parent_item_id,omitempty"` // 绑定的父item id
    OrderId      string          `json:"order_id,omitempty"`       // 绑定的订单号
    Sku          int64           `json:"sku"`
    Spu          int64           `json:"spu"`
    Channel      string          `json:"channel"`
    Num          int32           `json:"num"`
    Status       int32           `json:"status"`
    TTL          int32           `json:"ttl"`                     // 有效时间
    SalePrice    float64         `json:"sale_price"`              // 记录加车时候的销售价格
    SpecialPrice float64         `json:"special_price,omitempty"` // 指定价格加购物车
    PostFree     bool            `json:"post_free,omitempty"`     // 是否免邮
    Activities   []*ItemActivity `json:"activities,omitempty"`    // 参加的活动记录
    AddTime      int64           `json:"add_time"`
    UpdateTime   int64           `json:"update_time"`
}

// 活动
type ItemActivity struct {
    ActID    string `json:"act_id"`
    ActType  string `json:"act_type"`
    ActTitle string `json:"act_title"`
}

重点说一下 Item 这个结构,item_id 这个字段是标记购物车中某个商品的唯一标记,因为我们之前说过,同一个sku由于渠道不同,那么在购物车中会是两个不同的item;接下来的 parent_item_id 字段是用来标记父子关系的,这里将可能存在的树结构转成了顺序结构,我们不管是父商品还是子商品,都采用顺序存储,然后通过这个字段来进行关联;有些同学可能会奇怪,为什么会存order id这个字段呢?大家关注下自己的日常业务,比如:再来一单、定金预售等,这种一定是与某个订单相关联的,不管是为了资格验证还是数据统计。剩下的字段都是一些非常常规的字段,就不在一一介绍了;

字段的类型,大家根据自己的需要进行修改。

接下来该说怎么选择Redis的存储结构了,Redis常用的 Hash Table、集合、有序集合、链表、字符串 五种,我们一个个来分析。

首先购车一定有一个key来标记这个购物车属于哪个用户的,为了简化,我们的key假设是:uid:cart_type

我们先来看如果用 Hash Table;我们添加时,需要用到如下命令:HSET uid:cart_type sku ShoppingData;看起来没问题,我们可以根据sku快速定位某个商品然后进行相关的修改等,但是注意,ShoppingData是一个json串,如果用户购物车中有非常多的商品,我们用 HGETALL uid:cart_type 获取到的时间复杂度是O(n),然后代码中还需要一一反序列化,又是O(n)的复杂度。

如果用集合,也会遇到类似的问题,每个购物车看做一个集合,集合中的每个元素是 ShoppingData ,取到代码中依然需要逐一反序列化(反序列化是成本),关于有序集合与链表就不在分析,大家可以按照上面的思路去尝试下问题所在。

看起来我们没得选,只有使用String,那我们来看一下String的契合度是什么样子。首先SET uid:cart_type ShoppingDataArr;我们把购物车所有的数据序列化成一个字符串存储,每次取出来的时间复杂度是O(1),序列化、反序列化都只需要一次。看来是非常不错的选择。但是在使用中大家还是有几点需要注意。

  1. 单个Value不能太大,要不然就会出现大key问题,所以一般购物车有上限限制,比如item不能超过多少个;
  2. 对redis的操作性能提升上来了,但是代码的就是修改单个item时的不便,必须每次读取全部然后找到对应的item进行修改;这里我们可以把从redis中的数据读取出来后,在内存中构建一个HashTable,来减少每次遍历的复杂度;

网上也看到很多Redis数据结构组合使用来保存购物车数据的,但是无疑增加了网络开销,相比起来还是String最经济划算。

总结

至此对于购物车的实现设计算是完结了,其中关于订单表的设计会单独放到订单模块去讲。

对于整个购物车服务,虽然没有写的详细到某个具体的接口,但是分析到这一步,我相信大家心中都是有沟壑的,能够结合自己的业务去实现它。

文中有些很有意思的地方,建议大家动手去做做看,有任何问题,我们随时交流。

  • 改编版的责任链模式
  • Redis的分布式事务锁实现

接下来终于要到订单部分的设计了,希望大家继续关注我们。

项目地址:https://github.com/skr-shop/m...

查看原文

赞 31 收藏 19 评论 5

jsyzchen 收藏了文章 · 4月11日

Sentry的使用

公司项目里需要加入,于是自己倒腾了下,就总结了下入门使用手法,哈哈,欢迎指点,希望能帮助大家~~~

Sentry 自我理解,可以更便捷的了解到非必现的bug、无法通过日志追踪的异常等。

一、Sentry介绍:

sentry 是一个实时事件日志记录和聚合平台。它专门用于监视错误和提取执行适当的事后操作所需的所有信息, 而无需使用标准用户反馈循环的任何麻烦。
Sentry 是一个日志平台,分为客户端和服务端,客户端(目前客户端有Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web节目方便查看。Sentry由python编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest等

二、Sentry的注册和使用

公司已有现成的服务了,所以我们只需要用公司邮箱进行注册即可。

下面介绍一下简单的使用方法:

2.1 首先注册账号: 分为UAT 和线上(ONLINE)

online: https://sentry-fe... // 这里是正式线上链接

uat: http://sentry... // 这里是uat环境链接

(公司邮箱注册)

TLYhFvRNXW.gif

新注册的账号可能无法New Project 创建,这时候需要点击左侧栏目,点击 Project & Teams ,需要加入Teams,这是刷新页面,则可以在Select project 下面看到你的项目,如果无项目请联系管理员;

YqwsIuXTtX.gif

进入后进行简单配置,然后右上角可以点击 New Project 创建,选择需要项目类型,根据提示进行配置

c2CHwXF4ct.gif

选择相对应的技术

izBwzIPLN3.gif

下面这个生成的链接也就是日后项目实时监听bug的地址(项目中的sentry.ts文件中需要配置的这个)

847I23cYbJ.gif

2.2 sentry在项目中的配置

a. 首先在项目中下载依赖
$ yarn add raven-js -D
b. sentry配置文件

(目前有一套配置文件,可直接饮用,也可对里面的错误警报规则做修改)
如:

Fs88JMk64A.gif

c. sentry 在项目中的引用:
import * as Raven from "raven-js"; // 首先引入
import { ravenOptions, DSN_ONLINE, DSN_UAT } from "config/sentry"; // 上面对sentry的配置文件
import { isLocalEnv, isUatEnv } from "utils/url";  // 封装的页面链接获取

componentDidMount() {
    if (!isLocalEnv) {
            const dsn = isUatEnv ? DSN_UAT : DSN_ONLINE;
            Raven.config(
                dsn,
                Object.assign({}, ravenOptions, {
                    release: __webpack_global__.SENTRY_RELEASE
// 这里注意了,__webpack_global__相当于是全局的,需要在tsconfig.json里进行配置,深层的原理和关联可以追踪看ezpack库
                })
            ).install();
        }
}

//  componentDidCatch 错误捕获
componentDidCatch(error, errorInfo) {
        const group = errorInfo ? location.href : "default";
        Raven.captureException(error, { extra: errorInfo, fingerprint: [group] });
    }

<br/>

Xa9IoSkx10.gif
07LOdoo0p4.gif

以上是在项目中添加sentry最简单的配置使用,当完成到这里的时候,可以进行测试 比如在项目中添加
throw new Error("test error");
发布在环境上进行测试,在自己的账号项目中检查是否监听到错误。
以上只是简单地配置在项目中了,当然,我们可以将它运用的更友好,比如在请求的时候加上:
const captureException = (err, option) => {
    Raven.captureException(err, {
        fingerprint: ["API", option.url],
        message: err,
        extra: option
    });
};
//  在请求时,发生错误处理方法中可以加入此配置,在监听到错误时,详细信息会包括fingerprint: 类型, message: 错误信息, extra: 请求链接的信息以及连接等, 都将在 sentry监听到的详情里展现出来。
还有一些其他用法,待研究后更新。。。(^__^) 嘻嘻……

推荐参考资料:

【参考资料1】
【参考资料2】

查看原文

jsyzchen 赞了文章 · 4月11日

Sentry的使用

公司项目里需要加入,于是自己倒腾了下,就总结了下入门使用手法,哈哈,欢迎指点,希望能帮助大家~~~

Sentry 自我理解,可以更便捷的了解到非必现的bug、无法通过日志追踪的异常等。

一、Sentry介绍:

sentry 是一个实时事件日志记录和聚合平台。它专门用于监视错误和提取执行适当的事后操作所需的所有信息, 而无需使用标准用户反馈循环的任何麻烦。
Sentry 是一个日志平台,分为客户端和服务端,客户端(目前客户端有Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web节目方便查看。Sentry由python编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest等

二、Sentry的注册和使用

公司已有现成的服务了,所以我们只需要用公司邮箱进行注册即可。

下面介绍一下简单的使用方法:

2.1 首先注册账号: 分为UAT 和线上(ONLINE)

online: https://sentry-fe... // 这里是正式线上链接

uat: http://sentry... // 这里是uat环境链接

(公司邮箱注册)

TLYhFvRNXW.gif

新注册的账号可能无法New Project 创建,这时候需要点击左侧栏目,点击 Project & Teams ,需要加入Teams,这是刷新页面,则可以在Select project 下面看到你的项目,如果无项目请联系管理员;

YqwsIuXTtX.gif

进入后进行简单配置,然后右上角可以点击 New Project 创建,选择需要项目类型,根据提示进行配置

c2CHwXF4ct.gif

选择相对应的技术

izBwzIPLN3.gif

下面这个生成的链接也就是日后项目实时监听bug的地址(项目中的sentry.ts文件中需要配置的这个)

847I23cYbJ.gif

2.2 sentry在项目中的配置

a. 首先在项目中下载依赖
$ yarn add raven-js -D
b. sentry配置文件

(目前有一套配置文件,可直接饮用,也可对里面的错误警报规则做修改)
如:

Fs88JMk64A.gif

c. sentry 在项目中的引用:
import * as Raven from "raven-js"; // 首先引入
import { ravenOptions, DSN_ONLINE, DSN_UAT } from "config/sentry"; // 上面对sentry的配置文件
import { isLocalEnv, isUatEnv } from "utils/url";  // 封装的页面链接获取

componentDidMount() {
    if (!isLocalEnv) {
            const dsn = isUatEnv ? DSN_UAT : DSN_ONLINE;
            Raven.config(
                dsn,
                Object.assign({}, ravenOptions, {
                    release: __webpack_global__.SENTRY_RELEASE
// 这里注意了,__webpack_global__相当于是全局的,需要在tsconfig.json里进行配置,深层的原理和关联可以追踪看ezpack库
                })
            ).install();
        }
}

//  componentDidCatch 错误捕获
componentDidCatch(error, errorInfo) {
        const group = errorInfo ? location.href : "default";
        Raven.captureException(error, { extra: errorInfo, fingerprint: [group] });
    }

<br/>

Xa9IoSkx10.gif
07LOdoo0p4.gif

以上是在项目中添加sentry最简单的配置使用,当完成到这里的时候,可以进行测试 比如在项目中添加
throw new Error("test error");
发布在环境上进行测试,在自己的账号项目中检查是否监听到错误。
以上只是简单地配置在项目中了,当然,我们可以将它运用的更友好,比如在请求的时候加上:
const captureException = (err, option) => {
    Raven.captureException(err, {
        fingerprint: ["API", option.url],
        message: err,
        extra: option
    });
};
//  在请求时,发生错误处理方法中可以加入此配置,在监听到错误时,详细信息会包括fingerprint: 类型, message: 错误信息, extra: 请求链接的信息以及连接等, 都将在 sentry监听到的详情里展现出来。
还有一些其他用法,待研究后更新。。。(^__^) 嘻嘻……

推荐参考资料:

【参考资料1】
【参考资料2】

查看原文

赞 6 收藏 4 评论 0

jsyzchen 收藏了文章 · 3月2日

8个不可不知的Mac OS X专用命令行工具

OS X的终端下通用很多Unix的工具和脚本。如果从Linux迁移到OS X会发现很多熟悉的命令和脚本工具,其实并没有任何区别。

但是OS X也提供了很多其他系统所没有的特别的命令行工具。我们推荐8个这类的工具,希望有助于提高在Mac的命令行环境下的效率。

1. open

open命令用于打开文件、目录或执行程序。就等同于在命令行模式下,重复图形界面“双击”的动作。例如这个命令与在Finder中双击Safari是一样的:

$ open /Applications/Safari.app/

如果open一个文件,则会使用关联的程序打开之。例如open screenshot.png会在Preview中查看图片。

可以使用-a选项要求自行选择打开的程序,或使用-e强制在TextEdit中编辑此文件。

open一个目录会在Finder窗口中打开此目录。一个很有用的技巧是open .打开当前目录。

Finder和终端的交互是双向的——把文件从Finder中拖入终端,就等同于把文件的完整路径粘贴到命令行中。

2. pbcopy 和 pbpaste

这两个工具可以打通命令行和剪贴板。当然用鼠标操作复制粘贴也可以——但这两个工具的真正威力,发挥在将其用作Unix工具的时候。意思就是说:可以将这两个工具用作管道、IO重定向以及和其他命令的整合。例如:

$ ls ~ | pbcopy

可以将主目录的文件列表复制到剪贴板。

也可以把任意文件的内容读入剪贴板:

$ pbcopy < blogpost.txt

做点更疯狂的尝试:获取最新Google纪念徽标(doodle)的URL并复制到剪贴板:

$ curl http://www.google.com/doodles#oodles/archive | grep -A5 'latest-doodle on' | grep 'img src' | sed s/.*'<img data-original="\/\/'/''/ | sed s/'" alt=".*'/''/ | pbcopy

使用管道语法配合pbcopy工具可以简单的抓取命令的输出,而不必向上滚动翻阅终端窗口。可以用于和他人分享命令行的标准和错误输出。pbcopypbpaste也可以用于自动化或加速执行一些事情。例如把一些邮件的主题存为任务列表,就可以先从Mail.app中复制主题,再运行:

$ pbpaste >> tasklist.txt

3. mdfind

许多Linux用户都发现Linux下查找文件的方法在OS X上不好用。当然经典的Unix find命令总是可以,但既然OS X有杀手级搜索工具Spotlight,为什么不在命令行上也使用一下呢?

这就是mdfind命令了。Spotlight能做的查找,mdfind也能做。包括搜索文件的内容和元数据(metadata)。

mdfind还提供更多的搜索选项。例如-onlyin选项可以约束搜索范围为一个目录:

$ mdfind -onlyin ~/Documents essay

mdfind的索引数据库在后台自动更新,不过你也可以使用mdutil工具诊断数据库的问题,诊断mdfind的问题也等同于诊断Spotlight。如果Spotlight的工作不正确,mdutil -E命令可以强制重建索引数据库。也可以用mdutil -i彻底关闭文件索引。

4. screencapture

screencapture命令可以截图。和Grab.appcmd + shift + 3cmd + shift + 4热键相似,但更加的灵活。

抓取包含鼠标光标的全屏幕,并以image.png插入到新邮件的附件中:

$ screencapture -C -M image.png 

用鼠标选择抓取窗口(及阴影)并复制到剪贴板:

$ screencapture -c -W

延时10秒后抓屏,并在Preview中打开之:

$ screencapture -T 10 -P image.png

用鼠标截取一个矩形区域,抓取后存为pdf文件:

$ screencapture -s -t pdf image.pdf

更多用法请参阅screencapture --help

5. launchctl

launchctl管理OS X的启动脚本,控制启动计算机时需要开启的服务。也可以设置定时执行特定任务的脚本,就像Linux cron一样。

例如,开机时自动启动Apache服务器:

$ sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

运行launchctl list显示当前的启动脚本。sudo launchctl unload [path/to/script]停止正在运行的启动脚本,再加上-w选项即可去除开机启动。用这个方法可以一次去除Adobe或Microsoft Office所附带的所有“自动更新”后台程序。

Launchd脚本存储在以下位置:

~/Library/LaunchAgents    
/Library/LaunchAgents          
/Library/LaunchDaemons
/System/Library/LaunchAgents
/System/Library/LaunchDaemons

启动脚本的格式可以参考这篇blog,或苹果开发者中心的文章。你也可以使用Lingon应用来完全取代命令行。

6. say

say是一个文本转语音(TTS)的有趣的工具,引擎和OS X使用的一样也是VoiceOver。如果不加其他选项,则会简单的语音朗读你给定的字符串:

$ say "Never trust a computer you can't lift."

-f选项朗读特定文本文件,-o选项将朗读结果存为音频文件而不是播放:

$ say -f mynovel.txt -o myaudiobook.aiff

say命令可以用于在脚本中播放警告或提示。例如你可以设置Automator或Hazel脚本处理文件,并在任务完成时用say命令语音提示。

最好玩(不过也负罪感十足)的用法是:通过SSH连接到朋友或同事的计算机,然后用say命令给他们一个大大大惊喜……

可以在系统设置(System Preferences)的字典和语音(Dictation & Speech)选项中调整系统的语音选项甚至是语音的语言。

7. diskutil

diskutil是OS X磁盘工具应用的命令行版。既可以完成图形界面应用的所有任务,也可以做一些全盘填0、全盘填随机数等额外的任务。先使用diskutil list查看所有磁盘的列表和所在路径,然后对特定的磁盘执行命令。

警告:不正确使用diskutil可能意外的破坏磁盘数据。请小心。

8. brew

Homebrew程序提供的brew,严格来讲不是一个OS X的原生命令,但任何一个OS X的专业用户都不会错过它。“OS X缺少的包管理器”这个评价是恰如其分的。如果你曾经在Linux上使用过apt-get(或其他包管理器——译者注),你就会发现Homebrew基本上是一样的。

使用brew可以简单的获取数千种开源工具和函数库。例如brew install imagemagick就可以安装ImageMagick(几乎可以处理任何图像问题,转换任何格式的图像工具),brew install node可以安装Node.js(当前大热的服务器端JavaScript编程工具)。

也可以通过Homebrew做有趣的事情:brew install archey会安装Archey(在启动命令行时显示苹果LOGO和计算机硬件参数的小工具)。

请输入图片描述

Homebrew能安装的工具数量庞大,并且一直保持更新。Homebrew最棒的一点是:所有的文件都被约束在/usr/local/一个位置之下。也就是说可以通过Homebrew安装新版软件的同时,保持系统内置的依赖库或其他软件不变。同时如果想彻底删除Homebrew,也变得非常简单。

(注:删除Homebrew最好还是不要直接删除/usr/local/。应当用这个卸载脚本。)

最后,这里还有一篇OS X所有命令的完整列表


8个不可不知的Mac OS X专用命令行工具
原文:Eight Terminal Utilities Every OS X Command Line User Should Know
编译:SegmentFault
责任:沙渺

查看原文

jsyzchen 赞了文章 · 3月2日

8个不可不知的Mac OS X专用命令行工具

OS X的终端下通用很多Unix的工具和脚本。如果从Linux迁移到OS X会发现很多熟悉的命令和脚本工具,其实并没有任何区别。

但是OS X也提供了很多其他系统所没有的特别的命令行工具。我们推荐8个这类的工具,希望有助于提高在Mac的命令行环境下的效率。

1. open

open命令用于打开文件、目录或执行程序。就等同于在命令行模式下,重复图形界面“双击”的动作。例如这个命令与在Finder中双击Safari是一样的:

$ open /Applications/Safari.app/

如果open一个文件,则会使用关联的程序打开之。例如open screenshot.png会在Preview中查看图片。

可以使用-a选项要求自行选择打开的程序,或使用-e强制在TextEdit中编辑此文件。

open一个目录会在Finder窗口中打开此目录。一个很有用的技巧是open .打开当前目录。

Finder和终端的交互是双向的——把文件从Finder中拖入终端,就等同于把文件的完整路径粘贴到命令行中。

2. pbcopy 和 pbpaste

这两个工具可以打通命令行和剪贴板。当然用鼠标操作复制粘贴也可以——但这两个工具的真正威力,发挥在将其用作Unix工具的时候。意思就是说:可以将这两个工具用作管道、IO重定向以及和其他命令的整合。例如:

$ ls ~ | pbcopy

可以将主目录的文件列表复制到剪贴板。

也可以把任意文件的内容读入剪贴板:

$ pbcopy < blogpost.txt

做点更疯狂的尝试:获取最新Google纪念徽标(doodle)的URL并复制到剪贴板:

$ curl http://www.google.com/doodles#oodles/archive | grep -A5 'latest-doodle on' | grep 'img src' | sed s/.*'<img data-original="\/\/'/''/ | sed s/'" alt=".*'/''/ | pbcopy

使用管道语法配合pbcopy工具可以简单的抓取命令的输出,而不必向上滚动翻阅终端窗口。可以用于和他人分享命令行的标准和错误输出。pbcopypbpaste也可以用于自动化或加速执行一些事情。例如把一些邮件的主题存为任务列表,就可以先从Mail.app中复制主题,再运行:

$ pbpaste >> tasklist.txt

3. mdfind

许多Linux用户都发现Linux下查找文件的方法在OS X上不好用。当然经典的Unix find命令总是可以,但既然OS X有杀手级搜索工具Spotlight,为什么不在命令行上也使用一下呢?

这就是mdfind命令了。Spotlight能做的查找,mdfind也能做。包括搜索文件的内容和元数据(metadata)。

mdfind还提供更多的搜索选项。例如-onlyin选项可以约束搜索范围为一个目录:

$ mdfind -onlyin ~/Documents essay

mdfind的索引数据库在后台自动更新,不过你也可以使用mdutil工具诊断数据库的问题,诊断mdfind的问题也等同于诊断Spotlight。如果Spotlight的工作不正确,mdutil -E命令可以强制重建索引数据库。也可以用mdutil -i彻底关闭文件索引。

4. screencapture

screencapture命令可以截图。和Grab.appcmd + shift + 3cmd + shift + 4热键相似,但更加的灵活。

抓取包含鼠标光标的全屏幕,并以image.png插入到新邮件的附件中:

$ screencapture -C -M image.png 

用鼠标选择抓取窗口(及阴影)并复制到剪贴板:

$ screencapture -c -W

延时10秒后抓屏,并在Preview中打开之:

$ screencapture -T 10 -P image.png

用鼠标截取一个矩形区域,抓取后存为pdf文件:

$ screencapture -s -t pdf image.pdf

更多用法请参阅screencapture --help

5. launchctl

launchctl管理OS X的启动脚本,控制启动计算机时需要开启的服务。也可以设置定时执行特定任务的脚本,就像Linux cron一样。

例如,开机时自动启动Apache服务器:

$ sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

运行launchctl list显示当前的启动脚本。sudo launchctl unload [path/to/script]停止正在运行的启动脚本,再加上-w选项即可去除开机启动。用这个方法可以一次去除Adobe或Microsoft Office所附带的所有“自动更新”后台程序。

Launchd脚本存储在以下位置:

~/Library/LaunchAgents    
/Library/LaunchAgents          
/Library/LaunchDaemons
/System/Library/LaunchAgents
/System/Library/LaunchDaemons

启动脚本的格式可以参考这篇blog,或苹果开发者中心的文章。你也可以使用Lingon应用来完全取代命令行。

6. say

say是一个文本转语音(TTS)的有趣的工具,引擎和OS X使用的一样也是VoiceOver。如果不加其他选项,则会简单的语音朗读你给定的字符串:

$ say "Never trust a computer you can't lift."

-f选项朗读特定文本文件,-o选项将朗读结果存为音频文件而不是播放:

$ say -f mynovel.txt -o myaudiobook.aiff

say命令可以用于在脚本中播放警告或提示。例如你可以设置Automator或Hazel脚本处理文件,并在任务完成时用say命令语音提示。

最好玩(不过也负罪感十足)的用法是:通过SSH连接到朋友或同事的计算机,然后用say命令给他们一个大大大惊喜……

可以在系统设置(System Preferences)的字典和语音(Dictation & Speech)选项中调整系统的语音选项甚至是语音的语言。

7. diskutil

diskutil是OS X磁盘工具应用的命令行版。既可以完成图形界面应用的所有任务,也可以做一些全盘填0、全盘填随机数等额外的任务。先使用diskutil list查看所有磁盘的列表和所在路径,然后对特定的磁盘执行命令。

警告:不正确使用diskutil可能意外的破坏磁盘数据。请小心。

8. brew

Homebrew程序提供的brew,严格来讲不是一个OS X的原生命令,但任何一个OS X的专业用户都不会错过它。“OS X缺少的包管理器”这个评价是恰如其分的。如果你曾经在Linux上使用过apt-get(或其他包管理器——译者注),你就会发现Homebrew基本上是一样的。

使用brew可以简单的获取数千种开源工具和函数库。例如brew install imagemagick就可以安装ImageMagick(几乎可以处理任何图像问题,转换任何格式的图像工具),brew install node可以安装Node.js(当前大热的服务器端JavaScript编程工具)。

也可以通过Homebrew做有趣的事情:brew install archey会安装Archey(在启动命令行时显示苹果LOGO和计算机硬件参数的小工具)。

请输入图片描述

Homebrew能安装的工具数量庞大,并且一直保持更新。Homebrew最棒的一点是:所有的文件都被约束在/usr/local/一个位置之下。也就是说可以通过Homebrew安装新版软件的同时,保持系统内置的依赖库或其他软件不变。同时如果想彻底删除Homebrew,也变得非常简单。

(注:删除Homebrew最好还是不要直接删除/usr/local/。应当用这个卸载脚本。)

最后,这里还有一篇OS X所有命令的完整列表


8个不可不知的Mac OS X专用命令行工具
原文:Eight Terminal Utilities Every OS X Command Line User Should Know
编译:SegmentFault
责任:沙渺

查看原文

赞 53 收藏 144 评论 8

jsyzchen 收藏了文章 · 3月1日

电商系统设计之商品[番外篇]

clipboard.png

前言

这是电商系统设计系列在商品设计这块的最后一篇文章。以下是其他文章地址,按照逻辑顺序排列如下

在以上文章中,有些地方描述的不够全面,这篇文章就当补个漏了。

运费模版

运费模版的设计一般依照淘宝的设计来就比较完美了。

配置说明
模版名称用于商家选择模版
所在地区/详细地址主要是标记货源在什么位置
发货时间承诺多长时间可以发货
是否包邮很多买家都比较关心这个
运费如果不包邮,运费是多少
件数/重量/体积这里与运费相关,具体可看下方配图
指定条件包邮某些地区包邮,例如本市或者相邻的市等等

clipboard.png

商家相关

配置说明
商家名称显示在商品详情页等等
商家地址用于匹配用户所在地区
详细地址用于记录
所属行业用于匹配

如果是一家创业电商公司并且是一个多商户的电商,那你至少需要收集以下商户的相关信息

  • 法人代表姓名
  • 联系方式
  • 代理商公司
  • 代理商公司地址
  • 代理商公司法人
  • 公司邮箱
  • 法人身份证正反面
  • 营业执照
  • 开户行相关信息
  • 其他资质(授权书,资格证明等等)

商品配置

配置说明
收藏数商品收藏数只需记录用户点击收藏即可,取消收藏收藏数其实并不需要减少 (自行理解,不能多说)
分享数每次分享都需要记录
上架设置立即上架、定时上架还是不上架
是否推荐推荐到指定搜索关键字或者是首页类目等等

其他配置

配置说明
商品关键字(商品属性)手动填写,方便检索
虚拟销量前期总得有点假数据的嘛
是否允许退换货有些商品是可能无法退换货的,类似与互联网文档、源码等
减库存方式(拍下减、付款减、不减)有些商品是无库存限制的,类似于互联网文档、源码等
购买权限(单次最低购买数、一定时间段内最多购买数)有些商品是限购的或者某段时间只能购买指定数量
是否包邮、包邮条件这项并非单独的配置,一般在运费模版内设置
商品付款通知商品购买是否通知,如何通知,通知到哪里
库存提醒库存降低到指定数量提交商家补货
自动下架库存降低到指定数量自动下架

致谢

感谢你看到这里。

电商系统商品相关的文章已经到了尾声,如果有其他商品相关的文章需要编写,可以私信联系我,毕竟我也是公司员工,写这些文章并不是我的工作,只是记录我的职业生涯。当然我也希望可以帮助到各位。例如优惠券、活动(限时优惠)等等将在其他章节为大家呈现,谢谢。

代码多变,初心不变

查看原文