大家好,今天跟大家介绍一个对后端工程师特别有价值的工具——Vaadin。
说起来,上手前端基本的html, css开发,确实并不难,但是如果只会这些基本的东西,开发起来会很繁琐。如果想要使用前端生态中的各种轮子,虽说便利度提升了,但学习成本也会同步上升。所以,如果不是职业的全栈工程师,只是作为一个后端,想临时写点前端代码,比如自己想做点小项目,通常来说都会有个很痛苦的过程。
Vaadin很好的解决了这个痛点。通过vaadin包装好的常用前端组件,我们几乎可以零学习成本的编写出功能完备、不太难看的页面。对于后端背景的程序员来说,无疑会大幅度降低自己做些小项目的成本。
Vaadin提供的功能,就是可以直接用java代码来写页面。Vaadin提供了多种输入框、表单等等封装好的前端样式,而且与springboot做了深度的融合,使用起来非常方便。
Vaadin的实际原理并不复杂,主要是基于服务端渲染,即在后端生成最终的html代码,交给浏览器。服务端渲染,这个并不罕见,与客户端渲染的优势和劣势,我们在这里不多讲。当然,对于vaadin来说,使用服务端渲染,似乎也没什么好说的,毕竟是写的后端代码,直接在后端做渲染,是个再正常不过的实现路径。Vaadin的引擎对前后端之间的交互做了封装,所以对使用者来说,前后端之间的交互是无感的,在页面层,我们也可以正常的调用后端service.
下面是我写的一段代码示例,可以更直观的感受Vaadin的作用:
1 | @Route(value = "path", layout = MainView.class) |
这段代码中,我们完全使用java代码对页面中的各个组件进行了编排,包括button的click函数,也是使用java开发者习惯的方式定义,并且能够直接调用其他后端service, 可以说是几乎零学习成本了。
详细代码和运行效果,可以到 项目地址中查看。
如果你对Vaadin感兴趣,或者有任何问题或想法,欢迎在评论区交流。一起探索如何更好地利用Vaadin,提升我们的开发效率吧!
]]>其实,hexo的多语言方案,说白了就是两个思路,一个是在文章维度切换语言,借助hexo原生的能力和hexo-generator-i18n等插件,用一套框架维护两个语言的内容;另一个则是独立维护不同语言的样式、内容,在站点维度切换语言,只是在域名层面合并到同一个域名。
具体要选择什么方案,取决于大家想要一个什么样的多语言网站。一开始我想象中的多语言网站的是第一种,即统一的首页,在首页和每篇文章上都支持切换语言,对每篇文章,可以通过切换语言,直达对应的其他语言译文上。
这个思路,借助于hexo-generator-i18n插件,可以实现,但要做比较多的定制开发。所以,我又重新思量了一下思路,我要的多语言网站应该是什么样。重新考虑之后,逐渐觉得第二种思路才是更合理的。我们要做一个多语言网站的目的是什么?对我来说,其实就是要获取一些英文流量。作为一个未备案网站,已经很久无法获取百度的搜索流量了,google在中国占的份额又太低,所以单凭中文内容,很难从搜索中获取较高流量。所以我希望通过提供一些英文内容,获取英文流量。这个目标下,完全不需要对所有的文章都维护多语言版本。何况,不同文章的语言受众也的确有很大区别,所以,语言的切换可以直接在站点层面进行。
具体操作方式上,一种是直接维护多个站点,两个站点内容、样式完全独立,只是部署在同一个域名下。比如中文站点根路径是lichuanyang.top,英文站点根路径是lichuanyang.top/en . 两个站点上各做一个跳转链接,向对方跳转。
我采用的是另一种方式,相比上述方式,成本小一些,不需要实际维护两个站点。原理大致如下:
在本地,同样维护两个站点。但是英文站点不会去做实际的部署,只是用作生成静态网页的工具。英文版的内容生成之后,直接复制到中文网站的对应目录下,只对中文网站做部署即可。
具体操作步骤如下:
将博客目录整体复制一份,作为英文博客目录. 例如,我的博客根目录叫blog.source, 复制出一个blog.source.en. 这步完成后,如果博客源码也是以git维护的,可以直接在原目录的外层直接新建一个git项目,在外层做管理就行了。 示例如下:
1 | blog(git根目录)/blog.source |
将en目录下的文章全部删除;站点描述等文本调整为英文内容;英文语言设置为en;英文站点根 (_config.yml中的root配置)设置成/en
在两个站点下增加跳转链接。我是借助菜单功能,直接在菜单里加一个其他语言项。 next主题中配置如下:
1 | 中文站 : English: /en || fa fa-language |
之后配置基本就完成了,可以在en目录下写英文文章,流程与之前写中文文章时完全一致
最后就是生成和发布环节了,这一步要注意,每次生成时,我们需要先生成中文站点,再生成英文站点并将英文内容复制到中文目录下,避免生成中文内容时将英文内容覆盖掉。具体操作示例如下:
1 | hexo clean && hexo g &&cd ../blog.source.en && hexo clean && hexo g && cd ../blog.source &&cp -r ../blog.source.en/public/. public/en/ && hexo s |
命令最后一段,使用hexo s就是本地启动,使用hexo d就是发布出去,和正常使用时一样。
这样,我们就有了一个好用的多语言站点了。之后,写中文内容就在中文目录下进行,写英文内容就在英文目录下进行, 最后执行一下上边的命令就可以了。
]]>之前在 小众软件 上看到一个知乎评论区时间展示混乱的吐槽,有大佬立刻就给了一个油猴脚本。
用了一段时间很舒服,然后前几天知乎做了个更新,导致展示开始混乱。研究了一下,做了更新,想着就发布到GreasyFork上吧,后边知乎再有调整,可以直接从线上更新。
脚本地址: 安装地址
大家感兴趣的话,欢迎留言,后边考虑做个油猴脚本的开发教程。
]]>但是在一些劣质博客和公众号的努力下,这块知识已经成功的成为了一片烂八股。
接下来,只需要顺着我的思路,回答几个简答的问题,就可以将事务隔离级别理解透彻。
问:怎么实现不同事务直接的隔离?
对这个问题,当你处于讨论数据库事务隔离级别语境下时,是不是觉得很难回答?然后先把数据库三个字从脑子里剃掉,不同事务,也就是不同线程,操作数据怎么做隔离?答案是不是很简单?就是加锁。
问:锁有哪些类型
答:有很多种分类方式。在这里,我们把锁分成读锁和写锁。
问:加不同级别锁时,代码的表现会有什么区别?
到这里,原问题的答案是不是就呼之欲出了。
事实上,数据库事务为什么会有这些个不同的隔离级别,就是因为不同场景下使用了不同的锁。比前边说的稍微复杂的点,就是数据库中存在范围锁,在其他领域不太常用,就是把某一段数据整体锁住。
如果我们把所有操作能加的锁都加上,实际上就是串行化的操作了。这种方式隔离性当然很好,但性能就没法说了,所以一般也不会有人使用。
可重复读则是对涉及到的数据加读锁和写锁,并持有到事务结束,但不会加范围锁。这样就会出现幻读的问题,即一个事务内执行两次范围查询,如果这两次查询之间有新的数据被插入,就会导致两次范围查询的结果不一致。
读已提交和可重复读的区别是他的读锁会在查询操作结束之后立刻释放掉,这样,在事务执行过程中,已经查询过的数据是可以被其他事务任意修改的,所以也就会有不可重复读的问题。
读未提交级别下,则完全不会加读锁。这样造成的问题是,由于读操作时不会去申请读锁,所以反而会导致能够读到其他事务上加了写锁的数据,也就会出现脏读的问题。
当然,为了更好的平衡性能与隔离性,还有一些诸如MVCC之类的方案,用额外的存储来实现事务隔离的效果,这些是取巧解决80%问题的方式。
]]>对于kubernetes是什么,一个比较官方的定义,Kubernetes是一个开源容器编排平台,管理大规模分布式容器化软件应用,简称为k8s.
通俗一些来讲,k8s 的核心理念是以应用为中心,向下屏蔽基础设施的差异,向上通过容器镜像实现应用的标准化,帮助开发人员在仅需关注应用本身,无需考虑部署、容灾、伸缩等运维细节的情况下,开发出大规模、可靠的分布式应用。
如上所述,k8s的核心优势就是降低运维成本,让应用开发人员能够专注于应用本身。具体来讲,包括如下几点:
这篇kubernetes教程的剩余部分会从应用开发人员视角,介绍需要了解的k8s概念,主要包括以下几类:
这是k8s内部大部分逻辑的实现方式,了解这个逻辑,可以帮助开发人员更好的理解某些特定情况下k8s的行为。
如果翻看k8s的yaml配置,就会发现k8s的大部分组件中都有一个或若干个spec字段,它的含义很好理解,就是对于某个属性的期望值,比如一个服务节点数量的期望值。
controller的作用就是持续监测期望值和实际指标是否一致,不一致时就进行相应的操作进行处理,比如发现节点数不够了,就会启动对应数量的新节点。
这样,无论是上线、扩缩容、还是故障恢复,我们会发现这些逻辑都变得非常清晰,都是触发一下对容器数期望值或实际值的调整,然后让controller去增删节点就可以了。
这套机制是k8s里广泛使用的一个底层设计原则,作用是解开监控和操作逻辑之间的耦合。举个例子,我们在很多情况下都需要触发启动新的节点,包括要做扩容时、部分节点挂了时、要上线时,等等。这些完全没关联的场景如何去触发相同的操作,代码要如何保持整洁呢?k8s的做法就是用一个期望值的概念做中间层,将操作的触发时机和操作本身拆分成独立的逻辑。
了解了这个逻辑,我们可以更容易的理解k8s是怎么工作的。 比如基于HPA做自动扩容,就是根据当前的CPU使用情况,和期望的CPU使用量,决定是增加还是减少节点。
Deployment 用于指示 k8s 如何创建和更新应用程序,我们通常理解的一个“服务”、“应用”大体上就对应在deployment这一层上。 像镜像、内存cpu使用限制、节点数、环境变量等各种配置,都是在deployment上。
pod比较好理解,就是我们通常理解的一个节点。pod是 k8s 中最小可管理单元。
Service是一个需要着重了解的概念,它是一个抽象层,它选择具备某些特征的 Pod(容器组)并为它们定义一个访问方式。 通常的应用场景下,deployment和service是一一对应的,但实际上deployment和service之间并没有必然的联系。
一个请求恰好可以打到某个deployment关联的所有pod上,看起来是一件非常正常的事情。而事实上,这件事情的发生不是由于这些pod通过一个deployment产生,而是因为deployment上配置了一个标签,通过这个deployment生成的pod都获得了这个标签; 同时,另外存在一个service, 设置通过这个标签选择pod, 请求进入的是通过service选择出的一批pod.
k8s提供了一些用于应用扩展的接口,其中就包括probe和hook, 可以帮助应用更好的利用k8s的能力。
k8s提供了 liveness和 readiness 探针,顾名思义,liveness probe的作用是判断应用是否存活,readiness probe的作用是判断应用是否已经启动完毕。
应用可以用http接口等不同形式提供探针,k8s则负责在探测到不同状态时做不同的操作。比如说探测到liveness probe返回状态异常,则可以认为此节点已经丢失,对此节点做删除处理。
hook则支持应用在一些特定阶段插入一些行为,比如preStop hook, 运行应用关闭之前先做一些操作,可以用来做graceful shutdown.
通过对本篇kubernetes教程上文列出的概念的了解,相信大家可以从应用开发人员的视角完全理解kubernetes是什么, 也可以大致想象出应用方使用k8s的基本思路: 构建标准镜像,暴露出必要的监控数据,剩下的事情都交给k8s来做。
在准确的数据下,k8s可以帮我们做好系统的自愈、扩缩容等操作,而我们要做的是,结合应用场景,尽量给出更加准确的监控数据。
举一些例子,比如说应用提供的liveness探针,如何能尽量确保准确的展示系统的健康状态,某个接口可以访问,某个端口可以调通,能否等价的说明这个应用是否健康。
比如说k8s以cpu、内存等指标来判断系统负载时,这些机器指标能否真实的展现出应用的真实负载,是否有可能连接池、IO等其他瓶颈会在cpu、内存等瓶颈到来之前就达到。
这些都是需要应用方结合应用的实际情况精心设计的。
此外,k8s的设计理念决定了在k8s环境下,“重启”会是一件非常稀松平常的事,像硬件故障、小概率的死循环、不健康的gc, 等不同类型的问题,都可以在k8s的自主重启下实现自愈; 自动的扩缩容扩充中,也会经常性的有节点启停。 所以说,应用需要让自己的启动流程顺畅一些,避免大量耗时或大量资源消耗。
]]>之前我在看我自己账号知乎数据的时候,发现一个挺有意思的事情,就是PC端的流量居然有40%以上,这在移动互联网时代,简直是不可想象的。后来也从一些其他渠道了解到,知乎周末、节假日的流量是不如工作日的。
这说明什么呢?上班刷知乎的你,不是一个人。对相当一部分人来说,知乎主要是在上班摸鱼的时候使用的。
知乎这个网站,的确挺适合摸鱼,毕竟以文字内容为主,而且还有相当多的专业性内容,所以工作时上知乎还真的有可能是在查资料。
但是,知乎有些小体验,对摸鱼就不太友好了。比如说图片尺寸太大,还有浏览问题的时候标题浮窗总是会出现,还会很显眼的展示出来。所以,我就写了个简单的油猴脚本,优化了这两个问题,顺便把知乎的logo也给去掉了。
脚本地址: 安装地址
大家感兴趣的话欢迎使用。对知乎的使用有什么痛点也欢迎提出来。
]]>所以,这篇笔记中,我主要其实就是比较机械化的把书中的一些核心概念提炼出来,同时会尝试举一些身边的例子。 而若想更好的理解书中每一部分,还是推荐去读一遍书,并且跟着作者的思路一起去好好思考那些例子的逻辑。
系统是一组相互连接的事物,在一定时间内,以特定的行为模式相互影响。
任何一个系统都包括三种构成要件:要素、连接、功能或目标。
存量是所有系统的基础,比如浴缸中的水、人口数量、书店中的书、树木的体积、银行里的钱,等等。但是,存量不一定非得是物质的,你的自信、在朋友圈中的良好口碑,或者对世界的美好希冀等,都可以是存量。
存量会随着时间的变化而不断改变,使其发生变化的就是“流量”
绘制存量-流量图,是理解系统的基础方式。
当某一个存量的变化影响到与其相关的流入量或流出量时,反馈回路就形成了。
调节回路与增强回路是两种很常见的反馈回路,比较好理解,就是字面意思。调节回路趋向于将系统存量维持稳定,增强回路则是会不断放大、增强原有的发展态势。
接下来通过一些常见的系统模型,来帮助大家更好的理解系统是什么。
系统1.1: 一个存量、两个相互制衡的调节回路的系统, 比如温度调节器,通过一个升温调节回路、一个降温调节回路,控制存量(温度)稳定。
系统1.2:一个存量、一个增强回路以及一个调节回路的系统, 比如人口,新出生是增强回路,死亡是调节回路。
系统1.3:含有时间延迟的系统,比如库存,相比简单的温度调节器,主要区别在于调节回路上会存在延时。
系统2.1:一个可再生性存量受到另外一个不可再生性存量约束的系统, 比如开采石油等不可再生资源,就会存在资本和资源两个存量,其中存在一个增强回路,即开采资源过程中会获得利润,获得的利润能够增加资本,从而扩大产能,加大开采量。同时,由于石油不可再生,开采难度会越来越大,也就形成了一个调节回路。
系统2.2:有两个可再生性存量的系统,比如渔业,和石油比较类似,只是鱼是可再生资源,会导致系统的发展形态会和上一类系统有所区别。
适应力。 系统之所以会有适应力,是因为系统内部结构存在很多相互影响的反馈回路,正是这些回路相互支撑,即使在系统遭受巨大的扰动时,仍然能够以多种不同的方式使系统恢复至原有状态。比如人体就是一个适应力非常强的系统。
自组织。 系统所具备的使其自身结构更为复杂化的能力,被称为“自组织”。自组织对于系统来说,意味着系统可以自己去“进化”,从而完成更大的目标,或实现更复杂的功能。像我之前的老板,经常跟我说我们的目标是去建立一个具有“自组织”能力的团队。只有这样,团队才能跟着业务发展一起进步。
层次性。 在新结构不断产生、复杂性逐渐增加的过程中,自组织系统经常生成一定的层级或层次性。一个大的系统中包含很多子系统,一些子系统又可以分解成更多、更小的子系统。如果各个子系统基本上能够维系自身,发挥一定的功能,并服务于一个更大系统的需求,而更大的系统负责调节并强化各个子系统的运作,那么就可以产生并保持相对稳定的、有适应力和效率的结构。
我们认为自己所知道的关于这个世界的任何东西都只是一个模型。我们的模型通常是与现实世界高度一致的。这就是我们为什么会成为这个星球上最为成功的一个物种的原因。 我们的模型仍远远达不到能完整地描绘世界的程度。这就是我们为什么经常会犯错误、会感到出乎意料的原因。
所以,我们要看看从系统的角度可以发现哪些问题,帮助我们尽量的少犯错。作者主要整理了以下6点:
这一章则是一些更具体的问题,以及针对这些问题有哪些可能的对策。
有一些长期持续的行为模式,可能并不符合人们的预期,往往被视为一个问题。尽管人们发明了各种技术、采取了多项政策措施,试图去“修复”它们,但系统好像很顽固,每年都产生相同的行为。这是一种常见的系统陷阱,人们习惯称之为“治标不治本”或“政策阻力”, 比如毒品泛滥、失业等。
“政策阻力”来自于系统中各个参与者的有限理性,每一个参与者都有自己的目标。当各个子系统的目标不同或不一致时,就会产生变革的阻力。应对“政策阻力”最有效的方式是,设法将各个子系统的目标协调一致,通常是设立一个更大的总体目标,让所有参与者突破各自的有限理性。
对于人们共同分享的、有限的资源,很容易出现开发(或消耗)逐步升级或增长的态势。“公地悲剧”之所以产生,一个重要原因是资源的消耗与资源的使用者数量增长之间的反馈缺失了,或者时间延迟太长。
防止“公地悲剧”有以下三种方式:一是教育,帮助人们更清晰的看到后果;二是将资源私有化;三是采取一些配额制之类的管制措施。
绩效标准受过去绩效的影响,尤其是当人们对过去的绩效评价偏负面,也就是过于关注坏消息时,将启动一个恶性循环,使得目标和系统的绩效水平不断下滑。通俗的说法,就是温水煮青蛙,形势在不知不觉中慢慢变差。
对策是保持一个绝对的绩效标准。更好的状况是,将绩效标准设定为过去的最佳水平,从而不断提高自己的目标,并以此激励自己。
“以眼还眼,以牙还牙”。每一个参与者期望的系统状态都是相对于其他参与者而言的,并试图超越对方,领先一步,连并驾齐驱都不行;而且每个参与者都有高估对方的敌意、夸大对方实力的倾向。竞争升级的系统结构是一个增强回路,它是以指数级方式发展起来的,一旦超过某个限度,其使竞争激化的速度会超出绝大多数人的想象。
对策一种依托于一方主动让步;更优雅的方式则是期望双方达成协定。
利用积累起来的财富、权力、特殊渠道或内部信息,可以创造出更多的财富、权力、渠道以及信息。这些都是另外一个被称为“富者愈富”的基模的例子。
对策:多元化,即允许在竞争中落败的一方可以退出,开启另外一场新的博弈;反垄断法,即严格限制赢家所占有的最大份额比例;修正竞赛规则,限制最强的一些参与者的优势,或对处于劣势的参与者给予一些特别关照,增强他们的竞争力(例如施舍、馈赠、税赋调节、转移支付等);对获胜者给予多样化的奖励,避免他们在下一轮竞争中争夺同一有限的资源,或产生偏差。
当面对一个系统性问题时,如果采用的解决方案根本无助于解决潜在的根本问题,只是缓解(或掩饰)了问题的症状时,就会产生转嫁负担、依赖性和上瘾的状况。
应对这一陷阱最好的办法是提前预防,防止跌入陷阱。一定要意识到只是缓解症状或掩饰信号的政策或做法,都不能真正地解决问题。
任何规则都可能会有漏洞或例外情况,因而也存在规避规则的机会。也就是说,虽然一些行为在表面上遵守或未违背规则,但实质上却不符合规则的本意,甚至扭曲了系统。
对策是设计或重新设计规则,从规避规则的行为中获得创造性反馈,使其发挥积极的作用,实现规则的本来目的。
系统行为对于反馈回路的目标特别敏感。如果目标定义不准确或不完整,即使系统忠实地执行了所有运作规则,其产出的结果却不一定是人们真正想要的。
对策就是恰当地设定目标及指标,以反映系统真正的福利。
这一部分则是一些我们能够去干预系统的方式,按照有效性由低到高去排列。这一章里的很多理论,相信大家或多或少在其他的地方也见到过一些。很多优秀的人,即便没读过这本书,很多时候其实也是这么思考的。而系统之美这本书,能够帮助我们更加体系化的了解为什么要这么做。
12:数字,通过各种流量的数值来调节系统。像王者荣耀处理平衡性的策划,基本上很大一部分精力就在做这事,哪个英雄机制过强了,就狠狠的削一把数值,哪个英雄不大行,就加数值,让他站撸也有很高的强度。这种方式实际上是效力很低的一种方式,它无法改变系统基本的结构。但是大多数人90%的注意力都会集中在参数上。
11:缓冲器。通过提高缓冲器的容量,我们通常可以使系统稳定下来。但是,如果缓冲器过大,系统也将变得缺乏弹性,它对于变化的反应速度将过于缓慢。同时,要建立、扩大或维护某些缓冲器的容量,也需花费巨大的时间和资金,例如建设水库或仓库等。为此,一些企业发明了“零库存”的“及时生产”模式。在这些企业看来,与耗费巨资维持固定的库存相比,偶尔的波动或缺货造成的损失并不是很大。
10.存量—流量结构:实体系统及其交叉节点。这一点主要关乎于系统的整体设计。恰当的杠杆点,需要从一开始就被设计好。一旦实体的结构建立起来了,要想找到杠杆点,就需要理解系统的限制和瓶颈,在尽可能发挥它们的最大效率的同时,避免出现较大的波动或扩张,超出其承受能力。而如果系统已经运行起来,想再去调整其中的关键节点,就会变的很困难。软件工程中著名的“防腐层”概念,可以一定程度上优化这个问题,即通过一些预设的中间层,减少调整关键节点的成本。
9.时间延迟:系统对变化做出反应的速度。时间延迟是一个高杠杆点,但事实上,时间延迟通常不是很容易改变的。很多事物的发展有其内在规律,该花多长时间就得花多少时间。你不可能一夜之间积累起一大笔资本,孩子也不可能在一夜之间长大,拔苗助长也无法加快庄稼的生长。但如果有办法改变时间延迟,往往可以取得显著效果。比如近期疫情防控中,为什么要经常进行大规模的核酸检测,就是通过这种方式降低病毒从被传播到被发现这一段的时间延迟。
8.调节回路:试图修正外界影响的反馈力量。这一点很好理解,我们可以通过加调节回路的方式,加强对系统的控制。kubernetes里的controller可以认为就是调节回路的实现,通过不同的controller, 就可以将不同组件的值,也就是存量,控制在期望值上。
7.增强回路:驱动收益增长的反馈力量。和调节回路类似,只是目标不同。
6.信息流:谁能获得信息的结构。相当于一个新的回路,让人们在之前得不到信息的地方可以获得反馈。比如说资本主义罪恶的加班排名,就是将同事的加班信息告诉你,让你自然产生压力,从而促进加班。
5.系统规则:激励、惩罚和限制条件。这一部分,实际上就是将系统的范围、边界和自由度定义清楚。
4.自组织:系统结构增加、变化或进化的力量。接文章开头的话题,要建设一个自组织的团队,是要做很多事情的,比如提升团队中每个成员的能力,让大家都能有决策能力;加强团队内信息的透明度;等等。而这样的团队一旦能建成,不光产出会有质的提升,团队中成员的工作成就感和幸福度也会大幅度提升。
3.目标:系统的目的或功能。系统中的某个参与者可以清晰地设定、阐述、重复、支持并坚持新的目标,从而引导了系统的变革。这就是为什么这些年OKR这么流行的原因。通过明确合理的OKR, 指导大家更合理的决策和行动。
2.社会范式:决定系统之所以为系统的心智模式。一些社会公认的观念,一些潜在的基本假设以及关于社会现实本质的普遍看法,构成了社会的范式(paradigm),或者是一整套世界观,它们是人们普遍相信的、关于世界是如何运作的一系列基本假设、规则或信念。这些信念都是隐含的,因为在一个社会中,几乎每一个人都已经知道它们,因而无须特别申明。这些范式自然会对系统运行产生极其重大的影响,当然,要改变这些范式,也会比改变其他东西更困难。
1.超越范式。与改变范式相比,在更高的层次上,还有另外一个杠杆点,那就是使自己摆脱任何范式的控制。这一点其实有那么一点玄学的意思了,我们在这里就不多展开。核心点其实就是我们要有意识的去跳出当前系统。
在工业社会长大的人,若热衷于系统思考,很可能会犯一个严重的错误。他们可能会假定,通过系统分析,可以认清系统中的相互联系以及复杂纠葛,借助计算机的威力,最后找到预测和控制系统的钥匙。不幸的是,这是错误的观念,其根源在于工业时代根深蒂固的心智模式,即相信存在一把预测和控制的钥匙。但实际上,要做到这一点是完全不现实的,我们需要认识到并愿意放弃控制的错觉,需要换一种截然不同的方式。这样,我们仍然可以有很大的作为空间。这种方式就是与系统共舞。我们无法控制系统,但是可以在系统中生存的更好。
接下来就是和系统共舞的一些法则,这一部分相当于一些小tips, 也都比较好理解,我们就简单列举一下:
首先我们需要明确本文要讨论的分布式系统是什么,简单的说,就是满足多节点和有状态这两个条件即可。多节点很好理解,有状态则是指这个系统要维护一些数据,不然的话,其实我们无脑的水平扩容就没有任何问题,也就不存在分布式系统的问题了。
常见的分布式系统, 无论是mysql, cassandra, hbase这些数据库,还是rocketmq, kafka, pulsar这样的消息队列,还是zookeeper之类的基础设施,其实都满足这两个条件。
这些分布式系统的实现通常来说主要需要关注两个方面:一是自己本身功能的实现,二是在分布式环境下保持良好的性能与稳定性;即便是两个功能完全不一样的系统,其对第二类问题的处理方式也会有很多相似之处。本文的关注重点也即在对第二类问题的处理上。
接下来,我们列举一下分布式系统都有哪些常见目标,包括而不限于:
要达成这些目标,又有哪些挑战呢?大概有以下这些:
进程崩溃: 原因很多,包括硬件故障、软件故障、正常的例行维护等等,在云环境下会有一些更加复杂的原因;
进程崩溃导致的最大问题就是会丢数。出于性能的考虑,很多情况下我们不会进行同步的写磁盘,而是会将数据暂时放在内存的缓冲区,再定期刷入磁盘。而在进程崩溃的时候,内存缓冲区中的数据显然会丢失。
网络延迟和中断: 节点的通信变到很慢时,一个节点如何确认另一个节点是否正常;
网络分区: 集群中节点分裂成两个子集,子集内通信正常,子集之间断开(脑裂),这时候集群要如何提供服务。
这里插一个彩蛋,在CAP理论的前提下,现实中的系统通常只有两种模式:放弃高可用的CP模式和放弃强一致性的AP模式。为什么没有一种放弃分区容忍性的CA模式?就是因为我们无法假设网络通信一定正常,而一旦接受了集群变成两个分区,再想合并回来就不现实了。
进程暂停:比如full gc之类的原因导致进程出现短暂的不可用后又迅速恢复,不可用期间集群有可能已经做出了相关的反应,当这个节点再恢复的时候如何维持状态的一致性。
时钟不同步和消息乱序:集群内不同节点的操作,我们希望它的顺序是明确的;不同节点之间的时钟不同步,会导致我们无法利用时间戳确保这件事。而消息的乱序就给分布式系统的处理带来了更大的难度。
下面,我们就依次介绍,针对这些问题,都有什么处理方式。
对于进程崩溃的问题,首先要明确的是,单纯实现进程崩溃下不丢数,没有任何难度,重要的是怎么在保证系统性能的前提下达到这个目标。
首先要介绍的就是write-ahead log这种模式,服务器将每个状态更改作为命令存储在硬盘上的仅附加(append-only)文件中。 append操作由于是顺序的磁盘写,通常是非常快的,因此可以在不影响性能的情况下完成。 在服务器故障恢复时,可以重播日志以再次建立内存状态。
其关键思路是先以一个小成本的方式写入一份持久化数据,不一定局限于顺序写磁盘,此时就可以向client端确认数据已经写入,不用阻塞client端的其他行为。server端再异步的去进行接下来高消耗的操作。
典型场景及变体:mysql redo log; redis aof; kafka本身 ;业务开发中的常见行为:对于耗时较高的行为,先写一条数据库记录,表示这个任务将被执行,之后再异步进行实际的任务执行;
write-ahead log会附带一个小问题,日志会越攒越多,要如何处理其自身的存储问题呢?有两个很自然的思路: 拆分和清理。
拆分即将大日志分割成多个小日志,由于WAL的逻辑一般都很简单,所以其拆分也不复杂,比一般的分库分表要容易很多。这种模式叫做 Segmented Log, 典型的实现场景就是kafka的分区。
关于清理,有一种模式叫做low-water mark(低水位模式), 低水位,即对于日志中已经可以被清理的部分的标记。标记的方式可以基于其数据情况(redolog), 也可以基于预设的保存时间(kafka),也可以做一些更精细的清理和压缩(aof)。
再来看网络环境下的问题,首先使用一个非常简单的心跳(HeartBeat)模式,就可以解决节点间状态同步的问题。一段时间内没有收到心跳,就将这个节点视为已宕机处理。
而关于脑裂的问题,通常会使用大多数(Quorum)这种模式,即要求集群内存活的节点数要能达到一个Quorum值,(通常集群内有2f+1个节点时,最多只能容忍f个节点下线,即quorum值为f+1),才可以对外提供服务。我们看很多分布式系统的实现时,比如rocketmq, zookeeper, 都会发现需要满足至少存活多少个节点才能正常工作,正是Quorum模式的要求。
Quorum解决了数据持久性的问题,也就是说,成功写入的数据,在节点失败的情况下,是不会丢失的。但是单靠这个,无法提供强一致性的保证,因为不同节点上的数据是会存在时间差的,client连接到不同节点上时,会产生不同的结果。可以通过主从模式(Leader and Followers) 解决一致性的问题。其中一个节点被选举为主节点,负责协调节点间数据的复制,以及决定哪些数据对client是可见的。
高水位(High-Water Mark)模式是用来决定哪些数据对client可见的模式。一般来说,在quorum个从节点上完成数据写入后,这条数据就可以标记为对client可见。完成复制的这条线,就是高水位。
主从模式的应用范围实在太广,这里就不做举例了。分布式选举算法很多,比如bully, ZAB, paxos, raft等。其中,paxos无论是理解还是实现难度都太大,bully在节点频繁上下线时会频繁的进行选举,而raft可以说是一种稳定性、实现难度等各方面相对均衡,使用也最广泛的一种分布式选举算法。像elastic search, 在7.0版本里,将选主算法由bully更换为raft;kafka 2.8里,也由利用zk的ZAB协议,修改为raft.
到这儿,我们先总结一下。实际上,一个对分布式系统的操作,基本上就可以概括为下边这么几步:
其中,2-5步之间的顺序不是固定的。分布式系统平衡性能和稳定性的最重要方式,实质上就是决定这几步操作的顺序,以及决定在哪个时间点向client端返回操作成功的确认信息。例如,mysql的同步复制、异步复制、半同步复制,就是典型的这种区别的场景。
关于进程暂停,造成的主要的问题场景是这样的:假如主节点暂停了,暂停期间如果选出了新的主节点,然后原来的主节点恢复了,这时候该怎么办。这时候,使用Generation Clock这种模式就可以,简单的说,就是给主节点设置一个单调递增的代编号,表示是第几代主节点。像raft里的term, ZAB里的epoch这些概念,都是generation clock这个思路的实现。
再看看时钟不同步问题,在分布式环境下,不同节点的时钟之间必然是会存在区别的。在主从模式下,这种问题其实已经被最大限度的减少了。很多系统会选择将所有操作都在主节点上进行,主从复制也是采取复制日志再重放日志的形式。这样,一般情况下,就不用考虑时钟的事情了。唯一可能出问题的时机就是主从切换的过程中,原主节点和新主节点给出的数就有可能存在乱序。
一种解决时钟不同步问题的方案就是搞一个专门的服务用来做同步,这种服务叫做NTP服务。但这种方案也不是完美的,毕竟涉及到网络操作,所以难免产生一些误差。所以想依靠NTP解决时钟不同步问题时,系统设计上需要能够容忍一些非常微弱的误差。
其实,除了强行去把时钟对齐之外,还有一些简单一些的思路可以考虑。首先思考一个问题,我们真的需要保证消息绝对的按照真实世界物理时间去排列吗?其实不是的,我们需要的只是 一个自洽、可重复的确定消息顺序的方式,让各个节点对于消息的顺序能够达成一致即可。也就是说,消息不一定按照物理上的先后排列,但是不同节点排出来的应该一样。
有一种叫Lamport Clock的技术就能达到这个目标。它的逻辑很简单,如图所示
就是本机上的操作会导致本机上的stamp加1,发生网络通信时,比如C接收到B的数据时,会比较自己当前的stamp, 和B的stamp+1, 选出较大的值,变成自己当前的戳。 这样一个简单的操作,就可以保证任何有相关性的两个操作(包括出现在同一节点、有通信两种情况)的顺序在不同节点之间看来是一致的。
另外,还有一些相对简单些的事情,也是分布式系统设计中经常要考虑的,比如怎么让数据均匀的分布在各个节点上。对于这个问题,我们可能需要根据业务情况去找一个合适的分片key, 也可能需要找到一个合适的hash算法。另外,也有一致性哈希这种技术,让我们控制起来更自如。
分布式系统设计中还需要重点考虑的一块就是如何衡量系统性能,指标包括性能(延迟、吞吐量)、可用性、一致性、可扩展性等等,这些说起来都比较好理解,但要是想更完善的去衡量,尤其是想更方便的去观测这些指标的话,也是一个很大的话题。
]]>事实上,这个瓶颈,绝大多数情况下都是数据库。
对于90%以上的场景来说,高并发问题本质的难点就在于数据库能够承载的并发是有限的。而各种高并发技术方案的作用归根结底其实也都是去降低单库的连接数,
比如:
系统拆分:首先将不同业务的库分开,每个业务可以各自独享一个数据库;
缓存:使用缓存降低需要访问数据库的比例;
MQ等方式削峰:避免瞬时的数据库连接数过多;
分库、分表、读写分离:对同一业务的数据库做进一步的拆分,降低单库的访问量;
引入elastic search, clickhouse等其他存储:与上一条类似,将一些不适合mysql的业务拆分出来,进一步降低mysql的并发;
上面的思路,是提升应用的处理能力;不需要所有请求都去连数据库,那么自然就可以承载更高的并发了。
另一个方向的思路,是限流,这样做的主要意义有两个:保证处理能力内的这一部分请求能够被正常处理,而不是拖垮所有请求; 即使有问题,也将问题限制在一个很小的范围。
限流的方式很多,比如我们熟悉的漏桶、令牌桶等算法,这里就不再细说。
除此之外,还有各种我们所熟悉的“池子”,比如tomcat连接池、线程池等,包括应用里的mysql连接池,其实也是在限制能够被发出的最大请求数。
为了确定这些池子的状态,要做好监控,比如tomcat的活跃连接数、mysql的活跃连接数等等,快要满的时候就要看情况准备扩容了。
如何给这些池子设置更合理的参数,保证:机器的数据库的资源被充分利用;池子未满时,确定不会超过数据库的负载
这样理解下来,我们也就知道为什么大部分讲高并发处理思路的文章,关注焦点都在缓存、分库分表、连接池优化这些事情上了。
]]>讲互联网广告,首先要从广告本身是什么开始讲起。只有理解了广告的底层逻辑,才能更好的帮助我们理解在互联网广告中会有什么问题, 为什么会出现广告的程序化交易,adx, ssp, dsp,dmp等等各种各样不同的系统为什么会出现,以及诸如此类的等等问题。
广告这个行业其实自古就有,从远古时代小商小贩的吆喝,各种建筑物上的告示,到纸质报纸、杂志、电视等传统媒体上的广告,再到互联网广告,这个行业总是再随着社会的发展产生它自己的变化。
对于广告,先上一个非常官方、非常标准的定义:广告是由已确定的出资人通过各种媒介进行的有关产品(商品、服务和观点)的,并且通常是有偿的、综合的、劝服性的非人员的信息传播活动。
而更通俗的讲,在广告交易中,核心的参与方有三个:媒体(即供给方,Supply), 广告主(即需求方, Demand),受众(即媒体的用户)。其中,媒体和广告主是主动的参与方,受众则是被动的参与方。而广告实际上就是这三方相互博弈的一个过程。对于广告主来说,希望有更多的受众能看到广告,并且从广告中获取更高的转化;对于媒体来说,希望在获得最大的广告利润的同时,又能减少对用户的打扰;而对受众,也就是普通用户,则是希望不被自己不想看到的广告打扰。
接下来我们分析一下这个流程里的博弈点。众所周知,如果一件事对于各参与方是零和博弈的话,这件事情就难免陷入到恶性竞争中,必定是难以发展的。而广告行业既然能持续健康的发展,主要是靠对广告生态之外产生的影响,所以我们就分析一下有哪些对广告生态外的影响因素。
我们先拆解下各方都有哪些考虑指标,对于媒体,主要就是广告收益和自己产品的留存,对于广告主,则是广告费用和通过广告获得的收益,受众作为被动参与者,在整个过程中能做的不多,只能用脚投票,离开广告太烂的产品。这里边,在整个广告生态之外的,就是广告主通过广告获得的收益,和减少广告对受众造成的打扰,两个方面。事实上,对这两点的提升也就是整个广告行业不断发展的源动力。
理解了这一点,能帮助我们更好的理解互联网广告的发展历程。
最早的互联网广告,实质上就是把传统媒体的广告形式直接搬到线上来。比如说一份报纸里,会有一个区域专门用来放广告。像我们早些年在搜狐、新浪这些网站上看到的广告,也都是这种形式。广告主直接购买网页上的某个区域,固定的显示某些广告内容。
接下来,有些人会逐渐注意到互联网广告和传统媒体的一个关键区别:互联网广告的内容是可以因人而异的。已经印刷出去的报纸,所有人看到的都一样,而对于网站来说,完全可以做到对不同人群,比如男性和女性展示不同的广告。而媒体只需向广告主保证投放量,以及投放量不能完成时的一些赔偿方式。这就是合约广告。
合约广告的一个很自然的发展趋势就是定向越来越精细,诸如性别、年龄、地域、兴趣爱好等,都可以成为合约条件的一部分。同时,参与进来的广告主越来越多。这两方面的因素都会导致履约的难度急剧上升。同时,这么复杂的逻辑下,媒体也会觉得自己很多流量没有卖出最高价。
在这种情况下,新的广告形式应运而生,也就是竞价广告。供给方不再以合约的方式给出量的保证,只保证单位流量的成本;每一次广告展示都按照收益最高的原则来决策。这样,广告就逐渐发展成了类似「流量交易」这种模式, 形成了程序化广告交易这种模式。
在竞价广告的场景下,没有了合约的约束,大量媒体、广告主进入了一个多方博弈的环境,交易过程也就越来越复杂,产生了诸如RTB(real time bidding,实时竞价)这样的交易方式。程序化广告交易正是在这种情况下产生的。
同时,随着技术的发展,更多的数据、深入的计算与预测成为可能,广告主、媒体以及相关的广告代理等各个参与方也都需要更加丰富的功能。我们标题上提到的ssp,adx, dsp, rtb这些名词,也都是在这种情况下,逐步发展成为广告业态中非常重要的系统。
总体来说,程序化广告交易和股票交易等实时交易的场景都非常类似,都是存在一个交易场(ADX), 买方(广告主)和卖方(媒体),各自在其中实时的做买卖决策。唯一一个重要的区别是,对于媒体来说,没有择时的机会,流量来了只能立刻卖出去,没有办法攒起来等一个好时机。可以说,这一点正是广告相关各项技术发展的出发点。
前面说了,互联网广告发展的源动力有两点,一是提升广告主通过广告获得的收益,二是减少广告对受众造成的打扰。这两点关系到的是广告的有效性,所以我们分析一下广告有效性有哪些影响因素。
一个广告的生命周期大概可以拆解成这么几步:
曝光-关注-理解-接受-保持-决策
简单的说,就是广告先要展示出来,然后能被受众看到,并且理解广告想传达的信息,进一步,能接受这个信息,并且保持一段时间,最终完成对广告所宣传的产品决策(下载、购买、付费等)。
接下来,我们梳理一下每一步的具体影响,以及互联网技术能对其起到什么作用。
曝光:这一阶段主要取决于广告位的物理熟悉,比如一个app开屏位置的广告,和某个隐藏的很深的页面上的一条广告,其有效性必然差别很大。这一点,在现实世界的广告中也是一样的,也没有太多技术上可优化的空间。
关注:即让用户的注意力能够到这条广告上。值得注意的事,「看到」并不等于「关注到」,如果产品设计不合理,很容易导致用户对眼前的东西视而不见。比如说,用户到某个页面上是非常明确的要执行完一个流程的,比如要下载个东西,那么,你在他下载过程中强行插入一条广告,很可能会导致用户完全忽视掉它。所以,这一点和产品设计关系非常大。另外,这也是一个技术能发挥重要作用的阶段。通过搭建DMP平台,结合各种机器学习算法,将广告投放给更有可能感兴趣的用户,从而提升广告的有效性。
理解:这一步影响最大的是广告素材,素材内容设计的好,就能让用户更好的理解。
接受:这一点说白了就是能不能让用户认可你的广告给出来的信息或观点,这个一方面与素材是否专业有关,另一方面和媒体的自身属性也有很大关系,比如丁香医生和某莆田医院官网上放同样的信息,其有效性也必然是大为不同的。
保持和关注:这两步其实逐渐就脱离了广告业态的范围,不过通过一些优质的素材设计、合适的场景、精准的人群投放,还是可以为最终转化做好铺垫的。
此外,在其中这些技术无法直接干预的阶段,提供更完善的数据、更方便的测试方法,也是能给广告效果带来很大帮助的。
接下来,我们再分别从媒体和广告主出发,看看他们各自有什么需求, 以及简单看看ssp, dsp这些平台是怎么做的。由于篇幅限制,这一部分先不详细展开,如果感兴趣的话,我们后边再逐一展开介绍。
服务媒体的平台即常说的SSP, 其主要功能就是帮助媒体提升营收。里边一些基本的功能,比如让媒体配置一个广告位,以及广告位的长宽等信息。 对于提升营收,主要也就是通过合适的人群与广告匹配,优化定价策略等方式,同时设法控制网络等IT成本来进行。
与SSP相对的DSP, 就是服务广告主这一侧的平台。DSP的基本功能就是让广告主设置投放计划,比如在多长时间内,向什么样特征的用户,投放总预算多少的广告。而平台发挥的价值主要是帮助广告主花更少的钱获取更好的广告效果。
直到今天,广告生态中涉及到的每一个模块其实都还在持续不断的迭代中,但万变不离其宗,关键的动力其实就是让广告投放的效果更好,用户也能获得更好的浏览广告的体验。只有这样,整个行业才能朝着健康的方向去发展。
]]>所谓事务,就是保证数据库中的数据都是符合期望的,在不断的增删改查中,数据库会不断的从一个正确的状态变化到另一个正确的状态,而不会被外界感知到不“正确”的中间状态。
举个常见的例子,就是在 A有100元,B有100元的状态下,A要给B转账10元,肯定会有A先转出10元,再转给B,这样的中间状态。事务就是,对于用户来说,只能感知到 A100 B100 和 A90 B110 这两个状态,而不会感知到过程中的A90 B100等等奇奇怪怪的状态。
这个介绍,其实也就是事务ACID特性中一致性的概念。ACID这个说法虽然很流行,但A、C、I、D之间其实并不是平等的概念,简单来说,AID是方法,C是目的。也就是说,实现了原子性、持久性、隔离性,也就实现了一致性,也就实现了事务。
接下来,我们就依次看看AID分别要如何实现。
实现原子性和持久性面临的相同问题其实挺多的,所以把它们放在一起介绍。
先复习一下基本概念,所谓原子性,就是事务内的操作要么都成功,要么都失败;所谓持久性,就是已经完成的操作不会丢失。
值得说明的是,单纯的“实现原子性和持久性”并不存在任何难度,要讨论的问题其实是“如何更高性能的实现原子性和持久性”,(后面要说的隔离性也是一个概念,只有高性能的隔离性才有意义)。
其中一个关键点在于,写磁盘是一个非常重的操作,所以通常会存在一个内存缓冲区,要写磁盘的数据会先写到缓冲区里,再择机落盘。那么假如在事务已提交而尚未落盘的这个时间点,系统出现故障,那么这部分未落盘的数据自然就会丢失,数据库也就失去了持久性。针对这个问题,一个很自然的想法就是事务提交的时候强制刷盘。这个方案可不可行?当然是可行的。但它的问题就是会影响性能。系统出故障毕竟是小概率事件,为了处理这个小概率事件,相当于所有操作都要额外付出一些代价。
实际上为了解决这个问题,一个常规的处理思路就是使用一个commit Log, 也就是在实际写数据之前,先将所有要修改的信息记录在一个log里,如果出现上面描述的问题,系统重启时,会先根据commit log进行数据恢复。而由于写这份log是一个顺序写磁盘的操作,性能会远远好于随机写磁盘,所以这个方式的性能是没有问题的。在数据真正写入之后,再加一个标记,表示这条log已经完成了持久化。
接下来,我们再看一下commit log是否还有优化空间?自然是有的。Commit log的一个重要缺陷就是所有真实的磁盘操作都必须发生在事务提交之后。假如说这个事务非常大,就会占用很大的内存缓冲区,这也会影响系统的性能。改进方案是 write-ahead log 这个机制,这个机制我在之前的文章(https://lichuanyang.top/posts/3914/)里也介绍过,和commit log其实非常像,也是先顺序写一个log文件,唯一的区别就是write-ahead log允许在事务提交之前写入。mysql里的redo log,其实就是一个典型的write-ahead log实现。
讲到这里,我们先暂停一下,回顾一下上面的内容,会发现上边其实基本都在说持久性,原因是对于上边的机制来说,原子性其实都是自然而然的事情,commit log写进去了,这条事务就相当于完成了;commit log没写入,这个事务就相当于不存在。但是使用了write-ahead log的话,情况就不一样了,一个事务会涉及多次磁盘写入,所以也就不满足原子性了。因此,需要引入别的机制来保证原子性,undo log就是实现这个目标的一个典型思路。当变动数据写入磁盘前,必须先记录undo log,注明修改了哪个位置的数据、从什么值改成什么值等,以便在事务回滚或者崩溃恢复时根据undo log对提前写入的数据变动进行擦除。
像在mysql中,实际上也就是像我们上边讲的那样,利用redo log和undo log实现高效可靠的持久性和原子性。
如何实现不同事务之间的隔离,一个很自然的思路就是加锁,实际上常规的数据库实现也就是这么做的。一般来说,有这么几种类型的锁:读锁(也叫共享锁),写锁(也叫排他锁),范围锁。
对于一条数据,只有一个事务能持有写锁;不同的事务可以同时持有读锁,数据被添加读锁后不能再添加写锁, 添加写锁后也不能再添加读锁;范围锁则是对一个范围加写锁,在这个范围内都不能写入数据。
我们知道,数据库有四种常见的隔离级别:可串行化,可重复读,读已提交,读未提交。其区别其实就是加锁粒度的不同。
如果我们把所有操作能加的锁都加上,实际上就是串行化的操作了。这种方式隔离性当然很好,但性能就没法说了,所以一般也不会有人使用。
可重复读则是对涉及到的数据加读锁和写锁,并持有到事务结束,但不会加范围锁。这样就会出现幻读的问题,即一个事务内执行两次范围查询,如果这两次查询之间有新的数据被插入,就会导致两次范围查询的结果不一致。
读已提交和可重复读的区别是他的读锁会在查询操作结束之后立刻释放掉,这样,在事务执行过程中,已经查询过的数据是可以被其他事务任意修改的,所以也就会有不可重复读的问题。
读未提交级别下,则完全不会加读锁。这样造成的问题是,由于读操作时不会去申请读锁,所以反而会导致能够读到其他事务上加了写锁的数据,也就会出现脏读的问题。
其实说到底,隔离性和性能就是一对相互矛盾的需求。加锁加的越多,隔离性自然越好,性能也自然越差。我们需要根据实际的使用场景来决定锁究竟要加到什么程度。
而另一个思路,则是再看看有没有锁以外的方式,考虑28原则,看看有没有什么办法,牺牲20%的性能去解决80%的问题。具体来讲,涉及隔离性问题的场景,其实可以简化为一个读事务+一个写事务,和两个写事务,这两种场景。大部分场景下,当然是读+写的情况更多,所以我们找个方式去解决读+写场景下的幻读问题。相信很多人已经猜到了,这种方式就是MVCC。关于MVCC, 网上介绍资料实在太多,我们就不再赘述了。
经过上面对于事务几个特性的介绍,相信大家已经对本地事务有了非常深刻的认识。如有问题,欢迎留言讨论。下一篇文章,我会继续讲一下分布式事务的相关知识,如果感兴趣,可以关注我的个人博客、知乎或者公众号追更~
]]>首先,我们理一下整个开发流程中的必备步骤都有哪些,我大概罗列一下,包括项目的创建、开发、code review、部署测试环境、部署灰度和线上环境、查看线上监控、日志、以及排查问题等。
根据这整个流程链条,我们再来理一下为了尽可能的提升开发和运维效率,我们需要把哪些方面保障好,以及这些方面可以有什么低成本的解决方案。
有几个点吧:
接下来,我们就看一下这些问题分别怎么解决。
首先,在本文的标题中,其实已经假定了部署环境就是kubernetes, 所以,我先说一下为什么只有kubernetes这一个选项。随便翻开一个kubernetes的介绍资料,我们都可以看到很多它的优势说明,这里我们不多赘述。而其中最关键的,也是每个人都能切身体会到的好处,其实就是它提供了完善的自动扩缩容机制。可以非常简单的设置一个cpu消耗的期望值,k8s集群就可以根据目前cpu的负载去调整pod数量。我对比了我们启用自动扩缩容前后的数据,每天的机器成本可以相差接近600美金,占总成本的1/3还要多。
由于k8s本身的运维成本是很高的,推荐直接购买云服务商的服务,无论aws还是阿里云,都提供了很完善的k8s集群功能。
另外,推荐使用 kuboard (https://kuboard.cn/) 这个工具对k8s进行管理。kuboard是一个图像化的k8s管理工具,包括部署、配置、扩容、登入pod等常见操作,都可以在图形化界面下操作,使用很方便。
一般基于spring initializr创建一个项目就可以。对于常用的日志、监控等各类配置,建议整理一份最佳实践并做一个模板项目。新项目可以基于这个模板项目来创建,可以利用mvn archetype或者类似的工具让这个过程更加顺畅。
至于一些常用的数据库、缓存等基本代码,感觉spring全家桶已经足够好用了。
对于code review, 我们可以使用gitlab的webhook, 当代码提交时,自动给项目组的人发通知。
对于springboot项目,可以用buildpacks 打成docker镜像,而不用再考虑docker file的细节。具体的buildpacks产品,我们用的是paketo buildpacks, 其他的诸如cloudfoundry,heroku也都差不多,目前没有还研究这些包有什么具体需求。
接下来,我们仍然可以利用gitlab的webhook, 触发一次jenkins的构建任务。在jenkins的构建任务重,我们可以完成打包和部署到kubernetes测试集群这些操作。
推荐使用 prometheus + grafana 套餐即可,关于prometheus的使用,以及在springboot项目下的配置,可以参考我之前的文章 https://lichuanyang.top/posts/28288/ 。
在k8s上,可以装一个weave cloud agent, 然后就可以配置对prometheus接口进行自动抓取了。
在grafana里,可以直接写promeql配置监控报表。 另外,grafana官网上,有大量的别人共享出来的图表,可以直接使用。
在grafana里,也可以配置各种各样自定义规则的报警。如果使用飞书的话,在飞书里配置grafana助手,可以很容易的将飞书作为grafana的报警通道。
可以使用loki (https://grafana.com/oss/loki/)作为日志收集、查询的工具。loki可以认为是一个轻量级的ELK, 其维护成本会比ELK低很多。
对于java的项目,使用神器arthas可以解决绝大多数问题排查的需求。对于arthas的接入,可以采用arthas springboot starter这种方式 。而对于k8s上的pod, 如果环境能够和办公环境打通,那可以利用k8s的port forward 功能,将arthas的端口转发到本地来。 当然,这样做的话,务必要控制好权限。
关于金丝雀部署的作用和实现方式,可以参考我的另一篇文章 https://lichuanyang.top/posts/30764/
总结一下, 对于运维来说,只需要维护一些诸如gitlab, kuboard, prometheus, grafana, loki之类的基础设施,而且基本都是一些维护较简单的工具。在此基础上,我们辅助以合理的流程和技巧,就能实现一个非常好的开发体验。
]]>2021年里,最大的变化还是来自工作,由于所在的行业一夜之间直接在风口上消失,原本在一家公司长期发展的愿望落空,所以我也被迫重新开始了新的职业规划。一个关键的考量点就是要去大公司还是小公司。我在前边为什么说自己走到了一个重要的岔路口上,其实最关键的一点原因就是感觉无论做哪个选择,在这个年龄其实都不会有什么回头路了。大公司和小公司对一个资深员工的能力要求,差别实际上是非常大的。到了这个阶段,不会再有刚毕业那几年随时大小公司左右横跳的机会。
为了做这个决策,我问了自己几个问题。
在大公司,个人能力会比在小公司成长的更快吗?
在大公司,生活是否会比小公司更舒适?
我在将来是否还需要一份新大公司经历做背书?
在大公司能获得更多的经济收入吗?
经过慎重的考虑,对这几个问题的回答其实都是否。个人成长看个人,我实质上不是一个会受环境影响太大的人。对于work life balance的问题,按理说其实和公司大小没太大的关系,只是凑巧国内的几个大公司这方面都不太行。背书的问题,因为我本来也有大厂的经历,包括综合学历和之前的其他经历,我觉得是不太有可能在这方面吃亏的,另外,根据这一波找工作,以及这两年面试别人的感受,我也会觉得项目本身的含金量,会比在哪个公司更重要一些。至于收入,看大环境、看公司的变化情况、看老板风格、看运气,总之不会和公司大小有太大的关系。
想来想去,感觉去大厂的唯一作用,就是说出去面子上好看一些。所以,也就把这个决定做下了,就是彻底放弃大公司的想法,而是去寻找一些氛围好、前景优良的创业公司机会。说到这儿,有时候我其实非常感激上天的眷顾,仿佛她总会在我一些关键节点上适时的给我抛出一些合适的选项。当然实际上主要是感谢猎头小姐姐提供的信息和整个过程中的帮助,让我来到了一个各方面都非常满意的新公司。在新公司也快4个月了,这段时间包括做事的空间、个人成长、WLB、经济回报,其实都是高于预期的。
在新的工作时间下,也有了更多的时间陪女儿。小姑娘最近也处在一个语言的爆发期,基本每天都会说新的话,生活的幸福感也到了一个非常高的阶段。
另外,我每年初都会定一些个人成长上的目标。2021年初,我的关键目标其实就是扩大和巩固自己的能力圈,争取掌握一些新的技能,并巩固已有的技能。具体来说包括本职工作相关的技术和管理能力提升,提升商业认知和投资能力,以及建立一些底层的同样技能。
本职工作上,阅读了周志明老师的《凤凰架构》这本书,这本书的关键作用是帮助我建立起了更完善的知识体系,也补全了一些知识上的盲区。学习了极客时间上王争小哥的设计模式专栏,对设计模式有了更深刻的理解,也在公司内做了一次设计模式主题的分享。此外,对于用了很久都没太认真了解过的elasticsearch, 做了一次集中的学习。
对于管理,今年内学习了一些理论知识,也在设法将理论知识运用到实际工作中,感觉还不错。
投资上,今年实际上做了一些非常错误的尝试。今年初开始,我在试图自己去分析公司的基本面,从而找到合适的投资标的,读了很多书,也看了很多大V的经验。我的目前结论是,这个事情的门槛是非常高的,通过自己的学习去达到专业人士的投资效果,是不现实的。基于此次尝试,我的投资收益在今年第一次变成负数。而在未来,我也会放弃个股投资,转向彻底的基金和指数定投。但是我倒不会停止对商业和投资知识的学习,因为我感受到了这些知识对于我思维方式的转变,实际上是会有不错的收获的。
关于底层能力,我今年着重培养的是”清晰力“这么一个能力。这个概念来自于《认知觉醒》这本书,意思是说我们的恐惧很多时候是“模糊”,即没有想明白要面对的究竟是个什么事。而一旦能对事情有清晰的认知,会发现事情并没有那么难。当第一次看到这个概念的时候,我立刻就感觉到我自己一直以来在这方面上问题很大,所以就着手在这方面上进行刻意的练习。效果也很不错,无论是工作还是生活上的事,处理起来都感觉得心应手了很多。
总而言之,这一年对我来说,是一个关键的抉择之年,也在向着人生的目标稳步前行。感谢所有曾经帮助过我的人。希望2022也继续加油。
]]>首先介绍一下金丝雀发布是什么,金丝雀这个名字,起源是,矿井工人发现,金丝雀对瓦斯气体很敏感,矿工会在下井之前,先放一只金丝雀到井中,如果金丝雀不叫了,就代表瓦斯浓度高。
在系统层面的具体含义就是,发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试,启动的这个新版本应用,就是我们的金丝雀。在金丝雀上测试没有问题后,再将线上的流量切换到新版本上。
再具体点讲,可以将一个域名映射到两组服务器上,一组是正式的线上环境,另一组就可以是金丝雀环境了。
假如说我们不是用k8s做部署,而是直接部署到多台服务器上的话,金丝雀部署就很简单,任意指定一台或多台机器做金丝雀即可。
而在k8s环境下,由于k8s接管了部署的流程,我们就需要做一些别的事情来实现这个效果。
有一种很简单的思路,就是创建两个deployment, 但是通过label关联到同一个service上,就可以将同一个service的流量分配到两组容器中了。两个deployment可以分别部署,也就可以部署不同版本的镜像了。
例如下边两个deployment
1 | apiVersion: apps/v1 |
1 | apiVersion: apps/v1 |
只在第一个deployment中配置service,其配置的selector规则是app:nginx, 然后两个deployment都打上app:nginx的label.
这样就实现了我们所说的效果。
当然,这种方式只是实现了一种非常简单的金丝雀发布流程,是没有办法做更精细,比如说按用户信息去灰度的规则的。对于同一个用户,也有可能一次请求到了金丝雀中,下一次请求又到了正式环境中。假如需要更加精细的灰度规则,可以考虑采用spring cloud, istio等工具。
]]>首先,我们来思考一下,如果要做一个类似prometheus的监控系统,都有哪些难点,比如
带着这些问题,我们就来看看prometheus是怎么设计的。
让我们先从历史说起,prometheus最早由SoundCloud开发,后来捐赠到开源社区。在2016年假如CNCF, 即云原生计算基金会。Prometheus是CNCF的第二个项目,仅次于kubernets。 因此,可想而知,promethous在整个云原生体系中有多么重要的作用。Prometheus也逐渐成了云原生下监控系统的事实标准。
对于一个监控系统来说,核心要解决的问题其实就三个:
对于这三个问题,prometheus都给出了很巧妙的解决方案。
romethous的数据模型,简而言之,就是一个「时序」的 Metric数据。所谓metric, 就是数据的测量值,而所谓时序,就是这些metric, 会源源不断的产生不同时间点的数据。
Metric有唯一的名称标识,也可以设置多个label, 可以用于过滤和聚合,其格式如下。
1 | <metric name>{<label name>=<label value>, ...} |
这样,对于任何业务,我们都可以将监控数据设计成统一的metric格式。这样对于promethous来说,方案可以足够简单,只用处理这一种数据格式就可以。而同时又足以方便的应对千变万化的业务场景。
Prometheus提供了 counter, gauge, histogram, summary 四种核心的metric, 不过其区别仅体现在client端和promQL中。截至目前(2021.11), 不同的metric 类型在 prometheus server 这一侧并不会有什么区别,
Prometheus server会定时从要监控的服务暴露出的http接口上抓取数据,是一种典型的拉模型。
相对推模型,拉模型会有一些好处,比如更容易监测某一个节点是否正常;更容易本地调试等。当然,对于一个监控系统来说,采用推还是拉,其实并不是一个主要问题。
Prometheus的数据是典型的时序数据,prometheus本身会将数据存储在本地磁盘上。要注意的是,本地存储不可复制,无法构建集群,如果本地磁盘或节点出现故障,存储将无法扩展和迁移。因此一般只能把本地存储视为近期数据的短暂滑动窗口。
而关于持久化存储的问题,prometheus实际上并没有试图解决。它的做法是定义出标准的读写接口,从而可以将数据存储到任意一个第三方存储上。
Prometheus定义了功能强大的promQL, 可以满足各种复杂的查询场景,具体可参考 https://prometheus.io/docs/prometheus/latest/querying/basics/
一个开源项目的发展,当然离不开周边生态的发展。而prometheus目前已经有了很完善的生态,在java, go, python等主流的开发语言下,都有完善的client包可以使用; 像spring中,可以很容易的为多种组件增加打点,这一点,在下边的实战环节我们会细讲;在kubernetes中,可以轻易的配置自动去各个节点抓取prometheus数据;借助grafana等工具,也可以配置出多种多样的报表。
教程的接下来一部分,我们会以springboot项目为例,来看一看prometheus的实际效果。
其核心思路就是使用spring-actuator 为springboot应用配置监控,并以promethous的结构暴露出来。
首先,引入依赖
1 | implementation("org.springframework.boot:spring-boot-starter-actuator") |
然后添加spring配置
1 | management: |
这个配置里,其实做了几件事:将数据以prometheus的格式暴露出来;自动为http请求添加histogram监控;增加一个application标识,这个标识会作为一个label出现在所有metric中。
之后,启动springboot项目,并且访问/actuator/prometheus路径,就可以看到大量metric, 比如
1 | # HELP executor_pool_size_threads The current number of threads in the pool |
其中,除了我们显式配置的http监控,其实还有大量的jvm, 机器负载等基础的监控信息。
除此之外,对于其他组件的监控也很容易添加,诸如线程池、http连接池、自定义监控等,可以参考 https://github.com/lcy362/springboot-prometheus-demo
这样,无论这个springboot项目如何部署,无论是用java原生的部署,还是用docker部署,还是部署在kubernetes上,都可以非常容易的获取各个监控metrics数据。
]]>ip地址库的数据可以从http://download.ip2location.com/lite/ 获取。可以写一个简单的定时任务,定期去拉最新的数据。由于ip地址的更新不是非常频繁,因此一个月左右更新一次就足够了。
1 | ``` |
数据示例如上,每行数据是一个ip地址的段,包括段的起始地址和对应国家的代号。原始数据实际上还包含地址段的结束点,不过实际上我们拿到的数据都是连续的数据段,因此我们完全可以省略掉结束地址,以节约内存。
实现逻辑很简单,将数据全部导入到一个数组中,然后写一个二分查找就可以了。
项目整体使用springboot实现,可以直接运行springboot, 也可使用docker运行。
使用thymeleaf实现了一个极简版的页面。
访问 http://ip-country.lichuanyang.top/ , 简单体验工具效果。
项目源码在https://github.com/lcy362/ip-country, 如果觉得还不错,麻烦给一个star (^▽^)。
]]>iterm2 (https://iterm2.com/) 是mac下使用非常广泛的一款终端替代产品,提供了很多强大的功能。要实现ssh书签,实现免密码登录、自动登录的效果,关键点是其中的三个特性:profile, trigger 和 password manager.
profile顾名思义就是一套配置,像我们正常打开iterm2时,其实就是打开了default profile. 配置profile的入口就在工具栏 profiles 选项下,可以增加或编辑现有profile. 我们将需要的profile的general标签下的 commond 模块修改为 Command, 内容填入 ssh命令, 比如 ssh root@1.1.1.1, 就可以在打开profile时自动执行ssh命令。 profile中其他的文本、颜色等配置都不重要,可以按需填写。
trigger也是profile的一个特性,入口在profile配置页的advanced标签下,它的作用就是利用关键词触发一个动作,我们现在要做的就是用password这个关键词触发打开 password manager。 操作很简单,就是增加一个trigger, regular expression 填入 password, action选择 open password manager, 注意勾选instant和enabled两个选项。
最后一个要配置的是password manager, password manager 就是一个密码管理器,是item2中会默认安装的一款插件,入口在工具栏 window 标签下。打开password manager, 将需要保存的密码都录入进去就可以了。
这样,我们就实现了在iterm2中用“书签”保存远程服务器的地址和密码。使用时,直接访问对应的profile, 等待password manager 弹出,选择对应的密码记录,点击输入就可以了。
]]>写这篇文章,是希望从选题等方面出发,通过一些比较基础的规则,提升技术分享的下限,争取让80%的人都能做出一次80分的技术分享。
首先要了解到的是,线下的技术分享不是分享知识的唯一形式。其他的,比如写博客写文章,拍视频,或者简单的分享一两句话,都是常见的分享形式。针对不同的知识,要考虑用什么样的方式进行分享更加合适。
比如,对于某项技术的入门介绍,推荐以文章的形式进行。读者可以进行快速的浏览或者对关键信息进行检索,要比坐下来听一遍效率高很多。
通常来说,比较好的选题需要有较强的实用性,能够吸引到听众,且难度适中。能够让听众在听得懂和有新收获之间达到一个平衡。
举一些比较好的选题例子,以及一些注意事项:
对于分享效果,虽然会受各方面的影响,但归根结底最重要的还是分享内容中是否有足够的干货。
为了能够有干货, 最好的方法还是平常的积累,建议平常多思考,对平常工作中用到的东西建立起更深的认知。推荐阅读《卡片笔记写作法》这本书,平时有持续输入,才能有输出产生。
此外,
准备过程中注意思考和总结,形成自己的观点。单纯对其他人的东西做搬运,实际上造成的是自己时间的浪费;
最好能将实际的应用场景融汇到分享内容中;
比较难的知识点,注意分享技巧,让听众能听明白,可以考虑先就分享的关键部分,先找一两个同事试讲;
通过看视频,除了可以了解分享内容本身,也可以了解到其他人是怎么讲的,对分享效果比较有帮助。
除了分享的真正内容外,建议增加以下内容的准备:
预习资料。可以提前准备一些有助于理解分享内容的资料,比如某本书的某几章,某篇文章等,要求大家提前阅读。分享者需要给出每个材料的预计阅读时间,单次分享整体需要的阅读时间不宜超过1个小时。
对背景的详细阐述,尤其是涉及到一些业务知识或者不常见的场景时。
准备好分享内容后,需要找人进行review,reviewer可以是自己的mentor, leader, 或者其他在相关领域非常熟悉的人。
reviewer在充分理解分享内容的基础上,主要对选题和分享内容进行审核,确保满足本文列举的其他要求。同时尽量保证分享内容不要有技术性的错误。
分享材料一定要提前发出来,让听众有提前熟悉的机会。
分享时注意语速控制,不宜过快。
]]>要理解什么是云原生,就要从云原生的名字说起,云原生的英文名是cloud native, 很显然,其含义就包含云和原生两部分。云就是说应用是运行在云上,而非本地。原生,就是说应用要以最适合云的方式去运行,而不是仅仅从本地迁移到应用上。
那么,什么样的应用才是适合云的呢?其实就是能最大化利用云的能力,发挥云的优势。
而云计算的核心优势,其实无非就是将更多的资源集中管理,统一调配,也就更方便按需灵活配置资源,提高资源利用率。
类比一下,相信很多人用过storm等流式框架,它们的优势是什么呢?其中一个重要的因素就是可以将一个复杂的流程拆解成若干个子节点,每个节点可以根据其需求配置不同的并发度,并发需求高的节点可以获得更多资源。这样,资源利用率也就提升了。
对微服务来说,也是类似,将不同功能拆分成不同的服务,就可以单独对更小粒度的功能单独做扩缩容。
值得注意的是,拆分不仅包含拆分不同的业务,也包括将业务代码、三方软件(第三方库)、非功能特性(高可用、安全、可观测性等三类代码进行分离。
单纯业务的拆分,实际上从软件开发的非常早期的阶段就在进行了。而伴随着云原生概念兴起的趋势,正是将云应用中的非业务代码部分进行最大化的剥离,从而让云设施接管应用中原有的大量非功能特性(如弹性、韧性、安全、可观测性、灰度等),也就是所谓的service mesh.
由于云上的资源和应用并不是强绑定的,为了能更方便的利用资源,我们需要一种更通用的运行形势,让应用可以和运行环境有一定程度的解耦。这就是容器技术。容器提供了一种逻辑打包机制,以这种机制打包的应用可以脱离其实际运行的环境。利用这种脱离,不管目标环境是私有数据中心、公有云,还是开发者的个人笔记本电脑,您都可以轻松、一致地部署基于容器的应用。容器化使开发者和 IT 运营团队的关注点泾渭分明 - 开发者专注于应用逻辑和依赖项,而 IT 运营团队可以专注于部署和管理,不必为具体的软件版本和应用特有的配置等应用细节分心。
另一方面,服务更小粒度的拆分之后,系统本身的复杂度显然会有所提升,比如本地调用变成了网络请求,调用链也无法通过代码结构体现。因此,运维上需要更加智能化和自动化,要保障单个服务更强的稳定性;同时需要一个强大的监控系统,要能够分析出各个微服务之间的依赖关系,还要快速检测出系统中的异常。
同时,在单个服务规模更小,且监控数据很完善的前提下,我们有可能去更频繁的去部署,甚至每次更改之后直接部署到生产环境。如果部署有问题,我们可以通过监控及时的发现,从而将损失控制在更小的程度。而小规模的部署,也让我们更容易去定位问题或者回滚。
从上边的分析中,我们可以整理出和云原生相关的一些关键词,比如服务化、弹性、可观测、韧性、自动化等等,这些关键词可以被总结成4类,即微服务、DevOps、持续交付和容器化。
这4类的关键特征如下:
微服务:可被独立部署、更新、重启、scale
DevOps: 自动化、快速、开发运维协同
持续交付:频繁发布、快速反馈
容器化:逻辑打包机制
上边讲了很多理论上的知识,那么,要采用云原生的话,有什么具体的执行路径呢?可以从以下几个方面考虑:
我是流沙,希望通过这篇文章,可以让大家更清晰的理解云原生究竟是什么。其实,云原生说起来很简单,就是采用各种方式去更好的利用云上的资源。而具体说起来,它又是一套非常庞大的体系,涵盖了从开发到运维的方方面面。欢迎大家关注我的公众号(Mobility) ,或者个人网站, 我会在后续的文章里逐渐展开讲讲云原生的方方面面。
]]>在读这本书之前,我强烈建议先认真思考一下你和工作的关系究竟是什么样的。如果你就是公司的老板,那没什么好说,公司的收益就是你的。如果你不是,那就要理一理你创造的价值去了哪,是有了经济收益,有了能力的提升,有了将来可以成为自己能力证明的资历,还是帮老板多买了两辆跑车呢?
之前读过一篇文章,说是将你自己当做一家公司来经营,将任职的公司当做自己的客户。我觉得这是一个很好的思考的思路,你需要考虑你这家「公司」,是立刻就去追求现金收入,还是先打磨能力与口碑,以期将来去获得更大的收益?要考虑与任职公司的交易是否公平?
想清楚了这些,再来读《干法》这本书。否则,你可能会觉得这本书是马老师写的。话说回来,马老师确实也在践行这本书的很多理念,在他当英语老师的时候。没有当年“业余”时间的辛勤奋斗,哪里来今天的福报呢?
好了,下面进入正题。我会从三个方面讲述书中传递出的工作相关的价值观,一是工作的意义,二是如何对待工作,三是工作方法和态度。
就像我在文章一开头说的,努力工作,创造价值实际上是埋藏在人类内心深处的一种非常重要的精神诉求。可以了解到,很多已经财务自由的人,依然在寻求一些工作机会,或是创业,或是找个安逸自由的工作。这就是内心深处的这种诉求在驱动人的行为。
除了通过工作获得的经济收益以外,
一心一意工作,精益求精,本身就是磨炼人格的修行,促进我们成长。劳动可以带来喜悦感,让人明白生活的意义,劳动是高贵的行为。
如果你能接受「将自己当成一家公司来经营」这样的设定,你会发现,眼前的经济收益其实没有那么重要。而个人的成长、工作中带来的幸福感是更有意义的收获,而在收获了这些之后,经济收益也会自然而然的随之而来。
大多数人初出茅庐只能从自己不喜欢的工作开始。
这个世界上,真正将自己的爱好当做工作的,本身就是少数。在这些少数之中,绝大部分人工作了之后,也会感到这个事情已经不是自己的爱好了。因为作为一个爱好,和作为一份工作,这之中的差别是非常大的。游戏很多人都爱玩吧,很多人会羡慕那些职业的电竞选手,认为他们就是在做自己喜欢的工作。但是关注一下电竞圈,会发现不好好训练的人大有人在,有些人会抓住各种时机,去玩别的游戏。原本是因为喜欢这个游戏做了职业选手,最后却发现自己渐渐的不喜欢这个游戏了。
所以,做不喜欢的工作,才是这个世界的常态。而我们要做的,是让我们喜欢上工作本身。如何让自己喜欢上工作,作者给我们提供了一些方法。
付出了努力,才会让自己喜欢上工作。
为工作中的小小成功感到欣喜,将其带来的能量当做动力,更加努力的工作。
首先,要以高目标为动力。
能力要用“将来进行时”
可以想象一下,如果把一段时间之前的自己放到现在,是不是也觉得自己现在做的事情很难?这是一个正常的现象,当你处在一个正常的发展路径下的时候,能力的提升在不知不觉中就会产生。
有了高目标,就要持续的付出努力,要去思考目标怎么样才能实现。
一定要去想。不认真思考,就什么都实现不了。
自己的成长,只有自己能够负责,指望其他任何人都是不现实的。我最近看脉脉上的吐槽,很多人对公司的不满是「没有成长」,这就让我非常迷惑,成长和公司究竟是什么关系呢。这个要说回到人和公司的关系,公司作为你的一个客户,你完成一些事情,并且获得一些报酬。这和个人成长有任何关系吗?成长是需要自己去思考的,而不是等着别人派下来,说这些这些干完,你就成长了。我能想到的一个人在一家公司里会没有任何收获的唯一场景,就是他已经理解了这家公司的所有方面,可以轻而易举的经营另外一家公司来打败这家公司。
另外,要有完美主义。
橡皮绝对擦不掉错误
很多错误,即使你事后弥补了,也必然会留下一些痕迹。无论做什么事情,要尽力去将它做到,至少是自己认知范围内的,完美。像开发一个需求,要思考完整整个流程,确保没有逻辑上的缺陷,也没有实现上的bug, 而不是看一遍产品稿就简简单单的实现一遍,剩下的所有事交给测试。
工作的结果=思维方式热情能力
在书的最后,作者给了一个公式。对于工作来说,思维方式和热情是和能力一样重要的。而在你具备这种认知的前提下,提升热情和改变思维方式,要远比提升能力更简单。所以,想提升工作的效果,其实没有那么难,关键是要建立起对于工作的合理的认知。
我是流沙,感谢阅读这篇文章。如果觉得写的还不错的话,请多多点赞关注。如有问题,也可以在个人博客、微信公众号( Mobility ) 等平台和我交流
]]>关于评审的价值,首先对于团队来说,最大的作用其实就是提升团队的下限。试想一下,如果各个环节都不进行评审,而是直接产品交付一个产品稿,开发就对着开发,任何个人的疏忽都会给整个项目带来无尽的风险。多次的评审环节,实际上就是将一个业务方案,分别用产品稿、代码、测试用例等形式描述,再分别引入更多的人来检查,避免一个人的疏忽造成业务上的风险。
此外,本文的重点,我实际上是想讲讲除了给团队带来的价值外,对于一个个体的人来说,参与评审有什么价值。
首先说一下对于被评审的人,即产品稿、代码等的提交人来说,评审就是一个其他人来帮助你提升的过程,是难得的能获取直接的反馈信息的机会。其实对于很多人来说,相比于学生时代,工作时代学东西的最大难点就是没有人批改「作业」,看了书看了视频之后,没办法确认自己究竟学没学会。而及时的反馈,无论学什么,都是非常重要的。而在参与评审的人中,会包括你的上级,能力更强的同事。他们能够详细的了解你的方案、代码的话,相信是能够给出很多有价值的反馈的。评审,实际上就是给你自己提供一个机会,能够接收到他们的反馈信息。
这里其实隐含了一条对于提交人的要求,就是要能够清晰的表达出自己的方案,这样别人才能低成本的参与到评审中来。比如写代码,要运用合理的设计模式,写出易读的代码。写文档,要有合理的布局、分段,突出方案的重点。向别人讲方案时,要考虑听众的知识背景,确保他们可以听的懂你的方案。
评审对于提交人的另一个重要作用,是分担风险和责任。要知道,无论你公司的制度是什么样的,无论你的职位如何,当你负责的项目没有做好时,最终的结果,一定是有相当一部分是由你自己承担的。承担的形式,可能是直接的绩效损失,可能是别人对你的能力产生了质疑,不管怎样,都是大家不希望面对的。而评审,就是将一部分后果,转移到整个团队上。当然,别想太多,主体的责任一定是还在提交人本人身上。这里,同样体现了提交人清晰表达的重要性。提交人需要将自己方案的关键点,更多的让评审人们接受到。如果评审人成功的接收这些信息,并且在评审时进行充分的讨论并最终达成共识,这一部分决策就可以认为是团队共同做出的了。
接下来再来看对于评审人来说,积极的参与对其他人的评审,又有什么作用。首先的价值还是一个学习的机会。所谓三人行,必有我师,从别人的方案里,一定是能够发现一些非常好的点,值得自己学习的。当发现别人的方案和自己的设想不一致时,是一个进行深度思考的机会,可以对自己进行一个连续的追问,看看到底哪种方案比较好,直到得出一个无法质疑的结论。这种深度的思考,对于自己的思考方式会有很好的改善作用。
另外,参与评审也是一个快速提升经验的方法。参与评审,实际上是花费评审的时间,得到一个无限接近于 自己做了这件事 的效果。评审的足够细致的话,你就会对这件事怎么做,有一个非常全面的认识,这件事也就完全可以作为你项目经历的一部分。
本文到这里就要结束了,不知道大家能否通过本文建立起对为什么要做评审、怎么做评审的认知。大家可以通过 公众号( Mobility ), 个人网站 等和我交流。
]]>职业选择,最重要的无非就是城市和公司选择。城市选择是一件非常主观的事情,比如我,作为一个北方人,既不喜欢南方城市的气候,又想离家近,还需要大城市的机会,北京就几乎成了唯一选择。当然,给大家的建议,功利点的话,建议选择一些快速发展中的强二线城市,比如重庆、合肥,这样随着城市平台的发展,你自己身处其中,身价也会提升。
接下来,我会限定在北京的范围内,结合我自己的经历,说一下公司的选择。
提到北京,一个不得不提的问题就是户口的问题。不过,我强烈不建议你依据一个户口就决定了整个人生怎么走,比如明明不想进体制内,却为了个户口就去了。务必在考虑户口问题之前,优先想好职业发展方向的问题。
很多人会纠结于体制内/体制外、大公司/小公司这些明面上的选择,但这些其实都只是表象而已,同样是体制内,也可能有天差地别的变化。一个正确的做法是想清楚自己真正想要的,然后再去寻找合适的工作机会。对于什么是自己想要的,确实不是每个人都能想的清楚。所以,我把这个问题拆解一下,看看工作的选择究竟意味着什么。大家可以思考以下几个问题:
对于上边的任何一个问题,任何回答都没有高下之分,比如挑战困难问题并不比做常规工作更优越,这个只是大家根据个人价值观做出的选择而已。
这些选择的任意排列组合,相信都可以找到合适的机会。比如不期待从工作中有所得,不想工作占据太多人生,也愿意做常规的工作,那就可以考虑一些边缘的体制内单位;期望有权力,可以让工作占据很多生活时间,更想通过工作积累资源,就可以考虑核心的、有实权的体制内单位;而如果期望挣钱,更希望通过工作提升个人能力,程序员就是个不错的选择。
想好了上边的问题,就可以选择公司了。很多时候,我们可能无法找到并顺利的进入一个满足所有要求的地方。这也没关系,我们可以利用跳槽,每次解决其中一部分问题,并且持续的搜集信息和提升能力,寻找到合适的机会,并有能力获得这个机会。
我以自己的经历讲一下我的几个关键选择是怎么考虑的。
刚毕业的时候,我的期望是获得一个北京户口,并且能留在互联网行业内。那个时候我的选择有两个大方向,一是去互联网公司竞争ssp offer, 但是当时的能力确实不足,所以这条路很难走的通;另一条路就是在体制内寻找业务接近互联网场景的机会,最好是面向普通用户的C端产品,因为这样可以有充足的用户量和数据量,让我不至于离业内先进的技术太远。很幸运,我找到了这样的机会,也成功的抓住了。入职之后,也基本如我所料,虽然公司技术水平并不高,企业文化也有很多问题,但是业务场景是非常好的,我也有充足的自己发挥的机会,借助工作,也学到了很多东西。
接下来,我要解决的问题就是大厂经历,所以接下来跳槽的时候就只考虑了几个一线大厂。关于工作本身,倒没什么好说的,这段经历虽然过的很不开心,但是希望达到的目标也达到了,所以也没什么遗憾的。
再下一次跳槽,我期望解决的问题有三个:大幅提高收入,抹平因为在国企呆的时间比较长造成的和同龄人的收入差距;别加班太多;人际关系轻松一些。这三个问题看似矛盾是不是?但是用心去找总是能找到合适的机会的,我也就这样来到了现在的公司。当然不是所有问题都解决的非常好,但是已经足够让我的工作体验非常幸福了。
通过我自己的经历,其实大家也可以看到,我总是似乎在提互相矛盾的要求,总是想鱼与熊掌兼得,而且最后也能幸运的都得到。其实这世界上公司那么多,并不是说有哪两个条件是必然互相矛盾的。大家没有必要过早的就舍弃什么,比如做程序员就一定意味着996,意味着放弃生活吗?显然不是。
上边可以衍生出两个比较细的问题,我也一起说下吧。
一是要不要选择北京户口。我先说一下我通过这个户口获得了什么吧,首先是赶上了15,16年那波房价大涨,让家庭资产在我并没有什么理财观念的时候,也获得了大幅增值。这一点我觉得放在今天,已经不成立了,房住不炒的大前提下,就算北京的房子,也不可能跑的过股市。第二点,就是一些手续办的方便一点,这个我觉得基本可以忽略不计吧,一年估计都办不了一次事,而且越来越多的手续也支持异地办理了。第三点,就是可以放心的让孩子在北京接受教育,不用担忧未来的不确定性。为什么我说是不确定性,因为我觉得接近二十年之后,非京的孩子是否依旧不能在北京高考,是有很大的不确定性的。北京的高考难度会卷成什么样,也是有很大的不确定性的。北京户口,只能说让我个人未来的选择固定了下来,但这个选择未来会是什么效果,其实很难说的准。
所以说,当前来看,北京户口的价值其实非常有限。回到2015年的话,我仍然会毫不犹豫的选择要北京户口。但是如果让我在2021年的今天做这个选择,其实我大概率是不要的。
第二点,是关于大厂经历。这一点,我的看法是必须要有,而且尽早有,但是没必要一辈子都呆在大公司里。大厂经历的价值,其实很多人已经说的很清楚了,所以我也不想再赘述。感兴趣的话可以私下找我沟通。
上边的篇幅里,主要说的是方向的选择。选定了方向之后,还有个重要的问题是具体公司的选择。面对一个公司,其实考虑两个问题就可以:
以我的第一份工作来说,获得的就是户口和互联网的业务场景。同时会失去大公司的学习资源、优良的公司氛围、高薪水等等,这些问题都需要自己想办法去解决。
好了,上文已经讲完了本文的大部分内容。有个点,我一直没有提,就是中年危机的问题,可能会有人问。我为什么不提呢,因为我不觉得这是一个能靠选择解决的问题。有些人会觉得体制内很稳定,即使天天混日子也没人能把你怎么着。但是我不觉得这种依赖外部条件的事情能算稳定,历史上体制内不是没出过问题。走这条路或许你有99.99%的机会一辈子过的安安稳稳,但是如果那万分之一的可能性发生了,你打算怎么办呢。而做程序员就不一样,或许只有50%的机会能平稳的呆在一家公司,但是随着工作提升的能力,让我可以很安心的应对另一半可能性。解决中年危机的唯一方式就是提升自己,不管是提升能力还是积累资源,你总要从工作中有所得,才能持续的保持自己的竞争力。
我是流沙,大家可以通过公众号( Mobility ), 个人网站 等和我交流。
]]>先说输入, 这个我先说结论,当且仅当输入参数过多时,才应该做封装。其他情况下完全没有做封装的理由。
对于将输入参数包装成request类的理由,无非是下边这些
首先,第一条完全就是个伪命题,第一次看到这种论断的时候,我也差一点被绕进去,但是仔细思考一下,就会发现这个完全不成立。
即便使用request体的形式,如果接口提供方增加了必传的字段,调用方仍然需要增加这个参数才能调用,那相比于直接传参数,没有解决任何问题。而这种问题实际上也是无法解决的,因为就不应该让这种情况出现。接口提供方有义务保证后续的修改是向前兼容的。而对于增加的是非必传字段这种情况,即便不包装request类,也可以通过适配器的形式实现新接口并兼容老接口,后续再找机会异步的将老接口下线。
第二条的话,确实存在一定意义。
而将输入参数包装成request类的最大问题就是会破坏代码的可读性,会造成语义不明确。本身类似 queryById(long id) 这么一个方法的语义是非常明确的,扫一眼接口定义就可以看清楚输入输出。但是强行封装之后,这个接口究竟要输入什么就不是那么明确了。同时,如果request中有三个参数,是否传入任意组合都能返回结果呢?这也是靠看接口看不明白的。而正常的用参数定义的话,可以使用适配器的形式定义一系列接口,接口调用方也更容易理解。
封装输入参数的唯一条件就是传入参数过多,这时候需要通过其他的方式,比如注释、文档,去约定接口的合法输入值和升级方案。
接下来说输出,将输出结果封装成一个response体,相对于封装输入参数,会合理一些,但也不能乱用。一般将输出结果封装的目的就是包一层返回状态码。
这一点实质上就是强迫调用方来了解接口的异常情况。我们知道,基本的设计原则中有一条叫「接口隔离原则」,其具体描述是 Clients should not be forced to depend upon interfaces that they do not use。对于这儿的情况要类似,如果调用方确实不关心请求成功或者失败的原因,那就不应该强迫调用方来了解这个。比如我就是要查个数,只存在查到或者查不到两种情况,并不想关心到底为什么没查到。
所以,在设计接口的时候,需要慎重的考虑接口的异常原因是否是一个应当提供出去的信息,在此基础上,才能说保障response合适或者不合适。
综上所述,盲目的一味去封装所有输入和输出参数,一定是不可取的。对于何时应该做封装,何时不应该,务必要做慎重的考虑。大家有什么想法的话,也可以跟我沟通。
]]>首先,我想说一个很多人的认知,就是认为做底层的开发更容易学到东西,成长更快。我觉得这就是业务开发同学的臆想而已,底层开发没有大家想的那么利于学习,业务开发也没有大家想的那么不利于学习,原因我会在后边展开说。而之所以很多人有这样的认知,很大程度上是因为底层开发的招聘标准通常会高一些,所以这些人本身的学习能力和意愿就是要好一些。
其实底层开发的日常和业务开发也没有太大区别,都是在做需求。只是业务需求的来源是产品经理,底层需求的来源是其他开发,或者底层开发者本人的思考和想法。这里,我要说自己为什么不愿意做底层开发的第一个原因了,就是需求的来源不科学。首先,如果只做底层开发,而脱离了实际业务场景的话,对于技术的认知很可能是有偏差的,这样导致自己想做的一些事很可能是在自嗨,并没有实际应用价值。而如果从其他那些搜集需求呢,也会有类似的问题,业务开发的同学提出的意见可能也就是他自己的非常浅的想法而已,不一定经过了很完善的考虑;再者,业务同学对基础组件究竟该干什么也未必理解准确,他可能分不清楚什么事应该基础组件解决,什么事应该自己解决。
在这,引入了我不愿意做底层开发的第二个原因,就是职责边界不明确。这个和配合的业务方的能力强相关。有时候,自己有些事没做好,业务方也能找到办法解决;而有时,明明和自己没关系,也要被迫处理一些上层的逻辑。
大家也可以看到,上边两个问题都极度依赖同事的能力,所以,如果你一定要做底层开发的话,一定要去技术能力储备强的公司,这个直接关系到你的工作幸福度。公司的选择还影响一件事,就是工作内容,简单的说,就是要造轮子,还是完善开源工具,两类工作可以说是天差地别。造轮子,需要对各种底层知识很熟悉,确实是很能促进进步,这个也是“做底层有利于成长”这种想法的来源。但是这种事仅存在为数不多的几家大公司中。其他大量的完善开源工具的工作,其实和业务开发没有任何区别,只需要了解一下开源工具的边边角角,就可以完成大部分工作,既没有深度,也没有广度。我开篇的时候说底层开发并不能帮助学习,原因也就在这。
所以,要做底层开发,公司的选择至关重要,大,技术能力强,两个条件缺一不可。大家肯定会问,我去这样的公司做不就行了吗。话是这么说没错,但这带来的问题也就来了,这也是我不想做底层开发的第三个原因,职业选择面太窄。国内java底层做的好的也就某里某团两家而已,这两家的企业文化大家也都清楚。所以,不想去这样的公司,是不是就不干了?即使你能接受,巨大的供需量对比,也会带来疯狂的内卷,就像算法岗那样,你是否有足够的把握赢得这场内卷游戏呢?而对于业务开发来说,大厂经历只是一段必要的学习经历和自己简历上的一个亮点,当你对大厂的整个规范流程熟悉了之后,就可以自由的选择想去的公司了。
前边讲底层开发说的太多了,下边进入正题,就是业务开发的价值。在我看来,业务开发带给我的能力提升主要在三个方面:业务知识的积累,技术广度的提升和综合能力的提升。
先说业务知识,业务知识其实就是对现实世界的理解。对于我个人来说,工作之余,了解一下其他行业的运转模式,首先就是一个很有意思的事情。其次,不断的去了解不同行业的行业知识,我是完全能感受的到对我认知能力的提升的。这种能力能不能变现?当然是可以的。比如去炒股,工作中的积累让我有了分析一个公司商业模式的能力,对于什么样的是好公司好股票,显然可以比一般人理解的深一些。当然,这儿扯的太远了,大家看看就好,关键是看自己的兴趣,有没有意愿去了解一些技术之外的事情。
接下来是技术广度。业务开发和底层开发的最大区别,其实就是底层开发对深度要求更高,而业务开发对广度要求更高。业务开发需要对各种各样的框架、中间件都有所了解,并且不能了解的太浅,也需要一定的深度,要能够理解它们能解决什么问题,又会带来什么问题,适用于什么场景;面对复杂的业务场景和复杂的系统,要能够全面理解;发生问题时,要有能力快速定位到问题在哪。这些除了需要知识的积累,也需要实际场景下经验的积累。
然后说一下综合能力的提升,相比底层开发,业务开发对人的通用基础能力会有更大的锻炼,比如沟通能力、分析解决问题的能力、权衡取舍的能力。这些能力可以说和技术有关,但关联也没那么大,极端的讲,即使你将来转行了,这些普适性的能力也会很有帮助。我时常在想,学着用一个开源工具,和打王者荣耀,究竟有什么区别。作为新手,参考着现成的例子做一个demo, 就相当于第一次接触这个游戏,随便打打。而随着对这个逐渐熟悉,有的人可能就成了没有感情的调包机器,而有的人就可以逐渐去了解底层的实现。就像游戏里,有的人一直浑浑噩噩,打了几千场连电刀和无尽战刃有什么区别都不知道,而有的人就会去探索伤害的数值机制。所以,我觉得大家别把程序员看成一个多特殊的职业,它和其他职业并没有什么本质的区别。大部分职业事实上都是在用不同的方式解决问题,比如律师是依靠法律,基金经理是依靠对市场和基本面的认知,程序员就是靠代码。我们在不断的工作中,锻炼的是解决问题的能力,而解决的方法并没有人要求你一定要限制在技术之内。
我们在业务开发中,就是要不断积累上边三个方面的能力,这样才能滚起雪球,从而从容的应对中年危机。我写这篇文章,当然也并不是要说业务开发就是好,主要还是想帮大家分析清楚业务开发和底层开发的区别,从而做出一个理性的选择。
如果大家有什么想法,也可以来和我沟通。
]]>提到设计模式,其实首先需要理解清楚的是面向对象思想。相信大家即使不能非常清晰的描述出来,对面向对象也应该是比较熟悉的。
我们就快速讲一下,面向对象有四大基本特性:封装、抽象、继承、多态;
封装:仅暴露有限的接口,授权外部来访问。将逻辑集中,因此更可控;可读性、可维护性也更好;易用性也更好。
抽象:隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
继承:好处就是代码复用。
多态:子类可以替换父类。提高代码的可扩展和可复用性。
设计模式是针对软件开发中经常遇到的一些设计问题,根据基本的设计原则,总结出来的一套实用的解决方案或者设计思路。
可以看到,设计模式是非常偏实际应用的,相比设计原则更加具体、可执行。
因此,在了解设计模式之前,就需要了解一些基本的设计原则。这些原则才是指导我们写出好代码的关键。
那么什么是好代码呢?
这个其实很难描述,我们可以试着归纳一些好代码的特征,比如下面这些:
可维护、可扩展、可读、可测、可复用、简洁。
为了达到这些标准,就需要现有一些基本的设计原则。
描述很简单,一个类只负责完成一个职责或者功能。但是实际要做好其实还是很难的。
关于怎么样才算职责单一,可以定出一些简单的标准,比如代码行数过多;依赖的其他类过多;私有方法过多;难以给类起名;类中大量方法功能集中等。但是无法定义出一个普适的标准,一定要结合实际情况去考虑。
比如这样一个类
1 | Student { |
在其中至少有四个字段是和地址有关的,那么是否需要把地址相关的职责抽离出来,封装一个新的Address类?
其实,是要视情况而定的,如果地址这些属性只是单纯的展示信息,那么直接放在Student类里,就没有问题;如果在这背后还有一套复杂的物流系统,也就是有很多地址相关的复杂逻辑,那么这一部分就应当单独抽离出来。
在实际的开发中,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。
对扩展开放、对修改关闭。
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
开闭原则是相对难理解的一条原则。
“怎样的代码改动才被定义为‘扩展’?怎样的代码改动才被定义为‘修改’?怎么才算满足或违反‘开闭原则’?修改代码就一定意味着违反‘开闭原则’吗?”等问题,都很难回答。
而这条原则又是最有用,因为扩展性是代码质量最重要的衡量标准之一。
实际上,我们也没必要纠结某个代码改动是“修改”还是“扩展”,更没必要太纠结它是否违反“开闭原则”。我们应当回到它的初衷上,只要没有破坏原有代码的运行,就没有违反开闭原则。一个简单的判断标准,当功能没有变化时,对应的单元测试没被破坏,就是一次满足开闭原则的改动。
关于如何做到对扩展开放、修改关,也需要慢慢学习和积累。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
实际写代码时可以假定变化不会发生,当出现变化时,需要抽象以隔离将来的同类变化。
子类能够在任何地方替换父类,并且保证原来程序的逻辑行为不变及正确性不被破坏。
里式替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。描述里子类和父类的关系,可以替换成接口和实现的关系。换一种方式描述里式替换原则,就是实现类不能违反接口声明要提供的功能、输入、输出、异常等。
Clients should not be forced to depend upon interfaces that they do not use。
其核心要素就是一个「接口」的功能要尽量单一,调用方只需要依赖它需要的部分,而不需要去依赖或者调用整个「接口」。
这里的接口可以指代不同的含义,比如一组api接口集合(也就是我们用的一个proxy)。
比如有个项目,会做一些业务流程,生成一些数据,然后又要作为基础服务把数据提供出去,这时,最后就是提供了两个接口, 一个是用来处理内部流程;一个则是作为基础服务堆外提供。这样做的好处就是可以一定程度上避免误调用。
也可以将接口就理解为单独一个接口方法,这时候就是说一个方法的功能要单一。
接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,更侧重于接口的设计。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层.
这一原则更多针对框架开发,业务开发里,高层模块依赖底层模块,实际上没有任何问题。
以tomcat为例,tomcat调用web程序来运行,所以tomcat是高层模块。
Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
Keep It Simple and Stupid.
Keep It Short and Simple.
Keep It Simple and Straightforward
KISS原则有不同的表述方式,但是大体意思都差不多,就是要让代码尽量简单。
「简单」实际上是一个很主观的事情,code review是一种很好的间接评价代码是否简单的方式。如果同事review你的代码时,会有很多地方看不懂,这就很可能是代码不够简单。
一些简单的经验:不要重复造轮子;避免过度设计,无论是为了未来不确定的扩展性,还是细微的性能差别。
举个例子,StringUtils中的很多方法,在一个具体的场景中,我们是大概率可以写出一个性能更好的实现的,因为StringUtils中要考虑的很多通用性的问题,在具体的场景中就不需要考虑了。那么,我们是不是要真的这么去做?
这一条本身很简单,不过要注意这里的repeat和单纯代码重复是有区别的。比如两个无关的方法,实现恰好一模一样,这时候就不认为它违反了DRY原则,因为语义上是不重复的。而两个方法,如果方法名到实现都完全不一样,但是是在做一件事,比如isValidIp(), checkIpValid(), 两个方法各自用不同的方法实现了,我们也认为违反了DRY原则。
另外要注意执行重复的问题,比如
1 | void run1() { |
虽然看起来代码封装的很好,但是执行main方法时,a方法执行了两次。如果a是一个开销很大的方法,就会很有问题。
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.
“高内聚、松耦合”是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类、函数。
所谓高内聚,就是指相近的功能应该放到同一个类(模块、系统)中,不相近的功能不要放到同一个类(模块、系统)中。
所谓松耦合是说,在代码中,类(模块、系统)与类(模块、系统)之间的依赖关系简单清晰,只依赖必要的东西。
像qwerty中之前对作业相关逻辑的处理,需要了解每种类型作业的实现细节,就是一种违反了迪米特法则的做法。正确的做法是在作业这一侧定义一个通用的模型,模型中将各类作业的不同逻辑封装好,qwerty则只需要了解这个模型。
对于这些设计原则,不需要花费很多精力来扣它们的字眼。更重要的是,理解它们的思想,理解为什么这些原则能帮助我们写出来「可维护、可扩展、可读、可测、可复用、简洁」的代码。
接下来,我们就可以具体说设计模式了。
经典的设计模式有 23 种。随着编程语言的演进,一些设计模式(比如 Singleton)也随之过时,甚至成了反模式,一些则被内置在编程语言中(比如 Iterator),另外还有一些新的模式诞生(比如 Monostate)
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式,共七种:适配器模式、装饰者模式、代理模式、门面模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
我们只挑其中一些重要的来讲吧。
比如一个需要根据文件格式来创建不同parser,将配置读取到RuleConfig中的功能,简单工厂模式的实现如下。
1 | public class RuleConfigSource { |
也就是创建一个工厂类,专门用来「生产」parser。
这里如果就把创建parser的过程,放到load方法里,其实也不是不行。之所以拆出来,就是要把创建这一部分逻辑拆分开。所以,只有当创建逻辑比较复杂的时候才会使用工厂模式。
上边的实现还可以进一步优化,去除大量的if-else。
1 | public class RuleConfigSource { |
这就是工厂方法模式,工厂方法模式实质上是为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。
工厂模式的原理和实现都很简单,在实现时其实还有很多可以探究的细节,但在这里就不细说了。我们探讨的关键还是理解什么场景下应该用这种模式,它解决了什么问题。
对于工厂模式来说,应用的场景就是创建类的逻辑比较复杂时,比如需要有一堆if-else, 动态的决定要创建什么对象;或者虽然创建单个对象,但是创建本身的过程很复杂,调用方无需了解如何去创建。
建造者模式(Builder), 其实应用非常广泛,我们打开任何一个项目,都可以看到很多XXXBuilder.
建造者模式使用的场景是:一个类有多个可配置项,使用方可以自由选择其中若干个进行配置。
如果采用不同构造方法来实现这种效果,面对的就会说无穷无尽的组合。
而如果采用set方法来配置,那么首先当前类就彻底失去了对配置项的控制,调用方可以随时随地调用set方法。另外,如果需要在配置项被配置好之后进行一些合法性的校验,这种方式也无法实现。
这种情况下的解决方案就是建造者模式。
1 | ResourcePoolConfig config = new ResourcePoolConfig.Builder() |
实现也比较简单。
工厂模式是用来创建一组相关但不同的的对象,建造者模式则是对同一个对象进行不同方式的定制。
关于单例是什么,怎么实现单例,作为经典的面试题,就不多说了。只说一下单例的问题,为什么单例主键变成了一种反模式。
单例的问题:
隐藏类之间的依赖关系;
同时,依赖的直接就是单例的实现,违反了基于接口而非实现的设计原则;
可测性不好: 无法被mock(Mockito中);全局变量有可能导致测试之间互相影响;
不支持有参数的构造函数。
但是,单例模式的优点是有些场景下使用起来更简洁方便,所以也没必要直接就认定不能使用单例。
代理模式和被代理类实现同一个接口,负责在业务代码执行前后附加其他逻辑代码(比如性能监控、计数等),然后用委托的形式调用被代理类完成业务功能,从而实现业务代码和框架代码的解耦。
装饰器模式是通过继承被装饰类,来实现功能的增强。比如java中inputStream系列类,就是装饰器模式的应用。
实现层面,代理模式和装饰器模式几乎一样,但是其意图不同。
代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
还是这个观点,对于设计模式,我们没必要扣字眼深究每种设计模式到底是什么,只要知道有这么一种技巧,可以处理类似的这种场景就可以了。
适配器模式很简单,我们可以直接看例子。
1 | public void debug(String msg) { |
应用场景包括:
兼容老版本接口;
应对不同的输入;
封装有缺陷的接口设计;
…
门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。这里主要是关系到一个接口的粒度应该有多大,从而平衡接口的通用性和易用性。
通常,粒度小的接口通用性肯定会好一些,但这样调用方就需要调用多个方法,必然不太好用。相反,如果接口粒度太大,就会有通用性不足的问题。
门面模式的作用就是解决这个问题。我是做在线教育的,有一个场景是在B端展示各种各样的题目,比如课前的题、课中的题、课后的题,每类中又分很多具体类型,每个类型有不同的数据来源。
这里,就需要权衡接口的通用性和可用性了。而门面模式刚好可以解决我们的问题。首先,我们要定义一个通用的模型,包括题干、参考答案、用户作答等,然后实现一批具体类型的接口。在此之上根据业务场景,封装一些门面层,门面层中只是简单的将子系统的数据组合起来。
定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。
实现方式是定义一个策略接口和一组实现这个接口的策略,运行时可以根据一些条件动态的选择执行哪个策略。
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
比如java的AbstractList 的addAll方法就是一个模板方法。
1 | public boolean addAll(int index, Collection<? extends E> c) { |
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。也就是发布订阅模式。
观察者模式是一个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式,但是思想很容易理解,所以也就不列代码了。像我们使用mq, 也就是在使用观察者模式。
将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。
实际使用中,责任链模式的一种变体应用也很广泛,就是让链上的每个接收对象都处理一遍这个请求。
比如我们有一个拼装数据的逻辑,就是责任链变体的一个应用。
1 | private boolean fillUserDataCollection(Map<Integer, UserDataCollection> userExpandMap, Map<Integer, UserDataCollection> originUserExpandMap) { |
UserDataCollection中包含大量字段,每个fillingService负责其中一部分数据的补充。
责任链的核心思路就是拆分,将大类拆成小类。这也是解决代码复杂度的重要手段之一。
对于如何写出好的代码,最关键的是要先建立起心理上的认知,知道什么是好代码,也有写好代码的意愿。在此基础上,要对面向对象的思想和各类设计原则有充分的理解,知道为什么好代码要符合这些原则。
实践上,写出好代码的最佳方式就是持续重构。一开始业务发展不明朗的时候,代码无法设计到最好是很正常的。而随着业务的发展,我们会逐渐意识到代码里存在的问题,这时候可以翻翻设计模式中有没有什么现成的解决方案,然后应用到自己的项目中。
最后,说一下,这篇文章中的很多内容实际上来自极客时间的专栏《设计模式之美》。我觉得这个专栏是讲设计模式最好的一个专栏,推荐大家也可以去看看。点击下方图片购买即可。
]]>首先,列两个非常没效率的场景。
虽然有夸张的成分,但是这些事也是确实每时每刻都在发生的。造成这些现象的原因很多,我们今天就好好剖析一下。在剖析效率问题之前,我们需要先看看程序员日常都要做哪些事。其中对工作效率影响比较大的,我把他们主要归为三类:
接下来,我们就对这三项逐一分析,如何提升工作效率。
日常开发中都有什么情况会造成效率下降呢,我列举几个:
可以看到,这里边大部分是和代码设计有关的。对于设计,其实就两句话,一是要设计,二是要设计好。
也就是说,首先,你得知道开发之前是得有设计这么个环节的。软件工程是一门科学,它有概要设计、详细设计这些步骤是有道理的。要设计到什么程度再进开发呢?我的观点是越细越好,理论上,除了实际把代码码出来,其他所有事都可以在设计阶段做完。
关于设计的好不好,一个重要的评价标准,就是后续改起来是否容易。如果一个变化的原因会导致多处代码修改,那就是一个不好的设计。如何提升这一点,需要去学习设计模式、架构方面的知识,没有速成之路。
另外,关于注意力的问题,当然大家可以借助番茄钟这样的工具。但是工具永远都只能是辅助,最重要的还是自己去提升主观上的自控力。
接着说沟通这块,沟通分主动沟通和被动沟通。主动沟通就是你自己这边发起,要去找其他人问问题、讨论;被动沟通则是别人发起,来问你问题或者讨论事。
主动沟通的主要问题就是越聊越远,聊着聊着就不知道聊哪去了。这里我给两个建议,一是发起沟通前一定要想清楚自己想通过这次沟通达到什么目的,不能有个模糊的想法就迷迷糊糊的去了,这种沟通就是最大的时间杀手;二是可以列一个沟通清单,沟通过程中话题扩散其实是个常见的现象,我觉得没必要刻意去杜绝这种事,关键是要保证自己本来想聊的聊出结果来,这就需要一个可以用来check的清单。
被动沟通其实是一种很容易打乱工作节奏的事,但是很多情况下无法避免,比如突然又线上问题要处理,领导有事找你,等等。我们能做的就只有减少不必要的被动沟通次数。比如自己提供的功能,写好文档和注释,其他人就不需要靠问你就能看明白。方法名、返回值等团队内要建立起一些固有约定,大家都遵守,也就不需要问才能了解了。
另外还有一点是做好任务的拆分,做若干个小任务,而不是做一个大任务。这样,当被迫打断工作时,「切换上下文」的成本会低一些。
这也是很重要的一个方面。对有些人来说,查Bug改Bug的时间,可能会远高于开发新需求。对于这个,我们的主要目标就是降低排查难度,减少排查时间。可以从下面几个方面做。
设法尽早暴露问题,比如通过写单元测试。同样一个问题,出现在一个小模块里,和出现在一个复杂系统中,排查难度是不言而喻的。
要有足够多的信息,比如日志,比如各类监控数据。就像我们体检一样,只有先看得到那些异常指标,才能再去针对性的详细排查。否则什么指标没有,就只能两眼抓瞎乱查了。
再一个就是排查方式,每个人都需要逐渐积累经验,建立起相对固定的排查问题的套路。比如我,习惯就是遇到问题先把各种能看到的信息都扫一遍,从中发现异常指标,指标看不懂的话就借助搜索引擎或者他人来理解,然后根据指标思考有什么可能原因,再逐步去验证。这里边很重要的一点就是什么样的问题要自己思考,什么样的问题应该借助于他人或者搜索引擎。像上边小A的做法,就是非常不可取的,问别人或者搜索引擎的问题,一定是非常具体的,你随手一个报错发过来,别人首先得花大量时间搜集信息。一个1小时搞得定的问题,强行去问别人,很可能要花掉两个人一下午。
什么叫具体呢,比如遇到一个ClassNotFoundException, 如果你压根不知道它是什么意思,那去搜一下没有问题。而假如你已经知道它的意思就是找不到类,那再去搜索就没有任何意义,这是要做的是思考这个类为什么找不到,有哪些可能原因。
除了上边这些之外,其实还有一类原因,就是996大环境大家主动选择的摸鱼。对这一点,我还是建议大家快速干完活之后腾出时间提升自己,摸鱼无益。再一点就是用脚投票,多去找没有加班文化的公司、团队,用自己的努力让他们发展的更好。
大家有想法也可以在评论区或者公众号( Mobility )和我交流。
]]>我们先列一下当我们在说「复杂」的时候,到底在说什么。
上边列的这些问题,其实很多之间是互为因果的,比如模块划分的不好,导致了代码逻辑看不懂,也导致了代码没法复用,代码没法复用,又导致了大量重复代码,重复代码又进一步导致代码逻辑看不懂。
所以,下面我们尝试着规整一下这些问题,看看问题到底在哪。
我觉得,首先可以把代码复杂度的问题简单分个类,一类是代码本身的问题,一类是代码对现实世界的表现力的问题。前者主要关乎代码整体的整洁程度,以及容不容易进行持续的维护;后者则主要影响代码容不容易理解,别人看代码时是否需要大量的背景知识。
对代码本身问题来说,以我的观察,导致代码过度复杂的首要原因就是模块拆分不清晰,甚至没有模块拆分。像上边列的重复代码、无法复用、统一逻辑多处修改等问题,本质原因都在模块拆分。另外有一些命名上的问题,会导致代码进一步不可维护。
关于代码对现实世界的表现力,我觉得一个很大的问题就是代码和现实割裂太严重,导致单纯依靠业务上的背景知识,并不能理解代码的实现方式;同时,当你以一个不符合现实的设计去实现现实需求的时候,可想而知,每一步都会很难。比如说,一个展示用户机票的业务,上游各航司的数据key都是「用户证件号」,那你这儿的key应该是什么呢,也是证件号吗。如果这么做了,然后再如果做了分库分表,可以想一下,「查询用户最近一张机票」这么一个基础功能,实现起来要有多复杂。
其实理到这儿,会发现已经能基本涵盖一开始列的那些问题了。像bug多这类问题,其实是逻辑复杂的一种表现。因为逻辑复杂,看不懂,所以Bug才多。
另外,有一个问题是上边说的每个点,随着程序规模的增加,会出现一个指数级的放大效应。相信很多人可以感受的到,如果有一个5天工作量的需求,和5个1天工作量各自独立的需求,最后做下来,前者实际花费的时间和最后产生的bug都要多很多。
所以,接下来我们就对上边这些提供一些解决方案
首先,重中之重,就是做好拆分,要做好每个层次的拆分,比如微服务的边界怎么划分,哪些内容放到一个类里,哪些内容放到一个方法里。不夸张的说,模块拆分的好,可以解决开发上80%以上的问题。
比如,「把大象装进冰箱需要三步,一打开冰箱门,二把大象放进去,三关上冰箱门」,这就是一个非常合理的模块拆分。当然,对于三步中的每一步来说,都可以进一步细化。比如打开冰箱门包括:握住把手,用力拉两步;把大象放进去,包括,找一个大力士,举起大象,放到冰箱里,松手,四步。
拆分的核心要素是要拆的合理。对于业务逻辑来说,就是要符合对现实世界的认知。比如上面的例子里,你不能强行包一个「用力拉,找大力士,举起大象」这样的方法。但是,这个现象,在实际的业务开发中,实际上非常常见。很多人看到一个方法太长,知道要做下拆分,结果只是随机的从代码里拉住一块,抽了出来。这样,实际上只能让代码看着好一点,无法达到降低复杂度的效果。
另外,拆分没有止境,每个子模块,都可以进一步拆分成更细的子模块,直到这个模块已经足够容易理解。
在这儿说内聚,主要就是说要把复杂度控制在最小的范围内。以上边的例子来说,「找大力士」这一步就是最复杂的,因为根本没有这样的大力士。但是,这儿的复杂度只是这个小模块的内部实现,只有这一个小模块复杂,在上层或者其他模块是不应该感受到这个复杂度的。
但是,实际开发中,我们经常看到一些底层很复杂的结构,不断的暴露在系统各个层次、各个模块中,导致复杂度无法收敛。这样,随着项目规模的扩大,整体的复杂度就会呈指数级上升。
这个就比较细节了。命名的核心要求一是要准确,无论是变量名、方法名,看到这个名字,不需要去看代码细节,就能大体了解相关的逻辑。
再一个,命名要具备语境下的一致性。比如程序员,你不能一会叫coder, 一会叫programmer, 否则别人理解起来就很混乱。
其实,到这儿已经就说完了,是不是感到比预想的要简单。事实也是如此,只要做好模块的拆分,合理的将每段代码写在它该在的位置,开发就没有那么复杂。
]]>第一个,是北京西城德胜片区有一个小学叫三帆附小,因为可以直升三帆中学,这个在西城数一数二的初中,所以一直十分火爆。这个学校今年因为教室不够,招生由七个班缩减到了四个班,所以卡了落户四年,即落户不足四年的都要调剂。其实,调剂也不差,因为调剂校里有西城排名前五,而且学位极度充足的西师附小,也有雷锋消息这样的优质直升校。甚至有人买三帆附小学区房就是为了去调剂,因为西师附小的学区房要更贵。
然后,在一个公众号的高赞评论里,看到了「德胜」今年卡落户「两三年」这样的言论,言论作者还顺便以此为依据表达了对买学区房这种行为从智商上的鄙视。
结合事实和最后言论,其实大概猜的出整个传播链条中信息的变化。德胜的三帆附小逐渐变成了德胜,卡落户四年变成了落户两三年的被调剂,又变成了卡两三年。同时,调剂去的是西师附小这种关键信息被丢失。
第二个事,上周望京地区除了新冠的病例,然后各个社区在周末组织全员的核酸检测。其中有个检测点,不知道咋回事,排队的人特别多。不过实际上大部分检测点都挺好排队的,有的点甚至你到了还要等一会,凑够一组5个或者10个人才能测。
然后,这周我恰好听到一个财经类的大V说这个事情,信息变成了「望京地区」几万人寒风中排队做核酸。其实,他说的确实也没错,也没啥恶意,只是提一嘴疫情又有些严重了。不过大家也能感受到,稍微少了那么一些细节,整个感觉就不一样了。你看这个简版的表述,会感觉望京的小伙伴们还挺可怜的,但是实际上,大部分人都轻轻松松、开开心心的就检测完了,至于排大队的那个点,也随时可以去其他点检测。
所以,看这两件事,虽然都是很小的事,我也彻底对二手信息失去信任。有些信息,传播的过程中,遗失或者微调一些关键信息,会导致整个意思大不相同。再者,有些人说话的时候其实只是顺嘴一提,人家没把这个当成一个重要的事,所以也不会去深究信息到底准不准。但是听的人对这个事情重要程度的判断是不一样的,你如果拿这种信息去做决策,可以预见,会有多惨。
那么,什么算是一手的信息呢,我这里简单列一些:
总而言之,就是希望大家多去接触一手信息,杜绝二手信息,做重要决策的时候要尤其重视信息的可信度。
]]>其实从年初孩子出生就在考虑买学区房的事情了,不过自己本来房子要到明年初才满五,没法赶在西城731政策之前上车了,算是有点小遗憾。
首先针对要不要买学区房,我比较认同网上的一类观点:买房子实际上是在买圈子。买的是你的邻居是些什么人,你的孩子要和什么人做同学。同样价格,有的人会选择学区好一点,住的差一点,有的人会想住的舒适一些,学区差点。想了想,我还是愿意和第一类人混一起。
因此最后和爱人定下了基本的基调:在保障基本居住质量的前提下,去追求能力范围内最好的学区。算了下预算,期望控制在850万左右,最高不超过950万。户型别太差的小两居就可以。
首先做了一轮初步筛选,海淀大部分地方因为离我们俩工作地都太远,所以都不再考虑了。金融街的价格压力太大,因此首先将范围缩小到西城的月坛和德胜两个片区,先看看房子情况。
十一期间先去月坛看了次房子,主要是育民对口的小区,白云观、白云路、汽南、真武庙啥的。说实话,心理落差非常大,第一印象真的就是脏乱差,几乎就要放弃这个地方了。
所以之后两周的看房重点,主要就在德胜了。往德胜跑了好多趟,几乎把德胜的小区看了个遍。整体来说,小区环境第一感觉确实比月坛上次看的要好。中间一度几乎要定了,但是二次看房的时候,又感受了一遍德胜的小胡同,觉得无法接受,遂放弃。这样的话,也就意味着放弃了德胜大部分小区,只留了育翔对应的普天大院,三帆附小对口的裕中东里中的几栋楼,西师附小对口的六铺炕一二区做备选。不过这三个地方,都有比较大的难受的地方,普天大院和六铺炕房子太老,三帆附小对口房子太多,交易量又大,731之后调剂风险过高。
考虑了这几点之后,觉得心里确实会有个疙瘩,再加上觉得德胜学区属性太重,不像月坛本身有地段加成,站岗的风险也有点大。因此继续等这几个小区新出房子的同时,又把东城和朝阳纳入到了考虑范围内。主要是东城的安交和和平里片区,朝阳的陈经纶珠江帝景、朝阳外国语。还没有去实地看房的时候,先在网上看了下。存量房中,珠江帝景只买的起全北的,只能先不考虑了。另外一个综合看倒没什么大问题,但是居住品质并没有感受到相比西城有明显的提升,所以觉得不大合适。
所以,这几个还是仅仅放到了备选项里,有非常满意的房子出来的话可以考虑。然后又回月坛看了几次房。神奇的是,这几次,观感明显要比第一次好的多。不知道是习惯了还是经纪人的问题,可能第一次把月坛最烂的房子带我们看了个遍?
这几次看房过程中,也基本熟悉了月坛各小区的情况。但是真的没有什么各方面都满意的房子。所以跟几地的中介都说了情况,决定等新房源出现再看。
幸运的是,10月底就有一套各方面都非常好的房子,有小区环境,2000年的房子,在周边是最新的,近地铁,户型利用率也比较高,朝向、采光什么的也都可以接受。当然,除了价格。不过经纪人还是建议我们去约房东聊聊。当天从下午四点一直谈到十点,业主大哥确实是个很爽快,也很好的人,但是双方价格差距确实大,再者房子是他父母的,他只能一遍遍打电话去做父母的工作,最终也没谈成。
只能说,绿中介确实给力,业主大哥也确实给力,我们晚上回去之后,中介又去做大哥工作,大哥又去做父母工作,最后在快12点的时候答应了我们的报价。然后半夜12点,我们又跑回去迁合同。
这个过程,我也基本摸清了中介工作的套路哈哈。就是先想办法把双方约过去谈,然后硬磨,等双方心理都快崩溃了,价格就好谈了。^_^
现在回想起一个多月的整个历程,还可以淡定的描述出来。其实,那个时间里,心理压力其实是非常大的。一方面选房子就很难选,同时其实还很纠结,又怕买在高位,又怕多校政策执行的太狠。现在尘埃落定之后,心态其实还可以,毕竟已经尽可能降低风险了,只能尽人事,听天命。房价站岗和被调剂到渣小,这两件事别同时发生,对我来说就是可以接受的。
当然,还是期待有个好结果^_^。
]]>从书名可以看出,这本书其实讲了两个相对独立的话题,一个是文明、现代化,一个是价值投资,并分别阐述了这些和中国的关系。
作者首先用了很大篇幅讲述了文明产生与演化的整个过程。其中尤为精彩的就是对于农业文明天花板的论述。也就是农业文明的产出,本质上来源于光合作用。植物通过光合作用生长,而喂养牲畜也需要消耗植物。而光合作用产生的能量上限,受制于土地面积和土地的单位产出,这两者都有非常明显的上限,所以自然资源也就有了上限。有限的自然资源和近乎无限的人口增长,决定了人口最终只能通过非自然灾害来消化。
现代化的产生源自新大陆带来的环大西洋自由市场经济和同时代的科学革命。1776年是一个重要的年份,这一年发生了三件事情,对现代化的发展都起到了至关重要的作用。一是亚当斯密出版了《国富论》,《国富论》讨论的核心问题是大西洋经济的本质,即自由贸易;二是独立宣言发表,一个深受亚当斯密理论影响的全新政权诞生,对于自由市场的发展也会提供非常大的帮助;三是蒸汽机的产生,生产力得以大幅度提高。从此,自由市场和科技的结合,释放出惊人的力量。
自由市场本身是一个可以不断自我进化、自我进步、自我完善的机制。现代科技的介入使得这一过程异常迅猛。这样,在相互竞争的的不同市场之间,最大的市场最终会成为唯一的市场。
任何组织如果离开这个市场,都会不断落后,一如当年的苏联。
我国这几年的很多举措,诸如一带一路,RECP,都是在尽力提升能够影响到的自由市场规模。我们要感谢川建国,不断退群,自己离开这个最大的自由市场。
那么,现代化是否有可能诞生在中国呢。答案是否定的。因为现代化的诞生过程中,私人资本一直起着至关重要的作用。而中国自秦汉起,就一直有着非常强有力的中央政府,私人资本难以兴起。另外,新大陆的发现,其初衷原本就是要寻找中国。中国作为当时的文明中心,并没有动力去寻找更富足的地方。
可以说,现代化的产生原本就源自一系列非常偶然的因素。原本野蛮的盎格鲁撒克逊人,开中了这张彩票,得以弯道超车。但历史终究会回到她应该在的位置上。所以我们完全没有必要遗憾现代化为什么没有在中国产生。
过去几十年间,中国的快速发展,其实是在走其他人走过的路。这种情况下,一个强有力的政府可以起到非常重要的作用。而随着我们逐渐赶上了其他人的进度,需要去探索下一步的方向时,自由市场将会逐渐发挥出优势。
文化上,在中国,恢复传统文化的正确地位其实别无选择。文化的产生,有很深的历史和地理渊源,早已深入骨髓。随着经济发展,人们的精神需求不断提高,很可能会出现中国式的文艺复兴,让国人重新发现中国文化最精华的部分。
总有一天,大部分国人会认识到,二十四史、唐诗宋词、四大名著、论语、道德经、孙子兵法,这些渊远的文化遗产,是要远比“感恩印第安人被我们杀”这样的文化优质的多的。
说起投资,不得不看看当前整个行业的状态。当前投资业最大的问题当然就是没有办法评价一个产品的好坏。投资是一个长期的过程,短期的涨跌可以说没有任何参考价值。而当前基金的盈利模式,导致业绩很差的基金经理同样可以收很多钱。
作者讲到了一个重要的观念,也就是受托人责任。只有行业内大部分人都能有这个概念时,行业才能向好的方向发展。
长期来看,股票的收益远高过其他任何类型资产几个数量级。在中国,这个情况也不例外。有两方面的原因,一是通货膨胀,二是来源于经济增长,股票事实上可以很大程度体现出GDP的增长。
价值投资的基本理念只有四个。
投资的收益来源于公司的发展,你之所以能获得收益,是因为你给公司的发展带来了帮助。这不是零和游戏。而靠赌市场的波动盈利,这是投机行为,是和赌博没有区别的零和甚至负和游戏。因此,价值投资是一条正道、大道。我们为社会创造价值,然后获得收益,仅此而已,我们不希望通过欺骗、通过剥削别人去获取收益。
市场只是一个为你提供服务的机构而已。你可以在这里获得购买的机会,也可以在要用钱的时候卖出。市场透露出的其他信息,诸如一支股票的短期涨跌,纯粹是噪音而已。
投资本质是对未来的预测,而预测就必然不可能百分之百正确。因此,提升安全边际,就尤为重要。
能力圈是可以通过长期努力训练获得的。在建立能力圈之前有一个前提条件,是我们需要对自己的能力圈有所认知,能够识别自己的能力边界。这需要我们对知识有一个诚实的态度。当我们有一个观点时,需要找到最聪明的持反对意见的人,并且能够反驳他的观点,这样才配说有了这个观点。
关于如何建立能力圈,对每个人来说确实是不一样的,这个取决于每个人一贯的学习方法和学习能力。不过李录先生也给我们提了一些建议:1. 以所有者角度看生意 2.慢慢积累 3.用兴趣和机会去主导研究 4.多实践,选一家公司,以投资的心态把它彻头彻尾研究个透。
最后,说一下价值投资对人的本性的一些要求。
这本书是从俞敏洪创立新东方之前说起的,俞敏洪一开始是北大的老师,在和学校产生矛盾离开了学校。当时恰逢出国留学的热潮,俞敏洪原本打算考给别人培训GRE攒够钱就出国留学,最后却在机缘巧合下,将新东方越做越大。
简单来讲,新东方的发展过程分为这么几个阶段:
从这本书,我们看到了新东方从一个小的培训班逐步发展成一个国内领先的教育机构的经历,从中也有很多的感悟。
看所有成功人士的传记,会发现他们都有一个共同的特质:不给自己设限,不会说我只能干这些事,干不了那些事。俞敏洪也不例外。新东方起源于俞敏洪的英语确实非常好,赶上了出国留学的热潮。但今天回过头来看,会发现俞敏洪的英语能力在新东方的整个发展过程中,只是占比很小的一部分。
自俞敏洪开培训班开始,他就要备各个学科的课,要考虑如何去营销。随着公司发展,要不断去学习公司的管理,学习财务知识,不断的解决各种各样的新问题。
俞敏洪在书中也常说,一开始他不懂公司管理,不懂财务,往往都是在有了问题之后才去想如何去解决。这里解决这些问题,就需要自身能力的不断成长,自己学习或者引入外部力量,这都需要不断扩充自己的能力圈。
在职业发展的过程中,我们的工作边界上肯定会有很多不熟悉的领域。如果对这些领域抱着一个开放的态度,就可以很自然的扩大自己的能力边界。能力边界扩大了,自己就会有更多的机会,而在这些机会中,又能接触到新的领域。这样就形成了一个正向的循环,促成自己事业发展和能力圈的双重提升。
当然,在成长的过程中,运气因素是不可或缺的,我们看到的这些成功者的经历本身都带有幸存者偏差。新东方的发展过程中也的确有几次可以归结于运气好。但是,我们可以设想一下,假如去除那些运气因素,俞敏洪老师的发展路径,可能攒够钱就出国了,之后大概率找到一份很不错的工具;也可能把培训一直做下去,即便没有政策上的利好,只要他不断去扩充自己的能力圈,我想也足够他自己活的挺好的。
简单来说,大的成功靠命,但如果我们能一直保持自身的成长,保证一个挺舒服的小康生活,还是可以的。
新东方毕竟是一家教育公司。我恰好也在一家在线教育公司工作,所以格外关注对于教育业务的讲述。看完这本书之后,关于教育最大的感悟还是,不管你针对什么用户,做哪个学科,用什么形式,所有的一切的根本都只能是教学的质量,只有教学本身能支撑一家机构一直走下去。或许你可以靠大规模的营销来短期获得大量生源,或许可以通过销售技巧忽悠用户续报更贵的课程,有很多种途径可以短期内获得很高收益。但是,谁都不傻,用户们很快就会用脚投票。
对于一个组织来说,合理的规则是必不可少的。关于规则,最重要的两点,一是要有,而是要合理。
新东方从家族企业向正规企业转变,就是一个规则从无到有的过程。之后无论董事会的成立,还是分校的发展,确定分校的考核机制,则是规则不断发展的过程。规则制定的好了,公司就能不断发展壮大,达到一个企业和员工双赢的局面。制定不好,公司就会不可避免的走向崩溃。好的规则,关键点就在于能让员工的个人利益转化成公司利益。人的适应力都是非常强的,你说用分校的收入来考核分校,那么分校就能找到很多邪魔外道实现收入增长,但这过程中把公司的声誉消耗没了。好的规则下,员工其实还是在追求自己的利益,但在这过程中自然而然的就促成了公司的发展。
此外,其实还有很多零零碎碎的感悟,比如新东方发展过程中的各种商业模式,像大班模式,像早期的营销手段;又比如标准化对于一个企业做大的重要性。总之,这本书虽然只是一本自传,还是干货满满的,值得好好的去读几遍。
本文翻译自https://martinfowler.com/articles/patterns-of-distributed-systems/ ,原作者对目前各类企业级架构中使用的多种分布式系统进行了总结,从中提取出了一些通用的“模式”(pattern)。本文作为系列文章的第一篇,介绍了分布式系统的特点和一些常见问题。 建议好好阅读一下本文以及英文原文,对于分布式系统设计和分布式架构理念,会有非常大的帮助。
分布式系统给程序设计带来了特殊的挑战。 它们通常要求我们拥有多个数据副本,这些副本需要保持同步。 但是我们无法期望各节点能永远可靠地工作,同时,网络延迟也很容易导致不一致的现象出现。 尽管如此,许多组织仍依赖一系列核心的分布式系统来处理数据存储,消息传递,系统管理和计算等。 这些系统面临很多共同的问题,这些问题也可以通过类似的方案来解决。 本文将这些解决方案定义为模式,通过这些模式,我们对于如何更好地理解,交流和教授分布式系统,将建立起更好的认知。
在过去的几个月中,我一直在ThoughtWorks进行有关分布式系统的研讨会。研讨会中面临的主要挑战之一是如何将分布式系统的理论映射到诸如Kafka、Cassandra之类的开源项目中,同时还要保持讨论足够通用,能涵盖更广泛的解决方案。模式的概念提供了一个不错的出路。
“模式”这个结构,本质上使我们能够让我们专注于特定问题,从而很清楚地说明为什么需要特定的解决方案。然后,对解决方案的描述允许我们给出一个代码结构,这个结构足以具体到能够表示出实际解决方案的程度,但同时又足够通用以涵盖各种变化。模式还允许我们将各种模式链接在一起以构建一个完整的系统。这样,我们就有足够的词汇表,来讨论分布式系统的实现。
接下来是在主流开源分布式系统中观察到的第一组模式。我希望这些模式集对所有开发人员都有用。
现在的企业架构中有很多天然分布式的平台和框架。如果我们在当前典型的架构抽样查看平台、框架,会看到下面这么一些东西。
Type of platform/framework | Example |
---|---|
Databases | Cassandra, HBase, Riak |
Message Brokers | Kafka, Pulsar |
Infrastructure | Kubernetes, Mesos, Zookeeper, etcd, Consul |
In Memory Data/Compute Grids | Hazlecast, Pivotal Gemfire |
Stateful Microservices | Akka Actors, Axon |
File Systems | HDFS, Ceph |
这些系统都天然是分布式的。关于什么样的系统叫“分布式”系统,包括两个方面:
当多个服务器参与存储数据时,有几种出错的可能。 上述所有系统都需要解决这些问题。 对这些问题,这些系统的实现方式中有一些重复的解决方案。 以更通用的形式理解这些解决方案,有助于理解这些系统的广泛实现方式,并且在需要构建新系统时也可以作为很好的指导。
模式,是Christopher Alexander提出的,在软件社区中被广泛接受的概念,用于在软件系统设计时记录设计架构。 模式为解决问题提供了一种结构化的方式,可以通过多次查看和验证来解决问题。 使用模式的一种有趣方式是能够以模式序列或模式语言的形式将多个模式链接在一起,这为实现“整个”或完整的系统提供了一些指导。 将分布式系统视为一系列模式,是深入了解其实现方式的有效方法。
当用多台服务器存储数据时,会有几种出问题的方式。
由于硬件或软件因素,进程在任何时刻都可能崩溃。有若干种进程崩溃的方式。
最重要的是,如果进程负责存储数据,则必须将它们设计为能够对存储在服务器上的数据提供持久性保证。 即使进程突然崩溃,它也应保留所有成功给过用户ack确认消息的数据。 根据访问模式,不同的存储引擎具有不同的存储结构,范围从简单的hash表到复杂的图结构都有。 将数据刷新到磁盘是最耗时的操作之一,因此无法将每次插入或更新都立即刷新到磁盘。 因此,大多数数据库具有内存存储结构,内存中的数据定期刷新到磁盘。 如果进程突然崩溃,可能会丢失所有这些数据。
一种叫做Write-Ahead Log的技术就是用来解决这个问题的。服务器将每个状态更改作为命令存储在硬盘上的仅附加(append-only)文件中。 append操作通常是非常快的,因此可以在不影响性能的情况下完成。 顺序写单个日志文件,以存储每个更新。 在服务器启动时,可以重播日志以再次建立内存状态。
这提供了持久性的保证。 即使服务器突然崩溃,然后重新启动,数据也不会丢失。 但是,在服务器恢复之前,客户端将无法获取或存储任何数据。 因此,如果服务器发生故障,我们会缺乏可用性。
一种显而易见的解决方案是将数据存储在多个服务器上。 因此,我们可以在多个服务器上复制预写日志。
当涉及多个服务器时,还有更多的故障情况需要考虑。
在TCP / IP协议栈中,通过网络传输消息时所引起的延迟没有上限。 它会根据网络上的负载而变化。 例如,一个1 Gbps的网络连接可能会被定时触发的大数据作业淹没,从而填满网络缓冲区,并且可能导致某些消息在不确定的延迟后才到达服务器。
在典型的数据中心中,服务器打包在机架中,并且通过机架式交换机连接多个机架。 可能会有一棵交换机树将数据中心的一部分连接到另一部分。 在某些情况下,一组服务器可以相互通信,但与另一组服务器的连接是断开的。 这种情况称为网络分区。 服务器通过网络进行通信的基本问题之一是,何时能知道特定服务器发生了故障。
这里有两个问题要解决。
为了解决第一个问题,每台服务器都会定期向其他服务器发送心跳消息。如果没有收到心跳,则将对应的服务器视为已崩溃。心跳间隔足够短,能够确保不需要花费很长时间就能检测服务器故障。在最坏的情况下,服务器可能在正常运行,但是被集群整体认为已经宕机,并继续运行。心跳这种方式可以确保提供给客户端的服务不会中断。
第二个问题是脑裂。在脑裂的情况下,如果两组服务器独立接受更新,则不同的客户端可以获取和设置不同的数据,当脑裂的问题被解决后,是不可能自动解决冲突的。
为了解决脑裂的问题,我们必须确保彼此断开连接的两组服务器不能独立地正常运行下去。为确保这一点,服务器执行动作时,只有大多数服务器可以确认这个动作,该才将动作视为成功。如果服务器无法选出来一个”大多数“,则它们将无法提供服务,并且某些客户端组可能无法接收该服务。但是集群中的服务器将始终处于一致状态。占多数的服务器数量称为Quorum。如何确定Quorum?这是根据集群可以容忍的故障数决定的。比如,我们有五个节点的群集,则需要三个quorum。通常,如果我们要容忍f个故障,则需要2f + 1的集群大小。
Quorum确保我们有足够的数据副本以承受某些服务器故障。但是,仅向客户提供强大的一致性保证是不够的。假设客户端在quorum上启动了写操作,但是该写操作仅在一台服务器上成功。Quorum中的其他服务器仍具有旧值。当客户端从Quorum读取值时,如果具有最新值的服务器可用,则它可能会获得最新值。但是,如果仅当客户端开始读取值时,具有最新值的服务器不可用,它就可以获得旧值。为了避免这种情况,需要有人跟踪判断quorum是否同意特定的操作,并且仅将值发送给保证在所有服务器上都可用的客户端。在这种情况下使用主从模式(master&slave)。其中一台服务器当选为master,其他服务器充当slave。Master控制并协调对slave的复制。Master需要确定哪些更改应对客户可见。高水位标记(High-water mark)用于跟踪预写日志中已成功复制到足够的slave中的条目。客户端可以看到所有高水位之前的条目。Master还将高水位标记发送给slave。因此,如果Master失败并且slave之一成为新master,那么客户看到的内容就不会出现不一致之处。
但这还不是全部,即使有了Quorums和Master and Slave,仍然需要解决一个棘手的问题。 Master进程随时可能暂停。 进程暂停的原因很多。 对于支持垃圾回收的语言来说,可能会有很长的垃圾回收暂停。 具有较长垃圾收集暂停时间的Master可能会与slave断开连接,并在暂停结束后继续向slave发送消息。 同时,如果slave没有收到master的任何心跳信号,他们可能选择了新的master并接受了客户端的更新。 如果旧master的请求按原样处理,则它们可能会覆盖某些更新。 因此,我们需要一种机制来检测过时master的请求。 Generation Clock用于标记和检测来自老master的请求。 世代(generation)是单调增加的数字。
从较新的消息中检测较旧的master消息的问题是如何保证消息顺序。看起来我们可以使用系统时间来给一组消息排序,但实际上,是不行的。主要原因是我们无法保证跨服务器的系统时钟是同步的。计算机中的一天中的时钟由石英晶体管理,并根据晶体的振荡来测量时间。
这种机制容易出错,因为晶体可以更快或更慢地振荡,因此不同的服务器可能具有截然不同的时间。一组服务器上的时钟通过称为NTP的服务进行同步。该服务会定期检查一组全局时间服务器,并相应地调整计算机时钟。
由于这是通过网络上的通信发生的,并且网络延迟可能会如以上各节中所述发生变化,所以由于网络问题,时钟同步可能会延迟。这可能导致服务器时钟彼此之间漂移,并且在NTP同步发生后甚至会倒退。由于计算机时钟存在这些问题,因此通常不将一天中的时间用于顺序事件。取而代之的是使用一种称为Lamport’s timestamp的简单技术。Generation Clock就是一个例子。
我们可以看到,从头开始理解这些模式是如何帮助我们建立一个完整系统的。 我们将以一致性为例。 分布式一致性是分布式系统实现的一种特例,它提供了最强的一致性保证。 比如流行的企业架构中常见的Zookeeper,etcd和Consul。 他们实现了zab和Raft等一致性算法,以提供复制和强大的一致性。 还有其他流行的算法可以实现一致性,Google的Chubby中用于锁定服务的Paxos,考虑stamp replication and virtual-synchrony。 用非常简单的术语来说,一致性是指在一组服务器中,在存储的数据,存储的顺序以及何时使该数据对客户端可见,这些方面达成一致。
一致性的实现中,使用 state machine replication来实现容错。在state machine replication中,存储服务,比如键值存储,在所有服务器上复制,而且用户的输入也会在每个服务器上以相同顺序执行。用于实现此目的的关键实现技术是在所有服务器上复制Write-Ahead log以具有“ Replicated Wal”。
我们可以将这些模式放在一起以实现Replicated Wal,如下所示。
为了提供持久性保证,使用Write-Ahead log。使用Segmented Log将预写日志分为多个段。这有助于Low-Water Mark进行日志清理。通过在多个服务器上复制预写日志来提供容错能力。服务器之间的复制通过使用主从模式进行管理。更新High-Water Mark时,使用Quorum,以确定哪些值对客户可见。通过使用Single Update Quest,使所有请求均按严格顺序处理。使用Single Socket Channel, 将master的请求发送给slave时,事件将得到维护。为了优化Single Socket Channel上的吞吐量和延迟,使用了Request Pipeline。Slavet通过HeartBeat确定Master的可用性。如果Master由于网络分区而暂时从集群断开连接,则可以使用“Generation Clock”来检测它。
通过这种方式,理解这些问题,和它们的通用解决方式,帮助我们理解构建复杂系统的方式。
分布式系统是一个巨大的话题。 这里涵盖的模式集只是一小部分,涵盖了不同的类别,以展示模式方法如何帮助理解和设计分布式系统。 我将继续向这个合集添加下边这些问题。
Redis中主要包括这些对象:字符串、列表、哈希、集合、有序集合、HyperLogLog、GEO等,本文会主要介绍这些对象是如何实现的,对于其命令,可以参考redis官网中对每个命令的说明,这里就不一个一个列了。
在介绍具体的对象之前,先看一下redis中对于对象本身的定义。
1 | typedef struct redisObject { |
在一个redisObject中,prt字段就是数据的实际内容,这里的存储会应用之前讲的redis的底层数据结构。可以看到,ptr是作为引用出现在redisObject中,也就是说,redisObject本身和实际数据本身内存可能不在一起。
通过 OBJECT ENCODING KEY 这个命令,可以查看一个key底层实际使用的数据结构。
接下来,我们就具体讲一下各个对象。
字符串可以说是redis中最基本的一个对象类型了,set 一个普通的key,value,使用的就是字符串对象。字符串对象根据具体情况,会使用int, embstr, raw三种类型的底层编码方式。其中int很好理解,在字符串的值是数字时,就会使用int存储。embstr和raw的底层结构都是简单动态字符串(SDS),区别在于,embstr中,redisObject和实际存储的SDS在连续的内存空间上。embstr的使用场景是字符串小于等于39字节(如无特别说明,本文中的数值都是使用redis的默认配置),大于39字节时,则使用raw编码。
即list, redis中的list是一个双向队列,提供左右两侧的push,pop操作,range操作等。
list的底层实现可以是ziplist或者链表。上一篇文章中,我们介绍了ziplist是一种针对小数据集的,非常高效的数据结构。因此,在使用时,很自然的可以想到,小的list就会用ziplist实现,大的list则用链表实现。对于什么样的列表才算小,redis里使用两个判断标准,一是list中每个元素的最大长度,对应配置list-max-ziplist-value, 二是list中的元素数,对应配置list-max-ziplist-entries。
list的实际应用场景其实就是对双向队列的利用场景,比如微信朋友圈的时间线就可以通过list实现。有时候,也会拿redis的list用作消息队列。
即我们通常所说的map,与列表对象list, 哈希也会根据数据集的大小选用两个不同的底层结构,分别是ziplist和hashTable(底层是字典)。
这里可以引申出一个小技巧,当我们需要在redis中存储object时,可以使用hash,而非序列化成字符串存储。因为对于一个正常的object来说,是会被通过ziplist存储的。这样,就通过非常小的资源开销实现了对单个字段的get/set.
即set, 对应redis中的S系列命令,如SADD,SMEMBERS等,也就是元素不重复的”集合”。同样的,redis的集合也有两种实现方式,分别是intset和hashTable.
redis的集合提供了非常方便的求交集、并集、差集等的方法,可以用在查询共同好友等场景中。
对应redis中的Z系列命令,如ZADD,ZRANK等,底层的实现对应ziplist或者skiplist。期数,看完上一篇文章中对于这两个数据结构的介绍,我们是可以很容易的理解redis是如何实现有序集合的。
有序集合也是一种很常用的对象,可以用在排行榜、topK等问题中。
redis中的hyperLogLog是一个用于基数统计的结构,即统计不重复元素的个数。它的实现是基于概率算法,通过一定概率的误差,在对空间使用非常小的情况下,实现对大数据量的统计。其具体算法非常精妙,我后边会单开一篇文章介绍。这里就先介绍下它的效果,HyperLogLog可以使用12k的空间统计2的64次方的数据,标准误差在0.81%。
Redis中使用HyperLogLog非常简单,只包含三条命令,PFADD添加元素,PFCOUNT获取当前基数,PFMERGE对多个HyperLogLog进行合并。
顾名思义,redis中的geo就是一种用来处理位置相关信息的结构。redis中的geo对象提供了添加、查看位置信息,计算两个位置间的距离,查找范围内的元素这些功能。
Geo的底层基于zset实现。其核心处理在于通过GeoHash算法将二维的经纬度编码成一维的hash值,同时还能保住经纬度本身越接近的值,hash值也越接近。GeoHash的细节我们也在后续文章中专门做介绍。经过GeoHash做编码后,就可以利用zset本身的功能来做范围查找等事情了。
现在很多互联网服务的位置服务会选择使用redis来实现,比如附近的人,附近的餐厅。
通过上边的介绍,希望可以帮助大家更好的理解和使用redis。可以看到,redis里针对一些特定情况,进行了非常极致的内存优化,而我们在使用时,就要考虑如何更好的去利用这些优化。
]]>接下来,我们就依次讲讲这些数据结构。
Redis是用C语言实现的。先复习一下C,C里的字符串中不记录字符串长度,以空字符标记结尾。这样会显而易见的带来三个问题:1.获取字符串长度需要O(n)的复杂度;2.操作不慎会导致缓冲区溢出,例如内存中紧邻的两个字符串,如果对前一个调用strcat拼接其他字符串,就会造成溢出;3. 一些特殊内容,如图像、音频等转成二进制时,难免其中夹杂空字符等特殊字符,这样就无法被C字符串存储了,即C字符串不具备二进制安全性。
而这几点,对于Redis的应用场景来说,影响其实都是非常大的。因此,在redis中定义了一个新的结构,用来保存字符串,即SDS。
SDS的核心思想就是额外使用一个字段记录字符串的长度,这样,上面三个问题就都迎刃而解了。
此外,redis从4.0开始对SDS做了一个代码层面的优化,优化了内存占用,不过不影响其底层逻辑。
这是redis 3.0里SDS的源码:
1 | struct sdshdr { |
而这是redis 4.0之后SDS的源码:
1 | ... |
可以看到,在新版的源码里,数据存储会根据情况使用uint8,uint16等不同类型。在C里,一个int占用4个字节,因此,对于原版的SDS来说,即使存储的信息非常少,也会固定占到8个字节。而uint8只占一个字节,uint16只占2个字节,对于小数据来说,redis的内存占用会有明显优化。
此外,redis会有空间预分配、惰性释放等机制,减少内存分配的次数。SDS的实现方式也保证了大部分方法可以兼容C字符串,减少了大量实现成本。
Redis里的链表是一个普通的双向无环链表,相信大家都很熟悉了,就不细说了,结构如下。
1 | typedef struct listNode { |
Redis中的列表对象,底层就是链表。
字典也就是我们常说的map。
1 | typedef struct dictht { |
Redis中的字典是hash表,使用链地址法解决hash地址冲突。
类似于java等语言中的hashMap, redis的字典也会有rehash的机制,保证其负载因子维持在合理的范围内。
Skiplist是一种应用非常广的数据结构,通常是作为AVL树的一种替代选择,和AVL树一样,skiplist的查找复杂度也是O(logn), 但是实现会简单的多,下边我们用短短的几行字就能把SkipList的所有内容讲的非常清楚。此外,在并发环境下,SkipList也会有很大优势,因为AVL数在平衡过程中,可能会涉及到很多节点,也就需要锁住很多节点,SkipList则完全不存在这种问题。
从网上找了一张示意图,可以很清楚的展示出SkipList的结构。跳跃表说白了就是一个多层的列表,每一个元素会随机的出现在某一层上,然后某一层的链表中会包含所有高于或等于本层的元素。
跳跃表的查找就是从高层查起,逐步降层,定位到具体元素。比如要查询7, 其顺序就是9->6->7.
跳跃表的插入也是先做一次查找,然后直接给元素设置一个随机的层数,再调整指针。
删除则是删除节点,然后调整指针。
Redis中的有序集合,就是基于跳跃表实现的。
这两个结构非常像,因此就放在一起讲了。它们都是针对特定条件下的小数据集做的特定优化。
整数集合是一个有序集合,使用的条件是集合中只包含整数,且元素个数不多。
压缩列表同样是针对列表项非常少的情况,且要求元素只能是小整数值或短字符串。它可以提供类似双向链表的功能。
因为整数集合和压缩列表都是针对小数据集的,所以可以使用连续的内存空间去保存,实现也就简单了很多,这里就不细说了。
在实际应用中,zipList可以作为链表或者字典的替代品,应用在redis的列表、哈希、有序集合中。整数集合则作为字典的替代品,用在集合对象中。
以上就是redis中主要的数据结构,在这些结构的基础上,redis实现了大量功能完善的对象,供我们使用。理解了redis这些底层结构的原理,也可以帮助我们更好的发挥redis的价值。
]]>要说设计模式,一定要先提一遍六大基本设计原则。说白了,设计模式就是在这些原则的基础上被提炼出来得一系列最佳实践。
通过扩展解决问题,而不是修改已有的实现。(写代码时可以假定变化不会发生,当出现变化时,需要抽象以隔离将来的同类变化) 修改没有破坏原有的单元测试,即可认为不违反开闭原则
子类应当能代替父类
高层模块不能依赖低层模块;应当依赖抽象而非实现
接口尽量小
类间解耦
#设计模式
定义一个用于创建对象的接口
创建一组相关或者相互依赖的对象
一个类只有一个实例
分别build实例的各个模块
复制对象
处理接口不兼容
动态给一个对象添加一些额外的职责,比生成子类的方式灵活
对一些对象提供代理,以限制那些对象去访问其它对象
门面中包含若干个子系统的部分或全部方法,将子系统组合后对外提供。
将抽象部分和实现部分抽离。一个抽象类依赖一个接口,两部分分别可以做扩展
把一组相似的对象当作一个单一的对象
共享对象,数据库连接池,线程池等即是用享元模式的应用
多种不同“策略”动态切换,起到改变对象行为的效果
父类定义算法的骨架,部分细节由子类实现
对于一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知
给对象提供一个迭代器(Iterator)
多个对象都有机会处理请求,连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。
将来自客户端的请求传入一个对象,实现于“行为请求者”与“行为实现者”的解耦
备忘录对象就是一个用来存储另外一个对象内部状态的快照的对象
状态机
将数据结构与数据操作分离
用一个中介者对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用
给定一种语言,定义他的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中句子。典型场景SQL解析等
]]>我想从两个方面来说。一是从做的事儿上,在我看来,程序员的工作就是发现问题、分析问题、解决问题三类。事实上,世界上绝大多数工作都可以囊括进这三类事中。二是从能力上来看,我把能力分为两种:对现实世界的认知能力,和将现实世界映射成程序语言的能力。
对于三种类型的事,按发现问题,分析问题,解决问题的顺序,是从宏观到细节的区别,平时做的事,肯定是以解决问题为主,各种琐碎的事,也都是在解决问题。发现问题和分析问题对人的思维深度会有更高的要求,不过对于工作来说,我倒并不觉得这三类事有什么高下之分。如果对确定的问题,总能给出恰当的解决方案,这也是职场上非常硬核的核心竞争力。而对于两类能力,一般还是更关注第二种。但是我觉得对现实世界的认知能力,对于程序员的职业发展是非常重要的。就像抠业务,抠CRUD这事,有门槛吗?我觉得是有的,至少把业务做的很好门槛非常高。有些人总是以非常奇特的方式去实现业务,然后赖产品经理需求描述不清楚,很可能就是缺少对现实的认知。
接下来,就按发现问题、分析问题、解决问题的顺序,分别详细说一下。
发现问题的能力,对程序员来说,其实不是必需的。很多问题,即便你发现不了,或者发现了然后假装它不存在,系统一样可以运行的很好。但是,逐步的累积这种问题,最终必然会导致代码不可维护。下面,我拿一个自己的实际经历来举个例子。之前做过一个旅客飞行记录的项目,国内航班的数据,我们几乎都能拿到。数据存储的时候,主键是用的证件号,为什么呢,因为所有的订票系统,都是用证件号去做key的。但是我们的产品形态并不是这样,而是以用户的维度去显示,一个用户会有多个证件。这样就导致了一个问题:任何一个非常简单的问题,比如要查用户最近一条飞行记录,最近十条飞行记录,都会有一套复杂的处理流程。更不用说随着数据量增加,要分库分表,但同一个用户的证件没法往一张表上落,逻辑复杂度再提升一级。在这个时候,不发现这个问题,其实业务也完全开展的下去,但是很显然,做这块业务就会做的很难受。而意识到这是个问题,再做相应的调整后,才会发现原来这一块的业务如此简单。
那么,怎么才能发现问题呢。这需要我们有对现实世界的认知能力,也需要对设计模式等基础知识有足够的理解。要发现问题,我们首先得有“正常状态”是什么样子的概念,与正常状态不一样的,就是问题。比如前边的例子,一个“正常状态”是查询用户最近的飞行记录,这个需求应该非常简单,还有一个“正常状态”是我们的用户是一个人,而不是一个证件,发现了和政策状态不一致,也就发现了问题。再举一些例子,写单元测试应该是很简单的,当发现单元测试不好写的时候,就需要考虑一下是不是代码的设计出了问题;mysql的一次batch请求,耗时的数量级应该在毫秒级, 而且数据量不应该严重影响耗时,当发现数据量和耗时的关系成了一个非常陡的线性关系,就要考虑一下是不是真的做了次batch(参考 http://lichuanyang.top/posts/63688)。
接下来是分析问题。对于分析问题,我觉得说白了就是把一个表现出来的问题,拆解成一个或若干个“指标”出了问题。这些“指标”,可以是内存、cpu这样实际的指标,也可以是一些基本的概念理解、设计思路。还是上一个例子,我们发现的问题就是一些很简单的需求实现起来很难,最后拆解出来的出问题的“指标”就是存储记录主键的选择。
对于分析问题,要会做合理的可能性推测,然后多利用工具验证或者推翻猜测。之前有一次发现数据存不进数据库,这时候就可以做出若干个合理的推测:数据库本身有问题;和数据库的连接有问题;事务未能正常提交;逻辑错误导致数据未能生成;等等。然后去翻日志,看有没有报错,看Mysql监控,有没有大量未能提交的事务。逐步就定位到问题了。
最后说解决问题,对大部分程序员来说,这个一直会是做的最多的事。而程序员的解决问题,也就是把现实中的概念在计算机中表示出来。其中最关键的,我理解有两个,一是分解任务,把一个大任务拆分成若干个独立的子任务;二是迅速的理解并使用新工具能力,也就是持续学习的能力。很多人会经常抱怨程序员要学的东西太多了,学不过来了。不过一定要注意哪些东西是值得花费大量时间去学的,哪些东西是简单看看就足够了的。发现有很多人热衷于工具的学习,喜欢写各种版本的Hello World, 这个在我看来就是无效的学习。编程方面的学习,需要兼顾深度和广度,深度,是需要非常透彻的研究一些技术,这个看了一些之后,你会发现各种技术,看起来可能没什么关系,比如数据库和消息队列,但是其中就是会有非常多相似的概念和思想。学过一些之后,就可以建立起自己的知识体系了,然后再去学其他的东西,就会非常快。
因为一个新技术的产生,必然是迫切的要解决一些实际问题。比如为什么会有clickHouse, 因为mysql等的按行存储不适合数据分析,而市面上又没有性能、稳定性都比较好的列存储数据库。再比如jedis直接连redis-server, 在多线程环境下表现不好,所以有了Lettuce。当知道要解决什么问题后,就可以先试着想一下换成你的话,有什么解决方案。然后再去看看人家实际的解决方案。如果方案很接近,那么恭喜你,你对技术的理解已经又上了一个高度。如果不一样,那也恭喜你,又学到了一种新的思想。这些思想,在以后做业务的时候,随时都可以化用。
共勉。
]]>前阵子,发现一条sql的性能明细不正常,几千条数据批量insert,耗时居然到了秒级。排除了服务器性能、网络的问题后,开始怀疑是代码的问题,导致batch操作没有真正生效,感觉只有一条一条去跟数据库交互的话才会出现这种性能。
调了一下代码,翻到了mysql-connector里,关键的代码是这么一块:
1 | if (!this.batchHasPlainStatements && this.connection.getRewriteBatchedStatements()) { |
可以看到有executeBatchedInserts和executeBatchSerially两个分支,如果执行到executeBatchSerially的话,其实就是执行了个“假的”batch, 在这个方法里会每条数据单独去跟mysql交互。
为了能顺利的执行进executeBatchedInserts, 可以看到有一些先决条件:batchHasPlainStatements, connection.getRewriteBatchedStatements(), canRewriteAsMultiValueInsertAtSqlLevel。
事实上,为了能顺利的执行批量sql, mysql的驱动会对sql做一次“改写(rewrite)”,这个改写是什么意思呢?其实很简单,比如本来有一堆sql:
1 | insert into () values() |
改写完之后,变成了这样:
1 | insert into () values () () () |
变成了一条sql, 当然就是与mysql交互一次了。
成功rewrite后,才能批量执行sql。上边的几个条件也基本都是为了能让sql进行改写。这里边关键的其实就是两个点,一是数据库连接层面,需要配置rewriteBatchedStatements=true, 允许对sql进行改写。第二是sql层面,得要求这个sql能被改写。我们知道,mysql有这样一种语法:
1 | insert into () set x=x,y=y |
这种语法本身不属于sql标准,是mysql的“方言”,也是没有批量操作的形式的。因此,如果把sql写成这种形式,就不然没法执行真正的批量操作了。
]]>先说一下我们写单元测试的目的,这个是下边所有讨论的基础。单元测试的意义应当在于保障修改代码的时候,保障存量代码的正确性。也就是说,修改代码后,执行一遍已有的测试用例,能执行的话就代表之前的逻辑依然是可用的。
下面就具体列一下个人认为写单元测试应当遵守的准侧。
单元测试不应当依赖于环境,包括对网络、db、io、数据、外部接口等的依赖。这一点应该争议不大,因为单元测试会有很多的执行场景,比如开发人员手动触发、提交代码时自动触发、打包时自动触发等,如果单元测试不能解除对环境的依赖,它的执行场景就会大大受限。
实现这一点,需要借助很多工具,例如内存数据库、redis的内嵌server, mock等待,势必会给写单元测试这个过程增加很多工作量,但是是无法避免的。
测试应当是针对业务逻辑,而不是实现逻辑。这一点是争议会比较多的一点,主要是大家对于“单元”这个词的定义无法统一。有人认为一个函数就是一个“单元”,但是如果按这个标准去做单元测试,单元测试是达不到预期效果的。
举个简单的例子,代码重构的时候,要不要改单元测试?答案当然是不能,如果每次代码重构都要对应修改单元测试的话,单元测试的作用可以说就失去了一大半。所以说测试的一个“单元”应当是一个业务上的单元。当然这一点也不能太死板,比如你写了一个很复杂的方法,针对这个方法写一个单元测试也完全没有问题。这一点其实可以换个更准确的方法:针对意图写单元测试,而不是针对实现。也就是说,把代码的实现逻辑当做一个黑盒,只针对输入输出做测试。
前边第一点里,我们提到过可以利用mock避免对外部环境的依赖。但是一定不能滥用mock, 可以阅读一下mock七宗罪 , 一个原则就是能不mock的时候就不要mock。什么时候是必须要mock的呢?就是前边说的要依赖外部环境的时候。除了这种情况,都不要mock.
在写单元测试的时候,会发现有些测试会非常难写,而具体分析难写的原因,会发现有一些是因为代码本身写的不好。比如,一个类的依赖特别多,可能就是因为这个类的职责不明确,将不相关的代码放到了一起。这时候,就应该直接将代码重构,拆分成几个类,写单元测试的难度也会随之降低。
]]>首先,简单介绍一下rocketmq的queue是什么,从网上找了一个rocketMq的架构图。
我们知道,使用rocketMq时,我们都是针对一个topic区进行生产或者消费。而messageQueue就是topic的一部分,类似于kafka中分区的概念。queue是consumer消费时的最小单元,也就是说,consumer的数量无法高于queue的数量。
rocketMq中有很特殊的一点是分了writeQueue和readQueue。其实,在绝大对数情况下,这两件事是无法分开的,如果readQueue和writeQueue的数量不一致,会产生非常严重的问题。例如,writeQueue的数量高于readQueue, 就说明有些queue只有写没有读,也就是说会有一部分消息永远都不会消费掉。readQueue多于writeQueue的话,就说明有一部分consumer处在空跑的状态,不可能接收到消息。
那么,rocketMq这么设计的意义在哪里呢。其实,拆分readQueue和writeQueue, 目的就是能让扩容或缩容的过程更加平滑。以扩容为例,可以先增加readQueue的数量,这时候consumer已经能关联到queue上,然后再去修改writeQueue,因为consumer已经扩容完成了,进入新的queue的消息马上就可以被消费掉。这样整个过程就非常平滑且健壮,所有消息都可以及时交给consumer.
大家可以考虑一下如果没有这样的机制应该如何去设计扩容或者缩容的流程,其实,无论如何都会造成一些消息无法被及时处理。说明这种设计是非常巧妙的。
]]>Clickhouse是极其适合OLAP(联机分析处理)问题的一个数据库。这类问题有如下一些特点:
根据clickhouse提供的性能测试结果,clickhouse的性能要大大领先于所有同类产品。
针对这类问题,用到的最核心的思路就是列式存储。目前常用的数据库,像MySQL等,都是行式存储。而列式存储,简单的说就是把同一列的数据存储在一起,像下图这样。
相对行存储, 列存储查询时只会读取涉及到的列,减少io开销;任何一列都可以作为索引;但是数据写入会相对麻烦一些。对比前面我们介绍的OLAP问题的特点,可以看到,列式存储是及其适合这种在线数据分析的。
ClickHouse中提供了很多种存储引擎,不过90%以上的情况下用MergeTree这个引擎就可以了。下面详细介绍一下这个引擎。
简单来说,mergeTree有这么几个特点:
首先来看一下mergeTree的文件结构。(本节的图片都来自clickhouse中文社区(http://clickhouse.com.cn/)内来自新浪高鹏老师的分享)
其中20171001….这个目录就是代表一个part,下面这些文件,columns.txt记录列信息;每一列有一个bin文件和mrk文件, 其中bin文件是实际数据,primary.idx存储主键信息,结构与mrk一样,类似于稀疏索引。
这里展示了mrk文件和primary文件的具体结构,可以看到,数据是按照主键排序的,并且会每隔一定大小分隔出很多个block。每个block中也会抽出一个数据作为索引,放到primary.idx和各列的mrk文件中。
而利用mergetree进行查询时,最关键的步骤就是定位block,这里会根据查询的列是否在主键内有不同的方式。根据主键查询时性能会较好,但是非主键查询时,由于按列存储的关系,虽然会做一次全扫描,性能也没有那么差。所以索引在clickhouse里并不像mysql那么关键。实际使用时一般需要添加按日期的查询条件,保障非主键查询时的性能。
找到对应的block之后,就是在block内查找数据,获取需要的行,再拼装需要的其他列数据。
一些面向列的DBMS(例如InfiniDB CE和MonetDB)没有使用数据压缩。但是,对于clickhouse的性能提升,数据压缩起到了很大作用。对于列式存储来说。相同的字段存储在一起,类型一致,数据类似,更方便进行压缩。clickhouse支持LZ4和ZSTD等压缩算法。
clickhouse支持普通磁盘;因此在成本上有较大优势。
提供了针对raid, ssd,大内存等的配置,如果有的话,也能加以利用。
数据按主键顺序存储,因此可以实现延时极低的查询。
现代CPU中有一种叫SIMD的机制,即Single Instruction, Multiple Data,一条指令操作多个数据。对于列式存储的数据,可以很方便的利用这一机制,更好的发挥cpu的性能。
在clickHouse中,数据会存储到不同的分片上。查询会在多个分片上并行执行。
而在每台服务器上,也会多核并行处理,充分利用单机性能。
mergetree引擎下,新插入数据后,会先形成新的part, 这个时候数据就已经可以被查到,因此从数据写入到可以被查询的延时是很小的。后续不同part会继续异步进行merge, 以提高存储效率
clickHouse是基于zookeeper的主主复制。写入任何可用的副本后,数据将分发到所有剩余的副本。复制时是按块进行,复制失败的话可以直接重试。系统在不同的副本上保持相同的数据。
此外,相对其他列存储数据库,clickhouse对sql语法的支持非常好,包括group by, order by, in, join等常用sql语句都支持。
]]>针对这个问题,我们可以首先观察一下大小写字母的ascii码特点。
可以看到每一对大小写字母的ascii码值都是差32,对应到二进制上,就是只有第六位相反,其他的值都一样。
因此,用一个字母的值与 0010 0000 (32)作亦或,就可以得到它对应的大写或小写字母了。
]]>值得注意的是消费者,
1 | public void consume() throws JMSException { |
可以看到,实际上有三种方式都可以实现多线程消费同一队列,分别是定义多个connection, session和consumer。这三种方式在应用层面的效果是一样的,都是会生成多个消费者,并行处理队列中的消息。但是在性能上,这三种方式会有较明显的差别。下边详细介绍一下。
首先,定于多个consumer只是一种伪并行,并没有真正并发消费。原因是在JMS协议列,一个session在同一时间点只能被一个线程使用,所以多consumer复用同一session时,只是这些consumer轮流使用这一个session。
而可以考虑开多个connection对应的是物理的tcp连接,一个producer可以新建多个session,这些session就是复用同一个tcp链接。多session和多可以考虑开多个connection都是真正的并发操作,区别只在于是开一个还是多个tcp连接。这个造成的影响很容易理解,多个tcp连接,可以处理更大的网络流量,当然在建立、维护连接时也会带来一定开销。
总的来说,大部分情况下,我们可以定义多session来实现activemq的并行消费。在流量较大时,可以考虑开多个connection。而多个consumer, 目前没有想出有什么场景能用到。
]]>对于一个计费系统来说,并发问题事实上分为两类,一类是应用并发高,也就是纯粹的用户量大,访问量多,这类问题和一般的高并发问题没有区别,用分布式等手段就可以解决;另外一类问题则是一般分布式手段无法解决的用户高并发问题,也是本文要着重说的。
这类问题源自对某些高频账号,大量的并发访问,会导致瓶颈首先出现在某些数据库记录上,大量操作由于无法竞争到数据库的行锁而导致等待,这些等待中的操作又会占用其他资源,最终导致系统不可用。
针对这类问题,下边介绍一些常用的处理办法。
由于对于一个稳定的计费来说,一定是会记录计费流水明细的,所以完全可以不设置余额字段,而采用根据流水明细计算的方式来获取余额。
不过这种方法不是万能的,比如拿广告业务的计费系统来说,频率非常高,而每次的金额很小,这时候想通过计算求和去算余额,显然是不现实的。
这是两种方式,因为有一些相似之处,都是要降低对单条数据库记录的访问压力,所以也就放到一起说了。
合并,就是对单个账号的数次请求作合并处理,再往数据库写,这样就等于降低了数倍的压力。
拆分,则是把一个主账号拆分成数个子账号,然后把请求分配到各个子账号上,这样单个账号的压力就小了。然后再用其他手段把子账号的数据合并成主账号数据,返回给用户。
这是个代码层面的优化,前面说了,高频账号之所以会导致系统的性能问题,就是因为要竞争行锁,所以,如果我们能减少每次请求占用行锁的时间,系统性能也就会大幅度提升了。
所以,首先,要尽量加快从获取行锁到事务提交这个阶段的运行速度,将不必要的操作,尤其是一些耗时的操作,放到其他地方执行,比如获取行锁之前或者事务以外。
然后,尽量避免用
1 | select ... for update |
的方式去获取行锁,二是采用下面这种方式:
1 | update xxx set amount=amount-1 where id=x and amount>=1 |
如果业务层面允许余额扣减成负数的话,就可以不使用where条件中对金额的校验;否则就需要在将要把余额扣成负数时不去更新数据库,并在程序中返回异常。
既然高频账号是单账号的并发达到一定程度后才会导致系统的性能问题,所以我们就可以强制控制这个并发量,使它永远保持在系统可接受的范围内。
缓存也是一个常用的解决高频账号的方法,在缓存中对余额作操作,然后定时向数据库内同步。
上边介绍了很多方法,当然每一种都会有它的适用条件,也会有其局限性。比如像合并啊,限流啊,实际上都会造成延迟扣费,而在延迟的这个时间段内,可能账户余额已经耗尽,所以如果是严格余额不能为负也不能丢弃记录的业务场景下,其实并不适合用这种方式。
所以说,最重要的还是根据业务场景选择合适的方案。
今天用findbugs扫代码时遇到一个很有意思的问题,有关三目运算符的,在这儿记录一下。
就是类似这么一行代码:
1 | boolean b = true; |
Findbugs给出了”Boxed value is unboxed and then immediately reboxed”的提示,意思就是有装箱的对象做了拆箱,然后又马上做了装箱。这个问题其实很常见,一开始也没注意,只是习惯性的把Long.valueOf 改成了Long.parseLong, 确实把这个警告消掉了,不过之后才意识到不对:明明valueOf返回的是Long类型,parseLong返回的是long类型,而需要的正是Long类型,为什么反而用valueOf的时候有问题呢。
其实思考一下大概也能想明白,主要就在三元运算符的另一个分支,因为另一个分支返回的是一个未装箱的0,所以这个三元运算符的返回值就成了long,所以原本的Long类型就要经过一次拆箱才会被返回。要优化这个部分的话,保持两个分支的返回类型一致就可以了。
然后把相关的细节查了一下,了解清楚。
从JDK1.5开始,java引入了自动装箱和拆箱,不需要做显式转换,提高了我们的开发效率。比如:
1 | Double dWrap1 = 10d; |
这么一段代码就是可以正常运行的。
另外一个要注意的地方就是,在一个运算,比如前边提到的三元运算符,涉及到类型转换时,编译器会优先选择基本类型,也就是说会优先把已装箱的对象拆箱。
我们一开始的问题只是很细微的性能损耗
1 | Long B = null; |
但是像这样的代码的话就是有bug了, 看似只是把一个null赋给Long类型的A, 但是这过程中会做一次向long的拆箱,所以肯定会报空指针。
所以说,我们在平常的开发中,还是应该尽量避免无意义的装/拆箱和类型转换,不光是出于性能考虑,也是为了避免一些诡异的问题。
]]>最近在找房子,因为想找一个去几个地方都相对方便的位置,自己去地图上看还挺麻烦的,所以想做个小工具,用来对北京地铁的路线做规划,本文就简单介绍一下实现过程。目前的功能还比较简单,主体方法就是根据一个输入的始发站,列出其他所有站点到这个地方的站数最少路线。
从网上找了高德地图的接口数据: http://map.amap.com/service/subway?_1469083453978&srhdata=1100_drw_beijing.json . 对拿到的json串进行解析,其中包含每条地铁线路的信息,并依次列出线路上的每个站点。
通过解析数据,要得到的主要就是各个站点的信息,站点定义的数据结构如下:
1 | private String id; |
所有站点汇总后可以看做一张图,nextStations字段就是用来表示边的信息。
路线规划算法可以参考Dijkstra算法,由于现有的表现形式下,只是一个无向无权重的图,实现起来还要简单一些。
实现过程描述如下:
整个过程结束之后,会得到每个节点到起点的最短距离以及路线详情,然后可以根据这些数据计算路线详情的换乘情况。
对于换乘,主体判断逻辑是,如果一个站点的上一站和下一站所在路线不重合,就可以确定在这一站进行了换乘,比如灯市口-东四-朝阳门,灯市口在5号线上,朝阳门在2,6号线上,可以断定在东四肯定作了换乘。不过还有一些特殊情况要处理,比如说西直门到平安里,中间那站如果是车公庄(虽然现实中没人会这么干),实际就是作了换乘的,但是按刚才的逻辑就会判断成未换乘。
最终得到的路线信息中包含以下信息:
1 | private String stationId; |
主体功能在上面已经完成,后边就可以根据需要再去自定义处理了。比如,加入我现在需要找到”距奥林匹克公园站15站以内且距天安门东站7站以内的位置”,就可以分别输入奥林匹克公园和天安门东,然后在结果中作相应的过滤,再取交集。
目前做的还比较简单,只是简单考虑站数,但是实际上,不同站点的距离、耗时相差会比较大,再一个不同地方的换乘开销差别也很大。所以后续会试试能不能找到换乘站的详情数据,还有两站之间耗时的数据,并应用到算法中。
再一个是要找一个合适的方法做个界面出来,因为一直做的是纯后台,还没考虑清楚用什么合适。
其他的,还在考虑爬一下房价信息。
https://github.com/lcy362/FoxSubway
欢迎提意见。
]]>redis官方文档中提供的数据迁移办法是借助redis-trib脚本,其实严格来说,这个redis-trib并不是redis本体的一部分,它只是官方按照redis设计规范实现的一套脚本集合,帮助用户更方便的使用redis-cluster。 实际上,我们完全可以脱离这个脚本来使用cluster, 或者用其他方式实现这套逻辑,比如搜狐tv的redis运维工具cachecloud里,就用java实现了整套逻辑。
我们可以参考redis-trip或者cachecloud的代码来了解cluster数据迁移的流程,主要分为如下几部:
1 | CLUSTER SETSLOT <slot> IMPORTING <node_id> |
1 | CLUSTER SETSLOT <slot> MIGRATING <node_id> |
1 | CLUSTER SETSLOT <slot> NODE <node_id> |
在整个迁移中,会出现对于单个key的阻塞情况,原因是MIGRATE命令是原子性的,在单个key的迁移过程中,对这个key的访问会被阻塞。但是,一般来说,一个key的数据不会特别大,所以绝大多数情况下瞬间都能完成,所以一般不会真正影响使用。而其他任何情况都不会造成集群的不可用,如果出现了,比如出现slot级的不可用,说明client端的处理存在某些问题。接下来,本文也会介绍一些client端使用的注意事项。
前边说了,redis cluster的数据迁移基本不会影响集群使用,但是,在数据从节点A迁移到B的过程中,数据可能在A上,也可能在B上,redis是怎么知道要到哪个节点上去找的呢?这里就要先介绍一下ask和moved这两个转向信号了。顾名思义,出现这个信息就说明需要的数据并不在当前节点上,需要做一次转向处理,其中,MOVED是永久转向信号,ASK则表示只需要这一次操作做转向。
比如,在节点A向节点B的数据迁移过程中,各个key分散在节点A和节点B中,所以当客户端在A中没找到某个key时,就会得到一个ASK,然后再去B中查找,实际上就是多查一次。
需要注意的是,客户端查询B时,需要先发一条ASKING命令,否则这个针对带有IMPORTING状态的槽的命令请求将被节点B拒绝执行。
对于客户端,简单来说就是,收到MOVED时,需要更新slot映射信息,收到ASK时,则需要向新节点发ASKING命令并重新执行操作。
看了一下jedis代码,也正是按照这个逻辑实现的。
1 | } catch (JedisRedirectionException jre) { |
操作出现异常时,会分别判断MovedException和AskException,然后作相应处理。
]]>AsyncLoopThread是jstorm里自定义的一个循环执行任务的工具,实现不复杂,本来是不值当的专门开一篇文章介绍。不过这个在jstorm里应用实在太广泛了,诸如uspervisor/nimbus心跳,获取新topology,更新worker状态等大量功能都是利用AsyncLoopThread实现的,所以还是介绍一下吧,也方便后续看其他部分代码。
AsyncLoopThread其实完全可以拿出来用在我们自己的工程里,使用比较方便,可以参考 https://github.com/lcy362/Scenes/blob/4d8ec4ff166060cf5d491c33a96f5c86e8389333/src/main/java/com/mallow/jstormcode/AsyncLoop.java
只需要先定义一个RunnableCallback类,RunnableCallback是jstorm里封装的一个线程类,对Runable接口做了一些增强,可以回调,可以主动关闭。
1 | public class TestThread extends RunnableCallback { |
这里我们只实现了两个方法,run和getResult, 其中,run方法就是线程执行方法,和一般的线程一样的,getResult返回的是任务执行间隔时间,单位是秒。
之后我们只要把这个TestThread作为一个参数传给AsyncLoopThread,就可以实现循环执行TestThread的run方法了。
1 | public static void main(String args[]) { |
接下来,我们在具体看一下AsyncLoopThread的实现。
核心代码就是这个init方法
1 | private void init(RunnableCallback afn, boolean daemon, RunnableCallback kill_fn, int priority, boolean start) { |
这里主要就是新建了一个AsyncLoopRunnable类,更核心的代码在这里。此外,要注意的一个是kill_fn,在AsyncLoopRunnable里会用到,负责杀掉任务,另外就是会对线程做几个配置,优先级,异常处理之类的,此外这里的线程都被设置成了守护线程,所以上边的例子里,我们需要主动让main线程sleep才能看到运行效果。
接下来再看一下AsyncLoopRunnable.
1 | public void run() { |
这个代码也很简单,就是循环的去执行RunnableCallback的run方法,期间会有shutdown,needQuit(),异常几种情况导致任务中断,或者直接杀掉进程。其中,needQuit()方法里会根据前边说的getResult控制任务执行速度。
]]>数据的持久化是很多系统都会涉及到的一个问题,尤其是redis,activemq这些数据主要是存储在内存中的。既然存在内存中,就会面临宕机时数据丢失的风险。这一问题的解决方案就是通过某种方式将数据落到磁盘上,也就是所谓的持久化。
activemq提供了三种持久化方式,分别基于jdbc, kahadb和leveldb. 目前官方最推荐的是基于kahadb的持久化。 jdbc是activemq最早提供的一种持久化方式,但是用数据库去做持久化确实不合适,毕竟性能有瓶颈,而且只是需要简单的读写数据,不需要数据库各种强大的功能。现在去看文档的话,连基本的配置都被埋的很深,所以这种方式我们也就不细说了。
正是由于基于jdbc的方式存在的种种问题,activemq后来就接连提供了基于kahabd和leveldb的持久化方式.leveldb 是google开源的一个KV磁盘存储系统,应用很广,kahadb没找着源头,应该就是activemq团队开发的,也是一种基于磁盘的存储系统。按理说,leveldb的性能是要好一些的,之前无论是activemq的默认配置,还是文档里的推荐使用方法,都是首选leveldb. 但是有一天这个基于leveldb的持久化方式就突然被activemq废弃了,主要原因是leveldb是一个第三方系统,维护起来不如kahadb那么方便。到当前最新版本,leveldb持久化还存在不少严重问题,功能也不如kahadb完善。所以目前来说,最推荐的持久化方式就是kahadb. 接下来介绍一下基本配置。
基本配置很简单,看默认配置里的就可以,打开activemq.xml。
1 | <persistenceAdapter> |
这样数据会自动同步到kahadb目录下。可以去目录下看一下kahadb的文件结构,文件存储的主体是一系列的.log文件,每条需要持久化的数据会一次写入,一个log文件到达一定大小后,会新建一个新的。当一个文件内的所有消息都已经被消费完毕后,这个文件会被删除。
参数可以查阅文档:http://activemq.apache.org/kahadb.html.
列几个比较有用的:
介绍几点比较有用的经验,一个是日志,可以把kahadb的trace级别日志打开,在log4j里添加org.apache.activemq.store.kahadb.MessageDatabase这个logger的配置就可以,例如
1 | log4j.appender.kahadb=org.apache.log4j.RollingFileAppender |
有时候会遇到kahadb文件无法被删除的问题,直接看不一定看得出来是哪些队列的原因,这份日志里就会打印出清理了哪些文件,其他文件因为什么没被清理掉等关键信息。
另一个是可以为每个队列单独配置存储目录,
1 | <persistenceAdapter> |
这样做的原因还是和存储空间有关,kahadb写文件时是按消息顺序依次写入的,删文件时则要等到这个文件内的所有消息被消费完毕。也就是说,即使这个文件里只有一条消息没被消费掉,也需要占用完整的空间。如果本身队列特别多,恰好有一个队列消费没跟上,可能它本身占用空间非常小,但是会占用大量磁盘空间无法释放。给每个队列分别配置的话就可以大大缓解这一情况。
]]>从官网下载最新版本,解压,其他的前期准备只需要安装jdk。 从activemq 5.14开始,只支持jdk8。
核心配置文件是conf目录下的activemq.xml, 默认的配置无需修改即可使用,其他配置我们会在后续文章介绍activemq各种特性时详细介绍。
bin目录下运行 activemq start
即可 ,启动之后浏览器打开localhost:8161 可以打开web管理页面。
这里只介绍java下的api使用,activemq默认的openwire协议是只支持java的,因为实现的JMS协议是java下的协议。如果有其他语言的访问需求,可以用stomp,amqp等协议。
引入activemq官方的client包。
1 | <dependency> |
actibemq收发消息的主要步骤类似,都需要先建立连接,然后建立会话,然后再新建需要的producer,consumer.
示例代码如下
1 | public void produce() throws JMSException { |
1 | public void consume() throws JMSException { |
connection,session,consumer都可以创建多个,实现并行消费。一般来说需要使用多connection或者多session,因为只是多consumer的话,共用一个session,只是多个consumer轮流执行而已,不是真正的并行消费。至于connection和session的选择,一般看具体的流量需求。一个connection对应的是一个物理的tcp连接。
producer其实也类似,只是一般并行发消息意义不大,所以就不贴代码了。
activemq提供了一个有用的功能,brower, 作用与consumer类似,只是不会真正将消息消费掉,只是预览消息内容。
1 | public void brower() throws Exception{ |
消息中间件是分布式系统十分常见的组件,提供了以比较灵活的方式集成不同应用程序的一种机制,应用程序彼此不直接通信,而是与作为中介的消息中间件进行通信。
消息中间件主要有两个作用,一是解耦,二是平峰,都是大型系统中经常会遇到的问题。
所谓解耦,就是要保持系统内各个应用的相对独立性。例如,用户登录的时候,可能有多个应用要触发各种各样的操作,推荐啊,发消息啊之类的,这些操作跟登录这个操作本身是无关的,如果全都放在登录模块去做,显然不合适,而且用户体验会非常差。这个时候就可以在登陆的时候发一条消息出来,其他系统监听这个消息,再进行各自业务的处理。
平峰,就是为了应对突然的流量增长,毕竟系统分配资源时不可能按最大需求去分配。如果短时间流量太大,可以让消息先留在消息中间件里,业务慢慢处理。
消息中间件一般有两种传递模式:点对点模式(P2P)和发布-订阅模式(Pub/Sub)。
点对点模式在JMS里叫做Queue, 主要特点有三个:
发布-订阅模式则对应JMS里的Topic,与点对点模式向对应,有如下特点:
JMS是Sun定义的一种标准消息传送API。JMS自身并不是一种消息传送系统;它是消息传送客户端和消息传送系统通信时所需接口和类的一个抽象。与JDBC, JNDI等类似。
JMS对消息结构、连接、会话、生产者、消费者等都做了规定。
Activemq就是对JMS1.1的一种具体实现。在后续文章里,我们会对activemq的安装使用以及各种特性进行全面的介绍。
]]>Hexo是一款开源的博客系统。对于一个后端程序员来说,不想折腾前端的东西,但是csdn,博客园之类的用起来还是不太方便,自己搭博客又麻烦,做出来还丑。偶然间看到了hexo,这个对后端程序员来说可以说是非常友好了。所以也写篇文章记录一下hexo安装,一些关键配置,以及部署到github的过程。
参考官方文档 就可以了。hexo是基于node.js的,用过node的自然没有任何问题,没用过也没关系,照着说明文档做就可以了。
hexo支持直接向github的page发布,只需要配置好自己的github信息就可以。
hexo有很多定制主题, 按个人喜好使用吧, 我用的是next , 这款主题功能非常多,统计、搜索之类的都是一条配置都搞定了。不过这款的一些基础配置和其他主题似乎是有些区别的,所以用了以后如果以后想换别的可能会有点困难。
hexo提供了多个从其他博客迁移数据的插件,rss,blogger等等都可以。
以博客园为例,博客园的博客可以导出一个rss文件,然后我们用hexo-migrator-rss就可以生成hexo格式的文件了,不过有可能需要做一些微调。
前边说了,hexo配合next主题,很多工具用起来会非常方便。
主要参考next的文档就可以了,不过这份文档有些老了,具体的还要参考next的主题配置文件,里边的说明也比较详细。
推荐一些比较有用的:
包括部署到github, 也有现成的配置可以用,简单配置一下就好了。
弄完之后,就是大家现在看到的样子了。
]]>由于jstorm的监控相比于apache-storm进行了完全的重写,所以网上查到的storm的监控输出方式并不适用于jstorm. 而jstorm除了官方文档以外实在缺少资料,官方文档又太简略,给的只是一些线索性的东西,具体还要结合这些线索去翻阅源码。所以我整理了一个jstorm监控数据输出的例子。
首先需要实现MetricUploader这个接口,不过其实我们并不会实际使用这个接口里的哪个方法,主要是要去用它的TopologyMetricsRunnable这个参数,然后用这个参数去取监控信息。所以理论上只要拿到TopologyMetricsRunnable就行,并不一定非要实现MetricUploader接口。我的做法是实现MetricUploader,然后自己起一个定时的线程池,定时去取监控数据。
jstorm的metric数据存在rocksdb里,这里取的数据实质上是用jstorm封装好的接口去查询rocksdb。
具体代码如下:
1 | ClusterSummary clusterInfo = client.getClient().getClusterInfo(); |
整个流程比较清晰,首先需要去查询集群中topology的列表,然后使用每一个topology id去查询metric信息,得到一个TopologyMetric类, TopologyMetric里包含topologyMetric,componentMetric,workerMetric等属性,这个分别与UI页面里对应。
以componentMetric为例, 可以使用componentMetric.get_metrics(); 拿到具体的监控metric数据, 一个metric是一个Map<String, Map<Integer, MetricSnapshot>>, 其中key是一个@符分隔的字符串,里边包含topology名,component名,数据项等关键的key信息,value里这个map的key是一个时间,单位为秒,对应UI上1分钟,2分钟那几页,value就是具体的监控数据,这个数据其实比UI展示出来的更丰富,除了均值外,还有诸如95线,99线等。
在这个例子里,我只是用打日志的方式,将部分数据输出。具体用的时候,可以根据需求使用hbase, redis,mysql等存储介质。
]]>首先需要引入camel-test包:
1 | <dependency> |
之后新建一个类并实现CamelTestSupport 。
1 | public class CamelDebugger extends CamelTestSupport { |
CamelTestSupport 中有大量的方法,可以根据需要选择一些进行实现,介绍一下其中一些比较重要的。
1. createCamelContext() 这个方法可以定义自己的camelContext进行测试.
2. createRouteBuilder() 这个方法则是使用默认的camelContext,但是加入自己的route
3. debugBefore 和 debugAfter, 这两个方法分别在一条消息被处理前后被执行, 参数里包括exchange, processor等必要信息。真正debug时,也就是在这两个方法里写日志或者打断点。
]]>用过storm或者jstorm的都知道,如果在bolt代码中发生了没被catch住的异常,所在worker进程会退出。本文就从源码角度分析一下具体设计,其实并不是“有异常然后进程崩了”这么简单。
我们先看BasicBoltExecutor的源码:
1 | public void execute(Tuple input) { |
_bolt.execute(input, _collector) 就是执行我们自己编写的bolt里的excute方法。可以看到,在这里,只会catch storm自己定义的FailedException,并且发送fail消息,标记tuple处理失败, 其余异常则会被放过。
再外层是BoltExecutors的processTupleEvent方法:
1 | try { |
在这里,所有异常都会被catch住,但是只会进行report_error,并不会发fail消息,相关tuple只能等超时才能被标记为失败。
再来看report_error.report(e) 的具体实现,通过看构造函数,可以看到report_error是一个TaskReportErrorAndDie类,
1 |
|
在这里,reporterror是一个AsyncLoopDefaultKill类
1 |
|
这里就是整个过程的最终步骤了, JStormUtils.halt_process()方法会打印一条”Async loop died!”的日志后将worker进程杀死。
通过代码可以出来,对于jstorm,“异常后worker退出”是一个故意设计出的特性,并非程序不健壮。猜测这一块的设计理念就是对于已知异常,开发人员自己捕获并重新抛出FailedException,使相应消息失败;未知异常则强制使进程直接失败退出,避免过度的catch导致问题被掩盖。
不过虽然话是这么说,对这个设计还是持保留意见,毕竟storm和普通的java程序不一样,storm的worker进程在退出后是会自动被重启的,所以这种异常处理方式并不能起到failfast的效果。
相反,worker的持续重启,还会带来一些其他问题。再一个,不主动将消息标为失败,而是等超时,如果设置的超时时间过长(当然超时时间太长也不合理),也会引入一些问题。比如说kafkaSpout, 一条消息没被ack之前是不会继续取后边的数据的,这样如果有一条数据需要等超时,同分区下的数据在这一个超时周期内,就都无法被处理了。
从另一方面来说,如果像FailedException一样处理其他所有异常,由于异常之后可以看到有数据fail,也并不会掩盖问题。
所以说,这一块的处理逻辑,个人感觉还是需要斟酌一下。
]]>1 | KafkaLog4jAppender kafkaAppender = new KafkaLog4jAppender(); |
这里以一个kafkaappender做例子,其他的,例如DailyRollingFileAppender等,都是类似的。
]]>翻译出来贴在这儿:
来自Java HotSpot作者的撰写微基准的提示:
规则0:阅读有关JVM和微型基准测试的好论文。比如https://www.ibm.com/developerworks/java/library/j-jtp02225/。不要对这种测试有太高的期望;它们对JVM性能的测试仅能起到有限效果。
规则1:始终包含一个预热阶段,它一直运行,直到触发所有的初始化和编译。 (预热阶段的迭代次数可以减少,经验法则是几万次循环。)
规则2:始终使用-XX:+PrintCompilation -verbose:gc 等参数来运行,这样可以确定编译阶段和JVM的其他部分在计时时是否进行了一些意外的工作。
规则2.1:在计时和预热阶段的开始和结束打印消息,这样可以确定计时时是否有规则2的输出。
规则3:了解-client与-server之间的区别,还有OSR和常规汇编之间的区别。 -server优于-client, 常规编译优于OSR
规则4:注意初始化的影响,第一次计时不要打印结果,除非是在测试类加载的过程,规则2是你对抗这种效果的第一道防线。
规则5:注意编译器优化和重新编译的效果。计时时不要使用任何代码路径,因为编译器可能会基于一些乐观假设进行优化,导致根本不会使用该路径,从而可以对代码进行垃圾和重新编译。规则2是你对抗这种效果的第一道防线。
规则6:使用适当的工具读取编译器的工作过程,并对它产生一些令人惊讶的代码做好。在形成关于什么使事情更快或更慢的理论之前自己检查代码。
规则7:减少测量中的噪音。在一台安静的机器上运行基准测试,并运行几次,抛弃异常值。使用-Xbatch将编译器与应用程序串行化,并考虑设置 -XX:CICompilerCount = 1以防止编译器与其自身并行运行。
规则8:使用一些库用来做基准测试,因为它可能更有效率。比如JMH,Caliper,UCSD Benchmarks for Java等。
]]>在提交任务时,如果线程池已经被占满,任务会进到一个队列里等待执行。
这种机制在一些特定情况下会有些问题。今天我就遇到一种情况:创建线程比线程执行的速度要快的多,而且单个线程占用的内存又多,所以很快内存就爆了。
想了一个办法,就是在提交任务之前,先检查目前正在执行的线程数目,只有没把线程池占满的时候在去提交任务。
代码很简单:
1 | int threadCount = ((ThreadPoolExecutor)executor).getActiveCount(); |
有两种形式可以配置多个repository, 配置多个profile或者在同一个profile中配置多个repository.配置多profile时,还需要配置activeProfiles使配置生效。
配置示例:
1 | <profiles> |
单profile,多repository的配置也类似。
这样就实现了多站点的配置。下载依赖时,maven会按照配置从上到下的顺序,依次尝试从各个地址下载,成功下载为止。
个人感觉mirror的存在有些鸡肋,如果不想用repository里配的地址,完全可以直接改掉,而不用再加一条mirror配置。
如果setting.xml和pom里都配置了repository, 配置的mirror是可以对两个配置文件都生效的,这可能是mirror存在的唯一意义。
mirror的配置示例:
1 | <mirror> |
使用mirrorOf指定这个镜像是针对哪个repository的,配置成*就表示要代理所有repository的请求。
需要注意的是,与repository不同,配置到同一个repository的多个mirror时,相互之间是备份的关系,只有在仓库连不上的时候才会切换另一个,而如果在能连上的情况下找不到包,是不会尝试下一个地址的。
所以,一般情况下,无论是配置国内的maven仓库,还是配置nexus之类私服,都可以直接配置成repository, 这样即使配置的这些仓库有些问题导致一些包下不下来,也可以继续用别的仓库尝试。
]]>但是在不使用eclipse的时候呢?其实,借助maven,我们很容易实现同样功能。maven提供了一个shade plugin,可以用来打fat jar, 同时也提供了指定main方法的功能。
1 | <project> |
然后在用maven打包的时候就可以打出直接可运行的包了。
]]>首先需要在工程中添加activemq的依赖,为了方便,可以直接用activemq-all包。
1 | <dependency> |
Actviemq plugin的开发非常方便,只需要实现两个类。
一个是plugin类, 需要implements BrokerPlugin这个接口,然后将接口的installPlugin方法实现了即可。
这个方法的唯一作用就是指定一个broker类,代码如下:
1 | public Broker installPlugin(Broker broker) throws Exception { |
接下来就是在实现一个broker类,一般情况下可以extends BrokerFilter 这个类,然后选择需要的方法重写,构造方法和重写的每个方法执行完毕后,执行以下super类的对应方法,这样就不会对activemq的实际运行造成影响。
例如:
1 | public FoxBroker(Broker next) { |
这里我们重写了activemq的send方法, 并且增加了一些日志。
然后打包,将jar包放到activemq的lib目录下。再在activemq.xml下增加plugins这个模块,并把需要的插件依次列在里边。
1 | <plugins> |
重启activemq,再收发消息的时候就能看到效果了。
可以参考我在github上放的代码: https://github.com/lcy362/FoxActivemqPlugin
里边提供了两个非常简单的插件示例。FoxBrokerPlugin会在收发消息时记录生产者、消费者的ip; LimitQueueSizePlugin则可以控制队列大小,队列中积压1000条消息以后新的消息就会被丢弃,在测试环境下会比较有用。
此外,activemq自己也提供了几个常用的插件,包括LoggingBrokerPlugin, StatisticsBrokerPlugin, 等, 也可以参考他们的实现。
]]>这里介绍一种操作相对简单的方法,就是在运行之前把需要的jar包都加入到classpath中。
具体来说,就是写一个shell脚本,定义一个参数,可以就叫CLASSPATH, 也可以叫别的。
CLASSPATH=yourownjar.jar:xxx.jar:/xx/xx/xxx1.jar:”$CLASSPATH”
需要注意的是,自己写的主类所在的jar也要包含在自己定义的classpath中.
然后使用java -classpath命令运行即可:
java -classpath ${CLASSPATH} xx.Main
]]>1 | Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes |
查了一下,跟jar包的签名有关。关于签名可以参考: http://www.cnblogs.com/jackofhearts/p/jar_signing.html
有些Jar包会在metainf里包含一个.SF:包含原Jar包内的class文件和资源文件的Hash, 用来校验文件的完整度等验证。
但是在打fat-jar的时候,我们是把很多jar包合成了一个,这样fatjar下就会存在各个jar包中的签名文件,但是他们显然无法跟最终的fatjar作校验。
解决方法就是打包时把签名文件全都去掉,如果是使用maven的话,可以使用shade插件:
1 | <plugin> |
cluster是使用数据分片的形式实现的,一个 Redis cluster集群包含 16384 个哈希槽, 任意一个key都可以通过 CRC16(key) % 16384 这个公式计算出应当属于哪个槽。每个槽应当落在哪个节点上,也是事先定好。这样,进行任一操作时,首先会根据key计算出对应的节点,然后操作相应的节点就可以了。
所以说,其实cluster跟单点相比,只是多了一个给key计算sharding值的过程,并没有增加多少复杂度,完全可以放心使用。
在这样的设计下,一些对节点的操作也很方便,操作过程中对client端也不会有影响。
1. 增加节点:将原有节点的某些slot转移到新节点上
2. 删除节点: 将节点上的槽转移到其他节点上以后,移除空节点
3. 节点故障:落到故障节点上的操作会失败,而集群其他部分可以正常访问,这样就不会出现一致性哈希方案的“雪崩”问题。单点的故障也可以通过配置slave解决,节点故障时可以由对应的slave顶替
4. 节点重启: 正常重启即可,节点会自动读之前的配置然后加入集群中。
然后,我们可以实际安装一次,由于只是测试,装个两三个节点就可以,而如果是生产使用的话,至少需要3个master节点,也推荐同时至少有3个slave节点才能使用。
到redis官网(https://redis.io/download)上下载最新的stable版本。
以3.2.8为例,下载完以后执行以下命令
1 | $ tar xzf redis-3.2.8.tar.gz |
然后解压缩,make就可以了.
make之后会在redis的src目录下多出几个命令,包括redis-server, redis-cli等,可以把他们拷到/usr/local/bin下,这样就可以在任意路径下执行。也可以自己指到src目录下进行操作。
启动之前需要先修改一下配置,redis的根目录下有一个redis.conf文件,可以直接改它,也可以拷到别的路径下进行修改。
主要就是把cluster模式打开,然后配一下cluster配置文件。
1 | cluster-enabled yes |
其他的配置,port,bind,protect-mode之类的,可以根据需求改一下,不改也没关系。如果是想在一台机器上启动多个实例的话,需要给每个实例一个配置文件, port, cluster-config-file这些配置也需要区分一下。
然后执行以下命令,分别指定不同的配置文件启动。
1 | redis-server redis-6379.conf & |
需要的节点都启动以后,就可以开始配置cluster了。推荐的办法是使用官方提供的redis-trib.rb工具包,这个具体操作可以参考官方文档。我们在这里试一下手动操作,这样其实可以加深对cluster原理的理解。
首先通过redis-cli命令进入任意一个redis实例中,然后一次对其他几个节点执行meet命令。
1 | Cluster meet xxx:6380 |
然后执行cluster nodes命令就可以看到节点被加入到集群中了。
这之后还有一步是分配slot, 可以使用cluster setslot命令,不过由于slot比较多,分配起来很麻烦。还有另外一种方式是修改nodes.conf这个配置文件。
Nodes.conf这个配置包含了cluster相关的几乎所有信息,一般情况下无需自己去操作,redis会自动生成,可以打开看一下,里边每个节点都对应一行,一行是类似这种格式:
1 | 8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected |
然后slot的分配情况是加在最后
1 | 8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected 0-8000 |
需要注意的是,全部16384(0-16383)个slot都要分配出去,不能留空;集群中每个节点都对应一个Nodes.conf文件,每个文件里的slot配置要一致。
改完以后重启所有节点,在redis-cli中执行cluster info 命令,可以看到一些关键信息
1 | cluster_state:ok |
所有slot已经分配出去,Cluster stat也变成了ok. 这样一个cluster就算搭建完毕了。
]]>我们希望的效果至少要包括这么几个特点:
1. 元素自动去重,这是一个set的基本要求;
2. 增删改这些对集合单个元素的操作,无须处理其他元素;
3. 方便查询,包括查询整个集合,和判断某一特定元素是否存在
这里提供一种思路实现这种效果,就是把元素的值存放在hbase的qualifier中,作为一个键,而value则随便塞一个值,不实际使用。
这样,由于hbase本身会对qualifier判重,所以元素不会重复,所有对单个元素的操作,都只需要操作这个qualifier, 需要获取整个集合时,也可以通过直接查这一行来实现。
]]>1 |
|
和log4j相比,主要有这么一些变化,
首先整体结构上变化很大,appender、logger都被集中到了各自的一个根节点下。
xml各节点的名称也采用了新设计,名称直接就是有用信息,不再是之前appender xxx=”xxx”, param xxx=”xxx”的形式。
然后一些属性,包括fileName等,只能作为节点属性配置,不能像log4j那样配置成子节点。
此外,log4j2归档时支持压缩,在RollingFile节点的filePattern属性里将文件名后缀写成gz,zip等压缩格式,log4j2会自动选择相应压缩算法进行压缩。
现在发现的就这些,引入这个xml配置,再引用log4j-core, log4j-api包,就可以使用log4j2了。此外,如果有需要,可以用log4j-slf4j-impl,log4j-jcl,log4j-1.2-api分别实现对slf4j, jcl,log4j的兼容。
]]>其实很简单,万能的apache-commons早就想到了这一点,所以在commons-collections4中增加了外部输入equals和hashcode的方法,甚至equals和hashcode方法本身也不需要我们自己写代码,可以用comons-lang包实现,具体代码如下
1 | <dependency> |
1 | public static <T> boolean isEqualCollection(Collection<T> l1, Collection<T> l2, final String... exludedFields) { |
但是hmset这个命令,包括redis本身,jedis都没有提供nx版本的支持。当然,hset这个命令是有对应的hsetnx版本的,hmset意思就是multi hset,一次可以操作多个key, 从而减小网络开销。
所以,为了在使用hmset时也能降低网络的消耗,用lua写了一个脚本,实现hmsetnx的效果,即:向Hash表中set键值对时,只有键不存在时才会写入,不会覆盖原有值。
1 | local key |
脚本的原理还是比较简单,脚本中使用的参数和hmset完全一致。依次读入参数列表,迭代器i是奇数时给key赋值,偶数时执行一次hsetnx,循环结束后也就完成了。
之后再调用jedis封装好的eval接口,
Object eval(final String script, final List
或者
Object eval(final byte[] script, final List<byte[]> keys, final List<byte[]> args)
都可以,这两个接口的区别就是是否对参数进行序列化
keys中只放一个元素,就是hash表本身的key, 然后把键值对按照一个key,一个value的顺序依次放到args里。
当然,也可以用evalsha命令避免每次操作都要传输脚本本身,这里就不细说了。
]]>直接去官网(https://www.virtualbox.org/wiki/Downloads)下载即可
同样官网下载(https://www.centos.org/download/),我下载的是minimal iso
安装过程很简单,一路默认点下去就可以,中间内存、分区什么的可以根据需要调一下
vitualbox默认的分辨率非常低,可以通过安装增强工具进行优化。不过由于我们不需要图形化界面,其实可以通过其他方式解决这一问题,就是用xshell或者putty通过ssh远程登陆到虚拟机上。
1 | service sshd start |
分别启动ssh服务,并将ssh设定为自启动
由于只是弄着玩的,直接把防火墙关掉,方便。
centos7的防火墙操作和之前版本区别很大:
1 | sudo systemctl stop firewalld.service |
关闭防火墙和自动启动
在VitualBox下配置端口转发:设置-网络-高级-端口转发,将22端口转发到主机的端口上,可以同样是22,也可以配置成其他端口.
如果需要在主机上访问虚拟机的其他端口,例如tomcat的8080,activemq的61616,8161,也可以在这儿一并配了。
在主机上执行ipconfig,找sudo systemctl disable firewalld.service对应的ip, 然后就可以在xshell中配置对应的ip和端口,访问虚拟机了。
centos下配置自启动的方式很多,我们在这里提供一种最简单的方式。
例如vim /opt/app/service.sh
1 | #!/bin/bash |
把需要自启动的脚本全都放这儿,以后想增加自启动服务的时候,也只需要操作这个脚本。
chmod +x /opt/app/service.sh
centos7下/etc/rc.d/rc.local也需要自己去加执行权限:
chmod +x /etc/rc.d/rc.local
然后打开/etc/rc.d/rc.local,在最后把自己写的脚本加上:
/opt/app/service.sh
保存,就完成自启动服务的配置了。
之后,我们可以通过vitualbox的无界面方式启动,然后在xshell中自由操作。
]]>本文会介绍在一个单独的java进程(java standalone application)中嵌入hawtio,对应官方文档(http://hawt.io/getstarted/index.html)的 “Using hawtio inside a stand alone Java application”,不过这一节文档问题是比较多的,如果你只看这段,会遇到各种问题。
下面介绍具体步骤
除了官方文档里说的hawtio-embedded外,hawtio-insight-log,hawtio-core,hawtio-web这几个包都是必需的,我们都引入当前的最新版本.
1 | <dependency> |
同样在官方start文档(http://hawt.io/getstarted/index.html)中下载hawtio-default.war包,放到任意位置,war包的名字也可以随便改
基本的代码,如果不需要其他配置,非常简单
1 | Main main = new Main(); |
使用main.setWar配置的是war包具体路径,可以在工程内,也可以在工程外,但是并非官方文档所说的“包含war包的目录路径(somePathOrDirectoryContainingHawtioWar)”
之后运行main.run就可以启动hawtio了.
新版本的hawtio默认是要密码的,如果想简单,可以配置一条jvm参数: -Dhawtio.authenticationEnabled=false, 关掉权限验证。
示例代码可以在github (https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java) 上看,可以直接下载运行,不过需要在本地启动一个activemq. 这里的例子是一个使用hawtio监控apache-camel的简单例子,启动后,在hawtio页面上栏可以直接看到camel的标签,使用非常方便。
]]>其中,slf4j只包含日志的接口,logback只包括日志的具体实现,两者加起来才是一个完整的日志系统。Log4j则同时包含了日志接口和实现。
这两套日志系统之间有可以相互兼容的组件,分别是slf4j-log4j12和 log4j-over-slf4j,引入之后就可以用log4j打出slf4j接口的日志,或者用logback打出log4j接口的日志。
背景知识介绍到这里,再简单说一下标题里提到的问题。问题的现象就是我们在war包里配置了log4j的日志级别为info, 但在catalina里却一直在打大量的debug日志。初看现象肯定很诡异,前期各种研究tomcat配置也没什么头绪。直到磁盘压力太大,去看jstack发现大量进程是等待在Logback代码中,才发现之前关注错了重点。再去具体了解了java下的日志系统后,问题也就很明了了。
先把几个事实摆出来:
1. 打出的debug日志都是用slf4j写的,根据堆栈得知logback具体执行了日志打印
2. logback在无配置文件时默认debug级别
3. 我们的war包中同时包含logback,log4j和slf4j-log4j12
4. Slf4j无法主动选择具体的日志实现
想必看到这里,大家也明白了问题所在。根据我们引入的包,log4j和logback都可以实现打印slf4j日志,而具体谁来打,不是一个用正常办法可以控制的事情,在这个具体案例下,logback就成了具体的日志打印者。而因为我们其实是想用lo4j打日志,所以没有配logback配置,所以logback就按默认的debug级别打了大量日志。
解决办法也很简单,就是把logback的包全去掉。看似有些暴力,但确实是最合理的一个解决办法。
最后提供一个排查日志问题的通用套路,免得找不到方向乱看。
1. 找几行不符合自己日志配置的具体日志,翻阅对应代码,看看是哪个日志接口打的
2. 查jar包,看看这套日志框架有哪些具体实现
3. 把多的jar包去掉
json选择用fastjson.
序列化工具使用了protostuff和kyro. 为什么不用protobuf呢?因为感觉对于一个已有的上百个属性的java class来说,再去新建一个匹配的proto文件有点反人类。protostuff是protobuf的改良版本,可以直接将一个java object进行序列化,使用方法与kyro有点类似,没有protobuf那么多中间过程。其他的,hession, java自带序列化之类的,据说性能比kryo和protobuf差很多,就不测了。
简单测了一下,发现差距还挺明显的,所以感觉也不需要做具体的评测了。把日志截一段发出来,大家感受下。
fastjson serilise cost <span class="hljs-number">555805</span> <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>kyro serilise cost <span class="hljs-number">227375</span> length502protostuff serilise cost <span class="hljs-number">78950</span> length633fastjson deserilise cost <span class="hljs-number">130662</span>kyro deserilise cost <span class="hljs-number">201716</span>protostuff deserilise cost <span class="hljs-number">230533</span>fastjson serilise cost <span class="hljs-number">727915</span> <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>kyro serilise cost <span class="hljs-number">378958</span> length502protostuff serilise cost <span class="hljs-number">94739</span> length633fastjson deserilise cost <span class="hljs-number">154346</span>kyro deserilise cost <span class="hljs-number">373432</span>protostuff deserilise cost <span class="hljs-number">219085</span>fastjson serilise cost <span class="hljs-number">804892</span> <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>kyro serilise cost <span class="hljs-number">392380</span> length502protostuff serilise cost <span class="hljs-number">220664</span> length633fastjson deserilise cost <span class="hljs-number">243560</span>kyro deserilise cost <span class="hljs-number">360010</span>protostuff deserilise cost <span class="hljs-number">132241</span>fastjson serilise cost <span class="hljs-number">601991</span> <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>kyro serilise cost <span class="hljs-number">244349</span> length502protostuff serilise cost <span class="hljs-number">80924</span> length633fastjson deserilise cost <span class="hljs-number">241191</span>kyro deserilise cost <span class="hljs-number">230928</span>protostuff deserilise cost <span class="hljs-number">127109</span>
cost的时间用的是System.nanoTime(); 三种用的都是不加任何配置的默认配置。
序列化之后的占用空间,kryo略低于protostuff, 两者都远高于json. 这是很好理解的,毕竟json串是可读的,不要强求太多。
而序列化和反序列化的耗时,都是protostuff优于kyro优于fastjson, 而且差别挺明显。
所以结论呢,如果对空间没有极其苛刻的要求,protostuff也许是最佳选择。protostuff相比于kyro还有一个额外的好处,就是如果序列化之后,反序列化之前这段时间内,java class增加了字段(这在实际业务中是无法避免的事情),kyro就废了。但是protostuff只要保证新字段添加在类的最后,而且用的是sun系列的JDK, 是可以正常使用的。因此,如果序列化是用在缓存等场景下,序列化对象需要存储很久,也就只能选择protostuff了。
当然,如果有可读性之类的需求,就只能用json了。
]]>
分布式锁,顾名思义,就是分布式的锁,应用于一些分布式系统中。例如,有一个服务部在数太机器上,然后有可能操作数据库中的同一条记录。这时,就需要分布式锁。
分布式锁实现的方式很多,一般来说需要一个实体来代表一个锁,占用锁时就新建这个实体,锁释放时也对应将相应实体删除。同时,一般还需要一个锁超时过期的策略,避免一些异常情况造成锁无法被释放。
zookeeper和redis都是常用的实现分布式锁的方式。接下来就简单介绍一下这两种方式的使用。
使用zookeeper的话,建议直接使用curator客户端.
<span class="hljs-tag"><<span class="hljs-title">dependency</span>></span> <span class="hljs-tag"><<span class="hljs-title">groupId</span>></span>org.apache.curator<span class="hljs-tag"></<span class="hljs-title">groupId</span>></span> <span class="hljs-tag"><<span class="hljs-title">artifactId</span>></span>curator-recipes<span class="hljs-tag"></<span class="hljs-title">artifactId</span>></span> <span class="hljs-tag"><<span class="hljs-title">version</span>></span>2.9.1<span class="hljs-tag"></<span class="hljs-title">version</span>></span> <span class="hljs-tag"></<span class="hljs-title">dependency</span>></span>`</pre>curator中实现了一个InterProcessSemaphoreMutex类,用作分布式锁。实现原理其实也很直白:建立锁的时候约定一个路径新建一个节点,作为锁的实体;锁释放时就将这个节点删除。代码例子片段:<pre class="prettyprint">`InterProcessSemaphoreMutex lock;lock.acquire(…); <span class="hljs-comment">//acquire获取锁</span>….lock.release();释放锁
详细的使用例子代码可以参考 https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/zklock/InterProcessMutexExample.java
同样推荐一个第三方的redis客户端redisson, https://github.com/redisson/redisson. redisson的知名度不如curator高,但也是一个非常优秀的开源工具,支持各种集群、数据结构。
redis锁的原理就是占用锁时新建一个key, 锁释放时key删除。
]]>Given a string, find the length of the longest substring without repeating characters.
Examples:
Given “abcabcbb”, the answer is “abc”, which the length is 3.
Given “bbbbb”, the answer is “b”, with the length of 1.
Given “pwwkew”, the answer is “wke”, with the length of 3. Note that the answer must be a substring, “pwke” is a subsequence and not a substring.
也就是说给定一个字符串,输出不包含重复字母的最长子串长度。
遍历一次字符串,O(n)复杂度下可以解决。主要思路就是在遍历的过程中
1. 记录每个字母上一次出现的位置
2. 维持一个从当前位置往前数不包含重复字母的子串,记录这个字串的起止位置start, end
遍历的过程中就是根据相应位置字母是否出现过,以及上次出现的位置,不断更新start, end的过程。
可以到github上查看: https://github.com/lcy362/Algorithms/tree/master/src/main/java/com/mallow/algorithm
<span class="hljs-keyword">import</span> java.util.HashMap;<span class="hljs-javadoc">/** * leetcode 3 * https://leetcode.com/problems/longest-substring-without-repeating-characters/ * Created by lcy on 2017/2/15. */</span><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LongestSubstringNotRepeat</span> {</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">lengthOfLongestSubstring</span>(String s) { <span class="hljs-keyword">if</span> (s.length() <= <span class="hljs-number">1</span>) { <span class="hljs-keyword">return</span> s.length(); } HashMap<Character, Integer> charPos = <span class="hljs-keyword">new</span> HashMap<>(); <span class="hljs-keyword">char</span>[] chars = s.toCharArray(); <span class="hljs-keyword">int</span> len = <span class="hljs-number">0</span>; <span class="hljs-keyword">int</span> max = <span class="hljs-number">0</span>; <span class="hljs-keyword">int</span> start = <span class="hljs-number">0</span>; <span class="hljs-keyword">int</span> end = <span class="hljs-number">0</span>; <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < chars.length; i++) { <span class="hljs-keyword">if</span> (charPos.containsKey(chars[i])) { <span class="hljs-keyword">int</span> tempstart = charPos.get(chars[i]) + <span class="hljs-number">1</span>; <span class="hljs-keyword">if</span> (tempstart > start) { start = tempstart; } end++; len = end - start; } <span class="hljs-keyword">else</span> { len++; end++; } charPos.put(chars[i], i); <span class="hljs-keyword">if</span> (len > max) { max = len; } } <span class="hljs-keyword">return</span> max; } <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String args[]) { LongestSubstringNotRepeat l = <span class="hljs-keyword">new</span> LongestSubstringNotRepeat(); System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"abcabcbb"</span>)); System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"bbbbb"</span>)); System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"pwwkew"</span>)); System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"abba"</span>)); }
]]>这个问题主要还是需要设计一下锁的策略,这里只是提供了一种方式:
每个线程占用两把锁,分别代表自己(self)和前一个线程(prev), 三个线程的持有锁情况如下表所示:
线程号 | prev锁 | self锁 |
---|---|---|
A | c | a |
B | a | b |
C | b | c |
A 首先启动,持有ac, 运行后先释放a, b可以执行。
线程run方法代码如下:
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span>() { <span class="hljs-keyword">while</span> (<span class="hljs-keyword">true</span>) { <span class="hljs-keyword">synchronized</span> (prev) { <span class="hljs-keyword">synchronized</span> (self) { System.out.print(name); self.notify(); } <span class="hljs-keyword">try</span> { <span class="hljs-comment">//如果想控制输出速度, 可以将sleep加在此处</span> <span class="hljs-comment">//如果加在sout之后,会导致c线程启动并占有b锁之后,a线程才会释放a锁,输出顺序会变成acbacb</span> <span class="hljs-comment">//也可以加大三个线程启动的间隔时间解决这一问题</span> <span class="hljs-keyword">try</span> { Thread.sleep(<span class="hljs-number">1000</span>); } <span class="hljs-keyword">catch</span> (InterruptedException e) { e.printStackTrace(); } prev.wait(); } <span class="hljs-keyword">catch</span> (InterruptedException e) { e.printStackTrace(); } } }
需要注意的是,如果想控制输出速度,需要考虑一下sleep的位置和时间,避免在A线程执行完并释放a锁之前,C线程已经启动并持有了B锁,导致B线程无法正常启动。
]]>我司需要接收很多外部数据,数据源的形式很多,ibmmq, activemq, redis pubsub, 等等都有。为了将这些数据接到内部amq/kafka,之前运行了一大批进程,管理起来十分复杂,因此最近用apache-camel对这些进程作了整合。
上线几个小时之后,kafka磁盘空间开始报警。初步断定是这次上线导致的。
主要还是对kafka不熟悉,只是能用而已,因此排查过程走了不少弯路。
由于camel本身文档很不完善,一开始配置参数时也主要靠看源码+猜,所以首先怀疑配置的压缩参数compressionCodec=gzip是否无效。
因此进行了一次简单的测试(别问我为什么一开始不测)。建了一个单分区的测试topic, 然后分别使用gzip和none模式发送相同的数据,观察kafka的log file文件大小变化趋势。发现压缩确实是有效的。
因此又跟直接使用kafka api作了对比,发现差别非常大,即使是压缩之后,使用camel-kafka占用的空间也比使用api大数倍。然后去查了两边源码,发现最底层的代码是完全一致的,所以还是怀疑某些参数配的不对。
之后又注意到一个细节,使用camel时,log的大小跟消息数呈线性关系,比如一条占1字节,10条就占10字节。但使用api的话,1条也占1字节,连续发10条可能才占两字节。
这时候就怀疑kafka是不是有批量发送之类的机制,然后咨询了负责kafka的同事,果然是这个原因,而造成占用空间差别大的原因就是是否同步发的区别,同步发的话就不存在批量发送了。批量发送的话,这一批消息会被压缩在一起,而单条发时,就是每一条分别压缩。我们知道,在文件非常小的时候,使用gzip压缩的效果是很差的,甚至可能压完比源文件还大。然后又做了些测试,确定了是这个问题。这个参数被同事封装在了kafka的client接口里,因此导致我照着之前代码改参数时漏掉了这一个。
其实回想起来,这个问题挺low的,如果对kafka多些了解,是不会有这种问题的。
首先,对kafka没做过全面的了解,只是学会了怎么用,大概了解了一下它是什么。而很多基本的机制都没有概念。使用一个开源工具时,对它做一次全面的了解还是很重要的,虽然每个工具都深入研究底层代码不现实,但是系统性的了解一遍这些工具有什么机制,确实花不了多少时间。
再一个,公司内一般会封装一些访问各种组件的工具包,以提升效率,这些工具包最好也了解一下怎么实现的,否则可能不经意间就掉坑里了。
任重而道远啊..
]]>例如
Integer a = <span class="hljs-number">148</span>; Integer b = <span class="hljs-number">148</span>; System.out.println(a==b);`</pre>这时输出为false. 很容易理解。但是如果把值换成128以下的数,比如48.<pre class="prettyprint">` Integer a = <span class="hljs-number">48</span>; Integer b = <span class="hljs-number">48</span>; System.out.println(a==b);`</pre>这时就会发现输出变成了true。原因是jdk对128以下的整数作了缓存,当声明两个值为48的Integer对象时,其实是指向同一位置。当然也可以强制声明一个新的Integer对象。<pre class="prettyprint">` Integer a = <span class="hljs-number">48</span>; Integer b = <span class="hljs-keyword">new</span> Integer(<span class="hljs-number">48</span>); System.out.println(a==b);
这时输出就变成false了
]]>实现了与activemq等jms实现的交互。
这里主要介绍JmsSpout。由于storm中发送队列数据与普通java程序没有任何区别,专门封装一个bolt显得有些多此一举。
https://github.com/ptgoetz/storm-jms
包中自带了使用spring方式加载队列配置。
1 | JmsProvider jmsQueueProvider = new SpringJmsProvider( |
1 |
|
新建一个xml配置,amq:queue 节点配置队列名。
amq:connectionFactory节点配置activemq的连接url。
代码中调用
1 | JmsProvider jmsQueueProvider = new SpringJmsProvider( |
三个参数依次为配置文件名, connectionFactory和queue节点的名称。
通过实现JmsTupleProducer可以实现个性化的输出。
以JsonTupleProducer 为例:
1 | public class JsonTupleProducer implements JmsTupleProducer { |
即将整条消息作为一个字段emit出去。
declareOutputFields和new Values 即storm中的对应函数。
通过在toTuple中做相应处理,可以实现定制化的输出。
除了将ack数设为0外,conf.setNumAckers(0);
还需要将jms的确认模式修改为自动ack:
1 | queueSpout.setJmsAcknowledgeMode(Session.AUTO_ACKNOWLEDGE); |
JmsProvider类是JmsSpout中用于建立到队列的连接所用的类。
默认的SpringJmsProvider是通过spring配置来获取jms连接。
如果有特殊需求,也可以自己实现JmsProvider中的两个接口,用于获取连接ConnectionFactory和队列Destination
https://github.com/apache/storm/tree/master/external/storm-kafka
从kafka中获取数据。
1 | String topic = ""; //队列名 |
HdfsBolt支持向hdfs写入数据
1 | SyncPolicy syncPolicy = new CountSyncPolicy(100); //每100条hdfs同步一次磁盘 |
按照示例编码,输出到hdfs形式如下:
bolt接收到的每条数据为hdfs中的一行;
输出到hdfs路径为 设定的路径/日期/默认文件名;
hdfsbolt的每个线程会输出到一个文件,topology重启会产生新文件;
文件每天归档
FileSizeRotationPolicy 支持按固定文件大小归档;TimedRotationPolicy支持按固定时间归档;
如有其他归档方式需求,可以实现FileRotationPolicy接口,参考DailyRotationPolicy源码。
通过实现FileNameFormat接口自定义文件路径及文件名,参考FileFolderByDateNameFormat
FileFolderByDateNameFormat实现目前常用的存储路径。
使用示例
1 | FileFolderByDateNameFormat fileNameFormat = new FileFolderByDateNameFormat() |
会自动在路径之后增加日期,文件名中也会增加日期。
除了storm-hdfs本身源码以外,可以参考https://github.com/lcy362/StormTrooper 查看FileFolderByDateNameFormat和DailyRotationPolicy的实现。
]]>jstorm的UI相对于storm提供了更为丰富的监控项。UI本身是在tomcat中运行的一个war包,进行二次开发也相对容易。
cluster的整体信息, conf中是nimbus节点的配置。
当前运行的所有topology列表及概要信息,conf中对应的是topology单独配置的配置项。
所有supervisor列表, 可以查看supervisor的配置、日志等。
主要关注Topology Graph 项,这是Component Metrics 页的概要信息,可以较直观的看出运行状况。
其中,节点大小和箭头粗细代表数据量,节点颜色用于区分spout和bolt。箭头颜色对应TupleLifeCycle属性,由绿到红越来越慢。箭头变红色时,可以适当增加下游节点的并行度。
包含每个spout, bolt的统计信息。
各参数说明:
tasks : 并行度。对应代码中为每个spout/bolt设置的并行度。
emitted, acked, failed : 发射/收到ack/未收到ack的消息数。
sendTps, recvTps: 发送/接收数据tps.
需要注意,以上数据交互类的指标包含了与storm本身组件,topology_master,acker之间的交互,只能用来查看运行状况,不适合统计用。
process: 对于bolt来说,是excute()函数的执行时间,对于spout来说,是整条消息(包含下游各步)被完整处理的时间,体现系统运行效率的主要参数。
TupleLifeCycle: 从上游消息被emit,到消息处理完所需时间。和process相比相差太大时,说明在bolt队列中等待了较长时间,可以适当增加并发。
deser, ser: 序列化/反序列化耗时,一般无需关注. storm中每一个emit出来的数据都需要做序列化和反序列化。
error: 鼠标悬浮在E上,可以查看具体的报错信息。主要寻找带异常堆栈的数据, 一些诸如is dead, no response之类的信息无需关注。
]]>topology里spout/bolt的整体结构不再细讲,主要说说storm/jstorm topology运行时与传统java程序可能存在的区别。其实区别非常少,主要也体现在初始化上,本文的目的在于帮助开发人员在无需了解storm内核原理的情况下,排查topology程序可能出现的问题。
1个topology会包含多个spout线程和bolt线程,分散运行在数个worker(进程)中。同一个worker中可能同时运行多个bolt/spout的数个线程。
main方法只在启动时运行在nimbus中,因此除了storm本身的配置项外,其他程序相关的配置,如spring配置等,配置在main方法中不会起作用。
bolt 的主体结构包含prepare, excute, cleanup 三部分。
其中,prepare在初始化时执行一次,cleanup在退出前执行一次,excute每条消息执行。
一些配置,包括加密,spring加载等,建议都放到prepare中。多个bolt都需要加载spring时,建议使用同样的配置,避免一些诡异问题。
所有静态代码块中作了初始化的变量,emit的变量,由于都存在网络传输,需要能够被序列化。
storm默认使用kyro序列化,需要类有无参构造函数。如果无法增加无参构造函数,设置topology.fall.back.on.java.serialization: true使用java自带的序列化。
]]><bean id="securityConstraint" class="org.eclipse.jetty.util.security.Constraint"> <property name="name" value="BASIC" /> <property name=”roles” value=”admin” /> <property name=”authenticate” value=”true” /> </bean> |
<bean id="securityConstraintMapping" class="org.eclipse.jetty.security.ConstraintMapping"> <property name="constraint" ref="securityConstraint" /> <property name="pathSpec" value="/admin/send.jsp/" /> </bean> |
<bean id="securityHandler" class="org.eclipse.jetty.security.ConstraintSecurityHandler"> <property name="constraintMappings"> <list> <ref bean="securityConstraintMapping" /> </list> </property> |
]]>
先说一点,鼠标只到UI上的标题栏时,是可以看到这一属性的具体属性的,几篇google rank很高的文章,其实就是把这个信息整理了下来。
其实大部分属性都是很直白的,看到名字就知道是什么意思,我在这儿之把一些可能造成困扰的属性列一下,方便大家查问题。
]]>
通过jmap监控,可以看到java.util.concurrent.locks.ReentrantLock, org.apache.activemq.pool.PooledConnection这两个类占用的空间非常大,而且增长速度也很快。
网上查了一下,正好找到activemq的bug 报告.:https://issues.apache.org/jira/browse/AMQ-3997
这个bug 在5.7中已经修复,可以通过升级版本解决。
同时,也有另一种解决方式,就是使用spring带的连接池替换activemq自带的连接池,配置如下:
<bean id="jmsConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="vm://205-amq-broker2?create=false&waitForStart=10000" /> </bean><!-- <bean id="pooledConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" init-method="start" destroy-method="stop"> <property name="maxConnections" value="8" /> <property name="connectionFactory" ref="jmsConnectionFactory" /> </bean>--> <bean id="cachedConnectionFactory" class="<span style="color:#ff0000;">org.springframework.jms.connection.CachingConnectionFactory</span>"> <property name="targetConnectionFactory" ref="jmsConnectionFactory"></property> <property name="sessionCacheSize" value="10"></property> </bean> <bean id="jmsConfig" class="org.apache.camel.component.jms.JmsConfiguration"> <property name="connectionFactory" ref="cachedConnectionFactory"/> <property name="concurrentConsumers" value="10"/> </bean> <bean id="activemq" class="org.apache.activemq.camel.component.ActiveMQComponent"> <property name="configuration" ref="jmsConfig"/>
]]>
http://camel.apache.org/activemq.html
关于vm这种传输方式,参考http://activemq.apache.org/vm-transport-reference.html
看了下日志,发现这种配置下camel会有一个很严重的问题: camel每次执行转发操作时,都会新建一个到activemq的连接,之后再将其关闭。这严重拖慢了转发效率,因为事实上每次转发都可以使用同一个连接。
因此查了一下camel文档,找到了 http://camel.apache.org/activemq.html 。 里边有关于线程池的配置:
<pre name="code" class="html"><bean id="jmsConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616" /></bean><bean id="pooledConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" init-method="start" destroy-method="stop"> <property name="maxConnections" value="8" /> <property name="connectionFactory" ref="jmsConnectionFactory" /></bean><bean id="jmsConfig" class="org.apache.camel.component.jms.JmsConfiguration"> <property name="connectionFactory" ref="pooledConnectionFactory"/> <property name="concurrentConsumers" value="10"/></bean><bean id="activemq" class="org.apache.activemq.camel.component.ActiveMQComponent"> <property name="configuration" ref="jmsConfig"/> <!-- if we are using transacted then enable CACHE_CONSUMER (if not using XA) to run faster see more details at: http://camel.apache.org/jms <property name="transacted" value="true"/> <property name="cacheLevelName" value="CACHE_CONSUMER" /> --></bean>
这个正好符合我们的需要。而且顺便把连接换成了多线程,可以进一步提升效率。
需要注意的是,如果使用的是activemq5.6, 这样做会导致内存泄露,我会在下一篇博客中详述。
]]>
前几天开始学习lda,走了不少弯路,对lda仍然是一头雾水。看了这篇文档以后总算明白lda是干啥的了
最近在看LDA算法,经过了几天挣扎,总算大致了解了这个算法的整体框架和流程。
LDA要干的事情简单来说就是为一堆文档进行聚类(所以是非监督学习),一种topic就是一类,要聚成的topic数目是事先指定的。聚类的结果是一个概率,而不是布尔型的100%属于某个类。国外有个博客[1]上有一个清晰的例子,直接引用:
Suppose you have the following set of sentences:
- I like to eat broccoli and bananas.
- I ate a banana and spinach smoothie for breakfast.
- Chinchillas and kittens are cute.
- My sister adopted a kitten yesterday.
- Look at this cute hamster munching on a piece of broccoli.
What is latent Dirichlet allocation? It’s a way of automatically discovering topics that these sentences contain. For example, given these sentences and asked for 2 topics, LDA might produce something like
- Sentences 1 and 2: 100% Topic A
- Sentences 3 and 4: 100% Topic B
- Sentence 5: 60% Topic A, 40% Topic B
- Topic A: 30% broccoli, 15% bananas, 10% breakfast, 10% munching, … (at which point, you could interpret topic A to be about food)
- Topic B: 20% chinchillas, 20% kittens, 20% cute, 15% hamster, … (at which point, you could interpret topic B to be about cute animals)
上面关于sentence 5的结果,可以看出来是一个明显的概率类型的聚类结果(sentence 1和2正好都是100%的确定性结果)。
再看例子里的结果,除了为每句话得出了一个概率的聚类结果,而且对每个Topic,都有代表性的词以及一个比例。以Topic A为例,就是说所有对应到Topic A的词里面,有30%的词是broccoli。在LDA算法中,会把每一个文档中的每一个词对应到一个Topic,所以能算出上面这个比例。这些词为描述这个Topic起了一个很好的指导意义,我想这就是LDA区别于传统文本聚类的优势吧。
先定义一些字母的含义:
LDA以文档集合D作为输入(会有切词,去停用词,取词干等常见的预处理,略去不表),希望训练出的两个结果向量(设聚成k个Topic,VOC中共包含m个词):
LDA的核心公式如下:
*p(w|d) = p(w|t)p(t|d)
直观的看这个公式,就是以Topic作为中间层,可以通过当前的θd和φt给出了文档d中出现单词w的概率。其中p(t|d)利用θd计算得到,p(w|t)利用φt计算得到。
实际上,利用当前的θd和φt,我们可以为一个文档中的一个单词计算它对应任意一个Topic时的p(w|d),然后根据这些结果来更新这个词应该对应的topic。然后,如果这个更新改变了这个单词所对应的Topic,就会反过来影响θd和φt。
LDA算法开始时,先随机地给θd和φt赋值(对所有的d和t)。然后上述过程不断重复,最终收敛到的结果就是LDA的输出。
再详细说一下这个迭代的学习过程:
针对一个特定的文档ds中的第i单词wi,如果令该单词对应的topic为tj,可以把上述公式改写为:
pj(wi|ds) = p(wi|tj)*p(tj|ds)
先不管这个值怎么计算(可以先理解成直接从θds和φtj中取对应的项。实际没这么简单,但对理解整个LDA流程没什么影响,后文再说。)。现在我们可以枚举T中的topic,得到所有的pj(wi|ds),其中j取值1~k。然后可以根据这些概率值结果为ds中的第i个单词wi选择一个topic。最简单的想法是取令pj(wi|ds)最大的tj(注意,这个式子里只有j是变量),即
argmax[j]pj(wi|ds)
当然这只是一种方法(好像还不怎么常用),实际上这里怎么选择t在学术界有很多方法,我还没有好好去研究。
然后,如果ds中的第i个单词wi在这里选择了一个与原先不同的topic,就会对θd和φt有影响了(根据前面提到过的这两个向量的计算公式可以很容易知道)。它们的影响又会反过来影响对上面提到的p(w|d)的计算。对D中所有的d中的所有w进行一次p(w|d)的计算并重新选择topic看作一次迭代。这样进行n次循环迭代之后,就会收敛到LDA所需要的结果了。 【在这里突然想到了一个问题,就是对θd和φt这两个向量的更新究竟是在一次迭代对所有的d中的所有w更新之后统一更新(也就是在一次迭代中,θd和φt不变,统一在迭代结束时更新),还是每对一个d中的一个w更新topic之后,就马上对θd和φt进行更新呢?这个看来要去看一下那篇LDA最原始的论文了】
待续……
【1】Introduction to Latent Dirichlet Allocation:国外博客,很不错的入门文章
]]>
package org.apache.hadoop.examples;
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class WordCount {
public static class TokenizerMapper
extends Mapper<Object, Text, Text, IntWritable>{ //继承org.apache.hadoop.mapreduce包中Mapper类,并重写其map方法
private final static IntWritable one = new IntWritable(1); //Mapper<KEYIN,VALUEIN,KEYOUT,VALUEOUT>
private Text word = new Text();
public void map(Object key, Text value, Context context) //Called once for each key/value pair in the input split
throws IOException, InterruptedException { //value值存储的是文本文件中的一行(以回车符为行结束标记),而key值为该行的首字母相对于文本文件的首地址的偏移量
StringTokenizer itr = new StringTokenizer(value.toString()); //拆分成单词
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
context.write(word, one); //输出<word,1>
}
}
}
//系统自动对map结果进行排序等处理,reduce输入例 (asd,1-1-1)
public static class IntSumReducer
extends Reducer<Text,IntWritable,Text,IntWritable> { //Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT>
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values,Context context)
throws IOException, InterruptedException { //reducer输入为Map过程输出,<key,values>中key为单个单词,而values是对应单词的计数值
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length != 2) {
System.err.println(“Usage: wordcount <in> <out>”);
System.exit(2);
}
Job job = new Job(conf, “word count”);
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class); //setMapperClass:设置Mapper,默认为IdentityMapper
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);//setReducerClass:设置Reducer,默认为IdentityReducer
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));//FileInputFormat.addInputPath:设置输入文件的路径,可以是一个文件,一个路径,一个通配符。可以被调用多次添加多个路径
FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));//FileOutputFormat.setOutputPath:设置输出文件的路径,在job运行前此路径不应该存在
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
//setInputFormat:设置map的输入格式,默认为TextInputFormat,key为LongWritable, value为Text
setNumMapTasks:设置map任务的个数,此设置通常不起作用,map任务的个数取决于输入的数据所能分成的input split的个数
setMapRunnerClass:设置MapRunner, map task是由MapRunner运行的,默认为MapRunnable,其功能为读取input split的一个个record,依次调用Mapper的map函数
setMapOutputKeyClass和setMapOutputValueClass:设置Mapper的输出的key-value对的格式
setOutputKeyClass和setOutputValueClass:设置Reducer的输出的key-value对的格式
setPartitionerClass和setNumReduceTasks:设置Partitioner,默认为HashPartitioner,其根据key的hash值来决定进入哪个partition,每个partition被一个reduce task处理,所以partition的个数等于reduce task的个数
setOutputFormat:设置任务的输出格式,默认为TextOutputFormat
]]>
http://www.cnblogs.com/gpcuster/archive/2010/06/04/1751538.html hdfs命令介绍
http://hi.baidu.com/gkf8605/item/d6b8af09c3463512eafe38b1 hdfs命令
http://www.cnblogs.com/forfuture1978/archive/2010/11/14/1877086.html mapreduce入门
http://hadoop.apache.org/docs/r0.20.2/api/index.html hadoop0.20.2 api
]]>