<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>架构设计 on 易哥</title>
		<link>https://yeecode.top/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/</link>
		<description>Recent content in 架构设计 on 易哥</description>
		<generator>Hugo</generator>
		<language>zh-cn</language>
		
		
		
		
			<lastBuildDate>Wed, 07 Oct 2026 00:00:00 +0000</lastBuildDate>
		
			<atom:link href="https://yeecode.top/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml" />
			<item>
				<title>Java多线程状态总结！</title>
				<link>https://yeecode.top/blog/142/</link>
				<pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/142/</guid>
				<description>&lt;p&gt;搞定线程，第一步得把&amp;quot;线程到底有哪几种状态、怎么切换&amp;quot;弄清楚。Java 里线程一共五种状态。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;新建（New）：刚 new 出来的线程处于这个状态，此时线程有自己的内存空间。被触发后进入可运行状态。&lt;/li&gt;&#xA;&lt;li&gt;可运行（Runnable）：具备了运行条件，正等着 CPU 资源。拿到 CPU 后，转入运行中状态。&lt;/li&gt;&#xA;&lt;li&gt;运行中（Running）：占着 CPU，正在处理线程内的任务。可能因为任务结束转入结束状态；也可能因为等用户输入、等锁、等其他线程结束而进入阻塞状态。&lt;/li&gt;&#xA;&lt;li&gt;阻塞（Blocked）：释放了 CPU，要等某些条件满足后才能重新拿到 CPU。阻塞又能细分为三种子状态：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;等待阻塞：对锁对象执行等待方法后，持有锁的线程会进入等待阻塞，直到等待时间到或被重新唤醒，才会进入同步阻塞；&lt;/li&gt;&#xA;&lt;li&gt;同步阻塞：需要拿到同步锁，才能进入可运行状态；&lt;/li&gt;&#xA;&lt;li&gt;其他阻塞：需要等用户输入、等其他线程执行结束等，之后转入可运行状态。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;结束（Dead）：线程已经执行结束。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;图{@fig:engse}给出了线程的状态转换图，并标出了状态转换对应的 Java 操作（图中 t 表示线程对象，Thread 表示线程类，o 代表对象）。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/142/pic/xiancheng_hu_3358928dd6d3e244.webp&#34; alt=&#34;线程状态转化图&#34; width=&#34;758&#34; height=&#34;368&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:engse}&lt;/p&gt;&#xA;&lt;p&gt;上面这些状态里，只有处在&amp;quot;运行中&amp;quot;的线程才占 CPU。某个线程一旦阻塞，CPU 就腾出来给别的线程，既提升了 CPU 利用率，也就提升了程序的整体效率。每个线程还有个优先级属性：优先级高的线程拿到 CPU 的概率更大，但低优先级的线程也不是完全没机会。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;在 AI 编程越来越普及的今天，并发与多线程这些&amp;quot;底层硬功夫&amp;quot;并不会被取代，反而是写好高性能程序绕不开的内功。聊完 Java 线程的状态与转换，如果你觉得架构、并发这些话题有意思，强烈推荐一本书《高性能架构之道（第二版）》。&lt;/p&gt;&#xA;&lt;p&gt;这是一本理论联系实际的架构书，系统讲了怎么从顶层把软件架构好：覆盖分布式、并发编程、数据库调优、缓存、IO、高可用、前端性能优化等众多知识，最后还拿一个真实项目完整走了一遍架构全过程，把书里的方法真正落了地。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;&lt;img src=&#34;https://yeecode.top/img/promo/ha.jpg&#34; width=&#34;500&#34; height=&#34;333&#34; loading=&#34;lazy&#34;  alt=&#34;高性能架构之道（第二版）&#34; /&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;《高性能架构之道（第二版）》&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;这本书还在台湾地区出了繁体版，叫《巨型服务架构》——繁体版嘛，贵得有点离谱。&lt;/p&gt;&#xA;&lt;p&gt;就先聊到这儿。&lt;/p&gt;&#xA;&lt;p&gt;我是软件架构师易哥。可以关注我，我会偶尔冒个泡，分享点编程干货！&lt;/p&gt;</description>
			</item>
			<item>
				<title>什么是微服务系统？</title>
				<link>https://yeecode.top/blog/141/</link>
				<pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/141/</guid>
				<description>&lt;p&gt;在分布式应用里，每个子应用存在的意义，是完成分布式应用中的一部分功能——子应用和应用之间是严格的从属关系。但恰恰是这种严格的从属，可能造成性能浪费。&lt;/p&gt;&#xA;&lt;p&gt;举个例子：有个应用 A，包含三个子应用，A1 管订单、A2 管库存、A3 管金额核算。当我们要做销售金额核算（涉及订单管理和金额核算）时，得调用 A，而 A 下的 A2（库存）和这次操作无关，就这么闲置了。换句话说，应用 A 干活时，无关的那些子应用是闲着的，性能没发挥出来。&lt;/p&gt;&#xA;&lt;p&gt;这就好比商店只卖&amp;quot;汉堡+可乐+薯片&amp;quot;的固定套餐，你不要可乐，买这个套餐也是浪费。避免浪费的办法，是允许你自由组合着买。&lt;/p&gt;&#xA;&lt;p&gt;于是思路就变了：做销售金额核算时，直接调 A1 和 A3；做库存资产核算（涉及库存管理和金额核算）时，直接调 A2 和 A3。不再在子应用外面套一个应用 A，而是让各个子应用直接对外服务，外部调用者按需自由挑选。这就组成了微服务系统。&lt;/p&gt;&#xA;&lt;p&gt;在微服务集群里，每个微服务子应用都是完备的、可独立对外服务的，也能自由组合后对外服务，灵活性很高。图{@fig:uwufe}展示了微服务集群的示意。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/141/pic/weifuwu_hu_3a0ac3e4df199908.webp&#34; alt=&#34;微服务集群&#34; width=&#34;573&#34; height=&#34;364&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:uwufe}&lt;/p&gt;&#xA;&lt;p&gt;每个微服务子应用对各类资源的依赖程度不同、被调用的频次也不同，因此我们可以针对每个微服务去做资源配置、集群配置，提升它们各自的时间效率、资源利用率和容量。&lt;/p&gt;&#xA;&lt;p&gt;在单体应用内部，任何模块都可能和其他模块耦合；而在微服务集群里，每个微服务内聚性高、和其他微服务的耦合很低。所以只要对外接口不变，微服务就能自由改内部逻辑，由独立团队开发、维护、升级，不用去了解别的微服务的实现细节。这有利于提升系统的成熟度、可用性、容错性和可恢复性。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;在 AI 帮我们快速拼出业务功能的今天，怎么把系统拆成边界清晰、能独立演进的服务，恰恰是衡量一个架构好坏的关键。聊完微服务系统的来龙去脉，如果你觉得架构这些话题有意思，强烈推荐一本书《高性能架构之道（第二版）》。&lt;/p&gt;&#xA;&lt;p&gt;这是一本理论联系实际的架构书，系统讲了怎么从顶层把软件架构好：覆盖分布式、并发编程、数据库调优、缓存、IO、高可用、前端性能优化等众多知识，最后还拿一个真实项目完整走了一遍架构全过程，把书里的方法真正落了地。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;&lt;img src=&#34;https://yeecode.top/img/promo/ha.jpg&#34; width=&#34;500&#34; height=&#34;333&#34; loading=&#34;lazy&#34;  alt=&#34;高性能架构之道（第二版）&#34; /&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;《高性能架构之道（第二版）》&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;这本书还在台湾地区出了繁体版，叫《巨型服务架构》——繁体版嘛，贵得有点离谱。&lt;/p&gt;&#xA;&lt;p&gt;就先聊到这儿。&lt;/p&gt;&#xA;&lt;p&gt;我是软件架构师易哥。可以关注我，我会偶尔冒个泡，分享点编程干货！&lt;/p&gt;</description>
			</item>
			<item>
				<title>什么是分布式系统？</title>
				<link>https://yeecode.top/blog/140/</link>
				<pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/140/</guid>
				<description>&lt;p&gt;集群系统把单节点的并发请求分散到多个节点上，降低了每个节点的压力。它有个前提：各个节点是同质的，各跑一套完整且相同的应用。&lt;/p&gt;&#xA;&lt;p&gt;但应用是会长大的。功能越加越多、边界越扩越宽，最后往往长成一个包含众多模块的单体应用（也叫巨石应用）。这种应用跑在物理节点上，CPU、内存、IO 等资源一吃紧，性能就往下掉——而且这跟并发高不高无关，纯粹是系统自身太复杂了。&lt;/p&gt;&#xA;&lt;p&gt;除了效率，单体应用还带来开发、维护、可靠性上的一堆麻烦：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;业务复杂：模块多，彼此还耦合。新开发者上手就得先搞懂所有模块的业务逻辑；&lt;/li&gt;&#xA;&lt;li&gt;变更维护复杂：任何一点微小的升级，都要重新部署整个系统，外加一堆全量测试、回归测试；&lt;/li&gt;&#xA;&lt;li&gt;难以分拆升级：不同组件需要的资源不一样，但都被绑在一起，没法单独升级；&lt;/li&gt;&#xA;&lt;li&gt;可靠性变差：任何一个模块异常都可能拖垮整个应用，而模块众多又让恢复变得很慢。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;把这些问题的解法，就是把单体应用拆成多个子应用，让每个子应用单独部署到一台机器上。这一拆，单体应用就变成了分布式应用，如图{@fig:i00aasf}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/140/pic/fenbushi00_hu_15cd972da0d33e77.webp&#34; alt=&#34;分布式应用&#34; width=&#34;468&#34; height=&#34;308&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:i00aasf}&lt;/p&gt;&#xA;&lt;p&gt;分布式应用通过拆分子应用，把原本集中在一台机器上的压力分散到多台机器上，进一步提升了承载能力；同时让内部模块之间解耦，子应用可以独立地开发、部署、升级、维护。&lt;/p&gt;&#xA;&lt;p&gt;当然，分布式应用也要面对分布式一致性问题。比如一个分布式系统里，子应用 A1 管订单、A2 管库存，外部购买请求到达后，如果 A1 完成了订单生成，A2 就得完成库存扣减——两个子应用的状态变化也得是一致的。具体怎么实现，和我们在&amp;quot;信息一致的节点集群&amp;quot;里介绍的方法一样，这里不再赘述。&lt;/p&gt;&#xA;&lt;p&gt;实际生产里，建议优先把大的单体应用拆成分布式系统；等拆出来的某个子应用因为并发过高又遇到瓶颈时，再把它按需部署成集群，如图{@fig:i0145s}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/140/pic/fenbushi01_hu_7c4595f1207d2894.webp&#34; alt=&#34;分布式集群&#34; width=&#34;468&#34; height=&#34;308&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:i0145s}&lt;/p&gt;&#xA;&lt;p&gt;这种&amp;quot;先拆分布式、再按需上集群&amp;quot;的演进方式，让每个子应用能针对自身的制约因素去扩展——有的要扩计算、有的要扩存储、有的暂时不用扩，避免了直接扩展大单体造成的资源浪费，更合理、也更高效。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;在 AI 编程越来越能替我们写完业务代码的今天，怎么把一个庞杂的系统拆分、部署、扩展得既稳又省，反而成了更值钱的能力。聊完分布式系统的来龙去脉，如果你觉得架构这些话题有意思，强烈推荐一本书《高性能架构之道（第二版）》。&lt;/p&gt;&#xA;&lt;p&gt;这是一本理论联系实际的架构书，系统讲了怎么从顶层把软件架构好：覆盖分布式、并发编程、数据库调优、缓存、IO、高可用、前端性能优化等众多知识，最后还拿一个真实项目完整走了一遍架构全过程，把书里的方法真正落了地。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;&lt;img src=&#34;https://yeecode.top/img/promo/ha.jpg&#34; width=&#34;500&#34; height=&#34;333&#34; loading=&#34;lazy&#34;  alt=&#34;高性能架构之道（第二版）&#34; /&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;《高性能架构之道（第二版）》&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;这本书还在台湾地区出了繁体版，叫《巨型服务架构》——繁体版嘛，贵得有点离谱。&lt;/p&gt;&#xA;&lt;p&gt;就先聊到这儿。&lt;/p&gt;&#xA;&lt;p&gt;我是软件架构师易哥。可以关注我，我会偶尔冒个泡，分享点编程干货！&lt;/p&gt;</description>
			</item>
			<item>
				<title>服务集群解决方案大整理！</title>
				<link>https://yeecode.top/blog/139/</link>
				<pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/139/</guid>
				<description>&lt;p&gt;很多人一听到&amp;quot;集群&amp;quot;，脑子里浮现的就是&amp;quot;多部署几台机器&amp;quot;。这话没错，但不完整。集群系统的本质，是把原本落在一个节点上的请求，通过反向代理这类手段，分摊到多个同质节点上——这些节点配置一样、跑的程序一样，一起对外提供服务。系统结构如图{@fig:jiasd}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/139/pic/dengjia_hu_95c76c9d6773fde0.webp&#34; alt=&#34;集群系统&#34; width=&#34;427&#34; height=&#34;348&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:jiasd}&lt;/p&gt;&#xA;&lt;p&gt;但&amp;quot;多部署几台&amp;quot;马上带来一个绕不开的问题：同一个用户先后发出的多个请求，可能落在不同的节点上，服务的连贯性就被打破了。比如用户先发 R1、再发 R2，而 R2 要靠 R1 留下的信息才能跑（R1 触发了一个任务，R2 来查这个任务的结果）。要是 R1、R2 被分到不同节点，R2 自然就查不到。&lt;/p&gt;&#xA;&lt;p&gt;为了解决这个问题，业界沉淀出了几种典型的集群方案，我们挨个看。&lt;/p&gt;&#xA;&lt;h3 id=&#34;无状态的节点集群&#34;&gt;无状态的节点集群&lt;/h3&gt;&#xA;&lt;p&gt;最容易从单节点扩展到多节点的，是无状态系统。所谓无状态节点，是说用户先后发 R1、R2 两个请求，不管它们是不是落到同一个节点，R2 拿到的都应该是一样的结果——节点给出什么答复，和它之前有没有收到过 R1 完全无关。&lt;/p&gt;&#xA;&lt;p&gt;要让系统满足无状态，前提是它所有的接口都是&amp;quot;幂等类&amp;quot;接口，也就是调用前后系统状态不发生任何改变。显然，只有查询类接口才能做到这一点。&lt;/p&gt;&#xA;&lt;p&gt;不过，就算是一堆无状态节点凑成的系统，也会遇到协作问题，最典型的就是&amp;quot;并行唤醒&amp;quot;。比如我们要给这个无状态集群加个定时功能：每天凌晨对外发一封邮件。结果你会发现，集群里每个节点都会在凌晨被同时唤醒，然后各自发一封邮件——因为所有节点同质、跑的程序一致。&lt;/p&gt;&#xA;&lt;p&gt;解决办法是用&amp;quot;外部请求唤醒&amp;quot;：在指定时刻由外部系统发一个请求来触发定时任务。这个请求最终只会交给一个节点处理，于是就实现了&amp;quot;只发一封&amp;quot;的独立唤醒。&lt;/p&gt;&#xA;&lt;p&gt;无状态节点集群设计简单、扩展方便，但只适合本身就能满足无状态要求的系统，应用范围比较受限。&lt;/p&gt;&#xA;&lt;h3 id=&#34;单一服务节点集群&#34;&gt;单一服务节点集群&lt;/h3&gt;&#xA;&lt;p&gt;很多服务是有状态的：用户的历史请求在系统里攒成了上下文，系统必须结合上下文来回应。比如聊天系统里用户之前的对话、游戏里用户之前买的装备和等级，都是上下文。&lt;/p&gt;&#xA;&lt;p&gt;单一节点的系统里，把每个用户的信息都存这一台上就行；到了集群里就麻烦了。最简单的一招，是在&amp;quot;用户&amp;quot;和&amp;quot;节点&amp;quot;之间建立对应关系：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;任意用户都对应一个固定节点，这个节点上存着该用户的上下文；&lt;/li&gt;&#xA;&lt;li&gt;用户的请求永远落到与之对应的节点上。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;图{@fig:ingdd}展示了这种对应关系。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/139/pic/duiying_hu_787220d0d204497a.webp&#34; alt=&#34;用户与指定节点对应&#34; width=&#34;537&#34; height=&#34;387&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:ingdd}&lt;/p&gt;&#xA;&lt;p&gt;这种系统的特点是各个节点相互隔离：同样的代码、同样的配置，却各自守着不同用户的上下文，服务自己对应的那批用户。对用户来说，服务他的始终就是那一个节点，所以我们叫它&amp;quot;单一服务节点集群&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;建立和维护这种对应关系，常见的做法有几种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;注册时让用户自己选节点（很多游戏服务这么干）；&lt;/li&gt;&#xA;&lt;li&gt;注册时按用户所处网络分配节点（部分邮件服务这么干）；&lt;/li&gt;&#xA;&lt;li&gt;注册时按用户 id 分配节点（不少聊天系统这么干）；&lt;/li&gt;&#xA;&lt;li&gt;登录时随机或按规则分配节点，把结果写进 cookie，之后按 cookie 把请求路由到指定节点。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;最后一种和前面几种略有不同：前面几种能保证&amp;quot;用户→节点&amp;quot;的绑定在整个用户生命周期内不变，最后一种只保证在一次会话周期内不变，适合两次会话之间本就没有上下文关系的场景，比如登录系统、权限系统。&lt;/p&gt;&#xA;&lt;p&gt;无论哪种方式，都保证了会话过程中用户对应的节点不变，系统只要在会话里把请求路由过去即可，路由由反向代理、规则中心等组件完成。&lt;/p&gt;&#xA;&lt;p&gt;单一服务节点集群能解决有状态服务的问题，但因为节点彼此隔离、无法互相备份，一旦某个节点崩溃，它名下的用户就失去了服务——容错性比较差。&lt;/p&gt;&#xA;&lt;h3 id=&#34;信息共享的节点集群&#34;&gt;信息共享的节点集群&lt;/h3&gt;&#xA;&lt;p&gt;有没有既能解决有状态问题，又不会因单点崩溃而让用户失服的方案？有，就是信息共享的节点集群。在这种集群里，所有节点连到一个公共的信息池，用户的上下文信息都存在这个池子里。系统结构如图{@fig:izdiaa}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/139/pic/chizi_hu_9ddab290abad737c.webp&#34; alt=&#34;信息共享的节点集群&#34; width=&#34;362&#34; height=&#34;370&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:izdiaa}&lt;/p&gt;&#xA;&lt;p&gt;这是把单节点系统扩成多节点系统最常用的一招。通常服务节点和信息池分开部署在不同机器上，加节点就能扩集群。&lt;/p&gt;&#xA;&lt;p&gt;数据库就常当作信息池用：任意节点收到请求，都从库里读该用户的上下文，处理完再把新状态写回库。除了传统数据库，也可以是别的信息池——比如用 Redis 把信息池当共享内存，存用户的 Session 信息。&lt;/p&gt;&#xA;&lt;p&gt;在信息共享的集群里，每个节点都从信息池读写用户状态，所以对用户而言每个节点都是等价的，请求落谁都一样。&lt;/p&gt;&#xA;&lt;p&gt;节点间信息互通，就能用分布式锁解决&amp;quot;并行唤醒&amp;quot;这类协作问题。一个简单的做法是：定时任务触发时，每个节点都以同一个键往信息池里写一个&amp;quot;不允许覆盖&amp;quot;的数据，最终只有一个节点能写成功，它拿到执行权。&lt;/p&gt;&#xA;&lt;p&gt;信息共享的节点集群靠加节点提升了运算能力，但因为多个节点共享同一个信息池，受信息池容量和读写性能制约，系统在存储容量、数据吞吐上的提升并不明显。&lt;/p&gt;&#xA;&lt;h3 id=&#34;信息一致的节点集群&#34;&gt;信息一致的节点集群&lt;/h3&gt;&#xA;&lt;p&gt;信息共享的集群，运算能力分散在各个节点，存储能力却集中在信息池——信息池成了故障单点和性能瓶颈。&lt;/p&gt;&#xA;&lt;p&gt;要避免这个瓶颈，可以让每个节点各自拥有一个信息池。但为了保持有状态服务，又必须保证各个信息池里的数据是一致的，如图{@fig:i02bb}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/139/pic/chizi02_hu_2321dce61c6885d.webp&#34; alt=&#34;信息一致的节点集群&#34; width=&#34;351&#34; height=&#34;393&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:i02bb}&lt;/p&gt;&#xA;&lt;p&gt;这种&amp;quot;信息一致的节点集群&amp;quot;常被叫作分布式系统，但严格说它仍是集群——因为分布式系统的节点是异构的，分属不同模块；而这里的节点是同构的，出现的目的只是为了分担高并发带来的压力。不过，它也得面对分布式系统常遇到的那个老问题：分布式一致性。&lt;/p&gt;&#xA;&lt;p&gt;一致性要求：在系统的某个节点上做了变更，经过&amp;quot;一定时间&amp;quot;后，能从每个节点上读到这个变更。&lt;/p&gt;&#xA;&lt;p&gt;我们用图{@fig:zhiassdf}的例子直观看看一致性问题。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/139/pic/bingxyizhi_hu_e8524a432ef5608e.webp&#34; alt=&#34;分布式一致性示意图&#34; width=&#34;373&#34; height=&#34;321&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:zhiassdf}&lt;/p&gt;&#xA;&lt;p&gt;图里调用方先通过节点集群把变量 a 设成 5，再去读 a，结果读到的却是 3。这完全可能发生，因为两次读写可能访问的是两个节点，只要节点间信息不同步、或同步有时延，就会出现。&lt;/p&gt;</description>
			</item>
			<item>
				<title>并行和并发并不相同！</title>
				<link>https://yeecode.top/blog/138/</link>
				<pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/138/</guid>
				<description>&lt;p&gt;并行和并发，是我们在架构设计与软件开发中经常涉及的概念，不过两者的意义并不完全相同。&lt;/p&gt;&#xA;&lt;p&gt;并行（parallelism）是指在同一时刻有多个任务同时进行。例如，你在家中一边读书一边听歌，&amp;ldquo;既读书又听歌&amp;quot;描述的就是并行，因为读书和听歌这两件事是同时发生的，如图{@fig:xsing}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/138/pic/bingxing_hu_25fdd57ba56f303d.webp&#34; alt=&#34;并行示意图&#34; width=&#34;568&#34; height=&#34;475&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:xsing}&lt;/p&gt;&#xA;&lt;p&gt;并发（concurrency）是指多个任务中的每个任务都被拆分成细小的任务片，从属于不同任务的任务片被轮番处理。因此，任意时刻都只有一个任务在进行，但从宏观上看，这些任务像是被同时处理的。例如，你在家中一边读书一边看综艺，&amp;ldquo;既读书又看综艺&amp;quot;描述的就是并发，因为在任意时刻，你要么在低头读书，要么在抬头看综艺，这两件事实际是交替进行的，如图{@fig:ngfa}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/138/pic/bingfa_hu_f3da72149cfeda22.webp&#34; alt=&#34;并发示意图&#34; width=&#34;568&#34; height=&#34;475&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:ngfa}&lt;/p&gt;&#xA;&lt;p&gt;所以说，并行是一种真正意义上的&amp;quot;并&amp;rdquo;，而并发只是宏观表现上的&amp;quot;并&amp;rdquo;。&lt;/p&gt;&#xA;&lt;p&gt;不过要注意，并发和并行的区分仅限于微观上。从宏观上看，并发或并行的任务都像是同时开展的。以并发请求为例，在同一时刻向某个系统发送大量请求，这些请求几乎会被同时处理；而具体这些请求在系统内部是&amp;quot;并行&amp;quot;处理还是&amp;quot;并发&amp;quot;处理的，无法从系统外部判断出来。所以宏观上常常把&amp;quot;并行&amp;quot;和&amp;quot;并发&amp;quot;统称为&amp;quot;并发&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;后面我们会介绍一些使用并行和并发来提升软件性能的手段，而区分好这两个概念是基础。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;在 AI 编程迅猛发展的今天，编码能力逐步弱化，而架构能力则成了开发者更核心的竞争力。聊完并行与并发这对常被混淆的概念，如果你还想深入&amp;quot;单节点如何拆成多节点并行&amp;quot;的架构设计，推荐一本书《高性能架构之道（第二版）》。&lt;/p&gt;&#xA;&lt;p&gt;这是一本理论联系实际的架构书，系统讲了怎么从顶层把软件架构好：覆盖分布式、并发编程、数据库调优、缓存、IO、高可用、前端性能优化等众多知识，最后还拿一个真实项目完整走了一遍架构全过程，把书里的方法真正落了地。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;&lt;img src=&#34;https://yeecode.top/img/promo/ha.jpg&#34; width=&#34;500&#34; height=&#34;333&#34; loading=&#34;lazy&#34;  alt=&#34;高性能架构之道（第二版）&#34; /&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;《高性能架构之道（第二版）》&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;这本书还在台湾地区出了繁体版，叫《巨型服务架构》——繁体版嘛，贵得有点离谱。&lt;/p&gt;&#xA;&lt;p&gt;就先聊到这儿。&lt;/p&gt;&#xA;&lt;p&gt;我是软件架构师易哥。可以关注我，我会偶尔冒个泡，分享点编程干货！&lt;/p&gt;</description>
			</item>
			<item>
				<title>什么是反向代理？它有哪几类？</title>
				<link>https://yeecode.top/blog/137/</link>
				<pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/137/</guid>
				<description>&lt;p&gt;经过内容分发网络或者多地址直连方式的分流后，用户的请求已经分散到了不同的系统上。这里所说的一个系统，是指它对外呈现唯一的 IP 地址。&lt;/p&gt;&#xA;&lt;p&gt;通常来说，一个确定的 IP 地址会代表一台确定的机器（节点），但这不是绝对的。通过代理，可以让多个节点对外表现出唯一的 IP 地址。&lt;/p&gt;&#xA;&lt;p&gt;代理是软件架构和网络设计里都常接触的概念。例如，用户端可以设置代理服务器，让所有请求都由代理服务器发出。这样一来，从外部看，所有请求都由代理服务器发出，无法判断它背后代理了多少用户端。这种代理方式叫作正向代理，如图{@fig:zaili}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/137/pic/zhengxiangdaili_hu_6aadc5e55a3451c9.webp&#34; alt=&#34;正向代理示意图&#34; width=&#34;407&#34; height=&#34;318&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:zaili}&lt;/p&gt;&#xA;&lt;p&gt;同样的，服务端也可以设置代理服务器。所有请求都由代理服务器接收，再由它分发给后方的服务器。这样一来，从外部看，所有请求都由代理服务器处理，无法判断它后方到底有多少服务器。这种代理方式叫作反向代理，如图{@fig:faili}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/137/pic/fanxiangdaili_hu_f03b3927e9701b45.webp&#34; alt=&#34;反向代理示意图&#34; width=&#34;445&#34; height=&#34;317&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:faili}&lt;/p&gt;&#xA;&lt;p&gt;反向代理可以用来对请求进行分流，把请求分流到系统内部的多个节点上，从而减少每个节点的并发数。而这些节点在外界看来是一个系统，表现出唯一确定的 IP。&lt;/p&gt;&#xA;&lt;p&gt;实现反向代理的手段有很多，可以根据它工作的层级不同分为两类：四层反向代理和七层反向代理。这里说的层级，是 OSI 参考模型的层级。OSI 参考模型把通信功能划分为七个层次，每一层向相邻上层提供一套确定的服务，并使用相邻下层提供的服务实现当前层级的功能和协议。OSI 参考模型的层级划分和各层功能如图{@fig:fossi}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/137/pic/osi_hu_39ddfe0667dad94b.webp&#34; alt=&#34;OSI参考模型&#34; width=&#34;678&#34; height=&#34;416&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:fossi}&lt;/p&gt;&#xA;&lt;p&gt;在 OSI 参考模型中，第四层是传输层，TCP 协议和 UDP 协议等工作在这一层；第七层是应用层，HTTP 协议和 FTP 协议等工作在这一层。了解这些之后，就能理清楚四层和七层反向代理的工作原理：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;四层反向代理：可以根据用户请求的 IP 和端口号进行转发。&lt;/li&gt;&#xA;&lt;li&gt;七层反向代理：可以根据 FTP 请求、HTTP 请求中的具体内容进行转发，例如根据 HTTP 请求的请求方法、URL、请求首部、请求正文等信息进行转发。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;四层反向代理处在 OSI 协议的更底层，掌握的信息更少，因此实现原理更简单，运行效率也更高。七层反向代理工作在应用层，能收集到更多信息，因此可以做得更智能。例如，它可以分析请求的 cookie 信息，针对不同 cookie 做不同处理；也可以分析请求的 URL，把针对特殊资源（例如图像资源）的请求转发到特定服务器上。当然，获取全面信息的代价是牺牲效率，相比于四层反向代理，七层反向代理实现更复杂、效率也更低。&lt;/p&gt;&#xA;&lt;p&gt;在对系统请求进行内部分流时，七层反向代理应用得最为广泛。因为它可以对外表现出完全一致的 IP 地址和端口，再根据请求的具体内容分流。Nginx 就是一种典型的七层反向代理软件。下面以 Nginx 为例，简要介绍反向代理的实现。&lt;/p&gt;&#xA;&lt;p&gt;使用 Nginx 搭建反向代理时，主要用它的 upstream 模块。upstream 即&amp;quot;上游&amp;quot;的意思，指 Nginx 后方的服务节点，Nginx 只需要把请求以某种策略转发给后方节点即可。&lt;/p&gt;&#xA;&lt;p&gt;利用 Nginx 的转发策略，可以实现后方服务节点间的负载均衡。Nginx 支持的策略有轮询、加权轮询、请求源 IP 地址哈希，此外还支持用户自己扩展策略。其中轮询是指平均地为后方节点分配请求；加权轮询是指按一定比例分配；请求源 IP 地址哈希是指根据请求来源 IP 的哈希结果分配。这种哈希策略有个优点：只要请求方 IP 不变，服务它的后方节点也不变，这有利于维护 Session 等信息。&lt;/p&gt;</description>
			</item>
			<item>
				<title>什么是CDN？它的工作原理是怎样的？</title>
				<link>https://yeecode.top/blog/136/</link>
				<pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/136/</guid>
				<description>&lt;p&gt;我们部署的软件系统处在互联网的某个节点上，图{@fig:cdnnet}展示了这种情况，通常这个节点由一个 IP 地址标定。所有指向该系统的请求，都会在网络上经过多次路由后到达该节点。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/136/pic/cdnnet_hu_6fcc2ba3154bb82c.webp&#34; alt=&#34;网络上的系统&#34; width=&#34;554&#34; height=&#34;479&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:cdnnet}&lt;/p&gt;&#xA;&lt;p&gt;如果我们在网络的多个位置都部署这套系统，就可以把请求分散到多个系统上，减少每个系统承担的并发请求数。不仅如此，还可以让每个用户的请求落到离他最近的一个系统上，这样便降低了网络延时，从而减小平均响应时间，可谓一举两得。&lt;/p&gt;&#xA;&lt;p&gt;这里所说的&amp;quot;最近&amp;quot;，不是地理位置上的最近，而是网络拓扑结构上的&amp;quot;最近&amp;quot;。在网络拓扑中，一个节点到另一个节点的距离，会根据网络负载情况、内容的可用性、设备工作状况等实时变化，所以这种&amp;quot;最近&amp;quot;的关系是动态变动的。&lt;/p&gt;&#xA;&lt;p&gt;但上面这套思路会带来一个严重的维护问题：如果同一个系统被部署到不同位置，我们就需要同时部署和维护不同位置的多个系统，系统间协作成本、部署维护成本都会很高。因此在实际生产中，通常的做法是把图片、视频、附件这类流量消耗大、又不经常改变的静态资源部署到互联网的多个位置，而核心系统只部署在一个位置。这就构成了我们常见的内容分发网络（Content Delivery Network，简称 CDN）。&lt;/p&gt;&#xA;&lt;h2 id=&#34;内容分发网络的结构&#34;&gt;内容分发网络的结构&lt;/h2&gt;&#xA;&lt;p&gt;内容分发网络的组成如图{@fig:tpic}所示。在 CDN 中，部署核心系统的节点被称为源站，它是所有静态、动态信息的来源；而部署图片、视频、附件等静态资源的节点，叫作 CDN 节点、缓存节点或者边缘节点。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/136/pic/cdnnetpic_hu_505e85850d65d872.webp&#34; alt=&#34;内容分发网络示意图&#34; width=&#34;608&#34; height=&#34;479&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;{#fig:tpic}&lt;/p&gt;&#xA;&lt;p&gt;边缘节点是相对于复杂的网络结构提出的一个概念，它是指与接入用户之间具有较少中间环节的网络节点。因此，相比于源站，用户请求到达边缘节点的网络延迟更小。&lt;/p&gt;&#xA;&lt;p&gt;当用户请求边缘节点的内容时，边缘节点会先判断自身是否已经缓存了用户要请求的内容。如果该内容已经缓存，边缘节点就直接把内容返回给用户；如果尚未缓存，边缘节点会去源站请求内容再发给用户，并且还会根据设置决定是否在自身也缓存一份。所以，边缘节点中存储的只是源站中部分内容的备份。&lt;/p&gt;&#xA;&lt;p&gt;边缘节点（也就是 CDN 节点）的数目，对内容分发网络的效率有较大的影响。节点数目越多，用户与最近的 CDN 节点之间的中间环节越少，通讯时延也就越小。但在互联网上部署众多 CDN 节点需要极大的成本，因此 CDN 节点多由专门的 CDN 服务商部署并对外提供服务。&lt;/p&gt;&#xA;&lt;p&gt;内容分发网络有以下几个优点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;减小了系统并发数。系统不再是网络上的一个节点，而是多个节点，每个系统只承担一部分请求，减少了各自的并发数。&lt;/li&gt;&#xA;&lt;li&gt;减少了平均响应时间。用户请求会被分配到最近的节点上，减小了网络延迟，也就缩短了平均响应时间。&lt;/li&gt;&#xA;&lt;li&gt;减少了网络拥堵。每个用户的请求都被分配到最近的节点上，无须经过骨干网络，减轻了对骨干网络的压力。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;不过 CDN 节点中只能缓存静态内容，一些涉及动态内容的请求仍然需要源站处理。通常我们会通过 CDN 控制中心为各个 CDN 节点配置规则列表，节点会根据规则对请求内容进行分类：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;对于静态内容：CDN 节点从源站获取到该内容后，会在自身缓存一定的时间；之后再次收到针对同样内容的请求时，直接返回缓存内容。&lt;/li&gt;&#xA;&lt;li&gt;对于动态内容：CDN 节点不做缓存，而是直接将请求转交给源站处理。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;内容分发网络为源站分担了大量静态资源请求，降低了源站的并发请求数，使得同样软硬件配置的源站可以承担更多的外部并发。&lt;/p&gt;&#xA;&lt;h2 id=&#34;内容分发网络的原理&#34;&gt;内容分发网络的原理&lt;/h2&gt;&#xA;&lt;p&gt;调用方请求一个服务时给出的目的地址是源站的地址，对应网络中源站所在的节点。那么 CDN 是如何把指向源站地址的请求，分散到不同 CDN 节点上的呢？这是 CDN 要解决的一个核心问题，也就是把发往源站的请求拦截给 CDN 节点。这一过程，和域名解析有关。&lt;/p&gt;&#xA;&lt;p&gt;域名解析，就是把域名解析为 IP 地址的过程。IP 地址标志了网络中一个节点的位置，通过它可以在网络中寻址找到对应节点，例如&amp;quot;185.199.108.153&amp;quot;就是一个 IP 地址；域名则是为了让 IP 地址更容易被记住而设的代称，例如&amp;quot;yeecode.top&amp;quot;就是一个网站域名。&lt;/p&gt;&#xA;&lt;p&gt;域名解析由域名系统（Domain Name System，简称 DNS）来完成，可以把它看作一个保存了域名和 IP 地址对应关系的数据库；而域名解析，就是从这个数据库中查找某个域名对应 IP 地址的过程。&lt;/p&gt;</description>
			</item>
			<item>
				<title>什么是软件的性能？该怎么衡量与优化？</title>
				<link>https://yeecode.top/blog/135/</link>
				<pubDate>Sat, 21 Feb 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/135/</guid>
				<description>&lt;p&gt;我们写代码、聊架构的时候，经常把&amp;quot;高性能&amp;quot;三个字挂在嘴边。但你要是真被人问一句&amp;quot;高性能到底指什么&amp;quot;，不少人会愣一下。今天咱们就把这个词掰开揉碎，看看它背后到底对应着软件质量的哪些维度，以及我们平时是怎么衡量、又该怎么去优化它的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;什么是软件的性能&#34;&gt;什么是软件的性能&lt;/h2&gt;&#xA;&lt;p&gt;先说一个反直觉的点：在软件工程那套权威的《ISO/IEC 25010》质量模型里，其实压根没有&amp;quot;性能&amp;quot;这个特性维度。&lt;/p&gt;&#xA;&lt;p&gt;那&amp;quot;性能&amp;quot;是从哪来的？它其实是个俗称，主要对应着软件质量模型里的两个大维度：效率（efficiency）和可靠性（reliability）。一个软件如果在效率和可靠性上都表现不错，我们才会说它是&amp;quot;高性能&amp;quot;的。&lt;/p&gt;&#xA;&lt;p&gt;具体点说，在 ISO 25010 里，效率又拆成了三个子特性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;时间效率（Time behaviour）：执行功能时，响应时间、处理时间、吞吐率达标的情况。&lt;/li&gt;&#xA;&lt;li&gt;资源利用率（Resource utilization）：执行功能时，占用的资源数量和类型达不达标。&lt;/li&gt;&#xA;&lt;li&gt;容量（Capacity）：系统各项参数的最大上限达不达标。比如能存多少条数据、撑多少并发用户、通信带宽多大、事务吞吐多少、数据库多大。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;而可靠性则拆成了四个子特性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;成熟度（Maturity）：正常操作下满足可靠性需求的程度。&lt;/li&gt;&#xA;&lt;li&gt;可用性（Availability）：要用的时候，系统可操作、可访问的程度。&lt;/li&gt;&#xA;&lt;li&gt;容错性（Fault tolerance）：哪怕硬件或软件出了故障，系统还能按预期跑的程度。&lt;/li&gt;&#xA;&lt;li&gt;可恢复性（Recoverability）：出了中断或故障，能不能把受影响的数据恢复回来、把系统状态重新建起来。比如故障后计算机要关一阵子，关多久，就看可恢复性。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以，&amp;ldquo;高性能&amp;quot;的软件，得在这七个子特性上都有好表现。说大白话就是：处理能力强、响应快、容量大、故障率低、不容易崩、崩了恢复也快。&lt;/p&gt;&#xA;&lt;p&gt;为什么要死磕高性能？因为咱们国家的互联网用户群体太庞大了。海量的软件——尤其是互联网软件——得扛住巨量用户的访问：更短时间响应用户操作、用更少的资源服务更多人、存下更多用户的数据。这对软件的性能，提出了极高的要求。&lt;/p&gt;&#xA;&lt;h2 id=&#34;怎么衡量软件的性能&#34;&gt;怎么衡量软件的性能&lt;/h2&gt;&#xA;&lt;p&gt;聊完了&amp;quot;高性能&amp;quot;到底指什么，接下来我们逐个认识那些被归到&amp;quot;性能&amp;quot;名下的具体指标。日常里衡量软件性能，最常被提起的就是下面这三个。&lt;/p&gt;&#xA;&lt;h3 id=&#34;吞吐量&#34;&gt;吞吐量&lt;/h3&gt;&#xA;&lt;p&gt;&amp;ldquo;吞吐量&amp;quot;这个词最早是从电信网络那边借来的，指的是在一条通信信道上，单位时间里能成功传送的平均数据量。挪到软件系统里，它的意思也差不多：软件系统在单位时间里，能接收和发出多少数据量。&lt;/p&gt;&#xA;&lt;p&gt;不过要注意，吞吐量是个挺&amp;quot;模糊&amp;quot;的概念。不同系统、不同请求，对应的数据量不一样，操作复杂度也不一样，所以很难拿吞吐量去横向比较两个不同的系统。&lt;/p&gt;&#xA;&lt;p&gt;日常里，我们更常用几个更具体的指标来代表吞吐量，最典型的就是 TPS 和 QPS：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;TPS（Transaction Per Second）：每秒处理的事务数。这里的事务，一般指一次完整的操作，包含请求、变更、返回等全流程。&lt;/li&gt;&#xA;&lt;li&gt;QPS（Queries Per Second）：每秒进行的查询数。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;TPS 和 QPS 是有关联的，但这个关联并不固定。比如我们把&amp;quot;呈现一个页面&amp;quot;当成一个事务，那一个 TPS 里往往就包了好几个 QPS——因为一个页面通常要查 HTML、CSS、图片等好几个资源。所以 TPS 和 QPS 之间怎么换算，得看具体的业务场景。&lt;/p&gt;&#xA;&lt;p&gt;也正因为不同系统处理的事务、查询的内容千差万别，TPS 和 QPS 很难用来精确比出两个系统谁强谁弱；它们更多是用来衡量&amp;quot;同一个系统&amp;quot;在不同时间、不同环境下的性能变化。&lt;/p&gt;&#xA;&lt;p&gt;正因为吞吐量这么&amp;quot;虚&amp;rdquo;，本书后面统一用&amp;quot;吞吐量&amp;quot;这个词，来代表系统处理用户请求的能力。&lt;/p&gt;&#xA;&lt;h3 id=&#34;并发数&#34;&gt;并发数&lt;/h3&gt;&#xA;&lt;p&gt;说完吞吐量，另一个高频词就是&amp;quot;并发数&amp;quot;了。不过&amp;quot;并发数&amp;quot;和吞吐量一样，也是个挺宽泛的概念，它底下其实包含了好几样东西：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;并发用户数：同时使用软件功能的用户人数。但这里面，有些用户可能只是登录了，并没真在操作。&lt;/li&gt;&#xA;&lt;li&gt;并发连接数：软件承载的连接数量。这些连接里，有些正在传数据，有些仅仅只是保持着连接。&lt;/li&gt;&#xA;&lt;li&gt;并发请求数：软件同时扛着的请求数量。这些请求里，有的可能只是要个静态资源，有的可能要做读写操作。&lt;/li&gt;&#xA;&lt;li&gt;并发线程数：这是衡量系统内部运行情况的指标，指软件内部跑着的线程数。不同业务操作，触发的线程数也不一样。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;你看，上面这四样，没有哪一个是绝对清晰、绝对准确的——它们各自都有&amp;quot;含混&amp;quot;的地方。&lt;/p&gt;&#xA;&lt;p&gt;所以本书同样做了个统一约定：不再去纠结某一种具体的并发数，而是统一用&amp;quot;并发数&amp;quot;这个词，来代表系统同时服务的调用方有多少。&lt;/p&gt;&#xA;&lt;h3 id=&#34;平均响应时间&#34;&gt;平均响应时间&lt;/h3&gt;&#xA;&lt;p&gt;前面讲的吞吐量和并发数，都是衡量软件系统的重要指标。但站在一个&amp;quot;用户&amp;quot;的角度——这个用户可能是人，也可能是另一个系统——他其实根本感知不到这两个指标。用户作为一个个体，既不知道系统的吞吐量高低，也不知道此刻并发数是多少。用户能感受到的，是另一个指标：响应时间。&lt;/p&gt;&#xA;&lt;p&gt;响应时间，指的是用户发出调用请求，到收到系统回应，这中间花的时间。这个时间越短，体验就越好。&lt;/p&gt;&#xA;&lt;p&gt;放大到整个系统上，对应的指标就是平均响应时间，也就是系统服务的所有请求的响应时间的平均值。&lt;/p&gt;&#xA;&lt;p&gt;系统内部往往由很多模块组成，每个模块也都有自己的响应时间。我们想提升系统的平均响应时间，通常就是从提升各个模块的响应时间入手。这时候，就要请出阿姆达尔定律（Amdahl&amp;rsquo;s Law）了。&lt;/p&gt;&#xA;&lt;p&gt;阿姆达尔定律描述的是：当系统中某个模块的执行速度被提升时，整个系统的执行速度能提升多少。&lt;/p&gt;&#xA;&lt;p&gt;它先定义了&amp;quot;加速比&amp;quot;这个概念。假设我们优化了某个模块 $m$，让它的平均响应时间从 $T_{m,old}$ 缩短成了 $T_{m,new}$，那么这次优化带来的模块加速比 $r_m$ 就是：&lt;/p&gt;</description>
			</item>
			<item>
				<title>怎么评价软件的质量？</title>
				<link>https://yeecode.top/blog/134/</link>
				<pubDate>Tue, 17 Feb 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/134/</guid>
				<description>&lt;p&gt;我们平时用软件，总会不自觉地在心里给它打分：&amp;ldquo;这个界面真漂亮&amp;quot;&amp;ldquo;这个用着真顺手&amp;quot;&amp;ldquo;这个怎么老崩溃&amp;quot;&amp;ldquo;这个功能真强&amp;rdquo;……其实啊，这些随口说的话，都是在从不同角度评价一个软件的质量。&lt;/p&gt;&#xA;&lt;p&gt;那有没有一套正经的标准，能把这些&amp;quot;感觉&amp;quot;说清楚呢？还真有。ISO/IEC 25010:2011，这是软件工程里专门用来评价产品质量的国际标准。它把软件质量拆成了八个特性维度，如图{@fig:pqmas}所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/134/pic/pqm_hu_19771c7c464e2b50.webp&#34; alt=&#34;软件质量的八个特性维度&#34; width=&#34;565&#34; height=&#34;565&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;{#fig:pqmas}&lt;/p&gt;&#xA;&lt;p&gt;这八个特性维度分别是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;功能性（Functional suitability）：软件在指定条件下，能不能提供满足明确和隐含需求的功能。这里只要求&amp;quot;功能有没有&amp;rdquo;，至于功能规不规矩、漂不漂亮，倒不在它的考察范围内。&lt;/li&gt;&#xA;&lt;li&gt;效率（Performance efficiency）：在指定条件下，软件对资源的占用情况。资源包括别的软件、系统的软硬件配置等等。&lt;/li&gt;&#xA;&lt;li&gt;兼容性（Compatibility）：产品、系统或组件能不能跟别的产品、系统、组件交换信息、协同完成功能，同时还共享同一套软硬件环境。&lt;/li&gt;&#xA;&lt;li&gt;易用性（Usability）：指定的用户能不能在指定的环境下，有效、高效、满意地达成目标。&lt;/li&gt;&#xA;&lt;li&gt;可靠性（Reliability）：系统、产品或组件在指定条件下、指定时间内，能不能稳定地执行指定功能。这里说的&amp;quot;指定功能&amp;quot;其实涵盖了可用性、安全性、可维护性等等。软件不会像硬件那样用着用着就磨损，所以它不可靠，要么是需求、设计、实现里埋了错，要么是运行环境变了。&lt;/li&gt;&#xA;&lt;li&gt;安全性（Security）：产品或系统保护信息和数据的程度，确保人员或其他产品、系统只能按自己的权限级别去访问数据。&lt;/li&gt;&#xA;&lt;li&gt;可维护性（Maintainability）：预期的维护者能不能方便、高效地去修改它。&lt;/li&gt;&#xA;&lt;li&gt;可移植性（Portability）：系统、产品或组件，从一种硬件、软件或其他运行环境，迁移到另一种环境时，能不能又快又好地搞定。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;把这八个维度合起来，就是软件质量的完整画像。前面那些大白话，对号入座一下就明白了：说&amp;quot;好用&amp;quot;&amp;ldquo;界面漂亮&amp;rdquo;，是在评价易用性；说&amp;quot;老崩溃&amp;rdquo;，是在评价可靠性；说&amp;quot;功能强大&amp;rdquo;，是在评价功能性。&lt;/p&gt;&#xA;&lt;p&gt;做架构设计的时候，我们当然希望软件在这八个维度上都能拿高分。但现实里常常要做取舍——比如为了界面更漂亮（把易用性拉满），可能就让加载变慢了（效率下降）。这种权衡，几乎天天都在发生。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;在AI编程迅猛发展的今天，编码能力则逐步弱化，而架构能力则成了每个开发者更为核心的能力。聊完&amp;quot;怎么评价软件的质量&amp;quot;，如果你觉得架构、质量这些话题有意思，强烈推荐一本书《高性能架构之道（第二版）》。&lt;/p&gt;&#xA;&lt;p&gt;这是一本理论联系实际的架构书，系统讲了怎么从顶层把软件架构好：覆盖分布式、并发编程、数据库调优、缓存、IO、高可用、前端性能优化等众多知识，最后还拿一个真实项目完整走了一遍架构全过程，把书里的方法真正落了地。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;&lt;img src=&#34;https://yeecode.top/img/promo/ha.jpg&#34; width=&#34;500&#34; height=&#34;333&#34; loading=&#34;lazy&#34;  alt=&#34;高性能架构之道（第二版）&#34; /&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;《高性能架构之道（第二版）》&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;这本书还在台湾地区出了繁体版，叫《巨型服务架构》——繁体版嘛，贵得有点离谱。&lt;/p&gt;&#xA;&lt;p&gt;就先聊到这儿。&lt;/p&gt;&#xA;&lt;p&gt;我是软件架构师易哥。可以关注我，我会偶尔冒个泡，分享点编程干货！&lt;/p&gt;</description>
			</item>
			<item>
				<title>什么是架构，架构的关键设计原则是什么？</title>
				<link>https://yeecode.top/blog/127/</link>
				<pubDate>Sat, 03 Jan 2026 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/127/</guid>
				<description>&lt;p&gt;架构是一个来源于传统领域的古老概念，用来指对房屋、桥梁等实体物理结构的设计与研究。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/127/pic/001_hu_67d050b5df589568.webp&#34; alt=&#34;建筑物的架构&#34; width=&#34;800&#34; height=&#34;533&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;后来，架构的概念被引入到软件系统领域，指&lt;strong&gt;软件系统总体结构的设计&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;我们可以类比传统工程设计领域的概念，将软件系统领域的研究由上到下地分为三层：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;理念层面&lt;/strong&gt;：研究软件系统开发中的理念和思想，例如研究软件的开发模型、评价指标、架构风格等。类比于传统工程领域研究建筑质量标准、研究商业区与居住区的分布关系等。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;架构层面&lt;/strong&gt;：研究如何协调和组织软件系统、子系统、模块之间的关系，从而指导系统实现并提升各类质量指标。类比于规划和设计建筑物的承重结构、功能结构等，并协调各结构的关系等。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;技术层面&lt;/strong&gt;：研究如何高效、可靠、经济地实现软件系统、子系统、模块等。类比于搭建建筑物中的楼梯、墙体、阳台等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这一样一类比，是不是就比较好理解了。&lt;/p&gt;&#xA;&lt;p&gt;可见软件架构位于承上启下的中间层。因此，做出良好的架构需要我们对理念层面和技术层面的知识均由较为深刻的认识。&lt;/p&gt;&#xA;&lt;p&gt;理念层面的知识为我们的架构提供了思路和目标上的指引，技术层面的知识为我们的架构提供了手段和工具上的支撑。&lt;/p&gt;&#xA;&lt;p&gt;在软件开发过程中，架构发生在需求之后、规划之前。经过需求、架构、规划、开发几个主要步骤后，一个软件便从最初模糊的需求演变为最终可运行的实体。&lt;/p&gt;&#xA;&lt;p&gt;软件架构是在软件系统结构、行为和属性的高级抽象基础之上展开的全面的系统设计，其主要内容包括概要设计和详细设计两大步骤。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在概要设计阶段，研究构成系统的抽象组件以及组件之间的连接规则；&lt;/li&gt;&#xA;&lt;li&gt;在详细设计阶段，将这些抽象组件细化为模块、类、对象等实际的组件，并通过设计通信规则完成它们之间的连接。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/127/pic/002_hu_a388866421e21173.webp&#34; alt=&#34;软件的模块设计&#34; width=&#34;640&#34; height=&#34;480&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;软件系统架构的好坏对软件的质量有着重要的影响。在算法研究中，我们常使用时间复杂度和空间复杂度等指标来衡量算法的性能。然而，在一个完整的软件项目中，算法的研究与优化只是软件开发阶段的一个小环节，其对系统质量的影响是有限的。相比于算法优化，软件系统架构则是从顶层对软件系统的软硬件结构进行设计，其设计的好坏对系统质量的影响更为重大。&lt;/p&gt;&#xA;&lt;p&gt;因此，要想搭建高性能的系统，要先从架构层面出发对软件进行高性能架构，而不应将责任直接推给算法设计和代码优化。&lt;/p&gt;&#xA;&lt;p&gt;在了解什么是高性能架构之前，我们需要先了解什么是软件的质量。&lt;/p&gt;&#xA;&lt;p&gt;作为一名软件开发者和用户，我们经常在工作和生活中接触各种各样的软件，也会从不同的维度对软件进行评判。例如我们会使用“界面漂亮”、“好用”、“容易崩溃”、“功能强大”等各种类似的词语来描述一个软件，其实这都从不同的维度反映了一个软件的质量。&lt;/p&gt;&#xA;&lt;p&gt;那有没有确切的标准来衡量一个软件的质量呢？&lt;/p&gt;&#xA;&lt;p&gt;ISO/IEC 25010:2011是软件工程-产品质量评价国际标准。它给出了软件质量模型（Product Quality Model）并将软件质量划分成了八个特性维度，如图所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/127/pic/pqm_hu_19771c7c464e2b50.webp&#34; alt=&#34;软件质量的八个特性维度&#34; width=&#34;565&#34; height=&#34;565&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这八个特性维度的介绍如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;功能性（Functional suitability）：在特定条件下使用时，产品或系统提供满足指定和隐含需求的功能的程度。这里仅对是否满足功能做要求，而对功能的规范性不做要求。&lt;/li&gt;&#xA;&lt;li&gt;效率（Performance efficiency）：在指定条件下，软件产品对资源的占用。资源包括其他软件产品、系统的软件和硬件配置等。&lt;/li&gt;&#xA;&lt;li&gt;兼容性（Compatibility）：产品、系统或组件可以与其他产品，系统或组件交换信息或执行其所需功能，同时共享相同的硬件或软件环境的程度。&lt;/li&gt;&#xA;&lt;li&gt;易用性（Usability）：指定用户可以使用产品或系统在指定的使用环境下以有效、高效和满意的方式达到指定目标的程度。&lt;/li&gt;&#xA;&lt;li&gt;可靠性（Reliability）：系统、产品或组件在指定条件下在指定时间内执行指定功能的程度。这里所说的指定功能涵盖可用性、安全性、可维护性等。因为软件不会在运行过程中损耗，所以其不可靠是由于软件需求、设计、实施中的错误引发，或者是执行上下文变化引发。&lt;/li&gt;&#xA;&lt;li&gt;安全性（Security）：&#xA;产品或系统保护信息和数据的程度，以便人员或其他产品或系统具有与其权限类型和授权级别相适应的数据访问程度。&lt;/li&gt;&#xA;&lt;li&gt;可维护性（Maintainability）：预期的维护者可以修改产品或系统的有效性和效率的程度。&lt;/li&gt;&#xA;&lt;li&gt;可移植性（Portability）：系统、产品或组件从一种硬件，软件或其他操作或使用环境转移到另一种环境的有效性和效率的程度。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上八个特性维度共同构成了软件的质量。例如我们所说的“好用”、“界面漂亮”是在从易用性维度评价软件质量，“容易崩溃”是在从可靠性维度评价软件质量，“功能强大”是在从功能性维度评价软件质量。&lt;/p&gt;&#xA;&lt;p&gt;在软件架构设计过程中，我们应该&lt;strong&gt;尽量保证设计出的软件能够在以上八个维度都有较好的表现&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;当然也有很多时候，我们不得不做出取舍，为了某些重要的特性维度指标而牺牲一些次要的指标。例如为了界面漂亮（易用性高）而导致界面的加载时间变长（效率降低），需要不断权衡。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/127/pic/003_hu_a095b47814216207.webp&#34; alt=&#34;愈发美观的界面是以降低易用性为代价的&#34; width=&#34;640&#34; height=&#34;425&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;如果大家要学习架构方面的知识，可以参阅《高性能架构之道（第二版）》，这是一本理论联系实际的架构书籍，详细介绍了实现高性能架构的手段，涵盖分布式、并发编程、数据库调优、缓存、IO、高可用、前端性能优化等众多知识。最后还运用书中知识完成了一个实际软件项目的架构设计工作，向大家完整展示了软件架构的全过程。&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;&lt;img src=&#34;https://yeecode.top/img/promo/ha.jpg&#34; width=&#34;500&#34; height=&#34;333&#34; loading=&#34;lazy&#34;  alt=&#34;高性能架构之道（第二版）&#34; /&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://union-click.jd.com/jdc?e=&amp;amp;p=AyIGZRprFQASBl0SXBIyVlgNRQQlW1dCFFlQCxxKQgFHRE5XDVULR0UVABIGXRJcEh1LQglGaxFiGlEWTTtIYHIAS35dTnduVBFPO3UOHjdUK1sUAxIDXB1TFwIiN1Uca0NsEgZUGloUBxICVitaJQIVB1AcWhMEFwFcEl8lBRIOZUAOewQaVAcfDh0FEQVUGVklMiIEZStrFTIRNxd1DxwKRgBcSFpFAUJTUExTE1JBVFUeWxFVQVNSE19AUBA3VxpaEQs%253D&#34;&gt;《高性能架构之道（第二版）》&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;这本书还出版了繁体版，叫《巨型服务架构》。繁体版贵得很。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/127/pic/004_hu_73489fac7d6ce5b3.webp&#34; alt=&#34;《巨型服务架构》&#34; width=&#34;900&#34; height=&#34;675&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;就先说这么多吧。&lt;/p&gt;&#xA;&lt;p&gt;对于程序员而言，开始&lt;strong&gt;关注架构&lt;/strong&gt;就是重大进步。就已经从单纯写代码的层次里跳了出来，至少从“增删改查”中跳了出来，能以&lt;strong&gt;更宏观的视角&lt;/strong&gt;去思考代码、思考软件工程！而在AI编程迅猛发展的今天，编码能力则逐步弱化，而架构能力则更为重要。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;好了，我是软件架构师易哥。&lt;/p&gt;&#xA;&lt;p&gt;可以关注我，我会偶尔出没分享编程干货！&lt;/p&gt;</description>
			</item>
			<item>
				<title>为什么祖传代码被称为「屎山」？</title>
				<link>https://yeecode.top/blog/92/</link>
				<pubDate>Mon, 06 Oct 2025 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/92/</guid>
				<description>&lt;p&gt;祖传代码，又臭又恶心。每一个接触它的人都在心里骂了千百遍！&lt;/p&gt;&#xA;&lt;p&gt;但是，祖传代码在诞生之初，并不是这样的。&lt;/p&gt;&#xA;&lt;p&gt;时光经年，一份代码是慢慢变臭的。&lt;/p&gt;&#xA;&lt;p&gt;面对这些代码，我们要用历史发展观看它们。我们要理解，毕竟没有哪个程序员不想让自己的代码好，至少不会想让代码坏。&lt;/p&gt;&#xA;&lt;p&gt;最开始，你可能接触到的是一个很简单的需求。就和下图一样，四个模块关联起来。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/001_hu_4dae2ac940f0aad5.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;284&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;于是，你把它开发成了上图的样子。一主三从、左右分布、清晰明了。&lt;/p&gt;&#xA;&lt;p&gt;甚至，你还对代码进行了精炼和优化，一如上图中那优美的弧线一样。&lt;/p&gt;&#xA;&lt;p&gt;这是一个不错的项目。&lt;/p&gt;&#xA;&lt;p&gt;后来，需要新增一些功能。&lt;/p&gt;&#xA;&lt;p&gt;其实变化不大，只不过是原始版本的稍加变更，你继续优雅地完成了。就和下图一样。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/002_hu_35a13f63bd03b3dc.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;402&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;甚至，你还对这次变动进行了详细的注释。就像上图那绿色的连线一样，和初始版本的蓝色连线进行了清晰的区分。&lt;/p&gt;&#xA;&lt;p&gt;优雅，一如既往。&lt;/p&gt;&#xA;&lt;p&gt;后来，功能进一步地复杂了一些。你也从容出手，轻快地敲击键盘，完成了新功能。&lt;/p&gt;&#xA;&lt;p&gt;新功能就像下图的红色线条一样，虽然让项目多了几分凌乱，但也脉络清晰、功能完善。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/003_hu_d5d74de7de92d443.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;409&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;可是，你怎会停留于此？&lt;/p&gt;&#xA;&lt;p&gt;你手中的每一份代码，都是精美的艺术品。注定不是只能完成功能的工具。&lt;/p&gt;&#xA;&lt;p&gt;于是，你对它的结构进行了调整。&lt;/p&gt;&#xA;&lt;p&gt;你对一些模块进行的标注，并重新梳理了模块间的逻辑。于是得到了下面的结构。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/004_hu_7b8a45e787e8deae.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;352&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;依旧左右分布、依旧清晰明了。一如它初始时的样子，是一份艺术品。&lt;/p&gt;&#xA;&lt;p&gt;项目终究还是需要继续演进，模块间需要增加一些新的逻辑关系。&lt;/p&gt;&#xA;&lt;p&gt;不过，这次的逻辑关系稍有不同，这是你之前没有预料到的。&lt;/p&gt;&#xA;&lt;p&gt;出乎意料？这不正是编程的魅力所在么？&lt;/p&gt;&#xA;&lt;p&gt;每一个程序员都是着眼当下，面向未来！&lt;/p&gt;&#xA;&lt;p&gt;而且，这次的工期很紧、任务很难！&lt;/p&gt;&#xA;&lt;p&gt;纾难解困？这不正是编程的魅力所在么？&lt;/p&gt;&#xA;&lt;p&gt;每一个程序员都是危坐案前，敲击荆棘！&lt;/p&gt;&#xA;&lt;p&gt;所以，你还是修改代码，如期完成了上述功能。&lt;/p&gt;&#xA;&lt;p&gt;就和下面的图片一样。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/005_hu_aaf36ee971f62efc.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;321&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;甚至，为了这次功能的变动，你再次微调了模块之间的关系。让每一个箭头都从左往右流动。&lt;/p&gt;&#xA;&lt;p&gt;工期紧张，但功能依旧完善。&lt;/p&gt;&#xA;&lt;p&gt;模块众多，但结构依然清晰。&lt;/p&gt;&#xA;&lt;p&gt;“每一个箭头都从左往右流动”这一点被打破，还是在开发某次重要的功能时。&lt;/p&gt;&#xA;&lt;p&gt;那次功能复杂，时间也紧张。&lt;/p&gt;&#xA;&lt;p&gt;“不可能因为这次功能重构整个系统结构”，你这样告诉自己。&lt;/p&gt;&#xA;&lt;p&gt;“是的，即使重构，业务团队也无法将旧有的功能全部回归测试”，你再次肯定到。&lt;/p&gt;&#xA;&lt;p&gt;但是你依旧努力做到最好。&lt;/p&gt;&#xA;&lt;p&gt;系统演进成了下面的样子。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/006_hu_e29455227d97f4b4.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;397&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;岁月经年，没有那个团队会固守不变。&lt;/p&gt;&#xA;&lt;p&gt;时光荏苒，难有那个系统会止步不前。&lt;/p&gt;&#xA;&lt;p&gt;你也曾去负责其他系统，&lt;/p&gt;&#xA;&lt;p&gt;也曾再次接手过这个系统。&lt;/p&gt;&#xA;&lt;p&gt;系统也曾增加许多模块，&lt;/p&gt;&#xA;&lt;p&gt;也曾废弃很多模块。&lt;/p&gt;&#xA;&lt;p&gt;终于，&lt;/p&gt;&#xA;&lt;p&gt;系统演化成了下面的样子。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/92/pic/007_hu_456becb85540f128.webp&#34; alt=&#34;pic&#34; width=&#34;900&#34; height=&#34;449&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;凌乱，但能跑！&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;有一天，&lt;/p&gt;&#xA;&lt;p&gt;团队来了一个新同事。&lt;/p&gt;&#xA;&lt;p&gt;他看完这个系统结构后，随口说了一句：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;“这系统是谁写的啊？一坨屎!”&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;你心头一颤！&lt;/p&gt;&#xA;&lt;p&gt;想起了系统的每一个版本，每一次变动。&lt;/p&gt;&#xA;&lt;p&gt;想起了曾经功能简单的系统，&lt;/p&gt;&#xA;&lt;p&gt;想起了曾经秀发茂密的自己。&lt;/p&gt;&#xA;&lt;p&gt;你想反驳，却又不知从何说起。&lt;/p&gt;&#xA;&lt;p&gt;最终，&lt;/p&gt;&#xA;&lt;p&gt;你只是走过去，轻拍了他的肩。&lt;/p&gt;&#xA;&lt;p&gt;无言。&lt;/p&gt;</description>
			</item>
			<item>
				<title>讨论一种低耦合的数据持久化方案</title>
				<link>https://yeecode.top/blog/81/</link>
				<pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/81/</guid>
				<description>&lt;p&gt;在软件开发过程中，进行持久化操作是十分常见的，甚至很多后端程序员谑称自己为CRUD工程师。&lt;/p&gt;&#xA;&lt;h2 id=&#34;传统层式方案&#34;&gt;传统层式方案&lt;/h2&gt;&#xA;&lt;p&gt;通常，我们进行数据持久化时，会使用Service、Dao、XML（例如MyBati中配置的XML）这三层结构。然后，对象在这三层结构中流动，如下图所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/81/pic/lsg_hu_6f874fa6fd1f411f.webp&#34; width=&#34;600&#34; height=&#34;448&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;向下流动便完成了对象写入数据库的操作，向上流动便完成了对象从数据库读出的操作。&lt;/p&gt;&#xA;&lt;p&gt;这样的持久化方案十分简单且通用，因此得到了非常广泛的应用。但是也存在以下几个问题：&lt;/p&gt;&#xA;&lt;p&gt;首先是，耦合严重。一个对象和它的Service、Dao、XML都是存在耦合的，其耦合点很多。&lt;/p&gt;&#xA;&lt;p&gt;其次是使用复杂，例如要完成一个对象的持久化，则需要先找到它的Service然后调用。在进行包含多种对象的批量操作时，找到某个对象的Service的过程将变得十分复杂，往往需要按照一定的命名规则进行反射。&lt;/p&gt;&#xA;&lt;p&gt;例如，对不同类型对象的集合进行持久化，则如下所示：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt;(Object obj : objectSet) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#75715e&#34;&gt;// 找到该对象对应的Service&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;ObjService objService &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; findServie(obj);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#75715e&#34;&gt;// 调用其Service提供的持久化功能&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;objService.&lt;span style=&#34;color:#a6e22e&#34;&gt;insert&lt;/span&gt;(obj);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;findServie&lt;/code&gt;方法将会是一个较为复杂的方法。&lt;/p&gt;&#xA;&lt;p&gt;第三，其不太符合面向对象原则。因为，持久化这件事情，应该是某个对象自身的行为，没有必要依赖外部的其它类完成。&lt;/p&gt;&#xA;&lt;h2 id=&#34;对象行为式方案&#34;&gt;对象行为式方案&lt;/h2&gt;&#xA;&lt;p&gt;我们可以尝试将对象的持久化作为对象自身固有的行为。&lt;/p&gt;&#xA;&lt;p&gt;例如，在对象中设置一个&lt;code&gt;saveToDb()&lt;/code&gt;方法。只要调用某个对象的&lt;code&gt;saveToDb()&lt;/code&gt;方法，则将该对象持久化。按照这种方式，如果对不同类型对象的集合进行持久化，则如下所示：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt;(Object obj : objectSet) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#75715e&#34;&gt;// 持久化该对象&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;obj.&lt;span style=&#34;color:#a6e22e&#34;&gt;saveToDb&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;显然，操作变得更简单。&lt;/p&gt;&#xA;&lt;p&gt;更重要的是，&lt;code&gt;saveToDb()&lt;/code&gt;方法是对象自己的行为，与&lt;code&gt;toString()&lt;/code&gt;等方法一样，即如下图所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/81/pic/newcj_hu_a03c076658f7a335.webp&#34; width=&#34;541&#34; height=&#34;494&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这更符合面向对象的思想。&lt;/p&gt;&#xA;&lt;p&gt;当存在大量对象时，这一方案带来的便利性更加凸显。例如存在下图所示的一棵树。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/81/pic/tree_hu_8f699dc0baec2c12.webp&#34; width=&#34;504&#34; height=&#34;370&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;则每个节点的持久化方法可以这样写：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;save() {&#xA;&#x9;this.saveToDb();&#xA;&#x9;for(Node node: this.children) {&#xA;&#x9;&#x9;node.save();&#xA;&#x9;}&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后调用根节点的&lt;code&gt;save()&lt;/code&gt;方法便可以依次触发各个对象节点持久化，逐级触发，一气呵成。&lt;/p&gt;&#xA;&lt;p&gt;而不同对象节点的&lt;code&gt;saveToDb()&lt;/code&gt;可以自由掌控，按照不同的规划放入不同的表中。&lt;/p&gt;&#xA;&lt;h2 id=&#34;对象行为式方案中的查询&#34;&gt;对象行为式方案中的查询&lt;/h2&gt;&#xA;&lt;p&gt;为了实现单个对象的查询，我们可以设置一个&lt;code&gt;queryfromDb()&lt;/code&gt;方法，表示从数据库读取对象的数据。但是，读取数据之前，则必须要事先写定一些索引信息。例如下面的方法表示以&lt;code&gt;id=5&lt;/code&gt;和&lt;code&gt;name=&amp;quot;易哥&amp;quot;&lt;/code&gt;为索引，从数据库中查询出指定的User对象。&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;User user = new User();&#xA;user.setId(5);&#xA;user.setName(&amp;#34;易哥&amp;#34;);&#xA;// 以上面的既定信息为索引，去数据库查询一个满足条件的User对象&#xA;user.queryfromDb();&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果查询的类型比较多，可以演化出&lt;code&gt;queryFromDbById()&lt;/code&gt;、&lt;code&gt;queryFromDbByName()&lt;/code&gt;、&lt;code&gt;queryFromDbByIdAndName()&lt;/code&gt;等诸多版本。但是总体而言，这比传统层式方案要更清晰，也没有增加复杂度。&lt;/p&gt;</description>
			</item>
			<item>
				<title>并行计算的范式及其工程实现</title>
				<link>https://yeecode.top/blog/74/</link>
				<pubDate>Thu, 01 May 2025 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/74/</guid>
				<description>&lt;p&gt;本文将会介绍并行计算的基本概念和实现形式的演化，并引出MapReduce范式和DAG范式。这些知识将帮助大家理清楚并行计算的来龙去脉和两种范式之间的关系。&lt;/p&gt;&#xA;&lt;p&gt;本文也会详细介绍MapReduce范式和DAG范式的形式化定义，以及它们的工程实现。这些知识将帮助大家理清设计并行计算系统时面对的问题以及解决思路，将帮助大家设计实现一套并行计算系统。&lt;/p&gt;&#xA;&lt;p&gt;本文还会介绍Hadoop和Spark的使用。Hadoop和Spark分别是MapReduce范式和DAG范式的典型工程实现。这些知识将帮助大家了解这两个系统的核心实现，并可以上手它们。&lt;/p&gt;&#xA;&lt;p&gt;要注意的是，本文是从架构层面讨论如何设计和实现并行计算系统，而不是专门介绍Hadoop和Spark。因此在很多地方思路更为开阔，而不是全然依照这两种典型实现来阐述。本文最后也会提及，我们遵循DAG范式实现了一套与Spark截然不同的系统，服务于特定的业务场景。&lt;/p&gt;&#xA;&lt;p&gt;本文约有1.5万字，阅读约需要20分钟。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-并行计算概述&#34;&gt;1 并行计算概述&lt;/h2&gt;&#xA;&lt;p&gt;并行计算是指在多个计算引擎中开展的计算，这些计算引擎共同协作完成某一项任务。&lt;/p&gt;&#xA;&lt;p&gt;多个计算引擎可以分布在一个工作机内，例如一台多处理器工作机。多个计算引擎也可以分布在不同的工作机内，例如一个分布式系统。在本文的讨论中，我们统一用计算引擎或者节点来称呼它们，而不去细究它的物理实现。&lt;/p&gt;&#xA;&lt;p&gt;并行计算的实施是一个复杂的问题。并行计算系统要处理以下典型问题&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;计算分区问题：计算分区就是要将一个计算任务拆分为可以并行开展的小任务，并将这些小任务交给不同的节点处理，然后再将小任务的结果进行汇总。这是并行计算的核心问题。&lt;/li&gt;&#xA;&lt;li&gt;数据分区问题：在进行并行计算过程中，往往也需要将输入数据、中间数据、输出数据进行分区，以便于实现大文件的存取和提升数据的读取效率。&lt;/li&gt;&#xA;&lt;li&gt;数据交换问题：并行计算的过程中可能会出现数据的交换需求，例如某个计算节点的数据需要交给另外的节点进一步处理。并行计算系统需要为数据交换提供可靠的支持。并且由于并行计算的任务涉及的数据量比较大，因此需要数据交换具有较高的吞吐。&lt;/li&gt;&#xA;&lt;li&gt;节点调度问题：节点在运行过程中，可能因为宕机等原因退出，也可能因为数据依赖等原因等待。并行计算系统需要依据节点的工作状况、任务的进度等对参与并行计算的节点进行合理调度。整个过程中可能会涉及通信、同步等问题。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;本文更关心的是计算分区问题。计算分区问题较为复杂，包含可计算性、计算复杂性、系统可靠性、网络等众多领域的知识。如果把这些问题都交给并行计算系统的开发者来解决，势必会花费其大量精力进而导致业务问题被忽视&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;。因此，产生了并行计算的范式。&lt;/p&gt;&#xA;&lt;p&gt;接下来我们会介绍并行计算形式的演进，并引出其中两种极具参考性的形式：MapReduce范式和DAG范式。这两种范式的内部组件的耦合都是松散的，这使得它们的扩展能力和容错性都优于传统的并行计算模型（主要是指MPI）&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-并行计算的形式及范式&#34;&gt;2 并行计算的形式及范式&lt;/h2&gt;&#xA;&lt;p&gt;范式是成熟可靠的形式。并行计算的范式为并行计算系统的实施提供了标准的参考，只要按照范式提供的方案实施，开发者便可以实现一套较为成熟的并行计算系统。&lt;/p&gt;&#xA;&lt;p&gt;并行计算范式带来了以下的优点&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;为并行计算问题提供了抽象和分层，屏蔽了底层资源。&lt;/li&gt;&#xA;&lt;li&gt;降低了并行计算问题的理论复杂度，便于程序员进场开发。&lt;/li&gt;&#xA;&lt;li&gt;提升了并行计算系统间的通用性，便于进行跨系统的交流。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;目前典型的并行计算范式有MapReduce&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;和DAG&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;，对应的成熟产品有Hadoop和Spark。&lt;/p&gt;&#xA;&lt;p&gt;很多读者都通过Hadoop和Spark对MapReduce和DAG这两种并行计算范式有一定的了解，但往往也会存在不少疑惑，这些疑惑可能包括：为什么会产生MapReduce这种形式？MapReduce能解决什么问题？MapReduce形式为什么会演化出DAG形式？两者到底有什么关系？等等。&lt;/p&gt;&#xA;&lt;p&gt;因此在详细介绍MapReduce和DAG之前，我们先介绍并行计算框架的发展思路，以便于大家理清楚这两种范式所处的位置，以及它们之间的关系。&lt;/p&gt;&#xA;&lt;h2 id=&#34;21-最简单的并行计算形式&#34;&gt;2.1 最简单的并行计算形式&lt;/h2&gt;&#xA;&lt;p&gt;说起并行计算，我们首先想到的最简单形式应该如下所示：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b01_hu_62527e7eca4916dc.webp&#34; width=&#34;345&#34; height=&#34;184&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;但是这并不能称之为一个真正的并行计算，因为各个计算节点都在独立进行自身的任务，而不是共同完成一项任务。计算结束后我们将得到多个结果而不是一个汇总的结果。&lt;/p&gt;&#xA;&lt;h2 id=&#34;22-mapreduce形式&#34;&gt;2.2 MapReduce形式&lt;/h2&gt;&#xA;&lt;p&gt;显然，我们可以对上述形式进行完善，对各个节点的计算结果进行汇总，如下所示：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b02_hu_cefa69f3542af60b.webp&#34; width=&#34;451&#34; height=&#34;184&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这样，计算任务被分为了两类：前面的各个节点负责进行并行的计算；后面的一个节点负责对前面并行计算的结果进行汇总。这就是MapReduce范式的形式。我们将前面并行计算节点进行的操作称为Map操作，将后面汇总节点进行的操作称为Reduce操作。&lt;/p&gt;&#xA;&lt;p&gt;MapReduce范式的形式虽然简单，但是已经能够解决大量的并行计算问题。例如，过滤统计、搜索、排序等操作。&lt;/p&gt;&#xA;&lt;h2 id=&#34;23-mapreduce级联形式&#34;&gt;2.3 MapReduce级联形式&lt;/h2&gt;&#xA;&lt;p&gt;当然，仍然有一些问题没法通过单一的MapReduce解决。这时，我们想到通过多级MapReduce级联来解决更复杂的问题。&lt;/p&gt;&#xA;&lt;p&gt;例如，下面就展示了5个MapReduce的级联。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b03_hu_2bb685f8de00b99f.webp&#34; width=&#34;900&#34; height=&#34;620&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;24-dag形式&#34;&gt;2.4 DAG形式&lt;/h2&gt;&#xA;&lt;p&gt;这时我们依旧看着上图，思考一个问题。在MapReduce中，Map用来进行并行计算，Reduce用来进行汇总，两者共同完成一个任务。但在上图中，当多个MapReduce串联在一起时，并不是每个Map、Reduce都还有存在的意义。例如在极端情况下，Reduce操作可能只需要最后一个就可以了，而同样，很多Map操作也可以省略。这里所说的省略是指相关节点没有对数据流进行任何的修改，节点的输出数据和输入数据完全相同，即只进行了数据的透传。&lt;/p&gt;&#xA;&lt;p&gt;如果我们用虚线圆圈表示仅做透传操作的节点，则可能会得到下面的结果：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b04_hu_aead9eb99ef9aeaf.webp&#34; width=&#34;900&#34; height=&#34;620&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;显然，这些虚线圆圈表示的节点我们可以直接忽略掉，转化为下面的形式：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b05_hu_97e63883c734b287.webp&#34; width=&#34;900&#34; height=&#34;580&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这时，Map和Reduce不再成对存在，因此也不再是MapReduce级联的形式，成了一种新的形式。&lt;/p&gt;&#xA;&lt;p&gt;我们可以看出，这是一棵树。&lt;/p&gt;&#xA;&lt;p&gt;既然已经到了树，我们完全可以尝试让节点连接多个父亲，进而将其泛化为图，如下所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b06_hu_fa0c32438c921597.webp&#34; width=&#34;900&#34; height=&#34;580&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;我们发现，一个节点的输出导出给多个节点、导出给非上一级节点等，都是可行的。也就是说，将树的形式升级为图的形式也是可以的，不会增加额外的复杂性。其实也可以在MapReduce级联阶段完成从树到图的升级，都是一样的。&lt;/p&gt;&#xA;&lt;p&gt;但是这个图是有条件的。首先边是有方向的，边代表了数据流，边的方向代表了数据传输的方向。其次不能存在环，否则任务将是无法终止的。到这里，我们得到了并行计算的又一种范式：有向无环图，即DAG。&lt;/p&gt;&#xA;&lt;p&gt;现在，我们已经讨论了并行计算的形式演进，并引出了MapReduce和DAG这两种典型的并行计算范式。在接下来的章节中，我们会对这两种范式进行更为深入的介绍。&lt;/p&gt;&#xA;&lt;blockquote class=&#34;alert alert-note alert-note&#34;&gt;&#xA;    &lt;p class=&#34;alert-title&#34;&gt;拓展说明&lt;/p&gt;&#xA;    &lt;p&gt;在前面的介绍中，我们刻意避免了对包含循环调用的MapReduce展开讨论，这有利于简化讨论过程，但并不影响最终结论。&lt;/p&gt;&#xA;&lt;p&gt;循环调用是指某个MapReduce的输出结果作为输入再次输入到前面的MapReduce中，按照这样的形式，推导出的图是有向有环的。但是，只要任务是可终止的，则说明循环次数是有限的。这种情况下，我们可以将有向有环图等价转化为一个有向无环图，其转化过程和等价性证明可以规约为非确定型有穷自动机转确定型有穷自动机的过程。&lt;/p&gt;&#xA;&lt;p&gt;因此，通常我们使用有向无环图而不是有向有环图来描述一个并行计算任务，这不会缩小并行计算系统的应用范围，却能够极大地简化系统的设计和开发难度。&lt;/p&gt;&#xA;  &lt;/blockquote&gt;&lt;h2 id=&#34;3-mapreduce范式&#34;&gt;3 MapReduce范式&lt;/h2&gt;&#xA;&lt;p&gt;在本节，我们将介绍MapReduce范式的形式化表示，并介绍它的具体工程实现。&lt;/p&gt;&#xA;&lt;h2 id=&#34;31-形式化表示&#34;&gt;3.1 形式化表示&lt;/h2&gt;&#xA;&lt;p&gt;MapReduce范式十分简单，它将并行计算划分为并行计算的Map操作和汇总的Reduce操作，Map操作先于Reduce操作开展。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/74/pic/b02_hu_cefa69f3542af60b.webp&#34; width=&#34;451&#34; height=&#34;184&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;某些资料会把这里的“Map”翻译为“映射”&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;，将“Reduce”翻译为“化简”&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a href=&#34;#fn:8&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;，从“信、达、雅”的角度看还是十分特切的。但作者认为，用“并行计算”和“汇总”来称呼这两类操作可能更有助于让读者明白两类操作的具体内容，因此在本文中我们沿用后者。&lt;/p&gt;&#xA;&lt;p&gt;我们可以用下面的伪代码对MapReduce范式进行形式化的表示：&lt;/p&gt;</description>
			</item>
			<item>
				<title>面向对象中父类的确立</title>
				<link>https://yeecode.top/blog/29/</link>
				<pubDate>Sun, 30 Aug 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/29/</guid>
				<description>&lt;p&gt;封装、继承、多态，这是面向对象的三大特性。这三大特性中，有两个特性都与父类有着深深的关联：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;继承：是指子类可以获得父类的属性和方法。&lt;/li&gt;&#xA;&lt;li&gt;多态：是指父类引用可以指向父类对象，也可指向子类对象。在调用时，父类的引用可以根据引用具体指向的对象去调用。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可见，父类概念的区分在面向对象的设计中十分基础也十分重要。但实际上，大多数开发者对父类的概念有不小的认知偏差。本文我们将讨论这一点。&lt;/p&gt;&#xA;&lt;h2 id=&#34;传统认知的误区&#34;&gt;传统认知的误区&lt;/h2&gt;&#xA;&lt;p&gt;首先我们给出一个问题。&lt;/p&gt;&#xA;&lt;p&gt;问题1：矩形是不是正方形的父类？&lt;/p&gt;&#xA;&lt;p&gt;请先在心中给出这个问题的答案，然后在继续阅读本文。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;接下来，我们给出矩形类Rectangle和正方形类Square中的方法（省略方法的具体实现），如下所示。&lt;/p&gt;&#xA;&lt;p&gt;矩形类：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;public class Rectangle {&#xA;    private double length;&#xA;    private double width;&#xA;    private String color;&#xA;&#xA;    public Rectangle(double length, double width, String color) {}&#xA;    public void setLength(double length) {}&#xA;    public void setWidth(double width) {}&#xA;    public void setColor(String color) {}&#xA;    public double getArea() {}&#xA;    public String getColor() {}&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;正方形类：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;public class Square {&#xA;    private double sideLength;&#xA;    private String color;&#xA;    &#xA;    public Square(double sideLength, String color) {}&#xA;    public void setSideLength(double sideLength) {}&#xA;    public void setColor(String color) {}&#xA;    public double getArea() {}&#xA;    public String getColor() {}&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后，请再次回答问题。&lt;/p&gt;</description>
			</item>
			<item>
				<title>六边形架构和插件式架构</title>
				<link>https://yeecode.top/blog/28/</link>
				<pubDate>Sun, 26 Jul 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/28/</guid>
				<description>&lt;p&gt;在《软件复杂性的因素与解决思路》一文中我们说过，软件中技术逻辑和业务逻辑的混杂是导致软件系统复杂化的原因之一。为了降低软件的复杂度，我们需要尽量将软件中的技术逻辑和业务逻辑拆分开来。&lt;/p&gt;&#xA;&lt;p&gt;在通常的软件架构中，我们使用分层设计来实现两者的拆分。而领域驱动设计中的六边形架构则带来了新的拆分思路，进一步的，作者总结工作经验给出了插件式的拆分思路。&lt;/p&gt;&#xA;&lt;h2 id=&#34;六边形架构&#34;&gt;六边形架构&lt;/h2&gt;&#xA;&lt;p&gt;应用业务逻辑往往需要两个端口，一个是面向界面的端口，一个是面向持久层的端口。因此，我们往往会依据这两个端口将整个应用分层，进而实现业务逻辑和技术逻辑的拆离。例如典型的三层结构包含：接口层、业务层、持久层。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/28/pic/001_hu_3b2f0b4347c2b4fc.webp&#34; alt=&#34;图&#34; width=&#34;198&#34; height=&#34;188&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;在这种设计中，可能还会存在更多的层级，例如持久层又细分为service层、dao层等。而且，各个层级中可能还会穿插模型（Model）层。&lt;/p&gt;&#xA;&lt;p&gt;在这种分层的设计中，往往出现业务逻辑的碎片化。业务逻辑可能会散落到各个层级，例如，在SQL语句、模型方法、逻辑层中都存在业务逻辑，最终导致系统的混乱。&lt;/p&gt;&#xA;&lt;p&gt;六边形架构则忽略“上下”、“左右”的层级划分，而是主张“内外”。内层是指业务逻辑，外层是指接口。它是说，应用要以业务逻辑为核心，通过接口与外部系统进行交互。从其要表达的思想上看，“六边形”架构被称之为“同心圆”架构更为合适，因为它并不强调边的数目。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/28/pic/002_hu_7db257764bfa85da.webp&#34; alt=&#34;图&#34; width=&#34;587&#34; height=&#34;457&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;在六边形结构中，持久化功能只是一个挂载到业务逻辑核心上的外围系统，通过持久化接口与业务核心进行交互。同样的，客户端、WEB界面、服务API都是外围系统，通过特定的接口与业务逻辑核心进行交互。&lt;/p&gt;&#xA;&lt;p&gt;这种思想下，业务逻辑是核心，通过接口对外服务。外围系统多一方，架构也就多一个边，但无论外围系统怎么增加，接口都不会涉及业务逻辑。&lt;/p&gt;&#xA;&lt;h2 id=&#34;插件式架构&#34;&gt;插件式架构&lt;/h2&gt;&#xA;&lt;p&gt;六边形架构强调了业务逻辑的核心作用，而与之相对，我们可以以技术逻辑为核心，而将业务逻辑作为插件插入到应用中。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/28/pic/003_hu_dbd82125d26f0b0b.webp&#34; alt=&#34;图&#34; width=&#34;520&#34; height=&#34;208&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;在这种思想下，技术逻辑实现的是一个通用的平台。而业务逻辑则插接到平台上完成对应的功能。&lt;/p&gt;&#xA;&lt;p&gt;实现这种架构时要实现技术平台的通用性和易用性，以便于能够满足业务逻辑的需要。这种通用性和易用性不仅仅表现在某个业务逻辑内部，也表现在业务逻辑之间。因为，在某些场景下，业务逻辑之间需要通过平台进行互相的协作。&lt;/p&gt;&#xA;&lt;p&gt;规则引擎系统就是这种模型的良好实践，系统本身提供对规则的解析能力，而规则来决定业务逻辑。&lt;/p&gt;&#xA;&lt;p&gt;服务网格也是这种模型的良好实践，系统本身提供了服务发现和调用能力，业务应用只需要完成业务逻辑便可以插接到服务网格中享受这些服务。&lt;/p&gt;&#xA;&lt;p&gt;在插件式结构中，技术逻辑是核心，业务逻辑是插件。这种方式也能保证两类逻辑的松耦合。&lt;/p&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;&#xA;&lt;p&gt;本文介绍了六边形架构的核心思想，并结合工作实践提出了插件式架构。这两种架构方案都利于实现业务逻辑和技术逻辑的拆分，进而降低应用的复杂性，可供大家在进行软件架构时结合具体场景灵活选用。&lt;/p&gt;</description>
			</item>
			<item>
				<title>实体和值对象的聚合</title>
				<link>https://yeecode.top/blog/27/</link>
				<pubDate>Sat, 27 Jun 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/27/</guid>
				<description>&lt;p&gt;在《面向对象中的实体和值对象》一文中，我们探讨了面向对象设计中的实体和值对象。本文将在前文的基础上继续探讨实体和值对象的分簇。&lt;/p&gt;&#xA;&lt;p&gt;实体是对象，值对象也是对象。实体之间、实体和值对象之间互相引用，最终会形成一个图。如下所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/27/pic/001_hu_1c2bd1382e9317ed.webp&#34; alt=&#34;图片&#34; width=&#34;539&#34; height=&#34;393&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;实体之间，实体和值对象之间的互相引用是普遍的，因此这个图将是十分巨大的。这时面临一个拆分问题，即怎样将图拆分为多个块。&lt;/p&gt;&#xA;&lt;p&gt;在UML中，存在聚合概念。聚合是指对象之间的部分和整体的关系，并且，部分可以脱离整体而单独存在。（如果部分无法脱离整体而单独存在，则为组合。）&lt;/p&gt;&#xA;&lt;p&gt;在领域驱动设计（Domain-Driven Design，简称DDD）中，也存在聚合的概念。如果几个类必须强一致性地变化，则它们就属于一个聚合。例如，User、email、address等，它们都是和User一起变化的，因此属于一个聚合。这个聚合更像UML中的组合，因为email、address脱离了User后便失去了意义，因此具有和User相同的生命周期。&lt;/p&gt;&#xA;&lt;p&gt;DDD中的聚合有以下特点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;存在一个聚合根，它是唯一可以被外界访问的对象，且一定是实体。&lt;/strong&gt; 例如，User就是聚合根，只有它可以被外界访问，email、address等属性都从属与它。即外部对象只能持有对聚合根的引用。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;聚合内的各个对象都一致地变化，因此要被一起读写。&lt;/strong&gt; 为了实现这一点，往往将一个聚合组成一个表。也就是说，email，address信息都存在user表中。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;DDD的聚合中，非根的对象脱离了聚合根则没有意义。&lt;/strong&gt; 聚合根是核心，非根则是聚合根的说明与修饰。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为外部只能引用聚合根，于是可以得到下面的图。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/27/pic/002_hu_b436e5dcb236e363.webp&#34; alt=&#34;图片&#34; width=&#34;550&#34; height=&#34;433&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;图中虚线内的部分为一个聚合，外部只能访问到聚合根，而不能直接访问其他节点。&lt;/p&gt;&#xA;&lt;p&gt;聚合之间不需要强一致性地变动，可以放在不同的表中。&lt;/p&gt;&#xA;&lt;p&gt;这样我们就可以实现聚合的划分，即系统的类的划分。&lt;/p&gt;&#xA;&lt;p&gt;DDD中的聚合的概念是一种良好的类设计、类属性设计、类表结构设计的指导思想。&lt;/p&gt;</description>
			</item>
			<item>
				<title>面向对象中的实体和值对象</title>
				<link>https://yeecode.top/blog/26/</link>
				<pubDate>Sun, 31 May 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/26/</guid>
				<description>&lt;p&gt;在面向对象的软件设计中，万物皆为对象。甚至在Java中，为了保证万物皆为对象，还为基本类型设置了包装类。但即使这样，这些对象也需要我们进一步地进行区分。&lt;/p&gt;&#xA;&lt;p&gt;假设存在下面的对象：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;public class User {&#xA;    private Integer id;&#xA;    private String name;&#xA;    private String address;&#xA;    private String email;&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;显然User是我们定义的一个对象，而内部则是该对象的属性。&lt;/p&gt;&#xA;&lt;p&gt;我们继续分析这几个属性：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;name属性&lt;/strong&gt;。按照国人的习惯，最好是姓氏、名字单独存放，以便于在必要时展现“李先生”、“刘女士”等这样的称呼。使用一个简单的String存储显然无法实现。我们可以将name属性拆分为firstName和lastName两个属性，但这样一来，姓氏与名字的拼接工作必须由User对象负责。但是，总感觉不合适，因为所有人的姓氏和名字的拼接规则都一样，似乎不应该有独立的User完成拼接。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;address属性&lt;/strong&gt;。地址是一个长长的字符串，包含省、市、县等信息，更关键的，还有非必填的邮编等信息。如果我们把他们都放在一个String里面，显然过于混乱，也面临不好拆分的问题。于是，我们可以考虑将其作为一个地址对象存储，让地址对象包含各个子信息。但这时又让人感觉怪怪的，因为这个对象似乎难以复用。即使用户A和用户B填写了相同的地址也不好复用这一地址，因为不能让一方的修改影响另一方。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;email属性&lt;/strong&gt;。电子邮件地址有着固定的规则，在存入前需要校验。如果使用String存放电子邮件地址，则校验规则要放在别处，这破坏了内聚性。最好是让email属性自身完成。&lt;/p&gt;&#xA;&lt;p&gt;再进一步分析，我们发现&lt;strong&gt;id属性&lt;/strong&gt;也是如此。id不一定是数字，可能是一个字符串。而且，id可能包含了一套生成和校验规则，例如我们的身份证号就包含了所属人的籍贯、生日、性别等信息。&lt;/p&gt;&#xA;&lt;p&gt;分析完以上几个属性后，我们发现这些属性不是基本类型，但是又和User这种对象不同。例如我们可以分别定义Name、Address、Email类，他们介于User这类对象和基本类型之间。这些对象具有以下的特点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;没有编号，不能进行相等与否的比较。除非对象内存地址相同，否则他们就是不等的。&lt;/li&gt;&#xA;&lt;li&gt;不能更新，很像是一个基本值。如果我们要修改它，则直接生成一个新的它即可，而不是更新它。这意味着我们可以放心地多处引用同一个它，而不用担心一处修改了它影响别处。这点和String的不可更新特性一样。&lt;/li&gt;&#xA;&lt;li&gt;他们具有业务意义，仅从名字就能看出他们的作用。这一点是十分友好的。&lt;/li&gt;&#xA;&lt;li&gt;他们可以集成相关功能。因为他们本身就是对象，内部可以实现相关的验证逻辑等各类逻辑。因此是一个包含属性和行为的整体。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上这种对象，我们称之为值对象。&lt;strong&gt;值对象用来表示属性的不变值和属性的行为。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;在面向对象的编程中引入值对象能够提升代码的可读性，提升内聚性。&lt;/p&gt;&#xA;&lt;p&gt;而像User这样的对象则成为实体。实体和值对象是不同的，&lt;strong&gt;实体存在唯一性标志，而实体是否相等的判断依据就是唯一性标志。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;例如，两个User对象，他们的地址可能并不相同，但是只要两者的id一样，则这样两个对象就是相等的。假设User01存放在内存中，将其序列化再反序列化后得到User02，则User01和User02的地址并不相同，但因为两者id一样，实际为一个对象。&lt;/p&gt;&#xA;&lt;p&gt;另外再说一点，实体和值对象的界限不是绝对的。例如在一个外卖系统中，地址是一个值对象，从属于人；在社区管理系统中，地址则是一个实体，与人存在多对多的关系。具体的划分要根据业务场景来确定。&lt;/p&gt;&#xA;&lt;p&gt;接下来，我们会在实体和值对象的基础上继续探讨《实体和值对象的聚合》。&lt;/p&gt;</description>
			</item>
			<item>
				<title>软件复杂性的因素与解决思路</title>
				<link>https://yeecode.top/blog/19/</link>
				<pubDate>Sun, 26 Apr 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/19/</guid>
				<description>&lt;p&gt;软件是复杂的，这一点是软件从业者的共识。也正因为软件的复杂，使得Bug几乎成了和软件绑定的词语。在本文，我们将探讨软件复杂度的原因，及其降低软件复杂度的办法。&lt;/p&gt;&#xA;&lt;p&gt;我们认为造成软件复杂度的原因有三点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;软件的规模：软件的规模往往是庞大的，而且会随着时间的推移不断扩增。&lt;/li&gt;&#xA;&lt;li&gt;软件的结构：软件是在用技术将业务落地，因此在软件内部存在着技术逻辑和业务逻辑这两类逻辑。这两类信息的交织使得任何只懂得一方逻辑的人无法读懂软件的代码。&lt;/li&gt;&#xA;&lt;li&gt;软件的变化：相比于传统系统，软件的需求是在不断变化的。没有人会要求在已经建好的摩天大楼下再挖一个地下室，但在软件行业，这种需求屡见不鲜。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;而且以上三点是共存和互相影响的。例如，随着软件规模的扩充，则它会集成更多的功能，也便会接收到更多的变化；软件的变化可能会破坏原有的结构，使得结构进一步混乱；结构的混乱使得软件变得无序，这又增加了软件的代码规模……&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/19/pic/fuza_hu_3a63f708a83bde6.webp&#34; alt=&#34;图片&#34; width=&#34;289&#34; height=&#34;264&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;要想降低软件的复杂度，可以从以上三个方面分别入手。&lt;/p&gt;&#xA;&lt;h2 id=&#34;规模&#34;&gt;规模&lt;/h2&gt;&#xA;&lt;p&gt;控制软件的规模可以有效地降低软件的复杂度。要注意，这里的规模是指功能角度的规模，不是性能角度的规模。&lt;/p&gt;&#xA;&lt;p&gt;当软件的规模增大时，可以考虑将这些功能拆分开来，拆成不同的子应用。然后让这些应用互相协作，完成原有的功能。这实际上是复杂度的转移策略：将一个应用的复杂度转移到了多个应用和应用的协作中。在这个转移的过程中，复杂度的总量是提升的，但是可以保证拆分出的每个复杂度都在可控的范围内。&lt;/p&gt;&#xA;&lt;p&gt;往往，每一个应用的规模要控制在6-7个人能够掌控的范围内。因为这个人数时，交流成本相对较低。&lt;/p&gt;&#xA;&lt;p&gt;应用间的协作实现则可以有专门的团队搭建基础平台完成。这个工作的复杂度也是可控的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;结构&#34;&gt;结构&lt;/h2&gt;&#xA;&lt;p&gt;合理的结构规划可以降低软件的复杂度。应该保证软件结构的清晰。&lt;/p&gt;&#xA;&lt;p&gt;清晰的结构是说软件模块（或者说层次）分明，模块（或者说层次）之间的关系分明。而这些模块之间，最重要的是业务逻辑和技术逻辑之间的分明，确保两者不会交织杂糅，互相干扰。&lt;/p&gt;&#xA;&lt;p&gt;关于这一块的实现思想，我会单独理清思路介绍。&lt;/p&gt;&#xA;&lt;h2 id=&#34;变化&#34;&gt;变化&lt;/h2&gt;&#xA;&lt;p&gt;在软件的设计流程中，能否拥抱变化时一个重要的评价指标。也正因为如此，敏捷取代了传统的瀑布式编程，流行了起来。&lt;/p&gt;&#xA;&lt;p&gt;敏捷的“最小可用”思路与瀑布的“规划设计”思路不同，大大地压缩了规划设计阶段的时间，而是用频繁的迭代去补足规划设计的缺失。但是，规划的缺失可能造成工作量的成倍增加。或者，我们可以换一种描述：在敏捷开发中，当我们接到需求后，到底是要仔细地规划为以后的扩展留有余地，还是按照敏捷的原则给出最小实现？&lt;/p&gt;&#xA;&lt;p&gt;我们知道，前期规划设计的丧失，可能需要后期通过大量的返工才能补足。因此，上述问题是敏捷开发中必然面对的一个问题：选择仔细规划就背离了敏捷的初衷，选择最小实现就堵死了扩展的余地。&lt;/p&gt;&#xA;&lt;p&gt;关于此，有一个解决思路是：只被第一次变化侵扰。&lt;/p&gt;&#xA;&lt;p&gt;当面临一组需求时，直接按照敏捷的原则给出最小实现。然后，如果哪个需求发生了变更，则我们认为它可能还会变更，于是对哪个需求的实现进行详细的规划，为其保留扩展性。这样，在敏捷和扩展中寻求平衡，以此来拥抱变化。&lt;/p&gt;&#xA;&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;&#xA;&lt;p&gt;本文分析了造成软件复杂性的三个因素，并给出了各个因素的解决思路。当然，我们这里仅作思路的探讨，对于每一种思路的具体实现将会涉及到许多具体的细节问题，我们以后再逐渐展开。&lt;/p&gt;</description>
			</item>
			<item>
				<title>既然有 HTTP 请求，为什么还要用 RPC 调用？</title>
				<link>https://yeecode.top/blog/33/</link>
				<pubDate>Sun, 29 Mar 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/33/</guid>
				<description>&lt;p&gt;首先，实名赞扬题主的问题。这个问题非常好。&lt;/p&gt;&#xA;&lt;p&gt;其次，实名反对各个上来就讲RPC好而HTTP不好的答案。因为，题主的观点非常对。&lt;/p&gt;&#xA;&lt;p&gt;HTTP协议，以其中的Restful规范为代表，其优势很大。它&lt;strong&gt;可读性好，且可以得到防火墙的支持、跨语言的支持&lt;/strong&gt;。而且，在去年的报告中，&lt;strong&gt;Restful大有超过RPC的趋势&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;但是HTTP也有其缺点，这是与其优点相对应的。首先是有用&lt;strong&gt;信息占比少&lt;/strong&gt;，毕竟HTTP工作在第七层，包含了大量的HTTP头等信息。其次是&lt;strong&gt;效率低&lt;/strong&gt;，还是因为第七层的缘故。还有，其&lt;strong&gt;可读性似乎没有必要&lt;/strong&gt;，因为我们可以引入网关增加可读性。此外，使用HTTP协议调用远程方法&lt;strong&gt;比较复杂&lt;/strong&gt;，要封装各种参数名和参数值。&lt;/p&gt;&#xA;&lt;p&gt;而RPC则与HTTP互补，我们详细介绍下。&lt;strong&gt;看完这篇回答，能让你对RPC的产生、原理、实现代码都有着清晰的了解&lt;/strong&gt;。这样，也能在业务系统中，在RPC和HTTP之间做好抉择。&lt;/p&gt;&#xA;&lt;p&gt;但需要再说一句，不是说RPC好，也不是说HTTP好，两者各有千秋，还在比拼中。&lt;/p&gt;&#xA;&lt;p&gt;要问我站谁？我根据业务场景，灵活站位……&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;评论区产生了一些争论，我在这里统一进行说明。争论主要发生在两点：&lt;/p&gt;&#xA;&lt;p&gt;1、HTTP和RPC同一级别，还是被RPC包含？&lt;/p&gt;&#xA;&lt;p&gt;2、Restful也属于RPC么？&lt;/p&gt;&#xA;&lt;p&gt;对于以上两点，我画图来一一说明。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/33/pic/000_hu_d0ad16da11a58164.webp&#34; alt=&#34;图片&#34; width=&#34;718&#34; height=&#34;515&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;上图是一个比较完整的关系图，这时我们发现HTTP（图中蓝色框）出现了两次。其中一个是和RPC并列的，都是跨应用调用方法的解决方案；另一个则是被RPC包含的，是RPC通信过程的可选协议之一。&lt;/p&gt;&#xA;&lt;p&gt;因此，&lt;strong&gt;第一个问题的答案是都对。看指的是哪一个蓝色框&lt;/strong&gt;。从题主的提问看，既然题主在纠结这两者，应该是指与RPC并列的蓝色框。&lt;/p&gt;&#xA;&lt;p&gt;第二个问题是在问远程过程调用（红色框）是不是包含了Restful（黄色框），这种理解的关键在于对RPC的理解。&lt;/p&gt;&#xA;&lt;p&gt;RPC字面理解是远程过程调用，即在一个应用中调用另一个应用的方法。那Restful是满足的，通过它可以实现在一个应用中调用另一个应用的方法。&lt;/p&gt;&#xA;&lt;p&gt;但是，上述理解使得RPC的定义过于宽泛。RPC通常特指在一个应用中调用另一个应用的接口而实现的远程调用，即红色框所指的范围。这样，RPC是不包含Restful的。&lt;/p&gt;&#xA;&lt;p&gt;因此，&lt;strong&gt;第二个问题的答案是Restful不属于RPC，除非对RPC有着非常规的宽泛理解&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;RPC的英文全称是Remote Procedure Call，翻译为中文叫“远程过程调用”。其中稍显晦涩的其实就是“过程”，过程其实就是方法。所以，可以把RPC理解为“远程方法调用”。&lt;/p&gt;&#xA;&lt;p&gt;要了解远程过程调用，那先理解过程调用。非常简单，如下图，就是调用一个方法。这太常见了，不多解释。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/33/pic/001_hu_5e05eeef3cb00735.webp&#34; alt=&#34;图片&#34; width=&#34;416&#34; height=&#34;93&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;而在分布式系统中，因为每个服务的边界都很小，很有可能调用别的服务提供的方法。这就出现了服务A调用服务B中方法的需求，即远程过程调用。&lt;/p&gt;&#xA;&lt;p&gt;要想让服务A调用服务B中的方法，最先想到的就是通过HTTP请求实现。是的，这是很常见的，例如服务B暴露Restful接口，然后让服务A调用它的接口。基于Restful的调用方式因为可读性好（服务B暴露出的是Restful接口，可读性当然好）而且HTTP请求可以通过各种防火墙，因此非常不错。&lt;/p&gt;&#xA;&lt;p&gt;然而，如前面所述，基于Restful的远程过程调用有着明显的缺点，主要是效率低、封装调用复杂。当存在大量的服务间调用时，这些缺点变得更为突出。&lt;/p&gt;&#xA;&lt;p&gt;服务A调用服务B的过程是应用间的内部过程，&lt;strong&gt;牺牲可读性提升效率、易用性是可取的&lt;/strong&gt;。基于这种思路，RPC产生了。&lt;/p&gt;&#xA;&lt;p&gt;通常，RPC要求在调用方中放置被调用的方法的接口。&lt;strong&gt;调用方只要调用了这些接口，就相当于调用了被调用方的实际方法，十分易用&lt;/strong&gt;。于是，调用方可以像调用内部接口一样调用远程的方法，而不用封装参数名和参数值等操作。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/33/pic/002_hu_57955f25c0da7b55.webp&#34; alt=&#34;图片&#34; width=&#34;511&#34; height=&#34;287&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;那要想实现这个过程该怎么办呢？别急，咱们一步一步来。&lt;/p&gt;&#xA;&lt;p&gt;首先，调用方调用的是接口，必须得为接口构造一个假的实现。显然，要使用动态代理。这样，调用方的调用就被动态代理接收到了。&lt;/p&gt;&#xA;&lt;p&gt;第二，动态代理接收到调用后，应该想办法调用远程的实际实现。这包括下面几步：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;识别具体要调用的远程方法的IP、端口&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将调用方法的入参进行序列化&lt;/li&gt;&#xA;&lt;li&gt;通过通信将请求发送到远程的方法中&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这样，远程的服务就接收到了调用方的请求。它应该：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;反序列化各个调用参数&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;定位到实际要调用的方法，然后输入参数，执行方法&lt;/li&gt;&#xA;&lt;li&gt;按照调用的路径返回调用的结果&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;整个过程如下所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/33/pic/003_hu_2416b89822ebb0ab.webp&#34; alt=&#34;图片&#34; width=&#34;571&#34; height=&#34;450&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这样，RPC操作就完成了。&lt;/p&gt;&#xA;&lt;p&gt;调用方调用内部的一个方法，但是被RPC框架偷梁换柱为远程的一个方法。之间的通信数据可读性不需要好，只需要RPC框架能读懂即可，因此效率可以更高。通常使用UDP或者TCP作为通讯协议，当然也可以使用HTTP。例如下面的示例中，为了保证实现最简单，就用了HTTP进行通信。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;讲到这里，RPC的产生原因、原理应该清楚了。为了让大家真的明白，我写了一个真的是最最简单的RPC实现。把它放到了：&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/yeecode/EasyRPC&#34;&gt;https://github.com/yeecode/EasyRPC&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;​它包含一个客户端，一个服务端。客户端只要调用自身内部的接口，就通过这个小的RPC实现调用到了服务端的方法。&lt;/p&gt;&#xA;&lt;p&gt;下面是客户端的代码，看着类有点多，其实代码不长。其中的RPC代码完成完成动态代理、远程调用参数序列化、远程调用发起、远程调用结果反序列化的工作。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/33/pic/client_hu_8d3a469eab3f2b68.webp&#34; alt=&#34;图片&#34; width=&#34;478&#34; height=&#34;347&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;下面是服务端的代码，代码更少，完成远程调用接收、调用参数反序列化、调用实际触发、调用结果序列化的工作。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/33/pic/server_hu_59da3ef9910ac39.webp&#34; alt=&#34;图片&#34; width=&#34;466&#34; height=&#34;242&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这样，一个RPC小框架就做完了，并不复杂。&lt;/p&gt;&#xA;&lt;p&gt;所以，不要被RPC吓到，它就是让一个应用调用另一个应用中方法的一种实现方式。与调用远程接口区别不大，条条大路通罗马。&lt;/p&gt;&#xA;&lt;p&gt;再说一次，&lt;strong&gt;不是说RPC好，也不是说HTTP好，两者各有千秋&lt;/strong&gt;。本质上，&lt;strong&gt;两者是可读性和效率之间的抉择，通用性和易用性之间的抉择&lt;/strong&gt;。最终谁能发展更好，很难说。&lt;/p&gt;&#xA;&lt;p&gt;要问我站谁？&lt;strong&gt;我根据业务场景，灵活站位……&lt;/strong&gt;&lt;/p&gt;</description>
			</item>
			<item>
				<title>手机里塞两个处理器芯片运算能力会不会翻倍？</title>
				<link>https://yeecode.top/blog/42/</link>
				<pubDate>Sat, 29 Feb 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/42/</guid>
				<description>&lt;blockquote&gt;&#xA;    &lt;p&gt;就是想到现在手机厂商都在比处理器运算能力，骁龙，麒麟，苹果几乎每年都要更新高端处理器，那在一个手机里放两个处理器，是不是就两倍欢乐了？&lt;/p&gt;&#xA;&#xA;  &lt;/blockquote&gt;&lt;p&gt;该提问中提到的**“运算能力”是一个比较模糊的概念**，具体而言，存在两种理解：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;第一种理解：**计算能力是处理器的一种内在属性，就像质量、体积一样，不需要对外表现。**例如，时钟频率、MIPS都是其典型的衡量指标，这些指标侧重衡量处理器的内在属性。&lt;/li&gt;&#xA;&lt;li&gt;第二种理解：**计算能力是处理器的一种外在表现，要展现出来，比如能够在更短的时间内解决一个计算问题。**例如，PDR、所有的基准测试程序法都是在衡量处理器的外在表现，基本是让处理器解决一组特点问题来判断其性能好坏。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为“运算能力”是一个模糊的概念，没有明确的定义，因此这两种理解没有对错之分。&lt;strong&gt;事实上，时钟频率、MIPS、PDR、许多基准测试程序法都是衡量处理器性能的指标，也没有进行太明确的区分。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;如果题主问的是第一种理解，那显然。放两个就翻倍，再放两个再翻倍。线性增长关系。&lt;/p&gt;&#xA;&lt;p&gt;如果题主问的是第二种理解，那就不是翻倍了，做不到。&lt;/p&gt;&#xA;&lt;p&gt;而且考虑到在题目描述中，题主问“是不是就两倍欢乐了”，那题主应该是在侧重询问外在表现。因为，&lt;strong&gt;通常觉着手机用起来更欢乐了，应该是手机更流畅了，而不是得知手机CPU的晶体管数目增加了……&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;其实，一般情况下，消费者看重的是第二种理解。举例子：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“手机晶体管数多了，我真欢乐！”× 感觉这人不太聪明的样子&lt;/li&gt;&#xA;&lt;li&gt;“手机变得更流畅了，我真欢乐！”√ 感觉像个正常人了&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以，我们就按照第二种理解详细回答这个问题。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;首先我们不考虑CPU，先考虑人。&lt;/p&gt;&#xA;&lt;p&gt;是想本来打扫卫生的活是你自己完成，用时为10分钟。&lt;/p&gt;&#xA;&lt;p&gt;然后假设是你和你同桌完成，你俩干活一样麻利，那用时就是5分钟？&lt;/p&gt;&#xA;&lt;p&gt;注意：这里每个人都不能一边干活一边说话，而且如果想看下对方正在干什么，也要停下手中的活。这样是为了确保每个人都是单核的，每个时刻只能干一件事，就和单核CPU一样。&lt;/p&gt;&#xA;&lt;p&gt;那4个人呢？8个人呢？是不是隐隐感觉不对了？&lt;/p&gt;&#xA;&lt;p&gt;那如果这样，直接找600个人，1s钟完成教室打扫工作？！&lt;/p&gt;&#xA;&lt;p&gt;显然不是。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;两个人的时候，你们俩要协调分工，讨论下谁扫地谁整理桌子。这样多出来了一些协调工作，最终会大于5分钟，所以达不到翻倍的效果。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;CPU也一样啊，需要将任务进行并行拆分，然后并行计算，然后再合并结果。其中并行拆分和合并结果部分，是相比单核结算多出来的工作。是会额外消耗时间的。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;而且，这还是理想的情况。因为&lt;strong&gt;有些任务无法进行并行拆分。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;想当年，矩阵的并行计算，学术界和工程界可是费了老大劲的。矩阵的并行计算解决，才能使用多核来实现高效的协同推荐等工作啊！（这个具体我记不太清了，似乎是谷歌在这方面做了不少工作，欢迎补充）&lt;/p&gt;&#xA;&lt;p&gt;好了，以上都是定性的思考。接下来我要请出阿姆达尔了！&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;Gene Amdal 在1967提出了阿姆达尔定律。&lt;/p&gt;&#xA;&lt;p&gt;阿姆达尔定律（英语：Amdahl&amp;rsquo;s law，Amdahl&amp;rsquo;s argument），一个计算机科学界的经验法则，因吉恩·阿姆达尔而得名。它代表了处理器平行运算之后效率提升的能力。&lt;/p&gt;&#xA;&lt;p&gt;首先定律中提出了加速比的概念：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/42/pic/001_hu_e8f69541ace9c4a9.webp&#34; alt=&#34;图片&#34; width=&#34;413&#34; height=&#34;110&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;然后定律讨论了升级某个模块后，系统的总执行时间：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/42/pic/002_hu_8a6cb92475ccde9f.webp&#34; alt=&#34;图片&#34; width=&#34;415&#34; height=&#34;137&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;当然，这里有一点要注意：&lt;strong&gt;这是一个经验法则，不是绝对的，但是相对准确的。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;那我们再回到这个问题，假设手机任务中，必须串行计算的任务占比$n$ ，则 $0&amp;lt;n \leq1$ 。&lt;/p&gt;&#xA;&lt;p&gt;$ 0&amp;lt;n $ 是因为即使是完全可并行的任务，也需要进行任务拆分与汇总，因为手机的设备不是并行的。例如，计算完成后，必须通过唯一的显示屏进行显示，其中必定涉及到CPU串行汇总的工作，不可能两个核同时控制一个显示屏。&#xA;$ n \leq1 $ 是因为可能存在完全串行的任务，这在理论和实践中都是可能的。例如我写了一个没有进行多核优化的程序，而现在手机只运行了这一个程序。则此时，其他的核都闲着。电脑也一样，为啥玩游戏主要看单核性能？因为许多游戏没进行多核优化，其计算没办法拆解到多个核中并发，所以，核再多也没用，一个在拼命干活，其他在……闲着……&#xA;那么，由单核升级为多核后，则升级比例为 $ 1-n $ ；一核变两核，则加速比为 $ 2 $ ，那么有：&lt;/p&gt;&#xA;$$ 总加速比=\frac{1}{n&amp;#43;\frac{1-n}{2}} ，0&amp;lt;n\leq 1 $$&lt;p&gt;则显然有&lt;/p&gt;&#xA;$$ 1\leq总加速比&amp;lt;2 $$&lt;p&gt;所以，&lt;strong&gt;手机里塞两个处理器芯片运算能力可能会提升，但是不会翻倍。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;更经典的是，这个问题可以扩展到软件开发管理领域，甚至人力资源管理领域。&lt;/p&gt;&#xA;&lt;p&gt;例如在软件开发管理领域，我们知道&lt;strong&gt;两个程序员的产出不会达到一个程序员的两倍&lt;/strong&gt;（他们总得拿出一些时间来协si调bi）。&lt;/p&gt;&#xA;&lt;p&gt;那就出现几个问题：&lt;strong&gt;如果我有一个大项目，是人越多干的越快么？如果项目很大，必须要多人完成，该如何进行分工？&lt;/strong&gt;&lt;/p&gt;</description>
			</item>
			<item>
				<title>如何架构设计一个用户操作日志模块?</title>
				<link>https://yeecode.top/blog/30/</link>
				<pubDate>Sun, 09 Feb 2020 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/30/</guid>
				<description>&lt;p&gt;这是一个非常不错的题目。该题目涉及软件架构设计与开发的多个方面，具有很强的通用性。研究好这个问题对于开发能力的提升很大。&lt;/p&gt;&#xA;&lt;p&gt;今天有时间，我来解答一下这个问题。并且，最后还会附上实现代码。&lt;/p&gt;&#xA;&lt;p&gt;最终实现的效果如下所示，这是实际截取的图：&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/30/pic/009_hu_997c3b9520cb0dad.webp&#34; alt=&#34;图片&#34; width=&#34;720&#34; height=&#34;578&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;整个解答包括问题定义、模型设计、方案设计、最终实现等多个环节。展现了系统架构设计的全部流程。&lt;/p&gt;&#xA;&lt;p&gt;目录如下：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1 功能定义&#xA;2 模型设计&#xA;    2.1 上层切面&#xA;    2.2 下层切面&#xA;    2.3 混合切面&#xA;3 对象属性对比功能实现&#xA;4 对象属性处理&#xA;    4.1 普通属性&#xA;    4.2 特殊属性&#xA;    4.3 业务属性&#xA;5 易用性注解&#xA;6 存储设计&#xA;7 方案总结&#xA;8 系统实现&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;1-功能定义&#34;&gt;1 功能定义&lt;/h2&gt;&#xA;&lt;p&gt;在开发一个系统之前，我们&lt;strong&gt;先要对系统进行明确的定义&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;在一个软件系统中，通常存在增删改查四类操作。对于日志系统而已，这四类操作的处理难度不同。&lt;/p&gt;&#xA;&lt;p&gt;查询操作往往不需要记录日志，增加和删除操作涉及一个对象状态，编辑操作涉及对象编辑前和编辑后的两个状态。&lt;/p&gt;&#xA;&lt;p&gt;**编辑操作是整个日志模块中最难处理的。**只要掌握了编辑操作，则新增操作、删除操作、查询操作都很简单了。因为，新增操作可以理解为null到新对象的编辑，删除操作可以理解为旧对象到null的编辑，查询操作可以理解为旧对象到旧对象的编辑。&lt;/p&gt;&#xA;&lt;p&gt;因此，本文主要以编辑操作为例进行介绍。&lt;/p&gt;&#xA;&lt;p&gt;为了便于描述，我们假设一个学校卫生大扫除系统。&lt;/p&gt;&#xA;&lt;p&gt;这个系统中包含很多方法，例如分配大扫除工作的assignTask方法，开始某个具体工作的startTask方法，验收某个具体工作的checkTask方法，增加新的人员的addUser方法等。每个方法都有不同的参数，涉及不同的对象。&lt;/p&gt;&#xA;&lt;p&gt;以startTask方法为例，开始一个任务需要在任务中记录开始时间、责任人、使用的工具，整个方法如下：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;public String startTask(String taskId, Integer userId, Date startTime, Tool tool) {&#xA;     // 业务代码      &#xA;} &#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最简单的记录日志的方法便是在代码中直接根据业务逻辑写入日志操作语句，例如：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;public String startTask(String taskId, Integer userId, Date startTime, Tool tool) {&#xA;    // 业务代码&#xA;    log.add(&amp;#34;操作类型：开始任务。任务编号：&amp;#34; + taskId + &amp;#34;；责任人：&amp;#34; + userId ……);&#xA;    // 业务代码&#xA;}&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果你真的打算使用上述的方法记录日志，那已经没有什么可以教你的了。&lt;/p&gt;</description>
			</item>
			<item>
				<title>论接口的归属</title>
				<link>https://yeecode.top/blog/03/</link>
				<pubDate>Sat, 23 Sep 2017 00:00:00 +0000</pubDate>
				<guid>https://yeecode.top/blog/03/</guid>
				<description>&lt;p&gt;接口是系统架构设计中一种常用的抽象形式，它将功能的定义和功能的实现拆分开来。接口的使用也十分广泛，例如依赖倒置原则（Dependence Inversion Principle）便要求程序要依赖抽象接口，而不是依赖于具体的实现。&lt;/p&gt;&#xA;&lt;p&gt;在本文我们探讨接口的归属问题，即接口是归属于接口调用方还是接口的实现方。这是一个纯概念性的讨论，但将会对软件的架构设计理念有着重要的意义。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-接口属于实现方&#34;&gt;1. 接口属于实现方？&lt;/h2&gt;&#xA;&lt;p&gt;通常，我们认为一个接口属于它的实现方。&lt;/p&gt;&#xA;&lt;p&gt;因为，实现和接口共同组成了一个可以对外提供服务的模块整体，如下图所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/03/pic/shixian_hu_96e5104e4a285b7f.webp&#34; alt=&#34;接口属于实现方示意图&#34; width=&#34;310&#34; height=&#34;325&#34; loading=&#34;eager&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这时，接口成为了实现的描述，调用方只需要通过接口便可以了解被调用模块的功能，而不需要分析其具体的实现细节。&lt;/p&gt;&#xA;&lt;p&gt;而调用方则被排除在外，成了模块的外部系统。它能做的就是接受接口的定义，在此基础上才能依照接口的定义使用模块。&lt;/p&gt;&#xA;&lt;p&gt;按照上述的理解，保证了接口和实现组成的被调用模块的内聚性，但却带来了一个严重的问题：变更传递。当模块发生变化时，接口和实现都会变化，这导致调用方不得不修改代码。当然，实现方也可以只修改实现而不修改接口，但这完全依赖实现方的仁慈。毕竟，接口是属于实现方的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-接口属于调用方&#34;&gt;2. 接口属于调用方？&lt;/h2&gt;&#xA;&lt;p&gt;将接口划归调用方则可以解决变更传递的问题。而且，将接口划归调用方有以下依据。&lt;/p&gt;&#xA;&lt;h2 id=&#34;21-接口的契约功能&#34;&gt;2.1 接口的契约功能&lt;/h2&gt;&#xA;&lt;p&gt;接口实际可以理解为一个契约，契约的签约方是调用方和实现方。调用方承诺按照接口定义的方式来调用实现方；实现方承诺按照接口定义的方式来服务调用方。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/03/pic/qiyue_hu_73593a5835ab401c.webp&#34; alt=&#34;接口是契约&#34; width=&#34;312&#34; height=&#34;301&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;但是，我们会发现上述契约并不是对等的。如果调用方违背契约，例如修改了方法名或者参数数目，是无法对应到具体实现的，因此，调用方不会违背契约。如果实现方违背契约，例如修改了方法的实现逻辑或者返回值，是很有可能的，因此，实现方可能会违背契约。可见，接口这个契约，主要强调的应该是实现方对调用方的承诺。对于这样的契约应该交由调用方保管。&lt;/p&gt;&#xA;&lt;p&gt;或者换一种理解。实现方有修改接口（违背契约）的意愿，如果接口交由实现方管理，则接口很有可能会频繁变动，这就违背了契约精神。而将接口交由实现方管理，则可以保证接口的稳定。&lt;/p&gt;&#xA;&lt;p&gt;按照这种思路，接口应该交由调用方管理，即划归调用方。而实现方则必须按照接口的定义来实现具体的功能。&lt;/p&gt;&#xA;&lt;h2 id=&#34;22-接口的松耦合功能&#34;&gt;2.2 接口的松耦合功能&lt;/h2&gt;&#xA;&lt;p&gt;接口是调用方的桩，这种桩的耦合往往是紧密的，例如下面的一个DAO接口中，显然&lt;code&gt;RoleModel&lt;/code&gt;是来自于调用方的Bean,也就是说接口本身依赖了调用方。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RoleModel &lt;span style=&#34;color:#a6e22e&#34;&gt;queryByName&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;@Param&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;) String name, &lt;span style=&#34;color:#a6e22e&#34;&gt;@Param&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;appName&amp;#34;&lt;/span&gt;) String appName);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;接口是实现的抽象，同样是上述接口，其实现要确保返回值为&lt;code&gt;RoleModel&lt;/code&gt;，方法名为&lt;code&gt;queryByName&lt;/code&gt;，两个入参为&lt;code&gt;String&lt;/code&gt;。这些信息是在接口中定义的，会直接传递给实现方法，是一种强约束。&lt;/p&gt;&#xA;&lt;p&gt;因此，我们可以得到下面的图。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/03/pic/yilai_hu_9ace8c48990f83c7.webp&#34; alt=&#34;接口的归属划分&#34; width=&#34;474&#34; height=&#34;425&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这时我们发现：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果将接口划归实现方，如虚线1所示。则实现方和调用方之间存在双向依赖。&lt;/li&gt;&#xA;&lt;li&gt;如果将接口划归调用方，如虚线2所示。则调用方单向依赖实现方，实现方受到调用方给出的接口的强约束。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;显然，后者才是我们追求的调用方和实现方的关系。&lt;/p&gt;&#xA;&lt;p&gt;按照这种理解，调用方持有了接口，也就持有了它需要的服务。实现方则是服务的提供者，具体是由哪个具体实现提供，这不重要，只要符合调用方提供的接口规范即可。于是，这完美地实现了依赖倒置原则（Dependence Inversion Principle）的要求，避免了实现方变更的向上传递。如图所示。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://yeecode.top/blog/03/pic/diaoyongf_hu_8f179201c924e.webp&#34; alt=&#34;接口属于调用方&#34; width=&#34;615&#34; height=&#34;271&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;3-总结&#34;&gt;3. 总结&lt;/h2&gt;&#xA;&lt;p&gt;通常，我们认为接口由实现抽象而来，即在调用方和实现方中，实现方是接口的生成者。然而这种理解是片面的。&lt;/p&gt;&#xA;&lt;p&gt;接口，应该像是一种契约、一种需求，由调用方提出，并交由实现方给出具体的实现。这种思路利于保证接口契约的稳定，也利于实现模块间的松耦合。&lt;/p&gt;</description>
			</item>
	</channel>
</rss>
