
事故时常有,最近特别多!但每次事故总会有人出来背锅!如果不是自己的锅,解决了对自己是一种成长。如果是自己的锅,恐怕锅大了,就得走人了,哈哈哈。。。 这不,最近又出了一个锅:从周五开始,每天到11点就不停的接到服务器报警,对于一般的报警,我们早已见怪不怪了,然后作了稍微排查(监控工具: CAT),发现是redis问题,没找到原因,然后过了一会自己就好了,所以刚开始也没怎么管他。然后,第二天报警,第三天报警。然后领导火了,然后我们只好说,要不等到周一上班咱们再解决吧! 周一,开发同学还没去找运维同学查问题,运维同学倒先紧张起来了。 原因是,他们从监控(监控工具: granfana, zabbix)上发现,服务器到这个点就会有一个访问量的暴增,真的是暴增哦,从图中可以看出,一个笔直的线就上去了。然后运维同学也给出了具体哪些接口的访问次数,然后给出了对比性的数据,在这个点的接口访问次数比其他时间要多上一倍以上的访问量。 然后开始排查: 1. 是不是代码有问题,会不会因为自己调用自己导致流量激增? 确认最近项目有上线吗?我擦,我还真有一个项目是差不多这个时间上去的,吓死我了,赶紧查看代码是否有漏洞存在,几经排查后,确认没有问题。然后,抛弃该条路。 2. 是不是代码里连接redis后,不释放该连接? 从连接原理上和代码逻辑上,确认代码连接redis都是短链接,本次访问完成后释放该连接。(针对该问题,我还一度怀疑redis的连接可能被默默重用,但最终证明我是错的) 3. 对比之前没有报警时的访问情况和现在的情况? 对比出问题前后访问情况之后,得知:在没有该问题时,也会有流量高峰,但是不是这个点,而且服务器也是正常运行。所以可以肯定,是后面发生了什么,才导致的问题! 4. 会不会是定时任务反复访问自己的服务器,从而导致该流量高峰? 仔细检查任务中心(quartz),以及每台机器上的crontab,确认异常的脚本运行发生。不过,后来排除了该可能,可我们曾一度花了很长时间在排查这个可能性上! 5. 统计每个接口的访问量,对比问题前与问题后? 对于该问题,主要通过统计服务器的访问日志,如apache的access_log, 得到接口地址,当然了,我们都是很多的集群环境,如果要在每台机器进行日志搜索,自然是要累死人的。咱们使用 salt工具,进行一台机器上直接搜索所有机器上的日志文件,进行统计。如: salt 'cc*.cc' cmd.run "cat access_log | awk -F ' ' '{print $9}' | sort | uniq -c" > 1.log # 该处的双引号不一定能用哦 6. 发现可疑接口,怀疑可能被黑客攻击,重点排查? 排查过程中发现某些接口,正常的访问只能是get,但是却发现有post请求,以为是异常请求。于是找了一台测试环境下访问日志,也进行相应的统计: grep -E 'POST /x/cc/public/notice |POST /x/cc/Public/init ' access_log | less 结果,测试环境一样搜索出该情况,由于机制决定,最终确认该情况也为正常访问。 7. 统计每个ip的访问情况,确认是否有黑客攻击行为? 与每个接口访问统计一样,统计ip cat access_log | awk -F ' ' '{print $2}' | sort | uniq -c > 1.log 最后,发现,ip都是无规律分布的,我们假设是被肉机模拟的ip,但是这条路也已经走不通了。 8. 统计每个开放域名的访问情况,以确认是否是某个不安全的域名被扫描或者攻击了? 其实这个工作应该是留在前面进行的,但是我们也是到了后面,实在没了方向,才又折回来的,统计方法和(5)是一样的。 cat access_log | awk -F ' ' '{print $2}' | sort | uniq -c > 1.log 然后,发现我们好几个业务的域名都暴增了,然后又没方向了,因为并不是哪个特定的业务出了问题,而是整体的。 9. 查看业务代码日志,检查是否出现了相应的访问后端接口缓慢或异常的情况? 我随机抽看了下某台机器的日志,发现一切访问都正常,除了几个redis读取的异常外,并无异常值得注意。然后我作出了判定,后端接口没有问题。当然,这最终证明了我是错的,因为正是由于后端服务响应慢,从而导致了前端请求一直挂起,从而redis连接未释放情况,从而导致许多的redis连接! 10. 根据统计中发现,在出现问题时,access_log中,有大量的" OPTION * " 的请求,为什么? 日志如下: 0 127.0.0.1 cc.c.com - - [24/Jun/2017:21:21:47 +0800] "OPTIONS * HTTP/1.0" 200 - "-" "Apache/2.2.3 (CentOS) (internal dummy connection)" 85 202 "-" 该请求达到好几十万的访问,然后我们又去找,为什么会有这种请求,然后努力模拟这种请求,甚至想用线上服务器地址作为请求对象,但最终也没有模拟出这种情况。因为无论怎么请求,都会有一个相对路径地址产生,而且在OPTION成功之后,会默认触发一次GET请求。 最终证明,这只是apache在管理子进程时,对自身进程的监听所产生的access log日志,对不是问题的方向。 11. 所有问题都排查了,仍然不知道这流量是从哪里来的,只能问其他人了? 突然有人想起,产品改过某个流控规则,提示文案为”xx业务在xx点开抢,不要错过“!我靠,这不是秒杀系统了吗?流量不暴增才怪! 12. 终于找到问题了,然后再是拉上架构师,去理论!!! 原来是虚惊一场啊(业务人员一不小心搞的秒杀活动~,流量暴增属正常情况),虽然服务器多次挂掉,但是由于不是自己的锅,悬着的心总算掉下来了。 但是,归根结底,还是我们的系统不够牛逼啊,对于这突发的流量,一下没扛住,当然,在本案中,主要表现为redis没有扛住压力,赶紧强化进来吧!~ 对于推送活动一类的操作,一定要先跟技术运维做好沟通,将所有的流量预估,机器新增,安全因素考虑在内。让系统做好足够的准备,才能安稳地去搞自己的活动,否则任何一个环节都可能导致瓶颈,从而合使服务瘫痪。(应对这种情况,我们只有一种办法,重启服务甚至服务器) 当然,这里有个问题也是我没想太明白的,就是有时候对于调用第三方的东西,你搞活动不去跟别人打招呼(一般情况都不会),那么,你的系统扛住了压力,那么别人的系统呢? 不要害怕今日的苦,你要相信明天,更苦!
资料来源:有架构给我的一些资料,以及自己百度和论坛、社区找来的一些资料,权当做一个总结式的简介。。。 目录如下: 一、微服务架构介绍 二、出现和发展 三、传统开发模式和微服务的区别 四、微服务的具体特征 五、SOA和微服务的区别 六、如何具体实践微服务 七、常见的微服务设计模式和应用 八、微服务的优点和缺点 九、思考:意识的转变 十、参考资料和推荐阅读 一、微服务架构介绍 微服务架构(Microservice Architecture)是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦。你可以将其看作是在架构层次而非获取服务的 类上应用很多SOLID原则。微服务架构是个很有趣的概念,它的主要作用是将功能分解到离散的各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。 概念:把一个大型的单个应用程序和服务拆分为数个甚至数十个的支持微服务,它可扩展单个组件而不是整个的应用程序堆栈,从而满足服务等级协议。 定义:围绕业务领域组件来创建应用,这些应用可独立地进行开发、管理和迭代。在分散的组件中使用云架构和平台式部署、管理和服务功能,使产品交付变得更加简单。 本质:用一些功能比较明确、业务比较精练的服务去解决更大、更实际的问题。 二、出现和发展 微服务(Microservice)这个概念是2012年出现的,作为加快Web和移动应用程序开发进程的一种方法,2014年开始受到各方的关注,而2015年,可以说是微服务的元年; 越来越多的论坛、社区、blog以及互联网行业巨头开始对微服务进行讨论、实践,可以说这样更近一步推动了微服务的发展和创新。而微服务的流行,Martin Fowler功不可没。 这老头是个奇人,特别擅长抽象归纳和制造概念。特别是微服务这种新生的名词,都有一个特点:一解释就懂,一问就不知,一讨论就打架。 Martin Fowler是国际著名的OO专家,敏捷开发方法的创始人之一,现为ThoughtWorks公司的首 席科学家。在面向对象分析设计、UML、模式、软件开发方法学、XP、重构等方面,都是世界顶级的 专家,现为Thought Works公司的首席科学家。Thought Works是一家从事企业应用开发和——集 成的公司。早在20世纪80年代,Fowler就是使用对象技术构建多层企业应用的倡导者,他著有几 本经典书籍: 《企业应用架构模式》、《UML精粹》和《重构》等。 ———— 百度百科 三、传统开发模式和微服务的区别 先来看看传统的web开发方式,通过对比比较容易理解什么是Microservice Architecture。和Microservice相对应的,这种方式一般被称为Monolithic(单体式开发)。 所有的功能打包在一个 WAR包里,基本没有外部依赖(除了容器),部署在一个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。 优点: ①开发简单,集中式管理 ②基本不会重复开发 ③功能都在本地,没有分布式的管理和调用消耗 缺点: 1、效率低:开发都在同一个项目改代码,相互等待,冲突不断 2、维护难:代码功功能耦合在一起,新人不知道何从下手 3、不灵活:构建时间长,任何小修改都要重构整个项目,耗时 4、稳定性差:一个微小的问题,都可能导致整个应用挂掉 5、扩展性不够:无法满足高并发下的业务需求 常见的系统架构遵循的三个标准和业务驱动力: 1、提高敏捷性:及时响应业务需求,促进企业发展 2、提升用户体验:提升用户体验,减少用户流失 3、降低成本:降低增加产品、客户或业务方案的成本 基于微服务架构的设计: 目的:有效的拆分应用,实现敏捷开发和部署 关于微服务的一个形象表达: X轴:运行多个负载均衡器之后的运行实例 Y轴:将应用进一步分解为微服务(分库) Z轴:大数据量时,将服务分区(分表) 四、微服务的具体特征 官方的定义: 1、一些列的独立的服务共同组成系统 2、单独部署,跑在自己的进程中 3、每个服务为独立的业务开发 4、分布式管理 5、非常强调隔离性 大概的标准: 1、分布式服务组成的系统 2、按照业务,而不是技术来划分组织 3、做有生命的产品而不是项目 4、强服务个体和弱通信( Smart endpoints and dumb pipes ) 5、自动化运维( DevOps ) 6、高度容错性 7、快速演化和迭代 五、SOA和微服务的区别 1、SOA喜欢重用,微服务喜欢重写 SOA的主要目的是为了企业各个系统更加容易地融合在一起。 说到SOA不得不说ESB(EnterpriseService Bus)。 ESB是什么? 可以把ESB想象成一个连接所有企业级服务的脚手架。 通过service broker,它可以把不同数据格式或模型转成canonical格式,把XML的输入转成CSV传给legacy服务,把SOAP 1.1服务转成 SOAP 1.2等等。 它还可以把一个服务 路由到另一个服务上,也可以集中化管理业务逻辑,规则和验证等等。 它还有一个重要功能是消息队列和事件驱动的消息传递,比如把JMS服务转化成SOAP协议。 各服务间可能有 复杂的依赖关系。 微服务通常由重写一个模块开始。要把整个巨石型的应用重写是有很大的风险的,也不一定必要。我们向微服务迁移的时候通常从耦合度最低的模块或对扩展性要求最高的模块开始, 把它们一个一个剥离出来用敏捷地重写,可以尝试最新的技术和语言和框架,然 后单独布署。 它通常不依赖其他服务。微服务中常用的API Gateway的模式主要目的也不是重用代码, 而是减少客户端和服务间的往来。API gateway模式不等同与Facade模式,我们可以使用如future之类的调用,甚至返回不完整数据。 2、SOA喜欢水平服务,微服务喜欢垂直服务 SOA设计喜欢给服务分层(如Service Layers模式)。 我们常常见到一个Entity服务层的设计,美其名曰Data Access Layer。 这种设计要求所有的服务都通过这个Entity服务层 来获取数据。 这种设计非常不灵活,比如每次数据层的改动都可能影响到所有业务层的服务。 而每个微服务通常有它自己独立的data store。 我们在拆分数据库时可以适当的做些 去范式化(denormalization),让它不需要依赖其他服务的数据。 微服务通常是直接面对用户的,每个微服务通常直接为用户提供某个功能。 类似的功能可能针对手机有一个服务,针对机顶盒是另外一个服务。 在SOA设计模式中这种情况通常会用到 Multi-ChannelEndpoint的模式返回一个大而全的结果兼顾到所有的客户端的需求。 3、SOA喜欢自上而下,微服务喜欢自下而上 SOA架构在设计开始时会先定义好服务合同(service contract)。 它喜欢集中管理所有的服务,包括集中管理业务逻辑,数据,流程,schema,等等。 它使用Enterprise Inventory和Service Composition等方法来集中管理服务。 SOA架构通常会预先把每个模块服务接口都定义好。 模块系统间的通讯必须遵守这些接口,各服务是针对他们的调用者。 SOA架构适用于TOGAF之类的架构方法论。 微服务则敏捷得多。只要用户用得到,就先把这个服务挖出来。然后针对性的,快速确认业务需求,快速开发迭代。 六、怎么具体实践微服务 要实际的应用微服务,需要解决一下四点问题: 1、客户端如何访问这些服务 2、每个服务之间如何通信 3、如此多的服务,如何实现? 4、服务挂了,如何解决?(备份方案,应急处理机制) 1、客户端如何访问这些服务 原来的Monolithic方式开发,所有的服务都是本地的,UI可以直接调用,现在按功能拆分成独立的服务,跑在独立的一般都在独立的虚拟机上的 Java进程了。客户端UI如何访问他的? 后台有N个服务,前台就需要记住管理N个服务,一个服务下线/更新/升级,前台就要重新部署,这明显不服务我们 拆分的理念,特别当前台是移动应用的时候,通常业务变化的节奏更快。 另外,N个小服务的调用也是一个不小的网络开销。还有一般微服务在系统内部,通常是无 状态的,用户登录信息和权限管理最好有一个统一的地方维护管理(OAuth)。 所以,一般在后台N个服务和UI之间一般会一个代理或者叫API Gateway,他的作用包括: ① 提供统一服务入口,让微服务对前台透明 ② 聚合后台的服务,节省流量,提升性能 ③ 提供安全,过滤,流控等API管理功能 其实这个API Gateway可以有很多广义的实现办法,可以是一个软硬一体的盒子,也可以是一个简单的MVC框架,甚至是一个Node.js的服务端。他们最重要的作 用是为前台(通常是 移动应用)提供后台服务的聚合,提供一个统一的服务出口,解除他们之间的耦合,不过API Gateway也有可能成为单点故障点或者性能的瓶颈。 用过Taobao Open Platform(淘宝开放平台)的就能很容易的体会,TAO就是这个API Gateway。 2、每个服务之间如何通信 所有的微服务都是独立的Java进程跑在独立的虚拟机上,所以服务间的通信就是IPC(inter process communication),已经有很多成熟的方案。现在基本最通用的有两种方式: 同步调用: ①REST(JAX-RS,Spring Boot) ②RPC(Thrift, Dubbo) 异步消息调用(Kafka, Notify, MetaQ) 同步和异步的区别: 一般同步调用比较简单,一致性强,但是容易出调用问题,性能体验上也会差些,特别是调用层次多的时候。RESTful和RPC的比较也是一个很有意 思的话题。 一般REST基于HTTP,更容易实现,更容易被接受,服务端实现技术也更灵活些,各个语言都能支持,同时能跨客户端,对客户端没有特殊的要求,只要封装了HTTP的 SDK就能调用,所以相对使用的广一些。RPC也有自己的优点,传输协议更高效,安全更可控,特别在一个公司内部,如果有统一个 的开发规范和统一的服务框架时, 他的开发效率优势更明显些。就看各自的技术积累实际条件,自己的选择了。 而异步消息的方式在分布式系统中有特别广泛的应用,他既能减低调用服务之间的耦合,又能成为调用之间的缓冲,确保消息积压不会冲垮被调用方,同时能保证调用方的 服务体验,继续干自己该干的活,不至于被后台性能拖慢。不过需要付出的代价是一致性的减弱,需要接受数据最终一致性;还有就是后台服务一般要 实现幂等性,因为消息 发送出于性能的考虑一般会有重复(保证消息的被收到且仅收到一次对性能是很大的考验);最后就是必须引入一个独立的broker,如果公司内部没有技术积累, 对broker分布式管理也是一个很大的挑战。 3、如此多的服务,如何实现? 在微服务架构中,一般每一个服务都是有多个拷贝,来做负载均衡。一个服务随时可能下线,也可能应对临时访问压力增加新的服务节点。服务之间如何相互感知?服务如何管理? 这就是服务发现的问题了。一般有两类做法,也各有优缺点。基本都是通过zookeeper等类似技术做服务注册信息的分布式管理。当服务上线时,服务提供者将自己的服务信息 注册到ZK(或类似框架),并通过心跳维持长链接,实时更新链接信息。服务调用者通过ZK寻址,根据可定制算法, 找到一个服务,还可以将服务信息缓存在本地以提高性能。 当服务下线时,ZK会发通知给服务客户端。 客户端做:优点是架构简单,扩展灵活,只对服务注册器依赖。缺点是客户端要维护所有调用服务的地址,有技术难度,一般大公司都有成熟的内部框架支持,比如Dubbo。 服务端做:优点是简单,所有服务对于前台调用方透明,一般在小公司在云服务上部署的应用采用的比较多。 4、服务挂了,如何解决 前面提到,Monolithic方式开发一个很大的风险是,把所有鸡蛋放在一个篮子里,一荣俱荣,一损俱损。而分布式最大的特性就是网络是不可靠的。通过微服务拆分能降低这个风险, 不过如果没有特别的保障,结局肯定是噩梦。所以当我们的系统是由一系列的服务调用链组成的时候,我们必须确保任一环节出问题都不至于影响整体链路。相应的手段有很多: ①重试机制 ②限流 ③熔断机制 ④负载均衡 ⑤降级(本地缓存) 这些方法基本都很明确通用,比如Netflix的Hystrix:https://github.com/Netflix/Hystrix 七、常见的设计模式和应用 有一个图非常好的总结微服务架构需要考虑的问题,包括: 1、API Gateway 2、服务间调用 3、服务发现 4、服务容错 5、服务部署 6、数据调用 六种常见的微服务架构设计模式: 1、聚合器微服务设计模式 这是一种最常见也最简单的设计模式: 聚合器调用多个服务实现应用程序所需的功能。它可以是一个简单的Web页面,将检索到的数据进行处理展示。它也可以是一个更高层次的组合微服务,对检索到的数据增加业务逻辑后进一步 发布成一个新的微服务,这符合DRY原则。另外,每个服务都有自己的缓存和数据库。如果聚合器是一个组合服务,那么它也有自己的缓存和数据库。聚合器可以沿X轴和Z轴独立扩展。 2、代理微服务设计模式 这是聚合模式的一个变种,如下图所示: 在这种情况下,客户端并不聚合数据,但会根据业务需求的差别调用不同的微服务。代理可以仅仅委派请求,也可以进行数据转换工作。 3、链式微服务设计模式 这种模式在接收到请求后会产生一个经过合并的响应,如下图所示: 在这种情况下,服务A接收到请求后会与服务B进行通信,类似地,服务B会同服务C进行通信。所有服务都使用同步消息传递。在整个链式调用完成之前,客户端会一直阻塞。 因此,服务调用链不宜过长,以免客户端长时间等待。 4、分支微服务设计模式 这种模式是聚合器模式的扩展,允许同时调用两个微服务链,如下图所示: 5、数据共享微服务设计模式 自治是微服务的设计原则之一,就是说微服务是全栈式服务。但在重构现有的“单体应用(monolithic application)”时,SQL数据库反规范化可能会导致数据重复和不一致。 因此,在单体应用到微服务架构的过渡阶段,可以使用这种设计模式,如下图所示: 在这种情况下,部分微服务可能会共享缓存和数据库存储。不过,这只有在两个服务之间存在强耦合关系时才可以。对于基于微服务的新建应用程序而言,这是一种反模式。 6、异步消息传递微服务设计模式 虽然REST设计模式非常流行,但它是同步的,会造成阻塞。因此部分基于微服务的架构可能会选择使用消息队列代替REST请求/响应,如下图所示: 八、优点和缺点 1、微服务的优点: 关键点:复杂度可控,独立按需扩展,技术选型灵活,容错,可用性高 ①它解决了复杂性的问题。它会将一种怪异的整体应用程序分解成一组服务。虽然功能总量 不变,但应用程序已分解为可管理的块或服务。每个服务都以RPC或消息驱动的API的 形式定义了一个明确的边界;Microservice架构模式实现了一个模块化水平。 ②这种架构使每个服务都能够由专注于该服务的团队独立开发。开发人员可以自由选择任何有用的技术,只要该服务符合API合同。当然,大多数组织都希望避免完全无政府状态并 限制技术选择。然而,这种自由意味着开发人员不再有义务使用在新项目开始时存在的可能过时的技术。在编写新服务时,他们可以选择使用当前的技术。此外,由于服务相对较小, 因此使用当前技术重写旧服务变得可行。 ③Microservice架构模式使每个微服务都能独立部署。开发人员不需要协调部署本地服务的变更。这些变化可以在测试后尽快部署。例如,UI团队可以执行A | B测试,并快速迭代 UI更改。Microservice架构模式使连续部署成为可能。 ④Microservice架构模式使每个服务都可以独立调整。您可以仅部署满足其容量和可用性限制的每个服务的实例数。此外,您可以使用最符合服务资源要求的硬件。 2、微服务的缺点 关键点(挑战):多服务运维难度,系统部署依赖,服务间通信成本,数据一致性,系统集成测试,重复工作,性能监控等 ①一个缺点是名称本身。术语microservice过度强调服务规模。但重要的是要记住,这是一种手段,而不是主要目标。微服务的目标是充分分解应用程序,以便于敏捷应用程序开发和部署。 ②微服务器的另一个主要缺点是分布式系统而产生的复杂性。开发人员需要选择和实现基于消息传递或RPC的进程间通信机制。此外,他们还必须编写代码来处理部分故障, 因为请求的目的地可能很慢或不可用。 ③微服务器的另一个挑战是分区数据库架构。更新多个业务实体的业务交易是相当普遍的。但是,在基于微服务器的应用程序中,您需要更新不同服务所拥有的多个数据库。使用分布式事务 通常不是一个选择,而不仅仅是因为CAP定理。许多今天高度可扩展的NoSQL数据库都不支持它们。你最终不得不使用最终的一致性方法,这对开发人员来说更具挑战性。 ④测试微服务应用程序也更复杂。服务类似的测试类将需要启动该服务及其所依赖的任何服务(或至少为这些服务配置存根)。再次,重要的是不要低估这样做的复杂性。 ⑤Microservice架构模式的另一个主要挑战是实现跨越多个服务的更改。例如,我们假设您正在实施一个需要更改服务A,B和C的故事,其中A取决于B和B取决于C.在单片应用程序中, 您可以简单地更改相应的模块,整合更改,并一次性部署。相比之下,在Microservice架构模式中,您需要仔细规划和协调对每个服务的更改。例如,您需要更新服务C,然后更新服务B, 然后再维修A.幸运的是,大多数更改通常仅影响一个服务,而需要协调的多服务变更相对较少。 ⑥部署基于微服务的应用程序也更复杂。单一应用程序简单地部署在传统负载平衡器后面的一组相同的服务器上。每个应用程序实例都配置有基础架构服务(如数据库和消息代理) 的位置(主机和端口)。相比之下,微服务应用通常由大量服务组成。例如,每个服务将有多个运行时实例。更多的移动部件需要进行配置,部署,扩展和监控。此外,您还需要实现服务 发现机制,使服务能够发现需要与之通信的任何其他服务的位置(主机和端口)。传统的基于故障单和手动操作的方法无法扩展到这种复杂程度。因此,成功部署微服务应用程序需要 开发人员更好地控制部署方法,并实现高水平的自动化。 九、思考:意识的转变 微服务对我们的思考,更多的是思维上的转变。对于微服务架构:技术上不是问题,意识比工具重要。 关于微服务的几点设计出发点: 1、应用程序的核心是业务逻辑,按照业务或客户需求组织资源(这是最难的) 2、做有生命的产品,而不是项目 3、头狼战队,全栈化 4、后台服务贯彻Single Responsibility Principle(单一职责原则) 5、VM->Docker (to PE) 6、DevOps (to PE) 同时,对于开发同学,有这么多的中间件和强大的PE支持固然是好事,我们也需要深入去了解这些中间件背后的原理,知其然知其所以然,在有限的技术资源如何通过开源技术实施微服务? 最后,一般提到微服务都离不开DevOps和Docker,理解微服务架构是核心,devops和docker是工具,是手段。 十、参考资料和推荐阅读: http://kb.cnblogs.com/page/520922/ http://www.infoq.com/cn/articles/seven-uservices-antipatterns http://www.csdn.net/article/2015-08-07/2825412 http://blog.csdn.net/mindfloating/article/details/45740573 http://blog.csdn.net/sunhuiliang85/article/details/52976210 http://www.oschina.net/news/70121/microservice Has anything you've done made your life better?
版权声明:本文为博主原创文章,未经博主允许不得转载。 一. 下载并安装Nginx 去Nginx官网下载 我这里选取nginx/Windows-1.10.3版本,下载后解压出来即可,解压出来的路径不能含有中文 我解压后将其放置的路径如下 二、开始运行 在当前目录下按住shift+鼠标右键,选择“在此处打开命令窗口”,然后输入start nginx 此时,就可以进入浏览器输入访问地址,http://127.0.0.1/或者http://localhost/即可访问 三、配置文件讲解 核心配置文件就是nginx.conf,该文件位于conf目录下,大部分情况下我们就是修改该文件的配置 该文件的原始配置如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 #user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} } 其中#代表注释 nginx我们最主要的作用是拿来做反向代理和负载均衡,这个我后面会着重讲解。同时它还是一个web服务器,与我们常用的Apache、tomcat、IIS一样,也可以用来托管web服务。 本章先暂时介绍下该配置文件中的几个重要参数,后面会对nginx部署php和Python项目再进行着重讲解,至于java的项目通常是tomcat+nginx同时进行配置,nginx用来做负载均衡和处理静态页。 1、定义Nginx运行的用户和用户组 1 #user nobody; 2、nginx进程数,建议设置为等于CPU总核心数 1 worker_processes 1; 3、全局错误日志定义类型,[ debug | info | notice | warn | error | crit ] 1 2 #error_log logs/error.log notice; #error_log logs/error.log info; 4、进程文件 1 #pid logs/nginx.pid; 5、工作模式与连接数上限:worker_connections是单个后台worker process进程的最大并发链接数,并发总数是 worker_processes 和 worker_connections 的乘积, 即 max_clients = worker_processes * worker_connections 1 2 3 events { worker_connections 1024; } 6、http下的一些配置及其意义 1 2 3 4 5 6 7 8 include mime.types; #文件扩展名与文件类型映射表 default_type application/octet-stream; #默认文件类型 sendfile on; #开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来 输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置 为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。注意:如果图片显示不正常 把这个改成off。 autoindex on; #开启目录列表访问,合适下载服务器,默认关闭。 tcp_nopush on; #防止网络阻塞 tcp_nodelay on; #防止网络阻塞 keepalive_timeout 120; #长连接超时时间,单位是秒 gzip on; #开启gzip压缩输出 7、server虚拟主机的相关配置 我们平时配置各类服务器,配置最多的就是这些地方了 比如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 http{ #虚拟主机1 server{ listen 80; #监听端口,基于IP配置的时候变更此处,比如192.168.1.100:8080; server_name www.xdw.com; #主机域名,实际项目发布的话,填公网上的域名,本地部署的话,可以在C:\Windows\System32\drivers\etc\hosts文件中添加IP和域名的映射 location / { #映射解析,/代表根路径,此处解析还有正则表达式的解析方式,具体请参考http://tengine.taobao.org/nginx_docs/cn/docs/http/ngx_http_core_module.html#location root E:/xdw/0221; #工程所在路径 index index.html index.htm; #首页(默认页) } } #虚拟主机2,可以同时配置多个虚拟主机 server{ listen 8080; server_name localhost; location / { root D:/xiangmu/txym_web; index index.html index.htm; } } } 看到这个虚拟主机的配置,相信配置过tomcat或者Apache的人都很熟悉的感觉。此篇就到此结束,下面还会更新linux下的配置,php和python项目的部署,反向代理和负载均衡,配合tomcat部署java项目。
一、前言 消息队列中间件(简称消息中间件)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦、弹性伸缩、冗余存储、流量削峰、异步通信、数据同步等等功能,其作为分布式系统架构中的一个重要组件,有着举足轻重的地位。 目前开源的消息中间件可谓是琳琅满目,能让大家耳熟能详的就有很多,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ、ZeroMQ等。不管选择其中的哪一款,都会有用的不趁手的地方,毕竟不是为你量身定制的。有些大厂在长期的使用过程中积累了一定的经验,其消息队列的使用场景也相对稳定固化,或者目前市面上的消息中间件无法满足自身需求,并且也具备足够的精力和人力而选择自研来为自己量身打造一款消息中间件。但是绝大多数公司还是不会选择重复造轮子,那么选择一款合适自己的消息中间件显得尤为重要。就算是前者,那么在自研出稳定且可靠的相关产品之前还是会经历这样一个选型过程。 在整体架构中引入消息中间件,势必要考虑很多因素,比如成本及收益问题,怎么样才能达到最优的性价比?虽然消息中间件种类繁多,但是各自都有各自的侧重点,选择合适自己、扬长避短无疑是最好的方式。如果你对此感到无所适从,本文或许可以参考一二。 二、各类消息队列简述 ActiveMQ是Apache出品的、采用Java语言编写的完全基于JMS1.1规范的面向消息的中间件,为应用程序提供高效的、可扩展的、稳定的和安全的企业级消息通信。不过由于历史原因包袱太重,目前市场份额没有后面三种消息中间件多,其最新架构被命名为Apollo,号称下一代ActiveMQ,有兴趣的同学可行了解。 RabbitMQ是采用Erlang语言实现的AMQP协议的消息中间件,最初起源于金融系统,用于在分布式系统中存储转发消息。RabbitMQ发展到今天,被越来越多的人认可,这和它在可靠性、可用性、扩展性、功能丰富等方面的卓越表现是分不开的。 Kafka起初是由LinkedIn公司采用Scala语言开发的一个分布式、多分区、多副本且基于zookeeper协调的分布式消息系统,现已捐献给Apache基金会。它是一种高吞吐量的分布式发布订阅消息系统,以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera、Apache Storm、Spark、Flink等都支持与Kafka集成。 RocketMQ是阿里开源的消息中间件,目前已经捐献个Apache基金会,它是由Java语言开发的,具备高吞吐量、高可用性、适合大规模分布式系统应用等特点,经历过双11的洗礼,实力不容小觑。 ZeroMQ号称史上最快的消息队列,基于C语言开发。ZeroMQ是一个消息处理队列库,可在多线程、多内核和主机之间弹性伸缩,虽然大多数时候我们习惯将其归入消息队列家族之中,但是其和前面的几款有着本质的区别,ZeroMQ本身就不是一个消息队列服务器,更像是一组底层网络通讯库,对原有的Socket API上加上一层封装而已。 目前市面上的消息中间件还有很多,比如腾讯系的PhxQueue、CMQ、CKafka,又比如基于Go语言的NSQ,有时人们也把类似Redis的产品也看做消息中间件的一种,当然它们都很优秀,但是本文篇幅限制无法穷极所有,下面会针对性的挑选RabbitMQ和Kafka两款典型的消息中间件来做分析,力求站在一个公平公正的立场来阐述消息中间件选型中的各个要点。 三、选型要点概述 衡量一款消息中间件是否符合需求需要从多个维度进行考察,首要的就是功能维度,这个直接决定了你能否最大程度上的实现开箱即用,进而缩短项目周期、降低成本等。如果一款消息中间件的功能达不到想要的功能,那么就需要进行二次开发,这样会增加项目的技术难度、复杂度以及增大项目周期等。 1. 功能维度 功能维度又可以划分个多个子维度,大致可以分为以下这些: 优先级队列 优先级队列不同于先进先出队列,优先级高的消息具备优先被消费的特权,这样可以为下游提供不同消息级别的保证。不过这个优先级也是需要有一个前提的:如果消费者的消费速度大于生产者的速度,并且消息中间件服务器(一般简单的称之为Broker)中没有消息堆积,那么对于发送的消息设置优先级也就没有什么实质性的意义了,因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker中至多只有一条消息,对于单条消息来说优先级是没有什么意义的。 延迟队列 当你在网上购物的时候是否会遇到这样的提示:“三十分钟之内未付款,订单自动取消”?这个是延迟队列的一种典型应用场景。延迟队列存储的是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。延迟队列一般分为两种:基于消息的延迟和基于队列的延迟。基于消息的延迟是指为每条消息设置不同的延迟时间,那么每当队列中有新消息进入的时候就会重新根据延迟时间排序,当然这也会对性能造成极大的影响。实际应用中大多采用基于队列的延迟,设置不同延迟级别的队列,比如5s、10s、30s、1min、5mins、10mins等,每个队列中消息的延迟时间都是相同的,这样免去了延迟排序所要承受的性能之苦,通过一定的扫描策略(比如定时)即可投递超时的消息。 死信队列 由于某些原因消息无法被正确的投递,为了确保消息不会被无故的丢弃,一般将其置于一个特殊角色的队列,这个队列一般称之为死信队列。与此对应的还有一个“回退队列”的概念,试想如果消费者在消费时发生了异常,那么就不会对这一次消费进行确认(Ack),进而发生回滚消息的操作之后消息始终会放在队列的顶部,然后不断被处理和回滚,导致队列陷入死循环。为了解决这个问题,可以为每个队列设置一个回退队列,它和死信队列都是为异常的处理提供的一种机制保障。实际情况下,回退队列的角色可以由死信队列和重试队列来扮演。 重试队列 重试队列其实可以看成是一种回退队列,具体指消费端消费消息失败时,为防止消息无故丢失而重新将消息回滚到Broker中。与回退队列不同的是重试队列一般分成多个重试等级,每个重试等级一般也会设置重新投递延时,重试次数越多投递延时就越大。举个例子:消息第一次消费失败入重试队列Q1,Q1的重新投递延迟为5s,在5s过后重新投递该消息;如果消息再次消费失败则入重试队列Q2,Q2的重新投递延迟为10s,在10s过后再次投递该消息。以此类推,重试越多次重新投递的时间就越久,为此需要设置一个上限,超过投递次数就入死信队列。重试队列与延迟队列有相同的地方,都是需要设置延迟级别,它们彼此的区别是:延迟队列动作由内部触发,重试队列动作由外部消费端触发;延迟队列作用一次,而重试队列的作用范围会向后传递。 消费模式 消费模式分为推(push)模式和拉(pull)模式。推模式是指由Broker主动推送消息至消费端,实时性较好,不过需要一定的流制机制来确保服务端推送过来的消息不会压垮消费端。而拉模式是指消费端主动向Broker端请求拉取(一般是定时或者定量)消息,实时性较推模式差,但是可以根据自身的处理能力而控制拉取的消息量。 广播消费 消息一般有两种传递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。对于点对点的模式而言,消息被消费以后,队列中不会再存储,所以消息消费者不可能消费到已经被消费的消息。虽然队列可以支持多个消费者,但是一条消息只会被一个消费者消费。发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用。RabbitMQ是一种典型的点对点模式,而Kafka是一种典型的发布订阅模式。但是RabbitMQ中可以通过设置交换器类型来实现发布订阅模式而达到广播消费的效果,Kafka中也能以点对点的形式消费,你完全可以把其消费组(consumer group)的概念看成是队列的概念。不过对比来说,Kafka中因为有了消息回溯功能的存在,对于广播消费的力度支持比RabbitMQ的要强。 消息回溯 一般消息在消费完成之后就被处理了,之后再也不能消费到该条消息。消息回溯正好相反,是指消息在消费完成之后,还能消费到之前被消费掉的消息。对于消息而言,经常面临的问题是“消息丢失”,至于是真正由于消息中间件的缺陷丢失还是由于使用方的误用而丢失一般很难追查,如果消息中间件本身具备消息回溯功能的话,可以通过回溯消费复现“丢失的”消息进而查出问题的源头之所在。消息回溯的作用远不止与此,比如还有索引恢复、本地缓存重建,有些业务补偿方案也可以采用回溯的方式来实现。 消息堆积+持久化 流量削峰是消息中间件的一个非常重要的功能,而这个功能其实得益于其消息堆积能力。从某种意义上来讲,如果一个消息中间件不具备消息堆积的能力,那么就不能把它看做是一个合格的消息中间件。消息堆积分内存式堆积和磁盘式堆积。RabbitMQ是典型的内存式堆积,但这并非绝对,在某些条件触发后会有换页动作来将内存中的消息换页到磁盘(换页动作会影响吞吐),或者直接使用惰性队列来将消息直接持久化至磁盘中。Kafka是一种典型的磁盘式堆积,所有的消息都存储在磁盘中。一般来说,磁盘的容量会比内存的容量要大得多,对于磁盘式的堆积其堆积能力就是整个磁盘的大小。从另外一个角度讲,消息堆积也为消息中间件提供了冗余存储的功能。援引纽约时报的案例( https://www.confluent.io/blog/publishing-apache-kafka-new-york-times/),其直接将Kafka用作存储系统。 消息追踪 对于分布式架构系统中的链路追踪(trace)而言,大家一定不会陌生。对于消息中间件而言,消息的链路追踪(以下简称消息追踪)同样重要。对于消息追踪最通俗的理解就是要知道消息从哪来,存在哪里以及发往哪里去。基于此功能下,我们可以对发送或者消费完的消息进行链路追踪服务,进而可以进行问题的快速定位与排查。 消息过滤 消息过滤是指按照既定的过滤规则为下游用户提供指定类别的消息。就以kafka而言,完全可以将不同类别的消息发送至不同的topic中,由此可以实现某种意义的消息过滤,或者Kafka还可以根据分区对同一个topic中的消息进行分类。不过更加严格意义上的消息过滤应该是对既定的消息采取一定的方式按照一定的过滤规则进行过滤。同样以Kafka为例,可以通过客户端提供的ConsumerInterceptor接口或者Kafka Stream的filter功能进行消息过滤。 多租户 也可以称为多重租赁技术,是一种软件架构技术,主要用来实现多用户的环境下公用相同的系统或程序组件,并且仍可以确保各用户间数据的隔离性。RabbitMQ就能够支持多租户技术,每一个租户表示为一个vhost,其本质上是一个独立的小型RabbitMQ服务器,又有自己独立的队列、交换器及绑定关系等,并且它拥有自己独立的权限。vhost就像是物理机中的虚拟机一样,它们在各个实例间提供逻辑上的分离,为不同程序安全保密地允许数据,它既能将同一个RabbitMQ中的众多客户区分开,又可以避免队列和交换器等命名冲突。 多协议支持 消息是信息的载体,为了让生产者和消费者都能理解所承载的信息(生产者需要知道如何构造消息,消费者需要知道如何解析消息),它们就需要按照一种统一的格式描述消息,这种统一的格式称之为消息协议。有效的消息一定具有某种格式,而没有格式的消息是没有意义的。一般消息层面的协议有AMQP、MQTT、STOMP、XMPP等(消息领域中的JMS更多的是一个规范而不是一个协议),支持的协议越多其应用范围就会越广,通用性越强,比如RabbitMQ能够支持MQTT协议就让其在物联网应用中获得一席之地。还有的消息中间件是基于其本身的私有协议运转的,典型的如Kafka。 跨语言支持 对很多公司而言,其技术栈体系中会有多种编程语言,如C/C++、JAVA、Go、PHP等,消息中间件本身具备应用解耦的特性,如果能够进一步的支持多客户端语言,那么就可以将此特性的效能扩大。跨语言的支持力度也可以从侧面反映出一个消息中间件的流行程度。 流量控制 流量控制(flow control)针对的是发送方和接收方速度不匹配的问题,提供一种速度匹配服务抑制发送速率使接收方应用程序的读取速率与之相适应。通常的流控方法有Stop-and-wait、滑动窗口以及令牌桶等。 消息顺序性 顾名思义,消息顺序性是指保证消息有序。这个功能有个很常见的应用场景就是CDC(Change Data Chapture),以MySQL为例,如果其传输的binlog的顺序出错,比如原本是先对一条数据加1,然后再乘以2,发送错序之后就变成了先乘以2后加1了,造成了数据不一致。 安全机制 在Kafka 0.9版本之后就开始增加了身份认证和权限控制两种安全机制。身份认证是指客户端与服务端连接进行身份认证,包括客户端与Broker之间、Broker与Broker之间、Broker与ZooKeeper之间的连接认证,目前支持SSL、SASL等认证机制。权限控制是指对客户端的读写操作进行权限控制,包括对消息或Kafka集群操作权限控制。权限控制是可插拔的,并支持与外部的授权服务进行集成。对于RabbitMQ而言,其同样提供身份认证(TLS/SSL、SASL)和权限控制(读写操作)的安全机制。 消息幂等性 对于确保消息在生产者和消费者之间进行传输而言一般有三种传输保障(delivery guarantee):At most once,至多一次,消息可能丢失,但绝不会重复传输;At least once,至少一次,消息绝不会丢,但是可能会重复;Exactly once,精确一次,每条消息肯定会被传输一次且仅一次。对于大多数消息中间件而言,一般只提供At most once和At least once两种传输保障,对于第三种一般很难做到,由此消息幂等性也很难保证。 Kafka自0.11版本开始引入了幂等性和事务,Kafka的幂等性是指单个生产者对于单分区单会话的幂等,而事务可以保证原子性地写入到多个分区,即写入到多个分区的消息要么全部成功,要么全部回滚,这两个功能加起来可以让Kafka具备EOS(Exactly Once Semantic)的能力。 不过如果要考虑全局的幂等,还需要与从上下游方面综合考虑,即关联业务层面,幂等处理本身也是业务层面所需要考虑的重要议题。以下游消费者层面为例,有可能消费者消费完一条消息之后没有来得及确认消息就发生异常,等到恢复之后又得重新消费原来消费过的那条消息,那么这种类型的消息幂等是无法有消息中间件层面来保证的。如果要保证全局的幂等,需要引入更多的外部资源来保证,比如以订单号作为唯一性标识,并且在下游设置一个去重表。 事务性消息 事务本身是一个并不陌生的词汇,事务是由事务开始(Begin Transaction)和事务结束(End Transaction)之间执行的全体操作组成。支持事务的消息中间件并不在少数,Kafka和RabbitMQ都支持,不过此两者的事务是指生产者发生消息的事务,要么发送成功,要么发送失败。消息中间件可以作为用来实现分布式事务的一种手段,但其本身并不提供全局分布式事务的功能。 下表是对Kafka与RabbitMQ功能的总结性对比及补充说明。 功能项 Kafka**(1.1.0版本)** RabbitMQ**(3.6.10版本)** 优先级队列 不支持 支持。建议优先级大小设置在0-10之间。 延迟队列 不支持 支持 死信队列 不支持 支持 重试队列 不支持 不支持。RabbitMQ中可以参考延迟队列实现一个重试队列,二次封装比较简单。如果要在Kafka中实现重试队列,首先得实现延迟队列的功能,相对比较复杂。 消费模式 推模式 推模式+拉模式 广播消费 支持。Kafka对于广播消费的支持相对而言更加正统。 支持,但力度较Kafka弱。 消息回溯 支持。Kafka支持按照offset和timestamp两种维度进行消息回溯。 不支持。RabbitMQ中消息一旦被确认消费就会被标记删除。 消息堆积 支持 支持。一般情况下,内存堆积达到特定阈值时会影响其性能,但这不是绝对的。如果考虑到吞吐这因素,Kafka的堆积效率比RabbitMQ总体上要高很多。 持久化 支持 支持 消息追踪 不支持。消息追踪可以通过外部系统来支持,但是支持粒度没有内置的细腻。 支持。RabbitMQ中可以采用Firehose或者rabbitmq_tracing插件实现。不过开启rabbitmq_tracing插件件会大幅影响性能,不建议生产环境开启,反倒是可以使用Firehose与外部链路系统结合提供高细腻度的消息追踪支持。 消息过滤 客户端级别的支持 不支持。但是二次封装一下也非常简单。 多租户 不支持 支持 多协议支持 只支持定义协议,目前几个主流版本间存在兼容性问题。 RabbitMQ本身就是AMQP协议的实现,同时支持MQTT、STOMP等协议。 跨语言支持 采用Scala和Java编写,支持多种语言的客户端。 采用Erlang编写,支持多种语言的客户端。 流量控制 支持client和user级别,通过主动设置可将流控作用于生产者或消费者。 RabbitMQ的流控基于Credit-Based算法,是内部被动触发的保护机制,作用于生产者层面。 消息顺序性 支持单分区(partition)级别的顺序性。 顺序性的条件比较苛刻,需要单线程发送、单线程消费并且不采用延迟队列、优先级队列等一些高级功能,从某种意义上来说不算支持顺序性。 安全机制 (TLS/SSL、SASL)身份认证和(读写)权限控制 与Kafka相似 幂等性 支持单个生产者单分区单会话的幂等性。 不支持 事务性消息 支持 支持 2. 性能 功能维度是消息中间件选型中的一个重要的参考维度,但这并不是唯一的维度。有时候性能比功能还要重要,况且性能和功能很多时候是相悖的,鱼和熊掌不可兼得,Kafka在开启幂等、事务功能的时候会使其性能降低,RabbitMQ在开启rabbitmq_tracing插件的时候也会极大的影响其性能。消息中间件的性能一般是指其吞吐量,虽然从功能维度上来说,RabbitMQ的优势要大于Kafka,但是Kafka的吞吐量要比RabbitMQ高出1至2个数量级,一般RabbitMQ的单机QPS在万级别之内,而Kafka的单机QPS可以维持在十万级别,甚至可以达到百万级。 消息中间件的吞吐量始终会受到硬件层面的限制。就以网卡带宽为例,如果单机单网卡的带宽为1Gbps,如果要达到百万级的吞吐,那么消息体大小不得超过(1Gb/8)/100W,即约等于134B,换句话说如果消息体大小超过134B,那么就不可能达到百万级别的吞吐。这种计算方式同样可以适用于内存和磁盘。 时延作为性能维度的一个重要指标,却往往在消息中间件领域所被忽视,因为一般使用消息中间件的场景对时效性的要求并不是很高,如果要求时效性完全可以采用RPC的方式实现。消息中间件具备消息堆积的能力,消息堆积越大也就意味着端到端的时延也就越长,与此同时延时队列也是某些消息中间件的一大特色。那么为什么还要关注消息中间件的时延问题呢?消息中间件能够解耦系统,对于一个时延较低的消息中间件而言,它可以让上游生产者发送消息之后可以迅速的返回,也可以让消费者更加快速的获取到消息,在没有堆积的情况下可以让整体上下游的应用之间的级联动作更加高效,虽然不建议在时效性很高的场景下使用消息中间件,但是如果所使用的消息中间件的时延方面比较优秀,那么对于整体系统的性能将会是一个不小的提升。 3. 可靠性+可用性 消息丢失是使用消息中间件时所不得不面对的一个同点,其背后消息可靠性也是衡量消息中间件好坏的一个关键因素。尤其是在金融支付领域,消息可靠性尤为重要。然而说到可靠性必然要说到可用性,注意这两者之间的区别,消息中间件的可靠性是指对消息不丢失的保障程度;而消息中间件的可用性是指无故障运行的时间百分比,通常用几个9来衡量。 从狭义的角度来说,分布式系统架构是一致性协议理论的应用实现,对于消息可靠性和可用性而言也可以追溯到消息中间件背后的一致性协议。对于Kafka而言,其采用的是类似PacificA的一致性协议,通过ISR(In-Sync-Replica)来保证多副本之间的同步,并且支持强一致性语义(通过acks实现)。对应的RabbitMQ是通过镜像环形队列实现多副本及强一致性语义的。多副本可以保证在master节点宕机异常之后可以提升slave作为新的master而继续提供服务来保障可用性。Kafka设计之初是为日志处理而生,给人们留下了数据可靠性要求不要的不良印象,但是随着版本的升级优化,其可靠性得到极大的增强,详细可以参考KIP101。就目前而言,在金融支付领域使用RabbitMQ居多,而在日志处理、大数据等方面Kafka使用居多,随着RabbitMQ性能的不断提升和Kafka可靠性的进一步增强,相信彼此都能在以前不擅长的领域分得一杯羹。 同步刷盘是增强一个组件可靠性的有效方式,消息中间件也不例外,Kafka和RabbitMQ都可以支持同步刷盘,但是笔者对同步刷盘有一定的疑问:绝大多数情景下,一个组件的可靠性不应该由同步刷盘这种极其损耗性能的操作来保障,而是采用多副本的机制来保证。 这里还要提及的一个方面是扩展能力,这里我狭隘地将此归纳到可用性这一维度,消息中间件的扩展能力能够增强其用可用能力及范围,比如前面提到的RabbitMQ支持多种消息协议,这个就是基于其插件化的扩展实现。还有从集群部署上来讲,归功于Kafka的水平扩展能力,其基本上可以达到线性容量提升的水平,在LinkedIn实践介绍中就提及了有部署超过千台设备的Kafka集群。 5. 运维管理 在消息中间件的使用过程中难免会出现各式各样的异常情况,有客户端的,也有服务端的,那么怎样及时有效的进行监测及修复。业务线流量有峰值又低谷,尤其是电商领域,那么怎样前进行有效的容量评估,尤其是大促期间?脚踢电源、网线被挖等事件层出不穷,如何有效的做好异地多活?这些都离不开消息中间件的衍生产品——运维管理。 运维管理也可以进行进一步的细分,比如:申请、审核、监控、告警、管理、容灾、部署等。 申请、审核很好理解,在源头对资源进行管控,既可以进行有效校正应用方的使用规范,配和监控也可以做好流量统计与流量评估工作,一般申请、审核与公司内部系统交融性较大,不适合使用开源类的产品。 监控、告警也比较好理解,对消息中间件的使用进行全方位的监控,即可以为系统提供基准数据,也可以在检测到异常的情况配合告警,以便运维、开发人员的迅速介入。除了一般的监控项(比如硬件、GC等)之外,对于消息中间件还需要关注端到端时延、消息审计、消息堆积等方面。对于RabbitMQ而言,最正统的监控管理工具莫过于rabbitmq_management插件了,但是社区内还有AppDynamics, Collectd, DataDog, Ganglia, Munin, Nagios, New Relic, Prometheus, Zenoss等多种优秀的产品。Kafka在此方面也毫不逊色,比如:Kafka Manager, Kafka Monitor, Kafka Offset Monitor, Burrow, Chaperone, Confluent Control Center等产品,尤其是Cruise还可以提供自动化运维的功能。 不管是扩容、降级、版本升级、集群节点部署、还是故障处理都离不开管理工具的应用,一个配套完备的管理工具集可以在遇到变更时做到事半功倍。故障可大可小,一般是一些应用异常,也可以是机器掉电、网络异常、磁盘损坏等单机故障,这些故障单机房内的多副本足以应付。如果是机房故障就要涉及异地容灾了,关键点在于如何有效的进行数据复制,对于Kafka而言,可以参考MirrorMarker、uReplicator等产品,而RabbitMQ可以参考Federation和Shovel。 6. 社区力度及生态发展 对于目前流行的编程语言而言,如Java、Python,如果你在使用过程中遇到了一些异常,基本上可以通过搜索引擎的帮助来得到解决,因为一个产品用的人越多,踩过的坑也就越多,对应的解决方案也就越多。对于消息中间件也同样适用,如果你选择了一种“生僻”的消息中间件,可能在某些方面运用的得心应手,但是版本更新缓慢、遇到棘手问题也难以得到社区的支持而越陷越深;相反如果你选择了一种“流行”的消息中间件,其更新力度大,不仅可以迅速的弥补之前的不足,而且也能顺应技术的快速发展来变更一些新的功能,这样可以让你以“站在巨人的肩膀上”。在运维管理维度我们提及了Kafka和RabbitMQ都有一系列开源的监控管理产品,这些正是得益于其社区及生态的迅猛发展。 四、消息中间件选型误区探讨 在进行消息中间件选型之前可以先问自己一个问题:是否真的需要一个消息中间件?在搞清楚这个问题之后,还可以继续问自己一个问题:是否需要自己维护一套消息中间件?很多初创型公司为了节省成本会选择直接购买消息中间件有关的云服务,自己只需要关注收发消息即可,其余的都可以外包出去。 很多人面对消息中间件时会有一种自研的冲动,你完全可以对Java中的ArrayBlockingQueue做一个简单的封装,你也可以基于文件、数据库、Redis等底层存储封装而形成一个消息中间件。消息中间件做为一个基础组件并没有想象中的那么简单,其背后还需要配套的管理运维整个生态的产品集。自研还有会交接问题,如果文档不齐全、运作不规范将会带给新人噩梦般的体验。是否真的有自研的必要?如果不是KPI的压迫可以先考虑下这2个问题:1. 目前市面上的消息中间件是否都真的无法满足目前业务需求? 2. 团队是否有足够的能力、人力、财力、精力来支持自研? 很多人在做消息中间件选型时会参考网络上的很多对比类的文章,但是其专业性、严谨性、以及其政治立场问题都有待考证,需要带着怀疑的态度去审视这些文章。比如有些文章会在没有任何限定条件及场景的情况下直接定义某款消息中间件最好,还有些文章没有指明消息中间件版本及测试环境就来做功能和性能对比分析,诸如此类的文章都可以唾弃之。 消息中间件犹如小马过河,选择合适的才最重要,这需要贴合自身的业务需求,技术服务于业务,大体上可以根据上一节所提及的功能、性能等6个维度来一一进行筛选。更深层次的抉择在于你能否掌握其魂,笔者鄙见:RabbitMQ在于routing,而Kafka在于streaming,了解其根本对于自己能够对症下药选择到合适的消息中间件尤为重要。 消息中间件选型切忌一味的追求性能或者功能,性能可以优化,功能可以二次开发。如果要在功能和性能方面做一个抉择的话,那么首选性能,因为总体上来说性能优化的空间没有功能扩展的空间大。然而对于长期发展而言,生态又比性能以及功能都要重要。 很多时候,对于可靠性方面也容易存在一个误区:想要找到一个产品来保证消息的绝对可靠,很不幸的是这世界上没有绝对的东西,只能说尽量趋于完美。想要尽可能的保障消息的可靠性也并非单单只靠消息中间件本身,还要依赖于上下游,需要从生产端、服务端和消费端这3个维度去努力保证,《 RabbitMQ消息可靠性分析》这篇文章就从这3个维度去分析了RabbitMQ的可靠性。 消息中间件选型还有一个考量标准就是尽量贴合团队自身的技术栈体系,虽然说没有蹩脚的消息中间件只有蹩脚的程序员,但是让一个C栈的团队去深挖PhxQueue总比去深挖Scala编写的Kafka要容易的多。 五、总结 消息中间件大道至简:一发一存一消费,没有最好的消息中间件,只有最合适的消息中间件。
一、从单机到分布式: 二、分布式常见问题: 三、ACID事务的四大特性: 原子性:一次执行过程中,要么都成功,要么都失败 一致性:从一个一致性状态到另一个一致性状态 隔离性:事务之间互不干扰 持久性:一旦事务成功结束,它所做的操作会永久保存下来 四、CAP理论 一致性(Consistency): 在分布式环境中,数据在多个节点之间是否能保持一致性 可用性(Availabilty): 对于每个请求总是在有限时间内返回结果 分区容错性(Partition tolerance): 分布式系统当某个节点或部分节点故障了,仍可以对外提供满足一致性和可用性的服务,除非节点全部故障 分布式系统中,CAP无法同时满足,最多只能满足其中两项 满足谁 放弃谁 造成的 影响 AC P 放弃了系统的扩展性,所有数据放在一个节点 CP A 当遇到系统故障,受到影响的服务器需要等待一定的时间, 在等待期间,系统无法对外提供服务 AP C 放弃强一致性,但承诺最终一致性 架构师的精力往往就花在根据业务场景在A和C之间寻求平衡 五、BASE理论 基本可用(Basically Available):在分布式系统出现故障时,允许损失部分可用性(服务降级、页面降级) 软状态(Soft state):允许出现中间状态,且不影响系统可用性(主从延时) 最终一致性(Eventually consistent):经过一段时间达到最终一致性(主从复制) ...待续 版权声明:本文版权归作者潇邦和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
本文介绍一个笔者在实际工作中的实施的基于ActiveMQ的一个高稳定,可扩展的异步消息系统。 ActiveMQ是一个成熟的基于Java语言的开源消息系统,在实际应用中被大量使用。ActiveMQ在系统稳定性,系统的容错和扩展等方面都有很多成熟的方案,也有很多开源的管理工具,是部署异步消息系统的一个很好的选择。 ActiveMQ工作机制 ActiveMQ有两种消息使用方式: l Queue模式:Producer发出到Queue里的消息,只能由一个Consumer来使用。 l Topic模式:Producer发送到Topic里的消息,会传送到Subscribe这个Topic的每一个Consumer。 Producer发出的消息有两种Delivery模式。 l Persistent:Broker需要保存消息,然后把消息发送到Consumer。如果Broker崩溃后,重新启动后保存的消息可以重新发送给Consumer。 l NonPersistent:Broker不需要保存消息,直接把消息发送到Consumer。 ActiveMQ可以通过Networks of Brokers方式将多个Broker组成一个Cluster。Producer和Consumer可以任意的连接到该Cluster中的任意一个Broker。Producer发送的消息可以通过Cluser传送到需要的Consumer。 ActiveMQ提供了Master Slave机制实现Broker的HA,有以下几种方式: l JDBC Master Slave l Shared File System Master Slave l KahaDB Replication(ZooKeeper experimental) 同一个Broker,只能有一个Master来传送消息。当Master崩溃后,其他的一个Slave可以作为Master。采用HA的模式,会增加系统的复杂性,也会影响系统的性能。 方案 实际部署中,ActiveMQ采用Queue的消息使用模式。Producer发送的消息使用Persistent的Delivery模式。 在两个node上部署ActiveMQ的Broker,通过ActiveMQ的Networks of Brokers方式来组成Cluster。 系统里的消息应用Instance通过ActiveMQ提供的client类库采用failover TCP的方式随机的接入到ActiveMQ的cluster中。正常情况下,消息应用Instance可以通过ActiveMQ的cluster机制正常通信。如果某个ActiveMQ的node崩溃后,client会自动检测到该情况,切换到另一个ActiveMQ的node。 由于本系统只采用Queue的消息工作方式,而且消息的传送采用persistent的模式。如果一个node崩溃后,重新启动后,保存的消息还可以重新发送到Consumer。对Broker,就不采用Master/Slave的HA模式,避免增加系统的复杂性和降低系统的性能。 配置 ActiveMQ的Broker的配置如下。 #Broker 1: <!-- The transport connectors ActiveMQ will listen to --> <transportConnectors> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616"/> </transportConnectors> <!-- The store and forward broker networks ActiveMQ will listen to. We'll leave it empty as duplex network will be configured by another broker. --> <networkConnectors> </networkConnectors> #Broker 2: <!-- The transport connectors ActiveMQ will listen to --> <transportConnectors> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616"/> </transportConnectors> <!-- The store and forward broker networks ActiveMQ will listen to Create a duplex connector to the first broker --> <networkConnectors> <networkConnector uri="static:(tcp://{Broker1Ip}:61616)" duplex="true"/> </networkConnectors>
先说下自己开发的实例。 最近在使用 Spring Cloud Config 做分布式配置中心(基于 SVN/Git),当所有服务启动后,SVN/Git 中的配置文件更改后,客户端服务读取的还是旧的配置,并不能实时读取(配置信息会缓存在客户端),Spring Boot 提供了一种方式进行更新(通过spring-boot-starter-actuator监控模块),然后 Post 访问客户端服务的/refresh接口(也可以命令执行curl -X POST http://worker2:8115/refresh),这样客户端会重新从配置中心获取新的配置信息,请求命令可以写在 Git 的 Webhooks 脚本中(修改提交 Push 后执行)。 如果客户端服务比较少的话,这样的解决方式没问题,如果客户端服务多的话,执行的请求脚本就会非常多,而且单个服务的解决方式,也不利于后期的维护(点对点的方式),那该怎么解决上面的问题呢?答案就是通过 Spring Cloud Bus。 Spring Cloud Bus 翻译为消息总线,使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都能连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线。在总线上的各个实例都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息,例如配置信息的变更或者其他一些管理操作等。 架构示意图(引用来源): 下面,我们需要利用 Spring Cloud Bus 来改造 Spring Cloud Config 的服务端和客户端,其实非常简单。 添加下面的依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> 然后在bootstrap.yml中添加下面配置: spring: rabbitmq: host: manager1 port: 5672 username: admin password: admin123 management: security: enabled: false 上面的配置信息都是新增的,并且都需要配置在服务端和客户端,通过上面的示例图可以看到,配置信息更新后请求的是服务端,那么客户端我们就不需要配置management.security.enabled(也不需要配置spring-boot-starter-actuator监控模块)。 服务端和客户端的任何 Java 代码都不需要编写,重新启动服务,当配置信息更新后,通过 Git 的 Webhooks 执行请求脚本:curl -X POST http://manager1:port/bus/refresh,服务端接受到请求之后,会通过 Spring Cloud Bus 通知所有的客户端(通过 RabbitMQ),重新从配置中心获取配置信息,达到实时更新配置的目的。 上面的实例描述就到这里。 RabbitMQ 的基本概念 RabbitMQ,是一个使用 erlang 编写的 AMQP(高级消息队列协议)的服务实现,简单来说,就是一个功能强大的消息队列服务。 RabbitMQ 最基本模型: RabbitMQ 的基本概念: Producer:消息生产者。 Consumer:消息消费者。 Connection(连接):Producer 和 Consumer 通过TCP 连接到 RabbitMQ Server。 Channel(信道):基于 Connection 创建,数据流动都是在 Channel 中进行。 Exchange(交换器):生产者将消息发送到 Exchange(交换器),由 Exchange 将消息路由到一个或多个 Queue 中(或者丢弃);Exchange 并不存储消息;Exchange Types 常用有 Fanout、Direct、Topic 三种类型,每种类型对应不同的路由规则。 Queue(队列):是 RabbitMQ 的内部对象,用于存储消息;消息消费者就是通过订阅队列来获取消息的,RabbitMQ 中的消息都只能存储在 Queue 中,生产者生产消息并最终投递到 Queue 中,消费者可以从 Queue 中获取消息并消费;多个消费者可以订阅同一个 Queue,这时 Queue 中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。 Binding(绑定):是 Exchange(交换器)将消息路由给 Queue 所需遵循的规则。 Routing Key(路由键):消息发送给 Exchange(交换器)时,消息将拥有一个路由键(默认为空), Exchange(交换器)根据这个路由键将消息发送到匹配的队列中。 Binding Key(绑定键):指定当前 Exchange(交换器)下,什么样的 Routing Key(路由键)会被下派到当前绑定的 Queue 中。 另外,再说下 Exchange Types(交换器类型)的三种常用类型: Direct:完全匹配,消息路由到那些 Routing Key 与 Binding Key 完全匹配的 Queue 中。比如 Routing Key 为cleint-key,只会转发cleint-key,不会转发cleint-key.1,也不会转发cleint-key.1.2。 Topic:模式匹配,Exchange 会把消息发送到一个或者多个满足通配符规则的 routing-key 的 Queue。其中*表号匹配一个 word,#匹配多个 word 和路径,路径之间通过.隔开。如满足a.*.c的 routing-key 有a.hello.c;满足#.hello的 routing-key 有a.b.c.helo。 Fanout:忽略匹配,把所有发送到该 Exchange 的消息路由到所有与它绑定 的Queue 中。 下面通过一段代码,理解一下消息发布的流程(代码引用): import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() channel.exchange_declare(exchange='first', type='topic') channel.queue_declare(queue='A') channel.queue_declare(queue='B') channel.queue_bind(exchange='first', queue='A', routing_key='a.*.*') channel.queue_bind(exchange='first', queue='B', routing_key='a.#') channel.basic_publish(exchange='first', routing_key='a', body='Hello World!') channel.basic_publish(exchange='first', routing_key='a.b.c', body='Hello World!') 大致步骤: 先获取一个 Connection(连接)。 从 Connection(连接)上获取一个 Channel(信道)。 声明一个 Exchange(交换器),只会创建一次。 声明两个 Queue,只会创建一次。 把 Queue 绑定到 Exchange(交换器)上. 向指定的 Exchange(交换器)发送一条消息. 因为基于 Exchage Topic 模式,在上面发出的两条消息当中,消息a只会被a.#匹配到,而a.b.c会被两个都匹配到。所以,最终的结果会是队列 A 中有一条消息,队列 B 中有两条消息。 从队列取出消息代码: import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() channel.queue_declare(queue='A') def callback(ch, method, properties, body): print body channel.basic_consume(callback, queue='A', no_ack=True) channel.start_consuming() 服务消费者取出消息,需要重新创建 Connection(连接)和 Exchange(交换器),但 Queue 并不会创建,只需要从 Channel 中获取对应的 Queue 消息即可。 通过实例理解 RabbitMQ 基本概念 上面实例服务部署的情况是:三台管理服务器(config-server-git/config-server-svn)和一台工作服务器(config-client-git/config-server-svn),因为做了集群,服务的具体情况: config-server-git:3 个服务。 config-server-svn:3 个服务。 config-client-git:1 个服务。 config-client-svn:1 个服务。 所以,总的部署服务有 8 个。 我们通过 RabbitMQ Server 管理界面中的内容,说下 Connection(连接)、Channel(信道)、Exchange(交换器)和 Queue(队列)的具体使用情况(根据数量理解)。 1. Connection(连接) 为什么 Connection(连接)数量为 16 个?因为部署的 8 个服务,各自发布和接受消息(即作为小心发布者,也作为消息接受者),计算公式:16 = 8 * 2。 2. Channel(信道) 为什么 Channel(信道)数量为 16 个?因为 Connection(连接)数量为 16 个,Channel(信道)是在 Connection(连接)基础上创建的。 3. Exchange(交换器) 为什么 Exchange(交换器)数量为 1 个?因为都是使用的同一个 Exchange(交换器),名字为springCloudBus,Exchange Type 为topic,Routing Key 为#。 4. Queue(队列) 为什么 Queue(队列)数量为 8 个?因为部署的 8 个服务,各自发布和接受的 Queue 是同一个,一个服务对应一个 Queue。 参考资料: RabbitMQ 消息队列 - RabbitMQ 消息队列架构与基本概念(推荐) RabbitMQ 使用参考(推荐) RabbitMq(一)走进 RabbitMq RabbitMQ 基本概念和使用(推荐) 分布式消息队列 RabbitMQ 之一:基本概念理解 RabbitMQ 基础概念详细介绍 RabbitMQ 基本概念 作者:田园里的蟋蟀 微信公众号:你好架构 出处:http://www.cnblogs.com/xishuai/ 公众号会不定时的分享有关架构的方方面面,包含并不局限于:Microservices(微服务)、Service Mesh(服务网格)、DDD/TDD、Spring Cloud、Dubbo、Service Fabric、Linkerd、Envoy、Istio、Conduit、Kubernetes、Docker、MacOS/Linux、Java、.NET Core/ASP.NET Core、Redis、RabbitMQ、MongoDB、GitLab、CI/CD(持续集成/持续部署)、DevOps等等。 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
电脑操作系统是Win10中文版,新装的英文版SQL Server 2008,纯默认安装,没有做任何改动。 装完SQL Server 2008之后,发现只能用默认的机器名来登录: 如果用127.0.0.1登录就会报出如下的错误: 在网上查询一番之后,发现让开启SQL Server Configuration Manager中的Named Pipes就可以解决问题,然而试了并没有什么卵用。。。 于是开始自己动手尝试解决方案,经过一番尝试,功夫不负有心人啊!终于算是找到了,下面把步骤贴出来: 第一步,开启TCP/IP。因为装完SQL之后这项协议默认是不开通的: 第二步,设置TCP/IP中属性的"IP ALL"的端口为1433。众所周知,SQL Server的默认端口为1433,只是不知道为何,默认安装却没有这一项。 然后,重启SQL服务就可以用127.0.0.1登录上了:
Kafka作为时下最流行的开源消息系统,被广泛地应用在数据缓冲、异步通信、汇集日志、系统解耦等方面。相比较于RocketMQ等其他常见消息系统,Kafka在保障了大部分功能特性的同时,还提供了超一流的读写性能。 针对Kafka性能方面进行简单分析,相关数据请参考:https://segmentfault.com/a/1190000003985468,下面介绍一下Kafka的架构和涉及到的名词: Topic:用于划分Message的逻辑概念,一个Topic可以分布在多个Broker上。 Partition:是Kafka中横向扩展和一切并行化的基础,每个Topic都至少被切分为1个Partition。 Offset:消息在Partition中的编号,编号顺序不跨Partition。 Consumer:用于从Broker中取出/消费Message。 Producer:用于往Broker中发送/生产Message。 Replication:Kafka支持以Partition为单位对Message进行冗余备份,每个Partition都可以配置至少1个Replication(当仅1个Replication时即仅该Partition本身)。 Leader:每个Replication集合中的Partition都会选出一个唯一的Leader,所有的读写请求都由Leader处理。其他Replicas从Leader处把数据更新同步到本地,过程类似大家熟悉的MySQL中的Binlog同步。 Broker:Kafka中使用Broker来接受Producer和Consumer的请求,并把Message持久化到本地磁盘。每个Cluster当中会选举出一个Broker来担任Controller,负责处理Partition的Leader选举,协调Partition迁移等工作。 ISR(In-Sync Replica):是Replicas的一个子集,表示目前Alive且与Leader能够“Catch-up”的Replicas集合。由于读写都是首先落到Leader上,所以一般来说通过同步机制从Leader上拉取数据的Replica都会和Leader有一些延迟(包括了延迟时间和延迟条数两个维度),任意一个超过阈值都会把该Replica踢出ISR。每个Partition都有它自己独立的ISR。 更多关于Kafka的数据,参考:https://segmentfault.com/a/1190000003985468 **************************************************** **************************************************** Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输。 支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输。 同时支持离线数据处理和实时数据处理。 Scale out:支持在线水平扩展。 RabbitMQ RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。 Redis Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。 ZeroMQ ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter的Storm 0.9.0以前的版本中默认使用ZeroMQ作为数据流的传输(Storm从0.9版本开始同时支持ZeroMQ和Netty作为传输模块)。 ActiveMQ ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。 Kafka/Jafka Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。 以上转自:http://www.infoq.com/cn/articles/kafka-analysis-part-1/ **************************************************** **************************************************** 什么是Kafka? 引用官方原文: “ Kafka is a distributed, partitioned, replicated commit log service. ” 它提供了一个非常特殊的消息机制,不同于传统的mq。 官网:https://kafka.apache.org 它与传统的mq区别? 更快!单机上万TPS 传统的MQ,消息被消化掉后会被mq删除,而kafka中消息被消化后不会被删除,而是到配置的expire时间后,才删除 传统的MQ,消息的Offset是由MQ维护,而kafka中消息的Offset是由客户端自己维护 分布式,把写入压力均摊到各个节点。可以通过增加节点降低压力 基本术语 为方便理解,我用对比传统MQ的方式阐述这些基本术语。 Producer Consumer 这两个与传统的MQ一样,不解释了 Topic Kafka中的topic其实对应传统MQ的channel,即消息管道,例如同一业务用同一根管道 Broker 集群中的KafkaServer,用来提供Partition服务 Partition 假如说传统的MQ,传输消息的通道(channel)是一条双车道公路,那么Kafka中,Topic就是一个N车道的高速公路。每个车道都可以行车,而每个车道就是Partition。 一个Topic中可以有一个或多个partition。 一个Broker上可以跑一个或多个Partition。集群中尽量保证partition的均匀分布,例如定义了一个有3个partition的topic,而只有两个broker,那么一个broker上跑两个partition,而另一个是1个。但是如果有3个broker,必然是3个broker上各跑一个partition。 Partition中严格按照消息进入的顺序排序 一个从Producer发送来的消息,只会进入Topic的某一个Partition(除非特殊实现Producer要求消息进入所有Partition) Consumer可以自己决定从哪个Partition读取数据 Offset 单个Partition中的消息的顺序ID,例如第一个进入的Offset为0,第二个为1,以此类推。传统的MQ,Offset是由MQ自己维护,而kafka是由client维护 Replica Kafka从0.8版本开始,支持消息的HA,通过消息复制的方式。在创建时,我们可以指定一个topic有几个partition,以及每个partition有几个复制。复制的过程有同步和异步两种,根据性能需要选取。 正常情况下,写和读都是访问leader,只有当leader挂掉或者手动要求重新选举,kafka会从几个复制中选举新的leader。 Kafka会统计replica与leader的同步情况。当一个replica与leader数据相差不大,会被认为是一个"in-sync" replica。只有"in-sync" replica才有资格参与重新选举。 ConsumerGroup 一个或多个Consumer构成一个ConsumerGroup,一个消息应该只能被同一个ConsumerGroup中的一个Consumer消化掉,但是可以同时发送到不同ConsumerGroup。 通常的做法,一个Consumer去对应一个Partition。 传统MQ中有queuing(消息)和publish-subscribe(订阅)模式,Kafka中也支持: 当所有Consumer具有相同的ConsumerGroup时,该ConsumerGroup中只有一个Consumer能收到消息,就是 queuing 模式 当所有Consumer具有不同的ConsumerGroup时,每个ConsumerGroup会收到相同的消息,就是 publish-subscribe 模式 基本交互原理 每个Topic被创建后,在zookeeper上存放有其metadata,包含其分区信息、replica信息、LogAndOffset等 默认路径/brokers/topics/<topic_id>/partitions/<partition_index>/state Producer可以通过zookeeper获得topic的broker信息,从而得知需要往哪写数据。 Consumer也从zookeeper上获得该信息,从而得知要监听哪个partition。 基本CLI操作 1. 创建Topic ./kafka-create-topic.sh --zookeeper 10.1.110.21:2181 --replica 2 --partition 3 --topic test 2. 查看Topic信息 ./kafka-list-topic.sh --topic test --zookeeper 10.1.110.24:2181 3. 增加Partition ./kafka-add-partitions.sh --partition 4 --topic test --zookeeper 10.1.110.24:2181 更多命令参见:https://cwiki.apache.org/confluence/display/KAFKA/Replication+tools 创建一个Producer Kafka提供了java api,Producer特别的简单,举传输byte[] 为例 Properties p = new Properties(); props.put("metadata.broker.list", "10.1.110.21:9092"); ProducerConfig config = new ProducerConfig(props); Producer producer = new Producer<String, byte[]>(config); producer.send(byte[] msg);更具体的参见:https://cwiki.apache.org/confluence/display/KAFKA/0.8.0+Producer+Example 创建一个Consumer Kafka提供了两种java的Consumer API:High Level Consumer和Simple Consumer 看上去前者似乎要更牛B一点,事实上,前者做了更多的封装,比后者要Simple的多…… 具体例子我就不写了,参见 High Level Consumer: https://cwiki.apache.org/confluence/display/KAFKA/Consumer+Group+Example Simple Consumer: https://cwiki.apache.org/confluence/display/KAFKA/0.8.0+SimpleConsumer+Example 摘自:http://www.tuicool.com/articles/ruUzum **************************************************** **************************************************** 如何保证kafka的高容错性? producer不使用批量接口,并采用同步模型持久化消息。 consumer不采用批量化,每消费一次就更新offset ActiveMq RabbitMq Kafka producer容错,是否会丢数据 有ack模型,也有事务模型,保证至少不会丢数据。ack模型可能会有重复消息,事务模型则保证完全一致 批量形式下,可能会丢数据。 非批量形式下, 1. 使用同步模式,可能会有重复数据。 2. 异步模式,则可能会丢数据。 consumer容错,是否会丢数据 有ack模型,数据不会丢,但可能会重复处理数据。 批量形式下,可能会丢数据。非批量形式下,可能会重复处理数据。(ZK写offset是异步的) 架构模型 基于JMS协议 基于AMQP模型,比较成熟,但更新超慢。RabbitMQ的broker由Exchange,Binding,queue组成,其中exchange和binding组成了消息的路由键;客户端Producer通过连接channel和server进行通信,Consumer从queue获取消息进行消费(长连接,queue有消息会推送到consumer端,consumer循环从输入流读取数据)。rabbitMQ以broker为中心;有消息的确认机制 producer,broker,consumer,以consumer为中心,消息的消费信息保存的客户端consumer上,consumer根据消费的点,从broker上批量pull数据;无消息确认机制。 吞吐量 rabbitMQ在吞吐量方面稍逊于kafka,他们的出发点不一样,rabbitMQ支持对消息的可靠的传递,支持事务,不支持批量的操作;基于存储的可靠性的要求存储可以采用内存或者硬盘。 kafka具有高的吞吐量,内部采用消息的批量处理,zero-copy机制,数据的存储和获取是本地磁盘顺序批量操作,具有O(1)的复杂度,消息处理的效率很高 可用性 rabbitMQ支持miror的queue,主queue失效,miror queue接管 kafka的broker支持主备模式 集群负载均衡 rabbitMQ的负载均衡需要单独的loadbalancer进行支持 kafka采用zookeeper对集群中的broker、consumer进行管理,可以注册topic到zookeeper上;通过zookeeper的协调机制,producer保存对应topic的broker信息,可以随机或者轮询发送到broker上;并且producer可以基于语义指定分片,消息发送到broker的某分片上 参考:http://www.liaoqiqi.com/post/227 **************************************************** **************************************************** 注:下文转载自:http://blog.csdn.net/linsongbin1/article/details/47781187 MQ框架非常之多,比较流行的有RabbitMq、ActiveMq、ZeroMq、kafka。这几种MQ到底应该选择哪个?要根据自己项目的业务场景和需求。下面我列出这些MQ之间的对比数据和资料。 第一部分:RabbitMQ,ActiveMq,ZeroMq比较 1、 TPS比较 一 ZeroMq 最好,RabbitMq 次之, ActiveMq 最差。这个结论来自于以下这篇文章。 http://blog.x-aeon.com/2013/04/10/a-quick-message-queue-benchmark-activemq-rabbitmq-hornetq-qpid-apollo/ 测试环境: Model: Dell Studio 1749 CPU: Intel Core i3 @ 2.40 GHz RAM: 4 Gb OS: Windows 7 64 bits 其中包括持久化消息和瞬时消息的测试。注意这篇文章里面提到的MQ,都是采用默认配置的,并无调优。 更多的统计图请参看我提供的文章url。 2、TPS比较二 ZeroMq 最好,RabbitMq次之, ActiveMq最差。这个结论来自于一下这篇文章。http://www.cnblogs.com/amityat/archive/2011/08/31/2160293.html 显示的是发送和接受的每秒钟的消息数。整个过程共产生1百万条1K的消息。测试的执行是在一个Windows Vista上进行的。 3、持久化消息比较 zeroMq不支持,activeMq和rabbitMq都支持。持久化消息主要是指:MQ down或者MQ所在的服务器down了,消息不会丢失的机制。 4、技术点:可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统、社区 RabbitMq最好,ActiveMq次之,ZeroMq最差。当然ZeroMq也可以做到,不过自己必须手动写代码实现,代码量不小。尤其是可靠性中的:持久性、投递确认、发布者证实和高可用性。 所以在可靠性和可用性上,RabbitMQ是首选,虽然ActiveMQ也具备,但是它性能不及RabbitMQ。 5、高并发 从实现语言来看,RabbitMQ最高,原因是它的实现语言是天生具备高并发高可用的erlang语言。 总结: 按照目前网络上的资料,RabbitMQ、activeM、zeroMQ三者中,综合来看,RabbitMQ是首选。下面提供一篇文章,是淘宝使用RabbitMQ的心得,可以参看一些业务场景。 http://www.docin.com/p-462677246.html 第二部分:kafka和RabbitMQ的比较 关于这两种MQ的比较,网上的资料并不多,最权威的的是kafka的提交者写一篇文章。http://www.quora.com/What-are-the-differences-between-Apache-Kafka-and-RabbitMQ 里面提到的要点: 1、 RabbitMq比kafka成熟,在可用性上,稳定性上,可靠性上,RabbitMq超过kafka 2、 Kafka设计的初衷就是处理日志的,可以看做是一个日志系统,针对性很强,所以它并没有具备一个成熟MQ应该具备的特性 3、 Kafka的性能(吞吐量、tps)比RabbitMq要强,这篇文章的作者认为,两者在这方面没有可比性。 这里在附上两篇文章,也是关于kafka和RabbitMq之间的比较的: 1、http://www.mrhaoting.com/?p=139 2、http://www.liaoqiqi.com/post/227 总结: 两者对比后,我仍然是选择RabbitMq,性能其实是很强劲的,同时具备了一个成熟的MQ应该具有的特性,我们无需重新发明轮子。
Linux下的Valgrind真是利器啊(不知道Valgrind的请自觉查看参考文献(1)(2)),帮我找出了不少C++中的内存管理错误,前一阵子还在纠结为什么VS 2013下运行良好的程序到了Linux下用g++编译运行却崩溃了,给出一堆汇编代码也看不懂。久久不得解过后,想想肯定是内存方面的错误,VS在这方面一般都不检查的,就算你的程序千疮百孔,各种内存泄露、内存管理错误,只要不影响运行,没有读到不该读的东西VS就不会告诉你(应该是VS内部没实现这个内存检测功能),因此用VS写出的程序可能不是完美或健壮的。 ------------------------------------------------------------------------------------------------------------------------------ 更新:感谢博客园好心网友@shines77的热心推荐,即VS中有内存泄漏检测工具插件VLD(Visual Leak Detector),需要下载安装,安装方法请看官方介绍,使用非常简单,在第一个入口文件里加上#include <vld.h>就可以了,检测报告在输出窗口中。我安装使用了下,不知道是安装错误还是什么,无论程序有无内存泄露,输出都是“No memory leaks detected.” 下面是我通过 Valgrind第一次检测得到的结果和一点点修改后得到的结果(还没改完,所以还有不少内存泄露问题……): 第一次检测结果:惨不忍睹,因为程序规模有些大。 根据提示一点点修改过后,虽然还有个别错误和内存泄露问题,但还在修改中,至少已经能成功运行了…… 真感谢Valgrind帮我成功找出了一堆内存问题,查找过程中也为自己犯的低级错误而感到羞愧,所以记录下来以便谨记。 1. 最多最低级的错误:不匹配地使用malloc/new/new[] 和 free/delete/delete[] 这样的错误主要源于我对C++的new/new[]、delete/delete[]机制不熟悉,凡是new/new[]分配内存的类型变量我一概用delete进行释放,或者有的变量用malloc进行分配,结果释放的时候却用delete,导致申请、释放很多地方不匹配,很多内存空间没能释放掉。为了维护方便,我后来一律使用new/new[]和delete/delete[],抛弃C中的malloc和free。 如果将用户new的类型分为基本数据类型和自定义数据类型两种,那么对于下面的操作相信大家都很熟悉,也没有任何问题。 (1)基本数据类型 一维指针: // 申请空间 int *d = new int[5]; // 释放空间 delete[] d; 二维指针: // 申请空间 int **d = new int*[5]; for (int i = 0; i < 5; i++) d[i] = new int[10]; // 释放空间 for (int i = 0; i < 5; i++) delete[] d[i]; delete[] d; (2)自定义数据类型 比如下面这样一个类型: class DFA { bool is_mark; char *s; public: ~DFA() { printf("delete it.\n"); } }; 一维指针: DFA *d = new DFA(); delete d; 二维指针: // 申请空间 DFA **d = new DFA*[5]; for (int i = 0; i < 5; i++) d[i] = new DFA(); // 释放空间 for (int i = 0; i < 5; i++) delete d[i]; delete[]d; 这没有任何问题,因为我们都是配套使用new/delete和new[]/delete[]的。这在Valgrind下检测也是完美通过的,但为什么要这配套使用呢?原理是什么? 虽然深究这些东西好像没什么实际意义,但对于想深入了解C++内部机制或像我一样老是释放出错导致大量内存泄露的小白程序员还是值得研究的,至少知道了为什么,以后就不会犯现在的低级错误。 参考文献(3)是这样描述的: 通常状况下,编译器在new的时候会返回用户申请的内存空间大小,但是实际上,编译器会分配更大的空间,目的就是在delete的时候能够准确的释放这段空间。 这段空间在用户取得的指针之前以及用户空间末尾之后存放。 实际上:blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize; 其中,blockSize 是系统所分配的实际空间大小,_CrtMemBlockHeader是new的头部信息,其中包含用户申请的空间大小等其他一些信息。 nNoMansLandSize是尾部的越界校验大小,一般是4个字节“FEFEFEFE”,如果用户越界写入这段空间,则校验的时候会assert。nSize才是为我们分配的真正可用的内存空间。 用户new的时候分为两种情况 A. new的是基础数据类型或者是没有自定义析构函数的结构 B. new的是有自定义析构函数的结构体或类 这两者的区别是如果有用户自定义的析构函数,则delete的时候必须要调用析构函数,那么编译器delete时如何知道要调用多少个对象的析构函数呢,答案就是new的时候,如果是情况B,则编译器会在new头部之后,用户获得的指针之前多分配4个字节的空间用来记录new的时候的数组大小,这样delete的时候就可以取到个数并正确的调用。 这段描述可能有些晦涩难懂,参考文献(4)给了更加详细的解释,一点即通。这样的解释其实也隐含着一个推论:如果new的是基本数据类型或者是没有自定义析构函数的结构,那么这种情况下编译器不会在用户获得的指针之前多分配4个字节,因为这时候delete时不用调用析构函数,也就是不用知道数组个数的大小(因为只有调用析构函数时才需要知道要调用多少个析构函数,也就是数组的大小),而是直接传入数组的起始地址从而释放掉这块内存空间,此时delete与delete[]是等价的。 因此下面的释放操作也是正确的: // 申请空间 int *d = new int[5]; // 释放空间 delete d; 将其放在Valgrind下进行检测,结果如下: ==2955== Memcheck, a memory error detector ==2955== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al. ==2955== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info ==2955== Command: ./test_int ==2955== ==2955== Mismatched free() / delete / delete [] ==2955== at 0x402ACFC: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==2955== by 0x8048530: main (in /home/hadoop/test/test_int) ==2955== Address 0x434a028 is 0 bytes inside a block of size 20 alloc'd ==2955== at 0x402B774: operator new[](unsigned int) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==2955== by 0x8048520: main (in /home/hadoop/test/test_int) ==2955== ==2955== ==2955== HEAP SUMMARY: ==2955== in use at exit: 0 bytes in 0 blocks ==2955== total heap usage: 1 allocs, 1 frees, 20 bytes allocated ==2955== ==2955== All heap blocks were freed -- no leaks are possible ==2955== ==2955== For counts of detected and suppressed errors, rerun with: -v ==2955== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) 首先从“All heap blocks were freed -- no leaks are possible”可以看出上面的释放操作的确是正确的,而不是有些人认为的delete d;只会释放d[]的第一个元素的空间,后面的都不会得到释放。但是从“Mismatched free() / delete / delete []”知道Valgrind实际上是不允许这样操作的,虽然没有内存泄露问题,但是new[]与delete不匹配,这样的编程风格不经意间就容易犯低级错误,所以Valgrind报错了,但是我想Valgrind内部实现应该不会考虑的这么复杂,它就检查new是否与delete配对,new[]是否与delete[]配对,而不管有时候new[]与delete配对也不会出现问题的。 综上所述,给我的经验就是:在某些情况下,new[]分配的内存用delete不会出错,但是大多情况下会产生严重的内存问题,所以一定要养成将new和delete,new[]和delete[]配套使用的良好编程习惯。 2. 最看不懂的错误:一堆看不懂的Invalid read/write错误(更新:已解决) 比如下面这样一个程序: #include <stdio.h> #include <string.h> #include <stdlib.h> struct accept_pair { bool is_accept_state; bool is_strict_end; char app_name[0]; }; int main() { char *s = "Alexia"; accept_pair *ap = (accept_pair*)malloc(sizeof(accept_pair) + sizeof(s)); strcpy(ap->app_name, s); printf("app name: %s\n", ap->app_name); free(ap); return 0; } 首先对该程序做个扼要的说明: 这里结构体里定义零长数组的原因在于我的需求:我在其它地方要用到很大的accept_pair数组,其中只有个别accept_pair元素中的app_name是有效的(取决于某些值的判断,如果为true才给app_name赋值,如果为false则app_name无意义,为空),因此若是char app_name[20],那么大部分accept_pair元素都浪费了这20个字节的空间,所以我在这里先一个字节都不分配,到时谁需要就给谁分配,遵循“按需分配”的古老思想。可能有人会想,用char *app_name也可以啊,同样能实现按需分配,是的,只是多4个字节而已,属于替补方法。 在g++下经过测试,没有什么问题,能够正确运行,但用Valgrind检测时却报出了一些错误,不是内存泄露问题,而是内存读写错误: ==3511== Memcheck, a memory error detector ==3511== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al. ==3511== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info ==3511== Command: ./zero ==3511== ==3511== Invalid write of size 1 ==3511== at 0x402CD8B: strcpy (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484E3: main (in /home/hadoop/test/zero) ==3511== Address 0x420002e is 0 bytes after a block of size 6 alloc'd ==3511== at 0x402C418: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484C8: main (in /home/hadoop/test/zero) ==3511== ==3511== Invalid write of size 1 ==3511== at 0x402CDA5: strcpy (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484E3: main (in /home/hadoop/test/zero) ==3511== Address 0x4200030 is 2 bytes after a block of size 6 alloc'd ==3511== at 0x402C418: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484C8: main (in /home/hadoop/test/zero) ==3511== ==3511== Invalid read of size 1 ==3511== at 0x40936A5: vfprintf (vfprintf.c:1655) ==3511== by 0x409881E: printf (printf.c:34) ==3511== by 0x4063934: (below main) (libc-start.c:260) ==3511== Address 0x420002e is 0 bytes after a block of size 6 alloc'd ==3511== at 0x402C418: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484C8: main (in /home/hadoop/test/zero) ==3511== ==3511== Invalid read of size 1 ==3511== at 0x40BC3C0: _IO_file_xsputn@@GLIBC_2.1 (fileops.c:1311) ==3511== by 0x4092184: vfprintf (vfprintf.c:1655) ==3511== by 0x409881E: printf (printf.c:34) ==3511== by 0x4063934: (below main) (libc-start.c:260) ==3511== Address 0x420002f is 1 bytes after a block of size 6 alloc'd ==3511== at 0x402C418: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484C8: main (in /home/hadoop/test/zero) ==3511== ==3511== Invalid read of size 1 ==3511== at 0x40BC3D7: _IO_file_xsputn@@GLIBC_2.1 (fileops.c:1311) ==3511== by 0x4092184: vfprintf (vfprintf.c:1655) ==3511== by 0x409881E: printf (printf.c:34) ==3511== by 0x4063934: (below main) (libc-start.c:260) ==3511== Address 0x420002e is 0 bytes after a block of size 6 alloc'd ==3511== at 0x402C418: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484C8: main (in /home/hadoop/test/zero) ==3511== ==3511== Invalid read of size 4 ==3511== at 0x40C999C: __GI_mempcpy (mempcpy.S:59) ==3511== by 0x40BC310: _IO_file_xsputn@@GLIBC_2.1 (fileops.c:1329) ==3511== by 0x4092184: vfprintf (vfprintf.c:1655) ==3511== by 0x409881E: printf (printf.c:34) ==3511== by 0x4063934: (below main) (libc-start.c:260) ==3511== Address 0x420002c is 4 bytes inside a block of size 6 alloc'd ==3511== at 0x402C418: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3511== by 0x80484C8: main (in /home/hadoop/test/zero) ==3511== app name: Alexia ==3511== ==3511== HEAP SUMMARY: ==3511== in use at exit: 0 bytes in 0 blocks ==3511== total heap usage: 1 allocs, 1 frees, 6 bytes allocated ==3511== ==3511== All heap blocks were freed -- no leaks are possible ==3511== ==3511== For counts of detected and suppressed errors, rerun with: -v ==3511== ERROR SUMMARY: 9 errors from 6 contexts (suppressed: 0 from 0) 从检测报告可以看出: strcpy(ap->app_name, s);这句是内存写错误,printf("app name: %s\n", ap->app_name);这句是内存读错误,两者都说明Valgrind认为ap->app_name所处内存空间是不合法的,可是我明明已经为其分配了内存空间,只是没有注明这段空间就是给它用的,难道结构体中零长数组char app_name[0]是不能写入值的吗?还是我对零长数组的使用有误?至今仍不得解,求大神解答…… ------------------------------------------------------------------------------------------------------------------------------ 更新:谢谢博客园网友@shines77的好心指正,这里犯了个超级低级的错误,就是忘了main中s是char*的,因此sizeof(s)=4或8(64位机),因此accept_pair *ap = (accept_pair*)malloc(sizeof(accept_pair) + sizeof(s));这句并没有为app_name申请足够的空间,当然就会出现Invalid read/write了。这个低级错误真是。。。后来想了下,是自己在项目中直接拷贝过来的这句,项目中的s不是char*的,拷贝过来忘了改成accept_pair *ap = (accept_pair*)malloc(sizeof(accept_pair) + strlen(s) + 1);了,以后还是细心的好,真是浪费自己时间也浪费大家时间了。 3. 最不明所以的内存泄露:definitely lost/indefinitely lost(更新:已解决) 请看下面这样一个程序: #include <stdio.h> #include <string.h> class accept_pair { public: bool is_accept_state; bool is_strict_end; char *app_name; public: accept_pair(bool is_accept = false, bool is_end = false); ~accept_pair(); }; class DFA { public: unsigned int _size; accept_pair **accept_states; public: DFA(int size); ~DFA(); void add_state(int index, char *s); void add_size(int size); }; int main() { char *s = "Alexia"; DFA *dfa = new DFA(3); dfa->add_state(0, s); dfa->add_state(1, s); dfa->add_state(2, s); dfa->add_size(2); dfa->add_state(3, s); dfa->add_state(4, s); printf("\napp_name: %s\n", dfa->accept_states[4]->app_name); printf("size: %d\n\n", dfa->_size); delete dfa; return 0; } accept_pair::accept_pair(bool is_accept, bool is_end) { is_accept_state = is_accept; is_strict_end = is_end; app_name = NULL; } accept_pair::~accept_pair() { if (app_name) { printf("delete accept_pair.\n"); delete[] app_name; } } DFA::DFA(int size) { _size = size; accept_states = new accept_pair*[_size]; for (int s = 0; s < _size; s++) { accept_states[s] = NULL; } } DFA::~DFA() { for (int i = 0; i < _size; i++) { if (accept_states[i]) { printf("delete dfa.\n"); delete accept_states[i]; accept_states[i] = NULL; } } delete[] accept_states; } void DFA::add_state(int index, char *s) { accept_states[index] = new accept_pair(true, true); accept_states[index]->app_name = new char[strlen(s) + 1]; memcpy(accept_states[index]->app_name, s, strlen(s) + 1); } void DFA::add_size(int size) { // reallocate memory for accept_states. accept_pair **tmp_states = new accept_pair*[size + _size]; for (int s = 0; s < size + _size; s++) tmp_states[s] = new accept_pair(false, false); for (int s = 0; s < _size; s++) { tmp_states[s]->is_accept_state = accept_states[s]->is_accept_state; tmp_states[s]->is_strict_end = accept_states[s]->is_strict_end; if (accept_states[s]->app_name != NULL) { tmp_states[s]->app_name = new char[strlen(accept_states[s]->app_name) + 1]; memcpy(tmp_states[s]->app_name, accept_states[s]->app_name, strlen(accept_states[s]->app_name) + 1); } } // free old memory. for (int s = 0; s < _size; s++) { if (accept_states[s] != NULL) { delete accept_states[s]; accept_states[s] = NULL; } } _size += size; delete []accept_states; accept_states = tmp_states; } 虽然有点长,但逻辑很简单,其中add_size()首先分配一个更大的accept_pair数组,将已有的数据全部拷贝进去,然后释放掉原来的accept_pair数组所占空间,最后将旧的数组指针指向新分配的内存空间。这是个demo程序,在我看来这段程序是没有任何内存泄露问题的,因为申请的所有内存空间最后都会在DFA析构函数中得到释放。但是Valgrind的检测报告却报出了1个内存泄露问题(红色的是程序输出): ==3093== Memcheck, a memory error detector ==3093== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al. ==3093== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info ==3093== Command: ./test ==3093== delete accept_pair. delete accept_pair. delete accept_pair. app_name: Alexia size: 5 delete dfa. delete accept_pair. delete dfa. delete accept_pair. delete dfa. delete accept_pair. delete dfa. delete accept_pair. delete dfa. delete accept_pair. ==3093== ==3093== HEAP SUMMARY: ==3093== in use at exit: 16 bytes in 2 blocks ==3093== total heap usage: 21 allocs, 19 frees, 176 bytes allocated ==3093== ==3093== 16 bytes in 2 blocks are definitely lost in loss record 1 of 1 ==3093== at 0x402BE94: operator new(unsigned int) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so) ==3093== by 0x8048A71: DFA::add_size(int) (in /home/hadoop/test/test) ==3093== by 0x8048798: main (in /home/hadoop/test/test) ==3093== ==3093== LEAK SUMMARY: ==3093== definitely lost: 16 bytes in 2 blocks ==3093== indirectly lost: 0 bytes in 0 blocks ==3093== possibly lost: 0 bytes in 0 blocks ==3093== still reachable: 0 bytes in 0 blocks ==3093== suppressed: 0 bytes in 0 blocks ==3093== ==3093== For counts of detected and suppressed errors, rerun with: -v ==3093== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) 说明add_size()这个函数里存在用new申请的内存空间没有得到释放,这一点感觉很费解,开始以为tmp_states指针所指向的数据赋给accept_states后没有及时释放导致的,于是我最后加了句delete tmp_states;结果招致更多的错误。相信不是Valgrind误报,说明我对C++的new和delete机制还是不明不白,一些于我而言不明所以的内存泄露问题真心不得解,希望有人能够告诉我是哪里的问题? ------------------------------------------------------------------------------------------------------------------------------ 更新:谢谢博客园好心网友@NewClear的解惑。这里的确有泄露问题,下面是他的解答: 第3个问题,是有两个泄露 DFA::add_state里面直接 accept_states[index] = new accept_pair(true, true); 如果原来的accept_states[index]不为NULL就泄露了 而在DFA::add_size里面, for (int s = 0; s < size + _size; s++) tmp_states[s] = new accept_pair(false, false); 对新分配的tmp_states的每一个元素都new了一个新的accept_pair 所以在main函数里面dfa->add_size(2);以后,总共有5个成员,而且5个都不为NULL 之后 dfa->add_state(3, s); dfa->add_state(4, s); 结果就导致了index为3和4的原先的对象泄露了 你的系统是32位的,所以一个accept_pair大小是8byte,两个对象就是16byte 解决方案也很简单,修改add_size函数,重新申请空间时仅为已有的accept_pair数据申请空间,其它的初始化为NULL,这样在需要时才在add_state里面申请空间,也就是修改add_size函数如下: void DFA::add_size(int size) { // reallocate memory for accept_states. accept_pair **tmp_states = new accept_pair*[size + _size]; for (int s = 0; s < size + _size; s++) tmp_states[s] = NULL; for (int s = 0; s < _size; s++) { tmp_states[s] = new accept_pair(false, false); tmp_states[s]->is_accept_state = accept_states[s]->is_accept_state; tmp_states[s]->is_strict_end = accept_states[s]->is_strict_end; if (accept_states[s]->app_name != NULL) { tmp_states[s]->app_name = new char[strlen(accept_states[s]->app_name) + 1]; memcpy(tmp_states[s]->app_name, accept_states[s]->app_name, strlen(accept_states[s]->app_name) + 1); } } // free old memory. for (int s = 0; s < _size; s++) { if (accept_states[s] != NULL) { delete accept_states[s]; accept_states[s] = NULL; } } _size += size; delete[]accept_states; accept_states = tmp_states; } 4. 参考文献 (1) 如何使用Valgrind memcheck工具进行C/C++的内存泄漏检测 如何使用Valgrind memcheck工具进行C/C++的内存泄漏检测 (2) valgrind 详细说明 (3) 关于new和delete,new[] 和delete[] (4)浅谈 C++ 中的 new/delete 和 new[]/delete[]
转载:http://mingxinglai.com/cn/2015/12/material-of-mysql/ 我这里推荐几本MySQL的好书,应该能够有效避免学习MySQL的弯路,并且达到一个不错的水平。 我这里推荐的书或材料分为两个部分,分别是MySQL的使用和MySQL的源码学习。在介绍的过程中,我会穿插简单的评语或感想。 1.MySQL的使用 1.1 MySQL技术内幕:InnoDB存储引擎 学习MySQL的使用,首推姜承尧的《MySQL技术内幕:InnoDB存储引擎》,当然不是因为姜sir是我的经理才推荐这本书。这本书确实做到了由渐入深、深入浅出,是中国人写的最赞的MySQL技术书籍,符合国人的思维方式和阅读习惯,而且,这本书简直就是面试宝典,对于近期有求职MySQL相关岗位的朋友,可以认真阅读,对找工作有很大的帮助。当然,也有人说这本书入门难度较大,这个就自己取舍了,个人建议就以这本书入门即可,有不懂的地方可以求助官方手册和google。 1.2 MySQL的官方手册 我刚开始学习MySQL的时候误区就是,没有好好阅读MySQL的官方手册。例如,我刚开始很难理解InnoDB的锁,尤其是各个情况下如何加锁,这个问题在我师弟进入百度做DBA时,也困扰了他一阵子,我们两还讨论来讨论去,其实,MySQL官方手册已经写得清清楚楚,什么样的SQL语句加什么样的锁,当然,MySQL的官方手册非常庞大,一时半会很难看完,建议先看InnoDB相关的部分。 http://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html 1.3 MySQL排错指南 《MySQL排错指南》是2015年夏天引入中国的书籍,这本书可以说是DBA速成指南,介绍的内容其实比较简单,但是也非常实用,对于DBA这个讲究经验的工种,这本书就是传授经验的,可能对有较多工作经验的DBA来说,这本书基本没有什么用,但是,对于刚入职场的新人,或学校里的学生,这本书会有较大的帮助,非常推荐。 1.4 高性能MySQL 《高性能MySQL》是MySQL领域的经典之作,拥有广泛的影响力,学习MySQL的朋友都应该有所耳闻,所以我就不作过多介绍,唯一的建议就是仔细看、认真看、多看几遍,我每次看都会有不小的收获。这就是一本虽然书很厚,但是需要一页一页、一行一行都认真看的书。 1.5 数据库索引设计与优化 如果认真学习完前面几本书,基本上都已经对MySQL掌握得不错了,但是,如果不了解如何设计一个好的索引,仍然不能成为牛逼的DBA,牛逼的DBA和不牛逼的DBA,一半就是看对索引的掌握情况,《数据库索引设计与优化》就是从普通DBA走向牛逼DBA的捷径,这本书在淘宝内部非常推崇,但是在中国名气却不是很大,很多人不了解。这本书也是今年夏天刚有中文版本的,非常值得入手以后跟着练习,虽然知道的人不多,豆瓣上也几乎没有什么评价,但是,强烈推荐、吐血推荐! 1.6 Effective MySQL系列 《Effective MySQL系列》是指: Effective MySQL Replication Techniques in Depth Effective MySQL之SQL语句最优化 Effective MySQL之备份与恢复 这一系列并不如前面推荐的好,其中,我只看了前两本,这几本书只能算是小册子,如果有时间可以看看,对某一个”模块”进入深入了解。 2.MySQL的源码 关于MySQL源码的书非常少,还好现在市面上有两本不错的书,而且刚好一本讲server层,一本讲innodb存储引擎层,对于学习MySQL源码会很有帮助,至少能够更加快速地了解MySQL的原理和宏观结构,然后再深入细节。此外,还有一些博客或PPT将得也很不错,这里推荐最好的几份材料。 2.1 InnoDB - A journey to the core 《InnoDB - A journey to the core》 是MySQL大牛Jeremy Cole写的PPT,介绍InnoDB的存储模块,即表空间、区、段、页的格式、记录的格式、槽等等。是学习Innodb存储的最好的材料。感谢Jeremy Cole! 2.2 深入MySQL源码 登博的分享《深入MySQL源码》,相信很多想了解MySQL源码的朋友已经知道这份PPT,就不过多介绍,不过,要多说一句,登博的参考资料里列出的几个博客,都要关注一下,干货满满,是学习MySQL必须关注的博客。 2.3 深入理解MySQL核心技术 《深入理解MySQL核心技术》是第一本关于MySQL源码的书,着重介绍了MySQL的Server层,重点介绍了宏观架构,对于刚开始学习MySQL源码的人,相信会有很大的帮助,我在学习MySQL源码的过程中,反复的翻阅了几遍,这本书刚开始看的时候会很痛苦,但是,对于研究MySQL源码,非常有帮助,就看你是否需要,如果没有研究MySQL源码的决心,这本书应该会被唾弃。 2.4 MySQL内核:InnoDB存储引擎 我们组的同事写的《MySQL内核:InnoDB存储引擎》,可能宇宙范围内这本书就数我学得最认真了,虽然书中有很多编辑错误,但是,平心而论,还是写得非常好的,相对于《深入理解MySQL核心技术》,可读性更强一些,建议研究Innodb存储引擎的朋友,可以了解一下,先对Innodb有一个宏观的概念,对大致原理有一个整体的了解,然后再深入细节,肯定会比自己从头开始研究会快很多,这本书可以帮助你事半功倍。 2.5 MySQL Internals Manual 《MySQL Internals Manual》相对于MySQL Manual来说,写的太粗糙,谁让人家是官方文档呢,研究MySQL源码的时候可以简单地参考一下,但是,还是不要指望文档能够回答你的问题,还需要看代码才行。 http://dev.mysql.com/doc/internals/en/ 2.6 MariaDB原理与实现 评论里提到的《MariaDB原理与实现》我也买了一本,还不错,MariaDB讲的并不多,重点讲了Group Commit、线程池和复制的实现,都是MySQL Server层的知识,对MySQL Server层感兴趣的可以参考一下。 3. 后记 希望这里推荐的材料对学习MySQL的同学、朋友有所帮助,也欢迎推荐靠谱的学习材料,大家共同进步。
背景: 有一个项目做一个系统,分客户端和服务端,客户端用c++写的,用来收集信息然后传给服务端(客户端的数量还是比较多的,正常的有几千个), 服务端用Java写的(带管理页面),属于RPC模式,中间的通信框架使用的是thrift。 thrift很多优点就不多说了,它是facebook的开源的rpc框架,主要是它能够跨语言,序列化速度快,但是他有个不讨喜的地方就是它必须用自己IDL来定义接口 thrift版本:0.9.2. 问题定位与分析 步骤一.初步分析 客户端无法连接服务端,查看服务器的端口开启状况,服务端口并没有开启。于是启动服务端,启动几秒后,服务端崩溃,重复启动,服务端依旧在启动几秒后崩溃。 步骤二.查看服务端日志分析 分析得知是因为java.lang.OutOfMemoryError: Java heap space(堆内存溢出)导致的服务崩溃。 客户端搜集的主机信息,主机策略都是放在缓存中,可能是因为缓存较大造成的,但是通过日志可以看出是因为Thrift服务抛出的堆内存溢出异常与缓存大小无关。 步骤三.再次分析服务端日志 可以发现每次抛出异常的时候都会伴随着几十个客户端在向服务端发送日志,往往在发送几十条日志之后,服务崩溃。可以假设是不是堆内存设置的太小了? 查看启动参数配置,最大堆内存为256MB。修改启动配置,启动的时候分配更多的堆内存,改成java -server -Xms512m -Xmx768m。 结果是,能坚持多一点的时间,依旧会内存溢出服务崩溃。得出结论,一味的扩大内存是没有用的。 **为了证明结论是正确的,做了这样的实验:** > 内存设置为256MB,在公司服务器上部署了服务端,使用Java VisualVM远程监控服务器堆内存。 > > 模拟客户现场,注册3000个客户端,使用300个线程同时发送日志。 > > 结果和想象的一样,没有出现内存溢出的情况,如下图: > 上图是Java VisualVM远程监控,在压力测试的情况下,没有出现内存溢出的情况,256MB的内存肯定够用的。 步骤四.回到thrift源码中,查找关键问题 服务端采用的是Thrift框架中TThreadedSelectorServer这个类,这是一个NIO的服务。下图是thrift处理请求的模型: **说明:** >一个AcceptThread执行accept客户端请求操作,将accept到的Transport交给SelectorThread线程, > >AcceptThread中有个balance均衡器分配到SelectorThread;SelectorThread执行read,write操作, > >read到一个FrameBuffer(封装了方法名,参数,参数类型等数据,和读取写入,调用方法的操作)交给WorkerProcess线程池执行方法调用。 > >**内存溢出就是在read一个FrameBuffer产生的。** 步骤五.细致一点描述thrift处理过程 >1.服务端服务启动后,会listen()一直监听客户端的请求,当收到请求accept()后,交给线程池去处理这个请求 > >2.处理的方式是:首先获取客户端的编码协议getProtocol(),然后根据协议选取指定的工具进行反序列化,接着交给业务类处理process() > >3.process的顺序是,**先申请临时缓存读取这个请求数据**,处理请求数据,执行业务代码,写响应数据,**最后清除临时缓存** > > **总结:thrift服务端处理请求的时候,会先反序列化数据,接着申请临时缓存读取请求数据,然后执行业务并返回响应数据,最后请求临时缓存。** > > 所以压力测试的时候,thrift性能很高,而且内存占用不高,是因为它有自负载调节,使用NIO模式缓存,并使用线程池处理业务,每次处理完请求之后及时清除缓存。 步骤六.研读FrameBuffer的read方法代码 可以排除掉没有及时清除缓存的可能,方向明确,极大的可能是在申请NIO缓存的时候出现了问题,回到thrift框架,查看FrameBuffer的read方法代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public boolean read() { // try to read the frame size completely if (this.state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME_SIZE) { if (!this.internalRead()) { return false; } // if the frame size has been read completely, then prepare to read the actual time if (this.buffer_.remaining() != 0) { return true; } int frameSize = this.buffer_.getInt(0); if (frameSize <= 0) { this.LOGGER.error("Read an invalid frame size of " + frameSize + ". Are you using TFramedTransport on the client side?"); return false; } // if this frame will always be too large for this server, log the error and close the connection. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 if ((long)frameSize > AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) { this.LOGGER.error("Read a frame size of " + frameSize + ", which is bigger than the maximum allowable buffer size for ALL connections."); return false; } if (AbstractNonblockingServer.this.readBufferBytesAllocated.get() + (long)frameSize > AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) { return true; } AbstractNonblockingServer.this.readBufferBytesAllocated.addAndGet((long)(frameSize + 4)); this.buffer_ = ByteBuffer.allocate(frameSize + 4); this.buffer_.putInt(frameSize); this.state_ = AbstractNonblockingServer.FrameBufferState.READING_FRAME; } if (this.state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME) { if (!this.internalRead()) { return false; } else { if (this.buffer_.remaining() == 0) { this.selectionKey_.interestOps(0); this.state_ = AbstractNonblockingServer.FrameBufferState.READ_FRAME_COMPLETE; } return true; } } else { this.LOGGER.error("Read was called but state is invalid (" + this.state_ + ")"); return false; } } **说明:** >MAX_READ_BUFFER_BYTES这个值即为对读取的包的长度限制,如果超过长度限制,就不会再读了/ > >这个MAX_READ_BUFFER_BYTES是多少呢,thrift代码中给出了答案: 1 2 3 4 5 6 7 8 public abstract static class AbstractNonblockingServerArgs<T extends AbstractNonblockingServer.AbstractNonblockingServerArgs<T>> extends AbstractServerArgs<T> {<br> public long maxReadBufferBytes = 9223372036854775807L; public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) { super(transport); this.transportFactory(new Factory()); } } >从上面源码可以看出,默认值居然给到了long的最大值9223372036854775807L。 所以thrift的开发者是觉得使用thrift程序员不够觉得内存不够用吗,这个换算下来就是1045576TB,这个太夸张了,这等于没有限制啊,所以肯定不能用默认值的。 步骤七.通信数据抓包分析 需要可靠的证据证明一个客户端通信的数据包的大小。 这个是我抓到包最大的长度,最大一个包长度只有215B,所以需要限制一下读取大小 步骤八:踏破铁鞋无觅处 在论坛中,看到有人用http请求thrift服务端出现了内存溢出的情况,所以我抱着试试看的心态,在浏览器中发起了http请求, 果不其然,出现了内存溢出的错误,和客户现场出现的问题一摸一样。这个读取内存的时候数量过大,超过了256MB。 > 很明显的一个问题,正常的一个HTTP请求不会有256MB的,考虑到thrift在处理请求的时候有反序列化这个操作。 > > 可以做出假设是不是反序列化的问题,不是thrift IDL定义的不能正常的反序列化? > > 验证这个假设,我用Java socket写了一个tcp客户端,向thrift服务端发送请求,果不其然!java.lang.OutOfMemoryError: Java heap space。 > 这个假设是正确的,客户端请求数据不是用thrift IDL定义的话,无法正常序列化,序列化出来的数据会异常的大!大到超过1个G的都有。 步骤九. 找到原因 某些客户端没有正常的序列化消息,导致服务端在处理请求的时候,序列化出来的数据特别大,读取该数据的时候出现的内存溢出。 查看维护记录,在别的客户那里也出现过内存溢出导致服务端崩溃的情况,通过重新安装客户端,就不再复现了。 所以可以确定,客户端存在着无法正常序列化消息的情况。考虑到,客户端量比较大,一个一个排除,再重新安装比较困难,工作量很大,所以可以从服务端的角度来解决问题,减少维护工作量。 最后可以确定解决方案了,真的是废了很大的劲,不过也是颇有收获 问题解决方案 非常简单 在构造TThreadedSelectorServer的时候,增加args.maxReadBufferBytes = 1*1024 * 1024L;也就是说修改maxReadBufferBytes的大小,设置为1MB。 客户端与服务端通过thrift通信的数据包,最大十几K,所以设置最大1MB,是足够的。代码部分修改完成,版本不做改变** 修改完毕后,这次进行了异常流测试,发送了http请求,使服务端无法正常序列化。 服务端处理结果如下: thrift会抛出错误日志,并直接没有读这个消息,返回false,不处理这样的请求,将其视为错误请求。 3.国外有人对thrift一些server做了压力测试,如下图所示: 使用thrift中的TThreadedSelectorServer吞吐量达到18000以上 由于高性能,申请内存和清除内存的操作都是非常快的,平均3ms就处理了一个请求。 所以是推荐使用TThreadedSelectorServer 4.修改启动脚本,增大堆内存,分配单独的直接内存。 修改为java -server -Xms512m -Xmx768m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=512m -XX:MaxDirectMemorySize=128M。 设置持久代最大值 MaxPermSize:256m 设置年轻代大小 NewSize:256m 年轻代最大值 MaxNewSize:512M 最大堆外内存(直接内存)MaxDirectMemorySize:128M 5.综合论坛中,StackOverflow一些同僚的意见,在使用TThreadedSelectorServer时,将读取内存限制设置为1MB,最为合适,正常流和异常流的情况下不会有内存溢出的风险。 之前启动脚本给服务端分配的堆内存过小,考虑到是NIO,所以在启动服务端的时候,有必要单独分配一个直接内存供NIO使用.修改启动参数。 增加堆内存大小直接内存,防止因为服务端缓存太大,导致thrift服务没有内存可申请,无法处理请求。 总结: 真的是一次非常酸爽的过程,特此发个博客记录一下,如果有说的不对的对方,欢迎批评斧正!如果觉得写的不错,欢迎给我点个推荐,您的一个推荐是我莫大的动力!
1.基本介绍 Restful接口的调用,前端一般使用ajax调用,后端可以使用的方法比较多, 本次介绍三种: 1.HttpURLConnection实现 2.HttpClient实现 3.Spring的RestTemplate 2.HttpURLConnection实现 1 @Controller 2 public class RestfulAction { 3 4 @Autowired 5 private UserService userService; 6 7 // 修改 8 @RequestMapping(value = "put/{param}", method = RequestMethod.PUT) 9 public @ResponseBody String put(@PathVariable String param) { 10 return "put:" + param; 11 } 12 13 // 新增 14 @RequestMapping(value = "post/{param}", method = RequestMethod.POST) 15 public @ResponseBody String post(@PathVariable String param,String id,String name) { 16 System.out.println("id:"+id); 17 System.out.println("name:"+name); 18 return "post:" + param; 19 } 20 21 22 // 删除 23 @RequestMapping(value = "delete/{param}", method = RequestMethod.DELETE) 24 public @ResponseBody String delete(@PathVariable String param) { 25 return "delete:" + param; 26 } 27 28 // 查找 29 @RequestMapping(value = "get/{param}", method = RequestMethod.GET) 30 public @ResponseBody String get(@PathVariable String param) { 31 return "get:" + param; 32 } 33 34 35 // HttpURLConnection 方式调用Restful接口 36 // 调用接口 37 @RequestMapping(value = "dealCon/{param}") 38 public @ResponseBody String dealCon(@PathVariable String param) { 39 try { 40 String url = "http://localhost:8080/tao-manager-web/"; 41 url+=(param+"/xxx"); 42 URL restServiceURL = new URL(url); 43 HttpURLConnection httpConnection = (HttpURLConnection) restServiceURL 44 .openConnection(); 45 //param 输入小写,转换成 GET POST DELETE PUT 46 httpConnection.setRequestMethod(param.toUpperCase()); 47 // httpConnection.setRequestProperty("Accept", "application/json"); 48 if("post".equals(param)){ 49 //打开输出开关 50 httpConnection.setDoOutput(true); 51 // httpConnection.setDoInput(true); 52 53 //传递参数 54 String input = "&id="+ URLEncoder.encode("abc", "UTF-8"); 55 input+="&name="+ URLEncoder.encode("啊啊啊", "UTF-8"); 56 OutputStream outputStream = httpConnection.getOutputStream(); 57 outputStream.write(input.getBytes()); 58 outputStream.flush(); 59 } 60 if (httpConnection.getResponseCode() != 200) { 61 throw new RuntimeException( 62 "HTTP GET Request Failed with Error code : " 63 + httpConnection.getResponseCode()); 64 } 65 BufferedReader responseBuffer = new BufferedReader( 66 new InputStreamReader((httpConnection.getInputStream()))); 67 String output; 68 System.out.println("Output from Server: \n"); 69 while ((output = responseBuffer.readLine()) != null) { 70 System.out.println(output); 71 } 72 httpConnection.disconnect(); 73 } catch (MalformedURLException e) { 74 e.printStackTrace(); 75 } catch (IOException e) { 76 e.printStackTrace(); 77 } 78 return "success"; 79 } 80 81 } 3.HttpClient实现 1 package com.taozhiye.controller; 2 3 import org.apache.http.HttpEntity; 4 import org.apache.http.HttpResponse; 5 import org.apache.http.NameValuePair; 6 import org.apache.http.client.HttpClient; 7 import org.apache.http.client.entity.UrlEncodedFormEntity; 8 import org.apache.http.client.methods.HttpDelete; 9 import org.apache.http.client.methods.HttpGet; 10 import org.apache.http.client.methods.HttpPost; 11 import org.apache.http.client.methods.HttpPut; 12 import org.apache.http.impl.client.HttpClients; 13 import org.apache.http.message.BasicNameValuePair; 14 import org.springframework.beans.factory.annotation.Autowired; 15 import org.springframework.stereotype.Controller; 16 import org.springframework.web.bind.annotation.PathVariable; 17 import org.springframework.web.bind.annotation.RequestMapping; 18 import org.springframework.web.bind.annotation.RequestMethod; 19 import org.springframework.web.bind.annotation.ResponseBody; 20 21 import com.fasterxml.jackson.databind.ObjectMapper; 22 import com.taozhiye.entity.User; 23 import com.taozhiye.service.UserService; 24 25 import java.io.BufferedReader; 26 import java.io.IOException; 27 import java.io.InputStreamReader; 28 import java.io.OutputStream; 29 import java.net.HttpURLConnection; 30 import java.net.MalformedURLException; 31 import java.net.URL; 32 import java.net.URLEncoder; 33 import java.util.ArrayList; 34 import java.util.List; 35 36 @Controller 37 public class RestfulAction { 38 39 @Autowired 40 private UserService userService; 41 42 // 修改 43 @RequestMapping(value = "put/{param}", method = RequestMethod.PUT) 44 public @ResponseBody String put(@PathVariable String param) { 45 return "put:" + param; 46 } 47 48 // 新增 49 @RequestMapping(value = "post/{param}", method = RequestMethod.POST) 50 public @ResponseBody User post(@PathVariable String param,String id,String name) { 51 User u = new User(); 52 System.out.println(id); 53 System.out.println(name); 54 u.setName(id); 55 u.setPassword(name); 56 u.setEmail(id); 57 u.setUsername(name); 58 return u; 59 } 60 61 62 // 删除 63 @RequestMapping(value = "delete/{param}", method = RequestMethod.DELETE) 64 public @ResponseBody String delete(@PathVariable String param) { 65 return "delete:" + param; 66 } 67 68 // 查找 69 @RequestMapping(value = "get/{param}", method = RequestMethod.GET) 70 public @ResponseBody User get(@PathVariable String param) { 71 User u = new User(); 72 u.setName(param); 73 u.setPassword(param); 74 u.setEmail(param); 75 u.setUsername("爱爱啊"); 76 return u; 77 } 78 79 80 81 @RequestMapping(value = "dealCon2/{param}") 82 public @ResponseBody User dealCon2(@PathVariable String param) { 83 User user = null; 84 try { 85 HttpClient client = HttpClients.createDefault(); 86 if("get".equals(param)){ 87 HttpGet request = new HttpGet("http://localhost:8080/tao-manager-web/get/" 88 +"啊啊啊"); 89 request.setHeader("Accept", "application/json"); 90 HttpResponse response = client.execute(request); 91 HttpEntity entity = response.getEntity(); 92 ObjectMapper mapper = new ObjectMapper(); 93 user = mapper.readValue(entity.getContent(), User.class); 94 }else if("post".equals(param)){ 95 HttpPost request2 = new HttpPost("http://localhost:8080/tao-manager-web/post/xxx"); 96 List<NameValuePair> nvps = new ArrayList<NameValuePair>(); 97 nvps.add(new BasicNameValuePair("id", "啊啊啊")); 98 nvps.add(new BasicNameValuePair("name", "secret")); 99 UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nvps, "GBK"); 100 request2.setEntity(formEntity); 101 HttpResponse response2 = client.execute(request2); 102 HttpEntity entity = response2.getEntity(); 103 ObjectMapper mapper = new ObjectMapper(); 104 user = mapper.readValue(entity.getContent(), User.class); 105 }else if("delete".equals(param)){ 106 107 }else if("put".equals(param)){ 108 109 } 110 } catch (Exception e) { 111 e.printStackTrace(); 112 } 113 return user; 114 } 115 116 117 } 4.Spring的RestTemplate springmvc.xml增加 1 <!-- 配置RestTemplate --> 2 <!--Http client Factory --> 3 <bean id="httpClientFactory" 4 class="org.springframework.http.client.SimpleClientHttpRequestFactory"> 5 <property name="connectTimeout" value="10000" /> 6 <property name="readTimeout" value="10000" /> 7 </bean> 8 9 <!--RestTemplate --> 10 <bean id="restTemplate" class="org.springframework.web.client.RestTemplate"> 11 <constructor-arg ref="httpClientFactory" /> 12 </bean> controller 1 @Controller 2 public class RestTemplateAction { 3 4 @Autowired 5 private RestTemplate template; 6 7 @RequestMapping("RestTem") 8 public @ResponseBody User RestTem(String method) { 9 User user = null; 10 //查找 11 if ("get".equals(method)) { 12 user = template.getForObject( 13 "http://localhost:8080/tao-manager-web/get/{id}", 14 User.class, "呜呜呜呜"); 15 16 //getForEntity与getForObject的区别是可以获取返回值和状态、头等信息 17 ResponseEntity<User> re = template. 18 getForEntity("http://localhost:8080/tao-manager-web/get/{id}", 19 User.class, "呜呜呜呜"); 20 System.out.println(re.getStatusCode()); 21 System.out.println(re.getBody().getUsername()); 22 23 //新增 24 } else if ("post".equals(method)) { 25 HttpHeaders headers = new HttpHeaders(); 26 headers.add("X-Auth-Token", UUID.randomUUID().toString()); 27 MultiValueMap<String, String> postParameters = new LinkedMultiValueMap<String, String>(); 28 postParameters.add("id", "啊啊啊"); 29 postParameters.add("name", "部版本"); 30 HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>( 31 postParameters, headers); 32 user = template.postForObject( 33 "http://localhost:8080/tao-manager-web/post/aaa", requestEntity, 34 User.class); 35 //删除 36 } else if ("delete".equals(method)) { 37 template.delete("http://localhost:8080/tao-manager-web/delete/{id}","aaa"); 38 //修改 39 } else if ("put".equals(method)) { 40 template.put("http://localhost:8080/tao-manager-web/put/{id}",null,"bbb"); 41 } 42 return user; 43 44 } 45 }
一、GO语言安装 详情查看:GO语言下载、安装、配置 二、GoLang插件介绍 对于Visual Studio Code开发工具,有一款优秀的GoLang插件,它的主页为:https://github.com/microsoft/vscode-go 这款插件的特性包括: Colorization 代码着彩色 Completion Lists 代码自动完成(使用gocode) Snippets 代码片段 Quick Info 快速提示信息(使用godef) Goto Definition 跳转到定义(使用godef) Find References 搜索参考引用(使用go-find-references) File outline 文件大纲(使用go-outline) Workspace symbol search 工作区符号搜索(使用 go-symbols) Rename 重命名(使用gorename) Build-on-save 保存构建(使用go build和go test) Format 代码格式化(使用goreturns或goimports或gofmt) Add Imports 添加引用(使用 gopkgs) Debugging 调试代码(使用delve) 本插件的安装教程,请查看《Windows环境下vscode-go安装日记》 三、插件安装 3.1 Visual Studio Code 找到微软的官方网站,下载Visual Studio Code,官网地址 https://code.visualstudio.com/ 点击上图红框,可以下载其他平台的编辑器,如下图: 下载安装过程省略,当前版本是: 3.2 安装插件 进入Visual Studio Code,使用快捷键F1,打开命令面板 在上图光标处·输入exten ,然后选择“Extensions:Install Extension”,如下图: 查询插件: 显示插件列表: 在插件列表中,选择 Go,进行安装,安装之后,系统会提示重启Visual Studio Code。 3.3 设置环境变量GOPATH 在Windows系统中设置GOPATH环境变量,我的值为D:\GoWorks 缺少GOPATH环境变量通常会报“$GOPATH not set.”这样的错误。 3.4 开启Visual Studio Code自动保存功能 打开Visual Studio Code,找到菜单File->Preferences->User Settings,如下图: 添加或更改settings.json的“files.autoSave”属性为“onFocusChange”,并保存。 3.5 安装Git Windows安装Git的过程省略;安装之后git\bin配置到PATH环境变量中。 四、插件配置 4.1 Visual Studio Code Go插件配置选项 Visual Studio Code的配置选项支持Go插件的设置,可以通过用户偏好设置或workspace设置进行配置。在菜单File->Preferences处可以找到。 在settings.json中设置go配置环境,如下图: 4.2 执行命令 详情请查看官方网站:https://marketplace.visualstudio.com/items?itemName=lukehoban.Go 1)安装gocode 打开命令提示符(以管理员身份打开),输入: go get -u -v github.com/nsf/gocode 开始下载: 下载完毕: 下载完成,查看D:\GoWorks目录,多了一个src\github.com\nsf\gocode路径,如下图: 雷同,通过命令行安装以下8个工具。 2)安装godef go get -u -v github.com/rogpeppe/godef 3)安装golint go get -u -v github.com/golang/lint/golint 4)安装go-find-references go get -u -v github.com/lukehoban/go-find-references 5)安装go-outline go get -u -v github.com/lukehoban/go-outline 6)安装goreturns go get -u -v sourcegraph.com/sqs/goreturns 7)安装gorename go get -u -v golang.org/x/tools/cmd/gorename 8)安装gopkgs go get -u -v github.com/tpng/gopkgs 9)安装go-symbols go get -u -v github.com/newhook/go-symbols 集成安装命令,拷贝到cmd窗口就可完成安装: go get -u -v github.com/nsf/gocode go get -u -v github.com/rogpeppe/godef go get -u -v github.com/golang/lint/golint go get -u -v github.com/lukehoban/go-find-references go get -u -v github.com/lukehoban/go-outline go get -u -v sourcegraph.com/sqs/goreturns go get -u -v golang.org/x/tools/cmd/gorename go get -u -v github.com/tpng/gopkgs go get -u -v github.com/newhook/go-symbols 3、使用 Hello word 1)File-> Open Folder,现在工作目录: 2)新建go文件 看看,智能提示出来了。 关于Visual Studio Code的调试功能配置,可查看:Windows环境下vscode-go安装日记
inux删除文件后沒有释放空间 今天发现一台服务器的home空间满了,于是要清空没用的文件,当我删除文件后,发现可用空间沒有变化 os:centos4.7 现象: 发现当前磁盘空间使用情况: [root@ticketb ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/sda1 981M 203M 729M 22% / none 16G 0 16G 0% /dev/shm /dev/sda9 2.9G 37M 2.7G 2% /tmp /dev/sda7 4.9G 1.9G 2.7G 42% /usr /dev/sda8 2.9G 145M 2.6G 6% /var /dev/mapper/vghome-lvhome 20G 19G 11M 100% /home /dev/mapper/vgoradata-lvoradata 144G 48G 90G 35% /u01/oradata /dev/mapper/vgbackup-lvbackup 193G 7.8G 175G 5% /u01/backup 通过以下的命令找到没用的文件,然后删除 [root@ticketb ~]# find /home/oracle/admin/dbticb/udump/ -name "dbticb_*.trc" -mtime +50 | xargs rm -rf 然后在查看磁盘空间使用情况,发现沒有/home空间沒有变化 [root@ticketb ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/sda1 981M 203M 729M 22% / none 16G 0 16G 0% /dev/shm /dev/sda9 2.9G 37M 2.7G 2% /tmp /dev/sda7 4.9G 1.9G 2.7G 42% /usr /dev/sda8 2.9G 145M 2.6G 6% /var /dev/mapper/vghome-lvhome 20G 19G 11M 100% /home /dev/mapper/vgoradata-lvoradata 144G 48G 90G 35% /u01/oradata /dev/mapper/vgbackup-lvbackup 193G 7.8G 175G 5% /u01/backup 这个郁闷啊,明明删除文件了,怎么空间沒有被释放啊,rm命令应该是直接删除啊,在查看下/home下还有什么占用空间 [root@ticketb ~]# du -h --max-depth=1 /home 16K /home/lost+found 2.6G /home/oracle 2.6G /home 可这里显示空间已经释放了啊,于是google下, 未释放磁盘空间原因: 在Linux或者Unix系统中,通过rm或者文件管理器删除文件将会从文件系统的文件夹结构上解除链接(unlink).然而假设文件是被 打开的(有一个进程正在使用),那么进程将仍然能够读取该文件,磁盘空间也一直被占用。而我删除的是oracle的告警log文件 删除的时候文件应该正在被使用 解决方法 首先获得一个已经被删除可是仍然被应用程序占用的文件列表,例如以下所看到的: [root@ticketb ~]# lsof |grep deleted oracle 12639 oracle 5w REG 253,0 648 215907 /home/oracle/admin/dbticb/udump/dbticb_ora_12637.trc (deleted) oracle 12639 oracle 6w REG 253,0 16749822091 215748 /home/oracle/admin/dbticb/bdump/alert_dbticb.log (deleted) oracle 12639 oracle 7u REG 253,0 0 36282 /home/oracle/oracle/product/10.2.0/db_1/dbs/lkinstdbticb (deleted) oracle 12639 oracle 8w REG 253,0 16749822091 215748 /home/oracle/admin/dbticb/bdump/alert_dbticb.log (deleted) oracle 12641 oracle 5w REG 253,0 648 215907 /home/oracle/admin/dbticb/udump/dbticb_ora_12637.trc (deleted) oracle 12641 oracle 6w REG 253,0 16749822091 215748 /home/oracle/admin/dbticb/bdump/alert_dbticb.log (deleted) 。 。 。 oracle 23492 oracle 6w REG 253,0 16749822091 215748 /home/oracle/admin/dbticb/bdump/alert_dbticb.log (deleted) oracle 23492 oracle 7u REG 253,0 0 36282 /home/oracle/oracle/product/10.2.0/db_1/dbs/lkinstdbticb (deleted) oracle 23492 oracle 8w REG 253,0 16749822091 215748 /home/oracle/admin/dbticb/bdump/alert_dbticb.log (deleted) oracle 23494 oracle 10u REG 253,0 0 36307 /home/oracle/oracle/product/10.2.0/db_1/dbs/lkinstrmandb (deleted) 从输出结果能够看到/home/oracle/admin/dbticb/bdump/alert_dbticb.log还被使用,未被释放空间 怎样让进程释放呢? 一种方法是kill掉相应的进程,或者停掉使用这个文件的应用,让os自己主动回收磁盘空间 我这个环境有非常多进程在使用的这个文件,停掉进程有点麻烦,再有就是风险非常大 当linux打开一个文件的时候,Linux内核会为每个进程在/proc/ 『/proc/nnnn/fd/文件夹(nnnn为pid)』建立一个以其pid 为名的文件夹用来保存进程的相关信息,而其子文件夹fd保存的是该进程打开的全部文件的fd(fd:file descriptor)。 kill进程是通过截断proc文件系统中的文件能够强制要求系统回收分配给正在使用的的文件。 这是一项高级技术,仅到管理员确定不会对执行中的进程造成影响时使用。应用程序对这样的方 式支持的并不好,当一个正在使用的文件被截断可能会引发不可预知的问题 所以我还是採用停应用来解决 restart oracle数据库,发现/home/oracle/admin/dbticb/bdump/alert_dbticb.log相应的空间被释放 在查看磁盘空间的使用情况,发现空间已经回收了 [root@ticketb ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/sda1 981M 203M 729M 22% / none 16G 0 16G 0% /dev/shm /dev/sda9 2.9G 37M 2.7G 2% /tmp /dev/sda7 4.9G 1.9G 2.7G 42% /usr /dev/sda8 2.9G 145M 2.6G 6% /var /dev/mapper/vghome-lvhome 20G 2.6G 16G 15% /home /dev/mapper/vgoradata-lvoradata 144G 48G 90G 35% /u01/oradata /dev/mapper/vgbackup-lvbackup 193G 7.8G 175G 5% /u01/backup ok,问题解决,然后做下收尾工作就可以 ------------------------------------------------------------------------------------------------- 学习下lsof命令 lsof全名list opened files,也就是列举系统中已经被打开的文件。我们都知道,linux环境中,不论什么事物都是文件, 设备是文件,文件夹是文件,甚至sockets也是文件。所以,用好lsof命令,对日常的linux管理非常有帮助。 lsof是linux最常常使用的命令之中的一个,通常的输出格式为: 引用 COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME 常见包含例如以下几个字段:许多其他的可见manual。 1、COMMAND 默认以9个字符长度显示的命令名称。可使用+c參数指定显示的宽度,若+c后跟的參数为零,则显示命令的全名 2、PID:进程的ID号 3、PPID 父进程的IP号,默认不显示,当使用-R參数可打开。 4、PGID 进程组的ID编号,默认也不会显示,当使用-g參数时可打开。 5、USER 命令的执行UID或系统中登陆的username称。默认显示为username,当使用-l參数时,可显示UID。 6、FD 是文件的File Descriptor number,或者例如以下的内容: (这里非常难翻译相应的意思,保留英文) 引用 cwd current working directory; Lnn library references (AIX); jld jail directory (FreeBSD); ltx shared library text (code and data); Mxx hex memory-mapped type number xx. m86 DOS Merge mapped file; mem memory-mapped file; mmap memory-mapped device; pd parent directory; rtd root directory; tr kernel trace file (OpenBSD); txt program text (code and data); v86 VP/ix mapped file; 文件的File Descriptor number显示模式有: 引用 r for read access; w for write access; u for read and write access; N for a Solaris NFS lock of unknown type; r for read lock on part of the file; R for a read lock on the entire file; w for a write lock on part of the file; W for a write lock on the entire file; u for a read and write lock of any length; U for a lock of unknown type; x for an SCO OpenServer Xenix lock on part of the file; X for an SCO OpenServer Xenix lock on the entire file; space if there is no lock. 7、TYPE 引用 IPv4 IPv4的包; IPv6 使用IPv6格式的包,即使地址是IPv4的,也会显示为IPv6,而映射到IPv6的地址; DIR 文件夹 LINK 链接文件 详情请看manual中许多其他的凝视。 8、DEVICE 使用character special、block special表示的设备号 9、SIZE 文件的大小,假设不能用大小表示的,会留空。使用-s參数控制。 10、NODE 本地文件的node码,或者协议,如TCP等 11、NAME 挂载点和文件的全路径(链接会被解析为实际路径),或者连接两方的地址和端口、状态等 常常使用演示例子: 1.显示开启文件/home/oracle/10.2.0/db_1/bin/tnslsnr的进程 [root@svr-db-test ~]# lsof /home/oracle/10.2.0/db_1/bin/tnslsnr COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME tnslsnr 3520 oracle txt REG 253,5 431062 11408866 /home/oracle/10.2.0/db_1/bin/tnslsnr 2.知道22端口如今执行什么程序 [root@svr-db-test ~]# lsof -i :22 COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME sshd 3101 root 3u IPv6 8670 TCP *:ssh (LISTEN) sshd 4545 root 3u IPv6 4237972 TCP 203.aibo.com:ssh->win-avbmq9e8ka7.gdgg.local:nsjtp-ctrl (ESTABLISHED) 3.显示init进程如今打开的文件 [root@svr-db-test ~]# lsof -c init COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME init 1 root cwd DIR 253,0 4096 2 / init 1 root rtd DIR 253,0 4096 2 / init 1 root txt REG 253,0 43496 524446 /sbin/init init 1 root mem REG 253,0 130448 917826 /lib64/ld-2.5.so init 1 root mem REG 253,0 1678480 917827 /lib64/libc-2.5.so init 1 root mem REG 253,0 23520 917686 /lib64/libdl-2.5.so init 1 root mem REG 253,0 247528 917844 /lib64/libsepol.so.1 init 1 root mem REG 253,0 95480 917845 /lib64/libselinux.so.1 init 1 root 10u FIFO 0,16 2311 /dev/initctl 4. 看进程号为1的进程打开了哪些文件 [root@svr-db-test ~]# lsof -p 1 COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME init 1 root cwd DIR 253,0 4096 2 / init 1 root rtd DIR 253,0 4096 2 / init 1 root txt REG 253,0 43496 524446 /sbin/init init 1 root mem REG 253,0 130448 917826 /lib64/ld-2.5.so init 1 root mem REG 253,0 1678480 917827 /lib64/libc-2.5.so init 1 root mem REG 253,0 23520 917686 /lib64/libdl-2.5.so init 1 root mem REG 253,0 247528 917844 /lib64/libsepol.so.1 init 1 root mem REG 253,0 95480 917845 /lib64/libselinux.so.1 init 1 root 10u FIFO 0,16 2311 /dev/initctl 5. 显示归属3520的进程情况 [root@svr-db-test ~]# lsof -g 3520 COMMAND PID PGID USER FD TYPE DEVICE SIZE NODE NAME tnslsnr 3520 3520 oracle cwd DIR 253,5 4096 11059201 /home/oracle tnslsnr 3520 3520 oracle rtd DIR 253,0 4096 2 / tnslsnr 3520 3520 oracle txt REG 253,5 431062 11408866 /home/oracle/10.2.0/db_1/bin/tnslsnr tnslsnr 3520 3520 oracle mem REG 253,0 130448 917826 /lib64/ld-2.5.so tnslsnr 3520 3520 oracle mem REG 253,0 1678480 917827 /lib64/libc-2.5.so tnslsnr 3520 3520 oracle mem REG 253,0 23520 917686 /lib64/libdl-2.5.so tnslsnr 3520 3520 oracle mem REG 253,0 615136 917834 /lib64/libm-2.5.so tnslsnr 3520 3520 oracle mem REG 253,0 141208 917829 /lib64/libpthread-2.5.so tnslsnr 3520 3520 oracle mem REG 253,0 109824 917839 /lib64/libnsl-2.5.so tnslsnr 3520 3520 oracle mem REG 253,5 20706622 11405436 /home/oracle/10.2.0/db_1/lib/libclntsh.so.10.1 tnslsnr 3520 3520 oracle mem REG 253,5 3803097 11410641 /home/oracle/10.2.0/db_1/lib/libnnz10.so tnslsnr 3520 3520 oracle mem REG 253,5 83493 11407251 /home/oracle/10.2.0/db_1/lib/libons.so tnslsnr 3520 3520 oracle mem REG 253,0 53880 917532 /lib64/libnss_files-2.5.so tnslsnr 3520 3520 oracle mem REG 253,5 8545 11407615 /home/oracle/10.2.0/db_1/lib/libskgxn2.so tnslsnr 3520 3520 oracle mem REG 253,5 513705 11410332 /home/oracle/10.2.0/db_1/lib/libocrutl10.so tnslsnr 3520 3520 oracle mem REG 253,5 636161 11410330 /home/oracle/10.2.0/db_1/lib/libocr10.so tnslsnr 3520 3520 oracle mem REG 253,5 657825 11410331 /home/oracle/10.2.0/db_1/lib/libocrb10.so tnslsnr 3520 3520 oracle mem REG 253,5 1745769 11410365 /home/oracle/10.2.0/db_1/lib/libhasgen10.so tnslsnr 3520 3520 oracle mem REG 253,5 61985 11410366 /home/oracle/10.2.0/db_1/lib/libclsra10.so tnslsnr 3520 3520 oracle 0u CHR 1,3 2553 /dev/null tnslsnr 3520 3520 oracle 1u CHR 1,3 2553 /dev/null tnslsnr 3520 3520 oracle 2u CHR 1,3 2553 /dev/null tnslsnr 3520 3520 oracle 3w REG 253,5 318853012 11633459 /home/oracle/10.2.0/db_1/network/log/listener.log tnslsnr 3520 3520 oracle 4r FIFO 0,6 15661 pipe tnslsnr 3520 3520 oracle 5r REG 253,5 11776 11410579 /home/oracle/10.2.0/db_1/network/mesg/nlus.msb tnslsnr 3520 3520 oracle 6r REG 253,5 46592 11407160 /home/oracle/10.2.0/db_1/network/mesg/tnsus.msb tnslsnr 3520 3520 oracle 7w FIFO 0,6 15662 pipe tnslsnr 3520 3520 oracle 8u IPv4 15665 TCP 203.aibo.com:ncube-lm (LISTEN) tnslsnr 3520 3520 oracle 9u unix 0xffff81021b7d6980 15666 /var/tmp/.oracle/s#3520.1 tnslsnr 3520 3520 oracle 10u unix 0xffff81021b7d66c0 15668 /var/tmp/.oracle/s#3520.2 6.按照文件夹/home/oracle来搜寻,但不会打开子文件夹,用来显示文件夹下被进程开启的文件 [root@svr-db-test ~]# lsof +d /home/oracle COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME tnslsnr 3520 oracle cwd DIR 253,5 4096 11059201 /home/oracle 7. 打开/home/oracle文件夹以及其子文件夹搜寻,用来显示文件夹下被进程开启的文件 [root@svr-db-test ~]# lsof +D /home/oracle 显示内容太多了,不显示了 8. lsof -i 用以显示符合条件的进程情况 语法: lsof -i[46] [protocol][@hostname|hostaddr][:service|port] 46 --> IPv4 or IPv6 protocol --> TCP or UDP hostname --> Internet host name hostaddr --> IPv4位置 service --> /etc/service中的 service name (能够不仅仅一个) port --> 端口号 (能够不仅仅一个) 例: [root@svr-db-test ~]# lsof -i tcp@192.168.2.245:1521 -n COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME oracle 15633 oracle 16u IPv4 4069605 TCP 192.168.2.203:31580->192.168.2.245:ncube-lm (ESTABLISHED) 或 [root@svr-db-test ~]# lsof -i tcp@192.168.2.245:1521 COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME oracle 15633 oracle 16u IPv4 4069605 TCP 203.aibo.com:31580->192.168.2.245:ncube-lm (ESTABLISHED) lsof -n 不将IP转换为hostname,缺省是不加上-n參数 9. 显示某用户的已经打开的文件(或该用户执行程序已经打开的文件) [root@svr-db-test ~]# lsof -u oracle或 [root@svr-db-test ~]# lsof -u 0 10. 仅打印进程,方便shell脚本调用 [root@svr-db-test ~]# lsof -tc sshd 3101 4545 关注: 进程调试命令:truss、strace和ltrace 进程无法启动,软件执行速度突然变慢,程序的"SegmentFault"等等都是让每个Unix系统用户头痛的问题,而这些问题都能够通过使用truss、strace和ltrace这三个常常使用的调试工具来高速诊断软件的"疑难杂症"。
在平时和开发的交流 以及 在论坛回答问题的或称中会发现这个问题被问及的频率非常高。 程序中报错: MySQL server has gone away 是什么意思? 如何避免? 因此,感觉有必要总结一下发生这个问题的原因。今天正好看到一篇外文blog总结的比较好,就翻译过来了 原文:http://ronaldbradford.com/blog/sqlstatehy000-general-error-2006-mysql-server-has-gone-away-2013-01-02/ 原因1. MySQL 服务宕了 判断是否属于这个原因的方法很简单,执行以下命令,查看mysql的运行时长 $ mysql -uroot -p -e "show global status like 'uptime';" +---------------+-------+ | Variable_name | Value | +---------------+-------+ | Uptime | 68928 | +---------------+-------+ 1 row in set (0.04 sec) 或者查看MySQL的报错日志,看看有没有重启的信息 $ tail /var/log/mysql/error.log 130101 22:22:30 InnoDB: Initializing buffer pool, size = 256.0M 130101 22:22:30 InnoDB: Completed initialization of buffer pool 130101 22:22:30 InnoDB: highest supported file format is Barracuda. 130101 22:22:30 InnoDB: 1.1.8 started; log sequence number 63444325509 130101 22:22:30 [Note] Server hostname (bind-address): '127.0.0.1'; port: 3306 130101 22:22:30 [Note] - '127.0.0.1' resolves to '127.0.0.1'; 130101 22:22:30 [Note] Server socket created on IP: '127.0.0.1'. 130101 22:22:30 [Note] Event Scheduler: Loaded 0 events 130101 22:22:30 [Note] /usr/sbin/mysqld: ready for connections. Version: '5.5.28-cll' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server (GPL) 如果uptime数值很大,表明mysql服务运行了很久了。说明最近服务没有重启过。 如果日志没有相关信息,也表名mysql服务最近没有重启过,可以继续检查下面几项内容。 2. 连接超时 如果程序使用的是长连接,则这种情况的可能性会比较大。 即,某个长连接很久没有新的请求发起,达到了server端的timeout,被server强行关闭。 此后再通过这个connection发起查询的时候,就会报错server has gone away $ mysql -uroot -p -e "show global variables like '%timeout';" +----------------------------+----------+ | Variable_name | Value | +----------------------------+----------+ | connect_timeout | 30 | | delayed_insert_timeout | 300 | | innodb_lock_wait_timeout | 50 | | innodb_rollback_on_timeout | OFF | | interactive_timeout | 28800 | | lock_wait_timeout | 31536000 | | net_read_timeout | 30 | | net_write_timeout | 60 | | slave_net_timeout | 3600 | | wait_timeout | 28800 | +----------------------------+----------+ mysql> SET SESSION wait_timeout=5; ## Wait 10 seconds mysql> SELECT NOW(); ERROR 2006 (HY000): MySQL server has gone away No connection. Trying to reconnect... Connection id: 132361 Current database: *** NONE *** +---------------------+ | NOW() | +---------------------+ | 2013-01-02 11:31:15 | +---------------------+ 1 row in set (0.00 sec) 3. 进程在server端被主动kill 这种情况和情况2相似,只是发起者是DBA或者其他job。发现有长时间的慢查询执行kill xxx导致。 $ mysql -uroot -p -e "show global status like 'com_kill'" +---------------+-------+ | Variable_name | Value | +---------------+-------+ | Com_kill | 0 | +---------------+-------+ 4. Your SQL statement was too large. 当查询的结果集超过 max_allowed_packet 也会出现这样的报错。定位方法是打出相关报错的语句。 用select * into outfile 的方式导出到文件,查看文件大小是否超过 max_allowed_packet ,如果超过则需要调整参数,或者优化语句。 mysql> show global variables like 'max_allowed_packet'; +--------------------+---------+ | Variable_name | Value | +--------------------+---------+ | max_allowed_packet | 1048576 | +--------------------+---------+ 1 row in set (0.00 sec) 修改参数: mysql> set global max_allowed_packet=1024*1024*16; mysql> show global variables like 'max_allowed_packet'; +--------------------+----------+ | Variable_name | Value | +--------------------+----------+ | max_allowed_packet | 16777216 | +--------------------+----------+ 1 row in set (0.00 sec)
1、复制概述 1.1、复制解决的问题 数据复制技术有以下一些特点: (1) 数据分布 (2) 负载平衡(load balancing) (3) 备份 (4) 高可用性(high availability)和容错 1.2、复制如何工作 从高层来看,复制分成三步: (1) master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events); (2) slave将master的binary log events拷贝到它的中继日志(relay log); (3) slave重做中继日志中的事件,将改变反映它自己的数据。 下图描述了这一过程: 该过程的第一部分就是master记录二进制日志。在每个事务更新数据完成之前,master在二日志记录这些改变。MySQL将事务串行的写入二进制日志,即使事务中的语句都是交叉执行的。在事件写入二进制日志完成后,master通知存储引擎提交事务。 下一步就是slave将master的binary log拷贝到它自己的中继日志。首先,slave开始一个工作线程——I/O线程。I/O线程在master上打开一个普通的连接,然后开始binlog dump process。Binlog dump process从master的二进制日志中读取事件,如果已经跟上master,它会睡眠并等待master产生新的事件。I/O线程将这些事件写入中继日志。 SQL slave thread处理该过程的最后一步。SQL线程从中继日志读取事件,更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于OS的缓存中,所以中继日志的开销很小。 此外,在master中也有一个工作线程:和其它MySQL的连接一样,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要的限制——复制在slave上是串行化的,也就是说master上的并行更新操作不能在slave上并行操作。 2、体验MySQL复制 MySQL开始复制是很简单的过程,不过,根据特定的应用场景,都会在基本的步骤上有一些变化。最简单的场景就是一个新安装的master和slave,从高层来看,整个过程如下: (1)在每个服务器上创建一个复制帐号; (2)配置master和slave; (3)Slave连接master开始复制。 2.1、创建复制帐号 每个slave使用标准的MySQL用户名和密码连接master。进行复制操作的用户会授予REPLICATION SLAVE权限。用户名的密码都会存储在文本文件master.info中。假如,你想创建repl用户,如下: mysql> GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* -> TO repl@'192.168.0.%' IDENTIFIED BY 'p4ssword'; 2.2、配置master 接下来对master进行配置,包括打开二进制日志,指定唯一的servr ID。例如,在配置文件加入如下值: [mysqld] log-bin=mysql-bin server-id=10 重启master,运行SHOW MASTER STATUS,输出如下: 2.3、配置slave Slave的配置与master类似,你同样需要重启slave的MySQL。如下: log_bin = mysql-bin server_id = 2 relay_log = mysql-relay-bin log_slave_updates = 1 read_only = 1 server_id是必须的,而且唯一。slave没有必要开启二进制日志,但是在一些情况下,必须设置,例如,如果slave为其它slave的master,必须设置bin_log。在这里,我们开启了二进制日志,而且显示的命名(默认名称为hostname,但是,如果hostname改变则会出现问题)。 relay_log配置中继日志,log_slave_updates表示slave将复制事件写进自己的二进制日志(后面会看到它的用处)。 有些人开启了slave的二进制日志,却没有设置log_slave_updates,然后查看slave的数据是否改变,这是一种错误的配置。所以,尽量使用read_only,它防止改变数据(除了特殊的线程)。但是,read_only并是很实用,特别是那些需要在slave上创建表的应用。 2.4、启动slave 接下来就是让slave连接master,并开始重做master二进制日志中的事件。你不应该用配置文件进行该操作,而应该使用CHANGE MASTER TO语句,该语句可以完全取代对配置文件的修改,而且它可以为slave指定不同的master,而不需要停止服务器。如下: mysql> CHANGE MASTER TO MASTER_HOST='server1', -> MASTER_USER='repl', -> MASTER_PASSWORD='p4ssword', -> MASTER_LOG_FILE='mysql-bin.000001', -> MASTER_LOG_POS=0; MASTER_LOG_POS的值为0,因为它是日志的开始位置。然后,你可以用SHOW SLAVE STATUS语句查看slave的设置是否正确: mysql> SHOW SLAVE STATUS\G *************************** 1. row *************************** Slave_IO_State: Master_Host: server1 Master_User: repl Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000001 Read_Master_Log_Pos: 4 Relay_Log_File: mysql-relay-bin.000001 Relay_Log_Pos: 4 Relay_Master_Log_File: mysql-bin.000001 Slave_IO_Running: No Slave_SQL_Running: No ...omitted... Seconds_Behind_Master: NULL Slave_IO_State, Slave_IO_Running, 和Slave_SQL_Running表明slave还没有开始复制过程。日志的位置为4而不是0,这是因为0只是日志文件的开始位置,并不是日志位置。实际上,MySQL知道的第一个事件的位置是4。 为了开始复制,你可以运行: mysql> START SLAVE; 运行SHOW SLAVE STATUS查看输出结果: mysql> SHOW SLAVE STATUS\G *************************** 1. row *************************** Slave_IO_State: Waiting for master to send event Master_Host: server1 Master_User: repl Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000001 Read_Master_Log_Pos: 164 Relay_Log_File: mysql-relay-bin.000001 Relay_Log_Pos: 164 Relay_Master_Log_File: mysql-bin.000001 Slave_IO_Running: Yes Slave_SQL_Running: Yes ...omitted... Seconds_Behind_Master: 0 注意,slave的I/O和SQL线程都已经开始运行,而且Seconds_Behind_Master不再是NULL。日志的位置增加了,意味着一些事件被获取并执行了。如果你在master上进行修改,你可以在slave上看到各种日志文件的位置的变化,同样,你也可以看到数据库中数据的变化。 你可查看master和slave上线程的状态。在master上,你可以看到slave的I/O线程创建的连接: mysql> show processlist \G *************************** 1. row *************************** Id: 1 User: root Host: localhost:2096 db: test Command: Query Time: 0 State: NULL Info: show processlist *************************** 2. row *************************** Id: 2 User: repl Host: localhost:2144 db: NULL Command: Binlog Dump Time: 1838 State: Has sent all binlog to slave; waiting for binlog to be updated Info: NULL 2 rows in set (0.00 sec) 行2为处理slave的I/O线程的连接。 在slave上运行该语句: mysql> show processlist \G *************************** 1. row *************************** Id: 1 User: system user Host: db: NULL Command: Connect Time: 2291 State: Waiting for master to send event Info: NULL *************************** 2. row *************************** Id: 2 User: system user Host: db: NULL Command: Connect Time: 1852 State: Has read all relay log; waiting for the slave I/O thread to update it Info: NULL *************************** 3. row *************************** Id: 5 User: root Host: localhost:2152 db: test Command: Query Time: 0 State: NULL Info: show processlist 3 rows in set (0.00 sec) 行1为I/O线程状态,行2为SQL线程状态。 2.5、从另一个master初始化slave 前面讨论的假设你是新安装的master和slave,所以,slave与master有相同的数据。但是,大多数情况却不是这样的,例如,你的master可能已经运行很久了,而你想对新安装的slave进行数据同步,甚至它没有master的数据。 此时,有几种方法可以使slave从另一个服务开始,例如,从master拷贝数据,从另一个slave克隆,从最近的备份开始一个slave。Slave与master同步时,需要三样东西: (1)master的某个时刻的数据快照; (2)master当前的日志文件、以及生成快照时的字节偏移。这两个值可以叫做日志文件坐标(log file coordinate),因为它们确定了一个二进制日志的位置,你可以用SHOW MASTER STATUS命令找到日志文件的坐标; (3)master的二进制日志文件。 可以通过以下几中方法来克隆一个slave: (1) 冷拷贝(cold copy) 停止master,将master的文件拷贝到slave;然后重启master。缺点很明显。 (2) 热拷贝(warm copy) 如果你仅使用MyISAM表,你可以使用mysqlhotcopy拷贝,即使服务器正在运行。 (3) 使用mysqldump 使用mysqldump来得到一个数据快照可分为以下几步: <1>锁表:如果你还没有锁表,你应该对表加锁,防止其它连接修改数据库,否则,你得到的数据可以是不一致的。如下: mysql> FLUSH TABLES WITH READ LOCK; <2>在另一个连接用mysqldump创建一个你想进行复制的数据库的转储: shell> mysqldump --all-databases --lock-all-tables >dbdump.db <3>对表释放锁。 mysql> UNLOCK TABLES; 3、深入复制 已经讨论了关于复制的一些基本东西,下面深入讨论一下复制。 3.1、基于语句的复制(Statement-Based Replication) MySQL 5.0及之前的版本仅支持基于语句的复制(也叫做逻辑复制,logical replication),这在数据库并不常见。master记录下改变数据的查询,然后,slave从中继日志中读取事件,并执行它,这些SQL语句与master执行的语句一样。 这种方式的优点就是实现简单。此外,基于语句的复制的二进制日志可以很好的进行压缩,而且日志的数据量也较小,占用带宽少——例如,一个更新GB的数据的查询仅需要几十个字节的二进制日志。而mysqlbinlog对于基于语句的日志处理十分方便。 但是,基于语句的复制并不是像它看起来那么简单,因为一些查询语句依赖于master的特定条件,例如,master与slave可能有不同的时间。所以,MySQL的二进制日志的格式不仅仅是查询语句,还包括一些元数据信息,例如,当前的时间戳。即使如此,还是有一些语句,比如,CURRENT USER函数,不能正确的进行复制。此外,存储过程和触发器也是一个问题。 另外一个问题就是基于语句的复制必须是串行化的。这要求大量特殊的代码,配置,例如InnoDB的next-key锁等。并不是所有的存储引擎都支持基于语句的复制。 3.2、基于记录的复制(Row-Based Replication) MySQL增加基于记录的复制,在二进制日志中记录下实际数据的改变,这与其它一些DBMS的实现方式类似。这种方式有优点,也有缺点。优点就是可以对任何语句都能正确工作,一些语句的效率更高。主要的缺点就是二进制日志可能会很大,而且不直观,所以,你不能使用mysqlbinlog来查看二进制日志。 对于一些语句,基于记录的复制能够更有效的工作,如: mysql> INSERT INTO summary_table(col1, col2, sum_col3) -> SELECT col1, col2, sum(col3) -> FROM enormous_table -> GROUP BY col1, col2; 假设,只有三种唯一的col1和col2的组合,但是,该查询会扫描原表的许多行,却仅返回三条记录。此时,基于记录的复制效率更高。 另一方面,下面的语句,基于语句的复制更有效: mysql> UPDATE enormous_table SET col1 = 0; 此时使用基于记录的复制代价会非常高。由于两种方式不能对所有情况都能很好的处理,所以,MySQL 5.1支持在基于语句的复制和基于记录的复制之前动态交换。你可以通过设置session变量binlog_format来进行控制。 3.3、复制相关的文件 除了二进制日志和中继日志文件外,还有其它一些与复制相关的文件。如下: (1)mysql-bin.index 服务器一旦开启二进制日志,会产生一个与二日志文件同名,但是以.index结尾的文件。它用于跟踪磁盘上存在哪些二进制日志文件。MySQL用它来定位二进制日志文件。它的内容如下(我的机器上): (2)mysql-relay-bin.index 该文件的功能与mysql-bin.index类似,但是它是针对中继日志,而不是二进制日志。内容如下: .\mysql-02-relay-bin.000017 .\mysql-02-relay-bin.000018 (3)master.info 保存master的相关信息。不要删除它,否则,slave重启后不能连接master。内容如下(我的机器上): I/O线程更新master.info文件,内容如下(我的机器上): .\mysql-02-relay-bin.000019 254 mysql-01-bin.000010 286 0 52813 (4)relay-log.info 包含slave中当前二进制日志和中继日志的信息。 3.4、发送复制事件到其它slave 当设置log_slave_updates时,你可以让slave扮演其它slave的master。此时,slave把SQL线程执行的事件写进行自己的二进制日志(binary log),然后,它的slave可以获取这些事件并执行它。如下: 3.5、复制过滤(Replication Filters) 复制过滤可以让你只复制服务器中的一部分数据,有两种复制过滤:在master上过滤二进制日志中的事件;在slave上过滤中继日志中的事件。如下: 4、复制的常用拓扑结构 复制的体系结构有以下一些基本原则: (1) 每个slave只能有一个master; (2) 每个slave只能有一个唯一的服务器ID; (3) 每个master可以有很多slave; (4) 如果你设置log_slave_updates,slave可以是其它slave的master,从而扩散master的更新。 MySQL不支持多主服务器复制(Multimaster Replication)——即一个slave可以有多个master。但是,通过一些简单的组合,我们却可以建立灵活而强大的复制体系结构。 4.1、单一master和多slave 由一个master和一个slave组成复制系统是最简单的情况。Slave之间并不相互通信,只能与master进行通信。如下: 如果写操作较少,而读操作很时,可以采取这种结构。你可以将读操作分布到其它的slave,从而减小master的压力。但是,当slave增加到一定数量时,slave对master的负载以及网络带宽都会成为一个严重的问题。 这种结构虽然简单,但是,它却非常灵活,足够满足大多数应用需求。一些建议: (1) 不同的slave扮演不同的作用(例如使用不同的索引,或者不同的存储引擎); (2) 用一个slave作为备用master,只进行复制; (3) 用一个远程的slave,用于灾难恢复; 4.2、主动模式的Master-Master(Master-Master in Active-Active Mode) Master-Master复制的两台服务器,既是master,又是另一台服务器的slave。如图: 主动的Master-Master复制有一些特殊的用处。例如,地理上分布的两个部分都需要自己的可写的数据副本。这种结构最大的问题就是更新冲突。假设一个表只有一行(一列)的数据,其值为1,如果两个服务器分别同时执行如下语句: 在第一个服务器上执行: mysql> UPDATE tbl SET col=col + 1; 在第二个服务器上执行: mysql> UPDATE tbl SET col=col * 2; 那么结果是多少呢?一台服务器是4,另一个服务器是3,但是,这并不会产生错误。 实际上,MySQL并不支持其它一些DBMS支持的多主服务器复制(Multimaster Replication),这是MySQL的复制功能很大的一个限制(多主服务器的难点在于解决更新冲突),但是,如果你实在有这种需求,你可以采用MySQL Cluster,以及将Cluster和Replication结合起来,可以建立强大的高性能的数据库平台。但是,可以通过其它一些方式来模拟这种多主服务器的复制。 4.3、主动-被动模式的Master-Master(Master-Master in Active-Passive Mode) 这是master-master结构变化而来的,它避免了M-M的缺点,实际上,这是一种具有容错和高可用性的系统。它的不同点在于其中一个服务只能进行只读操作。如图: 4.4、带从服务器的Master-Master结构(Master-Master with Slaves) 这种结构的优点就是提供了冗余。在地理上分布的复制结构,它不存在单一节点故障问题,而且还可以将读密集型的请求放到slave上。 主要参考:《High Performance MySQL》
1. 声明 #include <boost/shared_ptr.hpp> class UsersBitmap { ... } typedef boost::shared_ptr<UsersBitmap> UsersBitmapPtr; 2. 使用 UsersBitmapPtr login_users_; UsersBitmapPtr temp_login_users(new UsersBitmap()); //指向对象 login_users_.reset(new UsersBitmap()); //指针指向新的地方 login_users_.reset(); //指针置空 /////////////////////////////////////////////////////////////////////////// //////////////////////////////////// 虽然boost.shared_ptr是个非常好的东西,使用它可以使得c++程序不需要考虑内存释放的问题,但是还是有很多必须注意的地方。下面罗列了一些本人在实际工作中经常碰到的使用shared_ptr出问题的几种情况。 1. shared_ptr多次引用同一数据,如下: { int* pInt = new int[100]; boost::shared_ptr<int> sp1(pInt); // 一些其它代码之后… boost::shared_ptr<int> sp2(pInt); } 这种情况在实际中是很容易发生的,结果也是非常致命的,它会导致两次释放同一块内存,而破坏堆。 2.使用shared_ptr包装this指针带来的问题,如下: class tester { public: tester() ~tester() { std::cout << "析构函数被调用!\n"; } public: boost::shared_ptr<tester> sget() { return boost::shared_ptr<tester>(this); } }; int main() { tester t; boost::shared_ptr<tester> sp = t.sget(); // … return 0; } 也将导致两次释放t对象破坏堆栈,一次是出栈时析构,一次就是shared_ptr析构。若有这种需要,可以使用下面代码。 class tester : public boost::enable_shared_from_this<tester> { public: tester() ~tester() { std::cout << "析构函数被调用!\n"; } public: boost::shared_ptr<tester> sget() { return shared_from_this(); } }; int main() { boost::shared_ptr<tester> sp(new tester); // 正确使用sp 指针。 sp->sget(); return 0; } 3. shared_ptr循环引用导致内存泄露,代码如下: class parent; class child; typedef boost::shared_ptr<parent> parent_ptr; typedef boost::shared_ptr<child> child_ptr; class parent { public: ~parent() { std::cout <<"父类析构函数被调用.\n"; } public: child_ptr children; }; class child { public: ~child() { std::cout <<"子类析构函数被调用.\n"; } public: parent_ptr parent; }; int main() { parent_ptr father(new parent()); child_ptr son(new child); // 父子互相引用。 father->children = son; son->parent = father; return 0; } 如上代码,将在程序退出前,father的引用计数为2,son的计数也为2,退出时,shared_ptr所作操作就是简单的将计数减1,如果为0则释放,显然,这个情况下,引用计数不为0,于是造成father和son所指向的内存得不到释放,导致内存泄露。 4. 在多线程程序中使用shared_ptr应注意的问题。代码如下: class tester { public: tester() {} ~tester() {} // 更多的函数定义… }; void fun(boost::shared_ptr<tester> sp) { // !!!在这大量使用sp指针. boost::shared_ptr<tester> tmp = sp; } int main() { boost::shared_ptr<tester> sp1(new tester); // 开启两个线程,并将智能指针传入使用。 boost::thread t1(boost::bind(&fun, sp1)); boost::thread t2(boost::bind(&fun, sp1)); t1.join(); t2.join(); return 0; } 这个代码带来的问题很显然,由于多线程同是访问智能指针,并将其赋值到其它同类智能指针时,很可能发生两个线程同时在操作引用计数(但并不一定绝对发生),而导致计数失败或无效等情况,从而导致程序崩溃,如若不知根源,就无法查找这个bug,那就只能向上帝祈祷程序能正常运行。 可能一般情况下并不会写出上面这样的代码,但是下面这种代码与上面的代码同样,如下: class tester { public: tester() {} ~tester() {} public: boost::shared_ptr<int> m_spData; // 可能其它类型。 }; tester gObject; void fun(void) { // !!!在这大量使用sp指针. boost::shared_ptr<int> tmp = gObject.m_spData; } int main() { // 多线程。 boost::thread t1(&fun); boost::thread t2(&fun); t1.join(); t2.join(); return 0; } 情况是一样的。要解决这类问题的办法也很简单,使用boost.weak_ptr就可以很方便解决这个问题。第一种情况修改代码如下: class tester { public: tester() {} ~tester() {} // 更多的函数定义… }; void fun(boost::weak_ptr<tester> wp) { boost::shared_ptr<tester> sp = wp.lock; if (sp) { // 在这里可以安全的使用sp指针. } else { std::cout << “指针已被释放!” << std::endl; } } int main() { boost::shared_ptr<tester> sp1(new tester); boost.weak_ptr<tester> wp(sp1); // 开启两个线程,并将智能指针传入使用。 boost::thread t1(boost::bind(&fun, wp)); boost::thread t2(boost::bind(&fun, wp)); t1.join(); t2.join(); return 0; } boost.weak_ptr指针功能一点都不weak,weak_ptr是一种可构造、可赋值以不增加引用计数来管理shared_ptr的指针,它可以方便的转回到shared_ptr指针,使用weak_ptr.lock函数就可以得到一个shared_ptr的指针,如果该指针已经被其它地方释放,它则返回一个空的shared_ptr,也可以使用weak_ptr.expired()来判断一个指针是否被释放。 boost.weak_ptr不仅可以解决多线程访问带来的安全问题,而且还可以解决上面第三个问题循环引用。Children类代码修改如下,即可打破循环引用: class child { public: ~child() { std::cout <<"子类析构函数被调用.\n"; } public: boost::weak_ptr<parent> parent; }; 因为boost::weak_ptr不增加引用计数,所以可以在退出函数域时,正确的析构。
我们使用的大部分 PC 是基于 Intel 微处理器的 x86 和 x64 架构计算机. 因此, 我们面对的 windows 避免不了和 Intel 架构有些设计上的契合. 比如接下来要说到的内存管理. 为简单起见, 我们只讨论 x86 体系架构的内存管理. 不考虑换页文件影响. 进程的内存 图 1 (本图摘自 ref 2) 对于系统中的每一个进程而言, 都有 4GB 的 "内存空间". 也就是每个进程都认为自己有 4GB 的内存可以使用. 系统将每个进程的 4GB 地址空间, 从逻辑上划分为两大部分: a) 蓝色的是用户空间, 此空间是被用户程序所使用的. 比如我在代码中写 "分配 100MB 内存", 其实占用的就是这一部分. b) 红色的是内核空间, 此空间是被用作操作系统执行必要的线程切换以及从用户态函数进入内核态执行功能所保留的内存地址. 应用程序无法操作此区域. Intel x86 体系内存管理 Intel 规定, 一个在计算机内部, 可以使用 "分页机制" 对硬件内存进行 "虚拟化". 其核心技术如下图: 图 2 (本图修改自 ref 1) 首先, 在程序中的一个地址 0x1234, 5678 被计算机的页部件(硬件)经过 1,2,3 步, 从线性地址(程序中的地址) 转变为真正机器上的物理地址(即实际内存的硬件地址). 每个线性地址都被分成 "页目录索引(PDE, 10-bit)", "页表索引(PTE, 10-bit)", "页内偏移(offset, 12-bit)" 三部分. 1) 在页目录中根据 PDE 找到页表的位置, 即通过 0x48 找到 0xa000, 0000. 2) 根据页表中的 PTE 找到页地址, 即通过 0x345 找到 0x4000, 0000. 3) 根据偏移, 在页中找到我们要的具体地址, 即已知页位于 0x4000, 0000, 我们需要存取其 0x678 偏移处的数据, 则我们所需要操作的真是物理地址就是 0x4000, 0678. 基于 x86 的 Windows 内存管理 图 3 首先澄清两个概念: 1. 一个进程中的内存有三种分类, 空闲, 保留, 提交. 具体的含义可以在 图 3 中找到说明. 这三种类型的内存在某一时刻可能位于内存中, 也可能位于交换文件中. 2. 工作集定义: The working set of a process is the set of pages in the virtual address space of the process that are currently resident in physical memory. 即: 实际在物理内存中的大小. 结合实际系统, 以我家安装的 win8.1 为例, 打开任务管理器, 可见如下: 图 4 工作集(内存): 可以这么理解, 此值就是该进程所占用的总物理内存. 但是这个值是由两部分组成, 即 '专用工作集' + '共享工作集'. 内存(专用工作集): 这对于一个进程是最重要的, 它代表了一个进程独占用了多少内存. 内存(共享工作集): 这是该进程和别的进程共享的内存量. 通常, 这是加载一个 dll 所占用的内存. 提交大小: 属于 Committed 那一类. 但是不一定在物理内存中, 有些可能位于交换文件中. 如果有一个程序, 原本占 500MB 内存, 但是绝大多数内存都不使用, 则可以通过 `EmptyWorkingSet` 向操作系统发送请求, 将此进程的不常用的内容从物理内存中换出到换页文件中保存, 如下图: 图 5 写在最后 0. 工作集, 即在物理内存中的数据的集合. 1. 工作集 = 专用 + 共享 2. 将所有的 "工作集" 相加后的值会大于任务管理器中内存占用的百分比, 因为百分比对共享内存进行排重了. 3. "提交大小" 和 "工作集" 是两个层面的概念, 大部分活跃进程的 "工作集" 会大于 "提交大小", 而大部分非活跃的进程 "工作集" 会小于 "提交大小", 但是两者没有绝对关系. 4. 虚拟内存: 就是换页文件. references: 1. http://www.mouseos.com/arch/paging.html 2. Pushing the Limits of Windows: Virtual Memory
上次我们介绍了在单机、集群下高并发场景可以选择的一些方案,传送门:高并发场景之一般解决方案 但是也发现了一些问题,比如集群下使用ConcurrentQueue或加锁都不能解决问题,后来采用Redis队列也不能完全解决问题, 因为使用Redis要自己实现分布式锁 这次我们来了解一下一个专门处理队列的组件:RabbitMQ,这个东西天生支持分布式队列。 下面我们来用RabbitMQ来实现上一篇的场景 一、新建RabbitMQ.Receive private static ConnectionFactory factory = new ConnectionFactory { HostName = "192.168.1.109", UserName = "ljr", Password = "root", VirtualHost = "/" }; 1 static void Main(string[] args) 2 { 3 using (var connection = factory.CreateConnection()) 4 { 5 using (var channel = connection.CreateModel()) 6 { 7 var consumer = new EventingBasicConsumer(); 8 consumer.Received += (model, ea) => 9 { 10 var body = ea.Body; 11 var message = Encoding.UTF8.GetString(body); 12 Console.WriteLine(" [x] Received {0}", message); 13 14 var total = DbHelper.ExecuteScalar("Select Total from ConCurrency where Id = 1", null).ToString(); 15 var value = int.Parse(total) + 1; 16 17 DbHelper.ExecuteNonQuery(string.Format("Update ConCurrency Set Total = {0} where Id = 1", value.ToString()), null); 18 }; 19 20 channel.QueueDeclare(queue: "queueName", durable: false, exclusive: false, autoDelete: false, arguments: null); 21 channel.BasicConsume(queue: "queueName", noAck: true, consumer: consumer); 22 23 Console.WriteLine(" Press [enter] to exit."); 24 Console.ReadLine(); 25 } 26 } 27 } 二、新建RabbitMQ.Send 1 static void Main(string[] args) 2 { 3 for (int i = 1; i <= 500; i++) 4 { 5 Task.Run(async () => 6 { 7 await Produce(); 8 }); 9 10 Console.WriteLine(i); 11 } 12 13 Console.ReadKey(); 14 } 15 16 public static Task Produce() 17 { 18 return Task.Factory.StartNew(() => 19 { 20 using (var connection = factory.CreateConnection()) 21 { 22 using (var channel = connection.CreateModel()) 23 { 24 var body = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()); 25 channel.QueueDeclare(queue: "queueName", durable: false, exclusive: false, autoDelete: false, arguments: null); 26 channel.BasicPublish(exchange: "", routingKey: "queueName", basicProperties: null, body: body); 27 } 28 } 29 }); 30 } 这里是模拟500个用户请求,正常的话最后Total就等于500 我们来说试试看,运行程序 2.1、打开接收端 2.2 运行客户端 2.3、可以看到2边几乎是实时的,再去看看数据库 三、我们在集群里执行 最后数据是1000 完全没有冲突,好了,就是这样 、。
在很多软件公司,特别是一些创业型的团队中,对于这样的情景可能大家都很熟悉:项目经理或者产品经理(产品狗)口头或者简单记录一下软件产品的大致要做的功能,直接就让研发团队的兄弟(程序猿)去狂撸代码。然后他就去喝茶撩妹或者回家陪老婆了... 这种撸起袖子就开干的方式,看似简单高效,便于直接沟通,能够快速迭代。却不知,发现没有一份正规且实时更新的功能需求设计文档,会付出三四倍的代价来弥补。 最终会引发一场产品狗和程序猿之间的“猿狗大战”... WHY - 为什么需要功能需求设计说明书 在没有功能设计文档时,主要有如下几个问题: 前期研究团队沟通成本 如何要让团队里面的所有人员对软件产品的功能需求设计有一个共识?没有功能设计文档,反正我是想不出有什么办法。当该项目的团队人员越多,沟通成本就变得很高。 研发人员很容易有一个通病:以为自己了解了一小块需求就立即开始埋头狂撸......代码。最终很可能与项目经理和客户真正想要的功能相差甚远。 更可怕的,研发人员把数据库设计好了,代码也已经写得差不多了,这时产品狗突然跑到程序猿这,说我们的需求要做一点变化,大家都知道,“对产品狗来说那一点变化,可能会害得程序猿撸过几天几夜”。那很小的变更可能导致之前设计的数据库,码的代码都不能用了。对于程序猿没有什么比加班加点写了几个月的代码,最终被产品狗告知需求变了,代码要删除重新写更可怕的。估计只能用涨工资来安慰一下那受伤的心灵了。 还有一个比较隐藏的事情是,每个程序猿都认为自己写的代码很牛逼(其实对于大多数人这只是一个错觉,你写得代码并不优秀),不太愿意删除之前所写的东西,总是想在原有的代码基础上进行修改,让他们删除代码比杀了他还难。 作为公司的技术负责人,我每几天都会Code Review团队里面所有人的代码,一直要求他们把不用的代码去掉,但他们的应对方式总是加两个//。注释掉他们写的代码,而不是去做真正的删除动作。他们总有自己的理由,“这只是暂时注释掉,后面会用到”,但最终的结果是那些代码就像尸体一样,一直在那里,干扰着团队人员正常的思路。所以我只能强制性让他们那些“暂时没有用,以后会用到的代码”干掉 。 前期任务进度安排和分配 该文档也是任务进度安排和分配的重要依据。在没有功能需求设计文档之前的所有任务进度计划都是瞎扯淡,都不知道具体要做什么东西,哪能拿出合理的任务进度计划。如果你拿出来了,我也不相信那是经过认真分析做的进度计划,我知道那只是用来看领导看的。 中期产品经理需求变更 软件在开发过程中难免会遇到功能的需求变更,将程序猿们召集在一起把所有的变更讲一遍?当走出会议室的时候可能每个人都有自己的理解。下一场战争已悄然临近... 后期测试团队产品测试 测试团队应该在项目Kickoff之时就应该介入,而不是在产品开发完成之后。测试团队应该对功能需求设计文档充分了解,且以此来编写具体的测试用例文档。否则,只能是在界面上进行简单的表面测试,而真正的BUG并不在表面,这些BUG会藏得很深,等发现的时候可能已经造成很大的损失。测试团队想覆盖全部的测试用例此时已经相当困难,他们甚至都不知道产品有哪些功能。 测试用例应该尽可能详细,尽量保证测试用例走完能确保产品能上线发布。下图为我们在登录注册时用到的一部分用例: WHERE - 文档应该放在何处 功能说明文档一定要保持实时性,任何变更的需求,新增的需求都必须在该文档中体现。 一只产品狗(或一群)在编写完文档后,要发给项目经理、研发人员、销售人员、运营推广人员等人,如何保证每个人的文档都是最新的呢?如果通过QQ,邮件等方式,是不是每次更新都要重新通知所有人:“嘿,各位兄弟,文档作了一次修改,我给大家都重新发一份新的”。每个人电脑里面都有好几个版本的文档,时间长了,自己都忘记哪个文档是最新的;产品狗也记不清是否是所有相关的人都发了最新的文档。 研发人员可能会说通过SVN来作版本管理啊,给每个人分配一个帐号。“天啊,SVN是啥?”-销售人员、运营推广人员估计一脸懵逼。 更好的办法是通过团队实时协作的云端工具。从而实现分享和实时讨论,告别反复修改版本再发送邮件的麻烦。如果你会FQ,那你可以使用Google Docs、Office Online。否则你可以使用石墨文档、一起写。 WHAT - 什么是功能需求设计文档 & 应该包含那些内容 功能需求设计文档最重要的是描述产品所要包含的所有功能,越详细越好,可以结合产品的原型设计图来讲解。让项目所有相关人知道产品是什么,包含哪些页面,页面如何跳转等。 该文档是产品经理、项目经理、研发人员、销售人员、运营推广人员沟通的一个桥梁,一份好的功能需求设计文档是软件产品是否能成功的关键。 考虑是该文档的受众,这份文档不应该包含具体的编程技术上的说明。不管你是用C#/.NET、JAVA还是其它,这应该是另外研发团队内部使用的一份文档。 一般人第一反映就是去网上找一份功能需求设计文档模板,我个人感觉那些模板90%根本没有存在的必要。都太过形式化,不要没有实际意义和模板化的内容,只会使文档成为一个摆饰,反而是在浪费大家的时间。 那么一份合格的软件需求设计文档应该包括哪些内容呢? 项目背景 项目产生的实际背景、具体的运用场景、大致要解决什么样的问题、针对的阅读对象、版本修改记录、文档作者以及修改人信息。 详细的功能点描述 写明产品所包含的所有功能点,对功能、界面、接口的描述一定要充分详细,每处可以交互的地方都要给出具体的说明。再次强调,一定要详细描述每一个页面所拥有的功能。 产品不包含的功能点说明 除了写明产品所包含的所有功能点外,还应该写明软件所不包含的功能,这一点也很重要。 使用场景(画面感) 将复杂的业务逻辑融入到具体的使用场景中,更容易让项目经理、研发人员、销售人员、运营推广人员不同背景的人产生共识。 流程图 大家都知道“一图胜千言”,能用图说明的尽量用图来说明,只通过大量枯燥的文字可能效果并不太好。流程图是一种用图形表示逻辑和算法的工具,特别对研发人员撸代码很有帮助。 Windows用户可以使用Visio,Mac用户可以使用OmniGraffle,还可以使用免费在线作图,实时协作工具ProcessOn。 我之前就用ProcessOn画了一个设置了缓存的网络请求的流程图,这里作个参考: 人员角色“实例化” 跟上面提到的“画面感”相结合,将人员和角色能够实例化。比如我们的产品要实现如下功能,有两种表达方式: 医生给患者测量血压,并记录到系统中。 上海华山医院肾内科的王主任医生在给32号病区1号病床的患者刘阿姨测量血压,将测量到的血压100/70mmHg输入到透析管理系统。 哪种方式更便于理解?特别是对医疗知识不太了解的码农们。当然可能有人觉得第一种方式更简洁。可能是我举的例子不够好,也可能是我的理解能力不够强。(但不要怀疑我的智商!哈哈哈...) 结合产品原型设计图 产品原型设计图可以粗枝大叶地产品大致的框架。便于项目经理、研发人员、销售人员、运营推广人员等人在产品未开发之前对产品有一个相对直观的认识。没有一个原型图,想到这帮人拉到同一个频道沟通一定是不可能的事。(如果你做到了,那么赶紧把你的简历发我,我决定录用你!) 常用的原型设计工具有墨刀、Mockplus、Axure。 扯了这么多,来个例子吧。 本软件是给北京某医院集团肾内科透析患者所使用的软件,包括院内管理系统、院外大数据平台、医护端APP、患者端APP... 版本 作者 修订时间 审核人 v1.0.0 Charlie Chu 2017-2-12 Vivian Wong 使用场景一: 肾内科的医生王医生给31号病床刘阿姨进行透析上机操作,王医生在院内透析管理系统上点击上机操作,信息会传递到院外的大数据平台以及医护端APP、患者端APP上... 刘阿姨患者的家属登录到患者端APP后,可以实时查看刘阿姨透析过程中的所有信息,还可以查看血压、血糖、体重等历史数据... 当刘阿姨在家中通过蓝牙血压计测量血压时,自动同步到医院内部,如果刘阿姨的血压超过预先设置的值,院内的王医生则会在自己的手机上查看到刘阿姨的血压异常报警信息,王医生可以立即跟刘阿姨的家属进行实时沟通... ...<此处省略N字>... 本软件(v1.0.1版本)不包括的功能需求如下: 医生与患者的实时IM 医生排班设置 修改密码 患者积分 功能模块详细描述: 一、APP登录页面 由于本产品不存在患者自己注册的场景,所有的患者录入都发生在院外透析系统中,患者及家属在院外只需要输入相应的手机号,即可登录系统。 登录页面只有两个输入框,一个手机号,一个密码。 当用户要输入手机号时,手机应该弹出纯数字键盘,最多只能输入手机号固定的11位。密码最多输入10位。 当用户点击登录时,APP与后台服务器进行交互: 不输入手机号和密码,直接点击登录按钮,应该提示用户输入手机号和密码。 输入手机号但不输入密码,点击登录,提示“请输入密码”。 输入不正确的手机号,点击登录,应该提示“不存在该用户”。 输入小于11位的手机号,应该提示“请输入正确的手机号”。 二、登录后首页 下图是左侧是一个首页,右侧是一个点击透析预警的详细页面: 首页包括功能点: 资讯信息轮播 首页顶部资讯信息轮播功能,点击可以跳转到新的页面可以查看资讯详情。 病情咨询 点击“病情咨询”模块,患者查看向指定的医生了解自己的病情。 透析记录 点击透析记录,患者可以随时随地查看自己的过往透析记录。 食物速查 点击食物速查,可以查看所有类别的食物成份含量。 透析上下机实时信息列表 当患者在医院内进行透析上下机等操作时,会记录患者的透析上机时间 、下机时间等信息。点击其中的一条记录,跳转到透析详情页面,如上图右侧所示。 HOW - 如何保证文档质量 要保证文档能够实时更新同步,而不是疲于应付。那就是让大家都通过该文档来进行沟通,谁有问题直接去看文档,需求一旦变更首先就更新到文档。 研发人员严格按文档上的描述来开发,在没有文档之前,对不起,拒绝开发!任何口头、QQ或邮件上的新的功能需求一概不理!提前是产品狗要比较给力,否则老板还是会让你狂撸代码...
转载请注明出处:http://www.cnblogs.com/4----/p/6549865.html 0.目录 RabbitMQ-从基础到实战(1)— Hello RabbitMQ RabbitMQ-从基础到实战(2)— 防止消息丢失 RabbitMQ-从基础到实战(4)— 消息的交换(中) RabbitMQ-从基础到实战(5)— 消息的交换(下) RabbitMQ-从基础到实战(6)— 与Spring集成 1.简介 在前面的例子中,每个消息都只对应一个消费者,即使有多个消费者在线,也只会有一个消费者接收并处理一条消息,这是消息中间件的一种常用方式。 另外一种方式,生产者生产一条消息,广播给一个或多个队列,所有订阅了这个队列的消费者,都可以消费这条消息,这就是消息订阅。 官方教程列举了这样一个场景,生产者发出一条记录日志的消息,消费者1接收到后写日志到硬盘,消费者2接收到后打印日志到屏幕。工作中还有很多这样的场景有待发掘,适当的使用消息订阅后可以成倍的增加效率。 2.RabbitMQ的交换中心(Exchange) 在前两章的例子中,我们涉及到了三个概念 生产者 队列 消费者 这不禁让我们以为,生产者生产消息后直接到发送到队列,消费者从队列中获取消息,再消费掉。 其实这是错误的,在RabbitMQ中,生产者不会直接把消息发送给队列,实际上,生产者甚至不知道一条消息会不会被发送到队列上。 正确的概念是,生产者会把消息发送给RabbitMQ的交换中心(Exchange),Exchange的一侧是生产者,另一侧则是一个或多个队列,由Exchange决定一条消息的生命周期--发送给某些队列,或者直接丢弃掉。 这个概念在官方文档中被称作RabbitMQ消息模型的核心思想(core idea) 如下图,其中X代表的是Exchange。 RabbitMQ中,有4种类型的Exchange direct 通过消息的routing key比较queue的key,相等则发给该queue,常用于相同应用多实例之间的任务分发 默认类型 本身是一个direct类型的exchange,routing key自动设置为queue name。注意,direct不等于默认类型,默认类型是在queue没有指定exchange时的默认处理方式,发消息时,exchange字段也要相应的填成空字符串“” topic 话题,通过可配置的规则分发给绑定在该exchange上的队列,通过地理位置推送等场景适用 headers 当分发规则很复杂,用routing key不好表达时适用,忽略routing key,用header取代之,header可以为非字符串,例如Integer或者String fanout 分发给所有绑定到该exchange上的队列,忽略routing key,适用于MMO游戏、广播、群聊等场景 更详细的介绍,请看官方文档 3.临时队列 可以对一个队列命名是十分重要的,在消费者消费消息时,要指明消费哪个队列的消息(下面的queue),这样就可以让多个消费者同时分享一个队列 String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException; 上述记录日志的场景中,有以下几个特点 所有消费者都需要监听所有的日志消息,因此每个消费者都需要一个单独的队列,不需要和别人分享 消费者只关心最新的消息,连接到RabbitMQ之前的消息不需要关心,因此,每次连接时需要创建一个队列,绑定到相应的exchange上,连接断开后,删除该队列 自己声明队列是比较麻烦的,因此,RabbitMQ提供了简便的获取临时队列的方法,该队列会在连接断开后销毁 String queueName = channel.queueDeclare().getQueue(); 这行代码会获取一个名字类似于“amq.gen-JzTY20BRgKO-HjmUJj0wLg”的临时队列 4.绑定 再次注意,在RabbitMQ中,消息是发送到Exchange的,不是直接发送的Queue。因此,需要把Queue和Exchange进行绑定,告诉RabbitMQ把指定的Exchange上的消息发送的这个队列上来 绑定队列使用此方法 Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException; 其中,queue是队列名,exchange是要绑定的交换中心,routingKey就是这个queue的routingKey 5.实践 下面来实现上述场景,生产者发送日志消息,消费者1记录日志,消费者2打印日志 下面的代码中,把连接工厂等方法放到了构造函数中,也就是说,每new一个对象,都会创建一个连接,在生产环境这样做是很浪费性能的,每次创建一个connection都会建立一次TCP连接,生产环境应使用连接池。而Channel又不一样,多个Channel是共用一个TCP连接的,因此可以放心的获取Channel(本结论出自官方文档对Channel的定义) AMQP 0-9-1 connections are multiplexed with channels that can be thought of as "lightweight connections that share a single TCP connection". For applications that use multiple threads/processes for processing, it is very common to open a new channel per thread/process and not share channels between them. 日志消息发送类 LogSender 1 import java.io.IOException; 2 import java.util.concurrent.TimeoutException; 3 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 7 import com.rabbitmq.client.Channel; 8 import com.rabbitmq.client.Connection; 9 import com.rabbitmq.client.ConnectionFactory; 10 11 public class LogSender { 12 13 private Logger logger = LoggerFactory.getLogger(LogSender.class); 14 private ConnectionFactory factory; 15 private Connection connection; 16 private Channel channel; 17 18 /** 19 * 在构造函数中获取连接 20 */ 21 public LogSender(){ 22 super(); 23 try { 24 factory = new ConnectionFactory(); 25 factory.setHost("127.0.0.1"); 26 connection = factory.newConnection(); 27 channel = connection.createChannel(); 28 } catch (Exception e) { 29 logger.error(" [X] INIT ERROR!",e); 30 } 31 } 32 /** 33 * 提供个关闭方法,现在并没有什么卵用 34 * @return 35 */ 36 public boolean closeAll(){ 37 try { 38 this.channel.close(); 39 this.connection.close(); 40 } catch (IOException | TimeoutException e) { 41 logger.error(" [X] CLOSE ERROR!",e); 42 return false; 43 } 44 return true; 45 } 46 47 /** 48 * 我们更加关心的业务方法 49 * @param message 50 */ 51 public void sendMessage(String message) { 52 try { 53 //声明一个exchange,命名为logs,类型为fanout 54 channel.exchangeDeclare("logs", "fanout"); 55 //exchange是logs,表示发送到此Exchange上 56 //fanout类型的exchange,忽略routingKey,所以第二个参数为空 57 channel.basicPublish("logs", "", null, message.getBytes()); 58 logger.debug(" [D] message sent:"+message); 59 } catch (IOException e) { 60 e.printStackTrace(); 61 } 62 } 63 } 在LogSender中,和之前的例子不一样的地方是,我们没有直接声明一个Queue,取而代之的是声明了一个exchange 发布消息时,第一个参数填了我们声明的exchange名字,routingKey留空,因为fanout类型忽略它。 在前面的例子中,我们routingKey填的是队列名,因为默认的exchange(exchange位空字符串时使用默认交换中心)会把队列的routingKey设置为queueName(声明队列的时候设置的,不是发送消息的时候),又是direct类型,所以可以通过queueName当做routingKey找到队列。 消费类 LogConsumer 1 package com.liyang.ticktock.rabbitmq; 2 3 import java.io.IOException; 4 import java.util.concurrent.TimeoutException; 5 6 import org.slf4j.Logger; 7 import org.slf4j.LoggerFactory; 8 9 import com.rabbitmq.client.AMQP; 10 import com.rabbitmq.client.Channel; 11 import com.rabbitmq.client.Connection; 12 import com.rabbitmq.client.ConnectionFactory; 13 import com.rabbitmq.client.Consumer; 14 import com.rabbitmq.client.DefaultConsumer; 15 import com.rabbitmq.client.Envelope; 16 17 public class LogConsumer { 18 19 private Logger logger = LoggerFactory.getLogger(LogConsumer.class); 20 private ConnectionFactory factory; 21 private Connection connection; 22 private Channel channel; 23 24 /** 25 * 在构造函数中获取连接 26 */ 27 public LogConsumer() { 28 super(); 29 try { 30 factory = new ConnectionFactory(); 31 factory.setHost("127.0.0.1"); 32 connection = factory.newConnection(); 33 channel = connection.createChannel(); 34 // 声明exchange,防止生产者没启动,exchange不存在 35 channel.exchangeDeclare("logs","fanout"); 36 } catch (Exception e) { 37 logger.error(" [X] INIT ERROR!", e); 38 } 39 } 40 41 /** 42 * 提供个关闭方法,现在并没有什么卵用 43 * 44 * @return 45 */ 46 public boolean closeAll() { 47 try { 48 this.channel.close(); 49 this.connection.close(); 50 } catch (IOException | TimeoutException e) { 51 logger.error(" [X] CLOSE ERROR!", e); 52 return false; 53 } 54 return true; 55 } 56 57 /** 58 * 我们更加关心的业务方法 59 */ 60 public void consume() { 61 try { 62 // 获取一个临时队列 63 String queueName = channel.queueDeclare().getQueue(); 64 // 把刚刚获取的队列绑定到logs这个交换中心上,fanout类型忽略routingKey,所以第三个参数为空 65 channel.queueBind(queueName, "logs", ""); 66 //定义一个Consumer,消费Log消息 67 Consumer consumer = new DefaultConsumer(channel) { 68 @Override 69 public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, 70 byte[] body) throws IOException { 71 String message = new String(body, "UTF-8"); 72 logger.debug(" [D] 我是来打印日志的:"+message); 73 } 74 }; 75 //这里自动确认为true,接收到消息后该消息就销毁了 76 channel.basicConsume(queueName, true, consumer); 77 } catch (IOException e) { 78 e.printStackTrace(); 79 } 80 } 81 } 复制一个项目,把72行改为如下代码,代表两个做不同工作的消费者 1 logger.debug(" [D] 我已经把消息写到硬盘了:"+message); 消费者App 1 public class App 2 { 3 public static void main( String[] args ) 4 { 5 LogConsumer consumer = new LogConsumer(); 6 consumer.consume(); 7 } 8 } 生产者App 1 public class App { 2 public static void main( String[] args ) throws InterruptedException{ 3 LogSender sender = new LogSender(); 4 while(true){ 5 sender.sendMessage(System.nanoTime()+""); 6 Thread.sleep(1000); 7 } 8 } 9 } 把消费者打包成两个可执行的jar包,方便观察控制台 用java -jar 命令执行,结果如下 6.结束语 本章介绍了RabbitMQ中消息的交换,再次强调,RabbitMQ中,消息是通过交换中心转发到队列的,不要被默认的exchange混淆,默认的exchange会自动把queue的名字设置为它的routingKey,所以消息发布时,才能通过queueName找到该队列,其实此时queueName扮演的角色就是routingKey。 本教程是参考官方文档写出来的,后续章节会介绍更多RabbitMQ的相关知识以及项目中的实战技巧
转载请注明出处 0.目录 RabbitMQ-从基础到实战(1)— Hello RabbitMQ RabbitMQ-从基础到实战(3)— 消息的交换(上) RabbitMQ-从基础到实战(4)— 消息的交换(中) RabbitMQ-从基础到实战(5)— 消息的交换(下) RabbitMQ-从基础到实战(6)— 与Spring集成 1.简介 RabbitMQ中,消息丢失可以简单的分为两种:客户端丢失和服务端丢失。针对这两种消息丢失,RabbitMQ都给出了相应的解决方案。 2.防止客户端丢失消息 如图,生产者P向队列中生产消息,C1和C2消费队列中的消息,默认情况下,RabbitMQ会平均的分发消费给C1C2(Round-robin dispatching),假设一个任务的执行时间非常长,在执行过程中,客户端挂了(连接断开),那么,该客户端正在处理且未完成的消息,以及分配给它还没来得及执行的消息,都将丢失。因为默认情况下,RabbitMQ分发完消息后,就会从内存中把消息删除掉。 3.消息确认(Message acknowledgment) 为了解决上述问题,RabbitMQ引入了消息确认机制,当消息处理完成后,给Server端发送一个确认消息,来告诉服务端可以删除该消息了,如果连接断开的时候,Server端没有收到消费者发出的确认信息,则会把消息转发给其他保持在线的消费者。 验证上述问题 首先,我们验证上述问题(客户端丢失消息)是否真的存在,对Consumer进行如下改造。 先生产两条消息 启动消费者,在消费者接收到消息,还没处理完成的时候,强制关掉 这时,观察控制台,发现两条消息都没有了,1条是在执行中丢失的,还有1条,已经分配给这个Consumer,还没来得及处理,也丢失了 这证明了上述问题是真的存在的,如果发生在生产环境,将产生难以预料的后果 引入消息确认机制 为了方便观察,我们用CMD来运行Consumer,要通过maven打成可执行的JAR包,需要在pom.xml中增加如下配置 <build> <finalName>Consumer</finalName> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.liyang.ticktock.rabbitmq.App</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>assembly</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> 上述配置描述了最终打包名字、入口类路径、带上依赖包、使用1.8版本的JDK进行打包,配置完后,就可以通过maven的install方法,在target目录生成可执行的jar包,如果包大小很小,应检查配置,是不是没有带上依赖包 再次改造Consummer类 install成可执行jar包,通过cmd开启两个consumer 通过Sender发送一条消息,然后用Ctrl+C结束先收到消息的Consumer,发现另外一个Consumer接收到了未处理完的消息 问题得到了解决,现在消费者在执行过程中死掉也不会丢失消息了 看一下发送确认的方法 1 /** 2 * Acknowledge one or several received 3 * messages. Supply the deliveryTag from the {@link com.rabbitmq.client.AMQP.Basic.GetOk} 4 * or {@link com.rabbitmq.client.AMQP.Basic.Deliver} method 5 * containing the received message being acknowledged. 6 * @see com.rabbitmq.client.AMQP.Basic.Ack 7 * @param deliveryTag the tag from the received 这个是RabbitMQ用来区分消息的,文档在这 8 * @param multiple true to acknowledge all messages up to and 为true的话,确认所有消息,为false只确认当前消息 9 * including the supplied delivery tag; false to acknowledge just 10 * the supplied delivery tag. 11 * @throws java.io.IOException if an error is encountered 12 */ 13 void basicAck(long deliveryTag, boolean multiple) throws IOException; 在官方文档中,这样描述deliveryTag 简单来说,就是RabbitMQ内部用来区分消息的一个标签,从envelope中获取就行了 忘记确认将引起内存泄漏 RabbitMQ只有在收到消费者确认后,才会从内存中删除消息,如果消费者忘了确认(更多情况是因为代码问题没有执行到确认的代码),将会导致内存泄漏 验证一下 注释掉Consumer中的确认代码 运行Sender和Consumer,不停的生产消费消息,发现消费者在正常的消费消息 查看控制台,发现已经被吃掉了43KB的内存,所以,在试用过程中,一定要保证消息确认在任何情况下都可以发出,否则即使消费者处理完成,RabbitMQ也不会把消息在内存中清除,在该消费者断开连接之后,还会把消息转发给其他消费者重新处理,将引发难以预计的问题 4.消息的持久化 现在,消费者宕机已经无法影响到我们的消息了,但如果RabbitMQ重启了,消息依然会丢失。所幸的是,RabbitMQ提供了持久化的机制,将内存中的消息持久化到硬盘上,即使重启RabbitMQ,消息也不会丢失。但是,仍然有一个非常短暂的时间窗口(RabbitMQ收到消息还没来得及存到硬盘上)会导致消息丢失,如果需要严格的控制,可以参考官方文档 要使用RabbitMQ的消息持久化,在声明队列时设置一个参数即可 注意,RabbitMQ不允许对一个已经存在的队列用不同的参数重新声明,对于试图这么做的程序,会报错,所以,改动之前代码之前,要在控制台中把原来的队列删除 重新声明队列后,发现Durable为true 重启RabbitMQ 队列的消息没有丢失 5.结束语 这一章介绍了RabbitMQ消息的确认和持久化,后面将会继续深入介绍RabbitMQ的其他特性 http://www.cnblogs.com/4----/p/6526033.html
转载请注明出处:http://www.cnblogs.com/4----/p/6518801.html 0.目录 RabbitMQ-从基础到实战(2)— 防止消息丢失 RabbitMQ-从基础到实战(3)— 消息的交换(上) RabbitMQ-从基础到实战(4)— 消息的交换(中) RabbitMQ-从基础到实战(5)— 消息的交换(下) RabbitMQ-从基础到实战(6)— 与Spring集成 1.简介 本篇博文介绍了在windows平台下安装RabbitMQ Server端,并用JAVA代码实现收发消息 2.安装RabbitMQ RabbitMQ是用Erlang开发的,所以需要先安装Erlang环境,在这里下载对应系统的Erlang安装包进行安装 点击这里下载对应平台的RabbitMQ安装包进行安装 Windows平台安装完成后如图 3.启用RabbitMQ Web控制台 RabbitMQ提供一个控制台,用于管理和监控RabbitMQ,默认是不启动的,需要运行以下命令进行启动 点击上图的Rabbit Command Prompt,打开rabbitMQ控制台 在官方介绍管理控制台的页面,可以看到,输入以下命令启动后台控制插件 rabbitmq-plugins enable rabbitmq_management 登录后台页面:http://localhost:15672/ 密码和用户名都是 guest ,界面如下 目前可以先不用理会此界面,后面使用到时会详细介绍,也可以到这里查看官方文档。 4.编写MessageSender Spring对RabbitMQ已经进行了封装,正常使用中,会使用Spring集成,第一个项目中,我们先不考虑那么多 在IDE中新建一个Maven项目,并在pom.xml中贴入如下依赖,RabbitMQ的最新版本依赖可以在这里找到 <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>4.1.0</version> </dependency> 等待Maven下载完成后,就可以在Maven Dependencies中看到RabbitMQ的JAR 在这里,我们发现,RabbitMQ的日志依赖了slf4j-api这个包,slf4j-api并不是一个日志实现,这样子是打不出日志的,所以,我们给pom加上一个日志实现,这里用了logback <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.1</version> </dependency> 之后maven依赖如下,可以放心写代码了 新建一个MessageSender类,代码如下 1 import java.io.IOException; 2 import java.util.concurrent.TimeoutException; 3 4 import org.slf4j.Logger; 5 import org.slf4j.LoggerFactory; 6 7 import com.rabbitmq.client.Channel; 8 import com.rabbitmq.client.Connection; 9 import com.rabbitmq.client.ConnectionFactory; 10 11 public class MessageSender { 12 13 private Logger logger = LoggerFactory.getLogger(MessageSender.class); 14 15 //声明一个队列名字 16 private final static String QUEUE_NAME = "hello"; 17 18 public boolean sendMessage(String message){ 19 //new一个RabbitMQ的连接工厂 20 ConnectionFactory factory = new ConnectionFactory(); 21 //设置需要连接的RabbitMQ地址,这里指向本机 22 factory.setHost("127.0.0.1"); 23 Connection connection = null; 24 Channel channel = null; 25 try { 26 //尝试获取一个连接 27 connection = factory.newConnection(); 28 //尝试创建一个channel 29 channel = connection.createChannel(); 30 //这里的参数在后面详解 31 channel.queueDeclare(QUEUE_NAME, false, false, false, null); 32 //注意这里调用了getBytes(),发送的其实是byte数组,接收方收到消息后,需要重新组装成String 33 channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); 34 logger.info("Sent '" + message + "'"); 35 //关闭channel和连接 36 channel.close(); 37 connection.close(); 38 } catch (IOException | TimeoutException e) { 39 //失败后记录日志,返回false,代表发送失败 40 logger.error("send message faild!",e); 41 return false; 42 } 43 return true; 44 } 45 } 然后在App类的main方法中调用sendMessage 1 public class App { 2 public static void main( String[] args ){ 3 MessageSender sender = new MessageSender(); 4 sender.sendMessage("hello RabbitMQ!"); 5 } 6 } 打印日志如下 打开RabbitMQ的控制台,可以看到消息已经进到了RabbitMQ中 点进去,用控制台自带的getMessage功能,可以看到消息已经成功由RabbitMQ管理了 至此,MessageSender已经写好了,在该类的31和33行,我们分别调用了队列声明和消息发送 channel.queueDeclare(QUEUE_NAME, false, false, false, null); channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); queueDeclare,有很多参数,我们可以看一下他的源码,注释上有详细的解释,我简单翻译了一下 1 /** 2 * Declare a queue 声明一个队列 3 * @see com.rabbitmq.client.AMQP.Queue.Declare 4 * @see com.rabbitmq.client.AMQP.Queue.DeclareOk 5 * @param queue the name of the queue队列的名字 6 * @param durable true if we are declaring a durable queue (the queue will survive a server restart)是否持久化,为true则在rabbitMQ重启后生存 7 * @param exclusive true if we are declaring an exclusive queue (restricted to this connection)是否是排他性队列(别人看不到),只对当前连接有效,当前连接断开后,队列删除(设置了持久化也删除) 8 * @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)自动删除,在最后一个连接断开后删除队列 9 * @param arguments other properties (construction arguments) for the queue 其他参数 10 * @return a declaration-confirm method to indicate the queue was successfully declared 11 * @throws java.io.IOException if an error is encountered 12 */ 13 Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, 14 Map<String, Object> arguments) throws IOException; 前面4个都非常好理解,最后一个“其他参数”,到底是什么其他参数,这个东西真的很难找,用到再解释吧,官方文档如下 TTL Time To Live 存活时间 Dead Lettering 遗言,当消息死亡时,做些什么 Length Limit 长度限制 Priority Queues 优先级 basicPublish的翻译如下 1 /** 2 * Publish a message.发送一条消息 3 * 4 * Publishing to a non-existent exchange will result in a channel-level 5 * protocol exception, which closes the channel. 6 * 7 * Invocations of <code>Channel#basicPublish</code> will eventually block if a 8 * <a href="http://www.rabbitmq.com/alarms.html">resource-driven alarm</a> is in effect. 9 * 10 * @see com.rabbitmq.client.AMQP.Basic.Publish 11 * @see <a href="http://www.rabbitmq.com/alarms.html">Resource-driven alarms</a> 12 * @param exchange the exchange to publish the message to 交换模式,会在后面讲,官方文档在这里 13 * @param routingKey the routing key 控制消息发送到哪个队列 14 * @param props other properties for the message - routing headers etc 其他参数 15 * @param body the message body 消息,byte数组 16 * @throws java.io.IOException if an error is encountered 17 */ 18 void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException; 这里又有个其他参数,它的类型是这样的,设置消息的一些详细属性 5.编写MessageConsumer 为了和Sender区分开,新建一个Maven项目MessageConsumer 1 package com.liyang.ticktock.rabbitmq; 2 3 import java.io.IOException; 4 import java.util.concurrent.TimeoutException; 5 6 import org.slf4j.Logger; 7 import org.slf4j.LoggerFactory; 8 9 import com.rabbitmq.client.AMQP; 10 import com.rabbitmq.client.Channel; 11 import com.rabbitmq.client.Connection; 12 import com.rabbitmq.client.ConnectionFactory; 13 import com.rabbitmq.client.Consumer; 14 import com.rabbitmq.client.DefaultConsumer; 15 import com.rabbitmq.client.Envelope; 16 17 public class MessageConsumer { 18 19 private Logger logger = LoggerFactory.getLogger(MessageConsumer.class); 20 21 public boolean consume(String queueName){ 22 //连接RabbitMQ 23 ConnectionFactory factory = new ConnectionFactory(); 24 factory.setHost("127.0.0.1"); 25 Connection connection = null; 26 Channel channel = null; 27 try { 28 connection = factory.newConnection(); 29 channel = connection.createChannel(); 30 //这里声明queue是为了取消息的时候,queue肯定会存在 31 //注意,queueDeclare是幂等的,也就是说,消费者和生产者,不论谁先声明,都只会有一个queue 32 channel.queueDeclare(queueName, false, false, false, null); 33 34 //这里重写了DefaultConsumer的handleDelivery方法,因为发送的时候对消息进行了getByte(),在这里要重新组装成String 35 Consumer consumer = new DefaultConsumer(channel){ 36 @Override 37 public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) 38 throws IOException { 39 String message = new String(body, "UTF-8"); 40 logger.info("Received '" + message + "'"); 41 } 42 }; 43 //上面是声明消费者,这里用声明的消费者消费掉队列中的消息 44 channel.basicConsume(queueName, true, consumer); 45 46 //这里不能关闭连接,调用了消费方法后,消费者会一直连接着rabbitMQ等待消费 47 48 } catch (IOException | TimeoutException e) { 49 //失败后记录日志,返回false,代表消费失败 50 logger.error("send message faild!",e); 51 return false; 52 } 53 54 55 return true; 56 } 57 } 然后在App的main方法中调用Cunsumer进行消费 1 public class App 2 { 3 //这个队列名字要和生产者中的名字一样,否则找不到队列 4 private final static String QUEUE_NAME = "hello"; 5 6 public static void main( String[] args ) 7 { 8 MessageConsumer consumer = new MessageConsumer(); 9 consumer.consume(QUEUE_NAME); 10 } 11 } 结果如下,消费者一直在等待消息,每次有消息进来,就会立刻消费掉 6.多个消费者同时消费一个队列 改造一下Consumer 在App中new多个消费者 改造Sender,使它不停的往RabbitMQ中发送消息 启动Sender 启动Consumer,发现消息很平均的发给四个客户端,一人一个,谁也不插队 如果我们把速度加快呢?把Sender的休息时间去掉,发现消费开始变得没有规律了,其实呢,它还是有规律的,这个是RabbitMQ的特性,称作“Round-robin dispatching”,消息会平均的发送给每一个消费者,可以看第一第二行,消息分别是56981和56985,相应的82、82、84都被分给了其他线程,只是在当前线程的时间片内,可以处理这么多任务,所以就一次打印出来了 7.结束语 这一章介绍了从安装到用JAVA语言编写生产者与消费者,在这里只是简单的消费消息并打印日志,如果一个消息需要处理的时间很长,而处理的过程中,这个消费者挂掉了,那消息会不会丢失呢?答案是肯定的,而且已经分配给这个消费者,但还没来得及处理的消息也会一并丢失掉,这个问题,RabbitMQ早就考虑到了,并且提供了解决方案,下一篇博文将进行详细介绍
本次和大家分享的是RedisMQ队列的用法,前两篇文章队列工厂之(MSMQ)和队列工厂之RabbitMQ分别简单介绍对应队列环境的搭建和常用方法的使用,加上本篇分享的RedisMQ那么就完成了咋们队列工厂"三剑客"的目标了哈哈;Redis的作用不仅仅局限于队列,更多的一般都使用它的key,value的形式来存储session或者hash的方式存储一些常用的数据,当然这不是本章分享的内容(之前有些文章有讲过redis的使用场景和代码分享各位可以看下),这QueueReposity-队列工厂最后一篇结束后,笔者后面分享的可能是netcore方面的一些东西了,vs2017出来了简单创建netcore项目之后发现与之前的版本有些变动,例如:没有project.json,怎么配置生成跨平台程序等问题,需要一个一个学习和尝试,网上搜索的文章还很少,全靠阅读全英文的官网来学习了哈哈;希望大家能够喜欢本篇文章,也希望各位多多"扫码支持"和"推荐"谢谢! » Redis安装和RedisClient工具的使用 » 封装RedisMQ队列的读和写 » 队列工厂之RedisMQ测试用例 下面一步一个脚印的来分享: » Redis安装和RedisClient工具的使用 首先要使用redis需要下载安装Redis,这里由于之前的文章有讲解在windows下怎么搭建redis服务,所以不再赘述,各位可以点击搭建Redis服务端,并用客户端连接,因此我这里直接分享怎么使用RedisClient工具,这工具使用起来比较简单和方便,首先去这个地址下载: http://dlsw.baidu.com/sw-search-sp/soft/a2/29740/RedisClient20140730.1406883096.exe 安装-》打开软件,能看到如图的界面: -》点击“Server”-》Add-》输入一个昵称,你redis服务端的ip,端口-》确认即可: 这个时候你redisclient的配置工作就完成了是不是很简单啊,-》再来点击刚才创建昵称-》双击打开redis的第一个数据库db0(这里就是在没有指定数据库位置时候存储数据的地方)-》能看到你存储的数据key: 如果想看某个name的数据直接双击对应的name就行了-》这里是我redis服务存储的一个hash数据的截图: 是不是很方便,这个客户端可以直接删除你不想要的数据-》右键选中您想删除的name-》Delete即可删除: 怎么样,这个RedisClient工具学会了么,是不是挺简单的呢; » 封装RedisMQ队列的读和写 到这里终于来到我们代码分享的时刻了,尽管QueueReposity-队列工厂已经开源了源码,这里还是单独分享一次只有RedisMQ的代码;首先创建一个名称为:QRedisMQ的class-》继承 PublicClass.ConfClass<T>-》再实现接口IQueue,最后就有了我们实现接口方法体代码: 1 /// <summary> 2 /// RedisMQ 3 /// </summary> 4 public class QRedisMQ : PublicClass.ConfClass<QRedisMQ>, IQueue 5 { 6 private IRedisClient redis = null; 7 8 public void Create() 9 { 10 if (string.IsNullOrWhiteSpace(this.ApiUrl) || 11 string.IsNullOrWhiteSpace(this.UserPwd)) { throw new Exception("创建QRedisMQ队列需要指定队列:ApiUrl,UserPwd"); } 12 13 this.ApiKey = string.IsNullOrWhiteSpace(this.ApiKey) ? "6379" : this.ApiKey; 14 redis = redis ?? new RedisClient(this.ApiUrl, Convert.ToInt32(this.ApiKey), this.UserPwd); 15 } 16 17 public long Total(string name = "Redis_01") 18 { 19 if (redis == null) { throw new Exception("请先创建队列连接"); } 20 if (string.IsNullOrWhiteSpace(name)) { throw new Exception("name不能为空"); } 21 22 return redis.GetListCount(name); 23 } 24 25 public Message Read(string name = "Redis_01") 26 { 27 if (redis == null) { throw new Exception("请先创建队列连接"); } 28 if (string.IsNullOrWhiteSpace(name)) { throw new Exception("name不能为空"); } 29 30 var message = new Message(); 31 try 32 { 33 message.Label = name; 34 var result = redis.DequeueItemFromList(name); 35 if (string.IsNullOrWhiteSpace(result)) { return message; } 36 message.Body = result; 37 } 38 catch (Exception ex) 39 { 40 throw new Exception(ex.Message); 41 } 42 return message; 43 } 44 45 public bool Write(string content, string name = "Redis_01") 46 { 47 if (redis == null) { throw new Exception("请先创建队列连接"); } 48 if (string.IsNullOrWhiteSpace(content) || string.IsNullOrWhiteSpace(name)) { throw new Exception("content和name不能为空"); } 49 redis.EnqueueItemOnList(name, content); 50 return true; 51 } 52 53 public void Dispose() 54 { 55 if (redis != null) 56 { 57 redis.Dispose(); 58 redis = null; 59 } 60 } 61 62 63 //public List<Message> ReadAll() 64 //{ 65 // throw new NotImplementedException(); 66 //} 67 } 这里用到的Redis的dll是引用了相关的nuget包: 封装的队列Redis工厂流程同样是:创建(Create)-》读(Read)|写(Write)-》释放(Dispose);有了具体的RedisMQ实现类,然后还需利用工厂模式提供的方法来创建这个类的实例: 1 /// <summary> 2 /// ================== 3 /// author:神牛步行3 4 /// des:该列工厂开源,包括队列有MSMQ,RedisMQ,RabbitMQ 5 /// blogs:http://www.cnblogs.com/wangrudong003/ 6 /// ================== 7 /// 队列工厂 8 /// </summary> 9 public class QueueReposity<T> where T : class,IQueue, new() 10 { 11 public static IQueue Current 12 { 13 get 14 { 15 return PublicClass.ConfClass<T>.Current; 16 } 17 } 18 } 到这儿RedisMQ工厂代码就完成了,下面开始分享我们的测试用例; » 队列工厂之RedisMQ测试用例 通过上面配置环境和封装自己的方法,这里写了一个简单的测试用例,分为Server(加入消息队列)和Client(获取消息队列),首先来看Server端的代码: 1 /// <summary> 2 /// 队列服务端测试用例 3 /// </summary> 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 Redis_Server(); 9 10 // RabbitMQ_Server(); 11 12 //MSMQ_Server(); 13 } 14 15 private static void Redis_Server() 16 { 17 //实例化QRedisMQ对象 18 var mq = QueueReposity<QRedisMQ>.Current; 19 20 try 21 { 22 Console.WriteLine("Server端创建:RedisMQ实例"); 23 mq.Create(); 24 25 var num = 0; 26 do 27 { 28 Console.WriteLine("输入循环数量(数字,0表示结束):"); 29 var readStr = Console.ReadLine(); 30 num = string.IsNullOrWhiteSpace(readStr) ? 0 : Convert.ToInt32(readStr); 31 32 Console.WriteLine("插入数据:"); 33 for (int i = 0; i < num; i++) 34 { 35 var str = "我的编号是:" + i; 36 mq.Write(str); 37 Console.WriteLine(str); 38 } 39 } while (num > 0); 40 } 41 catch (Exception ex) 42 { 43 } 44 finally 45 { 46 Console.WriteLine("释放。"); 47 mq.Dispose(); 48 } 49 Console.ReadLine(); 50 } 通过:创建(Create)-》读(Read)|写(Write)-》释放(Dispose) 的流程来使用我们的队列工厂,此时我们运行下这个Server端,然后分别录入4次参数: 能看到截图的文字描述,这些测试数据插入到了redis的队列中,下面我们通过第一节说的RedisClient工具查看数据,点击队列名称如: 通过工具能看到我们刚才插入的数据,然后我们来通过测试用例的client端读取队列,具体代码: 1 /// <summary> 2 /// 队列客户端测试用例 3 /// </summary> 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 RedisMQ_Client(); 9 10 // RabbitMQ_Client(); 11 12 //MSMQ_Client(); 13 } 14 15 private static void RedisMQ_Client() 16 { 17 //实例化QRedisMQ对象 18 var mq = QueueReposity<QRedisMQ>.Current; 19 try 20 { 21 Console.WriteLine("Client端创建:RedisMQ实例"); 22 mq.Create(); 23 24 while (true) 25 { 26 try 27 { 28 var total = mq.Total(); 29 if (total > 0) { Console.WriteLine("队列条数:" + total); } 30 31 var result = mq.Read(); 32 if (result.Body == null) { continue; } 33 Console.WriteLine(string.Format("接受队列{0}:{1}", result.Label, result.Body)); 34 } 35 catch (Exception ex) 36 { Console.WriteLine("异常信息:" + ex.Message); } 37 } 38 } 39 catch (Exception ex) 40 { 41 throw ex; 42 } 43 finally 44 { 45 Console.WriteLine("释放。"); 46 mq.Dispose(); 47 } 48 } 运行生成的exe,看效果: 通过图形能看出读取队列的数据正如我们想的那样依次读取,测试用例测试RedisMQ的代码没问题;以上对封装RedisMQ的代码分享和环境搭建讲解,到这里队列工厂(MSMQ,RabbitMQ,RedisMQ)就分享完了,希望能给您带来好的帮助,谢谢阅读;
近日,工作中突遇一需求:将一数据表分组,而后取出每组内按一定规则排列的前N条数据。乍想来,这本是寻常查询,无甚难处。可提笔写来,终究是困住了笔者好一会儿。冥思苦想,遍查网络,不曾想这竟然是SQL界的一个经典话题。今日将我得来的若干方法列出,抛砖引玉,以期与众位探讨。 正文之前,对示例表结构加以说明。 表SectionTransactionLog,用来记录各部门各项活动的日志表 SectionId,部门Id SectionTransactionType,活动类型 TotalTransactionValue,活动花费 TransactionDate,活动时间 我们设定的场景为:选出每部门(SectionId)最近两次举行的活动。 笔者用来测试的SectionTransactionLog表中数据超3,000,000。 一、 嵌套子查询方式 1 1 SELECT * FROM SectionTransactionLog mLog 2 where 3 (select COUNT(*) from SectionTransactionLog subLog 4 where subLog.SectionId = mLog.SectionId and subLog.TransactionDate >= mLog.TransactionDate)<=2 5 order by SectionId, TransactionDate desc 运行时间:34秒 该方式原理较简单,只是在子查询中确定该条记录是否是其Section中新近发生的2条之一。 2 1 SELECT * FROM SectionTransactionLog mLog 2 where mLog.Id in 3 (select top 2 Id 4 from SectionTransactionLog subLog 5 where subLog.SectionId = mLog.SectionId 6 order by TransactionDate desc) 7 order by SectionId, TransactionDate desc 运行时间:1分25秒 在子查询中使用TransactionDate排序,取top 2。并应用in关键字确定记录是否符合该子查询。 二、 自联接方式 1 select mLog.* from SectionTransactionLog mLog 2 inner join 3 (SELECT rankLeft.Id, COUNT(*) as rankNum FROM SectionTransactionLog rankLeft 4 inner join SectionTransactionLog rankRight 5 on rankLeft.SectionId = rankRight.SectionId and rankLeft.TransactionDate <= rankRight.TransactionDate 6 group by rankLeft.Id 7 having COUNT(*) <= 2) subLog on mLog.Id = subLog.Id 8 order by mLog.SectionId, mLog.TransactionDate desc 运行时间:56秒 该实现方式较为巧妙,但较之之前方法也稍显复杂。其中,以SectionTransactionLog表自联接为基础而构造出的subLog部分为每一活动(以Id标识)计算出其在Section内部的排序rankNum(按时间TransactionDate)。 在自联接条件rankLeft.SectionId = rankRight.SectionId and rankLeft.TransactionDate <= rankRight.TransactionDate的筛选下,查询结果中对于某一活动(以Id标识)而言,与其联接的只有同其在一Section并晚于或与其同时发生活动(当然包括其自身)。下图为Id=1的活动自联接示意: 从上图中一目了然可以看出,基于此结果的count计算,便为Id=1活动在Section 9022中的排次rankNum。 而后having COUNT(*) <= 2选出排次在2以内的,再做一次联接select出所需信息。 三、 应用ROW_NUMBER()(SQL SERVER 2005及之后) 1 select * from 2 ( 3 select *, ROW_NUMBER() over(partition by SectionId order by TransactionDate desc) as rowNum 4 from SectionTransactionLog 5 ) ranked 6 where ranked.rowNum <= 2 7 order by ranked.SectionId, ranked.TransactionDate desc 运行时间:20秒 这是截至目前效率最高的实现方式。ROW_NUMBER() over(partition by SectionId order by TransactionDate desc)完成了分组、排序、取行号的整个过程。 效率思考 下面我们对上述的4种方法做一个效率上的统计。 方法 耗时(秒) 排名 应用ROW_NUMBER() 20 1 嵌套子查询方式1 34 2 自联接方式 56 3 嵌套子查询方式2 85 4 4种方法中,嵌套子查询2所用时最长,其效率损耗在什么地方了呢?难道果真是使用了in关键字的缘故?下图为其执行计划(execute plan): 从图中,我们可以看出优化器将in解析为了Left Semi Join, 其损耗极低。而该查询绝大部分性能消耗在子查询的order by处(Top N Sort)。果然,若删掉子查询中的order by TransactionDate desc子句(当然结果不正确),其耗时仅为8秒。 添加有效索引可提高该查询方法的性能。
版权声明:本文为博主原创文章 未经许可不得转载 请通过右侧公告中的“联系邮箱(wlsandwho@foxmail.com)”联系我 未经作者授权勿用于学术性引用。 未经作者授权勿用于商业出版、商业印刷、商业引用以及其他商业用途。 本文不定期修正完善,为保证内容正确,建议移步原文处阅读。 <--------总有一天我要自己做一个模板干掉这只土豆 本文链接:http://www.cnblogs.com/wlsandwho/p/4829125.html 耻辱墙:http://www.cnblogs.com/wlsandwho/p/4206472.html ======================================================================= 只是写个简单的例子,不要在意星号什么的。 1 USE tempdb 2 3 IF EXISTS(SELECT * FROM sysobjects WHERE id=OBJECT_ID(N't_Test') AND OBJECTPROPERTY(id,N'IsUserTable')=1) 4 DROP TABLE t_Test 5 GO 6 CREATE TABLE t_Test( 7 OnLineDate DATETIME, 8 ProductID NVARCHAR(8), 9 WebPage NVARCHAR(32) 10 ) 11 GO 12 INSERT INTO t_Test VALUES(GETDATE(),'11111111','1cccccccccc') 13 WAITFOR DELAY '00:00:01' 14 INSERT INTO t_Test VALUES(GETDATE(),'11111111','1eeeeeeeeee') 15 WAITFOR DELAY '00:00:01' 16 INSERT INTO t_Test VALUES(GETDATE(),'11111111','1bbbbbbbbbb') 17 WAITFOR DELAY '00:00:01' 18 INSERT INTO t_Test VALUES(GETDATE(),'11111111','1dddddddddd') 19 WAITFOR DELAY '00:00:01' 20 INSERT INTO t_Test VALUES(GETDATE(),'11111111','1aaaaaaaaaa') 21 WAITFOR DELAY '00:00:01' 22 INSERT INTO t_Test VALUES(GETDATE(),'22222222','2aaaaaaaaaa') 23 WAITFOR DELAY '00:00:01' 24 INSERT INTO t_Test VALUES(GETDATE(),'22222222','2cccccccccc') 25 WAITFOR DELAY '00:00:01' 26 INSERT INTO t_Test VALUES(GETDATE(),'22222222','2eeeeeeeeee') 27 WAITFOR DELAY '00:00:01' 28 INSERT INTO t_Test VALUES(GETDATE(),'22222222','2dddddddddd') 29 WAITFOR DELAY '00:00:01' 30 INSERT INTO t_Test VALUES(GETDATE(),'22222222','2bbbbbbbbbb') 31 GO 32 33 SELECT OnLineDate,ProductID,WebPage,ROW_NUMBER() OVER(PARTITION BY ProductID ORDER BY OnLineDate DESC) AS rowRum FROM t_Test 34 GO 35 ----------------------------- 36 WITH t_Temp 37 AS 38 ( 39 SELECT OnLineDate,ProductID,WebPage,ROW_NUMBER() OVER(PARTITION BY ProductID ORDER BY OnLineDate DESC) AS rowRum FROM t_Test 40 ) 41 SELECT * FROM t_Temp 42 WHERE t_Temp.rowRum<=3 43 GO 44 ----------------------------- 45 WITH t_Temp 46 AS 47 ( 48 SELECT OnLineDate,ProductID,WebPage,ROW_NUMBER() OVER(PARTITION BY ProductID ORDER BY OnLineDate DESC) AS rowRum FROM t_Test 49 ) 50 SELECT * FROM t_Temp 51 WHERE t_Temp.rowRum<=3 ORDER BY ProductID ASC,OnLineDate DESC 52 GO ======================================================================= 最近没什么好写的,QQ群里有人问问题,就随手写了一个。 然而那人拿了之后就再也没有反应/反馈了。 所以只能对广大伸手党说一句,我真心希望你们都能看看《你的知识需要管理 田志刚》这本书。
目前业务上需要选用合适的消息队列来做数据传输,因此特意调研了一下当下较主流的消息队列的各特点: 消息中间件三要素:生产者、消息、消费者。 衡量标准:生产者、消息、消费者三者的交互。 1.消息路由:消息如何经过消息中间件到达消费者。 2.消息可靠性: 2.1.不允许消息丢失 2.2.允许消息丢失,性能需求大于可靠性 3.消息重放:已经消费过的消息是否能设置某一时间间隔后重新被消费(适用于新系统导入旧数据,防数据丢失) 4.消息堆积:流量高峰期时,消息中间件在高并发投递消息时可能会出现问题,所以把消息暂存在中间件,等流量高峰过去,再投给下游业务 5.消息优先级: 5.1.消息投递顺序和消费顺序一致 5.2.可设置某些消息的消费优先级 6.性能和扩展 6.1.性能:主要指tps、qps以及并发连接数 6.2.扩展:通过添加消费者来提高消费速率、消息中间件的容量上限 鉴于目前我们业务是做数据传输,主要是需要保证数据的完整性,综合比较,选择了RabbitMQ。
序言 Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案。实际上这意味着你可以使用Sentinel模式创建一个可以不用人为干预而应对各种故障的Redis部署。 它的主要功能有以下几点 监控:Sentinel不断的检查master和slave是否正常的运行。 通知:如果发现某个redis节点运行出现问题,可以通过API通知系统管理员和其他的应用程序。 自动故障转移:能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。 配置提供者:哨兵作为Redis客户端发现的权威来源:客户端连接到哨兵请求当前可靠的master的地址。如果发生故障,哨兵将报告新地址。 sentinel的分布式特性 很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处: 即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换; 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题); 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。 关于sentinel的稳定版本 当前的哨兵版本是sentinel 2。它是基于最初哨兵的实现,使用更健壮的和更简单的预算算法(在这个文档里有解释)重写的。 Redis2.8和Redis3.0附带稳定的哨兵版本。他们是Redis的两个最新稳定版本。 在不稳定版本的分支上执行新的改进,且有时一些新特性一旦被认为是稳定的就会被移植到Redis2.8和Redis3.0分支中。 Redis2.6附带Redis sentinel 1,它是弃用的不建议使用。 运行sentinel 运行Sentinel有两种方式,如下: redis-sentinel /path/to/sentinel.conf redis-server /path/to/sentinel.conf --sentinel 两种方式效果都是一样的。 然而在启动哨兵时必须使用一个配置文件,因为这个配置文件将用于系统保存当前状态和在重启时重新加载。哨兵会在没有指定配置文件或指定的配置文件不可写的时候拒绝启动。 Redis 哨兵默认监听26379 TCP端口,所以为了哨兵的正常工作,你的26379端口必须开放接收其他哨兵实例的IP地址的连接。否则哨兵不能通信和商定做什么,故障转移将永不会执行。 部署哨兵之前需要了解的基本事情 一个健壮的部署至少需要三个哨兵实例。 三个哨兵实例应该放置在客户使用独立方式确认故障的计算机或虚拟机中。例如不同的物理机或不同可用区域的虚拟机。 sentinel + Redis实例不保证在故障期间保留确认的写入,因为Redis使用异步复制。然而有方式部署哨兵使丢失数据限制在特定时刻,虽然有更安全的方式部署它。 你的客户端要支持哨兵,流行的客户端都支持哨兵,但不是全部。 没有HA设置是安全的,如果你不经常的在开发环境测试,在生产环境他们会更好。你可能会有一个明显的错误配置只是当太晚的时候。 Sentinel,Docker,或者其他形式的网络地址交换或端口映射需要加倍小心:Docker执行端口重新映射,破坏Sentinel自动发现其他的哨兵进程和master的slave列表。稍后在这个文档里检查关于Sentinel和Docker的部分,了解更多信息。 Sentinel的配置 Redis源码发布包包含一个sentinel.conf的文件,默认的配置文件中有关于各个配置项的详细解释,一个典型的最小的配置文件就像下面的配置: sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1 sentinel monitor resque 192.168.1.3 6380 4 sentinel down-after-milliseconds resque 10000 sentinel failover-timeout resque 180000 sentinel parallel-syncs resque 5 上面的配置项配置了两个名字分别为mymaster和resque的master,配置文件只需要配置master的信息就好啦,不用配置slave的信息,因为slave能够被自动检测到(master节点中有关于slave的消息)。 为了更清晰,我们逐行的解释每个选项的含义: 第一行的格式如下: sentinel monitor [master-group-name] [ip] [port] [quorum] master-group-name:master名称 quorun:本文叫做票数,Sentinel需要协商同意master是否可到达的数量。 sentinel monitor mymaster 127.0.0.1 6379 2 这一行用于告诉Redis监控一个master叫做mymaster,它的地址在127.0.0.1,端口为6379,票数是2。 这里的票数需要解释下:举个栗子,redis集群中有5个sentinel实例,其中master挂掉啦,如果这里的票数是2,表示有2个sentinel认为master挂掉啦,才能被认为是正真的挂掉啦。其中sentinel集群中各个sentinel也有互相通信,通过gossip协议。 除啦第一行其他的格式如下: sentinel [option_name] [master_name] [option_value] down-after-milliseconds sentinel会向master发送心跳PING来确认master是否存活,如果master在“一定时间范围”内不回应PONG 或者是回复了一个错误消息,那么这个sentinel会主观地认为这个master已经不可用了。而这个down-after-milliseconds就是用来指定这个“一定时间范围”的,单位是毫秒。 parallel-syncs 在发生failover主从切换时,这个选项指定了最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成主从故障转移所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为主从同步而不可用。可以通过将这个值设为1来保证每次只有一个slave处于不能处理命令请求的状态。 Sentinel的“仲裁会” 前面我们谈到,主从故障转移时,需要的sentinel认可的票数达到设置的值才可以。 不过,当failover主备切换真正被触发后,failover并不会马上进行,还需要sentinel中的大多数sentinel授权后才可以进行failover。 当sentinel认可不可用的票数达到时(ODOWN),failover被触发。failover一旦被触发,尝试去进行failover的sentinel会去获得“大多数”sentinel的授权(如果票数比大多数还要大的时候,则询问更多的sentinel) 这个区别看起来很微妙,但是很容易理解和使用。例如,集群中有5个sentinel,票数被设置为2,当2个sentinel认为一个master已经不可用了以后,将会触发failover,但是,进行failover的那个sentinel必须先获得至少3个sentinel的授权才可以实行failover。 如果票数被设置为5,要达到ODOWN状态,必须所有5个sentinel都主观认为master为不可用,要进行failover,那么得获得所有5个sentinel的授权。 配置版本号 为什么要先获得大多数sentinel的认可时才能真正去执行failover呢? 当一个sentinel被授权后,它将会获得宕掉的master的一份最新配置版本号,当failover执行结束以后,这个版本号将会被用于最新的配置。因为大多数sentinel都已经知道该版本号已经被要执行failover的sentinel拿走了,所以其他的sentinel都不能再去使用这个版本号。这意味着,每次failover都会附带有一个独一无二的版本号。我们将会看到这样做的重要性。 而且,sentinel集群都遵守一个规则:如果sentinel A推荐sentinel B去执行failover,B会等待一段时间后,自行再次去对同一个master执行failover,这个等待的时间是通过failover-timeout配置项去配置的。从这个规则可以看出,sentinel集群中的sentinel不会再同一时刻并发去failover同一个master,第一个进行failover的sentinel如果失败了,另外一个将会在一定时间内进行重新进行failover,以此类推。 redis sentinel保证了活跃性:如果大多数sentinel能够互相通信,最终将会有一个被授权去进行failover. redis sentinel也保证了安全性:每个试图去failover同一个master的sentinel都会得到一个独一无二的版本号。 配置传播 一旦一个sentinel成功地对一个master进行了failover,它将会把关于master的最新配置通过广播形式通知其它sentinel,其它的sentinel则更新对应master的配置。 一个faiover要想被成功实行,sentinel必须能够向选为master的slave发送SLAVE OF NO ONE命令,然后能够通过INFO命令看到新master的配置信息。 当将一个slave选举为master并发送SLAVE OF NO ONE`后,即使其它的slave还没针对新master重新配置自己,failover也被认为是成功了的,然后所有sentinels将会发布新的配置信息。 新配在集群中相互传播的方式,就是为什么我们需要当一个sentinel进行failover时必须被授权一个版本号的原因。 每个sentinel使用##发布/订阅##的方式持续地传播master的配置版本信息,配置传播的##发布/订阅##管道是:__sentinel__:hello。 因为每一个配置都有一个版本号,所以以版本号最大的那个为标准。 举个栗子:假设有一个名为mymaster的地址为192.168.1.50:6379。一开始,集群中所有的sentinel都知道这个地址,于是为mymaster的配置打上版本号1。一段时候后mymaster死了,有一个sentinel被授权用版本号2对其进行failover。如果failover成功了,假设地址改为了192.168.1.50:9000,此时配置的版本号为2,进行failover的sentinel会将新配置广播给其他的sentinel,由于其他sentinel维护的版本号为1,发现新配置的版本号为2时,版本号变大了,说明配置更新了,于是就会采用最新的版本号为2的配置。 这意味着sentinel集群保证了第二种活跃性:一个能够互相通信的sentinel集群最终会采用版本号最高且相同的配置。 SDOWN和ODOWN的更多细节 sentinel对于不可用有两种不同的看法,一个叫主观不可用(SDOWN),另外一个叫客观不可用(ODOWN)。 SDOWN是sentinel自己主观上检测到的关于master的状态。 ODOWN需要一定数量的sentinel达成一致意见才能认为一个master客观上已经宕掉,各个sentinel之间通过命令 SENTINEL is_master_down_by_addr 来获得其它sentinel对master的检测结果。 从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件。这个时间在配置中通过 is-master-down-after-milliseconds 参数配置。 当sentinel发送PING后,以下回复都被认为是合法的,除此之外,其它任何回复(或者根本没有回复)都是不合法的。 PING replied with +PONG. PING replied with -LOADING error. PING replied with -MASTERDOWN error. 从SDOWN切换到ODOWN不需要任何一致性算法,只需要一个gossip协议:如果一个sentinel收到了足够多的sentinel发来消息告诉它某个master已经down掉了,SDOWN状态就会变成ODOWN状态。如果之后master可用了,这个状态就会相应地被清理掉。 正如之前已经解释过了,真正进行failover需要一个授权的过程,但是所有的failover都开始于一个ODOWN状态。 ODOWN状态只适用于master,对于不是master的redis节点sentinel之间不需要任何协商,slaves和sentinel不会有ODOWN状态。 Sentinel之间和Slaves之间的自动发现机制 虽然sentinel集群中各个sentinel都互相连接彼此来检查对方的可用性以及互相发送消息。但是你不用在任何一个sentinel配置任何其它的sentinel的节点。因为sentinel利用了master的发布/订阅机制去自动发现其它也监控了统一master的sentinel节点。 通过向名为__sentinel__:hello的管道中发送消息来实现。 同样,你也不需要在sentinel中配置某个master的所有slave的地址,sentinel会通过询问master来得到这些slave的地址的。 每个sentinel通过向每个master和slave的发布/订阅频道__sentinel__:hello每秒发送一次消息,来宣布它的存在。 每个sentinel也订阅了每个master和slave的频道__sentinel__:hello的内容,来发现未知的sentinel,当检测到了新的sentinel,则将其加入到自身维护的master监控列表中。 每个sentinel发送的消息中也包含了其当前维护的最新的master配置。如果某个sentinel发现 自己的配置版本低于接收到的配置版本,则会用新的配置更新自己的master配置。 在为一个master添加一个新的sentinel前,sentinel总是检查是否已经有sentinel与新的sentinel的进程号或者是地址是一样的。如果是那样,这个sentinel将会被删除,而把新的sentinel添加上去。 网络隔离时的一致性 redis sentinel集群的配置的一致性模型为最终一致性,集群中每个sentinel最终都会采用最高版本的配置。然而,在实际的应用环境中,有三个不同的角色会与sentinel打交道: Redis实例. Sentinel实例. 客户端. 为了考察整个系统的行为我们必须同时考虑到这三个角色。 下面有个简单的例子,有三个主机,每个主机分别运行一个redis和一个sentinel: +-------------+ | Sentinel 1 | <--- Client A | Redis 1 (M) | +-------------+ | | +-------------+ | +------------+ | Sentinel 2 |-----+-- / partition / ----| Sentinel 3 | <--- Client B | Redis 2 (S) | | Redis 3 (M)| +-------------+ +------------+ 在这个系统中,初始状态下redis3是master, redis1和redis2是slave。之后redis3所在的主机网络不可用了,sentinel1和sentinel2启动了failover并把redis1选举为master。 Sentinel集群的特性保证了sentinel1和sentinel2得到了关于master的最新配置。但是sentinel3依然持着的是就的配置,因为它与外界隔离了。 当网络恢复以后,我们知道sentinel3将会更新它的配置。但是,如果客户端所连接的master被网络隔离,会发生什么呢? 客户端将依然可以向redis3写数据,但是当网络恢复后,redis3就会变成redis的一个slave,那么,在网络隔离期间,客户端向redis3写的数据将会丢失。 也许你不会希望这个场景发生: 如果你把redis当做缓存来使用,那么你也许能容忍这部分数据的丢失。 但如果你把redis当做一个存储系统来使用,你也许就无法容忍这部分数据的丢失了。 因为redis采用的是异步复制,在这样的场景下,没有办法避免数据的丢失。然而,你可以通过以下配置来配置redis3和redis1,使得数据不会丢失。 min-slaves-to-write 1 min-slaves-max-lag 10 通过上面的配置,当一个redis是master时,如果它不能向至少一个slave写数据(上面的min-slaves-to-write指定了slave的数量),它将会拒绝接受客户端的写请求。由于复制是异步的,master无法向slave写数据意味着slave要么断开连接了,要么不在指定时间内向master发送同步数据的请求了(上面的min-slaves-max-lag指定了这个时间)。 Sentinel状态持久化 snetinel的状态会被持久化地写入sentinel的配置文件中。每次当收到一个新的配置时,或者新创建一个配置时,配置会被持久化到硬盘中,并带上配置的版本戳。这意味着,可以安全的停止和重启sentinel进程。 无failover时的配置纠正 即使当前没有failover正在进行,sentinel依然会使用当前配置去设置监控的master。特别是: 根据最新配置确认为slaves的节点却声称自己是master(参考上文例子中被网络隔离后的的redis3),这时它们会被重新配置为当前master的slave。 如果slaves连接了一个错误的master,将会被改正过来,连接到正确的master。 Slave选举与优先级 当一个sentinel准备好了要进行failover,并且收到了其他sentinel的授权,那么就需要选举出一个合适的slave来做为新的master。 slave的选举主要会评估slave的以下几个方面: 与master断开连接的次数 Slave的优先级 数据复制的下标(用来评估slave当前拥有多少master的数据) 进程ID 如果一个slave与master失去联系超过10次,并且每次都超过了配置的最大失联时间(down-after-milliseconds option),并且,如果sentinel在进行failover时发现slave失联,那么这个slave就会被sentinel认为不适合用来做新master的。 更严格的定义是,如果一个slave持续断开连接的时间超过 (down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state 就会被认为失去选举资格。符合上述条件的slave才会被列入master候选人列表,并根据以下顺序来进行排序: sentinel首先会根据slaves的优先级来进行排序,优先级越小排名越靠前(?)。 如果优先级相同,则查看复制的下标,哪个从master接收的复制数据多,哪个就靠前。 如果优先级和下标都相同,就选择进程ID较小的那个。 一个redis无论是master还是slave,都必须在配置中指定一个slave优先级。要注意到master也是有可能通过failover变成slave的。 如果一个redis的slave优先级配置为0,那么它将永远不会被选为master。但是它依然会从master哪里复制数据。 Sentinel和Redis身份验证 当一个master配置为需要密码才能连接时,客户端和slave在连接时都需要提供密码。 master通过requirepass设置自身的密码,不提供密码无法连接到这个master。 slave通过masterauth来设置访问master时的密码。 但是当使用了sentinel时,由于一个master可能会变成一个slave,一个slave也可能会变成master,所以需要同时设置上述两个配置项。 Sentinel API Sentinel默认运行在26379端口上,sentinel支持redis协议,所以可以使用redis-cli客户端或者其他可用的客户端来与sentinel通信。 有两种方式能够与sentinel通信: 一种是直接使用客户端向它发消息 另外一种是使用发布/订阅模式来订阅sentinel事件,比如说failover,或者某个redis实例运行出错,等等。 Sentinel命令 sentinel支持的合法命令如下: PING sentinel回复PONG. SENTINEL masters 显示被监控的所有master以及它们的状态. SENTINEL master <master name> 显示指定master的信息和状态; SENTINEL slaves <master name> 显示指定master的所有slave以及它们的状态; SENTINEL get-master-addr-by-name <master name> 返回指定master的ip和端口,如果正在进行failover或者failover已经完成,将会显示被提升为master的slave的ip和端口。 SENTINEL reset <pattern> 重置名字匹配该正则表达式的所有的master的状态信息,清楚其之前的状态信息,以及slaves信息。 SENTINEL failover <master name> 强制sentinel执行failover,并且不需要得到其他sentinel的同意。但是failover后会将最新 动态修改Sentinel配置 从redis2.8.4开始,sentinel提供了一组API用来添加,删除,修改master的配置。 需要注意的是,如果你通过API修改了一个sentinel的配置,sentinel不会把修改的配置告诉其他sentinel。你需要自己手动地对多个sentinel发送修改配置的命令。 以下是一些修改sentinel配置的命令: SENTINEL MONITOR <name> <ip> <port> <quorum> 这个命令告诉sentinel去监听一个新的master SENTINEL REMOVE <name> 命令sentinel放弃对某个master的监听 SENTINEL SET <name> <option> <value> 这个命令很像Redis的CONFIG SET命令,用来改变指定master的配置。支持多个<option><value>。例如以下实例: SENTINEL SET objects-cache-master down-after-milliseconds 1000 只要是配置文件中存在的配置项,都可以用SENTINEL SET命令来设置。这个还可以用来设置master的属性,比如说quorum(票数),而不需要先删除master,再重新添加master。例如: SENTINEL SET objects-cache-master quorum 5 增加或删除Sentinel 由于有sentinel自动发现机制,所以添加一个sentinel到你的集群中非常容易,你所需要做的只是监控到某个Master上,然后新添加的sentinel就能获得其他sentinel的信息以及masterd所有的slave。 如果你需要添加多个sentinel,建议你一个接着一个添加,这样可以预防网络隔离带来的问题。你可以每个30秒添加一个sentinel。最后你可以用SENTINEL MASTER mastername来检查一下是否所有的sentinel都已经监控到了master。 删除一个sentinel显得有点复杂:因为sentinel永远不会删除一个已经存在过的sentinel,即使它已经与组织失去联系很久了。要想删除一个sentinel,应该遵循如下步骤: 停止所要删除的sentinel 发送一个SENTINEL RESET * 命令给所有其它的sentinel实例,如果你想要重置指定master上面的sentinel,只需要把*号改为特定的名字,注意,需要一个接一个发,每次发送的间隔不低于30秒。 检查一下所有的sentinels是否都有一致的当前sentinel数。使用SENTINEL MASTER mastername 来查询。 删除旧master或者不可达slave sentinel永远会记录好一个Master的slaves,即使slave已经与组织失联好久了。这是很有用的,因为sentinel集群必须有能力把一个恢复可用的slave进行重新配置。 并且,failover后,失效的master将会被标记为新master的一个slave,这样的话,当它变得可用时,就会从新master上复制数据。 然后,有时候你想要永久地删除掉一个slave(有可能它曾经是个master),你只需要发送一个SENTINEL RESET master命令给所有的sentinels,它们将会更新列表里能够正确地复制master数据的slave。 发布/订阅 客户端可以向一个sentinel发送订阅某个频道的事件的命令,当有特定的事件发生时,sentinel会通知所有订阅的客户端。需要注意的是客户端只能订阅,不能发布。 订阅频道的名字与事件的名字一致。例如,频道名为sdown 将会发布所有与SDOWN相关的消息给订阅者。 如果想要订阅所有消息,只需简单地使用PSUBSCRIBE * 以下是所有你可以收到的消息的消息格式,如果你订阅了所有消息的话。第一个单词是频道的名字,其它是数据的格式。 注意:以下的instance details的格式是: <instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port> 如果这个redis实例是一个master,那么@之后的消息就不会显示。 +reset-master <instance details> -- 当master被重置时. +slave <instance details> -- 当检测到一个slave并添加进slave列表时. +failover-state-reconf-slaves <instance details> -- Failover状态变为reconf-slaves状态时 +failover-detected <instance details> -- 当failover发生时 +slave-reconf-sent <instance details> -- sentinel发送SLAVEOF命令把它重新配置时 +slave-reconf-inprog <instance details> -- slave被重新配置为另外一个master的slave,但数据复制还未发生时。 +slave-reconf-done <instance details> -- slave被重新配置为另外一个master的slave并且数据复制已经与master同步时。 -dup-sentinel <instance details> -- 删除指定master上的冗余sentinel时 (当一个sentinel重新启动时,可能会发生这个事件). +sentinel <instance details> -- 当master增加了一个sentinel时。 +sdown <instance details> -- 进入SDOWN状态时; -sdown <instance details> -- 离开SDOWN状态时。 +odown <instance details> -- 进入ODOWN状态时。 -odown <instance details> -- 离开ODOWN状态时。 +new-epoch <instance details> -- 当前配置版本被更新时。 +try-failover <instance details> -- 达到failover条件,正等待其他sentinel的选举。 +elected-leader <instance details> -- 被选举为去执行failover的时候。 +failover-state-select-slave <instance details> -- 开始要选择一个slave当选新master时。 no-good-slave <instance details> -- 没有合适的slave来担当新master selected-slave <instance details> -- 找到了一个适合的slave来担当新master failover-state-send-slaveof-noone <instance details> -- 当把选择为新master的slave的身份进行切换的时候。 failover-end-for-timeout <instance details> -- failover由于超时而失败时。 failover-end <instance details> -- failover成功完成时。 switch-master <master name> <oldip> <oldport> <newip> <newport> -- 当master的地址发生变化时。通常这是客户端最感兴趣的消息了。 +tilt -- 进入Tilt模式。 -tilt -- 退出Tilt模式。 TILT 模式 redis sentinel非常依赖系统时间,例如它会使用系统时间来判断一个PING回复用了多久的时间。 然而,假如系统时间被修改了,或者是系统十分繁忙,或者是进程堵塞了,sentinel可能会出现运行不正常的情况。 当系统的稳定性下降时,TILT模式是sentinel可以进入的一种的保护模式。当进入TILT模式时,sentinel会继续监控工作,但是它不会有任何其他动作,它也不会去回应is-master-down-by-addr这样的命令了,因为它在TILT模式下,检测失效节点的能力已经变得让人不可信任了。 如果系统恢复正常,持续30秒钟,sentinel就会退出TITL模式。 BUSY状态 注意:该功能还未实现。 当一个脚本的运行时间超过配置的运行时间时,sentinel会返回一个-BUSY 错误信号。如果这件事发生在触发一个failover之前,sentinel将会发送一个SCRIPT KILL命令,如果script是只读的话,就能成功执行。 Sentinel部署示例 既然你知道了sentinel的基本信息,你可以很想知道应该将Sentinel放置在哪里,需要多少Sentinel进程等等。这个章节展示了几个部署示例。 我们为了图像化展示配置示例使用字符艺术,这是不同符号的意思: +--------------------+ | This is a computer | | or VM that fails | | independently. We | | call it a "box" | +--------------------+ 我们写在盒子里表示他们正在运行什么: +-------------------+ | Redis master M1 | | Redis Sentinel S1 | +-------------------+ 不同的盒子之间通过线条连接,表示他们可以相互通信: +-------------+ +-------------+ | Sentinel S1 |---------------| Sentinel S2 | +-------------+ +-------------+ 使用斜杠展示网络断开: +-------------+ +-------------+ | Sentinel S1 |------ // ------| Sentinel S2 | +-------------+ +-------------+ 还要注意: Master 被叫做 M1,M2,M3 ... Mn。 Slave 被叫做 R1,R2,R3 ... Rn(replica的首字母) Sentinels 被叫做 S1,S2,S3 ... Sn Clients 被叫做 C1,C2,C3 ... Cn 当一个实例因为Sentinel的行为改变了角色,我们把它放在方括号里,所以[M1]表示因为Sentinel的介入,M1现在是一个master。 注意永远不会显示的设置只是使用了两个哨兵,因为为了启动故障转移,Sentinel总是需要和其他大多数的Sentinel通信。 实例1,只有两个Sentinel,不要这样做 +----+ +----+ | M1 |---------| R1 | | S1 | | S2 | +----+ +----+ Configuration: quorum = 1 在这个设置中,如果master M1故障,R1将被晋升因为两个Sentinel可以达成协议并且还可以授权一个故障转移因为多数就是两个。所以他表面上看起来是可以工作的,然而检查下一个点了解为什么这个设置是不行的。 如果运行M1的盒子停止工作了,S1也停止工作。运行在其他盒子上的S2将不能授权故障转移,所以系统将变成不可用。 注意为了排列不同的故障转移需要少数服从多数,并且稍后向所有的Sentinel传播最新的配置。还要注意上面配置的故障转移的能力,没有任何协定,非常危险: +----+ +------+ | M1 |----//-----| [M1] | | S1 | | S2 | +----+ +------+ 在上面的配置中我们使用完美的对称方式创建了两个master(假定S2可以在未授权的情况下进行故障转移)。客户端可能会不确定往哪边写,并且没有途径知道什么时候分区配置是正确的,为了预防一个永久的断裂状态。 所有请永远部署至少三个Sentinel在三个不同的盒子里。 例2:使用三个盒子的基本设置 这是个非常简单的设置,它有简单调整安全的优势。它基于三个盒子,每个盒子同时运行一个Redis实例和一个Sentinel实例。 +----+ | M1 | | S1 | +----+ | +----+ | +----+ | R2 |----+----| R3 | | S2 | | S3 | +----+ +----+ Configuration: quorum = 2 如果M1故障,S2和S3将会商定故障并授权故障转移,使客户端可以继续。 在每个Sentinel设置里,Redis是异步主从复制,总会有丢失数据的风险,因为有可能当它成为master的时候,一个确认的写入操作还没有同步到slave。然后在上面的设置中有一个更高的风险由于客户端分区一直是老的master,就像下面的图像所示: +----+ | M1 | | S1 | [- C1 (writes will be lost) +----+ | / / +------+ | +----+ | [M2] |----+----| R3 | | S2 | | S3 | +------+ +----+ 在这个案例中网络分区隔离老的master M1,所以slave R2晋升为master。然而客户端,比如C1,还在原来的老的master的分区,可能继续往老master写数据。这个数据将会永久丢失,因为分区恢复时,master将会重新配置为新master的slave,丢弃它的数据集。 这个问题可以使用下面的Redis主从复制特性减轻,它可在master检查到它不再能传输它的写入操作到指定数量的slave的时候停止接收写入操作。 min-slaves-to-write 1 min-slaves-max-lag 10 使用上面的配置(请查看自带的redis.conf示例了解更多信息)一个Redis实例,当作为一个master,如果它不能写入至少1个slave将停止接收写入操作。(N个Slave以上不通就停止接收) 由于主从复制是异步的不能真实的写入,意味着slave断开连接,或者不再向我们发送异步确认的指定的max-lag秒数。(判定连接不通的超时时间) 在上面的示例中使用这个配置,老master M1将会在10秒钟后变为不可用。当分区恢复时,Sentinel配置将指向新的一个,客户端C1将能够获取到有效的配置并且将使用新master继续工作。 然而天下没有免费的午餐,这种改进,如果两个slave挂掉,master将会停止接收写入操作。这是个权衡。 例三:Sentinel在客户端盒子里 有时我们只有两个Redis盒子可用,一个master和一个slave。在例二中的配置在那样的情况下是不可行的,所谓我们可以借助下面的,Sentinel放置在客户端: +----+ +----+ | M1 |----+----| R1 | | S1 | | | S2 | +----+ | +----+ | +------------+------------+ | | | | | | +----+ +----+ +----+ | C1 | | C2 | | C3 | | S1 | | S2 | | S3 | +----+ +----+ +----+ Configuration: quorum = 2 在这个设置里,Sentinel的视角和客户端的视角相同:如果大多数的客户端认为master是可以到达的,它就是好的。C1,C2,C3是一般的客户端,这不意味着C1识别单独的客户端连接到Redis。它更像一些如应用服务,Rails应用之类的。 如果运行M1和S1的盒子故障,故障转移将会发生,然而很容看到不同的网络分区将导致不同的行为。例如如果客户端和Redis服务之间的断开连接,Sentinel将不能设置,因为master和slave将都不可用。 注意如果使用M1获取分区,我们有一个和例二中描述的相似的问题,不同的是这里我们没有办法打破对称,由于只有一个slave和master,所以当它的master断开连接时master不能停止接收查询,否则在slave故障期间master将永不可用。 所以这是个有效的设置但是在例二中的设置有像更容易管理HA系统的优点, 并有能力限制master接收写入的时间。 例4:少于3个客户端的Sentinel客户端 在例3中如果客户端少于3个就不能使用。在这个案例中我们使用一个混合的设置: +----+ +----+ | M1 |----+----| R1 | | S1 | | | S2 | +----+ | +----+ | +------+-----+ | | | | +----+ +----+ | C1 | | C2 | | S3 | | S4 | +----+ +----+ Configuration: quorum = 3 这里和例3非常类似,但是这里我们在4个盒子里运行四个哨兵。如果M1故障其他的三个哨兵可以执行故障转移。 本文参考文档: https://redis.io/topics/sentinel http://redis.majunwei.com/topics/sentinel.html https://segmentfault.com/a/1190000002680804 https://segmentfault.com/a/1190000002685515 总结 接下来是大家最喜欢的总结内容啦,内容有二,如下: 1、希望能关注我其他的文章。 2、博客里面有没有很清楚的说明白,或者你有更好的方式,那么欢迎加入左上方的2个交流群,我们一起学习探讨。
本次和大家分享的是RabbitMQ队列的用法,前一篇文章队列工厂之(MSMQ)中在描述的时候已经搭建了简单工厂,因此本章内容是在其之上扩充的子项不再过多讲解工厂的代码了;RabbitMQ应该是现在互联网公司消息队列用的最多的一种之一吧,看看招聘基本都会有这个单词的出现,她相比前一篇分享的MSMQ来说配置更多样化,安装步骤数两者都差不多吧,最大差别MSMQ是windows捆绑的服务几乎只能在windows上使用,而Rabbit现目前运行支持的系统比较多;在写队列工厂第二篇文章的时候,其实代码已经都完成了目前队列工厂包括有如下队列(msmq,redis,rabbitmq),你可以去下载源码和测试用例:QueueReposity-队列工厂;希望大家能够喜欢,也希望各位多多"扫码支持"和"推荐"谢谢! » RabbitMQ安装和控制台 » 封装RabbitMQ队列的读和写 » 队列工厂之RabbitMQ测试用例 下面一步一个脚印的来分享: » RabbitMQ安装和控制台 要说RabbitMQ的安装,首先我们要下载对应服务器操作系统的RabbitMQ安装文件,因为她有对应不同操作系统的安装版本,这点需要注意;我本地电脑系统是win7(属于windows)所以去官网下载安装包:https://www.rabbitmq.com/,进入官网后选择“Installation”,能看到很多安装版本的下载源,这里可选择从Downloads on rabbitmq.com下载,点击“windows”即可下载: 目前最新版本地址: https://www.rabbitmq.com/releases/rabbitmq-server/v3.6.6/rabbitmq-server-3.6.6.exe; 通常我都是进入这个界面的Installation Guides节点后-》With installer (recommended),这个时候进入的是windows系统所需的帮助文档吧,同样可以选择版本下载;进入该界面的主要目的是需要下载一个Erlang Windows的安装文件(更深层原因:RabbitMq运行依赖于Erlang语言),点击Erlang Windows Binary File进入下载界面,然后选择您操作系统对应的版本,如果您也是windows64位的可以直接用这个地址下载: http://erlang.org/download/otp_win64_19.2.exe 此刻两个必须的东西已经下载完成,先安装erlang语言的exe,再安装rabbitmq-server-3.6.6.exe;有刚开始接触RabbitMQ的朋友会问为什么需要Erlang的支持,因为她就是Erlang开发出来的,Erlang语言是一种通用的面向并发的编程语言,专门用来编写分布式的一种语言;当你安装前面说的那个erlang安装包后,您电脑开始菜单中就有Erlang开发编辑器,有时间您可以用来练练手,就目前而言这种语言单词一般出现在一流大公司的招聘中,中小型一般没有,可能也因为很少中小型公司会涉及到并发的原因吧;到这里安装就完成了,下面需要通过命令行执行一些指令,由于RabbitMQ配置很多这里我捡一定会用到的几个来示范,其他具体可以参考官方文档: 首先找到安装rabbitmq的目录并进入rabbitmq_server-3.6.6找到sbin文件夹-》按住Shift+鼠标右键sbin文件夹-》在此处打开命令窗体-》参考这个地址https://www.rabbitmq.com/management.html的命令:rabbitmq-plugins enable rabbitmq_management -》录入到刚才打开的cmd窗体中: 这个是开启rabbitmq管理器的指令,这个时候你可以在你浏览器中录入http://localhost:15672/ 通过游客账号进入rabbitmq的监控后台: url:http://localhost:15672/ Username:guest Password:guest 此刻如果你看到如下图的界面,那恭喜你成功了,搭建RabbitMQ服务成功了: 因为Rabbit不光有队列,还有其他的路由,交换机等功能,所以能看到很多的统计或描述,这里我们只用到Queues的选项,点击进入Queues界面能看到没有任何的数据,但是有一个Add queue的按钮,这个控制台允许你手动添加一个队列数据,当然这不是我们今天的话题: 上面的guest已经够咋们测试使用了,至于剩余的什么管理员账号或密码等操作的设置可以去看这个: rabbitmqctl操作的文档:https://www.rabbitmq.com/man/rabbitmqctl.1.man.html# plugins操作文档:https://www.rabbitmq.com/plugins.html » 封装RabbitMQ队列的读和写 在C#中运用RabbitMQ官网列举了几种方式,这里我选择直接使用其提供的RabbitMQ.Client的nuget包,就目前这个nuget而言4.0.0及以上版本必须要NETFramework 4.5.1及以上版本或netcore版本才允许使用,笔者这里用的是Framework4.5框架所以引用了此版本的nuget包: Install-Package RabbitMQ.Client -Version 3.6.6 引用过后就是往前面讲的队列工厂填写代码,首先继承统一配置文件读取类 PublicClass.ConfClass<QRabbitMQ> ,然后实现 IQueue 接口,这里封装了RabbitMq常用的几个操作方法,具体代码: 1 /// <summary> 2 /// RabbitMq 3 /// </summary> 4 public class QRabbitMQ : PublicClass.ConfClass<QRabbitMQ>, IQueue 5 { 6 private IConnection con = null; 7 8 public void Create() 9 { 10 if (string.IsNullOrWhiteSpace(this.ApiUrl) || string.IsNullOrWhiteSpace(this.ApiKey)) { throw new Exception("创建RabbitMq队列需要指定队列:HostName和Port"); } 11 12 try 13 { 14 var factory = new ConnectionFactory() { HostName = this.ApiUrl, Port = Convert.ToInt32(this.ApiKey) }; 15 con = con ?? factory.CreateConnection(); 16 } 17 catch (Exception ex) 18 { 19 throw new Exception(ex.Message); 20 } 21 } 22 23 public long Total(string name = "Redis_01") 24 { 25 if (con == null) { throw new Exception("请先创建队列连接"); } 26 using (var channel = con.CreateModel()) 27 { 28 return channel.MessageCount(name); 29 } 30 } 31 32 public Message Read(string name = "RabbitMQ_01") 33 { 34 if (con == null) { throw new Exception("请先创建队列连接"); } 35 if (string.IsNullOrWhiteSpace(name)) { throw new Exception("name不能为空"); } 36 37 var message = new Message(); 38 message.Label = name; 39 message.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) }); 40 using (var channel = con.CreateModel()) 41 { 42 var baseResult = channel.BasicGet(name, true); //true:获取后删除队列 false:不删除 43 if (baseResult == null) { return message; } 44 var body = baseResult.Body; 45 message.Body = Encoding.UTF8.GetString(body); 46 } 47 return message; 48 } 49 50 public bool Write(string content, string name = "RabbitMQ_Queue01") 51 { 52 if (con == null) { throw new Exception("请先创建队列连接"); } 53 if (string.IsNullOrWhiteSpace(content) || string.IsNullOrWhiteSpace(name)) { throw new Exception("content和name不能为空"); } 54 55 using (var channel = con.CreateModel()) 56 { 57 channel.QueueDeclare(name, false, false, false, null); 58 var body = Encoding.UTF8.GetBytes(content); 59 60 channel.BasicPublish(string.Empty, name, null, body); 61 return true; 62 } 63 } 64 65 public void Dispose() 66 { 67 if (con != null) 68 { 69 con.Close(); 70 con.Dispose(); 71 con = null; 72 } 73 } 74 } 代码主要使用流程是:创建(Create)-》读(Read)|写(Write)-》释放(Dispose);有了具体的RabbitMq实现类,那么在工厂中直接通过泛型映射来获取该实现类的对象: 1 /// <summary> 2 /// ================== 3 /// author:神牛步行3 4 /// des:该列工厂开源,包括队列有MSMQ,RedisMQ,RabbitMQ 5 /// blogs:http://www.cnblogs.com/wangrudong003/ 6 /// ================== 7 /// 队列工厂 8 /// </summary> 9 public class QueueReposity<T> where T : class,IQueue, new() 10 { 11 public static IQueue Current 12 { 13 get 14 { 15 return PublicClass.ConfClass<T>.Current; 16 } 17 } 18 } » 队列工厂之RabbitMQ测试用例 通过上面配置环境和封装自己的方法,这里写了一个简单的测试用例,分为Server(加入消息队列)和Client(获取消息队列),首先来看Server端的代码: 1 /// <summary> 2 /// 队列服务端测试用例 3 /// </summary> 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 //Redis_Server(); 9 10 RabbitMQ_Server(); 11 12 //MSMQ_Server(); 13 } 14 private static void RabbitMQ_Server() 15 { 16 //实例化QMsmq对象 17 var mq = QueueReposity<QRabbitMQ>.Current; 18 19 try 20 { 21 Console.WriteLine("Server端创建:RabbitMQ实例"); 22 mq.Create(); 23 24 var num = 0; 25 do 26 { 27 Console.WriteLine("输入循环数量(数字,0表示结束):"); 28 var readStr = Console.ReadLine(); 29 num = string.IsNullOrWhiteSpace(readStr) ? 0 : Convert.ToInt32(readStr); 30 31 Console.WriteLine("插入数据:"); 32 for (int i = 0; i < num; i++) 33 { 34 var str = "我的编号是:" + i; 35 mq.Write(str); 36 Console.WriteLine(str); 37 } 38 } while (num > 0); 39 } 40 catch (Exception ex) 41 { 42 } 43 finally 44 { 45 Console.WriteLine("释放。"); 46 mq.Dispose(); 47 } 48 Console.ReadLine(); 49 } 50 } 通过:创建(Create)-》读(Read)|写(Write)-》释放(Dispose) 的流程来使用我们的队列工厂,感觉挺简单的,此时我们运行下这个Server端,然后录入参数: 这个时候就往RabbitMq队列中加入了11条数据,我们通过她的后台去找刚才添加的队列: 能够看到我们刚刚插入的队列总数和名称,如果你想看里面具体内容,可以点击名字“mq_01”进入某一个队列的界面,往下面拉滚动条找到“Get messages”选项,默认查看Messages是1我们修改为10,再点击get messages就能够看到如下图我们刚才插入的具体内容了: 截图有点长哦,不知道dudu会不会怪我哈哈,到这里能看到队列插入是成功的,然后我们来通过client端消费队列,具体代码: 1 /// <summary> 2 /// 队列客户端测试用例 3 /// </summary> 4 class Program 5 { 6 static void Main(string[] args) 7 { 8 //RedisMQ_Client(); 9 10 RabbitMQ_Client(); 11 12 //MSMQ_Client(); 13 } 14 15 private static void RabbitMQ_Client() 16 { 17 //实例化QMsmq对象 18 var mq = QueueReposity<QRabbitMQ>.Current; 19 try 20 { 21 Console.WriteLine("Client端创建:RabbitMQ实例"); 22 mq.Create(); 23 24 while (true) 25 { 26 try 27 { 28 var total = mq.Total(); 29 if (total > 0) { Console.WriteLine("队列条数:" + total); } 30 31 var result = mq.Read(); 32 if (result.Body == null) { continue; } 33 Console.WriteLine(string.Format("接受队列{0}:{1}", result.Label, result.Body)); 34 } 35 catch (Exception ex) 36 { Console.WriteLine("异常信息:" + ex.Message); } 37 } 38 } 39 catch (Exception ex) 40 { 41 throw ex; 42 } 43 finally 44 { 45 Console.WriteLine("释放。"); 46 mq.Dispose(); 47 } 48 } 49 } 再来咋们运行exe看下效果: 此刻刚刚加入队列中的数据就读取出来了,这个时候我们再看Rabbitmq控制台,get messages已经获取不出来具体的内容信息了,因为这个客户端消费了数据,队列中的数据自动清除了,至于是否想清除数据这个设置在代码: 1 var baseResult = channel.BasicGet(name, true); //true:获取后删除队列 false:不删除 以上对封装RabbitMQ的代码分享和环境搭建讲解,希望能给您带来好的帮助,谢谢阅读;
最近vs2017神器正式版发布让人很是激动,vs2017支持了很多语言的开发,从前端-后端-底层的支持,堪称是工具中的神器;netcore我喜爱的架构之一也得到了大力的宣传,应群友的邀请将在队列工厂(msmq,redis,rabbitmq)一些列文章过后,继续增加.netcore方面的文章,只为.netcore发展更好贡献一份微弱的力量;本章内容分享的是队列(msmq,redis,rabbitmq)封装的队列工厂之MSMQ希望大家能够喜欢,也希望各位多多"扫码支持"和"推荐"谢谢! » 创建队列工厂QueueReposity<T> . 队列公共操作接口IQueue . 配置文件操作类ConfClass<T> . 非安全单例创建队列实例 » Win7和Server2008安装MSMQ支持 » MSMQ测试用例(服务端+客户端) 下面一步一个脚印的来分享: » 创建队列工厂QueueReposity<T> 首先,因为这里需要统一封装几个常用的队列方式的用法,因此采用了简单工厂模式,所以有了QueueReposity<T>; . 队列公共操作接口IQueue 工厂模式的特性创建实例,因为这里封装的都是队列,故而能提取出统一的规则来,因此定义了如下接口(这里没有考虑一些队列兼容的异步方法请忽略): 1 /// <summary> 2 /// 队列公共操作 3 /// </summary> 4 public interface IQueue : IDisposable 5 { 6 /// <summary> 7 /// 创建队列 8 /// </summary> 9 void Create(); 10 11 /// <summary> 12 /// 总数 13 /// </summary> 14 /// <returns></returns> 15 int Total(); 16 17 /// <summary> 18 /// 读取一个队列 19 /// </summary> 20 /// <returns></returns> 21 Message Read(); 22 23 ///// <summary> 24 ///// 读取多个队列 25 ///// </summary> 26 ///// <returns></returns> 27 //List<Message> ReadAll(); 28 29 /// <summary> 30 /// 写入队列 31 /// </summary> 32 /// <returns></returns> 33 bool Write(string content, string name = ""); 34 } . 配置文件操作类ConfClass<T> 因为每个队列的都有自己的配置信息,因此封装了统一管理的配置文件读取类ConfClass<T>,来读取配置在同一个xml文件中的配置信息,如下封装了自定义配置文件的属性和读取方法: 1 #region 文件操作类 2 /// <summary> 3 /// 配置文件操作类 4 /// </summary> 5 /// <typeparam name="T"></typeparam> 6 public class ConfClass<T> where T : class,new() 7 { 8 9 public ConfClass() { 10 11 var apiNodeName = this.GetType().Name; 12 Reader(apiNodeName); 13 } 14 15 #region 单例模式 16 17 public static readonly object Singleton_Lock = new object(); 18 19 /// <summary> 20 /// 单例对象 21 /// </summary> 22 private static T t = default(T); 23 24 /// <summary> 25 /// 通过方法获取单例 26 /// </summary> 27 /// <param name="t"></param> 28 /// <returns></returns> 29 public static T GetInstance(T t) 30 { 31 t = t ?? new T(); 32 return t; 33 } 34 35 /// <summary> 36 /// 通过属性获取单例(在继承的时候使用) 37 /// </summary> 38 public static T Current 39 { 40 get 41 { 42 t = t ?? new T(); 43 return t; 44 } 45 } 46 47 #endregion 48 49 #region 配置文件操作 50 51 #region 配置文件属性 52 /// <summary> 53 /// 配置文件地址 54 /// </summary> 55 //public string ConfPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Conf", "ShenNiuApi.xml"); 56 public string ConfPath = @"C:\Conf\ShenNiuApi.xml"; 57 58 /// <summary> 59 /// 配置文件父节点名称 60 /// </summary> 61 public string ConfParentNodeName = "ShenNiuApi"; 62 63 /// <summary> 64 /// 配置文件内容 65 /// </summary> 66 public string ConfContent { get; set; } 67 68 /// <summary> 69 /// 配置文件文档doc对象 70 /// </summary> 71 public XmlDocument doc { get; set; } 72 73 74 /// <summary> 75 /// 账号 76 /// </summary> 77 public string UserName { get; set; } 78 79 /// <summary> 80 /// 密码 81 /// </summary> 82 public string UserPwd { get; set; } 83 84 /// <summary> 85 /// 接口地址 86 /// </summary> 87 public string ApiUrl { get; set; } 88 89 /// <summary> 90 /// 秘钥 91 /// </summary> 92 public string ApiKey { get; set; } 93 94 #endregion 95 96 public ConfClass(string ConfPath, string ConfParentNodeName="") 97 { 98 99 this.ConfPath = string.IsNullOrWhiteSpace(ConfPath) ? this.ConfPath : ConfPath; 100 this.ConfParentNodeName = string.IsNullOrWhiteSpace(ConfParentNodeName) ? this.ConfParentNodeName : ConfParentNodeName; 101 102 var apiNodeName = this.GetType().Name; 103 Reader(apiNodeName); 104 } 105 106 /// <summary> 107 /// 读取配置信息 108 /// </summary> 109 /// <param name="apiNodeName"></param> 110 public void Reader(string apiNodeName) 111 { 112 try 113 { 114 if (string.IsNullOrWhiteSpace(ConfPath) || string.IsNullOrWhiteSpace(ConfParentNodeName)) 115 { 116 throw new Exception("配置文件地址或者配置文件父节点名称不能为空"); 117 } 118 119 if (!File.Exists(ConfPath)) { return; } 120 121 //获取配置文件信息 122 using (StreamReader reader = new StreamReader(ConfPath)) 123 { 124 this.ConfContent = reader.ReadToEndAsync().Result; 125 } 126 127 if (string.IsNullOrWhiteSpace(this.ConfContent)) { return; } 128 129 //加入doc中 130 this.doc = new XmlDocument(); 131 this.doc.LoadXml(this.ConfContent); 132 133 //解析 134 var parentNode = string.Format("{0}/{1}", this.ConfParentNodeName, apiNodeName); 135 var apiNode = this.doc.SelectSingleNode(parentNode); 136 if (apiNode == null) { throw new Exception("未能找到" + parentNode + "节点"); } 137 138 this.UserName = apiNode.SelectSingleNode("UserName").InnerText; 139 this.UserPwd = apiNode.SelectSingleNode("UserPwd").InnerText; 140 this.ApiUrl = apiNode.SelectSingleNode("ApiUrl").InnerText; 141 this.ApiKey = apiNode.SelectSingleNode("ApiKey").InnerText; 142 } 143 catch (Exception ex) 144 { 145 146 throw new Exception("加载配置文件" + this.ConfPath + "异常:" + ex.Message); 147 } 148 } 149 #endregion 150 } 151 #endregion 这个配置文件的类主要运用在队列实例继承上,只要继承了默认就会读取响应的配置节点信息;配置xml文件默认存储的地址: C:\Conf\ShenNiuApi.xml ,最大父节点名称默认:ShenNiuApi,格式如下所示: 1 <ShenNiuApi> 2 <QMsmq> 3 <UserName></UserName> 4 <UserPwd></UserPwd> 5 <ApiUrl>.\Private$\MyMsmq</ApiUrl> 6 <ApiKey></ApiKey> 7 </QMsmq> 8 </ShenNiuApi> . 非安全单例创建队列实例 由于工厂都是专门用来提供实例的存在,创建实例的模式也有很多这种,这里我选择的是非安全单例创建队列实例,所有在ConfClass类中默认加入了单例模式: 1 #region 单例模式 2 3 public static readonly object Singleton_Lock = new object(); 4 5 /// <summary> 6 /// 单例对象 7 /// </summary> 8 private static T t = default(T); 9 10 /// <summary> 11 /// 通过方法获取单例 12 /// </summary> 13 /// <param name="t"></param> 14 /// <returns></returns> 15 public static T GetInstance(T t) 16 { 17 t = t ?? new T(); 18 return t; 19 } 20 21 /// <summary> 22 /// 通过属性获取单例(在继承的时候使用) 23 /// </summary> 24 public static T Current 25 { 26 get 27 { 28 t = t ?? new T(); 29 return t; 30 } 31 } 32 33 #endregion 因此这里所说的工厂模式通过泛型传递类型,再创建实例的具体代码只有这么点,简短精炼: 1 /// <summary> 2 /// 队列工厂 3 /// </summary> 4 public class QueueReposity<T> where T : class,IQueue, new() 5 { 6 public static IQueue Current 7 { 8 get 9 { 10 return PublicClass.ConfClass<T>.Current; 11 } 12 } 13 } » Win7和Server2008安装MSMQ支持 上面分享的是队列工厂的结构,到这里就要开始我们的第一个MSMQ队列的安装和封装分享了;首先来看Win7测试环境上怎么安装MSMQ的支持:开始菜单-》控制面板-》程序和功能: -》打开或关闭Windows功能-》勾选如图所示队列安装组件: -》确定等待安装完成;到此win7安装msmq就完成了,因为msmq是系统默认的所以安装起来很方便,当然server2008也差不多,按照如下操作安装(这里我使用租的阿里云Server2008R2服务器为例):开始-》控制面板-》程序(下面的打开或关闭Window功能)->功能-》添加功能-》消息队列: 在server上安装的步骤基本没啥变化,是不是很简单;安装完成后这样你的电脑或服务器就支持msmq了,此刻的你是不是很兴奋,觉得又能学到新东西了呵呵; » MSMQ测试用例(服务端+客户端) 首先,这里我用控制台程序做测试用例,我分为客户端和服务端,用服务端通过分装的插入队列方法插入数据,然后通过客户端读取队列信息,先来上个图撑撑场面吧: 这里我创建了MSMQ的分装类 public class QMsmq : PublicClass.ConfClass<QMsmq>, IQueue 实现了队列接口IQueue和继承配置文件类ConfClass<QMsmq>,此时具体的方法体如下: 1 public class QMsmq : PublicClass.ConfClass<QMsmq>, IQueue 2 { 3 4 5 private MessageQueue _msmq = null; 6 7 public void Create() 8 { 9 if (string.IsNullOrWhiteSpace(this.ApiUrl)) { throw new Exception("创建队列需要指定队列:地址"); } 10 11 _msmq = MessageQueue.Exists(this.ApiUrl) ? 12 new MessageQueue(this.ApiUrl) : 13 _msmq ?? MessageQueue.Create(this.ApiUrl); 14 //设置数据格式 15 _msmq.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) }); 16 } 17 18 public int Total() 19 { 20 if (_msmq == null) { throw new Exception("请先创建队列"); } 21 return _msmq.GetAllMessages().Length; 22 } 23 24 public Message Read() 25 { 26 try 27 { 28 if (_msmq == null) { throw new Exception("请先创建队列"); } 29 30 //60s超时 31 return _msmq.Receive(TimeSpan.FromSeconds(60)); 32 } 33 catch (Exception ex) 34 { 35 throw new Exception(ex.Message); 36 } 37 } 38 39 //public List<Message> ReadAll() 40 //{ 41 // try 42 // { 43 // if (_msmq == null) { throw new Exception("请先创建队列"); } 44 45 // var messages = _msmq.GetAllMessages(); 46 // return messages.ToList(); 47 // } 48 // catch (Exception ex) 49 // { 50 // throw new Exception(ex.Message); 51 // } 52 //} 53 54 public bool Write(string content, string name = "") 55 { 56 try 57 { 58 if (_msmq == null) { throw new Exception("请先创建队列"); } 59 if (string.IsNullOrWhiteSpace(content)) { throw new Exception("填充内容不能为空"); } 60 61 var message = new Message(); 62 message.Body = content; 63 message.Label = name; 64 _msmq.Send(message); 65 return true; 66 } 67 catch (Exception ex) 68 { 69 throw new Exception(ex.Message); 70 } 71 } 72 73 public void Dispose() 74 { 75 if (_msmq != null) 76 { 77 _msmq.Close(); 78 _msmq.Dispose(); 79 _msmq = null; 80 } 81 } 82 } 到这里我们的MSMQ简单封装代码已经完成了,咋们再来通过控制台调用下这个队列客户端代码: 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Client(); 6 } 7 8 /// <summary> 9 /// 客户端 10 /// </summary> 11 private static void Client() 12 { 13 //实例化QMsmq对象 14 var msmq = QueueReposity<QMsmq>.Current; 15 try 16 { 17 Console.WriteLine("创建:msmq"); 18 msmq.Create(); 19 20 while (true) 21 { 22 try 23 { 24 var result = msmq.Read(); 25 Console.WriteLine(string.Format("接受第{0}个:{1}", result.Label, result.Body)); 26 } 27 catch (Exception ex) 28 { Console.WriteLine("异常信息:" + ex.Message); } 29 } 30 } 31 catch (Exception ex) 32 { 33 throw ex; 34 } 35 finally 36 { 37 Console.WriteLine("释放。"); 38 msmq.Dispose(); 39 } 40 } 41 } 这里能够看出客户端代码中使用MSMQ步骤主要有:QueueReposity<QMsmq>.Current工厂创建自定义队列实例-》Create()创建-》Read()读取-》Dispose()释放mq,流程还算清晰吧;如下服务端代码: 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Server(); 6 } 7 8 /// <summary> 9 /// 服务端 10 /// </summary> 11 private static void Server() 12 { 13 //实例化QMsmq对象 14 var msmq = QueueReposity<QMsmq>.Current; 15 16 try 17 { 18 Console.WriteLine("创建:msmq"); 19 msmq.Create(); 20 21 var num = 0; 22 do 23 { 24 Console.WriteLine("输入循环数量(数字,0表示结束):"); 25 var readStr = Console.ReadLine(); 26 num = string.IsNullOrWhiteSpace(readStr) ? 0 : Convert.ToInt32(readStr); 27 28 Console.WriteLine("插入数据:"); 29 for (int i = 0; i < num; i++) 30 { 31 var str = "我的编号是:" + i; 32 msmq.Write(str, i.ToString()); 33 Console.WriteLine(str); 34 } 35 } while (num > 0); 36 } 37 catch (Exception ex) 38 { 39 } 40 finally 41 { 42 Console.WriteLine("释放。"); 43 msmq.Dispose(); 44 } 45 Console.ReadLine(); 46 } 47 } 服务端的步骤几乎和客户端差不多,区别在于一个读取一个写入,服务端步骤:QueueReposity<QMsmq>.Current工厂创建自定义队列实例-》Create()创建-》Write()写入-》Dispose()释放mq;以上对MSMQ的代码分享和环境搭建讲解,希望能给您带来好的帮助,谢谢阅读;
一、Session 1、Session 介绍 我相信,搞Web开发的对Session一定再熟悉不过了,所以我就简单的介绍一下。 Session:在计算机中,尤其是在网络应用中,称为“会话控制”。 每个用户(浏览器)首次与web服务器建立连接时,就会产生一个Session,同时服务器会分配一个SessionId给用户的浏览器。我们可以用Fiddler查看cookies中,会看到有一个ASP.Net_SessionId的cookie。大家都知道Http是无状态请求,但是ASP.Net中的Session仿佛又让Http请求变得有状态,其核心就在于这个叫ASP.Net_SessionId的cookie。大家可以想象一下,这个相当于数据库的Key,服务器那边再有个Session内容缓存表,是不是Session的内容就很容易得到了?当然Session不是那么简单,但Session原理不是本文介绍重点,所以请大家自行度娘。 2、又爱又恨的Session 刚接触程序开发的人一定爱死Session了,因为Session让Http从无状态变成有状态了,页面之间传值、用户相关信息、一些不变的数据、甚至于查出来的DataTable也可以放进去,取值的时候只需要Session[Key]即可,真是方便极了。Session真是个利器,人挡杀人佛挡杀佛,但任何事物被封为利器基本也是双刃剑,Session的许多问题我们不得不去面对。 【常见问题请见下图】 我相信一见到这个问题,老程序员都会心里一哆嗦,Session是导致这个原因之一,大家也会想到这个情景,“我去,是不是Session又丢了,让用户重新登录”,事故报告中会填写:.NET规定,用户登陆后长时间没操作导致的。解决方案为:把Session时间调到9999。 结果该发生的还是继续发生着,Session照样丢失。 【常见Session丢失原因】 1、Session超时,用户打开页面,页面长时间不操作会导致此原因 2、IIS应用程序池回收,或者重启 3、Web.Config修改,即IIS应用程序池重启 4、dll被替换或者动态页面修改,即IIS应用程序池重启 5、杀毒软件对.config文件进行扫描,可能会导致IIS应用程序池回收 6、用户浏览器禁用cookie 7、其他原因 其他原因有点不负责,但是好多程序员无法查明是什么原因导致Session丢失,但Session丢失我归结为两大类,一个是数据的Key丢了,一个是Session内容数据库的丢了,大家这样就好理解了,用户浏览器禁用cookie一定是Key没了。IIS应用程序池回收必定会导致Session的内容缓存表丢失,当然还有一些其他原因。 3、解决Session丢失的漫长路 解决过Session丢失的都会用到这几种方法 1、InProc:将Session存到进程内。 2、StateServer:将Session存到独立的状态服务中(Asp.Net State Service)。 3、SqlServer:将Session存到SqlServer中。 4、Cookieless:设置客户端Session存储的方式。 用了这些方法之后,有的是该丢还丢,有的是稳定了速度却慢了。 大家也注意到了,还有个这个Custom自定义模式,有人会说:“除了大牛,有几个敢写的啊,写出来有问题怎么办,算了算了。” 等等,大家不要还停留在非开源模式下解决问题的思想,找找开源项目,一定能找到的,有人说ASP.NET上哪里找开源啊,非常简单NuGet,如果想了解开源,一定要学会使用NuGet。 二、Redis 1、前言 上文说了那么多,有人一定会说我是来解决Session丢失的,上哪里来的Session分布式共享,标题党,我还是继续用我的cookie吧。 我要说的是,几年前,在Stack Overflow上找到了这个方法解决了丢失问题,之后,发现这种方法还可以实现Session分布式共享。那就是运用Custom自定义模式,将Session持久化到Memcache和Redis中。Session丢失、以及持久化到SqlServer数据的性能问题也随之解决。 此种方法很适合老项目中大量应用Session而导致法搞成分布式而苦恼的.NET开发人员使用。因为很有可能老项目维护过程中,身边的JAVA团队、PHP团队,正在重构你的项目。 2、RedisSessionProvider 正文开始,首先,沿着我们的思路Session持久化到Memcache或者Redis中,通过nuget下载 RedisSessionProvider(别问我怎么找到的,因为我英文过了四级,我会使用度娘,嘿嘿) 【web.config配置如下】 <system.web> <sessionState mode="Custom" customProvider="RedisSessionProvider"> <providers> <add name="RedisSessionProvider" type="RedisSessionProvider.RedisSessionStateStoreProvider, RedisSessionProvider"/> </providers> </sessionState> </system.web> 【Global.asax】 void Application_Start(object sender, EventArgs e) { StackExchange.Redis.ConfigurationOptions redisConfigOpts = StackExchange.Redis.ConfigurationOptions.Parse("192.168.8.138:6379"); RedisSessionProvider.Config.RedisConnectionConfig.GetSERedisServerConfig = (HttpContextBase context) => { return new KeyValuePair<string, StackExchange.Redis.ConfigurationOptions>( "DefaultConnection", redisConfigOpts); }; } 【存储方法】 Session["Test"] = "aa"; 【调用方法】 string str = Session["Test"].ToString() !前方坑,请注意! 如果你配置好Redis,并且做好上面这些配置,运行会出现以下问题,请更新RedisSessionProvider的依赖包StackExchange.Redis到最新。 3、Redis安装 3-1、Redis for windows下载 如果会配置Redis的同学,请略过此章节,直接进入Nginx。 Redis下载:https://github.com/MSOpenTech/redis !此处为坑,请注意! 修改Redis.windows.conf,如果不修改,远程不能访问Redis 1、将bind 127.0.0.1 改成了bind 0.0.0.0。注意:进入生产环境时候,要启用密码,否则会是Redis漏洞,具体请自行度娘和自己公司的运维阿牛 2、protected-mode yes 改成 protected-mode no 详细修改的传送门: redis开启远程访问 3-2、启动Redis redis-server redis.windows.conf 上图为redis启动成功,默认6379,可以通过redis-cli进行测试,看别的机子能否访问。还可以在找个redis可视化工具看看里面存了啥,也可以监控Session是否持久化到Redis中了。 3-3、验证Session是否持久化到Redis 运行RedisSessionProvider这个项目。同一个IIS下,同域名,不同IP,同一浏览器,不同端口一个是2459,一个是2490。 【注意】 不同浏览器SessionId是不同的。必须保证SessionId,测试必须是同一个浏览器进程分出的不同子标签才可以,这样SessionId是共享的。 感觉成功了,让我们看看这样的拓扑图: 囧……这是啥玩意?我的分布式呢?这个拓扑图很显然不是分布式啊,还两个IP,我还要在前面做个路由登录页面?这时Nginx该登场了。 三、Ngnix 1、Ngnix安装&下载 下载地址:http://nginx.org/ 2、nginx.conf配置修改 2-1、【接口修改】 listen 80; 改成 listen 1100; 因为一般都被80都被使用。 2-2、【增加负载均衡】 upstream Jq_one { server 127.0.0.1:8770; server 192.168.8.138:7777; } server { ..... } 2-3、【location节点修改】 location / { root html; index index.aspx index.html index.htm; #其中jq_one 对应着upstream设置的集群名称 proxy_pass http://Jq_one; #设置主机头和客户端真实地址,以便服务器获取客户端真实IP proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } 2-4、【Nginx启动命令】 C:\server\nginx-1.0.2>start nginx 或 C:\server\nginx-1.0.2>nginx.exe 2-5、【Nginx重新载入命令】 C:\server\nginx-1.0.2>nginx.exe -s reload 2-6、【参考文章】 详细配置传送门:nginx+iis实现负载均衡 四、Session分布式共享 1、拓扑图 通过Nginx+Redis实现对Session的分布式共享功能。通过测试,发现Session分布式共享共有两种解决方案。 2、利用Nginx的Ip_Hash进行Session分布式共享 使用nginx将同一ip的请求分配到固定服务器,修改如下。ip_hash会计算ip对应hash值,然后分配到固定服务器 upstream Jq_one{ server 127.0.0.1:8770; server 192.168.8.138:7777; ip_hash; } 效果可以理解为就是一个Ip,通过Nginx路由到IIS_1上面,在多次请求,会一直在IIS_1上,不会路由到IIS_2上面。 3、利用MachineKey进行Session分布式共享 Ip_Hash在一定程度上解决了Session分布式共享的问题,但是总感觉没有发挥出nginx均衡负载的功能,继续改造 3-1、现将Ip_Hash去掉 去掉Ip_Hash重启Nginx,打开网站,点击设置Session按钮,结果报错 3-2、web.config添加MachineKey <machineKey validationKey="86B6275BA31D3D713E41388692FCA68F7D20269411345AA1C17A7386DACC9C46E7CE5F97F556F3CF0A07159659E2706B77731779D2DA4B53BC47BFFD4FD48A54" decryptionKey="9421E53E196BB56DB11B9C25197A2AD470638EFBC604AC74CD29DBBCF79D6046" validation="SHA1" decryption="AES" /> 【注意】 负载均衡的两个网站的MachineKey必须一样,否则出问题。 3-3、演示 下图,大家可以看到,服务器的Ip在不断变化,而Session却没有丢失,至此实现了Session分布式共享。 五、后记&感悟 希望能通过本文,解决有的项目中Session分布式共享和Session丢失的难题,给大家一些解决问题、分析问题启发。 ASP.Net给我们带来了新的一种编码体验,如今.Net已经15岁了,.Net的在企业中发展中扮演最多的角色是快枪手和背锅侠的角色,在企业刚起步时候选择易上手的.Net无非是最好的选择之一,但是因为.Net的高度封装,让.Net高级人才在市场上十分稀少,而且企业在创立之初应用.net的时候也不会考虑架构之类的问题。可是随着业务越来越复杂,.Net开发人员无法解决和满足市场的需求和项目中出现的技术难题,技术债随之产生,解决不了问题随之一些程序员便让.Net背锅,再加上.Net的新技术推陈出新(有好多人说微软瞎折腾,囧),WebForm、mvc、silverlight、sharepoint、wpf、window phone、wcf等等。可是中国大环境并不买账,慢慢成长的企业发现.Net现有架构无法满足,.Net开发人员又无法解决现有问题,又找不到.Net架构师之类的角色,又看到了京东等大厂转JAVA的成功,其他企业家便会想咱也转个JAVA试试,招聘Java架构师,结果一大堆人应聘(高级的、中级的、技术总监),企业家高兴坏了,很快JAVA便组建了一支精英团队准备重构.net项目。JAVA发展的历史比Net要长很多,早些做JAVA开发早已转型为管理,你会发现现在市场上,挖过来的其中一些CTO很喜欢组建JAVA、PHP团队,NET团队很少,原因多数是.Net不开源、不跨平台、解决方案不成熟,背后的原因就不得而知了。但不得不说,JAVA语言很容易培养牛人,因为当你学了JAVA中的Spring,你就开始接触了IOC容器,你就在慢慢的面向接口编程,当你学会了的AOP,你就开始在面向方面编程的道路上迈出了一小步。语言只是一个工具,干了这么多年.Net,看看招聘信息或多或少有了些迷茫,路是自己走的,做出的决定就不能后悔,但在十字路口时,还是多想一想。 以上为个人观点,有可能因为知识和阅历的原因,分析片面,请多谅解。 六、参考文章 redis开启远程访问 nginx+iis实现负载均衡 ASP.NET性能优化之分布式Session .Net分布式架构(一):Nginx实现负载均衡 .Net分布式架构(二):基于Redis的Session共享 非常感谢上述文章,对本文的启发,谢谢。
很早前就仓促的接触过activemq,但当时太赶时间.后面发现activemq 需要了解的东西实在是太多了. 关于activemq 一直想起一遍文章.但也一直缺少自己的见解.或许是网上这些文章太多了.也可能是自己知识还不足够. 0,activemq-cpp 能解决什么问题. 实际应用就是让开发者能从多线程,多消息通信中解救出来.更多的关注应用逻辑. CMS (stands for C++ Messaging Service) is a JMS-like API for C++ for interfacing with Message Brokers such as Apache ActiveMQ. CMS helps to make your C++ client code much neater and easier to follow. To get a better feel for CMS try the API Reference. ActiveMQ-CPP is a client only library, a message broker such as Apache ActiveMQ is still needed for your clients to communicate. Our implementation of CMS is called ActiveMQ-CPP, which has an architecture that allows for pluggable transports and wire formats. Currently we support the OpenWire and Stomp protocols, both over TCP and SSL, we also now support a Failover Transport for more reliable client operation. In addition to CMS, ActiveMQ-CPP also provides a robust set of classes that support platform independent constructs such as threading, I/O, sockets, etc. You may find many of these utilities very useful, such as a Java like Thread class or the "synchronized" macro that let's you use a Java-like synchronization on any object that implements the activemq::concurrent::Synchronizable interface. ActiveMQ-CPP is released under the Apache 2.0 License 大意: CMS (C++ 消息 服务)是一个面象apache activemq 的 消息 中间层的C++接口. CMS的实现 叫做activemq-cpp ,不过当前只支持 openwire,amqp,TCP,ssl. 现在还支持 主备切换功能(这个是重点,当时我不懂,结果就走了弯路!_!). -_- ,意思是 activemq\conf\activemq.xml中的stomp,mqtt,ws 是没办法的. <transportConnectors> <!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB --> <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/> <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/> <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/> <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/> <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/> </transportConnectors> 1,acticvemq-cpp 的配置使用. 参考:Active MQ C++实现通讯 http://blog.csdn.net/lee353086/article/details/6777261 activemq-cpp下载地址: http://activemq.apache.org/cms/download.html 相关依赖库 在 http://activemq.apache.org/cms/building.html 中是有介绍的.不过是en的. 还是再说下吧.本人en也特差. "With versions of ActiveMQ-CPP 2.2 and later, we have a dependency on the Apache Portable Runtime project. You'll need to install APR on your system before you'll be able to build ActiveMQ-CPP." "The package contains a complete set of CppUnit tests. In order for you to build an run the tests, you will need to download and install the CppUnit library. See http://cppunit.sourceforge.net/cppunit-wiki" 所以就包含了:apr,apr-iconv,apr-util,cppunit. http://mirrors.hust.edu.cn/apache/apr/ 中可以下载 apr,apr-iconv,apr-util(版本号都找最高的,不要一高一低,不然编译会出问题). apr-1.5.1-win32-src.zip, apr-iconv-1.2.1-win32-src-r2.zip, apr-util-1.5.4-win32-src.zip. 解压后记得重命令文件夹,去掉版本号,改成如下图,不然工程编译时默认的 [附加包含目录] 是找不到的. 所有文件夹放在一个根目录下. 打开 activemq-cpp-library\vs2008-build\activemq-cpp.sln 依次添加[现在项目]:libapr.vcproj,libapriconv.vcproj,libaprutil.vcproj. 只需要lib项就行了. 最后项目图: libapriconv.vcproj,libaprutil.vcproj 的[项目依赖项]都需要libapr activemq-cpp 的[项目依赖项]需要libapriconv,libaprutil,libapr. activemq-cpp 的[附加包含目录] 需要包含 这三个的的include目录. 经过漫长的编译后, 这个大lib文件就出来. activemq-cpp-example 这个工程 ,就有 hello world 的代码. 2,activemq-cpp-example 项目代码解析. 通过这个项目可以让我们更好的认识 activemq-cpp的结构. /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // START SNIPPET: demo #include <activemq/library/ActiveMQCPP.h> #include <decaf/lang/Thread.h> #include <decaf/lang/Runnable.h> #include <decaf/util/concurrent/CountDownLatch.h> #include <decaf/lang/Integer.h> #include <decaf/lang/Long.h> #include <decaf/lang/System.h> #include <activemq/core/ActiveMQConnectionFactory.h> #include <activemq/util/Config.h> #include <cms/Connection.h> #include <cms/Session.h> #include <cms/TextMessage.h> #include <cms/BytesMessage.h> #include <cms/MapMessage.h> #include <cms/ExceptionListener.h> #include <cms/MessageListener.h> #include <stdlib.h> #include <stdio.h> #include <iostream> #include <memory> using namespace activemq::core; using namespace decaf::util::concurrent; using namespace decaf::util; using namespace decaf::lang; using namespace cms; using namespace std; class HelloWorldProducer : public Runnable { private: Connection* connection; Session* session; Destination* destination; MessageProducer* producer; int numMessages; bool useTopic; bool sessionTransacted; std::string brokerURI; private: HelloWorldProducer(const HelloWorldProducer&); HelloWorldProducer& operator=(const HelloWorldProducer&); public: HelloWorldProducer(const std::string& brokerURI, int numMessages, bool useTopic = false, bool sessionTransacted = false) : connection(NULL), session(NULL), destination(NULL), producer(NULL), numMessages(numMessages), useTopic(useTopic), sessionTransacted(sessionTransacted), brokerURI(brokerURI) { } virtual ~HelloWorldProducer(){ cleanup(); } void close() { this->cleanup(); } virtual void run() { try { // Create a ConnectionFactory auto_ptr<ConnectionFactory> connectionFactory( ConnectionFactory::createCMSConnectionFactory(brokerURI)); // Create a Connection connection = connectionFactory->createConnection(); connection->start(); // Create a Session if (this->sessionTransacted) { session = connection->createSession(Session::SESSION_TRANSACTED); } else { session = connection->createSession(Session::AUTO_ACKNOWLEDGE); } // Create the destination (Topic or Queue) if (useTopic) { destination = session->createTopic("TEST.FOO"); } else { destination = session->createQueue("TEST.FOO"); } // Create a MessageProducer from the Session to the Topic or Queue producer = session->createProducer(destination); producer->setDeliveryMode(DeliveryMode::NON_PERSISTENT); // Create the Thread Id String string threadIdStr = Long::toString(Thread::currentThread()->getId()); // Create a messages string text = (string) "Hello world! from thread " + threadIdStr; for (int ix = 0; ix < numMessages; ++ix) { std::auto_ptr<TextMessage> message(session->createTextMessage(text)); message->setIntProperty("Integer", ix); printf("Sent message #%d from thread %s\n", ix + 1, threadIdStr.c_str()); producer->send(message.get()); } } catch (CMSException& e) { e.printStackTrace(); } } private: void cleanup() { if (connection != NULL) { try { connection->close(); } catch (cms::CMSException& ex) { ex.printStackTrace(); } } // Destroy resources. try { delete destination; destination = NULL; delete producer; producer = NULL; delete session; session = NULL; delete connection; connection = NULL; } catch (CMSException& e) { e.printStackTrace(); } } }; class HelloWorldConsumer : public ExceptionListener, public MessageListener, public Runnable { private: CountDownLatch latch; CountDownLatch doneLatch; Connection* connection; Session* session; Destination* destination; MessageConsumer* consumer; long waitMillis; bool useTopic; bool sessionTransacted; std::string brokerURI; private: HelloWorldConsumer(const HelloWorldConsumer&); HelloWorldConsumer& operator=(const HelloWorldConsumer&); public: HelloWorldConsumer(const std::string& brokerURI, int numMessages, bool useTopic = false, bool sessionTransacted = false, int waitMillis = 30000) : latch(1), doneLatch(numMessages), connection(NULL), session(NULL), destination(NULL), consumer(NULL), waitMillis(waitMillis), useTopic(useTopic), sessionTransacted(sessionTransacted), brokerURI(brokerURI) { } virtual ~HelloWorldConsumer() { cleanup(); } void close() { this->cleanup(); } void waitUntilReady() { latch.await(); } virtual void run() { try { // Create a ConnectionFactory auto_ptr<ConnectionFactory> connectionFactory( ConnectionFactory::createCMSConnectionFactory(brokerURI)); // Create a Connection connection = connectionFactory->createConnection(); connection->start(); connection->setExceptionListener(this); // Create a Session if (this->sessionTransacted == true) { session = connection->createSession(Session::SESSION_TRANSACTED); } else { session = connection->createSession(Session::AUTO_ACKNOWLEDGE); } // Create the destination (Topic or Queue) if (useTopic) { destination = session->createTopic("TEST.FOO"); } else { destination = session->createQueue("TEST.FOO"); } // Create a MessageConsumer from the Session to the Topic or Queue consumer = session->createConsumer(destination); consumer->setMessageListener(this); std::cout.flush(); std::cerr.flush(); // Indicate we are ready for messages. latch.countDown(); // Wait while asynchronous messages come in. doneLatch.await(waitMillis); } catch (CMSException& e) { // Indicate we are ready for messages. latch.countDown(); e.printStackTrace(); } } // Called from the consumer since this class is a registered MessageListener. virtual void onMessage(const Message* message) { static int count = 0; try { count++; const TextMessage* textMessage = dynamic_cast<const TextMessage*> (message); string text = ""; if (textMessage != NULL) { text = textMessage->getText(); } else { text = "NOT A TEXTMESSAGE!"; } printf("Message #%d Received: %s\n", count, text.c_str()); } catch (CMSException& e) { e.printStackTrace(); } // Commit all messages. if (this->sessionTransacted) { session->commit(); } // No matter what, tag the count down latch until done. doneLatch.countDown(); } // If something bad happens you see it here as this class is also been // registered as an ExceptionListener with the connection. virtual void onException(const CMSException& ex AMQCPP_UNUSED) { printf("CMS Exception occurred. Shutting down client.\n"); ex.printStackTrace(); exit(1); } private: void cleanup() { if (connection != NULL) { try { connection->close(); } catch (cms::CMSException& ex) { ex.printStackTrace(); } } // Destroy resources. try { delete destination; destination = NULL; delete consumer; consumer = NULL; delete session; session = NULL; delete connection; connection = NULL; } catch (CMSException& e) { e.printStackTrace(); } } }; int main(int argc AMQCPP_UNUSED, char* argv[] AMQCPP_UNUSED) { activemq::library::ActiveMQCPP::initializeLibrary(); { std::cout << "=====================================================\n"; std::cout << "Starting the example:" << std::endl; std::cout << "-----------------------------------------------------\n"; // Set the URI to point to the IP Address of your broker. // add any optional params to the url to enable things like // tightMarshalling or tcp logging etc. See the CMS web site for // a full list of configuration options. // // http://activemq.apache.org/cms/ // // Wire Format Options: // ========================= // Use either stomp or openwire, the default ports are different for each // // Examples: // tcp://127.0.0.1:61616 default to openwire // tcp://127.0.0.1:61616?wireFormat=openwire same as above // tcp://127.0.0.1:61613?wireFormat=stomp use stomp instead // // SSL: // ========================= // To use SSL you need to specify the location of the trusted Root CA or the // certificate for the broker you want to connect to. Using the Root CA allows // you to use failover with multiple servers all using certificates signed by // the trusted root. If using client authentication you also need to specify // the location of the client Certificate. // // System::setProperty( "decaf.net.ssl.keyStore", "<path>/client.pem" ); // System::setProperty( "decaf.net.ssl.keyStorePassword", "password" ); // System::setProperty( "decaf.net.ssl.trustStore", "<path>/rootCA.pem" ); // // The you just specify the ssl transport in the URI, for example: // // ssl://localhost:61617 // std::string brokerURI = "failover:(tcp://localhost:61616" // "?wireFormat=openwire" // "&transport.useInactivityMonitor=false" // "&connection.alwaysSyncSend=true" // "&connection.useAsyncSend=true" // "?transport.commandTracingEnabled=true" // "&transport.tcpTracingEnabled=true" // "&wireFormat.tightEncodingEnabled=true" ")"; //============================================================ // set to true to use topics instead of queues // Note in the code above that this causes createTopic or // createQueue to be used in both consumer an producer. //============================================================ bool useTopics = true; bool sessionTransacted = false; int numMessages = 2000; long long startTime = System::currentTimeMillis(); HelloWorldProducer producer(brokerURI, numMessages, useTopics); HelloWorldConsumer consumer(brokerURI, numMessages, useTopics, sessionTransacted); // Start the consumer thread. Thread consumerThread(&consumer); consumerThread.start(); // Wait for the consumer to indicate that its ready to go. consumer.waitUntilReady(); // Start the producer thread. Thread producerThread(&producer); producerThread.start(); // Wait for the threads to complete. producerThread.join(); consumerThread.join(); long long endTime = System::currentTimeMillis(); double totalTime = (double)(endTime - startTime) / 1000.0; consumer.close(); producer.close(); std::cout << "Time to completion = " << totalTime << " seconds." << std::endl; std::cout << "-----------------------------------------------------\n"; std::cout << "Finished with the example." << std::endl; std::cout << "=====================================================\n"; } activemq::library::ActiveMQCPP::shutdownLibrary(); } // END SNIPPET: demo 从main()开始吧. 这样便简快速的实现了应用逻辑. 3,activemq的几种通信模式. 可以参考: http://shmilyaw-hotmail-com.iteye.com/blog/1897635 目前本人需要的是activemq-cpp的 request-response 模式. 4,activemq-cpp的 request-response 模式的应用. 服务器与客户端通信 数据的交互 和 确认. 以下是本人修改后的简单代码,bug可能存在,请指出. 复制两份,一份定义 USE_COMSUMER 一份定义 USE_PRODUCER 就可以生成. /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // START SNIPPET: demo #include <activemq/library/ActiveMQCPP.h> #include <decaf/lang/Thread.h> #include <decaf/lang/Runnable.h> #include <decaf/util/concurrent/CountDownLatch.h> #include <decaf/lang/Integer.h> #include <decaf/lang/Long.h> #include <decaf/lang/System.h> #include <activemq/core/ActiveMQConnectionFactory.h> #include <activemq/util/Config.h> #include <cms/Connection.h> #include <cms/Session.h> #include <cms/TextMessage.h> #include <cms/BytesMessage.h> #include <cms/MapMessage.h> #include <cms/ExceptionListener.h> #include <cms/MessageListener.h> #include <stdlib.h> #include <stdio.h> #include <iostream> #include <memory> #include <decaf/util/Random.h> using namespace activemq::core; using namespace decaf::util::concurrent; using namespace decaf::util; using namespace decaf::lang; using namespace cms; using namespace std; #define QUEUE_NAME "eventQueue" #define NAME_BYTE_LEN 16 class HelloWorldProducer : public ExceptionListener, public MessageListener, public Runnable { private: CountDownLatch latch; CountDownLatch doneLatch; Connection* connection; Session* session; Destination* destination; MessageProducer* producer; int numMessages; bool useTopic; bool sessionTransacted; std::string brokerURI; bool bReciveMessage; long waitMillis; private: HelloWorldProducer(const HelloWorldProducer&); HelloWorldProducer& operator=(const HelloWorldProducer&); public: HelloWorldProducer(const std::string& brokerURI, int numMessages, bool useTopic = false, bool sessionTransacted = false, long waitMillis=3000) : latch(1), doneLatch(numMessages), connection(NULL), session(NULL), destination(NULL), producer(NULL), numMessages(numMessages), useTopic(useTopic), sessionTransacted(sessionTransacted), brokerURI(brokerURI) , bReciveMessage(false), waitMillis(waitMillis) { } virtual ~HelloWorldProducer(){ cleanup(); } void close() { this->cleanup(); } void waitUntilReady() { latch.await(); } virtual void run() { try { // Create a ConnectionFactory auto_ptr<ConnectionFactory> connectionFactory( ConnectionFactory::createCMSConnectionFactory(brokerURI)); // Create a Connection connection = connectionFactory->createConnection(); connection->start(); // Create a Session if (this->sessionTransacted) { session = connection->createSession(Session::SESSION_TRANSACTED); } else { session = connection->createSession(Session::AUTO_ACKNOWLEDGE); } session = connection->createSession(); // Create the destination (Topic or Queue) if (useTopic) { destination = session->createTopic(QUEUE_NAME); } else { destination = session->createQueue(QUEUE_NAME); } // Create a MessageProducer from the Session to the Topic or Queue producer = session->createProducer(destination); producer->setDeliveryMode(DeliveryMode::NON_PERSISTENT); // Create the Thread Id String string threadIdStr = Long::toString(Thread::currentThread()->getId()); // Create a messages string text = (string) "Hello world! from thread " + threadIdStr; for (int ix = 0; ix < numMessages; ++ix) { std::auto_ptr<TextMessage> message(session->createTextMessage(text)); //关键消息... std::auto_ptr<Destination> tempDest(session->createTemporaryQueue()); //cms::Destination tempDest=session->createTemporaryTopic() ; MessageConsumer * responseConsumer = session->createConsumer(tempDest.get()); responseConsumer->setMessageListener(this);//监听... message->setCMSReplyTo(tempDest.get()); Random random; char buffer[NAME_BYTE_LEN]={0}; random.nextBytes((unsigned char *)buffer,NAME_BYTE_LEN); string correlationId=""; for(int i=0;i<NAME_BYTE_LEN;++i) { char ch[NAME_BYTE_LEN*2]={0}; sprintf(ch,"%02X",(unsigned char)buffer[i]); string str(ch); correlationId+=str; } message->setCMSCorrelationID(correlationId); message->setIntProperty("Integer", ix); printf("Producer Sent message #%d from thread %s\n", ix + 1, threadIdStr.c_str()); producer->send(message.get()); // Indicate we are ready for messages. latch.countDown(); // Wait while asynchronous messages come in. doneLatch.await(waitMillis); } } catch (CMSException& e) { printf("Producer run() CMSException \n" ); // Indicate we are ready for messages. latch.countDown(); e.printStackTrace(); } } // Called from the Producer since this class is a registered MessageListener. virtual void onMessage(const Message* message) { static int count = 0; try { count++; const TextMessage* textMessage = dynamic_cast<const TextMessage*> (message); //ActiveMQMessageTransformation //std::auto_ptr<TextMessage> responsemessage(session->createTextMessage()); //responsemessage->setCMSCorrelationID(textMessage->getCMSCorrelationID()); //responsemessage->getCMSReplyTo() string text = ""; if (textMessage != NULL) { text = textMessage->getText(); } else { text = "NOT A TEXTMESSAGE!"; } printf("Producer Message #%d Received: %s\n", count, text.c_str()); //producer.send } catch (CMSException& e) { printf("Producer onMessage() CMSException \n" ); e.printStackTrace(); } // Commit all messages. if (this->sessionTransacted) { session->commit(); } // No matter what, tag the count down latch until done. doneLatch.countDown(); } // If something bad happens you see it here as this class is also been // registered as an ExceptionListener with the connection. virtual void onException(const CMSException& ex AMQCPP_UNUSED) { printf("Producer onException() CMS Exception occurred. Shutting down client. \n" ); ex.printStackTrace(); exit(1); } private: void cleanup() { if (connection != NULL) { try { connection->close(); } catch (cms::CMSException& ex) { ex.printStackTrace(); } } // Destroy resources. try { delete destination; destination = NULL; delete producer; producer = NULL; delete session; session = NULL; delete connection; connection = NULL; } catch (CMSException& e) { e.printStackTrace(); } } }; class HelloWorldConsumer : public ExceptionListener, public MessageListener, public Runnable { private: CountDownLatch latch; CountDownLatch doneLatch; Connection* connection; Session* session; Destination* destination; MessageConsumer* consumer; MessageProducer *producer; long waitMillis; bool useTopic; bool sessionTransacted; std::string brokerURI; private: HelloWorldConsumer(const HelloWorldConsumer&); HelloWorldConsumer& operator=(const HelloWorldConsumer&); public: HelloWorldConsumer(const std::string& brokerURI, int numMessages, bool useTopic = false, bool sessionTransacted = false, int waitMillis = 30000) : latch(1), doneLatch(numMessages), connection(NULL), session(NULL), destination(NULL), consumer(NULL), producer(NULL), waitMillis(waitMillis), useTopic(useTopic), sessionTransacted(sessionTransacted), brokerURI(brokerURI) { } virtual ~HelloWorldConsumer() { cleanup(); } void close() { this->cleanup(); } void waitUntilReady() { latch.await(); } virtual void run() { try { // Create a ConnectionFactory auto_ptr<ConnectionFactory> connectionFactory( ConnectionFactory::createCMSConnectionFactory(brokerURI)); // Create a Connection connection = connectionFactory->createConnection(); connection->start(); connection->setExceptionListener(this); // Create a Session if (this->sessionTransacted == true) { session = connection->createSession(Session::SESSION_TRANSACTED); } else { session = connection->createSession(Session::AUTO_ACKNOWLEDGE); } // Create the destination (Topic or Queue) if (useTopic) { destination = session->createTopic(QUEUE_NAME); } else { destination = session->createQueue(QUEUE_NAME); } producer = session->createProducer(); producer->setDeliveryMode(DeliveryMode::NON_PERSISTENT); // Create a MessageConsumer from the Session to the Topic or Queue consumer = session->createConsumer(destination); consumer->setMessageListener(this); std::cout.flush(); std::cerr.flush(); // Indicate we are ready for messages. latch.countDown(); // Wait while asynchronous messages come in. doneLatch.await(); } catch (CMSException& e) { printf("Consumer onException() CMS Exception occurred. Shutting down client. \n" ); // Indicate we are ready for messages. latch.countDown(); e.printStackTrace(); } } // Called from the consumer since this class is a registered MessageListener. virtual void onMessage(const Message* message) { static int count = 0; try { count++; // Create the Thread Id String string threadIdStr = Long::toString(Thread::currentThread()->getId()); static bool bPrintf=true; if(bPrintf) { bPrintf=false; printf("consumer Message threadid: %s\n", threadIdStr.c_str()); } string strReply="consumer return xxx,ThreadID="+threadIdStr; const TextMessage* textMessage = dynamic_cast<const TextMessage*> (message); if(NULL==textMessage) { printf("NULL==textMessage", message->getCMSType().c_str()); //const cms::MapMessage* mapMsg = dynamic_cast<const cms::MapMessage*>(message); //if(mapMsg) //{ // // std::vector<std::string> elements = mapMsg->getMapNames(); // std::vector<std::string>::iterator iter = elements.begin(); // for(; iter != elements.end() ; ++iter) // { // std::string key = *iter; // cms::Message::ValueType elementType = mapMsg->getValueType(key); // string strxxx; // int cc=0; // switch(elementType) { // case cms::Message::BOOLEAN_TYPE: // //msg->setBoolean(key, mapMsg->getBoolean(key)); // break; // case cms::Message::BYTE_TYPE: // //msg->setByte(key, mapMsg->getByte(key)); // break; // case cms::Message::BYTE_ARRAY_TYPE: // //msg->setBytes(key, mapMsg->getBytes(key)); // break; // case cms::Message::CHAR_TYPE: // //msg->setChar(key, mapMsg->getChar(key)); // break; // case cms::Message::SHORT_TYPE: // //msg->setShort(key, mapMsg->getShort(key)); // break; // case cms::Message::INTEGER_TYPE: // //msg->setInt(key, mapMsg->getInt(key)); // break; // case cms::Message::LONG_TYPE: // //msg->setLong(key, mapMsg->getLong(key)); // break; // case cms::Message::FLOAT_TYPE: // //msg->setFloat(key, mapMsg->getFloat(key)); // break; // case cms::Message::DOUBLE_TYPE: // //msg->setDouble(key, mapMsg->getDouble(key)); // break; // case cms::Message::STRING_TYPE: // //msg->setString(key, mapMsg->getString(key)); // strxxx=mapMsg->getString(key); // cc=1; // break; // default: // break; // } // } //} return; } std::auto_ptr<TextMessage> responsemessage(session->createTextMessage(strReply)); responsemessage->setCMSCorrelationID(textMessage->getCMSCorrelationID()); string text = ""; if (textMessage != NULL) { text = textMessage->getText(); } else { text = "NOT A TEXTMESSAGE!"; } int nProPerty=textMessage->getIntProperty("Integer"); printf("consumer Message #%d Received: %s,nProPerty[%d]\n", count, text.c_str(),nProPerty); const cms::Destination* destSend=textMessage->getCMSReplyTo(); if(destSend) { this->producer->send(destSend,responsemessage.get()); printf("consumer Message #%d send: %s\n", count, strReply.c_str()); } } catch (CMSException& e) { printf("Consumer onMessage() CMS Exception occurred. Shutting down client. \n" ); e.printStackTrace(); } // Commit all messages. if (this->sessionTransacted) { session->commit(); } // No matter what, tag the count down latch until done. //doneLatch.countDown(); } // If something bad happens you see it here as this class is also been // registered as an ExceptionListener with the connection. virtual void onException(const CMSException& ex AMQCPP_UNUSED) { printf("Consumer onException() CMS Exception occurred. Shutting down client. \n" ); //printf("CMS Exception occurred. Shutting down client.\n"); ex.printStackTrace(); exit(1); } private: void cleanup() { if (connection != NULL) { try { connection->close(); } catch (cms::CMSException& ex) { ex.printStackTrace(); } } // Destroy resources. try { delete destination; destination = NULL; delete consumer; consumer = NULL; delete session; session = NULL; delete connection; connection = NULL; } catch (CMSException& e) { e.printStackTrace(); } } }; int main(int argc AMQCPP_UNUSED, char* argv[] AMQCPP_UNUSED) { //if(argc<2) //{ // printf("argc<2\r\n"); // return 0; //} activemq::library::ActiveMQCPP::initializeLibrary(); { std::cout << "=====================================================\n"; std::cout << "Starting the example:" << std::endl; std::cout << "-----------------------------------------------------\n"; // Set the URI to point to the IP Address of your broker. // add any optional params to the url to enable things like // tightMarshalling or tcp logging etc. See the CMS web site for // a full list of configuration options. // // http://activemq.apache.org/cms/ // // Wire Format Options: // ========================= // Use either stomp or openwire, the default ports are different for each // // Examples: // tcp://127.0.0.1:61616 default to openwire // tcp://127.0.0.1:61616?wireFormat=openwire same as above // tcp://127.0.0.1:61613?wireFormat=stomp use stomp instead // // SSL: // ========================= // To use SSL you need to specify the location of the trusted Root CA or the // certificate for the broker you want to connect to. Using the Root CA allows // you to use failover with multiple servers all using certificates signed by // the trusted root. If using client authentication you also need to specify // the location of the client Certificate. // // System::setProperty( "decaf.net.ssl.keyStore", "<path>/client.pem" ); // System::setProperty( "decaf.net.ssl.keyStorePassword", "password" ); // System::setProperty( "decaf.net.ssl.trustStore", "<path>/rootCA.pem" ); // // The you just specify the ssl transport in the URI, for example: // // ssl://localhost:61617 // std::string brokerURI = "failover:(tcp://192.168.10.143:61616" // "?wireFormat=openwire" // "&transport.useInactivityMonitor=false" // "&connection.alwaysSyncSend=true" // "&connection.useAsyncSend=true" // "?transport.commandTracingEnabled=true" // "&transport.tcpTracingEnabled=true" // "&wireFormat.tightEncodingEnabled=true" ")"; //============================================================ // set to true to use topics instead of queues // Note in the code above that this causes createTopic or // createQueue to be used in both consumer an producer. //============================================================ bool useTopics = false; bool sessionTransacted = true; int numMessages = 1; bool useConsumer=true; bool useProducer=true; //int nSet=atoi(argv[1]); //if(1==nSet) //{ //#define USE_COMSUMER //} //else //{ //#define USE_PRODUCER // //} long long startTime = System::currentTimeMillis(); #ifdef USE_PRODUCER printf("当前 USE_PRODUCER \r\n"); int numProducerMessages = 30; int nThreadNumber=10; vector<HelloWorldProducer *> vHelloWorldProducer; for(int i=0;i<nThreadNumber;++i) { HelloWorldProducer * producerTemp=new HelloWorldProducer(brokerURI, numProducerMessages, useTopics); vHelloWorldProducer.push_back(producerTemp); } #endif #ifdef USE_COMSUMER printf("当前 USE_COMSUMER \r\n"); HelloWorldConsumer consumer(brokerURI, numMessages, useTopics, sessionTransacted); // Start the consumer thread. Thread consumerThread(&consumer); consumerThread.start(); // Wait for the consumer to indicate that its ready to go. consumer.waitUntilReady(); #endif #ifdef USE_PRODUCER // Start the producer thread. vector<Thread *> vThread; for(int i=0;i<nThreadNumber;++i) { HelloWorldProducer & ProducerTemp=*vHelloWorldProducer[i]; Thread * threadTemp=new Thread(&ProducerTemp); vThread.push_back(threadTemp); threadTemp->start(); ProducerTemp.waitUntilReady(); } for(int i=0;i<vThread.size();++i) { Thread * threadTemp=vThread[i]; //threadTemp->join(); } while(1) { Thread::sleep(10); } //Thread producerThread1(&producer1); //producerThread1.start(); //producer1.waitUntilReady(); //Thread producerThread2(&producer2); //producerThread2.start(); //producer2.waitUntilReady(); //Thread producerThread3(&producer3); //producerThread3.start(); //producer3.waitUntilReady(); #endif #ifdef USE_PRODUCER // Wait for the threads to complete. //producerThread1.join(); //producerThread2.join(); //producerThread3.join(); #endif #ifdef USE_COMSUMER consumerThread.join(); #endif long long endTime = System::currentTimeMillis(); double totalTime = (double)(endTime - startTime) / 1000.0; #ifdef USE_PRODUCER //producer1.close(); //producer2.close(); //producer3.close(); for(int i=0;i<vHelloWorldProducer.size();++i) { HelloWorldProducer * ProducerTemp=vHelloWorldProducer[i]; ProducerTemp->close(); if(ProducerTemp) { delete ProducerTemp; ProducerTemp=NULL; } } #endif #ifdef USE_COMSUMER consumer.close(); #endif std::cout << "Time to completion = " << totalTime << " seconds." << std::endl; std::cout << "-----------------------------------------------------\n"; std::cout << "Finished with the example." << std::endl; std::cout << "=====================================================\n"; } activemq::library::ActiveMQCPP::shutdownLibrary(); return 0; } // END SNIPPET: demo 程序运行结果: 关于activemq-cpp 的Message 消息转换. activemq-cpp 中的转换ActiveMQMessageTransformation.transformMessage 中是有相应的实现. //////////////////////////////////////////////////////////////////////////////// bool ActiveMQMessageTransformation::transformMessage(cms::Message* message, ActiveMQConnection* connection, Message** amqMessage) { if (message == NULL) { throw NullPointerException(__FILE__, __LINE__, "Provided source cms::Message pointer was NULL"); } if (amqMessage == NULL) { throw NullPointerException(__FILE__, __LINE__, "Provided target commands::Message pointer was NULL"); } *amqMessage = dynamic_cast<Message*>(message); if (*amqMessage != NULL) { return false; } else { if (dynamic_cast<cms::BytesMessage*>(message) != NULL) { cms::BytesMessage* bytesMsg = dynamic_cast<cms::BytesMessage*>(message); bytesMsg->reset(); ActiveMQBytesMessage* msg = new ActiveMQBytesMessage(); msg->setConnection(connection); try { for (;;) { // Reads a byte from the message stream until the stream is empty msg->writeByte(bytesMsg->readByte()); } } catch (cms::MessageEOFException& e) { // if an end of message stream as expected } catch (cms::CMSException& e) { } *amqMessage = msg; } else if (dynamic_cast<cms::MapMessage*>(message) != NULL) { cms::MapMessage* mapMsg = dynamic_cast<cms::MapMessage*>(message); ActiveMQMapMessage* msg = new ActiveMQMapMessage(); msg->setConnection(connection); std::vector<std::string> elements = mapMsg->getMapNames(); std::vector<std::string>::iterator iter = elements.begin(); for(; iter != elements.end() ; ++iter) { std::string key = *iter; cms::Message::ValueType elementType = mapMsg->getValueType(key); switch(elementType) { case cms::Message::BOOLEAN_TYPE: msg->setBoolean(key, mapMsg->getBoolean(key)); break; case cms::Message::BYTE_TYPE: msg->setByte(key, mapMsg->getByte(key)); break; case cms::Message::BYTE_ARRAY_TYPE: msg->setBytes(key, mapMsg->getBytes(key)); break; case cms::Message::CHAR_TYPE: msg->setChar(key, mapMsg->getChar(key)); break; case cms::Message::SHORT_TYPE: msg->setShort(key, mapMsg->getShort(key)); break; case cms::Message::INTEGER_TYPE: msg->setInt(key, mapMsg->getInt(key)); break; case cms::Message::LONG_TYPE: msg->setLong(key, mapMsg->getLong(key)); break; case cms::Message::FLOAT_TYPE: msg->setFloat(key, mapMsg->getFloat(key)); break; case cms::Message::DOUBLE_TYPE: msg->setDouble(key, mapMsg->getDouble(key)); break; case cms::Message::STRING_TYPE: msg->setString(key, mapMsg->getString(key)); break; default: break; } } *amqMessage = msg; } else if (dynamic_cast<cms::ObjectMessage*>(message) != NULL) { cms::ObjectMessage* objMsg = dynamic_cast<cms::ObjectMessage*>(message); ActiveMQObjectMessage* msg = new ActiveMQObjectMessage(); msg->setConnection(connection); msg->setObjectBytes(objMsg->getObjectBytes()); *amqMessage = msg; } else if (dynamic_cast<cms::StreamMessage*>(message) != NULL) { cms::StreamMessage* streamMessage = dynamic_cast<cms::StreamMessage*>(message); streamMessage->reset(); ActiveMQStreamMessage* msg = new ActiveMQStreamMessage(); msg->setConnection(connection); try { while(true) { cms::Message::ValueType elementType = streamMessage->getNextValueType(); int result = -1; std::vector<unsigned char> buffer(255); switch(elementType) { case cms::Message::BOOLEAN_TYPE: msg->writeBoolean(streamMessage->readBoolean()); break; case cms::Message::BYTE_TYPE: msg->writeBoolean(streamMessage->readBoolean()); break; case cms::Message::BYTE_ARRAY_TYPE: while ((result = streamMessage->readBytes(buffer)) != -1) { msg->writeBytes(&buffer[0], 0, result); buffer.clear(); } break; case cms::Message::CHAR_TYPE: msg->writeChar(streamMessage->readChar()); break; case cms::Message::SHORT_TYPE: msg->writeShort(streamMessage->readShort()); break; case cms::Message::INTEGER_TYPE: msg->writeInt(streamMessage->readInt()); break; case cms::Message::LONG_TYPE: msg->writeLong(streamMessage->readLong()); break; case cms::Message::FLOAT_TYPE: msg->writeFloat(streamMessage->readFloat()); break; case cms::Message::DOUBLE_TYPE: msg->writeDouble(streamMessage->readDouble()); break; case cms::Message::STRING_TYPE: msg->writeString(streamMessage->readString()); break; default: break; } } } catch (cms::MessageEOFException& e) { // if an end of message stream as expected } catch (cms::CMSException& e) { } *amqMessage = msg; } else if (dynamic_cast<cms::TextMessage*>(message) != NULL) { cms::TextMessage* textMsg = dynamic_cast<cms::TextMessage*>(message); ActiveMQTextMessage* msg = new ActiveMQTextMessage(); msg->setConnection(connection); msg->setText(textMsg->getText()); *amqMessage = msg; } else { *amqMessage = new ActiveMQMessage(); (*amqMessage)->setConnection(connection); } ActiveMQMessageTransformation::copyProperties(message, dynamic_cast<cms::Message*>(*amqMessage)); } return true; } 5,activemq 的activemq broker cluster (activemq 集群). 可以参考: http://bh-keven.iteye.com/blog/1617788 http://blog.csdn.net/jason5186/article/details/18702523 6,activemq.xml 中的配置和activemq Connection URIS 配置 Index > Apache.NMS.ActiveMQ > ActiveMQ URI Configuration http://activemq.apache.org/nms/activemq-uri-configuration.html http://activemq.apache.org/tcp-transport-reference.html 是有相应介绍,但需要花一些时间去读. //7,wireFormat=openwire 的几种方式.的优缺点. //openwire,amqp,stomp,mqtt,ws
一、 概述与介绍 ActiveMQ 是Apache出品,最流行的、功能强大的即时通讯和集成模式的开源服务器。ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现。提供客户端支持跨语言和协议,带有易于在充分支持JMS 1.1和1.4使用J2EE企业集成模式和许多先进的功能。 二、 特性 1、 多种语言和协议编写客户端。语言: Java、C、C++、C#、Ruby、Perl、Python、PHP。应用协议:OpenWire、Stomp REST、WS Notification、XMPP、AMQP 2、完全支持JMS1.1和J2EE 1.4规范 (持久化,XA消息,事务) 3、对Spring的支持,ActiveMQ可以很容易内嵌到使用Spring的系统里面去,而且也支持Spring2.0的特性 4、通过了常见J2EE服务器(如 Geronimo、JBoss 4、GlassFish、WebLogic)的测试,其中通过JCA 1.5 resource adaptors的配置,可以让ActiveMQ可以自动的部署到任何兼容J2EE 1.4 商业服务器上 5、支持多种传送协议:in-VM、TCP、SSL、NIO、UDP、JGroups、JXTA 6、支持通过JDBC和journal提供高速的消息持久化 7、从设计上保证了高性能的集群,客户端-服务器,点对点 8、支持Ajax 9、支持与Axis的整合 10、可以很容易得调用内嵌JMS provider,进行测试 三、 安装 开发环境: System:Windows JDK:1.6+ IDE:eclipse apache ActiveMQ 5.8 Email:hoojo_@126.com Blog:http://blog.csdn.net/IBM_hoojo http://hoojo.cnblogs.com/ 1、 下载ActiveMQ,下载地址:http://www.apache.org/dyn/closer.cgi?path=/activemq/apache-activemq/5.8.0/apache-activemq-5.8.0-bin.zip 2、 解压apache-activemq-5.8.0.zip即可完成ActiveMQ的安装 3、 解压后目录结构如下 +bin (windows下面的bat和unix/linux下面的sh) 启动ActiveMQ的启动服务就在这里 +conf (activeMQ配置目录,包含最基本的activeMQ配置文件) +data (默认是空的) +docs (index,replease版本里面没有文档) +example (几个例子) +lib (activeMQ使用到的lib) +webapps (系统管理员控制台代码) +webapps-demo(系统示例代码) -activemq-all-5.8.0.jar (ActiveMQ的binary) -user-guide.html (部署指引) -LICENSE.txt -NOTICE.txt -README.txt 其他文件就不相信介绍了,搞Java的应该都知道干什么用的。 你可以进入bin目录,使用activemq.bat双击启动(windows用户可以选择系统位数,如果你是linux的话,就用命令行的发送去启动),如果一切顺利,你就会看见类似下面的信息: 如果你看到这个,那么恭喜你成功了。如果你启动看到了异常信息: Caused by: java.io.IOException: Failed to bind to server socket: tcp://0.0.0.0:61616?maximumConnections=1000&wireformat.maxFrameSize=104857600 due to: java.net.SocketException: Unrecognized Windows Sockets error: 0: JVM_Bind 那么我告诉你,很不幸,你的端口被占用了。接下来你大概想知道是哪个程序占用了你的端口,并kill掉该进程或服务。或者你要尝试修改ActiveMQ的默认端口61616(ActiveMQ使用的默认端口是61616),在大多数情况下,占用61616端口的是Internet Connection Sharing (ICS) 这个Windows服务,你只需停止它就可以启动ActiveMQ了。 4、 启动成功就可以访问管理员界面:http://localhost:8161/admin,默认用户名和密码admin/admin。如果你想修改用户名和密码的话,在conf/jetty-realm.properties中修改即可。 其中在导航菜单中,Queues是队列方式消息。Topics是主题方式消息。Subscribers消息订阅监控查询。Connections可以查看链接数,分别可以查看xmpp、ssl、stomp、openwire、ws和网络链接。Network是网络链接数监控。Send可以发送消息数据。 5、 运行demo示例,在dos控制台输入activemq.bat xbean:../conf/activemq-demo.xml 即可启动demo示例。官方提供的user-guide.html中的access the web console中是提示输入:activemq.bat console xbean:conf/activemq-demo.xml,我用这种方式不成功。 当然你还可以用绝对的文件目录方式:activemq.bat xbean:file:D:/mq/conf/activemq-demo.xml 如果提示conf/activemq-demo.xml没有找到,你可以尝试改变下路径,也就是去掉上面的“..”。通过http://localhost:8161/demo/ 就可以访问示例了。 四、 消息示例 1、ActiviteMQ消息有3中形式 JMS 公共 点对点域 发布/订阅域 ConnectionFactory QueueConnectionFactory TopicConnectionFactory Connection QueueConnection TopicConnection Destination Queue Topic Session QueueSession TopicSession MessageProducer QueueSender TopicPublisher MessageConsumer QueueReceiver TopicSubscriber (1)、点对点方式(point-to-point) 点对点的消息发送方式主要建立在 Message Queue,Sender,reciever上,Message Queue 存贮消息,Sneder 发送消息,receive接收消息.具体点就是Sender Client发送Message Queue ,而 receiver Cliernt从Queue中接收消息和"发送消息已接受"到Quere,确认消息接收。消息发送客户端与接收客户端没有时间上的依赖,发送客户端可以在任何时刻发送信息到Queue,而不需要知道接收客户端是不是在运行 (2)、发布/订阅 方式(publish/subscriber Messaging) 发布/订阅方式用于多接收客户端的方式.作为发布订阅的方式,可能存在多个接收客户端,并且接收端客户端与发送客户端存在时间上的依赖。一个接收端只能接收他创建以后发送客户端发送的信息。作为subscriber ,在接收消息时有两种方法,destination的receive方法,和实现message listener 接口的onMessage 方法。 2、ActiviteMQ接收和发送消息基本流程 发送消息的基本步骤: (1)、创建连接使用的工厂类JMS ConnectionFactory (2)、使用管理对象JMS ConnectionFactory建立连接Connection,并启动 (3)、使用连接Connection 建立会话Session (4)、使用会话Session和管理对象Destination创建消息生产者MessageSender (5)、使用消息生产者MessageSender发送消息 消息接收者从JMS接受消息的步骤 (1)、创建连接使用的工厂类JMS ConnectionFactory (2)、使用管理对象JMS ConnectionFactory建立连接Connection,并启动 (3)、使用连接Connection 建立会话Session (4)、使用会话Session和管理对象Destination创建消息接收者MessageReceiver (5)、使用消息接收者MessageReceiver接受消息,需要用setMessageListener将MessageListener接口绑定到MessageReceiver消息接收者必须实现了MessageListener接口,需要定义onMessage事件方法。 五、 代码示例 在代码开始,我们先建一个project,在这个project中添加如下jar包 添加完jar包后就可以开始实际的代码工作了。 1、 使用JMS方式发送接收消息 消息发送者 package com.hoo.mq.jms; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.DeliveryMode; import javax.jms.Destination; import javax.jms.MessageProducer; import javax.jms.Session; import javax.jms.TextMessage; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; /** * <b>function:</b> 消息发送者 * @author hoojo * @createDate 2013-6-19 上午11:26:43 * @file MessageSender.java * @package com.hoo.mq.jms * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class MessageSender { // 发送次数 public static final int SEND_NUM = 5; // tcp 地址 public static final String BROKER_URL = "tcp://localhost:61616"; // 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp public static final String DESTINATION = "hoo.mq.queue"; /** * <b>function:</b> 发送消息 * @author hoojo * @createDate 2013-6-19 下午12:05:42 * @param session * @param producer * @throws Exception */ public static void sendMessage(Session session, MessageProducer producer) throws Exception { for (int i = 0; i < SEND_NUM; i++) { String message = "发送消息第" + (i + 1) + "条"; TextMessage text = session.createTextMessage(message); System.out.println(message); producer.send(text); } } public static void run() throws Exception { Connection connection = null; Session session = null; try { // 创建链接工厂 ConnectionFactory factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, BROKER_URL); // 通过工厂创建一个连接 connection = factory.createConnection(); // 启动连接 connection.start(); // 创建一个session会话 session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); // 创建一个消息队列 Destination destination = session.createQueue(DESTINATION); // 创建消息制作者 MessageProducer producer = session.createProducer(destination); // 设置持久化模式 producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); sendMessage(session, producer); // 提交会话 session.commit(); } catch (Exception e) { throw e; } finally { // 关闭释放资源 if (session != null) { session.close(); } if (connection != null) { connection.close(); } } } public static void main(String[] args) throws Exception { MessageSender.run(); } } 接受者 package com.hoo.mq.jms; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.Destination; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.Session; import javax.jms.TextMessage; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; /** * <b>function:</b> 消息接收者 * @author hoojo * @createDate 2013-6-19 下午01:34:27 * @file MessageReceiver.java * @package com.hoo.mq.jms * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class MessageReceiver { // tcp 地址 public static final String BROKER_URL = "tcp://localhost:61616"; // 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp public static final String DESTINATION = "hoo.mq.queue"; public static void run() throws Exception { Connection connection = null; Session session = null; try { // 创建链接工厂 ConnectionFactory factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, BROKER_URL); // 通过工厂创建一个连接 connection = factory.createConnection(); // 启动连接 connection.start(); // 创建一个session会话 session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); // 创建一个消息队列 Destination destination = session.createQueue(DESTINATION); // 创建消息制作者 MessageConsumer consumer = session.createConsumer(destination); while (true) { // 接收数据的时间(等待) 100 ms Message message = consumer.receive(1000 * 100); TextMessage text = (TextMessage) message; if (text != null) { System.out.println("接收:" + text.getText()); } else { break; } } // 提交会话 session.commit(); } catch (Exception e) { throw e; } finally { // 关闭释放资源 if (session != null) { session.close(); } if (connection != null) { connection.close(); } } } public static void main(String[] args) throws Exception { MessageReceiver.run(); } } 2、 Queue队列方式发送点对点消息数据 发送方 package com.hoo.mq.queue; import javax.jms.DeliveryMode; import javax.jms.MapMessage; import javax.jms.Queue; import javax.jms.QueueConnection; import javax.jms.QueueConnectionFactory; import javax.jms.QueueSession; import javax.jms.Session; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; /** * <b>function:</b> Queue 方式消息发送者 * @author hoojo * @createDate 2013-6-19 下午04:34:36 * @file QueueSender.java * @package com.hoo.mq.queue * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class QueueSender { // 发送次数 public static final int SEND_NUM = 5; // tcp 地址 public static final String BROKER_URL = "tcp://localhost:61616"; // 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp public static final String DESTINATION = "hoo.mq.queue"; /** * <b>function:</b> 发送消息 * @author hoojo * @createDate 2013-6-19 下午12:05:42 * @param session * @param sender * @throws Exception */ public static void sendMessage(QueueSession session, javax.jms.QueueSender sender) throws Exception { for (int i = 0; i < SEND_NUM; i++) { String message = "发送消息第" + (i + 1) + "条"; MapMessage map = session.createMapMessage(); map.setString("text", message); map.setLong("time", System.currentTimeMillis()); System.out.println(map); sender.send(map); } } public static void run() throws Exception { QueueConnection connection = null; QueueSession session = null; try { // 创建链接工厂 QueueConnectionFactory factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, BROKER_URL); // 通过工厂创建一个连接 connection = factory.createQueueConnection(); // 启动连接 connection.start(); // 创建一个session会话 session = connection.createQueueSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); // 创建一个消息队列 Queue queue = session.createQueue(DESTINATION); // 创建消息发送者 javax.jms.QueueSender sender = session.createSender(queue); // 设置持久化模式 sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT); sendMessage(session, sender); // 提交会话 session.commit(); } catch (Exception e) { throw e; } finally { // 关闭释放资源 if (session != null) { session.close(); } if (connection != null) { connection.close(); } } } public static void main(String[] args) throws Exception { QueueSender.run(); } } 接收方 package com.hoo.mq.queue; import javax.jms.JMSException; import javax.jms.MapMessage; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.Queue; import javax.jms.QueueConnection; import javax.jms.QueueConnectionFactory; import javax.jms.QueueSession; import javax.jms.Session; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; /** * <b>function:</b> 消息接收者; 依赖hawtbuf-1.9.jar * @author hoojo * @createDate 2013-6-19 下午01:34:27 * @file MessageReceiver.java * @package com.hoo.mq.queue * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class QueueReceiver { // tcp 地址 public static final String BROKER_URL = "tcp://localhost:61616"; // 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp public static final String TARGET = "hoo.mq.queue"; public static void run() throws Exception { QueueConnection connection = null; QueueSession session = null; try { // 创建链接工厂 QueueConnectionFactory factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, BROKER_URL); // 通过工厂创建一个连接 connection = factory.createQueueConnection(); // 启动连接 connection.start(); // 创建一个session会话 session = connection.createQueueSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); // 创建一个消息队列 Queue queue = session.createQueue(TARGET); // 创建消息制作者 javax.jms.QueueReceiver receiver = session.createReceiver(queue); receiver.setMessageListener(new MessageListener() { public void onMessage(Message msg) { if (msg != null) { MapMessage map = (MapMessage) msg; try { System.out.println(map.getLong("time") + "接收#" + map.getString("text")); } catch (JMSException e) { e.printStackTrace(); } } } }); // 休眠100ms再关闭 Thread.sleep(1000 * 100); // 提交会话 session.commit(); } catch (Exception e) { throw e; } finally { // 关闭释放资源 if (session != null) { session.close(); } if (connection != null) { connection.close(); } } } public static void main(String[] args) throws Exception { QueueReceiver.run(); } } 3、 Topic主题发布和订阅消息 消息发送方 package com.hoo.mq.topic; import javax.jms.DeliveryMode; import javax.jms.MapMessage; import javax.jms.Session; import javax.jms.Topic; import javax.jms.TopicConnection; import javax.jms.TopicConnectionFactory; import javax.jms.TopicPublisher; import javax.jms.TopicSession; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; /** * <b>function:</b> Queue 方式消息发送者 * @author hoojo * @createDate 2013-6-19 下午04:34:36 * @file QueueSender.java * @package com.hoo.mq.topic * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class TopicSender { // 发送次数 public static final int SEND_NUM = 5; // tcp 地址 public static final String BROKER_URL = "tcp://localhost:61616"; // 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp public static final String DESTINATION = "hoo.mq.topic"; /** * <b>function:</b> 发送消息 * @author hoojo * @createDate 2013-6-19 下午12:05:42 * @param session 会话 * @param publisher 发布者 * @throws Exception */ public static void sendMessage(TopicSession session, TopicPublisher publisher) throws Exception { for (int i = 0; i < SEND_NUM; i++) { String message = "发送消息第" + (i + 1) + "条"; MapMessage map = session.createMapMessage(); map.setString("text", message); map.setLong("time", System.currentTimeMillis()); System.out.println(map); publisher.send(map); } } public static void run() throws Exception { TopicConnection connection = null; TopicSession session = null; try { // 创建链接工厂 TopicConnectionFactory factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, BROKER_URL); // 通过工厂创建一个连接 connection = factory.createTopicConnection(); // 启动连接 connection.start(); // 创建一个session会话 session = connection.createTopicSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); // 创建一个消息队列 Topic topic = session.createTopic(DESTINATION); // 创建消息发送者 TopicPublisher publisher = session.createPublisher(topic); // 设置持久化模式 publisher.setDeliveryMode(DeliveryMode.NON_PERSISTENT); sendMessage(session, publisher); // 提交会话 session.commit(); } catch (Exception e) { throw e; } finally { // 关闭释放资源 if (session != null) { session.close(); } if (connection != null) { connection.close(); } } } public static void main(String[] args) throws Exception { TopicSender.run(); } } 接收方 package com.hoo.mq.topic; import javax.jms.JMSException; import javax.jms.MapMessage; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.Session; import javax.jms.Topic; import javax.jms.TopicConnection; import javax.jms.TopicConnectionFactory; import javax.jms.TopicSession; import javax.jms.TopicSubscriber; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; /** * <b>function:</b> 消息接收者; 依赖hawtbuf-1.9.jar * @author hoojo * @createDate 2013-6-19 下午01:34:27 * @file MessageReceiver.java * @package com.hoo.mq.topic * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class TopicReceiver { // tcp 地址 public static final String BROKER_URL = "tcp://localhost:61616"; // 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp public static final String TARGET = "hoo.mq.topic"; public static void run() throws Exception { TopicConnection connection = null; TopicSession session = null; try { // 创建链接工厂 TopicConnectionFactory factory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, ActiveMQConnection.DEFAULT_PASSWORD, BROKER_URL); // 通过工厂创建一个连接 connection = factory.createTopicConnection(); // 启动连接 connection.start(); // 创建一个session会话 session = connection.createTopicSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); // 创建一个消息队列 Topic topic = session.createTopic(TARGET); // 创建消息制作者 TopicSubscriber subscriber = session.createSubscriber(topic); subscriber.setMessageListener(new MessageListener() { public void onMessage(Message msg) { if (msg != null) { MapMessage map = (MapMessage) msg; try { System.out.println(map.getLong("time") + "接收#" + map.getString("text")); } catch (JMSException e) { e.printStackTrace(); } } } }); // 休眠100ms再关闭 Thread.sleep(1000 * 100); // 提交会话 session.commit(); } catch (Exception e) { throw e; } finally { // 关闭释放资源 if (session != null) { session.close(); } if (connection != null) { connection.close(); } } } public static void main(String[] args) throws Exception { TopicReceiver.run(); } } 4、 整合Spring实现消息发送和接收,在整合之前我们需要添加jar包,需要的jar包如下 这些jar包可以在D:\apache-activemq-5.8.0\lib这个lib目录中找到,添加完jar包后就开始编码工作。 消息发送者 package com.hoo.mq.spring.support; import java.util.Date; import javax.jms.JMSException; import javax.jms.MapMessage; import javax.jms.Message; import javax.jms.Session; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.core.MessageCreator; /** * <b>function:</b> Spring JMSTemplate 消息发送者 * @author hoojo * @createDate 2013-6-24 下午02:18:48 * @file Sender.java * @package com.hoo.mq.spring.support * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class Sender { public static void main(String[] args) { ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:applicationContext-*.xml"); JmsTemplate jmsTemplate = (JmsTemplate) ctx.getBean("jmsTemplate"); jmsTemplate.send(new MessageCreator() { public Message createMessage(Session session) throws JMSException { MapMessage message = session.createMapMessage(); message.setString("message", "current system time: " + new Date().getTime()); return message; } }); } } 消息接收者 package com.hoo.mq.spring.support; import java.util.Map; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.jms.core.JmsTemplate; /** * <b>function:</b> Spring JMSTemplate 消息接收者 * @author hoojo * @createDate 2013-6-24 下午02:22:32 * @file Receiver.java * @package com.hoo.mq.spring.support * @project ActiveMQ-5.8 * @blog http://blog.csdn.net/IBM_hoojo * @email hoojo_@126.com * @version 1.0 */ public class Receiver { @SuppressWarnings("unchecked") public static void main(String[] args) { ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:applicationContext-*.xml"); JmsTemplate jmsTemplate = (JmsTemplate) ctx.getBean("jmsTemplate"); while(true) { Map<String, Object> map = (Map<String, Object>) jmsTemplate.receiveAndConvert(); System.out.println("收到消息:" + map.get("message")); } } } 这里主要是用到了JmsTemplate这个消息模板,这个对象在spring的IoC容器中管理,所以要从spring的容器上下文中获取。下面看看spring的配置文件applicationContext-beans.xml内容: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> <!-- 连接池 --> <bean id="pooledConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop"> <property name="connectionFactory"> <bean class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616" /> </bean> </property> </bean> <!-- 连接工厂 --> <bean id="activeMQConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616" /> </bean> <!-- 配置消息目标 --> <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue"> <!-- 目标,在ActiveMQ管理员控制台创建 http://localhost:8161/admin/queues.jsp --> <constructor-arg index="0" value="hoo.mq.queue" /> </bean> <!-- 消息模板 --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="activeMQConnectionFactory" /> <property name="defaultDestination" ref="destination" /> <property name="messageConverter"> <bean class="org.springframework.jms.support.converter.SimpleMessageConverter" /> </property> </bean> </beans> 这里的整合就比较简单了,如果你是web工程,那你在需要用jms的时候,只需用注入jmsTemplate即可。
Webservice Webservice是使应用程序以与平台和编程语言无关的方式进行相互通信技术。 eg:站点提供访问的数据接口:新浪微博、淘宝。 官方解释:它是一种构建应用程序的普遍模型,可以在任何支持网络通信的操作系统中实施运行;它是一种新的web应用程序分支,是自包含、自描述、模块化的应用,可以发布、定位、通过web调用。WebService是一个应用组件,它逻辑性的为其他应用程序提供数据与服务.各应用程序通过网络协议和规定的一些标准数据格式(Http,XML,Soap)来访问WebService,通过WebService内部执行得到所需结果.Web Service可以执行从简单的请求到复杂商务处理的任何功能。一旦部署以后,其他WebService应用程序可以发现并调用它部署的服务。 SOAP(Simple Object Access Protocol):简单对象访问协议是在分散或分布式的环境中交换信息并执行远程过程调用的轻量级协议,是一个基于XML的协议。使用SOAP,不用考虑任何特定的传输协议(最常用的还是HTTP协议),可以允许任何类型的对象或代码,在任何平台上,以任何一种语言相互通信。 WSDL:Web Services Description Language的缩写,是一个用来描述Web服务和说明如何与Web服务通信的XML语言。为用户提供详细的接口说明书。 Axis:Axis本质上就是一个SOAP引擎(Apache Axis is an implementation of the SAOP),提供创建服务名、客户端和网关SOAP操作的基本框架。但是Axis并不完全是一个SOAP引擎,它还包括: 是一个独立的SOAP服务器。 是一个嵌入Servlet引擎(eg:Tomcat)的服务器。 支持WSDL。 提供转化WSDL为Java类的工具。 提供例子程序。 提供TCP/IP数据包监视工具。 Axis有四种Service styles,分别是: RPC(Remote Procedure Call Protocol远程访问调用协议,部署时属于默认选项) Document Wrapped Message WSDD(Web Service Deployment Descriptor):Web服务分布描述,它定义了Web服务的接口,如服务名、提供的方法、方法的参数信息。 UDDI(Universal Description,Discovery,and Integration):统一描述、发现和集成,用于集中存放和查找WSDL描述文件,起着目录服务器的作用。 WSDL元素 WSDL元素基于XML语法描述了与服务进行交互的基本元素: Type(消息类型):数据类型定义的容器,它使用某种类型系统(如XSD)。 Message(消息):通信数据的抽象类型化定义,它由一个或者多个part组成。 Part:消息参数 Operation(操作):对服务所支持的操作进行抽象描述,WSDL定义了四种操作: 单向(one-way):端点接受信息; 请求-响应(request-response):端点接受消息,然后发送相关消息; 要求-响应(solicit-response):端点发送消息,然后接受相关消息; 通知(notification):端点发送消息。 Port Type (端口类型):特定端口类型的具体协议和数据格式规范。 Binding:特定端口类型的具体协议和数据格式规范 Port :定义为绑定和网络地址组合的单个端点。 Service:相关端口的集合,包括其关联的接口、操作、消息等。 以上类图表达了Service、Port、Binding、Operation、Message之间的依赖、关联、聚合、合成、泛化、实现,这里暂不多说,若感兴趣,请参考该文章 UML类图关系大全:http://www.cnblogs.com/riky/archive/2007/04/07/704298.html WSDL伪代码 WSDL 文档是利用这些主要的元素来描述某个 web service 的: 元素 定义 web service 执行的操作 <message> web service 使用的消息 <types> web service 使用的数据类型 <binding> web service 使用的通信协议 一个 WSDL 文档的主要结构是类似这样的: <definitions> <types> definition of types........ </types> <message> definition of a message.... </message> <portType> definition of a port....... </portType> <binding> definition of a binding.... </binding> </definitions> WSDL 文档可包含其它的元素,比如 extension 元素,以及一个 service 元素,此元素可把若干个 web services 的定义组合在一个单一的 WSDL 文档中 实践 为了形成鲜明的对比,客户端用CS架构来创建客户端。 实践之一:创建服务端 创建ASP.NET Web服务 代码示例 using System; using System.Linq; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Xml.Linq; using DotNet.Model; [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] // 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消对下行的注释。 // [System.Web.Script.Services.ScriptService] public class Service : System.Web.Services.WebService { public Service () { //如果使用设计的组件,请取消注释以下行 //InitializeComponent(); } [WebMethod(Description="获取字符串",MessageName="HelloWorld")] public string HelloWorld() { return "Hello World"; } [WebMethod(Description="获取用户信息",MessageName="getCustomer")] public Customer getCustomer(Customer cus1) { return cus1; } [WebMethod(Description = "获取用户信息以参数形式", MessageName = "getCustomerFromParams")] public Customer getCustomerFromParams(int id, string name, string address) { Customer cus1 = new Customer(); cus1.cus_id = id; cus1.cus_name = name; cus1.cus_address = address; return cus1; } } 实践之二:创建客户端 创建ASP.NET WEB客户端(以CS架构) 代码示例 (其中一种方式通过创建“服务引用”的方式,输入“http://localhost:端口号/XX.asmx?wsdl”方式,获得服务访问接口) private ServiceReference1.ServiceSoapClient myclient = new ServiceReference1.ServiceSoapClient(); private void button1_Click(object sender, EventArgs e) { try { /* 第一种方式可以传参、传实体,返回实体 */ ServiceReference1.ServiceSoapClient myclient = new ServiceReference1.ServiceSoapClient(); ServiceReference1.Customer tem = new ServiceReference1.Customer(); tem.cus_id = int.Parse(textBox1.Text); tem.cus_name = textBox2.Text; tem.cus_address = textBox3.Text; string str = JsonHelper.Jso_ToJSON(myclient.getCustomer(tem)); richTextBox1.Text = str; } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void button2_Click(object sender, EventArgs e) { richTextBox1.Text = string.Empty; try { richTextBox1.Text = myclient.HelloWorld(); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void button4_Click(object sender, EventArgs e) { try { /* 第二种方式可以传参,返回实体(不能传入一个对象实体,但是配置动态灵活) */ string url = "http://localhost:3199/ServicePort/Service.asmx"; string methodname = "getCustomerFromParams"; object[] obj = new object[3]; obj[0] = int.Parse(textBox1.Text); obj[1] = textBox2.Text; obj[2] = textBox3.Text; string str = JsonHelper.Jso_ToJSON(WebServiceHelper.InvokeWebService(url, methodname, obj)); richTextBox1.Text = str; } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void button5_Click(object sender, EventArgs e) { try { /* 第三种方式可以传参、传实体,返回XmlDom(配置动态灵活,响应处理麻烦了点而已) */ string url = "http://localhost:3199/ServicePort/Service.asmx"; string methodname = "getCustomer"; Hashtable parm = new Hashtable(); string objectName = "cus1"; parm["cus_id"] = int.Parse(textBox1.Text); parm["cus_name"] = textBox2.Text; parm["cus_address"] = textBox3.Text; XmlDocument oo = WebServiceXmlHelper.QuerySoapWebServiceByObject(url, methodname, objectName, parm); richTextBox1.Text = oo.InnerXml; } catch (Exception ex) { MessageBox.Show(ex.Message); } } 运行效果 服务端 WSDL 客户端(支持多平台,如服务器的Webservice用Java、.Net等) 第1种方式:传参、传实体,URL配置缺少灵活,数据处理灵活 第2种方式:传参,不能传实体,URL配置灵活, 数据处理要稍微加工 第3种方式:传参、传实体、URL配置灵活,数据处理要稍微加工 小结 如果只传递参数,可以用HTTP来传递,Webservice提供的接口如下 HTTP POST 以下是 HTTP POST 请求和响应示例。所显示的占位符需替换为实际值。 POST /ServicePort/Service.asmx/getCustomerFromParams HTTP/1.1 Host: localhost Content-Type: application/x-www-form-urlencoded Content-Length: length id=string&name=string&address=string 返回接收串 HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <Customer xmlns="http://tempuri.org/"> <cus_id>int</cus_id> <cus_name>string</cus_name> <cus_address>string</cus_address> </Customer> 如果是传递实体或传递参数,也可以使用SOAP来传递,Webservice提供的接口如下 POST /ServicePort/Service.asmx HTTP/1.1 Host: localhost Content-Type: application/soap+xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <getCustomerFromParams xmlns="http://tempuri.org/"> <id>int</id> <name>string</name> <address>string</address> </getCustomerFromParams> </soap12:Body> </soap12:Envelope> 返回接收串: HTTP/1.1 200 OK Content-Type: application/soap+xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <getCustomerFromParamsResponse xmlns="http://tempuri.org/"> <getCustomerFromParamsResult> <cus_id>int</cus_id> <cus_name>string</cus_name> <cus_address>string</cus_address> </getCustomerFromParamsResult> </getCustomerFromParamsResponse> </soap12:Body> </soap12:Envelope> WebService向外发布接口的功能,能够更好的为其它平台提供数据以及现实信息平台一体化。 Word文档下载:浅淡Webservice、WSDL三种服务访问的方式.doc 源代码下载:http://files.cnblogs.com/yongfeng/001DotNetWebService.rar 参考网站:http://www.w3.org/TR/wsdl
1、关联 双向关联: C1-C2:指双方都知道对方的存在,都可以调用对方的公共属性和方法。 在GOF的设计模式书上是这样描述的:虽然在分析阶段这种关系是适用的,但我们觉得它对于描述设计模式内的类关系来说显得太抽象了,因为在设计阶段关联关系必须被映射为对象引用或指针。对象引用本身就是有向的,更适合表达我们所讨论的那种关系。所以这种关系在设计的时候比较少用到,关联一般都是有向的。 使用ROSE 生成的代码是这样的: class C1 ...{public: C2* theC2;};class C2 ...{public: C1* theC1;}; 双向关联在代码的表现为双方都拥有对方的一个指针,当然也可以是引用或者是值。 单向关联: C3->C4:表示相识关系,指C3知道C4,C3可以调用C4的公共属性和方法。没有生命期的依赖。一般是表示为一种引用。 生成代码如下: class C3 ...{public: C4* theC4;};class C4 ...{}; 单向关联的代码就表现为C3有C4的指针,而C4对C3一无所知。 自身关联(反身关联): 自己引用自己,带着一个自己的引用。 代码如下: class C14 ...{public: C14* theC14;}; 就是在自己的内部有着一个自身的引用。 2、聚合/组合 当类之间有整体-部分关系的时候,我们就可以使用组合或者聚合。 聚合:表示C9聚合C10,但是C10可以离开C9而独立存在(独立存在的意思是在某个应用的问题域中这个类的存在有意义。这句话怎么解,请看下面组合里的解释)。 代码如下: class C9 ...{public: C10 theC10;};class C10 ...{}; 组合(也有人称为包容):一般是实心菱形加实线箭头表示,如上图所示,表示的是C8被C7包容,而且C8不能离开C7而独立存在。但这是视问题域而定的,例如在关心汽车的领域里,轮胎是一定要组合在汽车类中的,因为它离开了汽车就没有意义了。但是在卖轮胎的店铺业务里,就算轮胎离开了汽车,它也是有意义的,这就可以用聚合了。在《敏捷开发》中还说到,A组合B,则A需要知道B的生存周期,即可能A负责生成或者释放B,或者A通过某种途径知道B的生成和释放。 他们的代码如下: class C7 ...{public: C8 theC8;};class C8 ...{}; 可以看到,代码和聚合是一样的。具体如何区别,可能就只能用语义来区分了。 3、依赖 依赖: 指C5可能要用到C6的一些方法,也可以这样说,要完成C5里的所有功能,一定要有C6的方法协助才行。C5依赖于C6的定义,一般是在C5类的头文件中包含了C6的头文件。ROSE对依赖关系不产生属性。 注意,要避免双向依赖。一般来说,不应该存在双向依赖。 ROSE生成的代码如下: // C5.h#include "C6.h"class C5 ...{};// C6.h#include "C5.h"class C6...{}; 虽然ROSE不生成属性,但在形式上一般是A中的某个方法把B的对象作为参数使用(假设A依赖于B)。如下: #include "B.h"class A...{ void Func(B &b);} 那依赖和聚合\组合、关联等有什么不同呢? 关联是类之间的一种关系,例如老师教学生,老公和老婆,水壶装水等就是一种关系。这种关系是非常明显的,在问题领域中通过分析直接就能得出。 依赖是一种弱关联,只要一个类用到另一个类,但是和另一个类的关系不是太明显的时候(可以说是“uses”了那个类),就可以把这种关系看成是依赖,依赖也可说是一种偶然的关系,而不是必然的关系,就是“我在某个方法中偶然用到了它,但在现实中我和它并没多大关系”。例如我和锤子,我和锤子本来是没关系的,但在有一次要钉钉子的时候,我用到了它,这就是一种依赖,依赖锤子完成钉钉子这件事情。 组合是一种整体-部分的关系,在问题域中这种关系很明显,直接分析就可以得出的。例如轮胎是车的一部分,树叶是树的一部分,手脚是身体的一部分这种的关系,非常明显的整体-部分关系。 上述的几种关系(关联、聚合/组合、依赖)在代码中可能以指针、引用、值等的方式在另一个类中出现,不拘于形式,但在逻辑上他们就有以上的区别。 这里还要说明一下,所谓的这些关系只是在某个问题域才有效,离开了这个问题域,可能这些关系就不成立了,例如可能在某个问题域中,我是一个木匠,需要拿着锤子去干活,可能整个问题的描述就是我拿着锤子怎么钉桌子,钉椅子,钉柜子;既然整个问题就是描述这个,我和锤子就不仅是偶然的依赖关系了,我和锤子的关系变得非常的紧密,可能就上升为组合关系(让我突然想起武侠小说的剑不离身,剑亡人亡...)。这个例子可能有点荒谬,但也是为了说明一个道理,就是关系和类一样,它们都是在一个问题领域中才成立的,离开了这个问题域,他们可能就不复存在了。 4、泛化(继承) 泛化关系:如果两个类存在泛化的关系时就使用,例如父和子,动物和老虎,植物和花等。 ROSE生成的代码很简单,如下: #include "C11.h"class C12 : public C11...{}; 5、这里顺便提一下模板 上面的图对应的代码如下: template<int>class C13 ...{}; 这里再说一下重复度,其实看完了上面的描述之后,我们应该清楚了各个关系间的关系以及具体对应到代码是怎么样的,所谓的重复度,也只不过是上面的扩展,例如A和B有着“1对多”的重复度,那在A中就有一个列表,保存着B对象的N个引用,就是这样而已。 好了,到这里,已经把上面的类图关系说完了,希望你能有所收获了,我也费了不少工夫啊(画图、生成代码、截图、写到BLOG上,唉,一头大汗)。不过如果能让你彻底理解UML类图的这些关系,也值得了。:) +++++++++++++++++++++++++++++++++++++++++++++++++++++ 在UML建模中,对类图上出现元素的理解是至关重要的。开发者必须理解如何将类图上出现的元素转换到Java中。以java为代表结合网上的一些实例,下面是个人一些基本收集与总结: 基本元素符号: 1. 类(Classes) 类包含3个组成部分。第一个是Java中定义的类名。第二个是属性(attributes)。第三个是该类提供的方法。 属性和操作之前可附加一个可见性修饰符。加号(+)表示具有公共可见性。减号(-)表示私有可见性。#号表示受保护的可见性。省略这些修饰符表示具有package(包)级别的可见性。如果属性或操作具有下划线,表明它是静态的。在操作中,可同时列出它接受的参数,以及返回类型,如下图所示: 2. 包(Package) 包是一种常规用途的组合机制。UML中的一个包直接对应于Java中的一个包。在Java中,一个包可能含有其他包、类或者同时含有这两者。进行建模时,你通常拥有逻辑性的包,它主要用于对你的模型进行组织。你还会拥有物理性的包,它直接转换成系统中的Java包。每个包的名称对这个包进行了惟一性的标识。 3. 接口(Interface) 接口是一系列操作的集合,它指定了一个类所提供的服务。它直接对应于Java中的一个接口类型。接口既可用下面的那个图标来表示(上面一个圆圈符号,圆圈符号下面是接口名,中间是直线,直线下面是方法名),也可由附加了<<interface>>的一个标准类来表示。通常,根据接口在类图上的样子,就能知道与其他类的关系。 关 系: 1. 依赖(Dependency) 实体之间一个“使用”关系暗示一个实体的规范发生变化后,可能影响依赖于它的其他实例。更具体地说,它可转换为对不在实例作用域内的一个类或对象的任何类型的引用。其中包括一个局部变量,对通过方法调用而获得的一个对象的引用(如下例所示),或者对一个类的静态方法的引用(同时不存在那个类的一个实例)。也可利用“依赖”来表示包和包之间的关系。由于包中含有类,所以你可根据那些包中的各个类之间的关系,表示出包和包的关系。 2. 关联(Association) 实体之间的一个结构化关系表明对象是相互连接的。箭头是可选的,它用于指定导航能力。如果没有箭头,暗示是一种双向的导航能力。在Java中,关联转换为一个实例作用域的变量,就像图E的“Java”区域所展示的代码那样。可为一个关联附加其他修饰符。多重性(Multiplicity)修饰符暗示着实例之间的关系。在示范代码中,Employee可以有0个或更多的TimeCard对象。但是,每个TimeCard只从属于单独一个Employee。 3. 聚合(Aggregation) 聚合是关联的一种形式,代表两个类之间的整体/局部关系。聚合暗示着整体在概念上处于比局部更高的一个级别,而关联暗示两个类在概念上位于相同的级别。聚合也转换成Java中的一个实例作用域变量。 关联和聚合的区别纯粹是概念上的,而且严格反映在语义上。聚合还暗示着实例图中不存在回路。换言之,只能是一种单向关系。 4. 合成(Composition) 合成是聚合的一种特殊形式,暗示“局部”在“整体”内部的生存期职责。合成也是非共享的。所以,虽然局部不一定要随整体的销毁而被销毁,但整体要么负责保持局部的存活状态,要么负责将其销毁。 局部不可与其他整体共享。但是,整体可将所有权转交给另一个对象,后者随即将承担生存期职责。Employee和TimeCard的关系或许更适合表示成“合成”,而不是表示成“关联”。 5. 泛化(Generalization) 泛化表示一个更泛化的元素和一个更具体的元素之间的关系。泛化是用于对继承进行建模的UML元素。在Java中,用extends关键字来直接表示这种关系。 6. 实现(Realization) 实例关系指定两个实体之间的一个合同。换言之,一个实体定义一个合同,而另一个实体保证履行该合同。对Java应用程序进行建模时,实现关系可直接用implements关键字来表示。 像聚合还分为:非共享聚合、共享聚合、复合聚合等。以及其它内容,下次再补充。
certutil -hashfile yourfilename.ext MD5 certutil -hashfile yourfilename.ext SHA1 certutil -hashfile yourfilename.ext SHA256 转:http://blog.163.com/licanli2082@126/blog/static/35748686201284611330/
文章结束给大家来个程序员笑话:[M] 最近在研究Thrift和Avro以及它们的区分,通过各种渠道搜集资料,现整顿出有关Avro的一些资料,方便当前参考。 一、弁言 1、 简介 Avro是Hadoop中的一个子项目,也是Apache中一个独立的项目,Avro是一个基于二进制数据传输高性能的旁边件。在Hadoop的其他项目中例如HBase(Ref)和Hive(Ref)的Client端与服务端的数据传输也采取了这个工具。Avro是一个数据序列化的系统。Avro可以将数据结构或对象转化成便于存储或传输的格式。Avro设计之初就用来支撑数据密集型应用,适合于近程或当地大规模数据的存储和交换。 2、 特色 Ø 丰硕的数据结构类型; Ø 快速可压缩的二进制数据形式,对数据二进制序列化后可以节约数据存储空间和网络传输带宽; Ø 存储持久数据的文件容器; Ø 可以实现近程进程调用RPC; Ø 简略的动态语言结合功能。 avro支撑跨编程语言实现(C, C++, C#,Java, Python, Ruby, PHP),相似于Thrift,但是avro的明显特征是:avro依附于模式,动态加载相关数据的模式,Avro数据的读写操纵很频繁,而这些操纵使用的都是模式,这样就减少写入每个数据文件的开销,使得序列化快速而又轻巧。这种数据及其模式的自我描述方便了动态脚本语言的使用。当Avro数据存储到文件中时,它的模式也随之存储,这样任何程序都可以对文件停止处置。如果读取数据时使用的模式与写入数据时使用的模式不同,也很轻易解决,因为读取和写入的模式都是已知的。 New schema Writer Reader Action Added field Old New The reader uses the default value of the new field, since it is not written by the writer. New Old The reader does not know about the new field written by the writer, so it is ignored (projection). Removed field Old New The reader ignores the removed field (projection). New Old The removed field is not written by the writer. If the old schema had a default defined for the field, the reader uses this; otherwise, it gets an error. In this case, it is best to update the reader’s schema, either at the same time as or before the writer’s. Avro和动态语言结合后,读/写数据文件和使用RPC协议都不需要生成代码,而代码生成作为一种可选的优化只需要在静态类型语言中实现。 Avro依附于模式(Schema)。通过模式定义各种数据结构,只有确定了模式才能对数据停止解释,所以在数据的序列化和反序列化之前,必须先确定模式的结构。恰是模式的引入,使得数据具有了自描述的功能,同时能够实现动态加载,另外与其他的数据序列化系统如Thrift相比,数据之间不存在其他的任何标识,有利于进步数据处置的效率。 二、技巧要领 1、 类型 数据类型标准化的意思:一方面使不同系统对相同的数据能够准确剖析,另一方面,数据类型的标准定义有利于数据序列化/反序列化。 简略的数据类型:Avro定义了几种简略数据类型,下表是其简略说明: 类型 说明 null no value boolean a binary value int 32-bit signed integer long 64-bit signed integer float single precision (32-bit) IEEE 754 floating-point number double double precision (64-bit) IEEE 754 floating-point number bytes sequence of 8-bit unsigned bytes string unicode character sequence 简略数据类型由类型名称定义,不包含属性信息,例如字符串定义如下: {"type": "string"} 复杂数据类型:Avro定义了六种复杂数据类型,每一种复杂数据类型都具有独特的属性,下表就每一种复杂数据类型停止说明。 类型 属性 说明 Records type name record name a JSON string providing the name of the record (required). namespace a JSON string that qualifies the name(optional). doc a JSON string providing documentation to the user of this schema (optional). aliases a JSON array of strings, providing alternate names for this record (optional). fields a JSON array, listing fields (required). name a JSON string. type a schema/a string of defined record. default a default value for field when lack. order ordering of this field. Enums type name enum name a JSON string providing the name of the enum (required). namespace a JSON string that qualifies the name. doc a JSON string providing documentation to the user of this schema (optional). aliases a JSON array of strings, providing alternate names for this enum (optional) symbols a JSON array, listing symbols, as JSON strings (required). All symbols in an enum must be unique. Arrays type name array items the schema of the array’s items. Maps type name map values the schema of the map’s values. Fixed type name fixed name a string naming this fixed (required). namespace a string that qualifies the name. aliases a JSON array of strings, providing alternate names for this enum (optional). size an integer, specifying the number of bytes per value (required). Unions a JSON arrays 每一种复杂数据类型都含有各自的一些属性,其中部分属性是必需的,部分是可选的。 这里需要说明Record类型中field属性的默认值,当Record Schema实例数据中某个field属性没有供给实例数据时,则由默认值供给,详细值见下表。Union的field默认值由Union定义中的第一个Schema决议。 avro type json type example null null null boolean boolean true int,long integer 1 float,double number 1.1 bytes string "\u00FF" string string "foo" record object {"a": 1} enum string "FOO" array array [1] map object {"a": 1} fixed string "\u00ff" 2、 序列化/反序列化 Avro指定两种数据序列化编码方式:binary encoding 和Json encoding。使用二进制编码会高效序列化,并且序列化后失掉的结果会比拟小;而JSON一般用于调试系统或是基于WEB的应用。 binary encoding规矩如下: 1、 简略数据类型 Type Encoding Example null Zero bytes Null boolean A single byte {true:1, false:0} int/long variable-length zig-zag coding float 4 bytes Java's floatToIntBits double 8 bytes Java's doubleToLongBits bytes a long followed by that many bytes of data string a long followed by that many bytes of UTF-8 encoded character data “foo”:{3,f,o,o} 06 66 6f 6f 2、 复杂数据类型 Type encoding Records encoded just the concatenation of the encodings of its fields Enums a int representing the zero-based position of the symbol in the schema Arrays encoded as series of blocks. A block with count 0 indicates the end of the array. block:{long,items} Maps encoded as series of blocks. A block with count 0 indicates the end of the map. block:{long,key/value pairs}. Unions encoded by first writing a long value indicating the zero-based position within the union of the schema of its value. The value is then encoded per the indicated schema within the union. fixed encoded using number of bytes declared in the schema 实例: Ø records { "type":"record", "name":"test", "fields" : [ {"name": "a","type": "long"}, {"name": "b","type": "string"} ] } 假设:a=27b=”foo” (encoding:36(27), 06(3), 66("f"), 6f("o")) binary encoding:3606 66 6f 6f Ø enums {"type": "enum","name": "Foo", "symbols": ["A","B", "C", "D"] } “D”(encoding: 06(3)) binary encoding: 06 Ø arrays {"type": "array","items": "long"} 每日一道理 我把卷子摊在课桌上,恨不得敲一阵锣,叫大家都来看看我这光彩的分数。 设:{3, 27 } (encoding:04(2), 06(3), 36(27) ) binary encoding:0406 36 00 Ø maps 设:{("a":1), ("b":2) } (encoding:61(“a”), 62(“b”), 02(1), 04(2)) binary encoding:0261 02 02 62 04 Ø unions ["string","null"] 设:(1)null; (2) “a” binary encoding: (1) 02;说明:02代表null在union定义中的位置1; (2) 00 02 61;说明:00为string在union定义的位置,02 61为”a”的编码。 图1表现的是Avro当地序列化和反序列化的实例,它将用户定义的模式和详细的数据编码成二进制序列存储在对象容器文件中,例如用户定义了包含学号、姓名、院系和电话的先生模式,而Avro对其停止编码后存储在student.db文件中,其中存储数据的模式放在文件头的元数据中,这样读取的模式即使与写入的模式不同,也可以迅速地读出数据。假如另一个程序需要获取先生的姓名和电话,只需要定义包含姓名和电话的先生模式,然后用此模式去读取容器文件中的数据便可。 图表 1 3、 模式Schema Schema通过JSON对象表现。Schema定义了简略数据类型和复杂数据类型,其中复杂数据类型包含不同属性。通过各种数据类型用户可以自定义丰硕的数据结构。 Schema由下列JSON对象之一定义: 1. JSON字符串:定名 2. JSON对象:{“type”: “typeName” …attributes…} 3. JSON数组:Avro中Union的定义 举例: {"namespace": "example.avro", "type":"record", "name":"User", "fields": [ {"name":"name", "type": "string"}, {"name":"favorite_number", "type": ["int", "null"]}, {"name":"favorite_color", "type": ["string","null"]} ] } 4、 排序 Avro为数据定义了一个标准的排列顺序。比拟在很多时候是经常被使用到的对象之间的操纵,标准定义可以停止方便有效的比拟和排序。同时标准的定义可以方便对Avro的二进制编码数据直接停止排序而不需要反序列化。 只有当数据项包含相同的Schema的时候,数据之间的比拟才有意思。数据的比拟按照Schema深度优先,从左至右的顺序递归的停止。找到第一个不匹配便可终止比拟。 两个具有相同的模式的项的比拟按照以下规矩停止: null:总是相等。 int,long,float:按照数值大小比拟。 boolean:false在true之前。 string:按照字典序停止比拟。 bytes,fixed:按照byte的字典序停止比拟。 array:按照元素的字典序停止比拟。 enum:按照符号在枚举中的位置比拟。 record:按照域的字典序排序,如果指定了以下属性: “ascending”,域值的顺序稳定。 “descending”,域值的顺序倒置。 “ignore”,排序的时候忽略域值。 map:不可停止比拟。 5、 对象容器文件 Avro定义了一个简略的对象容器文件格式。一个文件对应一个模式,所有存储在文件中的对象都是根据模式写入的。对象按照块停止存储,块可以采取压缩的方式存储。为了在停止mapreduce处置的时候有效的切分文件,在块之间采取了同步记号。一个文件可以包含恣意用户定义的元数据。 一个文件由两部分组成:文件头和一个或者多个文件数据块。 文件头: Ø 四个字节,ASCII‘O’,‘b’,‘j’,1。 Ø 文件元数据,用于描述Schema。 Ø 16字节的文件同步记号。 Ø 其中,文件元数据的格式为: i. 值为-1的长整型,标明这是一个元数据块。 ii. 标识块长度的长整型。 iii. 标识块中key/value对数目的长整型。 iv. 每一个key/value对的string key和bytesvalue。 v. 标识块中字节总数的4字节长的整数。 文件数据块: 数据是以块结构停止组织的,一个文件可以包含一个或者多个文件数据块。 Ø 表现文件中块中对象数目的长整型。 Ø 表现块中数据序列化后的字节数长度的长整型。 Ø 序列化的对象。 Ø 16字节的文件同步记号。 当数据块的长度为0时即为文件数据块的最后一个数据,此后的所有数据被自动忽略。 下图示对象容器文件的结构分解及说明: 一个存储文件由两部分组成:头信息(Header)和数据块(Data Block)。而头信息又由三部分构成:四个字节的前缀,文件Meta-data信息和随机生成的16字节同步标记符。Avro目前支撑的Meta-data有两种:schema和codec。 codec表现对前面的文件数据块(File Data Block)采取何种压缩方式。Avro的实现都需要支撑下面两种压缩方式:null(不压缩)和deflate(使用Deflate算法压缩数据块)。除了文档中认定的两种Meta-data,用户还可以自定义适用于自己的Meta-data。这里用long型来表现有多少个Meta-data数据对,也是让用户在实际应用中可以定义足够的Meta-data信息。对于每对Meta-data信息,都有一个string型的key(需要以“avro.”为前缀)和二进制编码后的value。对于文件中头信息当前的每个数据块,有这样的结构:一个long值记录当前块有多少个对象,一个long值用于记录当前块经过压缩后的字节数,真正的序列化对象和16字节长度的同步标记符。由于对象可以组织成不同的块,使用时就可以不经过反序列化而对某个数据块停止操纵。还可以由数据块数,对象数和同步标记符来定位损坏的块以确保数据完整性。 三、RPC实现 当在RPC中使用Avro时,服务器和客户端可以在握手连接时交换模式。服务器和客户端有彼此全部的模式,因此相同定名字段、缺失字段和多余字段等信息之间通信中需要处置的分歧性问题就可以轻易解决。如图2所示,协议中定义了用于传输的消息,消息使用框架后放入缓冲区中停止传输,由于传输的初始就交换了各自的协议定义,因此即使传输双方使用的协议不同所传输的数据也能够准确剖析。 图表 2 Avro作为RPC框架来使用。客户端希望同服务器端交互时,就需要交换双方通信的协议,它相似于模式,需要双方来定义,在Avro中被称为消息(Message)。通信双方都必须保持这种协议,以便于剖析从对方发送过去的数据,这也就是传说中的握手阶段。 消息从客户端发送到服务器端需要经过传输层(Transport Layer),它发送消息并接收服务器端的响应。达到传输层的数据就是二进制数据。通常以HTTP作为传输模型,数据以POST方式发送到对方去。在Avro中,它的消息被封装成为一组缓冲区(Buffer),相似于下图的模型: 如上图,每个缓冲区以四个字节扫尾,旁边是多个字节的缓冲数据,最后以一个空缓冲区开头。这种机制的利益在于,发送端在发送数据时可以很方便地组装不同数据源的数据,接收方也可以将数据存入不同的存储区。还有,当往缓冲区中写数据时,大对象可以独有一个缓冲区,而不是与其它小对象混合存放,便于接收方方便地读取大对象。 对象容器文件是Avro的数据存储的详细实现,数据交换则由RPC服务供给,与对象容器文件相似,数据交换也完整依附Schema,所以与Hadoop目前的RPC不同,Avro在数据交换之前需要通过握手进程先交换Schema。 1、 握手进程 握手的进程是确保Server和Client取得对方的Schema定义,从而使Server能够准确反序列化请求信息,Client能够准确反序列化响应信息。一般的,Server/Client会缓存最近使用到的一些协议格式,所以,大多数情况下,握手进程不需要交换全部Schema文本。 所有的RPC请求和响应处置都建立在已完成握手的基础上。对于无状态的连接,所有的请求响应之前都附有一次握手进程;对于有状态的连接,一次握手完成,全部连接的生命期内都有效。 详细进程: Client发起HandshakeRequest,其中含有Client本身SchemaHash值和对应Server端的Schema Hash值(clientHash!=null,clientProtocol=null, serverHash!=null)。如果当地缓存有serverHash值则直接填充,如果没有则通过猜想填充。 Server用如下之一HandshakeResponse响应Client请求: (match=BOTH, serverProtocol=null,serverHash=null):当Client发送准确的serverHash值且Server缓存相应的clientHash。握手进程完成,当前的数据交换都遵照本次握手结果。 (match=CLIENT, serverProtocol!=null,serverHash!=null):当Server缓存有Client的Schema,但是Client请求中ServerHash值不准确。此时Server发送Server端的Schema数据和相应的Hash值,此次握手完成,当前的数据交换都遵照本次握手结果。 (match=NONE):当Client发送的ServerHash不准确且Server端没有Client Schema的缓存。这种情况下Client需要重新提交请求信息 (clientHash!=null,clientProtocol!=null, serverHash!=null),Server响应 (match=BOTH, serverProtocol=null,serverHash=null),此次握手进程完成,当前的数据交换都遵照本次握手结果。 握手进程使用的Schema结构如下示。 { "type":"record", "name":"HandshakeRequest","namespace":"org.apache.avro.ipc", "fields":[ {"name":"clientHash", "type": {"type": "fixed","name": "MD5", "size": 16}}, {"name":"clientProtocol", "type": ["null","string"]}, {"name":"serverHash", "type": "MD5"}, {"name":"meta", "type": ["null", {"type":"map", "values": "bytes"}]} ] } { "type":"record", "name":"HandshakeResponse", "namespace":"org.apache.avro.ipc", "fields":[ {"name":"match","type": {"type": "enum","name": "HandshakeMatch", "symbols":["BOTH", "CLIENT", "NONE"]}}, {"name":"serverProtocol", "type": ["null","string"]}, {"name":"serverHash","type": ["null", {"type":"fixed", "name": "MD5", "size": 16}]}, {"name":"meta","type": ["null", {"type":"map", "values": "bytes"}]} ] } 2、 消息帧格式 消息从客户端发送到服务器端需要经过传输层,它发送请求并接收服务器端的响应。达到传输层的数据就是二进制数据。通常以HTTP作为传输模型,数据以POST方式发送到对方去。在 Avro中消息首先分帧后被封装成为一组缓冲区(Buffer)。 数据帧的格式如下: Ø 一系列Buffer: 1、4字节的Buffer长度 2、Buffer字节数据 Ø 长度为0的Buffer结束数据帧 3、 Call格式 一个调用由请求消息、结果响应消息或者错误消息组成。请求和响应包含可扩展的元数据,两种消息都按照之前提出的方法分帧。 调用的请求格式为: Ø 请求元数据,一个类型值的映射。 Ø 消息名,一个Avro字符串。 Ø 消息参数。参数根据消息的请求定义序列化。 调用的响应格式为: Ø 响应的元数据,一个类型值的映射。 Ø 一字节的错误标记位。 Ø 如果错误标记为false,响应消息,根据响应的模式序列化。 如果错误标记位true,错误消息,根据消息的错误结合模式序列化。 四、实例 1、 当地序列化/反序列化 user.avsc {"namespace":"example.avro", "type": "record", "name": "User", "fields": [ {"name": "name", "type":"string"}, {"name": "favorite_number", "type": ["int", "null"]}, {"name": "favorite_color", "type":["string", "null"]} ] } Main.java public class Main { public static void main(String[] args)throws Exception { User user1 = new User(); user1.setName("Alyssa"); user1.setFavoriteNumber(256); // Leave favorite color null // Alternate constructor User user2 = new User("Ben", 7,"red"); // Construct via builder User user3 = User.newBuilder() .setName("Charlie") .setFavoriteColor("blue") .setFavoriteNumber(null) .build(); // Serialize user1 and user2to disk File file = new File("users.avro"); DatumWriter<User> userDatumWriter = new SpecificDatumWriter<User>(User.class); DataFileWriter<User> dataFileWriter = newDataFileWriter<User>(userDatumWriter); dataFileWriter.create(user1.getSchema(),new File("users.avro")); dataFileWriter.append(user1); dataFileWriter.append(user2); dataFileWriter.append(user3); dataFileWriter.close(); // Deserialize Usersfrom disk DatumReader<User> userDatumReader = newSpecificDatumReader<User>(User.class); DataFileReader<User> dataFileReader = newDataFileReader<User>(file, userDatumReader); User user = null; while (dataFileReader.hasNext()) { // Reuse user object bypassing it to next(). This saves us from // allocating and garbagecollecting many objects for files with // many items. user = dataFileReader.next(user); System.out.println(user); } } } 2、 RPC mail.avsc {"namespace":"example.proto", "protocol": "Mail", "types": [ {"name": "Message", "type":"record", "fields": [ {"name": "to", "type": "string"}, {"name": "from", "type": "string"}, {"name": "body", "type":"string"} ] } ], "messages": { "send": { "request": [{"name": "message","type": "Message"}], "response": "string" } } } Main.java public class Main { public static class MailImpl implements Mail { // in this simple example just return details of the message public Utf8 send(Message message) { System.out.println("Sending message"); return new Utf8("Sending message to " + message.getTo().toString() + " from " +message.getFrom().toString() + " with body " +message.getBody().toString()); } } private static Server server; private static void startServer() throws IOException { server = new NettyServer(new SpecificResponder(Mail.class,new MailImpl()), newInetSocketAddress(65111)); // the server implements the Mail protocol (MailImpl) } public static void main(String[] args)throws IOException { System.out.println("Starting server"); // usually this would be anotherapp, but for simplicity startServer(); System.out.println("Server started"); NettyTransceiver client = new NettyTransceiver(new InetSocketAddress(65111)); // client code - attach to the server and send a message Mail proxy = (Mail) SpecificRequestor.getClient(Mail.class, client); System.out.println("Client built, got proxy"); // fill in the Message record and send it Message message = new Message(); message.setTo(new Utf8("127.0.0.1")); message.setFrom(new Utf8("127.0.0.1")); message.setBody(new Utf8("this is my message")); System.out.println("Calling proxy.send with message: " + message.toString()); System.out.println("Result: " +proxy.send(message)); // cleanup client.close(); server.close(); } } 文章结束给大家分享下程序员的一些笑话语录: N多年前,JohnHein博士的一项研究表明:Mac用户平均IQ要比PC用户低15%。超过6000多的参加者接受了测试,结果清晰的显示IQ比较低的人会倾向于使用Mac。Mac用户只答对了基础问题的75%,而PC用户却高达83%。 --------------------------------- 原创文章 By 数据和schema ---------------------------------
现在手上有一个不大不小的系统,运行了一段时间,因为是24*7不断运行,所以内存逐渐增高,慢慢的会飙到95%以上,然后不得不重启电脑,因为用的是云,怕虚拟机重启down掉起不来,重启操作还只能在凌晨4、5点人为弄,周而复始的搞很累,于是下决心找出来到底是什么吞内存 以上两张图是系统的配置和内存占有情况,可以计算出来,在任务管理器中实际显示使用的内存不到2G,而我4核8G的服务器已经是相对不错的配置了,到底是什么东东占用了内存呢,为什么没有在任务管理器里面显示出来? 为了达到这个目的,我找到了微软官方的工具 RAMMap http://technet.microsoft.com/zh-cn/sysinternals/ff700229.aspx 运行一看,AWE这条占了7G多,那AWE又是什么呢,具体是哪个软件导致的呢?继续往下挖 首先是AWE的定义,从这篇可以找到,可以看到AWE和SQL有关 http://blogs.technet.com/b/askperf/archive/2010/08/13/introduction-to-the-new-sysinternals-tool-rammap.aspx 于是找到这篇“Why does my SQL Server use AWE memory? and why is this not visible in RAMMap?” http://serverfault.com/questions/558287/why-does-my-sql-server-use-awe-memory-and-why-is-this-not-visible-in-rammap 从标题基本已经可以猜测到了,这事肯定是SQLSERVER干的,继续往下了解 http://dba.stackexchange.com/questions/48504/awe-memory-usage-growing-with-sql-server-2012 http://blogs.msdn.com/b/psssql/archive/2009/09/11/fun-with-locked-pages-awe-task-manager-and-the-working-set.aspx 这两篇会告诉你SQLSERVER和AWE的关系 http://technet.microsoft.com/zh-cn/library/ms178067(v=sql.105).aspx 这篇会告诉你如何限制SQLSERVER不停的吞噬内存,基本命令如下: sp_configure 'show advanced options', 1; GO RECONFIGURE; GO sp_configure 'max server memory', 4096; --设置最大可使用内存为4G GO RECONFIGURE; GO http://www.brentozar.com/archive/2011/09/sysadmins-guide-microsoft-sql-server-memory/ 这篇会告诉你限制 max server memory已经不合适了,应该加内存了 以上,基本解决内存问题,随笔记录
前三篇博客分别介绍了xml的三种解析方法,分别是SAX,DOM,PULL解析XML,兴趣的朋友可以去看一下这【XML解析(一)】SAX解析XML,【XML解析(二)】DOM解析XML,【XML解析(三)】PULL解析XML三篇文章学习一下XML解析。我们知道客户端请求服务器,服务器给我们返回的数据通常不只是xml,还可以是json,html,当然json和xml是用的最多的了,下篇文章将会向大家解析如何解析html数据,这篇文章先向大家介绍如何解析服务器给我们返回的json数据。 一、概述 JSON是JavaScript Object Notation的简称,起源于js(javascript)它是一种轻量级的数据交换格式,JSON不仅在js中广泛使用,同时还在其他领域得到广泛使用,如c,c++,java,Php,swift等等,成为了一种通用的理想数据交换格式,它有两种数据结构,分别是对象,数组,它形式上有花括号{}和中括号[]嵌套,{}中的是代表对象,[]中的为数组,即对象中有数组,数组中又有对象,而且以及键/值对出现。 JSON语法: json是javascript对象表示语法的子集 数据在键值对中 数据有逗号分隔 花括号保存对象 JSON的值: 数字(整数或浮点数) 字符串(在双引号中 逻辑值(true 或 false) 数组(在方括号中) 对象(在花括号中) null json和xml比较: 数据体积小,耗费流量比xml少; 可读性比xml稍差,但格式化后也很直观; 与JavaScript交互比Xml方便; 速度比xml快; 拥有和xml同样多的解析方式。 更多信息参考:json官网,json百科 大概了解了JSON,下面将介绍在Android中通过采用android内置的org.json包,android 3.0 新出的JsonReader,google提供的gson解析json这三种常用的方式解析json。 二、JSON数据准备 要学习怎么解析json,咋们先要得到json数据,得到json数据方式有很多种,比如:webservice接口api,自己写个服务器端,或者自己在代码中写一个json格式的字符串。下面我们将通过金山词霸开放平台为我们提供的每日一句的api接口演示三种解析json的方法。 金山词霸每日一句api接口:http://open.iciba.com/dsapi 要解析json,我们得先知道要解析json的格式及内容,我们先用浏览器访问每日有一句api接口看看返回的数据。 访问结果: {"sid":"1683","tts":"http:\/\/news.iciba.com\/admin\/tts\/2015-12-07-day.mp3","content":"You aspire to do great things? Begin with little ones.\t","note":"\u60f3\u6210\u5c31\u5927\u4e8b\uff0c\u5c31\u8981\u4ece\u5c0f\u4e8b\u5f00\u59cb\u3002\uff08Augustine of Hippo\uff09","love":"2437","translation":"\u8bcd\u9738\u5c0f\u7f16\uff1a\u62e5\u6709\u597d\u5fc3\u60c5\u7684\u6700\u4f73\u65b9\u5f0f\u5c31\u662f\u201c\u5e72\u6b63\u4e8b\u201d\u3002\u5b66\u4f1a\u4e86\u89c4\u5b9a\u7684\u5355\u8bcd\uff0c\u8bfb\u5b8c\u4e86\u5fc5\u8bfb\u7684\u4e66\uff0c\u6536\u5c3e\u4e86\u5de5\u4f5c\uff0c\u953b\u70bc\u6ca1\u6709\u5077\u61d2\u2026\u90a3\u4e48\u9047\u5230\u6001\u5ea6\u4e0d\u597d\u7684\u51fa\u79df\u53f8\u673a\uff0c\u591a\u6536\u94b1\u7684\u770b\u8f66\u5927\u5988\uff0c\u6392\u961f\u52a0\u585e\u7684\u65e0\u826f\u9752\u5e74\u4e5f\u4f1a\u4e00\u7b11\u7f6e\u4e4b\uff0c\u5fc3\u4e2d\u5145\u5b9e\uff0c\u624d\u6709\u5e95\u6c14\u5feb\u4e50\u3002\u3010\u5173\u6ce8\u8bcd\u9738\u5c0f\u59b9\u5fae\u4fe1\uff08\u5fae\u4fe1\u53f7\uff1aijinshanciba\uff09\uff0c\u6709\u60ca\u559c\u5466\uff01\u3011","picture":"http:\/\/cdn.iciba.com\/news\/word\/2015-12-07.jpg","picture2":"http:\/\/cdn.iciba.com\/news\/word\/big_2015-12-07b.jpg","caption":"\u8bcd\u9738\u6bcf\u65e5\u4e00\u53e5","dateline":"2015-12-07","s_pv":"6694","sp_pv":"121","tags":[{"id":"13","name":"\u540d\u4eba\u540d\u8a00"},{"id":"16","name":"\u6cbb\u6108\u7cfb"}],"fenxiang_img":"http:\/\/cdn.iciba.com\/web\/news\/longweibo\/imag\/2015-12-07.jpg"} 哎呀,这尼玛!,这是什么啊,简直是无法直视啊,还要解析?不过不要紧,我们可以对该json数据先格式化一下,市场上有很多json格式化工具及在线json格式工具等等,这里我推荐一个工具,叫HiJson,点击这里下载:HiJson下载 HiJson的使用非常简单,下面用一张图说明一下HiJson的使用: 其中面板3显示面板2选择节点的键/值 将每日一句的json的数据格式化后的结果: { "caption": "词霸每日一句", "content": "You aspire to do great things? Begin with little ones. ", "dateline": "2015-12-07", "fenxiang_img": "http://cdn.iciba.com/web/news/longweibo/imag/2015-12-07.jpg", "love": "2437", "note": "想成就大事,就要从小事开始。(Augustine of Hippo)", "picture": "http://cdn.iciba.com/news/word/2015-12-07.jpg", "picture2": "http://cdn.iciba.com/news/word/big_2015-12-07b.jpg", "s_pv": "6694", "sid": "1683", "sp_pv": "121", "tags": [ { "id": "13", "name": "名人名言" }, { "id": "16", "name": "治愈系" } ], "translation": "词霸小编:拥有好心情的最佳方式就是“干正事”。学会了规定的单词,读完了必读的书,收尾了工作,锻炼没有偷懒…那么遇到态度不好的出租司机,多收钱的看车大妈,排队加塞的无良青年也会一笑置之,心中充实,才有底气快乐。【关注词霸小妹微信(微信号:ijinshanciba),有惊喜呦!】", "tts": "http://news.iciba.com/admin/tts/2015-12-07-day.mp3" } 可以看到格式化后的数据结构非常清晰,这就是标准的json格式,有了json数据,下面我们就用org.json包(无需导入),JsonReader,Gson解析该json数据。 三、3种方式解析JSON实战(代码编写) 打开eclipse(这里只是演示一下解析json,就不用as(Android Studio)了)新建一个Android项目,为了方便,这里先将整个项目的目录结构贴出来,整个项目的代码将会在文章贴在文章末尾,方便下载。 目录结构: 代码编写: 1、 写一个工具类,JsonParseUtils.java,写一个请求得到json数据的方法 /** * 连接网络请求数据,这里使用HttpURLConnection */ public static String getJsonData() { URL url = null; String jsonData = ""; // 请求服务器返回的json字符串数据 InputStreamReader in = null; // 读取的内容(输入流) try { url = new URL("http://open.iciba.com/dsapi"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 这一步会连接网络得到输入流 in = new InputStreamReader(conn.getInputStream()); // 为输入创建BufferedReader BufferedReader br = new BufferedReader(in); String inputLine = null; while(((inputLine = br.readLine()) != null)){ jsonData += inputLine; } in.close(); // 关闭InputStreamReader // 断开网络连接 conn.disconnect(); } catch (Exception ex) { ex.printStackTrace(); } return jsonData; } 注意:HttpURLConnection默认是Get请求,如果要使用Post请求,需要对HttpURLConnection做一些配置,得到数据后记得关闭流和断开网络链接。 不要忘了添加请求网络的权限 <uses-permission android:name="android.permission.INTERNET"/> 2、解析JSON 方式一:使用org.json包解析 /** * 通过org.json解析json * @param jsonStr json字符串 * @throws JSONException 格式不对,转换异常 */ public static Sentence parseJsonByOrgJson(String jsonStr) throws JSONException{ // 使用该方法解析思路,遇到大括号用JsonObject,中括号用JsonArray // 第一个是大括号{} JSONObject jsonObj = new JSONObject(jsonStr); // 新建Sentence对象 Sentence sentence = new Sentence(); // 以下是无脑操作 String caption = jsonObj.getString("caption"); String content = jsonObj.getString("content"); String dateline = jsonObj.getString("dateline"); String fenxiang_img = jsonObj.getString("fenxiang_img"); String love = jsonObj.getString("love"); String note = jsonObj.getString("note"); String picture = jsonObj.getString("picture"); String picture2 = jsonObj.getString("picture2"); String s_pv = jsonObj.getString("s_pv"); String sp_pv = jsonObj.getString("sp_pv"); String translation = jsonObj.getString("translation"); String tts = jsonObj.getString("tts"); sentence.caption = caption; sentence.content = content; sentence.dateline = dateline; sentence.fenxiang_img = fenxiang_img; sentence.love = love; sentence.note = note; sentence.picture = picture; sentence.picture2 = picture2; sentence.s_pv = s_pv; sentence.sp_pv = sp_pv; sentence.translation = translation; sentence.tts = tts; // 解析关键字tags,它是一个JsonArray,遇到[] JSONArray jsonArray = jsonObj.getJSONArray("tags"); // 新建Tag集合 List<Sentence.Tag> tags = new ArrayList<Sentence.Tag>(); for(int i=0;i<jsonArray.length();i++){ Sentence.Tag tag = new Sentence.Tag(); // jsonArray里的每一项都是JsonObject JSONObject jsonObject = jsonArray.getJSONObject(i); tag.id = jsonObject.getInt("id"); tag.name = jsonObject.getString("name"); tags.add(tag); } sentence.tags = tags; return sentence; } 使用这种方法解析JSON,看注释,没什么好多的,总结一句话就是:遇到{}用JSONObject,遇到[]用JSONArray,这样你就可以说你精通org.json解析JSON了。 方式二:使用JsonReader解析JSON,JsonReader解析JSON有点类似PULL解析XML,主要的方法还是nextName()将游标后移。 /** * Call requires API level 11 (current min is 8): new * android.util.JsonReader 通过org.json解析json * * @param jsonStr * json字符串 * @throws Exception */ @SuppressLint("NewApi") public static Sentence parseJsonByJsonReader(String jsonStr) throws Exception { // 新建Sentence Sentence sentence = new Sentence(); // 新建Tag集合 List<Sentence.Tag> tags = new ArrayList<Sentence.Tag>(); JsonReader reader = new JsonReader(new StringReader(jsonStr)); // 遇到{,开始解析对象 reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if ("caption".equals(name)) { sentence.caption = reader.nextString(); } if ("content".equals(name)) { sentence.content = reader.nextString(); } if ("dateline".equals(name)) { sentence.dateline = reader.nextString(); } if ("fenxiang_img".equals(name)) { sentence.fenxiang_img = reader.nextString(); } if ("love".equals(name)) { sentence.love = reader.nextString(); } if ("note".equals(name)) { sentence.note = reader.nextString(); } if ("picture".equals(name)) { sentence.picture = reader.nextString(); } if ("picture2".equals(name)) { sentence.picture2 = reader.nextString(); } if ("s_pv".equals(name)) { sentence.s_pv = reader.nextString(); } if ("sid".equals(name)) { sentence.sid = reader.nextString(); } if ("sp_pv".equals(name)) { sentence.sp_pv = reader.nextString(); } if ("translation".equals(name)) { sentence.translation = reader.nextString(); } if ("tts".equals(name)) { sentence.tts = reader.nextString(); } if ("tags".equals(name)) { // 遇到[,开始解析数组 reader.beginArray(); while (reader.hasNext()) { // 遇到{,开始解析对象 reader.beginObject(); Sentence.Tag tag = new Sentence.Tag(); if ("id".equals(reader.nextName())) { tag.id = reader.nextInt(); } if ("name".equals(reader.nextName())) { tag.name = reader.nextString(); } // 遇到},对象解析结束 reader.endObject(); tags.add(tag); } sentence.tags = tags; // 遇到],数组解析结束 reader.endArray(); } } // 遇到},对象解析结束 reader.endObject(); return sentence; } JsonReader解析JSON: 当开始解析对象时(遇到"{"就JsonReader.beginObject()),当这个对象解析结束了(遇到"}")就endObject()结束对象的解析。 当开始解析数组时(遇到"["就JsonReader.beginArray()),当这个数组解析结束了(遇到"]")就endArray()结束数组的解析。 注意nextXXX()都会是游标后移,如果有数据忘了解析等等导致游标错位,将会导致数据类型错乱,这个时候就会很容易发生:java.lang.IllegalStateException: Expected a X but was Y ...参数匹配异常。所以一定不要漏了数据和注意每一次移动游标。 方式三:使用GSON解析 1、下载Gson包放到项目lib目录下面,并添加到构建路径中 gson的jar包下载:gson-2.5.jar 2、编写JavaBean package com.example.jsonparsedemo; import java.util.List; // 由于简单起见,直接用public public class Sentence { public String caption; public String content; public String dateline; public String fenxiang_img; public String love; public String note; public String picture; public String picture2; public String s_pv; public String sid; public String sp_pv; public String translation; public String tts; public List<Tag> tags; static class Tag{ public int id; public String name; @Override public String toString() { return "id=" + id + "\n" + "name=" + name + "\n" ; } } @Override public String toString() { return "caption=" + caption + "\n" + "content=" + content+ "\n" + "dateline=" + dateline+ "\n" + "fenxiang_img=" + fenxiang_img+ "\n" + "love=" + love + "\n" + "note=" + note + "\n" + "picture=" + picture + "\n" + "picture2=" + picture2 + "\n" + "s_pv=" + s_pv + "\n" + "sid=" + sid + "\n" + "sp_pv="+ sp_pv + "\n" + "translation=" + translation + "\n" + "tts=" + tts + "\n" + "tags=" + tags + "\n"; } } JavaBean的编写(重点): 其中属性名称和json数据的键名必须相同 内部类不必是static(亲测) 类属性可以是其他权限修饰符,包括private,并且可以不用写setter和getter(亲测) 3、新建gson对象解析json, /** * 通过GSON解析json * @param jsonStr * @return */ public static Sentence parseJsonByGson(String jsonStr){ Gson gson = new Gson(); Sentence sentence = gson.fromJson(jsonStr, Sentence.class); return sentence; } 就三行代码,没什么说的,到这里三种解析json的方式都介绍完了,感觉每种方式都好简单,想不精通JSON解析都难。 JsonParseUtils.java的完整代码: package com.example.jsonparsedemo; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.annotation.SuppressLint; import android.util.JsonReader; import com.google.gson.Gson; @SuppressLint("NewApi") public class JsonParseUtils { /** * 连接网络请求数据,这里使用HttpURLConnection */ public static String getJsonData() { URL url = null; String jsonData = ""; // 请求服务器返回的json字符串数据 InputStreamReader in = null; // 读取的内容(输入流) try { url = new URL("http://open.iciba.com/dsapi"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 这一步会连接网络得到输入流 in = new InputStreamReader(conn.getInputStream()); // 为输入创建BufferedReader BufferedReader br = new BufferedReader(in); String inputLine = null; while (((inputLine = br.readLine()) != null)) { jsonData += inputLine; } in.close(); // 关闭InputStreamReader // 断开网络连接 conn.disconnect(); } catch (Exception ex) { ex.printStackTrace(); } return jsonData; } /** * 通过org.json解析json * * @param jsonStr * json字符串 * @throws JSONException * 格式不对,转换异常 */ public static Sentence parseJsonByOrgJson(String jsonStr) throws JSONException { // 使用该方法解析思路,遇到大括号用JsonObject,中括号用JsonArray // 第一个是大括号 JSONObject jsonObj = new JSONObject(jsonStr); // 新建Sentence对象 Sentence sentence = new Sentence(); // 以下是无脑操作 String caption = jsonObj.getString("caption"); String content = jsonObj.getString("content"); String dateline = jsonObj.getString("dateline"); String fenxiang_img = jsonObj.getString("fenxiang_img"); String love = jsonObj.getString("love"); String note = jsonObj.getString("note"); String picture = jsonObj.getString("picture"); String picture2 = jsonObj.getString("picture2"); String s_pv = jsonObj.getString("s_pv"); String sid = jsonObj.getString("sid"); String sp_pv = jsonObj.getString("sp_pv"); String translation = jsonObj.getString("translation"); String tts = jsonObj.getString("tts"); sentence.caption = caption; sentence.content = content; sentence.dateline = dateline; sentence.fenxiang_img = fenxiang_img; sentence.love = love; sentence.note = note; sentence.picture = picture; sentence.picture2 = picture2; sentence.s_pv = s_pv; sentence.sid = sid; sentence.sp_pv = sp_pv; sentence.translation = translation; sentence.tts = tts; // 解析关键字tags,它是一个JsonArray JSONArray jsonArray = jsonObj.getJSONArray("tags"); // 新建Tag集合 List<Sentence.Tag> tags = new ArrayList<Sentence.Tag>(); for (int i = 0; i < jsonArray.length(); i++) { Sentence.Tag tag = new Sentence.Tag(); // jsonArray里的每一项都是JsonObject JSONObject jsonObject = jsonArray.getJSONObject(i); tag.id = jsonObject.getInt("id"); tag.name = jsonObject.getString("name"); tags.add(tag); } sentence.tags = tags; return sentence; } /** * Call requires API level 11 (current min is 8): new * android.util.JsonReader 通过org.json解析json * * @param jsonStr * json字符串 * @throws Exception */ @SuppressLint("NewApi") public static Sentence parseJsonByJsonReader(String jsonStr) throws Exception { // 新建Sentence Sentence sentence = new Sentence(); // 新建Tag集合 List<Sentence.Tag> tags = new ArrayList<Sentence.Tag>(); JsonReader reader = new JsonReader(new StringReader(jsonStr)); // 遇到{,开始解析对象 reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if ("caption".equals(name)) { sentence.caption = reader.nextString(); } if ("content".equals(name)) { sentence.content = reader.nextString(); } if ("dateline".equals(name)) { sentence.dateline = reader.nextString(); } if ("fenxiang_img".equals(name)) { sentence.fenxiang_img = reader.nextString(); } if ("love".equals(name)) { sentence.love = reader.nextString(); } if ("note".equals(name)) { sentence.note = reader.nextString(); } if ("picture".equals(name)) { sentence.picture = reader.nextString(); } if ("picture2".equals(name)) { sentence.picture2 = reader.nextString(); } if ("s_pv".equals(name)) { sentence.s_pv = reader.nextString(); } if ("sid".equals(name)) { sentence.sid = reader.nextString(); } if ("sp_pv".equals(name)) { sentence.sp_pv = reader.nextString(); } if ("translation".equals(name)) { sentence.translation = reader.nextString(); } if ("tts".equals(name)) { sentence.tts = reader.nextString(); } if ("tags".equals(name)) { // 遇到[,开始解析数组 reader.beginArray(); while (reader.hasNext()) { // 遇到{,开始解析对象 reader.beginObject(); Sentence.Tag tag = new Sentence.Tag(); if ("id".equals(reader.nextName())) { tag.id = reader.nextInt(); } if ("name".equals(reader.nextName())) { tag.name = reader.nextString(); } // 遇到},对象解析结束 reader.endObject(); tags.add(tag); } sentence.tags = tags; // 遇到],数组解析结束 reader.endArray(); } } // 遇到},对象解析结束 reader.endObject(); return sentence; } /** * 通过GSON解析json * @param jsonStr * @return */ public static Sentence parseJsonByGson(String jsonStr){ Gson gson = new Gson(); Sentence sentence = gson.fromJson(jsonStr, Sentence.class); return sentence; } } 四 、3种方式解析JSON实战(测试结果) 由于本篇文章并不打算将解析的数据用android控件显示出来,只是解析json数据,有了数据还不会用吗,哈哈,数据在手,天下我有。下面我们通过Android的单元测试3种方式解析的结果。 其实Android的单元测试已经在【XML解析(一)】SAX解析XML文章中介绍过了,这里为了完整性及照顾新手,这里在说一遍吧,也可以去看那篇文章。 Android单元测试 第一步:环境搭建 在 AndroidManifest.xml的根节点下面添加一个instrumentation <instrumentation android:targetPackage="com.example.jsonparsedemo" android:name="android.test.InstrumentationTestRunner"></instrumentation> 第二步: 新建一个测试类JsonParseTest.java(测试的类可以放在该应用的任何包下面,但必须继承AndroidTestCase) 写一个测试方法测试obj.json解析json /** * 测试obj.json解析json */ public void testObjJsonParseJson() throws Exception{ Log.e("obj.json", "-------------------------"); String jsonData = JsonParseUtils.getJsonData(); Sentence sentence = JsonParseUtils.parseJsonByOrgJson(jsonData); Log.e("result", sentence.toString()); } 写一个测试方法测试JsonReader解析json /** * 测试JsonReader解析json */ public void testJsonReaderParseJson() throws Exception{ Log.e("JsonReader", "-------------------------"); String jsonData = JsonParseUtils.getJsonData(); Sentence sentence = JsonParseUtils.parseJsonByJsonReader(jsonData); Log.e("result", sentence.toString()); } 写一个测试方法测试JsonReader解析json /** * 测试JsonReader解析json */ public void testGsonParseJson() throws Exception{ Log.e("gson", "-------------------------"); String jsonData = JsonParseUtils.getJsonData(); Sentence sentence = JsonParseUtils.parseJsonByGson(jsonData); Log.e("result", sentence.toString()); } 分别运行上面的三个方法,Run As Android JUnit Test 测试结果: org.json解析结果 JsonReader解析结果 gson解析结果 OK,三种方法都成功解析json,是不是感觉json解析简直是太简单了,简单到不想解析。 总结:无论是XML还是JSON,或者HTML的解析都很简单,我们只要知道解析的原理和技巧就能解析任何的XML,JSON数据了,虽然在这之前还没写过解析HTML,但HTML解析也非常简单,下篇文章将会介绍如何解析HTML,实现网络爬虫,敬请期待。 由于个人水平有限,文章所介绍的知识有错误的地方,欢迎大家指出,与君共勉,一起进步。 下篇文章:【解析HTML】HTML解析,网络爬虫。敬请期待。 Demo下载 http://www.cnblogs.com/ydxlt/p/5027527.html?utm_source=tuicool&utm_medium=referral
简介 理解SQL Server对于内存的管理是对于SQL Server问题处理和性能调优的基本,本篇文章讲述SQL Server对于内存管理的内存原理。 二级存储(secondary storage) 对于计算机来说,存储体系是分层级的。离CPU越近的地方速度愉快,但容量越小(如图1所示)。比如:传统的计算机存储体系结构离CPU由近到远依次是:CPU内的寄存器,一级缓存,二级缓存,内存,硬盘。但同时离CPU越远的存储系统都会比之前的存储系统大一个数量级。比如硬盘通常要比同时代的内存大一个数量级。 图1.计算机存储体系 因此对于SQL Server来说,正常的生产系统所配置的内存通常不能装载所有数据,因此会涉及到二级存储,也就是磁盘。磁盘作为现代计算机系统中最后的机械存储部件,读取数据需要移动磁头(具体关于磁盘的原理,可以看我之前写的一篇文章),并且由于数据库所访问的数据往往是随机分布在磁盘的各个位置,因此如果频繁的读取磁盘需要频繁的移动磁头,这个性能将会十分底下。 由计算机体存储体系结构可以知道,计算机对于所有硬盘内数据的操作都需要首先读取到内存,因此利用好内存的缓冲区而减少对磁盘IO的访问将会是提升SQL Server性能的关键,这也是本篇文章写作的出发点之一。 SQL Server引擎,一个自我调整的引擎 由于SQL Server过去一直面向是中小型企业市场的原因,SQL Server存储引擎被设计成一个不需要太多配置就能使用的产品,从而减少了部署成本,但这也是很多人一直诟病的微软开放的配置过少。而对于SQL Server如何使用内存,几乎没有直接可以配置的空间,仅仅开放的配置只有是否使用AWE,以及实例占用的最大或最小内存,如图2所示。 图2.SQL Server可控控制内存的选项 而对于具体的SQL Server如何使用内存,例如分配给执行计划缓存多少,分配给数据buffer多少,这些都无法通过配置进行调控。这也是很多其它技术的开发人员对于使用微软技术的开发人员充满优越感的原因,而在我看来,虽然SQL Server提供可控配置的地方很少,但是很多地方都可以在通晓原理的情况下进行“间接”的配置。这也需要了解一些Windows的原理。 SQL Server是如何使用内存的 SQL Server存储引擎本身是一个Windows下的进程,所以SQL Server使用内存和其它Windows进程一样,都需要向Windows申请内存。从Windows申请到内存之后,SQL Server使用内存粗略可以分为两部分:缓冲池内存(数据页和空闲页),非缓冲内存(线程,DLL,链接服务器等)。而缓冲池内存占据了SQL Server的大部分内存使用。缓冲池所占内存也就是图2最大最小内存所设置的,因此sqlservr.exe所占的内存有可能会大于图2中所设置的最大内存。 还有一点是,SQL Server使用内存的特点是:有多少用多少,并且用了以后不释放(除非收到Windows内存压力的通知)。比如我所在公司的开发服务器,在几乎没有负载的时候来看内存使用,如图3所示。 图3.SQL Server 进程的内存使用 可以看到CPU在0负载的时候,内存却占据了13个G。这其实是在之前的使用SQL Server向Windows申请的内存一直没有释放所致。 具体SQL Server能够使用多少内存是由以下几个因素决定的: 1.物理内存的大小 2.所安装Windows版本对于内存的限制(比如windows server 2008标准版限制最大内存只能使用32GB) 3.SQL Server是32位或64位 4.如图2所示配置SQL Server对于内存的使用量 5.SQL Server的版本(比如express版只能用1G内存) SQL Server OS的三层内存分配 SQL Server OS对于内存的分配分为三个层级,依赖关系如图4所示。 图4.SQL Server OS内存依赖关系 Memory Node 首先最底层的是Memory Node,Memory Node的作用是使得分配内存由Windows移交到SQL Server OS层面执行。每个SQL Server实例通常都只拥有一个Memory Node,Memory Node的多寡只取决于NUMA构架的硬件配置。我们通过 DBCC MEMORYSTATUS 可以看到Memory Node的一些信息,如图5所示。 图5.查看Memory Node信息 我们可以看出 ,按照申请内存大小分类,可以分为两部分 1.申请小于等于8KB为一个单位的内存,这些内存被用于缓存。(图5中的SinglePage Allocator) 2.申请大于8KB为一个单位的内存,这些内存称为Multi-Page(或MemToLeave)(图5中的MultiPage Allocator) 对于为什么叫MemToLeave,被称为MemToLeave的原因是由于SQL Server虽然大部分内存被用于缓冲区,但还需要一些连续的内存用于SQL CLR,linked server,backup buffer等操作,32位SQL Server在启动实例时会保留一部分连续的虚拟地址(VAS)用于进行MultiPage Allocator。具体保留多少可以用如下公式计算: 保留地址=((CPU核数量-4)+256)*0.5MB+256MB,通常在384MB左右。 Memory Clerk 让我们再来看Memory Clerk,Memory Clerk用于分配内存,用于将Allocate出去的内存进行分类,可以简单的进行如下语句,如图6所示. 图6.按照Memory Clerk的类别进行分类 注意:由图4可以看到,Memory Clerk只是分配内存的一部分,另一部分是数据缓存(Buffer Pool) Buffer Pool 在开始讲述Buffer Pool之前,首先想讲一下虚拟内存。 在Windows中每个进程都有一个虚拟内存(Virtual Address Space VAS),32位系统是2的32次方,也就是4G,这4G被Windows划为两部分,一部分是Windows使用,另一部分才是应用程序使用。虚拟内存并不是实际的物理内存,而是对于物理内存的映射,当物理内存不存在虚拟内存指向的内容时,产生缺页中断,将一部分页面置换出内存,然后将需要的部分从硬盘读到内存,关于这块,可以读我之前写的一篇文章:浅谈操作系统对内存的管理。 因此Buffer Pool的作用是缓冲数据页,使得未来读取数据时减少对磁盘的访问。 这个Buffer Pool这部分就是图2中设置最大最小服务器内存所占用的空间。这个最小值并不意味着SQL Server启动时就能占用这么多内存,而是SQL Server Buffer Pool的使用一旦超过这个值,就不会再进行释放了。 在DBCC MEMORYSTATUS 其中有一部分我们可以看到Buffer Pool的信息,如图7所示。 图7.Buffer Pool的相关信息 在SQL Server实例启动时,Buffer Pool所保留的VAS地址空间取决于多个因素:包括实际的物理内存和SQL Server是32位或是64位(这个限制32位是4G,还要划一半给Windows和减去MemToLeave空间),而对于实际上SQL Server所使用的物理内存,可以通过如下语句查看,如图8所示。 图8.查看Buffer Pool所使用物理内存 Buffer Pool会按照需要不断的提出内存申请。Buffer Pool如果需要,Buffer Pool会不断消耗内存,直到Windows通知SQL Server内存过低时,Buffer Pool才有可能释放内存,否则Buffer Pool占据了内存不会释放。 另外值得注意的一点是,Buffer Pool所分配的页面和SQL Server OS页面大小是一致的,也就是8192字节,当SQL Server其它部分需要向”Buffer Pool”借内存时,也只能按照8k为单位借,并且这部分内存在物理内存中是不连续的,这听上去像是Buffer Pool内存管理自成体系,可以这么理解,因为Buffer Pool 不使用任何SQL Server的page allocator,而直接使用virtual或AWE SQLOS's的接口。 所以SQL Server所占用的内存可以用这个公式粗略估算出来: buffer pool占用的内存+从buffer pool借的页占得内存+multiPageAllocator分配的非buffer pool内存,如图9所示。 图9.可以近似的估算出sql server所占的内存 Memory Object menory object本质上是一个堆,由Page Allocator进行分配,可以通过sys.dm_os_memory_objects这个DMV进行查看,这个DMV可以看到有一列Page_Allocator_Address列,这列就是Memory Clerk的标识,表明这个Memory Object是由哪个Memory Clerk进行分配的。 32位SQL Server的内存瓶颈 由文章前面所述的一些基本原理可以看出,由于32位的SQL Server使用的是VAS进行地址分配,因此寻址空间被限制在4GB,这4GB还要有一半分给Windows,使得Buffer Pool最多只能用到2G的内存,这使得32位SQL Server即使有多余的物理内存,也无法使用。 解决办法之一是通过减少Windows默认占用的2G到1G,使得SQL Server可以使用的内存变为3G。这个可以通过在Windows Server 2008中的命令行键入 BCDEdit /set设置increaseuserva选项,设置值为3072MB,对于Windows Server 2003来说,需要在boot.ini中加上/3gb启动参数。 另一种办法是使用AWE(Address Window Extension)分配内存。AWE通过计算机物理地址扩展(Physical Address Extension PAE),增加4位,使得32位的CPU寻址范围增加到2的36次方,也就是64GB。基本解决了寻址范围不够的问题。 VirtualAlloc和AllocateUserPhysicalPages VirtualAlloc和AllocateUserPhysicalPages是SQL Server向Windows申请内存所使用的方法。在默认情况下,SQL Server所需要的所有内存都会使用VirtualAlloc去Windows申请内存,这种申请是操作系统层面的,也就是直接对应的虚拟内存。这导致一个问题,所有通过VirtualAlloc分配的内存都可以在Windows面临内存压力时被置换到虚拟内存中。这会造成IO占用问题。 而使用AllocateUserPhysicalPages所申请的内存,直接和更底层的页表(Page Table)进行匹配,因此使用这个方法申请的内存不会被置换出内存。在32位SQL Server的情况下,通过开启AWE分配内存,buffer pool中的data cache部分将会使用这个函数,而MemToLeave部分和Buffer Pool中的另一部分内存(主要是执行计划缓存)依然通过VirtualAlloc进行内存分配。 因此在开启通过AWE分配内存之前,SQL Server首先需要对应的权限,否则就会在日志中报错,如图10所示。 图10.开启AWE却没有开启对应权限报错 我们可以在组策略里设置启动SQL Server的账户拥有这个权限,如图11所示。 图11.锁定内存页(Lock Page In Memory) 64位SQL Server的问题 64位Windows基本已经不存在上述的内存问题,但是依然要注意,在默认情况下,64位的SQL Server使用的依然是VirtualAlloc进行内存分配,这意味着所有分配的内存都会在Windows面临压力时将页置换出去,这很可能造成抖动(Buffer Pool Churn),这种情况也就是SQL Server Buffer Pool中的页不断的被交换进硬盘,造成大量的IO占用(可以通过sys.dm_exec_query_memory_grants这个DMV查看等待内存的查询),因此64位SQL Server将Buffer Pool中的Date Page通过AllocateUserPhysicalPages来进行内存分配就能避免这个问题。与32位SQL Server不同的是,64位SQL Server并不需要开启AWE,只需开启如图11所示的“Lock Page In Memory”就行了。 但这又暴漏出了另一个问题,因为SQL Server锁定了内存页,当Windows内存告急时,SQL Server就不能对Windows的内存告急做出响应(当然了Buffer Pool中的非data cache和MemToLeave部分依然可以,但往往不够,因为这部分内存相比Data Cache消耗很小),因为SQL Server的特性是内存有多少用多少,因此很有可能在无法做出对Windows低内存的响应时造成Windows的不稳定甚至崩溃。因此开启了”Lock Page In Memory”之后,要限制SQL Server Buffer Pool的内存使用,前面图2中已经说了,这里就不再细说了。 还有一个问题是当Buffer Pool通过AllocateUserPhysicalPages分配内存时,我们在任务管理器中看到的sqlservr.exe占用的内存就仅仅包含Buffer Pool中非Data Cache部分和MemToLeave部分,而不包含Data Cache部分,因此看起来有可能造成sqlservr.exe只占用了几百兆内存而内存的使用是几十G。这时我们就需要在Perfmon.exe中查看SQL Server:Memory Manager\Total Server Memory计数器去找到SQL Server真实占用的内存。 总结 本文讲述了SQL Server对内存管理的基本原理和SQL Server对内存使用所分的部分,对于SQL Server性能调优来说,理解内存的使用是非常关键的一部分,很多IO问题都有可能是内存所引起的。 点击这里下载本文的PDF版本
性能计数器和sql profiler都是常用的性能诊断工具和优化工具,最近和群友聊天发现很多人竟然不知道这两个可以“组合”使用,所以这篇算是一篇扫盲贴吧。 两种工具简述 通过计数器可以收集两部分内容:WINDOWS 的运行指标,和SQL Server的指标。比如:服务器的CPU使用率、磁盘队列、内存情况、锁的情况等等。 通过profiler主要收集语句的运行情况,运行时间,读写消耗等。SQL Server的从业者,如果没用过profiler,那么需要补一下了。 这部分我想我不用介绍了吧。百度上,园子里太多太多的文章了,请自行学习吧。 组合使用 很多时候经验比较丰富的数据库从业人员,可能根本用不到我今天说的组合方法,因为他们对指标(数据)已经很敏感了,看到数据基本已经能看出问题。但是可能对于一般从业者或者向完全不懂数据库的领导汇报时也算是一种漂亮的展现吧。 跳过前两步(计数器和profiler)的收集,我已经准备好了两份文件,(sql server profiler文件和性能监控的文件) 注:这里有一个注意点,性能计数器收集的时间必须和profiler的收集时间有重合。也不难理解,要把两部分在一起展现,那么昨天的计数器和今天的profiler 必然没什么关联性! 首先打开收集的profiler文件,选择【文件】-点击【导入性能数据】 选择之前收集的windows性能计数器文件 选择要展示的计数器 效果:上半部分显示profiler的语句执行情况,下半部分展示的是对应语句的计数器指标。并可以勾选不同计数器,了解(profiler中选中的)语句执行时各种计数器的指标是什么样子,或排查到底是那条语句使得你CPU过高或磁盘明显压力! --------------博客地址------------------------------------------------------------------------------ 原文地址: http://www.cnblogs.com/double-K/ 如有转载请保留原文地址! ----------------------------------------------------------------------------------------------------- 总结 : 文章只是简单介绍了性能计数器与profiler组合使用的方法,可能大部分人都单独使用过计数器和profiler,但没有组合使用过,算是一项小功能介绍一下吧。 对于排查到底是哪条语句使得你CPU过高或磁盘明显压力,是一种不错的图形展示,给领导汇报的时候也有点依据。 ---------------------------------------------------------------------------------------------------- 注:此文章为原创,欢迎转载,请在文章页面明显位置给出此文链接! 若您觉得这篇文章还不错请点击下右下角的推荐,非常感谢!
【原文地址:http://www.cppblog.com/pansunyou/archive/2011/01/26/io_design_patterns.html】 综述 这篇文章探讨并比较两种用于TCP服务器的高性能设计模式. 除了介绍现有的解决方案, 还提出了一种更具伸缩性,只需要维护一份代码并且跨平台的解决方案(含代码示例), 以及其在不同平台上的微调. 此文还比较了java,c#,c++对各自现有以及提到的解决方案的实现性能. 系统I/O 可分为阻塞型, 非阻塞同步型以及非阻塞异步型[1, 2]. 阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里. 结果调用者被阻塞了, 这段时间了做不了任何其它事情. 更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧, 调用此函数的代码会一直僵在此处直至它所读的socket缓存中有数据到来. 相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试度看吧。比如read()操作, 如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用read()者"数据还没准备好,你稍后再试". 在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。 在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。 这篇文章探讨不同的I/O利用机制并提供一种跨平台的设计模式(解决方案). 希望此文可以给于TCP高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较 Java, c#, C++各自对探讨方案的实现以及性能. 我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式I/O实在是缺少可伸缩性,性能也达不到高性能服务器的要求。 两种IO多路复用方案:Reactor and Proactor 一般情况下,I/O 复用机制需要事件分享器(event demultiplexor [1, 3]). 事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁的什么东西送了, 快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(event handlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数. 涉及到事件分享器的两种模式称为:Reactor and Proactor [1]. Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的. 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个 事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。 而在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起 时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这 个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称有 overlapped的技术),事件分离者等IOCompletion事件完成[1]. 这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。 举另外个例子来更好地理解Reactor与Proactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法: 某个事件处理者宣称它对某个socket上的读事件很感兴趣; 事件分离者等着这个事件的发生; 当事件发生了,事件分离器被唤醒,这负责通知先前那个事件处理者; 事件处理者收到消息,于是去那个socket上读数据了. 如果需要,它再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤; 下面再来看看真正意义的异步模式Proactor是如何做的: 事件处理者直接投递发一个写操作(当然,操作系统必须支持这个异步操作). 这个时候,事件处理者根本不关心读事件,它只管发这么个请求,它魂牵梦萦的是这个写操作的完成事件。这个处理者很拽,发个命令就不管具体的事情了,只等着别人(系统)帮他搞定的时候给他回个话。 事件分离者等着这个读事件的完成(比较下与Reactor的不同); 当事件分离者默默等待完成事情到来的同时,操作系统已经在一边开始干活了,它从目标读取数据,放入用户提供的缓存区中,最后通知事件分离者,这个事情我搞完了; 事件分享者通知之前的事件处理者: 你吩咐的事情搞定了; 事件处理者这时会发现想要读的数据已经乖乖地放在他提供的缓存区中,想怎么处理都行了。如果有需要,事件处理者还像之前一样发起另外一个写操作,和上面的几个步骤一样。 现行做法 开源C++开发框架 ACE[1, 3](Douglas Schmidt, et al.开发) 提供了大量平台独立的底层并发支持类(线程、互斥量等). 同时在更高一层它也提供了独立的几组C++类,用于实现Reactor及Proactor模式。 尽管它们都是平台独立的单元,但他们都提供了不同的接口. ACE Proactor在MS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API. [4, 5]. (这段可能过时了点吧) 不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说, 许多Unix系统就有麻烦.因此, ACE Reactor可能是Unix系统上更合适的解决方案. 正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码: 为Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor. 就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为Reactor和Proactor设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。 更好的解决方案 在文章这一段时,我们将尝试提供一种融合了Proactor和Reactor两种模式的解决方案. 为了演示这个方案,我们将Reactor稍做调整,模拟成异步的Proactor模型(主要是在事件分离器里完成本该事件处理者做的实际读写工作,我们称这种方法为"模拟异步")。 下面的示例可以看看read操作是如何完成的: 事件处理者宣称对读事件感兴趣,并提供了用于存储结果的缓存区、读数据长度等参数; 调试者等待(比如通过select()); 当有事件到来(即可读),调试者被唤醒, 调试者去执行非阻塞的读操作(前面事件处理者已经给了足够的信息了)。读完后,它去通知事件处理者。 事件处理者这时被知会读操作已完成,它拥有完整的原先想要获取的数据了. 我们看到,通过为分离者(也就上面的调试者)添加一些功能,可以让Reactor模式转换为Proactor模式。所有这些被执行的操作,其实是和 Reactor模型应用时完全一致的。我们只是把工作打散分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细 看看下面的两个过程,他们实际上完成了一样的事情: 标准的经典的 Reactor模式: 步骤 1) 等待事件 (Reactor 的工作) 步骤 2) 发"已经可读"事件发给事先注册的事件处理者或者回调 ( Reactor 要做的) 步骤 3) 读数据 (用户代码要做的) 步骤 4) 处理数据 (用户代码要做的) 模拟的Proactor模式: 步骤 1) 等待事件 (Proactor 的工作) 步骤 2) 读数据(看,这里变成成了让 Proactor 做这个事情) 步骤 3) 把数据已经准备好的消息给用户处理函数,即事件处理者(Proactor 要做的) 步骤 4) 处理数据 (用户代码要做的) 在没有底层异步I/O API支持的操作系统,这种方法可以帮我们隐藏掉socket接口的差异(无论是性能还是其它), 提供一个完全可用的统一"异步接口"。这样我们就可以开发真正平台独立的通用接口了。 TProactor 我们提出的TProactor方案已经由TerabitP/L [6]公司实现了. 它有两种实现: C++的和Java的.C++版本使用了ACE平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。 TProactor中最重要的组件要数Engine和WaitStrategy了. Engine用于维护异步操作的生命周期;而WaitStrategy用于管理并发策略. WaitStrategy和Engine一般是成对出现的, 两者间提供了良好的匹配接口. Engines和等待策略被设计成高度可组合的(完整的实现列表请参照附录1)。TProactor是高度可配置的方案,通过使用异步内核API和同步Unix API(select(), poll(), /dev/poll (Solaris 5.8+), port_get (Solaris 5.10),RealTime (RT) signals (Linux 2.4+), epoll (Linux 2.6), k-queue (FreeBSD) ),它内部实现了三种引擎(POSIX AIO, SUN AIO and Emulated AIO)并隐藏了六类等待策略。TProactor实现了和标准的 ACE Proactor一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。 Engines和WaitStrategies可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。 可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层API,开发人员可以选择真正的异步策略,否则用户也 可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。 举例来说,对于运行在Sun Solaris上的HTTP服务器,如果需要支持大量的连接数,/dev/poll或者port_get()之类的引擎是比较合适的选择;如果需要高吞吐 量,那使用基本select()的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准ACE Reactor/Proactor模式所无法提供的(见附录2)。 在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor相较标签的ACE Reactor在Unix/Linux系统上有大约10-35%性能提升,而在Windows上差不多(测试了吞吐量及响应时间)。 性能比较 (JAVA / C++ / C#). 除了C++,我们也在Java中实现了TProactor. JDK1.4中, Java仅提供了同步方法, 像C中的select() [7, 8]. Java TProactor基于Java的非阻塞功能(java.nio包),类似于C++的TProactor使用了select()引擎. 图1、2显示了以 bits/sec为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的echo服务器:标准ACE Reactor实现(基于RedHat Linux9.0)、TProactor C++/Java实现(Microsoft Windows平台及RedHat v9.0), 以及C#实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。 这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。 图 1. Windows XP/P4 2.6GHz HyperThreading/512 MB RAM. 图 2. Linux RedHat 2.4.20-smp/P4 2.6GHz HyperThreading/512 MB RAM. 用户代码示例 下面是TProactor Java实现的echo服务器代码框架。总的来说,开发者只需要实现两个接口:一是OpRead,提供存放读结果的缓存;二是OpWrite,提供存储待 写数据的缓存区。同时,开发者需要通过回调onReadComplated()和onWriteCompleted()实现协议相关的业务代码。这些回调 会在合适的时候被调用. class EchoServerProtocol implements AsynchHandler { AsynchChannel achannel = null; EchoServerProtocol( Demultiplexor m, SelectableChannel channel ) throws Exception { this.achannel = new AsynchChannel( m, this, channel ); } public void start() throws Exception { // called after construction System.out.println( Thread.currentThread().getName() + ": EchoServer protocol started" ); achannel.read( buffer); } public void onReadCompleted( OpRead opRead ) throws Exception { if ( opRead.getError() != null ) { // handle error, do clean-up if needed System.out.println( "EchoServer::readCompleted: " + opRead.getError().toString()); achannel.close(); return; } if ( opRead.getBytesCompleted () <= 0) { System.out.println("EchoServer::readCompleted: Peer closed " + opRead.getBytesCompleted(); achannel.close(); return; } ByteBuffer buffer = opRead.getBuffer(); achannel.write(buffer); } public void onWriteCompleted(OpWrite opWrite) throws Exception { // logically similar to onReadCompleted ... } } 结束语 TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录2中提到的问题都被很好地隐藏在内部实现中了。 从上面的图中我们可以看出C++仍旧是编写高性能服务器最佳选择,虽然Java已紧随其后。然而因为Java本身实现上的问题,其在Windows上表现不佳(这已经应该成为历史了吧)。 需要注意的是,以上针对Java的测试,都是以裸数据的形式测试的,未涉及到数据的处理(影响性能)。 纵观AIO在Linux上的快速发展[9], 我们可以预计Linux内核API将会提供大量更加强健的异步API, 如此一来以后基于此而实现的新的Engine/等待策略将能轻松地解决能用性方面的问题,并且这也能让标准ACE Proactor接口受益。 附录 I TProactor中实现的Engines 和 等待策略 引擎类型 等待策略 操作系统 POSIX_AIO (true async)aio_read()/aio_write() aio_suspend() Waiting for RT signal Callback function POSIX complained UNIX (not robust) POSIX (not robust) SGI IRIX, LINUX (not robust) SUN_AIO (true async)aio_read()/aio_write() aio_wait() SUN (not robust) Emulated Async Non-blocking read()/write() select()poll() /dev/poll Linux RT signals Kqueue generic POSIX Mostly all POSIX implementations SUN Linux FreeBSD 附录 II 所有同步等待策略可划分为两组: edge-triggered (e.g. Linux实时信号) - signal readiness only when socket became ready (changes state); level-triggered (e.g. select(), poll(), /dev/poll) - readiness at any time. 让我们看看这两组的一些普遍的逻辑问题: edge-triggered group: after executing I/O operation, the demultiplexing loop can lose the state of socket readiness. Example: the "read" handler did not read whole chunk of data, so the socket remains still ready for read. But the demultiplexor loop will not receive next notification. level-triggered group: when demultiplexor loop detects readiness, it starts the write/read user defined handler. But before the start, it should remove socket descriptior from theset of monitored descriptors. Otherwise, the same event can be dispatched twice. Obviously, solving these problems adds extra complexities to development. All these problems were resolved internally within TProactor and the developer should not worry about those details, while in the synch approach one needs to apply extra effort to resolve them. 资源 [1] Douglas C. Schmidt, Stephen D. Huston "C++ Network Programming." 2002, Addison-Wesley ISBN 0-201-60464-7 [2] W. Richard Stevens "UNIX Network Programming" vol. 1 and 2, 1999, Prentice Hill, ISBN 0-13- 490012-X [3] Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann "Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects, Volume 2" Wiley & Sons, NY 2000 [4] INFO: Socket Overlapped I/O Versus Blocking/Non-blocking Mode. Q181611. Microsoft Knowledge Base Articles. [5] Microsoft MSDN. I/O Completion Ports.http://msdn.microsoft.com/library/default.asp?url=/library/en- us/fileio/fs/i_o_completion_ports.asp [6] TProactor (ACE compatible Proactor).www.terabit.com.au [7] JavaDoc java.nio.channelshttp://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/package-summary.html [8] JavaDoc Java.nio.channels.spi Class SelectorProvider http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/spi/SelectorProvider.html [9] Linux AIO development http://lse.sourceforge.net/io/aio.html, andhttp://archive.linuxsymposium.org/ols2003/Proceedings/All-Reprints/Reprint-Pulavarty-OLS2003.pdf 更多 Ian Barile "I/O Multiplexing & Scalable Socket Servers", 2004 February, DDJ Further reading on event handling- http://www.cs.wustl.edu/~schmidt/ACE-papers.html The Adaptive Communication Environmenthttp://www.cs.wustl.edu/~schmidt/ACE.html Terabit Solutionshttp://terabit.com.au/solutions.php 关于作者 Alex Libman has been programming for 15 years. During the past 5 years his main area of interest is pattern-oriented multiplatform networked programming using C++ and Java. He is big fan and contributor of ACE. Vlad Gilbourd works as a computer consultant, but wishes to spend more time listening jazz :) As a hobby,he started and runs www.corporatenews.com.au website.
Linux中一切皆文件,不论是我们存储在磁盘上的字符文件,可执行文件还是我们的接入电脑的I/O设备等都被VFS抽象成了文件,比如标准输入设备默认是键盘,我们在操作标准输入设备的时候,其实操作的是默认打开的一个文件描述符是0的文件,而一切软件操作硬件都需要通过OS,而OS操作一切硬件都需要相应的驱动程序,这个驱动程序里配置了这个硬件的相应配置和使用方法。Linux的I/O分为阻塞I/O,非阻塞I/O,I/O多路复用,信号驱动I/O四种。对于I/O设备的驱动,一般都会提供关于阻塞和非阻塞两种配置。我们最常见的I/O设备之一--键盘(标准输入设备)的驱动程序默认是阻塞的。多路复用就是为了使进程能够从多个阻塞I/O中获得自己想要的数据并继续执行接下来的任务。其主要的思路就是同时监视多个文件描述符,如果有文件描述符的设定状态的被触发,就继续执行进程,如果没有任何一个文件描述符的设定状态被触发,进程进入sleep多路复用的一个主要用途就是实现"I/O多路复用并发服务器",和多线程并发或者多进程并发相比,这种服务器的系统开销更低,更适合做web服务器,但是由于其并没有实现真正的多任务,所以当压力大的时候,部分用户的请求响应会较慢 阻塞I/O 阻塞I/O,就是当进程试图访问这个I/O设备而这个设备并没有准备好的时候,设备的驱动程序会通过内核让这个试图访问的进程进入sleep状态。阻塞I/O的一个好处就是可以大大的节约CPU时间,因为一旦一个进程试图访问一个没有准备好的阻塞I/O,就会进入sleep状态,而进入sleep状态的进程是不在内核的进程调度链表中,直到目标I/O准备好了将其唤醒并加入调度链表,这样就可以节约CPU时间。当然阻塞I/O也有其固有的缺点,如果进程试图访问一个阻塞I/O,但是否访问成功并不对接下来的任务有决定性影响,那么直接使其进入sleep状态显然会延误其任务的完成。 典型的默认阻塞IO有标准输入设备,socket设备,管道设备等,当我们使用gets(),scanf(),read()等操作请求这些IO时而IO并没有数据流入,就会造成进程的sleep。 进程会一直阻塞下去直到接收缓冲区中有数据可读,此时内核再去唤醒该进程,通过相应的函数从中获取数据。如果阻塞过程中对方发生故障,那么这个进程将会永远阻塞下去。 写操作时发生阻塞的情况要比读操作少,主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下,这时写操作将不进行任何任何拷贝工作,将发生阻塞。一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。udp不用等待确认,没有实际的发送缓冲区,所以udp协议中不存在发送缓冲区满的情况,在udp套接字上执行的写操作永远都不会阻塞 现假设一个进程希望通过三个管道中任意一个中读取数据并显示,伪代码如下 read(pipe_0,buf,sizeof(buf)); //sleep print buf; read(pipe_1,buf,sizeof(buf)); print buf; read(pipe_2,buf,sizeof(buf)); print buf; 由于管道是阻塞I/O,所以如果pipe_0没有数据流入,进程就是在第一个read()处进入sleep状态而即使pipe_1和pipe_2有数据流入也不会被读取。 如果我们使用下述代码重新设置管道的阻塞属性,显然,如果三个管道都没有数据流入,那么进程就无法获得请求的数据而继续执行,倘若这些数据很重要(所以我们才要用阻塞I/O),那结果就会十分的糟糕,改为轮询却又大量的占据CPU时间。 int fl = fcntl(pipe_fd, F_GETFL); fcntl(pipe_fd, F_SETFL, fl | O_NONBLOCK); 如何让进程同时监视三个管道,其中一个有数据就继续执行而不会sleep,如果全部没有数据流入再sleep,就是多路复用技术需要解决的问题。 非阻塞I/O 非阻塞I/O就是当一个进程试图访问一个I/O设备的时候,无论是否从中获取了请求的数据都会返回并继续执行接下来的任务。,但非常适合请求是否成功对接下来的任务影响不大的I/O请求。但如果访问一个非阻塞I/O,但这个请求如果失败对进程接下来的任务有致命影响,最粗暴的就是使用while(1){read()}轮询。显然,这种方式会占用大量的CPU时间。对于非阻塞IO,除了直接返回,一个更重要的应用就是利用IO多路复用机制同时监视多个非阻塞IO。 select机制 select是一种非常"古老"的同步I/O接口,但是提供了一种很好的I/O多路复用的思路 模型 fd_set //创建fd_set对象,将来从中增减需要监视的fd FD_ZERO() //清空fd_set对象 FD_SET() //将一个fd加入fd_set对象中 select() //监视fd_set对象中的文件描述符 pselect() //先设定信号屏蔽,再监视 FD_ISSET() //测试fd是否属于fd_set对象 FD_CLR() //从fd_set对象中删除fd Note: select的第一个参数nfds是指集合中的最大的文件描述符+1,因为select会无差别遍历整个文件描述符表直到找到目标,而文件描述符是从0开始的,所以一共是集合中的最大的文件描述符+1次。 上一条导致了这种机制的低效,如果需要监视的文件描述符是0和100那么每一次都会遍历101次 select()每次返回都会修改fd_set,如果要循环select(),需要先对初始的fd_set进行备 例子_I/O多路复用并发服务器 关于server本身的编程模型,参见tcp/ip协议服务器模型和udp/ip协议服务器模型这里仅是使用select实现伪并行的部分模型 #define BUFSIZE 100 #define MAXNFD 1024 int main() { /***********服务器的listenfd已经准本好了**************/ fd_set readfds; fd_set writefds; FD_ZERO(&readfds); FD_ZERO(&writefds); FD_SET(listenfd, &readfds); fd_set temprfds = readfds; fd_set tempwfds = writefds; int maxfd = listenfd; int nready; char buf[MAXNFD][BUFSIZE] = {0}; while(1){ temprfds = readfds; tempwfds = writefds; nready = select(maxfd+1, &temprfds, &tempwfds, NULL, NULL) if(FD_ISSET(listenfd, &temprfds)){ //如果监听到的是listenfd就进行accept int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //将新accept的scokfd加入监听集合,并保持maxfd为最大fd FD_SET(sockfd, &readfds); maxfd = maxfd>sockfd?maxfd:sockfd; //如果意见检查了nready个fd,就没有必要再等了,直接下一个循环 if(--nready==0) continue; } int fd = 0; //遍历文件描述符表,处理接收到的消息 for(;fd<=maxfd; fd++){ if(fd == listenfd) continue; if(FD_ISSET(fd, &temprfds)){ int ret = read(fd, buf[fd], sizeof buf[0]); if(0 == ret){ //客户端链接已经断开 close(fd); FD_CLR(fd, &readfds); if(maxfd==fd) --maxfd; continue; } //将fd加入监听可写的集合 FD_SET(fd, &writefds); } //找到了接收消息的socket的fd,接下来将其加入到监视写的fd_set中 //将在下一次while()循环开始监视 if(FD_ISSET(fd, &tempwfds)){ int ret = write(fd, buf[fd], sizeof buf[0]); printf("ret %d: %d\n", fd, ret); FD_CLR(fd, &writefds); } } } close(listenfd); } poll机制 poll是一种基于select的改良机制,其针对select的一些缺陷进行了重新设计,包括不需要备份fd_set等等,但是依然是遍历整个文件描述符表,效率较低 模型 struct pollfd fds //创建一个pollfd类型的数组 fds[0].fd //向fds[0]中放入需要监视的fd fds[0].events //向fds[0]中放入需要监视的fd的触发事件 POLLIN //I/O有输入 POLLPRI //有紧急数据需要读取 POLLOUT //I/O可写 POLLRDHUP //流式套接字连接断开或套接字处于半关闭状态 POLLERR //错误条件(仅针对输出) POLLHUP //挂起(仅针对输出) POLLNVAL //无效的请求:fd没有被打开(仅针对输出) 例子_I/O多路复用并发服务器 /* ... */ int main() { /* ... */ struct pollfd myfds[MAXNFD] = {0}; myfds[0].fd = listenfd; myfds[0].events = POLLIN; int maxnum = 1; int nready; //准备二维数组buf,每个fd使用buf的一行,数据干扰 char buf[MAXNFD][BUFSIZE] = {0}; while(1){ //poll直接返回event被触发的fd的个数 nready = poll(myfds, maxnum, -1) int i = 0; for(;i<maxnum; i++){ //poll通过将相应的二进制位置一来表示已经设置 //如果下面的条件成立,表示revent[i]里的POLLIN位已经是1了 if(myfds[i].revents & POLLIN){ if(myfds[i].fd == listenfd){ int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //将新accept的scokfd加入监听集合 myfds[maxnum].fd = sockfd; myfds[maxnum].events = POLLIN; maxnum++; //如果意见检查了nready个fd,就直接下一个循环 if(--nready==0) continue; } else{ int ret = read(myfds[i].fd, buf[myfds[i].fd], sizeof buf[0]); if(0 == ret){ //如果连接断开了 close(myfds[i].fd); //初始化将文件描述符表所有的文件描述符标记为-1 //close的文件描述符也标记为-1 //打开新的描述符时从表中搜索第一个-1 //open()就是这样实现始终使用最小的fd //这里为了演示并没有使用这种机制 myfds[i].fd = -1; continue; } myfds[i].events = POLLOUT; } } else if(myfds[i].revents & POLLOUT){ int ret = write(myfds[i].fd, buf[myfds[i].fd], sizeof buf[0]); myfds[i].events = POLLIN; } } } close(listenfd); } epoll epoll在poll基础上实现的更为健壮的接口,它每次只会遍历我们关心的文件描述符,也是现在主流的web服务器使用的多路复用技术,epoll一大特色就是支持EPOLLET(边沿触发)和EPOLLLT (水平触发),前者表示如果读取之后缓冲区还有数据,那么只要读取结束,剩余的数据也会丢弃,而后者表示里面的数据不会丢弃,下次读的时候还在,默认是EPOLLLT 模型 epoll_create() //创建epoll对象 struct epoll_event //准备事件结构体和事件结构体数组 event.events event.data.fd ... epoll_ctl() //配置epoll对象 epoll_wait() //监控epoll对象中的fd及其相应的event 例子_I/O多路复用并发服务器 /* ... */ int main() { /* ... */ /* 创建epoll对象 */ int epoll_fd = epoll_create(1024); //准备一个事件结构体 struct epoll_event event = {0}; event.events = EPOLLIN; event.data.fd = listenfd; //data是一个共用体,除了fd还可以返回其他数据 //ctl是监控listenfd是否有event被触发 //如果发生了就把event通过wait带出。 //所以,如果event里不标明fd,我们将来获取就不知道哪个fd epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listenfd, &event); struct epoll_event revents[MAXNFD] = {0}; int nready; char buf[MAXNFD][BUFSIZE] = {0}; while(1){ //wait返回等待的event发生的数目 //并把相应的event放到event类型的数组中 nready = epoll_wait(epoll_fd, revents, MAXNFD, -1) int i = 0; for(;i<nready; i++){ //wait通过在events中设置相应的位来表示相应事件的发生 //如果输入可用,那么下面的这个结果应该为真 if(revents[i].events & EPOLLIN){ //如果是listenfd有数据输入 if(revents[i].data.fd == listenfd){ int sockfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); struct epoll_event event = {0}; event.events = EPOLLIN; event.data.fd = sockfd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event); } else{ int ret = read(revents[i].data.fd, buf[revents[i].data.fd], sizeof buf[0]); if(0 == ret){ close(revents[i].data.fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, revents[i].data.fd, &revents[i]); } revents[i].events = EPOLLOUT; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, revents[i].data.fd, &revents[i]); } } else if(revents[i].events & EPOLLOUT){ int ret = write(revents[i].data.fd, buf[revents[i].data.fd], sizeof buf[0]); revents[i].events = EPOLLIN; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, revents[i].data.fd, &revents[i]); } } } close(listenfd); }
背景: MySQL5.7之后多了一个备份工具:mysqlpump。它是mysqldump的一个衍生,mysqldump就不多说明了,现在看看mysqlpump到底有了哪些提升,可以查看官方文档,这里针对如何使用做下说明。 mysqlpump和mysqldump一样,属于逻辑备份,备份以SQL形式的文本保存。逻辑备份相对物理备份的好处是不关心undo log的大小,直接备份数据即可。它最主要的特点是: 并行备份数据库和数据库中的对象的,加快备份过程。 更好的控制数据库和数据库对象(表,存储过程,用户帐户)的备份。 备份用户账号作为帐户管理语句(CREATE USER,GRANT),而不是直接插入到MySQL的系统数据库。 备份出来直接生成压缩后的备份文件。 备份进度指示(估计值)。 重新加载(还原)备份文件,先建表后插入数据最后建立索引,减少了索引维护开销,加快了还原速度。 备份可以排除或则指定数据库。 参数:绝大部分参数和mysqldump一致,顺便复习一下。对于mysqlpump参数会用背景色 标记出来。 1:--add-drop-database:在建立库之前先执行删库操作。 DROP DATABASE IF EXISTS `...`; 2:--add-drop-table:在建表之前先执行删表操作。 DROP TABLE IF EXISTS `...`.`...`; 3:--add-drop-user:在CREATE USER语句之前增加DROP USER,注意:这个参数需要和--users一起使用,否者不生效。 DROP USER 'backup'@'192.168.123.%'; 4:--add-locks:备份表时,使用LOCK TABLES和UNLOCK TABLES。注意:这个参数不支持并行备份,需要关闭并行备份功能:--default-parallelism=0 LOCK TABLES `...`.`...` WRITE; ... UNLOCK TABLES; 5:--all-databases:备份所有库,-A。 6:--bind-address:指定通过哪个网络接口来连接Mysql服务器(一台服务器可能有多个IP),防止同一个网卡出去影响业务。 7:--complete-insert:dump出包含所有列的完整insert语句。 8:--compress: 压缩客户端和服务器传输的所有的数据,-C。 9:--compress-output:默认不压缩输出,目前可以使用的压缩算法有LZ4和ZLIB。 shell> mysqlpump --compress-output=LZ4 > dump.lz4 shell> lz4_decompress dump.lz4 dump.txt shell> mysqlpump --compress-output=ZLIB > dump.zlib shell> zlib_decompress dump.zlib dump.txt 10:--databases:手动指定要备份的库,支持多个数据库,用空格分隔,-B。 11:--default-character-set:指定备份的字符集。 12:--default-parallelism:指定并行线程数,默认是2,如果设置成0,表示不使用并行备份。注意:每个线程的备份步骤是:先create table但不建立二级索引(主键会在create table时候建立),再写入数据,最后建立二级索引。 13:--defer-table-indexes:延迟创建索引,直到所有数据都加载完之后,再创建索引,默认开启。若关闭则会和mysqldump一样:先创建一个表和所有索引,再导入数据,因为在加载还原数据的时候要维护二级索引的开销,导致效率比较低。关闭使用参数:--skip--defer-table-indexes。 14:--events:备份数据库的事件,默认开启,关闭使用--skip-events参数。 15:--exclude-databases:备份排除该参数指定的数据库,多个用逗号分隔。类似的还有--exclude-events、--exclude-routines、--exclude-tables、--exclude-triggers、--exclude-users。 mysqlpump --exclude-databases=mysql,sys #备份过滤mysql和sys数据库 mysqlpump --exclude-tables=rr,tt #备份过滤所有数据库中rr、tt表 mysqlpump -B test --exclude-tables=tmp_ifulltext,tt #备份过滤test库中的rr、tt表 ... 注意:要是只备份数据库的账号,需要添加参数--users,并且需要过滤掉所有的数据库,如: mysqlpump --users --exclude-databases=sys,mysql,db1,db2 --exclude-users=dba,backup #备份除dba和backup的所有账号。 16:--include-databases:指定备份数据库,多个用逗号分隔,类似的还有--include-events、--include-routines、--include-tables、--include-triggers、--include-users,大致方法使用同15。 17:--insert-ignore:备份用insert ignore语句代替insert语句。 18:--log-error-file:备份出现的warnings和erros信息输出到一个指定的文件。 19:--max-allowed-packet:备份时用于client/server直接通信的最大buffer包的大小。 20:--net-buffer-length:备份时用于client/server通信的初始buffer大小,当创建多行插入语句的时候,mysqlpump 创建行到N个字节长。 21:--no-create-db:备份不写CREATE DATABASE语句。要是备份多个库,需要使用参数-B,而使用-B的时候会出现create database语句,该参数可以屏蔽create database 语句。 22:--no-create-info:备份不写建表语句,即不备份表结构,只备份数据,-t。 23:--hex-blob: 备份binary字段的时候使用十六进制计数法,受影响的字段类型有BINARY、VARBINARY、BLOB、BIT。 24:--host :备份指定的数据库地址,-h。 25:--parallel-schemas=[N:]db_list:指定并行备份的库,多个库用逗号分隔,如果指定了N,将使用N个线程的地队列,如果N不指定,将由 --default-parallelism才确认N的值,可以设置多个--parallel-schemas。 mysqlpump --parallel-schemas=4:vs,aa --parallel-schemas=3:pt #4个线程备份vs和aa,3个线程备份pt。通过show processlist 可以看到有7个线程。 mysqlpump --parallel-schemas=vs,abc --parallel-schemas=pt #默认2个线程,即2个线程备份vs和abc,2个线程备份pt ####当然要是硬盘IO不允许的话,可以少开几个线程和数据库进行并行备份 26:--password:备份需要的密码。 27:--port :备份数据库的端口。 28:--protocol={TCP|SOCKET|PIPE|MEMORY}:指定连接服务器的协议。 29:--replace:备份出来replace into语句。 30:--routines:备份出来包含存储过程和函数,默认开启,需要对 mysql.proc表有查看权限。生成的文件中会包含CREATE PROCEDURE 和 CREATE FUNCTION语句以用于恢复,关闭则需要用--skip-routines参数。 31:--triggers:备份出来包含触发器,默认开启,使用--skip-triggers来关闭。 31:--set-charset:备份文件里写SET NAMES default_character_set 到输出,此参默认开启。 -- skip-set-charset禁用此参数,不会在备份文件里面写出set names... 32:--single-transaction:该参数在事务隔离级别设置成Repeatable Read,并在dump之前发送start transaction 语句给服务端。这在使用innodb时很有用,因为在发出start transaction时,保证了在不阻塞任何应用下的一致性状态。对myisam和memory等非事务表,还是会改变状态的,当使用此参的时候要确保没有其他连接在使用ALTER TABLE、CREATE TABLE、DROP TABLE、RENAME TABLE、TRUNCATE TABLE等语句,否则会出现不正确的内容或则失败。--add-locks和此参互斥,在mysql5.7.11之前,--default-parallelism大于1的时候和此参也互斥,必须使用--default-parallelism=0。5.7.11之后解决了--single-transaction和--default-parallelism的互斥问题。 33:--skip-definer:忽略那些创建视图和存储过程用到的 DEFINER 和 SQL SECURITY 语句,恢复的时候,会使用默认值,否则会在还原的时候看到没有DEFINER定义时的账号而报错。 34:--skip-dump-rows:只备份表结构,不备份数据,-d。注意:mysqldump支持--no-data,mysqlpump不支持--no-data 35:--socket:对于连接到localhost,Unix使用套接字文件,在Windows上是命名管道的名称使用,-S。 36:--ssl:--ssl参数将要被去除,用--ssl-mode取代。关于ssl相关的备份,请看官方文档。 37:--tz-utc:备份时会在备份文件的最前几行添加SET TIME_ZONE='+00:00'。注意:如果还原的服务器不在同一个时区并且还原表中的列有timestamp字段,会导致还原出来的结果不一致。默认开启该参数,用 --skip-tz-utc来关闭参数。 38:--user:备份时候的用户名,-u。 39:--users:备份数据库用户,备份的形式是CREATE USER...,GRANT...,只备份数据库账号可以通过如下命令: mysqlpump --exclude-databases=% --users #过滤掉所有数据库 40:--watch-progress:定期显示进度的完成,包括总数表、行和其他对象。该参数默认开启,用--skip-watch-progress来关闭。 使用说明: mysqlpump的架构如下图所示: mysqlpump支持基于库和表的并行导出,mysqlpump的并行导出功能的架构为:队列+线程,允许有多个队列(--parallel-schemas?),每个队列下有多个线程(N?),而一个队列可以绑定1个或者多个数据库(逗号分隔)。mysqlpump的备份是基于表并行的,对于每张表的导出只能是单个线程的,这里会有个限制是如果某个数据库有一张表非常大,可能大部分的时间都是消耗在这个表的备份上面,并行备份的效果可能就不明显。这里可以利用mydumper其是以chunk的方式批量导出,即mydumper支持一张表多个线程以chunk的方式批量导出。但是相对于mysqldump还是有了很大的提升。这里大致测试下mysqlpump和mysqldump的备份效率。 #mysqlpump压缩备份vs数据库 三个并发线程备份,消耗时间:222s mysqlpump -uzjy -p -h192.168.123.70 --single-transaction --default-character-set=utf8 --compress-output=LZ4 --default-parallelism=3 -B vs > /home/zhoujy/vs_db.sql.lz4 #mysqldump备份压缩vs数据库 单个线程备份,消耗时间:900s,gzip的压缩率比LZ4的高 mysqldump -uzjy -p -h192.168.123.70 --default-character-set=utf8 -P3306 --skip-opt --add-drop-table --create-options --quick --extended-insert --single-transaction -B vs | gzip > /home/zhoujy/vs.sql.gz #mydumper备份vs数据库 三个并发线程备份,消耗时间:300s,gzip的压缩率比LZ4的高 mydumper -u zjy -p -h 192.168.123.70 -P 3306 -t 3 -c -l 3600 -s 10000000 -B vs -o /home/zhoujy/vs/ #mydumper备份vs数据库,五个并发线程备份,并且开启对一张表多个线程以chunk的方式批量导出,-r。消耗时间:180s mydumper -u zjy -p -h 192.168.123.70 -P 3306 -t 5 -c -r 300000 -l 3600 -s 10000000 -B vs -o /home/zhoujy/vs/ 从上面看出,mysqlpump的备份效率是最快的,mydumper次之,mysqldump最差。所以在IO允许的情况下,能用多线程就别用单线程备份。并且mysqlpump还支持多数据库的并行备份,而mydumper要么备份一个库,要么就备份所有库。姜大神的Oracle官方并行逻辑备份工具mysqlpump这篇文章的测试结果也说明了mysqlpump比mysqldump的测试好。由于实际情况不同,测试给出的速度提升只是参考。到底开启多少个并行备份的线程,这个看磁盘IO的承受能力,若该服务器只进行备份任务,可以最大限制的来利用磁盘。 总结: mysqldump和mysqlpump的使用方法绝大部分一致,mysqlpump新的参数文章上已经标明,到底用那种工具备份数据库这个要在具体的环境下才能做出选择,有些时候可能用物理备份更好(xtrabackup),总之根据需要进行测试,最后再决定使用哪种备份工具进行备份。 参考文档: mysqlpump官方文档 Oracle官方并行逻辑备份工具mysqlpump
概念 redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)和zset(有序集合)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。 开始 这里我使用的是redis-2.2.2.tar.gz版本的redis(下载地址)首先要对它进行安装,这里我选择使用cygwin工具进行安装,加入我把该压缩包放在F盘下,使用cygwin工具进行shell命令: 1 2 3 $ tar xzf redis-2.2.2.tar.gz $ cd redis-2.2.2 $ make 这里的make实际上操作的是Makefile文件,Makefile按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个shell脚本一样,其中也可以执行操作系统的命令。 浏览下Redis根目录中的Makefile文件: 通过make命令可以执行“cd src && make all”。而此时的make all实际上已经开始执行src目录中的Makefile文件。这个文件比较复杂,大致就是将一系列的c文件以及h文件链接起来,通过cc/gcc编译器将文件生成目标文件o,接着将相应的o目标文件在通过编译器生成exe文件,当你编译完毕后,在src的目录上将产生5个exe文件: redis-benchmark.exe:用于做性能测试; redis-check-aof.exe:更新日志检查; redis-check-dump.exe:用于本地数据库检查; redis-cli.exe:客户端程序; redis-server.exe:服务端程序; 具体用法这里不多说了,可以参考(http://www.cnblogs.com/daizhj/articles/1956681.html) 现在来看下src包含的文件(我按照首字母顺序来讲): adlist.h/adlist.c:用于对list的定义,它是个双向链表结构,从头文件可以找到: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // list节点 typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; // list迭代器 typedef struct listIter { listNode *next; int direction; } listIter; // list数据结构 typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned int len; } list; 在ListNode节点下包含prev指针和next指针,说明它通过指针将节点进行双向链接。 并且从adlist.h的头文件可以找到非常丰富的方法声明,包括list创建,list释放,list头部/尾部添加节点等等,具体在后面的系列会做出介绍。 ae.h/ae.c:用于Redis的事件处理,包括句柄事件和超时事件。 在ae.c中的头部可以发现: 1 2 3 4 5 6 7 8 9 #ifdef HAVE_EPOLL #include "ae_epoll.c" #else #ifdef HAVE_KQUEUE #include "ae_kqueue.c" #else #include "ae_select.c" #endif #endif 在网络相关操作中,定义了一组公共操作接口:aeApiCreate,aeApiFree,aeApiAddEvent,aeApiDelEvent,aeApiPoll,aeApiName方法。在ae_epoll.c、ae_kqueue.c和ae_select.c中,分别实现了基于epoll/kqueue和select系统调用的接口。系统调用的选择顺序依次为epoll,kqueue,select。 anet.h/anet.c:这两个文件非常重要,作为Server/Client通信的基础封装,包括anetTcpServer,anetTcpConnect,anetTcpAccept,anetRead,anetWrite等等方法。 aof.c:aof,全称为append only file,作用就是记录每次的写操作,在遇到断电等问题时可以用它来恢复数据库状态。但是他不是bin的,而是text的。一行一行,写得很规范.如果你是一台redis,那你也能人肉通过它恢复数据。 config.h/config.c:用于将配置文件redis.conf文件中的配置读取出来的属性通过程序放到server对象中。在main函数(server服务主入口点处)可以发现里面调用loadServerConfig(char *filename)方法,这个方法就是使用config.c里面的方法实现。具体会在后面的系列中详细介绍。 db.c:对于Redis内存数据库的相关操作。 debug.c:用于调试使用。 dict.h/dict.c:也是很重要的两个文件,主要对于内存中的hash进行管理: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct dictEntry { void *key; void *val; struct dictEntry *next; } dictEntry; typedef struct dictType { unsigned int (*hashFunction)(const void *key); void *(*keyDup)(void *privdata, const void *key); void *(*valDup)(void *privdata, const void *obj); int (*keyCompare)(void *privdata, const void *key1, const void *key2); void (*keyDestructor)(void *privdata, void *key); void (*valDestructor)(void *privdata, void *obj); } dictType; typedef struct dict { dictType *type; void *privdata; dictht ht[2]; int rehashidx; /* rehashing not in progress if rehashidx == -1 */ int iterators; /* number of iterators currently running */ } dict; 这里dictEntry作为一个dict字段结构,里面包括key以及value,已经指向下一个dictEntry的指针。dictType作为一些dict的操作结构。dict作为一个hash结构。后面的文章会具体介绍。 fmacros.h:用于Mac下的兼容性处理。 help.h:辅助于命令的提示信息,作用于redis-cli.exe可执行文件中。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct commandHelp { char *name; char *params; char *summary; int group; char *since; } commandHelp[] = { { "APPEND", "key value", "Append a value to a key", 1, "1.3.3" }, { "AUTH", "password", "Authenticate to the server", 8, "0.08" }, { "BGREWRITEAOF", "-", "Asynchronously rewrite the append-only file", 9, .... }; intset.h/intset.c:整数范围内的使用set,并包含相关set操作。 lzf.h/lzf_c.c/lzf_d.c/lzfP.h:对于本地数据库的保存,使用的是LZF压缩算法,很神奇,算法只有200-300行的代码。 multi.c:用于事务处理操作。请看这样的一个例子: 通过执行exec,可以提交整个事务过程,如果你想撤销整个事务过程,你可以使用discard命令: 可以发现get age已经取不到值了,说明discard命令让事务失效。 networking.c:网络协议传输方法定义相关的都放在这个文件里面了。包括让Client连接上Server,让Slave挂接到Master,已经Server/Client之间的信息交互的实现等等。 object.c:用于创建和释放redisObject对象,redisObject结构为: 1 2 3 4 5 6 7 8 9 10 11 12 typedef struct redisObject { unsigned type:4; unsigned storage:2; /* REDIS_VM_MEMORY or REDIS_VM_SWAPPING */ unsigned encoding:4; unsigned lru:22; /* lru time (relative to server.lruclock) */ int refcount; void *ptr; /* VM fields are only allocated if VM is active, otherwise the * object allocation function will just allocate * sizeof(redisObjct) minus sizeof(redisObjectVM), so using * Redis without VM active will not have any overhead. */ } robj; pqsort.h/pqsort.c/sort.c:关于排序算法,sort.c具体作为Redis场景下的排序实现。 pubsub.c:用于订阅模式的实现,有点类似于Client广播发送的方式。 rdb.c:对于Redis本地数据库的相关操作,默认文件是dump.rdb(通过配置文件获得),包括的操作包括保存,移除,查询等等。 redis-benchmark.c:用于redis性能测试的实现。请看main方法以下设置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 config.debug = 0; config.numclients = 50; config.requests = 10000; config.liveclients = 0; config.el = aeCreateEventLoop(); aeCreateTimeEvent(config.el,1,showThroughput,NULL,NULL); config.keepalive = 1; config.donerequests = 0; config.datasize = 3; config.randomkeys = 0; config.randomkeys_keyspacelen = 0; config.quiet = 0; config.loop = 0; config.idlemode = 0; config.latency = NULL; config.clients = listCreate(); config.hostip = "127.0.0.1"; config.hostport = 6379; config.hostsocket = NULL; parseOptions(argc,argv); config.latency = zmalloc(sizeof(long long)*config.requests); 默认性能测试中的客户端数量为50个,并行发送的请求有10000条,你也可以通过redis-benchmark命令行参数进行设置。 redis-check-aof.c:用于更新日志检查的实现。 redis-check-dump.c:用于本地数据库检查的实现。 redis-cli.c:客户端程序的实现。具体会在后面的文章详细介绍。 redis.h/redis.c:服务端程序的实现。具体会在后面的文章详细介绍。 release.h/release.c:用于发布使用。 replication.c:用于主从数据库的复制操作的实现。 sds.h/sds.c:用于对字符串的定义,从头文件可以找到: 1 2 3 4 5 6 //字符串 struct sdshdr { int len; int free; char buf[]; }; 还可以看到对于字符串的相关操作,包括复制,连接,清零等等。 sha1.h/sha1.c:有关于sha算法的实现。 solarisfixes.h:Solaris系统的兼容性实现。 syncio.c:用于同步Socket和文件I/O操作。 t_hash.c/t_list.c/t_set.c/t_string.c/t_zset.c:hash,list,set,string,zset在Server/Client中的应答操作。主要通过redisObject进行类型转换。 testhelp.h:一个C风格的小型测试框架。 util.c:关于通用工具的方法实现。 version.h:Redis版本号定义。 vm.c:关于虚拟内存的管理实现。 zipmap.h/zipmap.c:zipmap是一个类似于hash的存储对象。在新建一个hash对象时开始是用zipmap(又称为small hash)来存储的。这个zipmap其实并不是hash table但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销,如果field或者value的大小超出一定限制后,redis会在内部自动将zipmap替换成正常的hash实现。 ziplist.h/ziplist.c:ziplist是一个类似于list的存储对象。它的原理类似于zipmap。 zmalloc.h/zmalloc.c:关于Redis的内存分配的封装实现。 下一篇我会介绍下redis-server以及redis-cli的源码实现。
之前写过一篇nginx多tomcat负载均衡,主要记录了使用nginx对多个tomcat 进行负载均衡,其实进行负载均衡之前还有一个问题没有解决,那就是集群间的session共享,不然用户在登录网站之后session保存在tomcat A,但是下次访问的时候nginx分发到了tomcat B,这个时候tomcat B没有刚刚用户登录的session,所以用户就失去了(本次)登录状态,下次访问的时候nginx可能又分发到了tomcat A(其实通过配置可以给各个服务器分配权重,nginx根据权重来转发到对应的服务器),用户本次又是登录的状态了,这样飘忽不定肯定是不行的,所以在进行集群负载均衡之前需要解决session共享的问题。 目录 概述:简述本次记录的主要内容 shiro的session:关于shiro的session管理 实现共享 总结 概述 因为项目中用到了shiro的权限控制,而且使用的是shiro的session,所以我就基于shiro的session管理基础上对session进行多tomcat共享,共享的思路也很简单,就是将session保存到数据库,每个服务器在收到客户端请求的时候都从数据库中取,这样就统一了多个服务器之间的session来源,实现了共享。只不过这里我使用的数据库是redis。 shiro的session 之前在另外一篇博客(shiro实现APP、web统一登录认证和权限管理)里面也提到了shiro的session问题,其实shiro的session只不过是基于认证的需要对tomcat的session进行了封装,所以只要实现对shiro的session进行持久化就可以了,关于shiro的session管理,开涛老师的这一篇博客讲得很清楚了(http://jinnianshilongnian.iteye.com/blog/2028675),可以参考这一篇博客来了解shiro对session的管理。 实现共享 在明白了shiro的session管理之后,我们就可以在此基础上进行session的共享了,其实只需要继承EnterpriseCacheSessionDAO(其实继承CachingSessionDAO就可以了,但是这里考虑到集群中每次都访问数据库导致开销过大,这里在本地使用ehcache进行缓存,每次读取session的时候都先尝试读取本地ehcache缓存,没有的话再去远程redis数据库中读取),然后覆盖原来的增删改查操作,这样多个服务器就共享了session,具体实现如下: import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.SimpleSession; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; public class SessionRedisDao extends EnterpriseCacheSessionDAO { // 创建session,保存到数据库 @Override protected Serializable doCreate(Session session) { Serializable sessionId = super.doCreate(session); RedisDb.setObject(sessionId.toString().getBytes(), sessionToByte(session)); return sessionId; } // 获取session @Override protected Session doReadSession(Serializable sessionId) { // 先从缓存中获取session,如果没有再去数据库中获取 Session session = super.doReadSession(sessionId); if(session == null){ byte[] bytes = RedisDb.getObject(sessionId.toString().getBytes()); if(bytes != null && bytes.length > 0){ session = byteToSession(bytes); } } return session; } // 更新session的最后一次访问时间 @Override protected void doUpdate(Session session) { super.doUpdate(session); RedisDb.setObject(session.getId().toString().getBytes(), sessionToByte(session)); } // 删除session @Override protected void doDelete(Session session) { super.doDelete(session); RedisDb.delString(session.getId() + ""); } // 把session对象转化为byte保存到redis中 public byte[] sessionToByte(Session session){ ByteArrayOutputStream bo = new ByteArrayOutputStream(); byte[] bytes = null; try { ObjectOutputStream oo = new ObjectOutputStream(bo); oo.writeObject(session); bytes = bo.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return bytes; } // 把byte还原为session public Session byteToSession(byte[] bytes){ ByteArrayInputStream bi = new ByteArrayInputStream(bytes); ObjectInputStream in; SimpleSession session = null; try { in = new ObjectInputStream(bi); session = (SimpleSession) in.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return session; } } 上面的主要逻辑是实现session的管理,下面是和redis数据库交互 import java.util.Arrays; import java.util.Date; import java.util.Set; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisDb { private static JedisPool jedisPool; // session 在redis过期时间是30分钟30*60 private static int expireTime = 1800; // 计数器的过期时间默认2天 private static int countExpireTime = 2*24*3600; private static String password = "123456"; private static String redisIp = "10.10.31.149"; private static int redisPort = 6379; private static int maxActive = 200; private static int maxIdle = 200; private static long maxWait = 5000; private static Logger logger = Logger.getLogger(RedisDb.class); static { initPool(); } // 初始化连接池 public static void initPool(){ JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(maxActive); config.setMaxIdle(maxIdle); config.setMaxWaitMillis(maxWait); config.setTestOnBorrow(false); jedisPool = new JedisPool(config, redisIp, redisPort, 10000, password); } // 从连接池获取redis连接 public static Jedis getJedis(){ Jedis jedis = null; try{ jedis = jedisPool.getResource(); // jedis.auth(password); } catch(Exception e){ ExceptionCapture.logError(e); } return jedis; } // 回收redis连接 public static void recycleJedis(Jedis jedis){ if(jedis != null){ try{ jedis.close(); } catch(Exception e){ ExceptionCapture.logError(e); } } } // 保存字符串数据 public static void setString(String key, String value){ Jedis jedis = getJedis(); if(jedis != null){ try{ jedis.set(key, value); } catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 获取字符串类型的数据 public static String getString(String key){ Jedis jedis = getJedis(); String result = ""; if(jedis != null){ try{ result = jedis.get(key); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } return result; } // 删除字符串数据 public static void delString(String key){ Jedis jedis = getJedis(); if(jedis != null){ try{ jedis.del(key); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 保存byte类型数据 public static void setObject(byte[] key, byte[] value){ Jedis jedis = getJedis(); String result = ""; if(jedis != null){ try{ if(!jedis.exists(key)){ jedis.set(key, value); } // redis中session过期时间 jedis.expire(key, expireTime); } catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 获取byte类型数据 public static byte[] getObject(byte[] key){ Jedis jedis = getJedis(); byte[] bytes = null; if(jedis != null){ try{ bytes = jedis.get(key);; }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } return bytes; } // 更新byte类型的数据,主要更新过期时间 public static void updateObject(byte[] key){ Jedis jedis = getJedis(); if(jedis != null){ try{ // redis中session过期时间 jedis.expire(key, expireTime); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // key对应的整数value加1 public static void inc(String key){ Jedis jedis = getJedis(); if(jedis != null){ try{ if(!jedis.exists(key)){ jedis.set(key, "1"); jedis.expire(key, countExpireTime); } else { // 加1 jedis.incr(key); } }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } } // 获取所有keys public static Set<String> getAllKeys(String pattern){ Jedis jedis = getJedis(); if(jedis != null){ try{ return jedis.keys(pattern); }catch(Exception e){ ExceptionCapture.logError(e); } finally{ recycleJedis(jedis); } } return null; } } 总结 这里只是实现了简单的session共享,但是对session的管理还不够全面,比如说session的验证。其实通过tomcat容器本身就可以实现session共享,后面再详细了解下tomcat对于session的管理。
最近发布在windows server2012 IIS8.0上的一个WebAPI项目,才几十个人在线,CPU就会出现过百情况,并且CPU一旦过百应用程序池就自动暂停掉,看到这个问题我感觉应该是程序哪个地方出了问题, 8盒16G 应该配置还是可以的。打算使用windbg找到这个问题。 为了快速定位问题我就直接在生产环境安装了windbg,为了采集dump文件,我选择Procdump。Procdump无需安装,下载下来直接放到一个目录下即可。以下是解决问题的过程+截图: 步骤一: 安装windbg,注意32和64,要安装相应的版本,直接点击下一步即可。 步骤二: Copy Procdump 文件到服务器上的一个目录下,目录没有限制 如图:C:\software\Procdump,这里的dbghelp.dll是从windbg安装目录下copy过来的,是我为了解决下面这个坑:调试High CPU问题的时候经常用到的一个命令是!runaway,但是有些时候!runway在ProcDump抓取的dump中提取不出来。解决的方法是将Debug Tools for Windows (windbg)安装目录下的dbghelp.dll拷贝到procdump目录下,然后再运行命令抓取dump。 0:000> !runaway ERROR: !runaway: extension exception 0x80004002. "Unable to get thread times - dumps may not have time information" 步骤三: 在doc窗口下执行procdump命令,cd /d c:\Software\Procdump 步骤四: 执行procdum命令,执行 procdump -c 50 -s 4 -ma -n 3 w3wp 命令含义为:当w3wp.exe cpu超过50%,并且持续4秒,抓取3个dump文件存储起来,存储位置默认为procdump文件所在的目录。 如图: 出现如图结果证明已经进入监控状态。接下来就是等着CPU超过50%了。 没过一会就看到效果了 dump文件已经抓取到,我们来看下dump文件存储位置: 那么接下来就是开始分析了。 步骤五: 启动已经安装好的Windbg,开始分析采集的dump文件 步骤六: 为了不影响正在运行的项目,我将发布的项目文件单独从copy了一份出来,如图所示:我是web api项目 步骤七: 设置系列目录: Windbg->file->Symbol File Path Windbg->file->Source File Path 步骤八: 加载dump文件 Windbg->file->open Crash Dump 先选择第一个dump文件。 步骤九: 载入sos.dll 执行.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.DLL 我是4.0 的 注意版本 64位 步骤十: !threadpool 查看当前CPU状况 线程数等等 步骤十一: 执行 !runaway 命令 查看那几个线程使用的高 步骤十二: ~线程IDs 跳转到那个线程 步骤十三: !clrstack 看看这个线程再干嘛 执行那些方法 步骤十四: 将图中红框列出来的方法去项目中查找下,发现了问题: 重载方法的时候参数传递不正确,出现了死循环,至此问题得到了解决。 原文出处:http://www.mamingbo.com/page/yunwei/109/index.htm
抢购钻石不稀奇,稀奇的是有钱赚不到,事情发生在2015年5月20日,大好的日子自然少不了商家的参与。即可为您还原现场,解决思路献给各位,请欣赏Show Time,everybody~ 1、优化起因及工作准备 2014年5月20日下午三点四十接到对方不愿意透漏姓名的“王大锤”领导的电话,对方火急火燎的仅提供了网站访问慢一条信息,当时博主那个心里一万只XX奔腾而过,俗话说的好,酒肉穿肠过,拿人钱财必替人消灾。 对博主来说网站访问慢,首先不能乱了阵脚,先想到的就是看web、先看静态,如果静态ok就看动态,如果还不ok就看存储,再不行就看访问DB时长是否正常。此时原因就可以定位了。不会再有其他原因了。如果你太菜,那你可以把我的思路背过,相信对你来说是一个很好的帮助,此时一边与对方沟通更可能多的获得信息,可是对方一点都不懂,只好无能为力,与对方协商相关责任制后立即登录服务器(本人兼职XX钻世界集团技术顾问一职)。 凭借个人经验查看web负载并不高,静态访问速度正常,由于线上活动正在进行,晚一分钟对商家即是损失,此时无法进行许多系统的排查,直接则判断是否是后端DB的问题?随登录DB查看负载。发现DB负载不正常,就没有进行其他的判断(什么IO看一下啊,内存看一下啊,网卡看一下啊,再看公司都倒闭了。),紧急恢复问题就是最大化的恢复问题,找到问题所在即刻解决问题。此时判断数据库有慢查询。 1 ================2015年5月20日 13:38:08日负载如下:================ 2 [lcp@ZCdb01 ~]$ uptime 3 13:50:36 up 122 days, 21:51, 1 user, load average: 6.44, 5.76, 5.38 4 5 [lcp@ZCdb01 ~]$ uptime 6 13:51:38 up 122 days, 21:22, 1 user, load average: 8.01, 6.30, 5.58 2、判断问题所在 随登录数据库show full processlist;此工具运维人员必备,干了几年的运维别说你不会。不会的话看了我的博客也应该会了。 连抓了两遍之后发现,这一堆东西不动啊,前面排着的update被锁定,想写还写不进去。select过多,读也读不出来。 1 mysql> show processlist; 2 +----+-------------+-----------+------+---------+------+-----------------------------------------------------------------------------+------------------+ 3、定位待优化语句 再返回来看后面的查询语句是通过三个条件进行查询的。于是定位了待优化的语句也就是下方的select出现次数最多的语句 ↑↑↑查询语句如上↑↑↑ 随后抓出一条命令explain,多次确认后加SQL_NO_CACHE不让其走缓存再反复确认,最终判断次语句没有建立索引或走索引,共查阅7万3千多条数据耗时惊人。 1 mysql> select SQL_NO_CACHE id from **_**_detail where ader='**_**-jazz_flash' and dateline='**_**' and pos='**_**'; 此时看到可能走的索引和索引都是不存在的。独立奔跑在七万多条语句中 1 possible_keys:NULL 2 3 key:NULL 4 5 rows:71328 #接近全盘扫描 我记得这台机器是戴尔服务器2850很老的一台服务器,但这很明显不是硬件问题,随问对方的主管,有没有人对这台机器进行优化,一边电话询问一边进行查看,去证实自己的想法,使用show查看表结构show create table **_**_detai\G,果不其然,除了主键索引,一个索引都没有建立(为这台年老失修的服务器感到骄傲,它竟然扛了那么久授小弟一拜)。 4、解决方案 扯淡归扯淡我们继续,此时已耗时3分钟,建立索引的规则相信大家也都清楚,此处不过多解释,一会看总结。得到以上结论后,查看哪一字段列的唯一值数量较多。使用select count(distinct XX)from **_**_detai;以上三个语句都使用次等命令查看,最后发现三列的数值为766/531/154都不高,原因是有一列是日期,它的唯一值是最少的,第二列看不懂。。 再使用select count(*) from **_**_detail;命令查看一下总数量达到了七万多条的数量。 根据以上的情况,而且查询语句里面也很特殊都是等号。这种情况下建立索引就容易走索引。这种情况下考虑走联合索引。根据以上信息及咨询研发经理其他语句的情况下,创建如下索引: 1 mysql> create index d_a_p on **_**_detail(dateline,daer(20),azz(10),pos(20)); 语句的查询顺序是询问的研发经理,因为联合索引有前缀生效的特性,所以此时确定了索引之后并没有直接创建,而是与研发经理协商,此时需要杀掉几个读的请求。在前面选几个。show proacesslist;update根据业务需求去考虑。谨慎使用至于杀掉的方法..kill+id相信没几个不会的吧。 索引建立完成再使用explain查看索引是否生效,然后同样还是使用select+SQL_NO_CACHE参数不走缓存查询语句。发现此时仅扫描了12条语句,查询时间更是少之又少。 再次使用show proacesslist;查看mysql线程,几乎看不到了。说明效果很明显。 5、解决效果 优化之后的负载,已经从之前的6.x、8.x慢慢下降为2.x,1.72,五分钟后降到了0.07、0.21的正常值 1 [lcp@ZCdb01 ~]$ uptime 2 13:59:09 up 120 days, 21:29, 2 users, load average: 2.40, 4.62, 5.09 3 [lcp@ZCdb01 ~]$ uptime 4 13:59:29 up 120 days, 21:29, 2 users, load average: 1.72, 4.32, 4.98 5 [lcp@ZCdb01 ~]$ uptime 6 13:59:30 up 120 days, 21:29, 1 users, load average: 1.66, 4.26, 4.95 7 [lcp@ZCdb01 ~]$ uptime 8 14:05:27 up 120 days, 21:35, 1 users, load average: 0.07, 1.39, 3.42 9 [lcp@ZCdb01 ~]$ uptime 10 14:05:35 up 120 days, 21:36, 1 users, load average: 0.21, 1.38, 3.40 6、总结 问题判断+解决时长10分钟以内 优化判断+后期观察15分钟左右 此次问题解决总用时25分钟左右 此次问题由于对方对mysql数据库优化不到位,此公司并无相关技术人员,日常维护工作无法正常开展,导致突发状况访问异常。为保证以后服务器正常工作,优化完成后在配置文件(my.cnf)下添加如下参数记录慢查询语句。 1 long_query_time =2 #<==超过2秒,记录到LOG里。 2 3 log_queries_not_using_indexes #<==没有走索引的语句,记录到LOG里。 4 5 log-slow-queries = /data/3306/slow.log #<==LOG文件 但是建立索引的前提是,生产场景,表中数据多的情况下及高峰期不能建立索引,例如:300万记录。由于此次问题解决中使用的是联合索引,联合索引的特性是前缀生效,这也是有别于其他索引,所以创建时更为谨慎,需要与开发共同商议创建规则。否则索引无效。 关于mysql的优化从此次解决问题的过程中得出以下几个结论: 1、紧急情况抓慢查询SQL语句: 登录数据库 show full prcesslist; 2、未雨绸缪:重要不紧急:分析慢查询日志。(生成日志方法在上述总结中有具体参数) 分析慢查询SQL语句,每天定时发邮件给相关工作人员,核心开发、高级运维或DBA 每天切割慢查询日志,去重分析后发给大家。 切割方法: 1)mv ,relaod进程。2)cp,>清空 2)利用定时任务 以上分享内容到此结束,如有疑问欢迎发送邮件到lcp779401@cntv.cn探讨交流,希望对大家有所帮助。
前言: 趁着最近的休息时间,只能多勤快些:多写代码,多更新文章。 因为一旦投入新的工作,估计博客又会恢复到一年才产几篇的状态。 对于DBImport,因为用户的意见,增加了一个亮点功能,让软件B格升为数据库时时同步工具,所以值的介绍一下。 相比上一版本的主要功能更新: 1:优化MySql的导入效率。 2:增加定时功能(B格提升到时时数据同步功能)。 3:优化导出的表脚本和数据脚本。 4:从.NET 2.0 升级编绎成.NET 4.0 版本:(主要是为了支持Oracle:Oracle.ManagedDataAccess.dll 是4.0编绎的) 5:刚补充处理了字符转义问题(包括:生成SQL数据脚本、Mysql的指Load Data 语句的数据) DBImport V3.5介绍: 1:主图:界面的变化主去掉了存储过程分页选项,增加了定时功能配置项 定时功能介绍:(用户给我提醒:增加定时功能,那么软件就具备了时时数据同步功能,而且还是跨数据库的) 于是,软件的B格一下子就提升起来了,因为市场上的数据同步软件都很昂贵,而且只适配同类数据库。 现在,大伙多了一种简单的选择。 定时功能使用介绍: 1:打勾定时(按天或按间隔)=》操作选择会自动切换到第四选项(按主键自动识别更新或插入) 2:勾选Check【Time...】(如果表存在EditTime或UpdateTime字段,会自动根据此标识选出最新更新的数据) 3:开始导数据【如果要停止,把打勾的定时取消即可】 PS1:时间字段的名称是可以配置的,见软件目录的Config.txt文件。 PS2:下面的Where条件也增加了一个标签[EXETIME],适用于需要自定义条件的的定时器。 应用场景: 之前发布了ASP.NET Aries 框架的示例站,结果总有人捣乱,改密码,删数据搞破坏,影响其它人使用。 每次都是用户提醒我说账号登陆不了,要不菜单不见了,我只好默默打开电脑,开了DBImport,从本机导数据还原回去,累啊.... 现在把DBImport扔上去,定时半小时更新数据回去,一下子省心了,再也不用担心这些流氓用户破坏数据了。 如果你也有演示站,怕用户删数据,呵呵,扔个DBImport上去,设个定时,管你爱删不删。 2:主图2:界面调整了数据库的顺序,增加了Xml选项 这里优化了几个点: 1:显示数据脚本时,从同步变成线程(有用户反应字段多时会卡) 2:修正Txt和Xml的导出数据脚本(为Json格式的文本和Xml格式的Xml) 3:导出MSSQL数据脚本对于nvarchar等n开头字段,增加:N''(有用户反应不带N,英文环境下中文乱码) 4:导出的脚本处理Bit类型,统一转为1,0数据。(之前MySql必须False,MSSQL必须'False‘,有没有引号都要细心处理。) 3:主图3:数据库链接示例,根据不同的数据库类型在这里选择示例链接 主要说明: 软件目录下有(使用说明必看.txt),像SQLite、Sybase、Oracle,是需要根据情况解压对应的DLL再运行软件使用的。 4:关于MySQL批量执行的技术说明 1:MySql.Data.dll下有个:MySqlBulkLoader类,适用于批量插入。 2:看了一下源码,底层还是调用的Load Data 语法。 3:所以框架去调用Load Data语法实现。 4:发现Load Data 语法不支持二进制等数据。 5:发现Load Data 语法还不支持Bit类型(因为Bit类型在Mysql还是二进制) 6:框架在处理时:如果数据是由数字、字符串,时间类型的,走Load Data,反之则走原来的事务。 其它: 有网友说:秋天出品,必属精品 -- 我只有更加努力,以致做到毫不费力,来维护我这些开源或未开源的产品了。 1:历史版本集合:http://www.cnblogs.com/cyq1162/category/813601.html 2:下载地址:http://www.cyqdata.com/download/article-detail-42517
1、数据;
2、排序;
3、选择关键字;
是类似科学计数法吗
节省内存,单片机中使用较多
2层循环就可以了
Node find(Node root,int Key){
if(root == NULL)
return NULL;
if(root.val >= key){
return find(root.left, key);
}else{
Node * tmp = find(root.right, key);
if(tmp == NULL){
return root;
}else{
return tmp;
}
}
}
编译器就是这么做的,记住就行了,不必太纠结。
对linux来说,程序加载后内存是分段的。
screenshot
验证:
include
int main(){
char a[]="abc";
char b[]="abc";
char *c="abc";
char *d="abc";
printf("%pn", a);
printf("%pn", b);
printf("%pn", c);
printf("%pn", d);
}
结果:
@ ➜ ~ ./a.out
0x7fff52ac3afc
0x7fff52ac3af8
0x10d13cf8a
0x10d13cf8a
include include define MAXSIZE 100
typedef int Position;
typedef struct LNode *List;
struct LNode{
int Data[MAXSIZE];
Position last;
};
List initList(){
List L;
L = (List)malloc(sizeof(struct LNode));
L->last = -1;
printf("初始化成功n");
return L;
}
List createList(List L){
int n,i=1;
printf("请输入创建表的元素个数:");
scanf("%d",&n);
for(i=1;i<=n;i++){
printf("\n请输入第%d个元素:",i);
scanf("%d",&L->Data[i]);
}
printf("n");
for(i=1;i<=n;i++){
printf("创建的表为:");
printf("%d\n",L->Data[i]);
}
}
void main(){
List L = initList(L);
createList(L);
}
代码是哪里复制来的吗?
没有明白想表达什么意思
struct stu
{
char name[6];
int xuehao;
int grade;
struct stu *next;
};
typedef struct stu STU;
main()
{
STU p,p_start,p2,p_print,p_charu,p_charu2;
int i,xuehao;
//输入
for (i=0;i<3;i++)
{
p=(STU *)malloc(sizeof(STU));
printf("请输入学生姓名:n");
scanf("%s",p->name);
printf("请输入学生的学号:n");
scanf("%d",&p->xuehao);
printf("请输入学生的成绩:n");
scanf("%d",&p->grade);
if (i==0)
{
p2=p_start=p;
}
else
{
p2->next=p;
p2=p;
if (i==2)
p->next=NULL;
}
}
// printf("%d",p_start->next->next->next);
// printf("%d",p_start->next->next->xuehao);
//链表的插入
p_charu=p_start;
printf("请输入要删除的学号n");
scanf("%d",&xuehao);
while (1)
{
p_charu2=p_charu->next;
if (p_charu->next->xuehao==xuehao)
{
// if (p_charu->next->next==NULL)
// p_charu->next=NULL;
}
else
{
p_charu->next=p_charu->next->next;
free(p_charu2);
break;
}
if (p_charu->xuehao==xuehao)
{
p_start=p_charu->next;
free(p_charu);
break;
}
p_charu=p->next;
}
p_print=p_start;
}
代码格式好,花括号都缺少了
问题1:这个算法还有一点可以优化,就是对已经有序的序列的处理,比如{1,2,3,5,4};,处理方法是如果没有交换就跳出循环不过我没有完成优化,因为测试过没能完成排序。
问题2:j条件的设置:取决于i次循环后未排序的长度
c++支持,c不支持
第二个输入后循环直接退出了,没有进循环里呀
C中, char literal(字面量) 会被作为int处理。
C++中,C++的类型比C语言稍强,这个'a'这个字面量是作为char处理的。
循环遍历每行
int main()
{
int a;
char ch;
scanf("%dn", &a);
printf("a=%dn",a);
scanf("%c", &ch);
printf("ch=%cn",ch);
return 0;
}
第一次读入后添加换行
init函数不应该使用未初始化的指针作为参数
文件太乱,能不能说清楚每个文件内容是什么
using namespace std;
class Complex
{
public:
Complex(){real=0;imag=0;}
Complex(double r,double i){real=r;imag=i;}
Complex operator +(Complex &c2);
friend ostream& operator<<(ostream&os,Complex&);
private:
double real;
double imag;
};
ostream &operator<<(ostream &output,Complex&c)
{
output}
Complex Complex::operator+(Complex &c2)
{
return Complex(real+c2.real,imag+c2.imag);
}
int main()
{
Complex c1(2,4),c2(2,5),c3;
c3=c1+c2;
cout}
类的定义分号结束。分号是必需的,因为在类定义之后可以接一个对象定义列表。定义必须以分号结束:
using namespace std;
class Customer
{
public:
Customer(int bal):balance(bal){};
char account[10];
char password[10];
char name[10];
int balance;
};
int operator-(Customer& p, Customer& q)
{
return p.balance - q.balance;
}
int main(int argc, char* argv[])
{
Customer c1(60);
Customer c2(50);
cout << c1-c2 << endl;
}
非常量引用的初始值必须为左值,i++ 不可以作为左值;
++i 可以作为左值。
int main()
{
int i = 9;
//i++ = 10;
++i = 10;
return 0;
}