每一个模式都描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。” ——Christopher Alexander
注:模型思维
招式套路可以千变万化,扎实深厚的“内功”却是始终如一!虽然企业应用涉及的软件技术不断翻新,但是基本的架构及设计思想却没有太多变化。
将以前行之有效的设计思路和方法加以适当调整,并应用到当前的问题上,是最高效的做法。
Fowler将51种经常出现的解决方案转化成模式,最终融会成这本“内功心法”。
“模式的关键点是它们源于实践。必须观察人们的工作过程,发现其中好的设计,并找出‘这些解决方案的核心’。这不是一个简单的过程,但是一旦发现了某个模式,它将是非常有价值的。
模式只是一个有益的起点,而非最终的解决之道。
很多人都试图给“架构”下定义,而这些定义本身却很难统一。能够统一的内容有两点:一点是“系统分解为部件的最高级别”;另一点是“系统中不易改变的决定”。越来越多的人发现:表述一个系统架构的方式不止一种;一个系统中也可能有很多种不同的架构,而且,对于什么在架构上意义重大的看法也会随着系统的生命周期变化。
注:核心原则————隔离术:核心业务和非核心业务,不怎么更改的业务和易变业务,稳定和不稳定业务等等。
实际上,你可以做的最好的事情之一是通过简化架构和过程,将一个大型项目变成小型项目。
当你使用模式时请记住:它们只是开始,而不是结束。任何作者去囊括项目开发中的所有变化和技术是不可能的。
请大家记住:所有模式都是不完备的,你们都有责任在自己的系统中完善它们,你们也会在这个过程中得到乐趣。
注:存在即合理,且拘于场合之束
分层术
注:分而治之,降耦合
在分解复杂的软件系统时,软件设计者用得最多的技能之一就是分层。
将系统按照层次分解有很多重要的好处:
❑在无须过多了解其他层次的基础上,可以将某一层作为一个连贯的整体来理解。
❑可以替换某层的具体实现,只要前后提供的服务相同即可。
❑可以将层次间的依赖性减到最低。
❑分层有利于标准化工作。TCP和IP是标准,因为它们定义了各自层次的操作方式。
❑一旦构建好了某一层次,就可以用它为很多上层服务提供支持。因此,TCP/IP同时被FTP、telnet、SSH和HTTP使用。否则,所有这些高层协议都必须编写它们各自的低层协议。
分层是一种重要的技术,但也有缺陷:
❑层次封装了一些内容,但不是全部。有时它会为我们带来级联修改。在分层企业应用中,一个经典例子是添加一个需要在用户界面上显示且必须在数据库中的字段,那么还必须在用户界面和数据库之间的每一层做相应的修改。
❑过多的层次会影响性能。在每一层,一般都会从一种表现形式转换到另一种。不过底层功能的封装通常带来比代价更大的效率提升。例如,可以优化事务控制层,提高其他各层的效率。
然而,分层架构中最困难的问题是决定建立哪些层次以及每一层的职责是什么。
将系统中各部分分离,以降低不同部分之间的耦合。即使是都运行在同一台计算机上,不同层次间的分离也是有用的。当然,系统物理结构的不同会有所影响。
映射到关系数据库
尽量使用已预先编译好的静态SQL,而不是每次都编译动态SQL。一个重要的规则是避免使用字符串串联起多个SQL查询。
注:预编译提高效率,并防止 sql 注入
无论创建连接的代价是高还是低,连接都必须好好管理。因为它们是珍贵的资源,必须在使用完毕时立刻关闭。还有,如果正在进行一次事务,通常需要保证:在这次特定的事务中,每一个命令都是从同一个连接发出的。
注:事务与连接绑定,让事务管理连接(池),在 java 实现的 ORM 框架里,底层大多与 ThreaLoacal 有关⚠️
由于连接对于事务来说如此密不可分,因此管理它们的好方法就是把它们捆绑到事务中去。当开始一个事务的时候打开一个连接,当提交或者回滚的时候就关闭它。让事务知道它在使用什么样的连接,这样就可以完全不管连接而仅仅处理事务就可以了。因为事务的完成有一种可见的效果,所以即使是忘了提交,也很容易把它标识出来。工作单元很自然地适用于管理事务和连接。
Web表示层
使用模型-视图-控制器的理由是要保证模型和Web表示层的完全分离,把处理过程放到分离的事务脚本或分离的领域模型对象中也会使得它们容易测试。
并发
并发问题
更新丢失(Lost update):
例如:Martin编辑了一个文件,对其中的checkConcurrency方法进行了一些修改,这个操作需要花几分钟的时间。与此同时,David对相同文件中的updateImportantParameter方法也进行了修改。David很快开始并完成了他的修改,虽然他是在Martin之后开始,但是却在Martin之前完成。很不幸,Martin读的文件并没有包括David的更新,因此当Martin写入文件时,就会覆盖David更新过的那个版本,David的更新就永远丢失了。
注:丢失更新属于写写并发覆盖问题,一般用乐观锁可解决
不一致读(inconsistent read)
发生在读取两份各自正确的数据而它们却在同一时间互相矛盾时。
注:不一致读属于读读或读写并发问题
隔离与不变性
并发问题由来已久,人们提出了各种不同的解决方案。对于企业应用来说,有两个非常重要的解决方案:一个是隔离(isolation),一个是不变性(immutability)。
并发问题发生在多个执行单元(例如进程或线程)同时访问同一片数据的时候。一个解决的办法就是隔离:划分数据,使得每一片数据都只能被一个执行单元访问。操作系统为每个进程单独分配一片内存,并且只有这个进程可以对这片内存进行读或写操作。同样地,你可以发现在很多流行的高效应用软件中都有文件锁。
隔离是一种减少错误发生概率的有效技术。
只有在共享数据可以修改的情况下,并发问题才会出现。所以,一个避免并发冲突的方法是识别哪些是不变的数据。
另一个观点是把那些只读取数据的程序分开来,让它们只使用拷贝的数据源,这样就可以放松所有的并发控制。
注:写时复制可以解决一部分并发问题
乐观并发控制和悲观并发控制
当有一些可变数据无法隔离的时候,会发生什么样的情况呢?总的来说,我们可以使用两种形式的并发控制策略:乐观并发控制和悲观并发控制。
如果把乐观锁看作是关于冲突检测的,那么悲观锁就是关于冲突避免的。
在实际应用的源代码控制系统中,这两种策略都可以被使用,但是现在大多数源代码开发者更倾向于使用乐观锁策略。(有一种很有道理的说法:乐观锁并不是真正的锁定,但是这种叫法很方便并且广泛流传,以至于不容忽略。)
悲观锁的问题是减少了并发的程度。
注:悲观锁影响性能时可考虑分段锁、分离锁、读写锁、细粒度锁、批量操作等优化。
在乐观锁和悲观锁之间进行选择的标准是:冲突的频率与严重性。如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突的结果对于用户来说是痛苦的,那么就需要使用悲观锁策略。
但是这两种策略都存在问题。使用的时候确实很容易引入其他问题,而且可能产生的麻烦和原先想要解决的并发问题一样多。
事务
为了处理最大的吞吐率,现代的事务处理系统被设计成保证事务尽可能短。为此,要尽可能不让事务跨越多个请求。跨越多个请求的事务称为长事务。
注:避免大事务场景:……
而大多数的事务系统并不能很有效地支持长事务。
使用长事务可以避免许多麻烦。然而,应用将失去可伸缩性,因为长事务使数据库成为主要的瓶颈。另外,将长事务改写成短事务是一个复杂且不好理解的过程。
另一种方法是尽可能晚打开事务。使用延迟事务时,应在事务外完成读取数据的操作,只在修改数据的时候启动事务。这样做的好处是减少了事务执行的时间。在启动事务和第一次写操作之间有较长时间间隔的情况下,这样做更能增加系统的灵活性。然而,这意味着在事务启动前没有任何并发控制机制,可能会导致不一致读问题。因此通常并不这么做,除非数据竞争很激烈,或者业务事务跨越多个请求。
使用事务时,需要清楚地知道被锁住的到底是什么。对于许多数据库操作来说,事务系统锁住的是被访问的数据行,这样就可以允许多个事务同时访问一个表。然而,如果一个事务锁住了一个表中的许多行,则数据库无法处理那么多锁,只能将锁升级到锁住整个表——从而将其他事务锁在外面。这种锁升级(lock escalation)对并发有很大影响。