首页 文章 精选 留言 我的

精选列表

搜索[图像理解],共10000篇文章
优秀的个人博客,低调大师

从零开始理解HTTP协议及报文分析

前言 从事性能测试必不可绕过的就是协议,对基本知识的了解,还是深入掌握协议的机制,都能让你在从事性能测试实施时显得更加顺手。 下面我们就HTTP协议及性能测试过程必须掌握的一些分析工具来进行分享。 重点分享性能测试实施过程中必须掌握的关键技术、工具。更细节的请参考HTTP相关书籍或RFC文档。 HTTP基本架构 下面我们用一张简单的流程图来展示HTTP协议基本架构,以便大家先有个基本的了解。 Web Client可以是浏览器、搜索引擎、机器人等等一切基于HTTP协议发起http请求的工具。 Web Server可以是任何的能解析HTTP请求,并返回给Web Client可识别的响应的服务,常见的有apache、nginx、IIS等等web服务器。 浓缩就是精华,看下最简洁的HTTP交互图: HTTP报文结构 请求报文 HTTP请求报文由请求行、请求头、空行和请求内容4个部分构成。 如下图所示: 下面对上图进行简单的分析: 请求行 由请求方法字段、URL字段、协议版本字段三部分构成,它们之间由空格隔开。常用的请求方法有:GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。 请求头 请求头由key/value对组成,每行为一对,key和value之间通过冒号(:)分割。请求头的作用主要用于通知服务端有关于客户端的请求信息。 典型的请求头有: User-Agent:生成请求的浏览器类型 Accept:客户端可识别的响应内容类型列表;星号*用于按范围将类型分组。*/*表示可接受全部类型,type/*表示可接受type类型的所有子类型。 Accept-Language: 客户端可接受的自然语言 Accept-Encoding: 客户端可接受的编码压缩格式 Accept-Charset:可接受的字符集 Host: 请求的主机名,允许多个域名绑定同一IP地址 connection:连接方式(close或keeplive) Cookie: 存储在客户端的扩展字段 空行 最后一个请求头之后就是空行,用于告诉服务端以下内容不再是请求头的内容了。 请求内容 请求内容主要用于POST请求,与POST请求方法配套的请求头一般有Content-Type(标识请求内容的类型)和Content-Length(标识请求内容的长度) 响应报文 HTTP响应报文由状态行、响应头、空行和响应内容4个部分构成。 如下图所示: 下面对响应报文格式进行简要的分析说明: 状态行 由HTTP协议版本、状态码、状态码描述三部分构成,它们之间由空格隔开。 状态码由3位数字组成,第一位标识响应的类型,常用的5大类状态码如下: 1xx:表示服务器已接收了客户端的请求,客户端可以继续发送请求 2xx:表示服务器已成功接收到请求并进行处理 3xx:表示服务器要求客户端重定向 4xx:表示客户端的请求有非法内容 5xx:标识服务器未能正常处理客户端的请求而出现意外错误 常见状态码说明: 200 OK:表示客户端请求成功 400 Bad Request:表示客户端请求有语法错误,不能被服务器端解析 401 Unauthonzed:表示请求未经授权,该状态码必须与WWW-Authenticate报文头一起使用 404 Not Found:请求的资源不存在,例如输入了错误的url 500 Internal Server Error:表示服务器发生了不可预期的错误,导致无法完成客户端的请求 503 Service Unavailable:表示服务器当前不能处理客户端的请求,在一段时间后服务器可能恢复正常 响应头 一般情况下,响应头会包含以下,甚至更多的信息。 Location:服务器返回给客户端,用于重定向到新的位置 Server:包含服务器用来处理请求的软件信息及版本信息 Vary:标识不可缓存的请求头列表 Connection: 连接方式。 对于请求端来讲:close是告诉服务端,断开连接,不用等待后续的求请了。keeplive则是告诉服务端,在完成本次请求的响应后,保持连接,等待本次连接后的后续请求。 对于响应端来讲:close表示连接已经关闭。keeplive则表示连接保持中,可以继续处理后续请求。Keep-Alive表示如果请求端保持连接,则该请求头部信息表明期望服务端保持连接多长时间(秒),例如300秒,应该这样写Keep-Alive: 300 空行 最后一个响应头之后就是空行,用于告诉请求端以下内容不再是响应头的内容了。 响应内容 服务端返回给请求端的文本信息。 HTTP报文示例 在这里我们在Firefox下用firebug随意抓取一个HTTP包和上文的报文结构做下一一对应关系图,以便大家了解实际的包和标准报文结构的对应关系。 总结 对于HTTP协议的交互过程这里就不再进行说明了,大家可以搜索下相关的资料进行学习,上述的内容请务必熟练掌握、深刻了解。更详细的内容推荐大家学习RFC 2616(http协议1.1版本,有中文版本) 更多精彩: 工具篇 - JMeter之旅01 工具篇 - JMeter目录及关键配置分析02 webdriver介绍&与Selenium RC的比较 Python Selenium Webdriver安装手册 创建你的第一个webdriver python代码 基于Excel参数化你的Selenium2测试 Python Selenium设计模式-POM Python多线程Selenium跨浏览器测试 python selenium2示例 - 生成 HTMLTestRunner 测试报告 随意支持下,一起做好分享 扫一扫或长按关注开源优测公众号,原创文章首发平台 在公众号里回复一下关键字获取对应的系列文章 物联网 appium 大数据测试 RobotFramework Python 自动驾驶 jmeter selenium jenkins 接口测试 顺手点点点右下角的 本文分享自微信公众号 - 开源优测(DeepTest)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

谈谈对于企业级系统架构的理解

云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 在我们刚开始学习架构的时候,首先会想到分层的概念,分层架构比较经典的是三层架构,那么,什么是三层架构呢?它包括表现层、业务层、数据访问层;而对于一个新手来说,从抽象意义上的三层架构,逻辑上就划分为三个层。 这个是最基本的三层架构模式。 表现层充当系统的界面呈现以及UI逻辑的角色,也就是说,UI(用户界面)属于表现层; 举一个对于asp.net WebForm来说,人们喜欢把对于UI的控制逻辑(服务器控件的读取、设置、事件等等)写在页面的后置隐藏代码中,并且依赖业务逻辑层。当然,服务器控件支持数据绑定的功能,可以通过数据源进行绑定控件。这样就可以节省在后置隐藏中的代码。 因此,我们就可以把表现层分为UI用户界面以及UI逻辑: UI用户界面的职责只是作为数据输入和输出后的展示工作。 UI逻辑的职责是负责业务逻辑层以及UI用户界面之间的数据交互,并且尽可能地让UI逻辑不依赖于UI技术。 其中UI用户界面的实现方式有很多,包括ASP.NET、WinForm、WPF、Silverlight、移动Web、智能设备等等。 将表现层中UI页面和UI逻辑分离的策略中,当前使用最多的两种模式是MVC模式和MVP模式。 MVC模式,即模型-视图-控制器模式,通过视图触发并执行某个操作,调用控制器,通过控制器去操作业务层,最终返回模型,在视图中进行展示。这里的模型可以是一个领域模型(DM),也可以是一个数据迁移对象(DTO)。 MVP模式,即模型-视图-展示器模式,和MVC模式有点像,不同的是MVP中视图和模型是被完全分离出来的,视图中定义一个接口,而展示器通过调用该接口的方法以控制视图。因此,视图和模型是松散的,展示器也充当了一个控制器的角色,同时它也不依赖于UI技术。 另外再介绍一种模式PM(Preentation Model),它可以说是MVP的变体,在PM中,视图不定义接口,这里的模型只是表示视图状态的类,视图中的元素被直接绑定到模型属性上。例如在WPF中,WPF就先天的具有数据双向绑定机制以及事件通知属性机制。 所以它特别适用于WPF,Sliverlight等等。 在开始业务层之前,不得不说一个前提,在一个小型项目中,直接让表现层调用业务层,足以解决所有问题。但是,当项目大到使用多种表现形式,如使用了各种UI技术、ASP.NET、WPF、移动设备等等,就要考虑在你的表现层和业务层之间增加一个层,以至于让表现层和业务层解耦,因为业务层作为一个业务中间件的平台,最好不要暴露于表现层中,这个层就是传说中的服务层。架构图又演化为: 服务层实际上并不执行任何具体的工作,其功能在于组织各个业务对象,服务层将业务层所有的细节对表现层都隐藏起来,服务器将组织业务逻辑层中的组件,并且通过数据迁移对象(DTO)与表现层交互,因此就产生一个DTO模型。 为了实现服务的可重用性,需要使用服务接口,表现层通过规定的接口访问功能。服务的实现继承服务接口,而服务的实现专注于业务层的调用。 对于服务层,常用的方法包括Web服务、.NET Remoting、Rest以及WCF技术。 本人比较建议使用WCF作为服务,因为可以方便地通过配置达到远程调用服务的目的。 服务层消除了两个表现层和业务层之间的耦合,服务层可以实现一个远程接口,达到多UI技术甚至多平台上的通信。 当然增加服务层也有缺点,假如使用WCF服务,会增加系统的调用开销,进而影响性能。 业务层中包含系统所需要业务过程上的实现,并与下层的数据访问层交互。 我们通常也叫做业务层叫做业务逻辑层,但我认为业务逻辑层是属于业务层的一方面,业务逻辑更专注于业务上逻辑算法的实现。因为业务层还可以包括其他的方面。 业务层必须包括对业务实体尽心建模的对象模型,表达了客户的所有策略和需求的业务规则,因此就产生了领域模型。 (PS:如果这里你不使用领域模型,那么需要采用业务规则层进行业务功能上的业务规则的验证和控制) 领域模型包括对实体的属性定义,方法定义以及实体与实体之间的关系。从这个角度上看,UML建模至关重要,通过对UML动态图和静态图的描述,可以映射到领域模型中。 从服务层刚才讲到了DTO模型,这里需要一个机制将DTO转化为领域模型,所以产生了DTO映射层(DTOMapper)。 另外业务层还包括核心中间件技术,包括第三方组件,以及工作流引擎等等。 业务层需要考虑到一些与数据访问层交互的设计模式,模式中包括事物脚本模式、表模块模式、活动记录模式、领域模型模式。 事物脚本模式是通过方法来执行业务流程,它是一个过程式模型,事物脚本的每个方法都有一个特定的事物脚本,它侧重于业务上一系列流程上的顺序操作,它实现起来很简单,但是它有个致命的缺点就是它会造成很多重复的代码。 表模块模式比起事物脚本模式,具有一定的结构,它的思想也很简单,每个数据表都定义一个业务组件(实体类,实体操作类),在.NET中更多的使用DataSet作为表模型的数据交互。但是它也有一个缺点就是它是从数据库驱动它不适合于大量的数据表以及数据表之间的复杂关系。 活动记录模式中的对象中,可以包含数据和方法。它接近于数据表的结构,它的对象中执行方法中可以包含CRUD操作,验证算法,以及其他的计算功能。一般来说,领域模型不是太复杂,活动记录模式是个好选择。当然他也存在问题,同样地,它对于复杂的业务上,维护的成本也很高,并且如果需求变更导致数据库修改,就需要调整记录对象模型中的相关代码。 经典应用:LINQ-TO-SQL以及Castle ActiveRecord。 领域模型模式是从领域驱动设计中衍生来的,它是以业务为核心的设计模式。它对于复杂的业务逻辑,相当适用。前三种方式使用的是以数据驱动方式,数据驱动方式特点简单,但是当系统到了一定的规模后,就会到难以维护的程度。 数据访问层的目的很明确,主要作为提供数据持久化的功能,包括数据的读取和写入,另外还必须包括事务处理,并发控制等等。 操作数据库的方法可以有两种方式:ORM方式、ADO.NET方式。 ORM可以采用一些第三方的ORM框架来实现,ADO.NET采用ASP.NET自带的数据库操作来实现。 不同的数据库具有不同的持久化实现,因此这里添加一个存储仓库接口层,来适应不同的数据库实现,这里你可以使用IOC依赖注入方式进行数据库选型,可以利用Unity、Spring.NET、Castle的IOC容器等等。 最后各个层中都可以依赖于公共基础设施层。 公共基础设施层可以包括Common通用模块、Logging日志模块、Exception异常模块、Configuration配置模块、DI依赖注入模块、单元测试模块以及第三方组件(例如NHibernate、Sprint.NET、Castle、Quartz计划任务等等) 最终图: 总结:项目类型、项目规模以及业务上的需求,都影响着系统架构的设计,系统架构并不是一层不变的,没有最好的架构,只有更好的架构,并且从项目中多思考系统的扩展性。 【云栖号在线课堂】每天都有产品技术专家分享!课程地址:https://yqh.aliyun.com/live 立即加入社群,与专家面对面,及时了解课程最新动态!【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK 原文发布时间:2020-07-31本文作者:CRM咨询本文来自:“51CTO”,了解相关信息可以关注“51CTO”

优秀的个人博客,低调大师

java深入理解浅拷贝和深拷贝

云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 简介 拷贝对象是java中经常会遇到的问题。java中存在两种类型,基础类型和引用类型。 java的赋值都是传值的,对于基础类型来说,会拷贝具体的内容,但是对于引用对象来说,存储的这个值只是指向实际对象的地址,拷贝也只会拷贝引用地址。 因为引用对象的存在,所以经常会出现和预期不一样的情况。 本文将会深入的探讨一下在拷贝对象中会出现的浅拷贝和深拷贝的情况。 拷贝接口 java中所有的对象都是继承自java.lang.Object。Object对象中提供了一个clone方法,来供我们对java对象进行拷贝。 这个clone方法是native的,所以不需要我们来实现,但是注意clone方法还是protected,这意味着clone方法只能在java.lang包或者其子类可见。 如果我们想要在一个程序中调用某个对象的clone方法则是不可以的。因为clone方法是定义在Object中的,该对象并没有对外可见的clone方法。 JDK的建议是让我们去实现接口Cloneable,实现了这个接口就表示这个对象可以调用Object的clone方法。 注意,即使你实现了Cloneable接口,还是无法在外部程序中调用该对象的clone方法: 因为Cloneable是空的,明没有强制要你去实现clone方法。 这是JDK在设计上的问题,导致clone方法并不像预期那么好用。 首先clone只是对象的拷贝,它只是简单的拷贝对象,而不会去执行对象的构造函数。 其次clone会导致浅拷贝的问题。 使用clone导致的浅拷贝 我们举个clone产生的浅拷贝的例子,我们定义一个对象中的对象,然后尝试拷贝: 上面的例子中,我们定义了CustUser和Address。 浅拷贝我们只调用了CustUser的clone方法。看下输出结果: 我们可以看到拷贝之后的Address变化会影响到被拷贝的对象。 上面的例子我们还要关注两个点:第一点String是不可变的。不管是拷贝还是赋值,String都是不可变的。 第二点,上面的例子中我们定义了一个数组,可以看到如果只是调用clone的话,数组也是浅拷贝。 使用clone的深拷贝 要使用深拷贝,只需要修改CustUser的构造函数就可以了: 在重写的clone方法中,我们分别调用了CustUser,Address和数组的clone方法来进行拷贝。 再运行一次上面的测试代码: 可以看到address和cars是不同的,这表示我们的深拷贝是成功的。 不要overridden clone 上面的例子我们是通过overridden Object的clone方法来实现的。 但是最佳实践是不要overridden clone。那我们怎么做呢? 使用构造函数来构建新的对象: 据说数组直接用clone来拷贝会更快,也可以使用下面的方式来拷贝数组: 总结 本文讲解了浅拷贝和深拷贝的应用,并对clone方法做了深入的探讨。 【云栖号在线课堂】每天都有产品技术专家分享!课程地址:https://yqh.aliyun.com/live 立即加入社群,与专家面对面,及时了解课程最新动态!【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK 原文发布时间:2020-05-11本文作者:flydean本文来自:“掘金”,了解相关信息可以关注“掘金”

优秀的个人博客,低调大师

每日一博 | 深入理解JVM - 方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。 解析 所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution),在Java语言中符合这种要求的主要有静态方法和私有方法。 方法调用指令 invokestatic:用于调用静态方法。 invokespecial:用于调用实例构造器<init>()方法、私有方法和父类中的方法。 invokevirtual:用于调用所有的虚方法。 invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象。 invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。 前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。 方法分类 在java语言中方法主要分为“虚方法”和“非虚方法”。 非虚方法:在类加载的时候就可以把符号引用解析为该方法的直接引用。比如:静态方法、私有方法、实例构造器、父类方法和被final修饰的方法。 虚方法:需要在运行时才能将符号引用转换成直接引用,如,分派。 分派 分派(Dispatch)它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。 静态分派 依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载,虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型来作为判定依据的。 public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void sayHello(Human guy) { System.out.println("hello,guy!"); } public void sayHello(Man guy) { System.out.println("hello,gentleman!"); } public void sayHello(Woman guy) { System.out.println("hello,lady!"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.sayHello(man); sr.sayHello(woman); } } 运行结果: hello,guy! hello,guy! Human man = new Man(); 这里的Human 就是变量的“静态类型”(Static Type),或者叫“外观类型”(Apparent Type);Man就是变量的“实际类型”(Actual Type)或者叫“运行时类型”(Runtime Type)。 动态分派 我们把在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的表现就是重写。 public class DynamicDispatch { static abstract class Human { abstract void sayHello(); } static class Man extends Human { public void sayHello() { System.out.println("hello,Man!"); } } static class Woman extends Human { public void sayHello() { System.out.println("hello,Woman!"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); } } 运行结果: hello,Man! hello,Woman! 我们通过javap命令看下main方法的字节码: ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class com/xiaolyuh/DynamicDispatch$Man 3: dup 4: invokespecial #3 // Method com/xiaolyuh/DynamicDispatch$Man."<init>":()V 7: astore_1 8: new #4 // class com/xiaolyuh/DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method com/xiaolyuh/DynamicDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method com/xiaolyuh/DynamicDispatch$Human.sayHello:()V 20: aload_2 21: invokevirtual #6 // Method com/xiaolyuh/DynamicDispatch$Human.sayHello:()V 24: return LineNumberTable: line 27: 0 line 28: 8 line 29: 16 line 30: 20 line 31: 24 LocalVariableTable: Start Length Slot Name Signature 0 25 0 args [Ljava/lang/String; 8 17 1 man Lcom/xiaolyuh/DynamicDispatch$Human; 16 9 2 woman Lcom/xiaolyuh/DynamicDispatch$Human; } ... 通过字节码我们发现:在main方法中,sayHello()方法的调用对应的符号引用是一样的,com/xiaolyuh/DynamicDispatch$Human.sayHello:()V 。在这里我们可以得出一个结论:在动态分派的情况下,在编译时期我们是无法确定方法的直接引用的,那么它是怎么实现重载方法的调用的呢?问题关键是在invokevirtual指令上,在执行invokevirtual指令时,invokevirtual指令会去确定方法的调用版本。 invokevirtual指令的运行过程 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。 正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。 当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段 动态分派的实现 因为动态方法执行非常频繁,并且动态分派的方法版本选择需要在运行时,在实际接受者类型的方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,虚拟机会为类型在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。在图中,Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。 虚方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕。 单分派与多分派 方法的接收者与方法的参数统称为方法的宗量。分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据两个及以上的宗量对目标方法进行选择。 静态分派需要根据静态类型和方法参数两个宗量来确定方法调用,所以属于多分派。 动态分派只需要根据实际类型一个宗量来确定方法的调用,所以属于单分派。 在动态分派的过程中,方法签名是确定的,所以方法参数就不会变,方法调用就取决于参数的实际类型。 总结 解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。分派(Dispatch)调用则要复杂许多,它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。

优秀的个人博客,低调大师

Streams:深入理解Redis5.0新特性

概述 相较于Redis4.0,Redis5.0增加了很多新的特性,而streams是其中最重要的特性之一。streams是redis 的一种基本数据结构,它是一个新的强大的支持多播的可持久化的消息队列,在设计上借鉴了kafaka。streams的数据类型本身非常简单,有点类似于hash结构,但是它的额外特性异常强大且复杂: 支持持久化。streams能持久化存储数据,不同于pub/sub机制和list 消息被消费后就会被删除,streams消费过的数据会被持久化的保存在历史中。 支持多播。 这一点跟 pub/sub有些类似。 支持消费者组。streams 允许同一消费组内的消费者竞争消息,并提供了一系列机制允许消费者查看自己的历史消费消息。并允许监控streams的消费者组信息,消费者组内消费者信息,也可以监控streams内消息的状态。 基础内容 数据 ID streams 提供了默认的id模式用来唯一标识streams中的每一条数据,由两部分组成: <millisecondsTime>-<sequenceNumber> millisecondsTime是redis服务所在机器的时间,sequenceNumber用于同一毫秒创建的数据。需要注意的一点是streams的id总是单调增长的,即使redis服务所在的服务器时间异常。如果当前的毫秒数小于以前的毫秒数,就会使用历史记录中最大的毫秒数,然后序列号递增。而这样做的原因是因为streams的机制允许根据时间区间或者某一个时间节点或者某一id查找数据。 向streams插入数据 streams 的基础写命令为XADD,其语法为XADD key ID field value [field value ...] 127.0.0.1:6379> XADD mystream * name dwj age 18 "1574925508730-0" 127.0.0.1:6379> 上面的例子使用XADD向名为mystream的streams中添加了一条数据,ID使用*表示使用streams使用默认的ID,在本例中redis返回的1574925508730-0就是redis为我们插入的数据生成的ID。 另外streams 查看streams长度的命令为XLEN 127.0.0.1:6379> XLEN mystream (integer) 3 127.0.0.1:6379> 从streams中读取数据 从streams中读取数据会比写数据复杂很多,用日志文件进行对比,我们可以查看历史日志,可以根据范围查询日志,我们可以通过unix的命令tail -f来监听日志,可以多个用户查看到同一份日志,也可以多个用户只能查看到自己有权限查看的那一部分日志。 按范围查询: XRANGE 和 XREVRANGE 首先来介绍一下 根据范围查询,这两种操作都比较简单,以XRANGE为例,它的语法格式为XRANGE key start end [COUNT count], 我们只需要提供两个id,start和end,返回的将是一个包含start和end的闭区间。两个特殊的ID-和+分别表示可能的最小ID和最大ID。 127.0.0.1:6379> XRANGE mystream - + 1) 1) "1574835253335-0" 2) 1) "name" 2) "bob" 3) "age" 4) "23" 2) 1) "1574925508730-0" 2) 1) "name" 2) "dwj" 3) "age" 4) "18" 127.0.0.1:6379> 我们前边提到过数据id中包含了创建数据的时间信息,这意味着我们可以根据时间范围查询数据,为了根据时间范围查询,我们省略掉ID的序列号部分,如果省略,对于start ID会使用0作为默认的序列号,对于end ID会使用最大序列号作为默认值,这样的话我们使用两个unix时间戳去查询数据就可以得到那个时间区间内所有的数据。 1) 1) "1574835253335-0" 2) 1) "name" 2) "bob" 3) "age" 4) "23" 127.0.0.1:6379> 可能还会有同学注意到语法的最后边还有count参数,这个参数允许我们一次只返回固定数量的数据,然后根据返回数据的last_id,作为下一次查询的start,这样就允许我们在一个量非常大的streams里批量返回数据。 XREVRANGE命令与XRANGE相同,但是以相反的顺序返回元素,就不重复介绍了。 通过XREAD读取数据 XREAD允许我们从某一结点开始从streams中读取数据,它的语法为XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...],我们在这里主要将的是通过XREAD来订阅到达streams新的数据。这种操作可能跟REDIS中原有的pub/sub机制或者阻塞队列的概念有些类似,都是等待一个key然后获取到新的数据,但是跟这两种有着本质的差别: streams跟pub/sub和阻塞队列允许多个客户端一起等待数据,默认情况下,streams会把消息推送给所有等待streams数据的客户端,这个能力跟pub/sub有点类似,但是streams也允许把消息通过竞争机制推送给其中的一个客户端(这种模式需要用到消费者组的概念,会在后边讲到)。 pub/sub的消息是fire and forget并且从不存储,你只可以订阅到在你订阅时间之后产生的消息,并且消息只会推送给客户端一次,不能查看历史记录。以及使用阻塞队列时,当客户端收到消息时,这个元素会从队列中弹出,换句话说,不能查看某个消费者消费消息的历史。而在streams中所有的消息会被无限期的加入到streams中(消息可以被显式的删除并且存在淘汰机制),客户端需要记住收到的最后一条消息,用于获取到节点之后的新消息。 Streams 消费者组提供了一种Pub/Sub或者阻塞列表都不能实现的控制级别,同一个Stream不同的群组,显式地确认已经处理的项目,检查待处理的项目的能力,申明未处理的消息,以及每个消费者拥有连贯历史可见性,单个客户端只能查看自己过去的消息历史记录。 从streams中读取数据 127.0.0.1:6379> XREAD COUNT 2 STREAMS mystream 0 1) 1) "mystream" 2) 1) 1) "1574835253335-0" 2) 1) "name" 2) "bob" 3) "age" 4) "23" 2) 1) "1574925508730-0" 2) 1) "name" 2) "dwj" 3) "age" 4) "18" 127.0.0.1:6379> 同list结构一样,streams也提供了阻塞读取的命令 XREAD BLOCK 0 STREAMS mystream 在上边的命令中指定了BLOCK选项,超时时间为0毫秒(意味着永不会过期)。此外,这个地方使用了特殊的id $,这个特殊的id代表着当前streams中最大的id,这就意味着你只会读取streams中在你监听时间以后的消息。有点类似于Unix的tail -f。另外XREAD可以同时监听多个流中的数据。 消费者组 如果我们想要的不是多个客户端处理相同的消息,而是多个客户端从streams中获取到不同的消息进行处理。也就是我们常用的生产者-消费者模型。假如想象我们具有两个生产者p1,p2,三个消费者c1,c2,c3以及7个商品。我们想按照下面的效果进行处理 p1 =>item1 => c1 p2 =>item2 => c2 p1 =>item3 => c3 p2 =>item4 => c1 p1 =>item5 => c2 p2 =>item6 => c3 p1 =>item7 => c1 为了解决这种场景,redis使用了一个名为消费者的概念,有点类似于kafka,但只是表现上。消费者组就像是一个伪消费者,它从流内读取数据,然后分发给组内的消费者,并记录该消费者组消费了哪些数据,处理了那些数据,并提供了一系列功能。 每条消息都提供给不同的消费者,因此不可能将相同的消息传递给多个消费者。 消费者在消费者组中通过名称来识别,该名称是实施消费者的客户必须选择的区分大小写的字符串。这意味着即便断开连接过后,消费者组仍然保留了所有的状态,因为客户端会重新申请成为相同的消费者。 然而,这也意味着由客户端提供唯一的标识符。 每一个消费者组都有一个第一个ID永远不会被消费的概念,这样一来,当消费者请求新消息时,它能提供以前从未传递过的消息。 消费消息需要使用特定的命令进行显式确认,表示:这条消息已经被正确处理了,所以可以从消费者组中逐出。 消费者组跟踪所有当前所有待处理的消息,也就是,消息被传递到消费者组的一些消费者,但是还没有被确认为已处理。由于这个特性,当访问一个Stream的历史消息的时候,每个消费者将只能看到传递给它的消息。 它的模型类似于如下 | consumer_group_name: mygroup | | consumer_group_stream: somekey | | last_delivered_id: 1292309234234-92 | | | | consumers: | | "consumer-1" with pending messages | | 1292309234234-4 | | 1292309234232-8 | | "consumer-42" with pending messages | | ... (and so forth) | 从上边的模型中我们可以看出消费者组记录处理的最后一条消息,将消息分发给不同的消费者,每个消费者只能看到自己的消息。如果把消费者组看做streams的辅助数据结构,我们可以看出一个streams可以拥有多个消费者组,一个消费者组内可以拥有多个消费者。实际上,一个streams允许客户端使用XREAD读取的同时另一个客户端通过消费者群组读取数据。 创建一个消费者群组 我们首先创建一个包含了一些数据的streams 127.0.0.1:6379> XADD fruit * message apple "1574935311149-0" 127.0.0.1:6379> XADD fruit * message banada "1574935315886-0" 127.0.0.1:6379> XADD fruit * message pomelo "1574935323628-0" 然后创建一个消费者组 127.0.0.1:6379> XGROUP CREATE fruit mygroup $ OK 注意我们需要指定一个id,这里我们使用的是特殊id$,我们也可以使用0或者一个unix时间戳,这样,消费者组只会读取这个节点之后的消息。 现在消费者组创建好了,我们可以使用XREADGROUP命令立即开始尝试通过消费者组读取消息。 XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...],与XREAD类似,提供了BLOCK选项。假设指定消费者分别是Alice和Bob,来看看系统会怎样返回不同消息给Alice和Bob。 127.0.0.1:6379> XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS fruit > 1) 1) "fruit" 2) 1) 1) "1574936034258-0" 2) 1) "message" 2) "apple" 127.0.0.1:6379> 上边命令代表的信息是:我要通过mygroup读取streams fruit中的数据,我在群组中的身份是Alice,请给我一条数据。 >操作符只在消费者组的上线文中有效,代表消息到目前为止没有交给其它消费者处理过。 我们也可以使用一个有效的id,在这种情况下,消费者组会告诉我们的历史待处理消息,而不会告诉我们新的消息。这个特性也是很有用的,当消费者因为某些原因重新启动后,我们可以查看自己的历史待处理消息,处理完待处理消息后再去处理新的消息。 我们可以通过XACK命令告诉消费者组某条消息已经被正确处理,不要显示在我的历史待处理消息列表中。XACK的语法为XACK key group ID [ID ...] 127.0.0.1:6379> XACK fruit mygroup 1574936034258-0 (integer) 1 有几件事需要记住: 消费者是在他们第一次被提及的时候自动创建的,不需要显式创建。 即使使用XREADGROUP,你也可以同时从多个key中读取,但是要让其工作,你需要给每一个Stream创建一个名称相同的消费者组。这并不是一个常见的需求,但是需要说明的是,这个功能在技术上是可以实现的。 XREADGROUP命令是一个写命令,因为当它从Stream中读取消息时,消费者组被修改了,所以这个命令只能在master节点调用。 从永久失败中恢复 在一个消费者群组中可能存在多个消费者消费消息,但是也可能会存在某一个消费者永久退出消费者群组的情况,这样我们就需要一种机制,把该消费者的待处理消息分配给消费者群组的另一个消费者。这就需要我们具有查看待处理消息的能力以及把某个消息分配给指定消费者的能力。前者是通过一个叫XPENDING的命令,它的语法为XPENDING key group [start end count] [consumer] 127.0.0.1:6379> XPENDING fruit mygroup 1) (integer) 1 2) "1574936042937-0" 3) "1574936042937-0" 4) 1) 1) "Alice" 2) "1" 上述返回结果代表的是消费者群组有1条待处理命令,待处理消息的起始id为1574936042937-0,结束id为1574936042937-0,名为Alice的消费者有一个待处理命令,可能有人会好奇我们在前边往fruit放入了3个水果,使用XACK处理了一个水果,消费者待处理列表中应该有两个水果,而事实上消费者群组的待处理列表为该群组下消费者待处理消息的合集,当有消费者通过群组获取消息的时候会改变消费者群组的状态,这也是前边提到的为什么XREADGROUP必须在master节点进行调用。 我们可以使用start end count 参数来查看某个范围内消息的状态 127.0.0.1:6379> XPENDING fruit mygroup - + 10 Alice 1) 1) "1574936042937-0" 2) "Alice" 3) (integer) 903655 4) (integer) 1 2) 1) "1574936052018-0" 2) "Alice" 3) (integer) 491035 4) (integer) 1 这样我们就看到了一条消息的详细信息,id为1574936042937-0的消息的消费者为Alice,它的pending时间为903655,这个消息被分配了1次。 我们会发现第一条消息的处理时间有点长,我们怀疑Alice已经不能处理这条消息了,于是我们想把这条消息分配给Bob,这种场景下就需要用到了XCLAIM命令,它的语法为XCLAIM ... ,其中min-idle-time为消息的最小空闲时间,只有消息的空闲时间大于这个值消息才会被分配,因为消息被分配的时候会重置消息的空闲时间,如果有同时把一条消息分配给两个客户端,只会第一条命令生效,因为当消息分配给第一个客户端的时候重置空闲时间,第二条命令则会失效。 我们也可以使用一个独立的进程来不断寻找超时的消息,并把它分配给活跃的消费者,不过需要注意的是,如果消息的分配次数达到某个阙值,不应该把消息再分配出去,而是应该放到别的地方。 streams的可观察性 streams具有不错的可观察性,前边的XPENDING命令允许我们查看streams在某个消费者群组内待处理消息的状态。但是我们想看的更多,比如在这个streams下有多少个group, 在这个group下有多少消费者。这就要用到XINFO命令: 查看streams信息: 127.0.0.1:6379> XINFO STREAM mystream 1) "length" 2) (integer) 2 3) "radix-tree-keys" 4) (integer) 1 5) "radix-tree-nodes" 6) (integer) 2 7) "groups" 8) (integer) 1 9) "last-generated-id" 10) "1574925508730-0" 11) "first-entry" 12) 1) "1574835253335-0" 2) 1) "name" 2) "bob" 3) "age" 4) "23" 13) "last-entry" 14) 1) "1574925508730-0" 2) 1) "name" 2) "dwj" 3) "age" 4) "18" 输出中会告诉我们streams的长度,群组数量,第一条和最后一条信息的详情。下面看一下streams下群组的信息: 127.0.0.1:6379> XINFO GROUPS fruit 1) 1) "name" 2) "mygroup" 3) "consumers" 4) (integer) 1 5) "pending" 6) (integer) 2 7) "last-delivered-id" 8) "1574936052018-0" 2) 1) "name" 2) "mygroup-1" 3) "consumers" 4) (integer) 0 5) "pending" 6) (integer) 0 7) "last-delivered-id" 8) "0-0" 我们可以从输出中看到fruit下有两个群组,群组的名称以及待处理消息的数量,处理的最后一条消息。我们可以在详细的查看下消费者群组内消费者的状态。 127.0.0.1:6379> XINFO CONSUMERS fruit mygroup 1) 1) "name" 2) "Alice" 3) "pending" 4) (integer) 2 5) "idle" 6) (integer) 1990242 2) 1) "name" 2) "Bob" 3) "pending" 4) (integer) 1 5) "idle" 6) (integer) 9178 从输出中可以看到消费者待处理消息的数量以及消费者的闲置时间。 设置streams上限 如果从streams可以查看到历史记录,我们可能会有疑惑,如果streams无限期的加入内存会不会够用,一旦消息数量达到上限,将消息永久删除或者持久化到数据库都是有必要的,redis也提供了诸如此类场景的支持。 一种方法是我们使用XADD的时候指定streams的最大长度,XADD mystream MAXLEN ~ 1000 其中的数值前可以加上~标识不需要精确的将长度保持在1000,比1000多一些也可以接受。如果不使用该标识,性能会差一些。另一种方法是使用XTRIM,该命令也是使用MAXLEN选项,> XTRIM mystream MAXLEN ~ 10 一些特殊的id 前面提到了在streams API里边存在一些特殊的id。 首先是-和+,这两个ID在XRANGE命令中使用,分别代表最小的id和最大的id。-代表0-1,+代表18446744073709551615-18446744073709551615,从使用上方便了很多。在XPENDING等范围查询中都可以使用。 $代表streams中当前存在的最大的id,在XREAD和XGROUP中代表只获取新到的消息。需要注意的是$跟+的含义并不一致。 还有一个特殊的id是>,这个id只能够在XREADGROUP命令中使用,意味着在这个消费者群组中,从来没有分配给其他的消费者,所以总是使用>作为群组中的last delivered ID。 持久化,复制和消息安全性 与redis的其它数据结构一样,streams会异步复制到从节点,并持久化到AOF和RDB文件中,并且消费者群组的状态也会按照此机制进行持久化。 需要注意的几点是: 如果消息的持久化以及状态很重要,则AOF必须使用强fsync配合(AOF记录每一条更改redis数据的命令,有很多种持久化机制,在这个地方要用到的是appendfsync always 这样会严重降低Redis的速度) 默认情况下,异步复制不能保证从节点的数据与主节点保持一致,在故障转移以后可能会丢失一些内容,这跟从节点从主节点接受数据的能力有关。 WAIT命令可以用于强制将更改传输到一组从节点上。虽然这使得数据不太可能会丢失,但是redis的Sentinel和cluster在进行故障转移的时候不一定会使用具有最新数据的从节点,在一些特殊故障下,反而会使用缺少一些数据的从节点。 因此在使用redis streams和消费者群组在设计程序的时候,确保了解你的应用程序在故障期间的应对策略,并进行相应地配置,评估它对你的程序是否足够安全。 从streams中删除数据 删除streams中的数据使用XDEL命令,其语法为XDEL key ID [ID ...],需要注意的是在当前的实现中,在宏节点完全为空之前,内存并没有真正回收,所以你不应该滥用这个特性。 streams的性能 streams的不阻塞命令,比如XRANGE或者不使用BLOCK选项的XREAD和XREADGROUP跟redis普通命令一致,所以没有必要讨论。如果有兴趣的话可以在redis的文档中查看到对应命令的时间复杂度。streams命令的速度在一定范围内跟set是一致的,XADD命令的速度非常快,在一个普通的机器上,一秒钟可以插入50w~100w条数据。 我们感兴趣的是在消费者群组的阻塞场景下,从通过XADD命令向streams中插入一条数据,到消费者通过群组读取到这条消息的性能。 为了测试消息从产生到消费间的延迟,我们使用ruby程序进行测试,将消息的产生时间作为消息的一个字段,然后把消息推送到streams中,客户端收到消息后使用当前时间跟生产时间进行对比,从而计算出消息的延迟时间。这个程序未进行性能优化,运行在一个双核的机器上,同时redis也运行在这台机器上,以此来模拟不是理想条件下的场景。消息每秒钟产生1w条,群组内有10个消费者消费数据。测试结果如下: Processed between 0 and 1 ms -> 74.11% Processed between 1 and 2 ms -> 25.80% Processed between 2 and 3 ms -> 0.06% Processed between 3 and 4 ms -> 0.01% Processed between 4 and 5 ms -> 0.02% 99.9%的请求的延迟小于等于2毫秒,而且异常值非常接近平均值。另外需要注意的两点: 消费者每次处理1w条消息,这样增加了一些延迟,这样做是为了消费速度较慢的消费者能够保持保持消息流。 用来做测试的系统相比于现在的系统非常慢。 原文链接: https://redis.io/topics/streams-intro 译者:Worktile工程师 杜文杰 文章来源:Worktile技术博客 欢迎访问交流更多关于技术及协作的问题。 文章转载请注明出处。

优秀的个人博客,低调大师

深入理解JVM:元空间大小详细解析

前言 JVM加载类的时候,需要记录类的元数据,这些数据会保存在一个单独的内存区域内,在Java 7里,这个空间被称为永久代(Permgen),在Java 8里,使用元空间(Metaspace)代替了永久代。永久代和元空间保存的数据并不完全一样,永久代中还保存另一些与类的元数据无关的杂项。 在Java 7里将字符串常量从永久代移动到了堆区域,但是永久代并没有完全改造完成。直到Java 8,永久代的改造才算完全搞定,在元空间中保存的数据比永久代中纯粹很多,就是类的元数据,这些信息只对编译期或JVM的运行时有用。 理论学习 使用Java 8以后,关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N,对于64位JVM来说,元空间的默认初始大小是20.75MB,默认的元空间的最大值是无限。MaxMetaspaceSize用于设置metaspace区域的最大值,这个值可以通过mxbean中的MemoryPoolBean获取到,如果这个参数没有设置,那么就是通过mxbean拿到的最大值是-1,表示无穷大。由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M(PS:读者可以根据自己的实际情况再调整)。欢迎大家关注我的公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。 源码分析 MetaspaceSize表示metaspace首次使用不够而触发FGC的阈值,只对触发起作用,原因是:垃圾搜集器内部是根据变量 _capacity_until_GC来判断metaspace区域是否达到阈值的,初始化代码如下所示: void MetaspaceGC::initialize() { // Set the high-water mark to MaxMetapaceSize during VM initializaton since // we can't do a GC during initialization. _capacity_until_GC = MaxMetaspaceSize; } GC收集器会在发生对metaspace的回收会,会计算新的capacityuntil_GC值,以后发生FGC就跟MetaspaceSize没有关系了。 如果不设置MetaspaceSize,则默认的capacityuntil_GC为20M左右,具体代码如下: 最后 欢迎大家一起交流,喜欢文章记得点个赞哟,感谢支持!

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册