十分钟带你了解 Apache Flink 核心技术

Apache Flink 介绍

Apache Flink 是近年来越来越流行的一款开源大数据计算引擎,它同时支持了批处理和流处理,也能用来做一些基于事件的应用。使用官网的一句话来介绍 Flink 就是 “Stateful Computations Over Streams”

首先 Flink 是一个纯流式的计算引擎,它的基本数据模型是数据流。流可以是无边界的无限流,即一般意义上的流处理。也可以是有边界的有限流,这样就是批处理。因此 Flink 用一套架构同时支持了流处理和批处理。其次,Flink 的一个优势是支持有状态的计算。如果处理一个事件(或一条数据)的结果只跟事件本身的内容有关,称为无状态处理;反之结果还和之前处理过的事件有关,称为有状态处理。稍微复杂一点的数据处理,比如说基本的聚合,数据流之间的关联都是有状态处理。

Apache Flink 之所以能越来越受欢迎,我们认为离不开它最重要的四个基石:Checkpoint、State、Time、Window。

首先是Checkpoint机制,这是 Flink 最重要的一个特性。Flink 基于 Chandy-Lamport 算法实现了分布式一致性的快照,从而提供了 exactly-once 的语义。在 Flink 之前的流计算系统(如 Strom,Samza)都没有很好地解决 exactly-once 的问题。提供了一致性的语义之后,Flink 为了让用户在编程时能够更轻松、更容易地去管理状态,引入了托管状态(managed state)并提供了 API 接口,让用户使用起来感觉就像在用 Java 的集合类一样。除此之外,Flink 还实现了 watermark 的机制,解决了基于事件时间处理时的数据乱序和数据迟到的问题。最后,流计算中的计算一般都会基于窗口来计算,所以 Flink 提供了一套开箱即用的窗口操作,包括滚动窗口、滑动窗口、会话窗口,还支持非常灵活的自定义窗口以满足特殊业务的需求。

在 Flink 1.0.0 时期,加入了 State API,即 ValueState、ReducingState、ListState 等等。State API 可以认为是 Flink 里程碑式的创新,它能够让用户像使用 Java 集合一样地使用 Flink State,却能够自动享受到状态的一致性保证,不会因为故障而丢失状态。包括后来 Apache Beam 的 State API 也从中借鉴了很多。

在 Flink 1.1.0 时期,支持了 Session Window 以及迟到数据容忍的功能。

在 Flink 1.2.0 时期,提供了 ProcessFunction,这是一个 Lower-level 的API,用于实现更高级更复杂的功能。它除了能够注册各种类型的 State 外,还支持注册定时器(支持 EventTime 和 ProcessingTime),常用于开发一些基于事件、基于时间的应用程序。

在 Flink 1.3.0 时期,提供了 Side Output 功能。算子的输出一般只有一种输出类型,但是有些时候可能需要输出另外的类型,比如除了输出主流外,还希望把一些异常数据、迟到数据以侧边流的形式进行输出,并分别交给下游不同节点进行处理。简而言之,Side Output 支持了多路输出的功能。

在 Flink 1.5.0 时期,加入了BroadcastState。BroadcastState是对 State API 的一个扩展。它用来存储上游被广播过来的数据,这个 operator 的每个并发上存的BroadcastState里面的数据都是一模一样的,因为它是从上游广播而来的。基于这种State可以比较好地去解决 CEP 中的动态规则的功能,以及 SQL 中不等值Join的场景。

在 Flink 1.6.0 时期,提供了State TTL功能、DataStream Interval Join功能。State TTL实现了在申请某个State时候可以在指定一个生命周期参数(TTL),指定该state过了多久之后需要被系统自动清除。在这个版本之前,如果用户想要实现这种状态清理操作需要使用ProcessFunction注册一个Timer,然后利用Timer的回调手动把这个State清除。从该版本开始,Flink框架可以基于TTL原生地解决这件事情。另外 DataStream Interval Join 功能也叫做 区间Join。例如左流的每一条数据去Join右流前后5分钟之内的数据,这种就是5分钟的区间Join。

在 Flink 1.0.0 时期,Table API (结构化数据处理API)和 CEP(复杂事件处理API)这两个框架被首次加入到仓库中。Table API 是一种结构化的高级 API,支持 Java 语言和 Scala 语言,类似于 Spark 的 DataFrame API。但是当时社区对于 SQL 的需求很大,而 SQL 和 Table API 非常相近,他们都是一种处理结构化数据的语言,实现上可以共用很多内容。所以在 Flink 1.1.0里面,社区基于Apache Calcite对整个 Table 模块做了重构,使得同时支持了 Table API 和 SQL 并共用了大部分代码。

在 Flink 1.2.0 时期,社区在Table API和SQL上支持丰富的内置窗口操作,包括Tumbling Window、Sliding Window、Session Window。

在 Flink 1.3.0 时期,社区首次提出了Dynamic Table这个概念,借助Dynamic Table,流和批之间可以相互进行转换。流可以是一张表,表也可以是一张流,这是流批统一的基础之一。其中Retraction机制是实现Dynamic Table的基础,基于Retraction才能够正确地实现多级Aggregate、多级Join,才能够保证流式 SQL 的语义与结果的正确性。另外,在该版本中还支持了 CEP 算子的可伸缩容(即改变并发)。

在 Flink 1.5.0 时期,在 Table API 和 SQL 上支持了Join操作,包括无限流的 Join 和带窗口的 Join。还添加了 SQL CLI 支持。SQL CLI 提供了一个类似Shell命令的对话框,可以交互式执行查询。

Checkpoint机制在Flink很早期的时候就已经支持,是Flink一个很核心的功能,Flink 社区也一直努力提升 Checkpoint 和 Recovery 的效率。

在 Flink 1.0.0 时期,提供了 RocksDB 状态后端的支持,在这个版本之前所有的状态数据只能存在进程的内存里面,JVM 内存是固定大小的,随着数据越来越多总会发生 FullGC 和 OOM 的问题,所以在生产环境中很难应用起来。如果想要存更多数据、更大的State就要用到 RocksDB。RocksDB是一款基于文件的嵌入式数据库,它会把数据存到磁盘,同时又提供高效的读写性能。所以使用RocksDB不会发生OOM这种事情。

在 Flink 1.1.0 时期,支持了 RocksDB Snapshot 的异步化。在之前的版本,RocksDB 的 Snapshot 过程是同步的,它会阻塞主数据流的处理,很影响吞吐量。在支持异步化之后,吞吐量得到了极大的提升。

在 Flink 1.2.0 时期,通过引入KeyGroup的机制,支持了 KeyedState 和 OperatorState 的可扩缩容。也就是支持了对带状态的流计算任务改变并发的功能。

在 Flink 1.3.0 时期,支持了 Incremental Checkpoint (增量检查点)机制。Incemental Checkpoint 的支持标志着 Flink 流计算任务正式达到了生产就绪状态。增量检查点是每次只将本次 checkpoint 期间新增的状态快照并持久化存储起来。一般流计算任务,GB 级别的状态,甚至 TB 级别的状态是非常常见的,如果每次都把全量的状态都刷到分布式存储中,这个效率和网络代价是很大的。如果每次只刷新增的数据,效率就会高很多。在这个版本里面还引入了细粒度的recovery的功能,细粒度的recovery在做恢复的时候,只需要恢复失败节点的联通子图,不用对整个 Job 进行恢复,这样便能够提高恢复效率。

在 Flink 1.5.0 时期,引入了本地状态恢复的机制。因为基于checkpoint机制,会把State持久化地存储到某个分布式存储,比如HDFS,当发生 failover 的时候需要重新把数据从远程HDFS再下载下来,如果这个状态特别大那么下载耗时就会较长,failover 恢复所花的时间也会拉长。本地状态恢复机制会提前将状态文件在本地也备份一份,当Job发生failover之后,恢复时可以在本地直接恢复,不需从远程HDFS重新下载状态文件,从而提升了恢复的效率。

在 Flink 1.2.0 时期,提供了Async I/O功能。Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性,主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。例如,为了关联某些字段需要查询外部 HBase 表,同步的方式是每次查询的操作都是阻塞的,数据流会被频繁的I/O请求卡住。当使用异步I/O之后就可以同时地发起N个异步查询的请求,不会阻塞主数据流,这样便提升了整个job的吞吐量,提升CPU利用率。

在 Flink 1.3.0 时期,引入了HistoryServer的模块。HistoryServer主要功能是当job结束以后,会把job的状态以及信息都进行归档,方便后续开发人员做一些深入排查。

在 Flink 1.4.0 时期,提供了端到端的 exactly-once 的语义保证。Exactly-once 是指每条输入的数据只会作用在最终结果上有且只有一次,即使发生软件或硬件的故障,不会有丢数据或者重复计算发生。而在该版本之前,exactly-once 保证的范围只是 Flink 应用本身,并不包括输出给外部系统的部分。在 failover 时,这就有可能写了重复的数据到外部系统,所以一般会使用幂等的外部系统来解决这个问题。在 Flink 1.4 的版本中,Flink 基于两阶段提交协议,实现了端到端的 exactly-once 语义保证。内置支持了 Kafka 的端到端保证,并提供了 TwoPhaseCommitSinkFunction 供用于实现自定义外部存储的端到端 exactly-once 保证。

在 Flink 1.5.0 时期,Flink 发布了新的部署模型和处理模型(FLIP6)。新部署模型的开发工作已经持续了很久,该模型的实现对Flink核心代码改动特别大,可以说是自 Flink 项目创建以来,Runtime 改动最大的一次。简而言之,新的模型可以在YARN, MESOS调度系统上更好地动态分配资源、动态释放资源,并实现更高的资源利用率,还有提供更好的作业之间的隔离。

除了 FLIP6 的改进,在该版本中,还对网站栈做了重构。重构的原因是在老版本中,上下游多个 task 之间的通信会共享同一个 TCP connection,导致某一个 task 发生反压时,所有共享该连接的 task 都会被阻塞,反压的粒度是 TCP connection 级别的。为了改进反压机制,Flink应用了在解决网络拥塞时一种经典的流控方法——基于Credit的流量控制。使得流控的粒度精细到具体某个 task 级别,有效缓解了反压对吞吐量的影响。

总结

Flink 同时支持了流处理和批处理,目前流计算的模型已经相对比较成熟和领先,也经历了各个公司大规模生产的验证。社区在接下来将继续加强流计算方面的性能和功能,包括对 Flink SQL 扩展更丰富的功能和引入更多的优化。另一方面也将加大力量提升批处理、机器学习等生态上的能力。

关注微信公众号 爱解决,更多技术大牛帮你解决问题!

Java开发中的23种设计模式详解

【放弃了原文访问者模式的Demo,自己写了一个新使用场景的Demo,加上了自己的理解】

      【源码地址:https://github.com/leon66666/DesignPattern

      一、设计模式的分类

      总体来说设计模式分为三大类:

      创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

      结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

      行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

      其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:

      二、设计模式的六大原则

      1、开闭原则(Open Close Principle)

      开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

      2、里氏代换原则(Liskov Substitution Principle)

      里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科

      3、依赖倒转原则(Dependence Inversion Principle)

      这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。

      4、接口隔离原则(Interface Segregation Principle)

      这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

      5、迪米特法则(最少知道原则)(Demeter Principle)

      为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

      6、合成复用原则(Composite Reuse Principle)

      原则是尽量使用合成/聚合的方式,而不是使用继承。

      三、Java的23中设计模式

      从这一块开始,我们详细介绍Java中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。

      1、工厂方法模式(Factory Method)

      工厂方法模式分为三种:

      11、普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:

      举例如下:(我们举一个发送邮件和短信的例子)

      首先,创建二者的共同接口:

      [java] view plaincopy

  1.       public interface Sender {  
  2.       public void Send();  
  3.       }  

      其次,创建实现类:

      [java] view plaincopy

  1.       public class MailSender implements Sender {  
  2.       @Override  
  3.       public void Send() {  
  4.       System.out.println(“this is mailsender!”);  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class SmsSender implements Sender {  
  2.       @Override  
  3.       public void Send() {  
  4.       System.out.println(“this is sms sender!”);  
  5.       }  
  6.       }  

      最后,建工厂类:

      [java] view plaincopy

  1.       public class SendFactory {  
  2.       public Sender produce(String type) {  
  3.       if (“mail”.equals(type)) {  
  4.       return new MailSender();  
  5.       } else if (“sms”.equals(type)) {  
  6.       return new SmsSender();  
  7.       } else {  
  8.       System.out.println(“请输入正确的类型!”);  
  9.       return null;  
  10.       }  
  11.       }  
  12.       }  

      我们来测试下:

  1.       public class FactoryTest {  
  2.       public static void main(String[] args) {  
  3.       SendFactory factory = new SendFactory();  
  4.       Sender sender = factory.produce(“sms”);  
  5.       sender.Send();  
  6.       }  
  7.       }  

      输出:this is sms sender!

      22、多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:

      将上面的代码做下修改,改动下SendFactory类就行,如下:

      [java] view plaincopypublic class SendFactory {  

      public Sender produceMail(){  

  1.       return new MailSender();  
  2.       }  
  3.       public Sender produceSms(){  
  4.       return new SmsSender();  
  5.       }  
  6.       }  

      测试类如下:

      [java] view plaincopy

  1.       public class FactoryTest {  
  2.       public static void main(String[] args) {  
  3.       SendFactory factory = new SendFactory();  
  4.       Sender sender = factory.produceMail();  
  5.       sender.Send();  
  6.       }  
  7.       }  

      输出:this is mailsender!

      33、静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

      [java] view plaincopy

  1.       public class SendFactory {  
  2.       public static Sender produceMail(){  
  3.       return new MailSender();  
  4.       }  
  5.       public static Sender produceSms(){  
  6.       return new SmsSender();  
  7.       }  
  8.       }  

      [java] view plaincopy

  1.       public class FactoryTest {  
  2.       public static void main(String[] args) {      
  3.       Sender sender = SendFactory.produceMail();  
  4.       sender.Send();  
  5.       }  
  6.       }  

      输出:this is mailsender!

      总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。

      2、抽象工厂模式(Abstract Factory)

      工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。

      请看例子:

      [java] view plaincopy

  1.       public interface Sender {  
  2.       public void Send();  
  3.       }  

      两个实现类:

      [java] view plaincopy

  1.       public class MailSender implements Sender {  
  2.       @Override  
  3.       public void Send() {  
  4.       System.out.println(“this is mailsender!”);  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class SmsSender implements Sender {  
  2.       @Override  
  3.       public void Send() {  
  4.       System.out.println(“this is sms sender!”);  
  5.       }  
  6.       }  

      两个工厂类:

      [java] view plaincopy

  1.       public class SendMailFactory implements Provider {  
  2.       @Override  
  3.       public Sender produce(){  
  4.       return new MailSender();  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class SendSmsFactory implements Provider{  
  2.       @Override  
  3.       public Sender produce() {  
  4.       return new SmsSender();  
  5.       }  
  6.       }  

      在提供一个接口:

      [java] view plaincopy

  1.       public interface Provider {  
  2.       public Sender produce();  
  3.       }  

      测试类:

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       Provider provider = new SendMailFactory();  
  4.       Sender sender = provider.produce();  
  5.       sender.Send();  
  6.       }  
  7.       }  

      其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!

      3、单例模式(Singleton)

      单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

      1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

      2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

      3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

      首先我们写一个简单的单例类:

      [java] view plaincopy

  1.       public class Singleton {  
  2.       /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
  3.       private static Singleton instance = null;  
  4.       /* 私有构造方法,防止被实例化 */  
  5.       private Singleton() {  
  6.       }  
  7.       /* 静态工程方法,创建实例 */  
  8.       public static Singleton getInstance() {  
  9.       if (instance == null) {  
  10.       instance = new Singleton();  
  11.       }  
  12.       return instance;  
  13.       }  
  14.       /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
  15.       public Object readResolve() {  
  16.       return instance;  
  17.       }  
  18.       }  

      这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字,如下:

      [java] view plaincopy

  1.       public static synchronized Singleton getInstance() {  
  2.       if (instance == null) {  
  3.       instance = new Singleton();  
  4.       }  
  5.       return instance;  
  6.       }  

      但是,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:

      [java] view plaincopy

  1.       public static Singleton getInstance() {  
  2.       if (instance == null) {  
  3.       synchronized (instance) {  
  4.       if (instance == null) {  
  5.       instance = new Singleton();  
  6.       }  
  7.       }  
  8.       }  
  9.       return instance;  
  10.       }  

      似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:

      a>A、B线程同时进入了第一个if判断

      b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

      c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

      d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

      e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

      所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:

      [java] view plaincopy

  1.       private static class SingletonFactory{           
  2.       private static Singleton instance = new Singleton();           
  3.       }           
  4.       public static Singleton getInstance(){           
  5.       return SingletonFactory.instance;           
  6.       }   

      实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

      [java] view plaincopy

  1.       public class Singleton {  
  2.       /* 私有构造方法,防止被实例化 */  
  3.       private Singleton() {  
  4.       }  
  5.       /* 此处使用一个内部类来维护单例 */  
  6.       private static class SingletonFactory {  
  7.       private static Singleton instance = new Singleton();  
  8.       }  
  9.       /* 获取实例 */  
  10.       public static Singleton getInstance() {  
  11.       return SingletonFactory.instance;  
  12.       }  
  13.       /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
  14.       public Object readResolve() {  
  15.       return getInstance();  
  16.       }  
  17.       }  

      其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:

      [java] view plaincopy

  1.       public class SingletonTest {  
  2.       private static SingletonTest instance = null;  
  3.       private SingletonTest() {  
  4.       }  
  5.       private static synchronized void syncInit() {  
  6.       if (instance == null) {  
  7.       instance = new SingletonTest();  
  8.       }  
  9.       }  
  10.       public static SingletonTest getInstance() {  
  11.       if (instance == null) {  
  12.       syncInit();  
  13.       }  
  14.       return instance;  
  15.       }  
  16.       }  

      考虑性能的话,整个程序只需创建一次实例,所以性能也不会有什么影响。

      补充:采用”影子实例”的办法为单例对象的属性同步更新

      [java] view plaincopy

  1.       public class SingletonTest {  
  2.       private static SingletonTest instance = null;  
  3.       private Vector properties = null;  
  4.       public Vector getProperties() {  
  5.       return properties;  
  6.       }  
  7.       private SingletonTest() {  
  8.       }  
  9.       private static synchronized void syncInit() {  
  10.       if (instance == null) {  
  11.       instance = new SingletonTest();  
  12.       }  
  13.       }  
  14.       public static SingletonTest getInstance() {  
  15.       if (instance == null) {  
  16.       syncInit();  
  17.       }  
  18.       return instance;  
  19.       }  
  20.       public void updateProperties() {  
  21.       SingletonTest shadow = new SingletonTest();  
  22.       properties = shadow.getProperties();  
  23.       }  
  24.       }  

      通过单例模式的学习告诉我们:

      1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。

      2、synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

      到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?

      首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

      其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。

      再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。

      最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

      4、建造者模式(Builder)

      工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的。我们看一下代码:

      还和前面一样,一个Sender接口,两个实现类MailSender和SmsSender。最后,建造者类如下:

      [java] view plaincopy

  1.       public class Builder {  
  2.       private List<Sender> list = new ArrayList<Sender>();  
  3.       public void produceMailSender(int count){  
  4.       for(int i=0; i<count; i++){  
  5.       list.add(new MailSender());  
  6.       }  
  7.       }  
  8.       public void produceSmsSender(int count){  
  9.       for(int i=0; i<count; i++){  
  10.       list.add(new SmsSender());  
  11.       }  
  12.       }  
  13.       }  

      测试类:

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       Builder builder = new Builder();  
  4.       builder.produceMailSender(10);  
  5.       }  
  6.       }  

      从这点看出,建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工程模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。

      5、原型模式(Prototype)

      原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在Java中,复制对象是通过clone()实现的,先创建一个原型类:

      [java] view plaincopy

  1.       public class Prototype implements Cloneable {  
  2.       public Object clone() throws CloneNotSupportedException {  
  3.       Prototype proto = (Prototype) super.clone();  
  4.       return proto;  
  5.       }  
  6.       }  

      很简单,一个原型类,只需要实现Cloneable接口,覆写clone方法,此处clone方法可以改成任意的名称,因为Cloneable接口是个空接口,你可以任意定义实现类的方法名,如cloneA或者cloneB,因为此处的重点是super.clone()这句话,super.clone()调用的是Object的clone()方法,而在Object类中,clone()是native的,具体怎么实现,我会在另一篇文章中,关于解读Java中本地方法的调用,此处不再深究。在这儿,我将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念:

      浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

      深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。

      此处,写一个深浅复制的例子:

      [java] view plaincopy

  1.       public class Prototype implements Cloneable, Serializable {  
  2.       private static final long serialVersionUID = 1L;  
  3.       private String string;  
  4.       private SerializableObject obj;  
  5.       /* 浅复制 */  
  6.       public Object clone() throws CloneNotSupportedException {  
  7.       Prototype proto = (Prototype) super.clone();  
  8.       return proto;  
  9.       }  
  10.       /* 深复制 */  
  11.       public Object deepClone() throws IOException, ClassNotFoundException {  
  12.       /* 写入当前对象的二进制流 */  
  13.       ByteArrayOutputStream bos = new ByteArrayOutputStream();  
  14.       ObjectOutputStream oos = new ObjectOutputStream(bos);  
  15.       oos.writeObject(this);  
  16.       /* 读出二进制流产生的新对象 */  
  17.       ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());  
  18.       ObjectInputStream ois = new ObjectInputStream(bis);  
  19.       return ois.readObject();  
  20.       }  
  21.       public String getString() {  
  22.       return string;  
  23.       }  
  24.       public void setString(String string) {  
  25.       this.string = string;  
  26.       }  
  27.       public SerializableObject getObj() {  
  28.       return obj;  
  29.       }  
  30.       public void setObj(SerializableObject obj) {  
  31.       this.obj = obj;  
  32.       }  
  33.       }  
  34.       class SerializableObject implements Serializable {  
  35.       private static final long serialVersionUID = 1L;  
  36.       }  

      要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。

      我们接着讨论设计模式,上篇文章我讲完了5种创建型模式,这章开始,我将讲下7种结构型模式:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。其中对象的适配器模式是各种模式的起源,我们看下面的图:

      适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。首先,我们来看看类的适配器模式,先看类图:

      核心思想就是:有一个Source类,拥有一个方法,待适配,目标接口时Targetable,通过Adapter类,将Source的功能扩展到Targetable里,看代码:

      [java] view plaincopy

  1.       public class Source {  
  2.       public void method1() {  
  3.       System.out.println(“this is original method!”);  
  4.       }  
  5.       }  

      [java] view plaincopy

  1.       public interface Targetable {  
  2.       /* 与原类中的方法相同 */  
  3.       public void method1();  
  4.       /* 新类的方法 */  
  5.       public void method2();  
  6.       }  

      [java] view plaincopy

  1.       public class Adapter extends Source implements Targetable {  
  2.       @Override  
  3.       public void method2() {  
  4.       System.out.println(“this is the targetable method!”);  
  5.       }  
  6.       }  

      Adapter类继承Source类,实现Targetable接口,下面是测试类:

      [java] view plaincopy

  1.       public class AdapterTest {  
  2.       public static void main(String[] args) {  
  3.       Targetable target = new Adapter();  
  4.       target.method1();  
  5.       target.method2();  
  6.       }  
  7.       }  

      输出:

      this is original method!
      this is the targetable method!

      这样Targetable接口的实现类就具有了Source类的功能。

      对象的适配器模式

      基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。看图:

      只需要修改Adapter类的源码即可:

      [java] view plaincopy

  1.       public class Wrapper implements Targetable {  
  2.       private Source source;  
  3.       public Wrapper(Source source){  
  4.       super();  
  5.       this.source = source;  
  6.       }  
  7.       @Override  
  8.       public void method2() {  
  9.       System.out.println(“this is the targetable method!”);  
  10.       }  
  11.       @Override  
  12.       public void method1() {  
  13.       source.method1();  
  14.       }  
  15.       }  

      测试类:

      [java] view plaincopy

  1.       public class AdapterTest {  
  2.       public static void main(String[] args) {  
  3.       Source source = new Source();  
  4.       Targetable target = new Wrapper(source);  
  5.       target.method1();  
  6.       target.method2();  
  7.       }  
  8.       }  

      输出与第一种一样,只是适配的方法不同而已。

      第三种适配器模式是接口的适配器模式,接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。看一下类图:

      这个很好理解,在实际开发中,我们也常会遇到这种接口中定义了太多的方法,以致于有时我们在一些实现类中并不是都需要。看代码:

      [java] view plaincopy

  1.       public interface Sourceable {  
  2.       public void method1();  
  3.       public void method2();  
  4.       }  

      抽象类Wrapper2:

      [java] view plaincopy

  1.       public abstract class Wrapper2 implements Sourceable{  
  2.       public void method1(){}  
  3.       public void method2(){}  
  4.       }  

      [java] view plaincopy

  1.       public class SourceSub1 extends Wrapper2 {  
  2.       public void method1(){  
  3.       System.out.println(“the sourceable interface’s first Sub1!”);  
  4.       }  
  5.       }  

      [java] view plaincopy

  1.       public class SourceSub2 extends Wrapper2 {  
  2.       public void method2(){  
  3.       System.out.println(“the sourceable interface’s second Sub2!”);  
  4.       }  
  5.       }  

      [java] view plaincopy

  1.       public class WrapperTest {  
  2.       public static void main(String[] args) {  
  3.       Sourceable source1 = new SourceSub1();  
  4.       Sourceable source2 = new SourceSub2();  
  5.       source1.method1();  
  6.       source1.method2();  
  7.       source2.method1();  
  8.       source2.method2();  
  9.       }  
  10.       }  

      测试输出:

      the sourceable interface’s first Sub1!
      the sourceable interface’s second Sub2!

      达到了我们的效果!

      讲了这么多,总结一下三种适配器模式的应用场景:

      类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。

      对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。

      接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

      7、装饰模式(Decorator)

      顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例,关系图如下:

      Source类是被装饰类,Decorator类是一个装饰类,可以为Source类动态的添加一些功能,代码如下:

      [java] view plaincopy

  1.       public interface Sourceable {  
  2.       public void method();  
  3.       }  

      [java] view plaincopy

  1.       public class Source implements Sourceable {  
  2.       @Override  
  3.       public void method() {  
  4.       System.out.println(“the original method!”);  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class Decorator implements Sourceable {  
  2.       private Sourceable source;  
  3.       public Decorator(Sourceable source){  
  4.       super();  
  5.       this.source = source;  
  6.       }  
  7.       @Override  
  8.       public void method() {  
  9.       System.out.println(“before decorator!”);  
  10.       source.method();  
  11.       System.out.println(“after decorator!”);  
  12.       }  
  13.       }  

      测试类:

      [java] view plaincopy

  1.       public class DecoratorTest {  
  2.       public static void main(String[] args) {  
  3.       Sourceable source = new Source();  
  4.       Sourceable obj = new Decorator(source);  
  5.       obj.method();  
  6.       }  
  7.       }  

      输出:

      before decorator!
      the original method!
      after decorator!

      装饰器模式的应用场景:

      1、需要扩展一个类的功能。

      2、动态的为一个对象增加功能,而且还能动态撤销。(继承不能做到这一点,继承的功能是静态的,不能动态增删。)

      缺点:产生过多相似的对象,不易排错!

      8、代理模式(Proxy)

      其实每个模式名称就表明了该模式的作用,代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候回去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。先来看看关系图:

      根据上文的阐述,代理模式就比较容易的理解了,我们看下代码:

      [java] view plaincopy

  1.       public interface Sourceable {  
  2.       public void method();  
  3.       }  

      [java] view plaincopy

  1.       public class Source implements Sourceable {  
  2.       @Override  
  3.       public void method() {  
  4.       System.out.println(“the original method!”);  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class Proxy implements Sourceable {  
  2.       private Source source;  
  3.       public Proxy(){  
  4.       super();  
  5.       this.source = new Source();  
  6.       }  
  7.       @Override  
  8.       public void method() {  
  9.       before();  
  10.       source.method();  
  11.       atfer();  
  12.       }  
  13.       private void atfer() {  
  14.       System.out.println(“after proxy!”);  
  15.       }  
  16.       private void before() {  
  17.       System.out.println(“before proxy!”);  
  18.       }  
  19.       }  

      测试类:

      [java] view plaincopy

  1.       public class ProxyTest {  
  2.       public static void main(String[] args) {  
  3.       Sourceable source = new Proxy();  
  4.       source.method();  
  5.       }  
  6.       }  

      输出:

      before proxy!
      the original method!
      after proxy!

      代理模式的应用场景:

      如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:

      1、修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。

      2、就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。

      使用代理模式,可以将功能划分的更加清晰,有助于后期维护!

      9、外观模式(Facade)

      外观模式是为了解决类与类之家的依赖关系的,像spring一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个Facade类中,降低了类类之间的耦合度,该模式中没有涉及到接口,看下类图:(我们以一个计算机的启动过程为例)

      我们先看下实现类:

      [java] view plaincopy

  1.       public class CPU {  
  2.       public void startup(){  
  3.       System.out.println(“cpu startup!”);  
  4.       }  
  5.       public void shutdown(){  
  6.       System.out.println(“cpu shutdown!”);  
  7.       }  
  8.       }  

      [java] view plaincopy

  1.       public class Memory {  
  2.       public void startup(){  
  3.       System.out.println(“memory startup!”);  
  4.       }  
  5.       public void shutdown(){  
  6.       System.out.println(“memory shutdown!”);  
  7.       }  
  8.       }  

      [java] view plaincopy

  1.       public class Disk {  
  2.       public void startup(){  
  3.       System.out.println(“disk startup!”);  
  4.       }  
  5.       public void shutdown(){  
  6.       System.out.println(“disk shutdown!”);  
  7.       }  
  8.       }  

      [java] view plaincopy

  1.       public class Computer {  
  2.       private CPU cpu;  
  3.       private Memory memory;  
  4.       private Disk disk;  
  5.       public Computer(){  
  6.       cpu = new CPU();  
  7.       memory = new Memory();  
  8.       disk = new Disk();  
  9.       }  
  10.       public void startup(){  
  11.       System.out.println(“start the computer!”);  
  12.       cpu.startup();  
  13.       memory.startup();  
  14.       disk.startup();  
  15.       System.out.println(“start computer finished!”);  
  16.       }  
  17.       public void shutdown(){  
  18.       System.out.println(“begin to close the computer!”);  
  19.       cpu.shutdown();  
  20.       memory.shutdown();  
  21.       disk.shutdown();  
  22.       System.out.println(“computer closed!”);  
  23.       }  
  24.       }  

      User类如下:

      [java] view plaincopy

  1.       public class User {  
  2.       public static void main(String[] args) {  
  3.       Computer computer = new Computer();  
  4.       computer.startup();  
  5.       computer.shutdown();  
  6.       }  
  7.       }  

      输出:

      start the computer!
      cpu startup!
      memory startup!
      disk startup!
      start computer finished!
      begin to close the computer!
      cpu shutdown!
      memory shutdown!
      disk shutdown!
      computer closed!

      如果我们没有Computer类,那么,CPU、Memory、Disk他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来其他类的修改,这不是我们想要看到的,有了Computer类,他们之间的关系被放在了Computer类里,这样就起到了解耦的作用,这,就是外观模式!

      10、桥接模式(Bridge)

      桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化。桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。我们来看看关系图:

      实现代码:

      先定义接口:

      [java] view plaincopy

  1.       public interface Sourceable {  
  2.       public void method();  
  3.       }  

      分别定义两个实现类:

      [java] view plaincopy

  1.       public class SourceSub1 implements Sourceable {  
  2.       @Override  
  3.       public void method() {  
  4.       System.out.println(“this is the first sub!”);  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class SourceSub2 implements Sourceable {  
  2.       @Override  
  3.       public void method() {  
  4.       System.out.println(“this is the second sub!”);  
  5.       }  
  6.       }  

      定义一个桥,持有Sourceable的一个实例:

      [java] view plaincopy

  1.       public abstract class Bridge {  
  2.       private Sourceable source;  
  3.       public void method(){  
  4.       source.method();  
  5.       }  
  6.       public Sourceable getSource() {  
  7.       return source;  
  8.       }  
  9.       public void setSource(Sourceable source) {  
  10.       this.source = source;  
  11.       }  
  12.       }  

      [java] view plaincopy

  1.       public class MyBridge extends Bridge {  
  2.       public void method(){  
  3.       getSource().method();  
  4.       }  
  5.       }  

      测试类:

      [java] view plaincopy

  1.       public class BridgeTest {  
  2.       public static void main(String[] args) {  
  3.       Bridge bridge = new MyBridge();  
  4.       /*调用第一个对象*/  
  5.       Sourceable source1 = new SourceSub1();  
  6.       bridge.setSource(source1);  
  7.       bridge.method();  
  8.       /*调用第二个对象*/  
  9.       Sourceable source2 = new SourceSub2();  
  10.       bridge.setSource(source2);  
  11.       bridge.method();  
  12.       }  
  13.       }  

      output:

      this is the first sub!
      this is the second sub!

      这样,就通过对Bridge类的调用,实现了对接口Sourceable的实现类SourceSub1和SourceSub2的调用。接下来我再画个图,大家就应该明白了,因为这个图是我们JDBC连接的原理,有数据库学习基础的,一结合就都懂了。

      11、组合模式(Composite)

      组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便,看看关系图:

      直接来看代码:

      [java] view plaincopy

  1.       public class TreeNode {  
  2.       private String name;  
  3.       private TreeNode parent;  
  4.       private Vector<TreeNode> children = new Vector<TreeNode>();  
  5.       public TreeNode(String name){  
  6.       this.name = name;  
  7.       }  
  8.       public String getName() {  
  9.       return name;  
  10.       }  
  11.       public void setName(String name) {  
  12.       this.name = name;  
  13.       }  
  14.       public TreeNode getParent() {  
  15.       return parent;  
  16.       }  
  17.       public void setParent(TreeNode parent) {  
  18.       this.parent = parent;  
  19.       }  
  20.       //添加孩子节点  
  21.       public void add(TreeNode node){  
  22.       children.add(node);  
  23.       }  
  24.       //删除孩子节点  
  25.       public void remove(TreeNode node){  
  26.       children.remove(node);  
  27.       }  
  28.       //取得孩子节点  
  29.       public Enumeration<TreeNode> getChildren(){  
  30.       return children.elements();  
  31.       }  
  32.       }  

      [java] view plaincopy

  1.       public class Tree {  
  2.       TreeNode root = null;  
  3.       public Tree(String name) {  
  4.       root = new TreeNode(name);  
  5.       }  
  6.       public static void main(String[] args) {  
  7.       Tree tree = new Tree(“A”);  
  8.       TreeNode nodeB = new TreeNode(“B”);  
  9.       TreeNode nodeC = new TreeNode(“C”);  
  10.       nodeB.add(nodeC);  
  11.       tree.root.add(nodeB);  
  12.       System.out.println(“build the tree finished!”);  
  13.       }  
  14.       }  

      使用场景:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树,数等。

      12、享元模式(Flyweight)

      享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。

      FlyWeightFactory负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,FlyWeight是超类。一提到共享池,我们很容易联想到Java里面的JDBC连接池,想想每个连接的特点,我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,url、driverClassName、username、password及dbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据,在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。

      看个例子:

      看下数据库连接池的代码:

      [java] view plaincopy

  1.       public class ConnectionPool {  
  2.       private Vector<Connection> pool;  
  3.       /*公有属性*/  
  4.       private String url = “jdbc:mysql://localhost:3306/test”;  
  5.       private String username = “root”;  
  6.       private String password = “root”;  
  7.       private String driverClassName = “com.mysql.jdbc.Driver”;  
  8.       private int poolSize = 100;  
  9.       private static ConnectionPool instance = null;  
  10.       Connection conn = null;  
  11.       /*构造方法,做一些初始化工作*/  
  12.       private ConnectionPool() {  
  13.       pool = new Vector<Connection>(poolSize);  
  14.       for (int i = 0; i < poolSize; i++) {  
  15.       try {  
  16.       Class.forName(driverClassName);  
  17.       conn = DriverManager.getConnection(url, username, password);  
  18.       pool.add(conn);  
  19.       } catch (ClassNotFoundException e) {  
  20.       e.printStackTrace();  
  21.       } catch (SQLException e) {  
  22.       e.printStackTrace();  
  23.       }  
  24.       }  
  25.       }  
  26.       /* 返回连接到连接池 */  
  27.       public synchronized void release() {  
  28.       pool.add(conn);  
  29.       }  
  30.       /* 返回连接池中的一个数据库连接 */  
  31.       public synchronized Connection getConnection() {  
  32.       if (pool.size() > 0) {  
  33.       Connection conn = pool.get(0);  
  34.       pool.remove(conn);  
  35.       return conn;  
  36.       } else {  
  37.       return null;  
  38.       }  
  39.       }  
  40.       }  

      通过连接池的管理,实现了数据库连接的共享,不需要每一次都重新创建连接,节省了数据库重新创建的开销,提升了系统的性能!本章讲解了7种结构型模式,因为篇幅的问题,剩下的11种行为型模式,

      本章是关于设计模式的最后一讲,会讲到第三种设计模式——行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。这段时间一直在写关于设计模式的东西,终于写到一半了,写博文是个很费时间的东西,因为我得为读者负责,不论是图还是代码还是表述,都希望能尽量写清楚,以便读者理解,我想不论是我还是读者,都希望看到高质量的博文出来,从我本人出发,我会一直坚持下去,不断更新,源源动力来自于读者朋友们的不断支持,我会尽自己的努力,写好每一篇文章!希望大家能不断给出意见和建议,共同打造完美的博文!

      先来张图,看看这11中模式的关系:

      第一类:通过父类与子类的关系进行实现。第二类:两个类之间。第三类:类的状态。第四类:通过中间类

      13、策略模式(strategy)

      策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下:

      图中ICalculator提供同意的方法,
      AbstractCalculator是辅助类,提供辅助方法,接下来,依次实现下每个类:

      首先统一接口:

      [java] view plaincopy

  1.       public interface ICalculator {  
  2.       public int calculate(String exp);  
  3.       }  

      辅助类:

      [java] view plaincopy

  1.       public abstract class AbstractCalculator {  
  2.       public int[] split(String exp,String opt){  
  3.       String array[] = exp.split(opt);  
  4.       int arrayInt[] = new int[2];  
  5.       arrayInt[0] = Integer.parseInt(array[0]);  
  6.       arrayInt[1] = Integer.parseInt(array[1]);  
  7.       return arrayInt;  
  8.       }  
  9.       }  

      三个实现类:

      [java] view plaincopy

  1.       public class Plus extends AbstractCalculator implements ICalculator {  
  2.       @Override  
  3.       public int calculate(String exp) {  
  4.       int arrayInt[] = split(exp,”\\+”);  
  5.       return arrayInt[0]+arrayInt[1];  
  6.       }  
  7.       }  

      [java] view plaincopy

  1.       public class Minus extends AbstractCalculator implements ICalculator {  
  2.       @Override  
  3.       public int calculate(String exp) {  
  4.       int arrayInt[] = split(exp,”-“);  
  5.       return arrayInt[0]-arrayInt[1];  
  6.       }  
  7.       }  

      [java] view plaincopy

  1.       public class Multiply extends AbstractCalculator implements ICalculator {  
  2.       @Override  
  3.       public int calculate(String exp) {  
  4.       int arrayInt[] = split(exp,”\\*”);  
  5.       return arrayInt[0]*arrayInt[1];  
  6.       }  
  7.       }  

      简单的测试类:

      [java] view plaincopy

  1.       public class StrategyTest {  
  2.       public static void main(String[] args) {  
  3.       String exp = “2+8”;  
  4.       ICalculator cal = new Plus();  
  5.       int result = cal.calculate(exp);  
  6.       System.out.println(result);  
  7.       }  
  8.       }  

      输出:10

      策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。

      14、模板方法模式(Template Method)

      解释一下模板方法模式,就是指:一个抽象类中,有一个主方法,再定义1…n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用,先看个关系图:

      就是在AbstractCalculator类中定义一个主方法calculate,calculate()调用spilt()等,Plus和Minus分别继承AbstractCalculator类,通过对AbstractCalculator的调用实现对子类的调用,看下面的例子:

      [java] view plaincopy

  1.       public abstract class AbstractCalculator {  
  2.       /*主方法,实现对本类其它方法的调用*/  
  3.       public final int calculate(String exp,String opt){  
  4.       int array[] = split(exp,opt);  
  5.       return calculate(array[0],array[1]);  
  6.       }  
  7.       /*被子类重写的方法*/  
  8.       abstract public int calculate(int num1,int num2);  
  9.       public int[] split(String exp,String opt){  
  10.       String array[] = exp.split(opt);  
  11.       int arrayInt[] = new int[2];  
  12.       arrayInt[0] = Integer.parseInt(array[0]);  
  13.       arrayInt[1] = Integer.parseInt(array[1]);  
  14.       return arrayInt;  
  15.       }  
  16.       }  

      [java] view plaincopy

  1.       public class Plus extends AbstractCalculator {  
  2.       @Override  
  3.       public int calculate(int num1,int num2) {  
  4.       return num1 + num2;  
  5.       }  
  6.       }  

      测试类:

      [java] view plaincopy

  1.       public class StrategyTest {  
  2.       public static void main(String[] args) {  
  3.       String exp = “8+8”;  
  4.       AbstractCalculator cal = new Plus();  
  5.       int result = cal.calculate(exp, “\\+”);  
  6.       System.out.println(result);  
  7.       }  
  8.       }  

      我跟踪下这个小程序的执行过程:首先将exp和”\\+”做参数,调用AbstractCalculator类里的calculate(String,String)方法,在calculate(String,String)里调用同类的split(),之后再调用calculate(int ,int)方法,从这个方法进入到子类中,执行完return num1 + num2后,将值返回到AbstractCalculator类,赋给result,打印出来。正好验证了我们开头的思路。

      15、观察者模式(Observer)

      包括这个模式在内的接下来的四个模式,都是类和类之间的关系,不涉及到继承,学的时候应该 记得归纳,记得本文最开始的那个图。观察者模式很好理解,类似于邮件订阅和RSS订阅,当我们浏览一些博客或wiki时,经常会看到RSS图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。先来看看关系图:

      我解释下这些类的作用:MySubject类就是我们的主对象,Observer1和Observer2是依赖于MySubject的对象,当MySubject变化时,Observer1和Observer2必然变化。AbstractSubject类中定义着需要监控的对象列表,可以对其进行修改:增加或删除被监控对象,且当MySubject变化时,负责通知在列表内存在的对象。我们看实现代码:

      一个Observer接口:

      [java] view plaincopy

  1.       public interface Observer {  
  2.       public void update();  
  3.       }  

      两个实现类:

      [java] view plaincopy

  1.       public class Observer1 implements Observer {  
  2.       @Override  
  3.       public void update() {  
  4.       System.out.println(“observer1 has received!”);  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class Observer2 implements Observer {  
  2.       @Override  
  3.       public void update() {  
  4.       System.out.println(“observer2 has received!”);  
  5.       }  
  6.       }  

      Subject接口及实现类:

      [java] view plaincopy

  1.       public interface Subject {  
  2.       /*增加观察者*/  
  3.       public void add(Observer observer);  
  4.       /*删除观察者*/  
  5.       public void del(Observer observer);  
  6.       /*通知所有的观察者*/  
  7.       public void notifyObservers();  
  8.       /*自身的操作*/  
  9.       public void operation();  
  10.       }  

      [java] view plaincopy

  1.       public abstract class AbstractSubject implements Subject {  
  2.       private Vector<Observer> vector = new Vector<Observer>();  
  3.       @Override  
  4.       public void add(Observer observer) {  
  5.       vector.add(observer);  
  6.       }  
  7.       @Override  
  8.       public void del(Observer observer) {  
  9.       vector.remove(observer);  
  10.       }  
  11.       @Override  
  12.       public void notifyObservers() {  
  13.       Enumeration<Observer> enumo = vector.elements();  
  14.       while(enumo.hasMoreElements()){  
  15.       enumo.nextElement().update();  
  16.       }  
  17.       }  
  18.       }  

      [java] view plaincopy

  1.       public class MySubject extends AbstractSubject {  
  2.       @Override  
  3.       public void operation() {  
  4.       System.out.println(“update self!”);  
  5.       notifyObservers();  
  6.       }  
  7.       }  

      测试类:

      [java] view plaincopy

  1.       public class ObserverTest {  
  2.       public static void main(String[] args) {  
  3.       Subject sub = new MySubject();  
  4.       sub.add(new Observer1());  
  5.       sub.add(new Observer2());  
  6.       sub.operation();  
  7.       }  
  8.       }  

      输出:

      update self!
      observer1 has received!
      observer2 has received!

      这些东西,其实不难,只是有些抽象,不太容易整体理解,建议读者:根据关系图,新建项目,自己写代码(或者参考我的代码),按照总体思路走一遍,这样才能体会它的思想,理解起来容易! 

      16、迭代子模式(Iterator)

      顾名思义,迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。我们看下关系图:

      这个思路和我们常用的一模一样,MyCollection中定义了集合的一些操作,MyIterator中定义了一系列迭代操作,且持有Collection实例,我们来看看实现代码:

      两个接口:

      [java] view plaincopy

  1.       public interface Collection {  
  2.       public Iterator iterator();  
  3.       /*取得集合元素*/  
  4.       public Object get(int i);  
  5.       /*取得集合大小*/  
  6.       public int size();  
  7.       }  

      [java] view plaincopy

  1.       public interface Iterator {  
  2.       //前移  
  3.       public Object previous();  
  4.       //后移  
  5.       public Object next();  
  6.       public boolean hasNext();  
  7.       //取得第一个元素  
  8.       public Object first();  
  9.       }  

      两个实现:

      [java] view plaincopy

  1.       public class MyCollection implements Collection {  
  2.       public String string[] = {“A”,”B”,”C”,”D”,”E”};  
  3.       @Override  
  4.       public Iterator iterator() {  
  5.       return new MyIterator(this);  
  6.       }  
  7.       @Override  
  8.       public Object get(int i) {  
  9.       return string[i];  
  10.       }  
  11.       @Override  
  12.       public int size() {  
  13.       return string.length;  
  14.       }  
  15.       }  

      [java] view plaincopy

  1.       public class MyIterator implements Iterator {  
  2.       private Collection collection;  
  3.       private int pos = -1;  
  4.       public MyIterator(Collection collection){  
  5.       this.collection = collection;  
  6.       }  
  7.       @Override  
  8.       public Object previous() {  
  9.       if(pos > 0){  
  10.       pos–;  
  11.       }  
  12.       return collection.get(pos);  
  13.       }  
  14.       @Override  
  15.       public Object next() {  
  16.       if(pos<collection.size()-1){  
  17.       pos++;  
  18.       }  
  19.       return collection.get(pos);  
  20.       }  
  21.       @Override  
  22.       public boolean hasNext() {  
  23.       if(pos<collection.size()-1){  
  24.       return true;  
  25.       }else{  
  26.       return false;  
  27.       }  
  28.       }  
  29.       @Override  
  30.       public Object first() {  
  31.       pos = 0;  
  32.       return collection.get(pos);  
  33.       }  
  34.       }  

      测试类:

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       Collection collection = new MyCollection();  
  4.       Iterator it = collection.iterator();  
  5.       while(it.hasNext()){  
  6.       System.out.println(it.next());  
  7.       }  
  8.       }  
  9.       }  

      输出:A B C D E

      此处我们貌似模拟了一个集合类的过程,感觉是不是很爽?其实JDK中各个类也都是这些基本的东西,加一些设计模式,再加一些优化放到一起的,只要我们把这些东西学会了,掌握好了,我们也可以写出自己的集合类,甚至框架!

      17、责任链模式(Chain of Responsibility)
      接下来我们将要谈谈责任链模式,有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。先看看关系图:

      Abstracthandler类提供了get和set方法,方便MyHandle类设置和修改引用对象,MyHandle类是核心,实例化后生成一系列相互持有的对象,构成一条链。

      [java] view plaincopy

  1.       public interface Handler {  
  2.       public void operator();  
  3.       }  

      [java] view plaincopy

  1.       public abstract class AbstractHandler {  
  2.       private Handler handler;  
  3.       public Handler getHandler() {  
  4.       return handler;  
  5.       }  
  6.       public void setHandler(Handler handler) {  
  7.       this.handler = handler;  
  8.       }  
  9.       }  

      [java] view plaincopy

  1.       public class MyHandler extends AbstractHandler implements Handler {  
  2.       private String name;  
  3.       public MyHandler(String name) {  
  4.       this.name = name;  
  5.       }  
  6.       @Override  
  7.       public void operator() {  
  8.       System.out.println(name+”deal!”);  
  9.       if(getHandler()!=null){  
  10.       getHandler().operator();  
  11.       }  
  12.       }  
  13.       }  

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       MyHandler h1 = new MyHandler(“h1”);  
  4.       MyHandler h2 = new MyHandler(“h2”);  
  5.       MyHandler h3 = new MyHandler(“h3”);  
  6.       h1.setHandler(h2);  
  7.       h2.setHandler(h3);  
  8.       h1.operator();  
  9.       }  
  10.       }  

      输出:

      h1deal!
      h2deal!
      h3deal!

      此处强调一点就是,链接上的请求可以是一条链,可以是一个树,还可以是一个环,模式本身不约束这个,需要我们自己去实现,同时,在一个时刻,命令只允许由一个对象传给另一个对象,而不允许传给多个对象。

      18、命令模式(Command)

      命令模式很好理解,举个例子,司令员下令让士兵去干件事情,从整个事情的角度来考虑,司令员的作用是,发出口令,口令经过传递,传到了士兵耳朵里,士兵去执行。这个过程好在,三者相互解耦,任何一方都不用去依赖其他人,只需要做好自己的事儿就行,司令员要的是结果,不会去关注到底士兵是怎么实现的。我们看看关系图:

      Invoker是调用者(司令员),Receiver是被调用者(士兵),MyCommand是命令,实现了Command接口,持有接收对象,看实现代码:

      [java] view plaincopy

  1.       public interface Command {  
  2.       public void exe();  
  3.       }  

      [java] view plaincopy

  1.       public class MyCommand implements Command {  
  2.       private Receiver receiver;  
  3.       public MyCommand(Receiver receiver) {  
  4.       this.receiver = receiver;  
  5.       }  
  6.       @Override  
  7.       public void exe() {  
  8.       receiver.action();  
  9.       }  
  10.       }  

      [java] view plaincopy

  1.       public class Receiver {  
  2.       public void action(){  
  3.       System.out.println(“command received!”);  
  4.       }  
  5.       }  

      [java] view plaincopy

  1.       public class Invoker {  
  2.       private Command command;  
  3.       public Invoker(Command command) {  
  4.       this.command = command;  
  5.       }  
  6.       public void action(){  
  7.       command.exe();  
  8.       }  
  9.       }  

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       Receiver receiver = new Receiver();  
  4.       Command cmd = new MyCommand(receiver);  
  5.       Invoker invoker = new Invoker(cmd);  
  6.       invoker.action();  
  7.       }  
  8.       }  

      输出:command received!

      这个很哈理解,命令模式的目的就是达到命令的发出者和执行者之间解耦,实现请求和执行分开,熟悉Struts的同学应该知道,Struts其实就是一种将请求和呈现分离的技术,其中必然涉及命令模式的思想!

      其实每个设计模式都是很重要的一种思想,看上去很熟,其实是因为我们在学到的东西中都有涉及,尽管有时我们并不知道,其实在Java本身的设计之中处处都有体现,像AWT、JDBC、集合类、IO管道或者是Web框架,里面设计模式无处不在。因为我们篇幅有限,很难讲每一个设计模式都讲的很详细,不过我会尽我所能,尽量在有限的空间和篇幅内,把意思写清楚了,更好让大家明白。本章不出意外的话,应该是设计模式最后一讲了,首先还是上一下上篇开头的那个图:

      本章讲讲第三类和第四类。

      19、备忘录模式(Memento)

      主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象,个人觉得叫备份模式更形象些,通俗的讲下:假设有原始类A,A中有各种属性,A可以决定需要备份的属性,备忘录类B是用来存储A的一些内部状态,类C呢,就是一个用来存储备忘录的,且只能存储,不能修改等操作。做个图来分析一下:

      Original类是原始类,里面有需要保存的属性value及创建一个备忘录类,用来保存value值。Memento类是备忘录类,Storage类是存储备忘录的类,持有Memento类的实例,该模式很好理解。直接看源码:

      [java] view plaincopy

  1.       public class Original {  
  2.       private String value;  
  3.       public String getValue() {  
  4.       return value;  
  5.       }  
  6.       public void setValue(String value) {  
  7.       this.value = value;  
  8.       }  
  9.       public Original(String value) {  
  10.       this.value = value;  
  11.       }  
  12.       public Memento createMemento(){  
  13.       return new Memento(value);  
  14.       }  
  15.       public void restoreMemento(Memento memento){  
  16.       this.value = memento.getValue();  
  17.       }  
  18.       }  

      [java] view plaincopy

  1.       public class Memento {  
  2.       private String value;  
  3.       public Memento(String value) {  
  4.       this.value = value;  
  5.       }  
  6.       public String getValue() {  
  7.       return value;  
  8.       }  
  9.       public void setValue(String value) {  
  10.       this.value = value;  
  11.       }  
  12.       }  

      [java] view plaincopy

  1.       public class Storage {  
  2.       private Memento memento;  
  3.       public Storage(Memento memento) {  
  4.       this.memento = memento;  
  5.       }  
  6.       public Memento getMemento() {  
  7.       return memento;  
  8.       }  
  9.       public void setMemento(Memento memento) {  
  10.       this.memento = memento;  
  11.       }  
  12.       }  

      测试类:

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       // 创建原始类  
  4.       Original origi = new Original(“egg”);  
  5.       // 创建备忘录  
  6.       Storage storage = new Storage(origi.createMemento());  
  7.       // 修改原始类的状态  
  8.       System.out.println(“初始化状态为:” + origi.getValue());  
  9.       origi.setValue(“niu”);  
  10.       System.out.println(“修改后的状态为:” + origi.getValue());  
  11.       // 回复原始类的状态  
  12.       origi.restoreMemento(storage.getMemento());  
  13.       System.out.println(“恢复后的状态为:” + origi.getValue());  
  14.       }  
  15.       }  

      输出:

      初始化状态为:egg
      修改后的状态为:niu
      恢复后的状态为:egg

      简单描述下:新建原始类时,value被初始化为egg,后经过修改,将value的值置为niu,最后倒数第二行进行恢复状态,结果成功恢复了。其实我觉得这个模式叫“备份-恢复”模式最形象。

      20、状态模式(State)

      核心思想就是:当对象的状态改变时,同时改变其行为,很好理解!就拿QQ来说,有几种状态,在线、隐身、忙碌等,每个状态对应不同的操作,而且你的好友也能看到你的状态,所以,状态模式就两点:1、可以通过改变状态来获得不同的行为。2、你的好友能同时看到你的变化。看图:

      State类是个状态类,Context类可以实现切换,我们来看看代码:

      [java] view plaincopy

  1.       package com.xtfggef.dp.state;  
  2.       /** 
  3.       * 状态类的核心类 
  4.       * 2012-12-1 
  5.       * @author erqing 
  6.       * 
  7.       */  
  8.       public class State {  
  9.       private String value;  
  10.       public String getValue() {  
  11.       return value;  
  12.       }  
  13.       public void setValue(String value) {  
  14.       this.value = value;  
  15.       }  
  16.       public void method1(){  
  17.       System.out.println(“execute the first opt!”);  
  18.       }  
  19.       public void method2(){  
  20.       System.out.println(“execute the second opt!”);  
  21.       }  
  22.       }  

      [java] view plaincopy

  1.       package com.xtfggef.dp.state;  
  2.       /** 
  3.       * 状态模式的切换类   2012-12-1 
  4.       * @author erqing 
  5.       *  
  6.       */  
  7.       public class Context {  
  8.       private State state;  
  9.       public Context(State state) {  
  10.       this.state = state;  
  11.       }  
  12.       public State getState() {  
  13.       return state;  
  14.       }  
  15.       public void setState(State state) {  
  16.       this.state = state;  
  17.       }  
  18.       public void method() {  
  19.       if (state.getValue().equals(“state1”)) {  
  20.       state.method1();  
  21.       } else if (state.getValue().equals(“state2”)) {  
  22.       state.method2();  
  23.       }  
  24.       }  
  25.       }  

      测试类:

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       State state = new State();  
  4.       Context context = new Context(state);  
  5.       //设置第一种状态  
  6.       state.setValue(“state1”);  
  7.       context.method();  
  8.       //设置第二种状态  
  9.       state.setValue(“state2”);  
  10.       context.method();  
  11.       }  
  12.       }  

      输出:

      execute the first opt!
      execute the second opt!

      根据这个特性,状态模式在日常开发中用的挺多的,尤其是做网站的时候,我们有时希望根据对象的某一属性,区别开他们的一些功能,比如说简单的权限控制等。
21、访问者模式(Visitor)

      访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。—— From 百科

      简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。

      来看看我自己写的demo。场景:银行柜台提供的服务和来办业务的人。把银行的服务和业务的办理解耦了。缺点:如果银行要修改底层业务接口,所有继承接口的类都需要作出修改。不过java8的新特性接口默认方法可以解决这个问题,或者java8之前可以通过接口的适配器模式来解决这个问题

      publicclass VisitorDemo {
      // 银行柜台服务,以后银行要新增业务,只需要新增一个类实现这个接口就可以了。 interface Service { publicvoid accept(Visitor visitor); }
      // 来办业务的人,里面可以加上权限控制等等 staticclass Visitor { publicvoid process(Service service) { // 基本业务 System.out.println(“基本业务”); } publicvoid process(Saving service) { // 存款 System.out.println(“存款”); } publicvoid process(Draw service) { // 提款 System.out.println(“提款”); } publicvoid process(Fund service) { System.out.println(“基金”); // 基金 } } staticclass Saving implements Service { publicvoid accept(Visitor visitor) { visitor.process(this); } } staticclass Draw implements Service { publicvoid accept(Visitor visitor) { visitor.process(this); } } staticclass Fund implements Service { publicvoid accept(Visitor visitor) { visitor.process(this); } } publicstaticvoid main(String[] args) { Service saving = new Saving(); Service fund = new Fund(); Service draw = new Draw(); Visitor visitor = new Visitor(); Visitor guweiwei = new Visitor(); fund.accept(guweiwei); saving.accept(visitor); fund.accept(visitor); draw.accept(visitor); } }

      测试:

      publicstaticvoid main(String[] args) { Service saving = new Saving(); Service fund = new Fund(); Service draw = new Draw(); Visitor visitor = new Visitor(); Visitor guweiwei =new Visitor(); fund.accept(guweiwei); saving.accept(visitor); fund.accept(visitor); draw.accept(visitor); }

      输出:

      基金
      存款
      基金
      提款

      该模式适用场景:如果我们想为一个现有的类增加新功能,不得不考虑几个事情:1、新功能会不会与现有功能出现兼容性问题?2、以后会不会再需要添加?3、如果类不允许修改代码怎么办?面对这些问题,最好的解决方法就是使用访问者模式,访问者模式适用于数据结构相对稳定的系统,把数据结构和算法解耦,
22、中介者模式(Mediator)

      中介者模式也是用来降低类类之间的耦合的,因为如果类类之间有依赖关系的话,不利于功能的拓展和维护,因为只要修改一个对象,其它关联的对象都得进行修改。如果使用中介者模式,只需关心和Mediator类的关系,具体类类之间的关系及调度交给Mediator就行,这有点像spring容器的作用。先看看图:

      User类统一接口,User1和User2分别是不同的对象,二者之间有关联,如果不采用中介者模式,则需要二者相互持有引用,这样二者的耦合度很高,为了解耦,引入了Mediator类,提供统一接口,MyMediator为其实现类,里面持有User1和User2的实例,用来实现对User1和User2的控制。这样User1和User2两个对象相互独立,他们只需要保持好和Mediator之间的关系就行,剩下的全由MyMediator类来维护!基本实现:

      [java] view plaincopy

  1.       public interface Mediator {  
  2.       public void createMediator();  
  3.       public void workAll();  
  4.       }  

      [java] view plaincopy

  1.       public class MyMediator implements Mediator {  
  2.       private User user1;  
  3.       private User user2;  
  4.       public User getUser1() {  
  5.       return user1;  
  6.       }  
  7.       public User getUser2() {  
  8.       return user2;  
  9.       }  
  10.       @Override  
  11.       public void createMediator() {  
  12.       user1 = new User1(this);  
  13.       user2 = new User2(this);  
  14.       }  
  15.       @Override  
  16.       public void workAll() {  
  17.       user1.work();  
  18.       user2.work();  
  19.       }  
  20.       }  

      [java] view plaincopy

  1.       public abstract class User {  
  2.       private Mediator mediator;  
  3.       public Mediator getMediator(){  
  4.       return mediator;  
  5.       }  
  6.       public User(Mediator mediator) {  
  7.       this.mediator = mediator;  
  8.       }  
  9.       public abstract void work();  
  10.       }  

      [java] view plaincopy

  1.       public class User1 extends User {  
  2.       public User1(Mediator mediator){  
  3.       super(mediator);  
  4.       }  
  5.       @Override  
  6.       public void work() {  
  7.       System.out.println(“user1 exe!”);  
  8.       }  
  9.       }  

      [java] view plaincopy

  1.       public class User2 extends User {  
  2.       public User2(Mediator mediator){  
  3.       super(mediator);  
  4.       }  
  5.       @Override  
  6.       public void work() {  
  7.       System.out.println(“user2 exe!”);  
  8.       }  
  9.       }  

      测试类:

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       Mediator mediator = new MyMediator();  
  4.       mediator.createMediator();  
  5.       mediator.workAll();  
  6.       }  
  7.       }  

      输出:

      user1 exe!
      user2 exe!
23、解释器模式(Interpreter)
      解释器模式是我们暂时的最后一讲,一般主要应用在OOP开发中的编译器的开发中,所以适用面比较窄。

      Context类是一个上下文环境类,Plus和Minus分别是用来计算的实现,代码如下:

      [java] view plaincopy

  1.       public interface Expression {  
  2.       public int interpret(Context context);  
  3.       }  

      [java] view plaincopy

  1.       public class Plus implements Expression {  
  2.       @Override  
  3.       public int interpret(Context context) {  
  4.       return context.getNum1()+context.getNum2();  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class Minus implements Expression {  
  2.       @Override  
  3.       public int interpret(Context context) {  
  4.       return context.getNum1()-context.getNum2();  
  5.       }  
  6.       }  

      [java] view plaincopy

  1.       public class Context {  
  2.       private int num1;  
  3.       private int num2;  
  4.       public Context(int num1, int num2) {  
  5.       this.num1 = num1;  
  6.       this.num2 = num2;  
  7.       }  
  8.       public int getNum1() {  
  9.       return num1;  
  10.       }  
  11.       public void setNum1(int num1) {  
  12.       this.num1 = num1;  
  13.       }  
  14.       public int getNum2() {  
  15.       return num2;  
  16.       }  
  17.       public void setNum2(int num2) {  
  18.       this.num2 = num2;  
  19.       }  
  20.       }  

      [java] view plaincopy

  1.       public class Test {  
  2.       public static void main(String[] args) {  
  3.       // 计算9+2-8的值  
  4.       int result = new Minus().interpret((new Context(new Plus()  
  5.       .interpret(new Context(9, 2)), 8)));  
  6.       System.out.println(result);  
  7.       }  
  8.       }  

      最后输出正确的结果:3。  

      基本就这样,解释器模式用来做各种各样的解释器,如正则表达式等的解释器等等!
      设计模式基本就这么大概讲完了,总体感觉有点简略,的确,这么点儿篇幅,不足以对整个23种设计模式做全面的阐述,此处读者可将它作为一个理论基础去学习,通过这四篇博文,先基本有个概念,虽然我讲的有些简单,但基本都能说明问题及他们的特点,如果对哪一个感兴趣,可以继续深入研究!同时我也会不断更新,尽量补全遗

Flink 官方文档解读-导读

该文章,写给想看开源项目,不知道怎么看,或者不会看的朋友

访问Flink官方地址:https://flink.apache.org/ ,可以通过Apache官方 www.apache.org 点击Project或者直接搜索“Flink”

我的一个阅读习惯,从Documentation开始,然后找到最新的一个Release版本,当前是1.7

访问页面,我们会看到上面的页面,其中,一些知识点,都被加上了超链接,后续我们单独介绍,先来看看这一页说了什么内容。

一上来,先说了一些文档的编辑时间。说了一下Flink是一个分布式流处理和批处理的开源平台,Flink核心是一个流式处理引擎,他提供了数据分发,通信,在分布式计算数据流上进行数据容错,在留引擎上构建了批处理,包含本地迭代支持,内存管理,编程优化。

第一步,介绍一些概念,数据流编程模型分布式运行环境,它会帮助你理解其他章节,包括安装和编程,所以强烈建议你读一下。

初识

编程指南,你需要阅读我们的手册,包括基本API概念和去学习一下,DataStreaming APIDataSet API,如何去写一个流式程序。

部署

在你准备发布一个产品时,请先阅读产品清单

其他资源

一些讲座可以关注flink官方网站,或者看 YouTube

培训教材

博客data Artisans官方博客

美团外卖iOS App冷启动治理(转载)

一、背景

冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代开发,产品形态不断完善,业务功能日趋复杂;同时外卖App也已经由原来的独立业务App演进成为一个平台App,陆续接入了闪购、跑腿等其他新业务。因此,更多更复杂的工作需要在App冷启动的时候被完成,这给App的冷启动性能带来了挑战。对此,我们团队基于业务形态的变化和外卖App的特点,对冷启动进行了持续且有针对性的优化工作,目的就是为了呈现更加流畅的用户体验。

二、冷启动定义

一般而言,大家把iOS冷启动的过程定义为:从用户点击App图标开始到appDelegate didFinishLaunching方法执行完成为止。这个过程主要分为两个阶段:

  • T1:main()函数之前,即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数。
  • T2:main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕。

然而,当didFinishLaunchingWithOptions执行完成时,用户还没有看到App的主界面,也不能开始使用App。例如在外卖App中,App还需要做一些初始化工作,然后经历定位、首页请求、首页渲染等过程后,用户才能真正看到数据内容并开始使用,我们认为这个时候冷启动才算完成。我们把这个过程定义为T3。

综上,外卖App把冷启动过程定义为:从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2+T3。在App冷启动过程当中,这三个阶段中的每个阶段都存在很多可以被优化的点。

三、问题现状

性能存量问题

美团外卖iOS客户端经过几十个版本的迭代开发后,在冷启动过程中已经积累了若干性能问题,解决这些性能瓶颈是冷启动优化工作的首要目标,这些问题主要包括:

注:启动项的定义,在App启动过程中需要被完成的某项工作,我们称之为一个启动项。例如某个SDK的初始化、某个功能的预加载等。

性能增量问题

一般情况下,在App早期阶段,冷启动不会有明显的性能问题。冷启动性能问题也不是在某个版本突然出现的,而是随着版本迭代,App功能越来越复杂,启动任务越来越多,冷启动时间也一点点延长。最后当我们注意到,并想要优化它的时候,这个问题已经变得很棘手了。外卖App的性能问题增量主要来自启动项的增加,随着版本迭代,启动项任务简单粗暴地堆积在启动流程中。如果每个版本冷启动时间增加0.1s,那么几个版本下来,冷启动时长就会明显增加很多。

四、治理思路

冷启动性能问题的治理目标主要有三个:

  1. 解决存量问题:优化当前性能瓶颈点,优化启动流程,缩短冷启动时间。
  2. 管控增量问题:冷启动流程规范化,通过代码范式和文档指导后续冷启动过程代码的维护,控制时间增量。
  3. 完善监控:完善冷启动性能指标监控,收集更详细的数据,及时发现性能问题。

五、规范启动流程

截止至2017年底,美团外卖用户数已达2.5亿,而美团外卖App也已完成了从支撑单一业务的App到支持多业务的平台型App的演进(美团外卖iOS多端复用的推动、支撑与思考),公司的一些新兴业务也陆续集成到外卖App当中。下面是外卖App的架构图,外卖的架构主要分为三层,底层是基础组件层,中层是外卖平台层,平台层向下管理基础组件,向上为业务组件提供统一的适配接口,上层是基础组件层,包括外卖业务拆分的子业务组件(外卖App和美团App中的外卖频道可以复用子业务组件)和接入的其他非外卖业务。

App的平台化为业务方提供了高效、标准的统一平台,但与此同时,平台化和业务的快速迭代也给冷启动带来了问题:

  1. 现有的启动项堆积严重,拖慢启动速度。
  2. 新的启动项缺乏添加范式,杂乱无章,修改风险大,难以阅读和维护。

面对这个问题,我们首先梳理了目前启动流程中所有的启动项,然后针对App平台化设计了新的启动项管理方式:分阶段启动和启动项自注册

分阶段启动

早期由于业务比较简单,所有启动项都是不加以区分,简单地堆积到didFinishLaunchingWithOptions方法中,但随着业务的增加,越来越多的启动项代码堆积在一起,性能较差,代码臃肿而混乱。

通过对SDK的梳理和分析,我们发现启动项也需要根据所完成的任务被分类,有些启动项是需要刚启动就执行的操作,如Crash监控、统计上报等,否则会导致信息收集的缺失;有些启动项需要在较早的时间节点完成,例如一些提供用户信息的SDK、定位功能的初始化、网络初始化等;有些启动项则可以被延迟执行,如一些自定义配置,一些业务服务的调用、支付SDK、地图SDK等。我们所做的分阶段启动,首先就是把启动流程合理地划分为若干个启动阶段,然后依据每个启动项所做的事情的优先级把它们分配到相应的启动阶段,优先级高的放在靠前的阶段,优先级低的放在靠后的阶段。

下面是我们对美团外卖App启动阶段进行的重新定义,对所有启动项进行的梳理和重新分类,把它们对应到合理的启动阶段。这样做一方面可以推迟执行那些不必过早执行的启动项,缩短启动时间;另一方面,把启动项进行归类,方便后续的阅读和维护。然后把这些规则落地为启动项的维护文档,指导后续启动项的新增和维护。

通过上面的工作,我们梳理出了十几个可以推迟执行的启动项,占所有启动项的30%左右,有效地优化了启动项所占的这部分冷启动时间。

启动项自注册

确定了启动项分阶段启动的方案后,我们面对的问题就是如何执行这些启动项。比较容易想到的方案是:在启动时创建一个启动管理器,然后读取所有启动项,然后当时间节点到来时由启动器触发启动项执行。这种方式存在两个问题:

  1. 所有启动项都要预先写到一个文件中(在.m文件import,或用.plist文件组织),这种中心化的写法会导致臃肿的代码,难以阅读维护。
  2. 启动项代码无法复用:启动项无法收敛到子业务库内部,在外卖App和美团App中要重复实现,和外卖App平台化的方向不符。

而我们希望的方式是,启动项维护方式可插拔,启动项之间、业务模块之间不耦合,且一次实现可在两端复用。下图是我们采用的启动项管理方式,我们称之为启动项的自注册:一个启动项定义在子业务模块内部,被封装成一个方法,并且自声明启动阶段(例如一个启动项A,在独立App中可以声明为在willFinishLaunch阶段被执行,在美团App中则声明在resignActive阶段被执行)。这种方式下,启动项即实现了两端复用,不相关的启动项互相隔离,添加/删除启动项都更加方便。

那么如何给一个启动项声明启动阶段?又如何在正确的时机触发启动项的执行呢?在代码上,一个启动项最终都会对应到一个函数的执行,所以在运行时只要能获取到函数的指针,就可以触发启动项。美团平台开发的组件启动治理基建Kylin正是这样做的:Kylin的核心思想就是在编译时把数据(如函数指针)写入到可执行文件的__DATA段中,运行时再从__DATA段取出数据进行相应的操作(调用函数)。

为什么要用借用__DATA段呢?原因就是为了能够覆盖所有的启动阶段,例如main()之前的阶段。

Kylin实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个{key(key代表不同的启动阶段), *pointer}对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。

上述方式,可以封装成一个宏,来达到代码的简化,以调用宏 KLN_STRINGS_EXPORT(“Key”, “Value”)为例,最终会被展开为:

__attribute__((used, section("__DATA" "," "__kylin__"))) static const KLN_DATA __kylin__0 = (KLN_DATA){(KLN_DATA_HEADER){"Key", KLN_STRING, KLN_IS_ARRAY}, "Value"};

使用示例,编译器把启动项函数注册到启动阶段A:

KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在a.m文件中,通过注册宏,把启动项A声明为在STAGE_KEY_A阶段执行
    // 启动项代码A
}
KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在b.m文件中,把启动项B声明为在STAGE_KEY_A阶段执行
    // 启动项代码B
}

在启动流程中,在启动阶段STAGE_KEY_A触发所有注册到STAGE_KEY_A时间节点的启动项,通过对这种方式,几乎没有任何额外的辅助代码,我们用一种很简洁的方式完成了启动项的自注册。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 其他逻辑
    [[KLNKylin sharedInstance] executeArrayForKey:STAGE_KEY_A];  // 在此触发所有注册到STAGE_KEY_A时间节点的启动项
    // 其他逻辑
    return YES;
}

完成对现有的启动项的梳理和优化后,我们也输出了后续启动项的添加&维护规范,规范后续启动项的分类原则,优先级和启动阶段。目的是管控性能问题增量,保证优化成果。

六、优化main()之前

在调用main()函数之前,基本所有的工作都是由操作系统完成的,开发者能够插手的地方不多,所以如果想要优化这段时间,就必须先了解一下,操作系统在main()之前做了什么。main()之前操作系统所做的工作就是把可执行文件(Mach-O格式)加载到内存空间,然后加载动态链接库dyld,再执行一系列动态链接操作和初始化操作的过程(加载、绑定、及初始化方法)。这方面的资料网上比较多,但重复性较高,此处附上一篇WWDC的Topic:Optimizing App Startup Time 。

加载过程—从exec()到main()

真正的加载过程从exec()函数开始,exec()是一个系统调用。操作系统首先为进程分配一段内存空间,然后执行如下操作:

  1. 把App对应的可执行文件加载到内存。
  2. 把Dyld加载到内存。
  3. Dyld进行动态链接。

下面我们简要分析一下Dyld在各阶段所做的事情:

阶段工作
加载动态库Dyld从主执行文件的header获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以所需要加载的是动态库列表一个递归依赖的集合
Rebase和Bind– Rebase在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化,所以需要在原来的地址根据随机的偏移量做一下修正
– Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现
Objc setup– 注册Objc类 (class registration)
– 把category的定义插入方法列表 (category registration)
– 保证每一个selector唯一 (selector uniquing)
Initializers– Objc的+load()函数
– C++的构造函数属性函数
– 非基本类型的C++静态全局变量的创建(通常是类或结构体)

最后 dyld 会调用 main() 函数,main() 会调用 UIApplicationMain(),before main()的过程也就此完成。

了解完main()之前的加载过程后,我们可以分析出一些影响T1时间的因素:

  1. 动态库加载越多,启动越慢。
  2. ObjC类,方法越多,启动越慢。
  3. ObjC的+load越多,启动越慢。
  4. C的constructor函数越多,启动越慢。
  5. C++静态对象越多,启动越慢。

针对以上几点,我们做了如下一些优化工作:

代码瘦身

随着业务的迭代,不断有新的代码加入,同时也会废弃掉无用的代码和资源文件,但是工程中经常有无用的代码和文件被遗弃在角落里,没有及时被清理掉。这些无用的部分一方面增大了App的包体积,另一方便也拖慢了App的冷启动速度,所以及时清理掉这些无用的代码和资源十分有必要。

通过对Mach-O文件的了解,可以知道__TEXT:__objcmethname:中包含了代码中的所有方法,而\_DATA__objc_selrefs中则包含了所有被使用的方法的引用,通过取两个集合的差集就可以得到所有未被使用的代码。核心方法如下,具体可以参考:objc_cover:

def referenced_selectors(path):
    re_sel = re.compile("__TEXT:__objc_methname:(.+)") //获取所有方法
    refs = set()
    lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法
    for line in lines:
        results = re_sel.findall(line)
        if results:
            refs.add(results[0])
    return refs
}

通过这种方法,我们排查了十几个无用类和250+无用的方法。

+load优化

目前iOS App中或多或少的都会写一些+load方法,用于在App启动执行一些操作,+load方法在Initializers阶段被执行,但过多+load方法则会拖慢启动速度,对于大中型的App更是如此。通过对App中+load的方法分析,发现很多代码虽然需要在App启动时较早的时机进行初始化,但并不需要在+load这样非常靠前的位置,完全是可以延迟到App冷启动后的某个时间节点,例如一些路由操作。其实+load也可以被当做一种启动项来处理,所以在替换+load方法的具体实现上,我们仍然采用了上面的Kylin方式。

使用示例:

// 用WMAPP_BUSINESS_INIT_AFTER_HOMELOADING声明替换+load声明即可,不需其他改动
WMAPP_BUSINESS_INIT_AFTER_HOMELOADING() { 
    // 原+load方法中的代码
}
// 在某个合适的时机触发注册到该阶段的所有方法,如冷启动结束后
[[KLNKylin sharedInstance] executeArrayForKey:@kWMAPP_BUSINESS_INITIALIZATION_AFTER_HOMELOADING_KEY] 
}

七、优化耗时操作

在main()之后主要工作是各种启动项的执行(上面已经叙述),主界面的构建,例如TabBarVC,HomeVC等等。资源的加载,如图片I/O、图片解码、archive文档等。这些操作中可能会隐含着一些耗时操作,靠单纯阅读非常难以发现,如何发现这些耗时点呢?找到合适的工具就会事半功倍。

Time Profiler

Time Profiler是Xcode自带的时间性能分析工具,它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。Time Profiler的使用方法网上有很多使用教程,这里我们也不过多介绍,附上一篇使用文档:Instruments Tutorial with Swift: Getting Started

火焰图

除了Time Profiler,火焰图也是一个分析CPU耗时的利器,相比于Time Profiler,火焰图更加清晰。火焰图分析的产物是一张调用栈耗时图片,之所以称为火焰图,是因为整个图形看起来就像一团跳动的火焰,火焰尖部是调用栈的栈顶,底部是栈底,纵向表示调用栈的深度,横向表示消耗的时间。一个格子的宽度越大,越说明其可能是瓶颈。分析火焰图主要就是看那些比较宽大的火苗,特别留意那些类似“平顶山”的火苗。下面是美团平台开发的性能分析工具-Caesium的分析效果图:

通过对火焰图的分析,我们发现了冷启动过程中存在着不少问题,并成功优化了0.3S+的时间。优化内容总结如下:

优化点举例
发现隐晦的耗时操作发现在冷启动过程中archive了一张图片,非常耗时
推迟&减少I/O操作减少动画图片组的数量,替换大图资源等。因为相比于内存操作,硬盘I/O是非常耗时的操作
推迟执行的一些任务如一些资源的I/O,一些布局逻辑,对象的创建时机等

八、优化串行操作

在冷启动过程中,有很多操作是串行执行的,若干个任务串行执行,时间必然比较长。如果能变串行为并行,那么冷启动时间就能够大大缩短。

闪屏页的使用

现在许多App在启动时并不直接进入首页,而是会向用户展示一个持续一小段时间的闪屏页,如果使用恰当,这个闪屏页就能帮我们节省一些启动时间。因为当一个App比较复杂的时候,启动时首次构建App的UI就是一个比较耗时的过程,假定这个时间是0.2秒,如果我们是先构建首页UI,然后再在Window上加上这个闪屏页,那么冷启动时,App就会实实在在地卡住0.2秒,但是如果我们是先把闪屏页作为App的RootViewController,那么这个构建过程就会很快。因为闪屏页只有一个简单的ImageView,而这个ImageView则会向用户展示一小段时间,这时我们就可以利用这一段时间来构建首页UI了,一举两得。

缓存定位&首页预请求

美团外卖App冷启动过程中一个重要的串行流程就是:首页定位–>首页请求–>首页渲染过程,这三个操作占了整个首页加载时间的77%左右,所以想要缩短冷启动时间,就一定要从这三点出发进行优化。

之前串行操作流程如下:

优化后的设计,在发起定位的同时,使用客户端缓存定位,进行首页数据的预请求,使定位和请求并行进行。然后当用户真实定位成功后,判断真实定位是否命中缓存定位,如果命中,则刚才的预请求数据有效,这样可以节省大概40%的时间首页加载时间,效果非常明显;如果未命中,则弃用预请求数据,重新请求。

九、数据监控

Time Profiler和Caesium火焰图都只能在线下分析App在单台设备中的耗时操作,局限性比较大,无法在线上监控App在用户设备上的表现。外卖App使用公司内部自研的Metrics性能监控系统,长期监控App的性能指标,帮助我们掌握App在线上各种环境下的真实表现,并为技术优化项目提供可靠的数据支持。Metrics监控的核心指标之一,就是冷启动时间。

冷启动开始&结束时间节点

  1. 结束时间点:结束时间比较好确定,我们可以将首页某些视图元素的展示作为首页加载完成的标志。
  2. 开始时间点:一般情况下,我们都是在main()之后才开始接管App,但以main()函数作为冷启动起始点显然不合适,因为这样无法统计到T1时间段。那么,起始时间如何确定呢?目前业界常见的有两种方法,一是以可执行文件中任意一个类的+load方法的执行时间作为起始点;二是分析dylib的依赖关系,找到叶子节点的dylib,然后以其中某个类的+load方法的执行时间作为起始点。根据Dyld对dylib的加载顺序,后者的时机更早。但是这两种方法获取的起始点都只在Initializers阶段,而Initializers之前的时长都没有被计入。Metrics则另辟蹊径,以App的进程创建时间(即exec函数执行时间)作为冷启动的起始时间。因为系统允许我们通过sysctl函数获得进程的有关信息,其中就包括进程创建的时间戳。
#import <sys/sysctl.h>
#import <mach/mach.h>

+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(*procInfo);
    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}

+ (NSTimeInterval)processStartTime
{
    struct kinfo_proc kProcInfo;
    if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
        return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
    } else {
        NSAssert(NO, @"无法取得进程的信息");
        return 0;
    }
}

进程创建的时机非常早。经过实验,在一个新建的空白App中,进程创建时间比叶子节点dylib中的+load方法执行时间早12ms,比main函数的执行时间早13ms(实验设备:iPhone 7 Plus (iOS 12.0)、Xcode 10.0、Release 模式)。外卖App线上的数据则更加明显,同样的机型(iPhone 7 Plus)和系统版本(iOS 12.0),进程创建时间比叶子节点dylib中的+load方法执行时间早688ms。而在全部机型和系统版本中,这一数据则是878ms。

冷启动过程时间节点

我们也在App冷启动过程中的所有关键节点打上一连串测速点,Metrics会记录下测速点的名称,及其距离进程创建时间的时长。我们没有采用自动打点的方式,是因为外卖App的冷启动过程十分复杂,而自动打点无法做到如此细致,并不实用。另外,Metrics记录的是时间轴上以进程创建时间为原点的一组顺序的时间点,而不是一组时间段,是因为顺序的时间点可以计算任意两个时间点之间的距离,即可以将时间点处理成时间段。但是,一组时间段可能无法还原为顺序的时间点,因为时间段之间可能并不是首尾相接的,特别是对于异步执行或者多线程的情况。

在测速完毕后,Metrics会统一将所有测速点上报到后台。下图是美团外卖App 6.10版本的部分过程节点监控数据截图:

Metrics还会由后台对数据做聚合计算,得到冷启动总时长和各个测速点时长的50分位数、90分位数和95分位数的统计数据,这样我们就能从宏观上对冷启动时长分布情况有所了解。下图中横轴为时长,纵轴为上报的样本数。

十、总结

对于快速迭代的App,随着业务复杂度的增加,冷启动时长会不可避免的增加。冷启动流程也是一个比较复杂的过程,当遇到冷启动性能瓶颈时,我们可以根据App自身的特点,配合工具的使用,从多方面、多角度进行优化。同时,优化冷启动存量问题只是冷启动治理的第一步,因为冷启动性能问题并不是一日造成的,也不能简单的通过一次优化工作就能解决,我们需要通过合理的设计、规范的约束,来有效地管控性能问题的增量,并通过持续的线上监控来及时发现并修正性能问题,这样才能够长期保证良好的App冷启动体验。

作者简介

郭赛,美团点评资深工程师。2015年加入美团,目前作为外卖iOS团队主力开发,负责移动端业务开发,业务类基础设施的建设与维护。

徐宏,美团点评资深工程师。2016年加入美团,目前作为外卖iOS团队主力开发,负责移动端APM性能监控,高可用基础设施支撑相关推进工作。

来自:https://tech.meituan.com/waimai_ios_optimizing_startup.html

数据库智能运维探索与实践(转载)

从自动化到智能化运维过渡时,美团DBA团队进行了哪些思考、探索与实践?本文根据赵应钢在“第九届中国数据库技术大会”上的演讲内容整理而成,部分内容有更新。

背景

近些年,传统的数据库运维方式已经越来越难于满足业务方对数据库的稳定性、可用性、灵活性的要求。随着数据库规模急速扩大,各种NewSQL系统上线使用,运维逐渐跟不上业务发展,各种矛盾暴露的更加明显。在业务的驱动下,美团点评DBA团队经历了从“人肉”运维到工具化、产品化、自助化、自动化的转型之旅,也开始了智能运维在数据库领域的思考和实践。

本文将介绍美团点评整个数据库平台的演进历史,以及我们当前的情况和面临的一些挑战,最后分享一下我们从自动化到智能化运维过渡时,所进行的思考、探索与实践。

数据库平台的演变

我们数据库平台的演进大概经历了五个大的阶段:

第一个是脚本化阶段,这个阶段,我们人少,集群少,服务流量也比较小,脚本化的模式足以支撑整个服务。

第二个是工具化阶段,我们把一些脚本包装成工具,围绕CMDB管理资产和服务,并完善了监控系统。这时,我们的工具箱也逐渐丰富起来,包括DDL变更工具、SQL Review工具、慢查询采集分析工具和备份闪回工具等等。

第三个是产品化阶段,工具化阶段可能还是单个的工具,但是在完成一些复杂操作时,就需要把这些工具组装起来形成一个产品。当然,并不是说这个产品一定要做成Web系统的形式,而是工具组装起来形成一套流程之后,就可以保证所有DBA的操作行为,对流程的理解以及对线上的影响都是一致的。我们会在易用性和安全性层面不断进行打磨。而工具产品化的主要受益者是DBA,其定位是提升运维服务的效率,减少事故的发生,并方便进行快速统一的迭代。

第四个是打造私有云平台阶段,随着美团点评业务的高速发展,仅靠十几、二十个DBA越来越难以满足业务发展的需要。所以我们就把某些日常操作开放授权,让开发人员自助去做,将DBA从繁琐的操作中解放出来。当时整个平台每天执行300多次改表操作;自助查询超过1万次;自助申请账号、授权并调整监控;自助定义敏感数据并授权给业务方管理员自助审批和管理;自定义业务的高峰和低峰时间段等等;自助下载、查询日志等等。

第五个是自动化阶段,对这个阶段的理解,其实是“仁者见仁,智者见智”。大多数人理解的自动化,只是通过Web平台来执行某些操作,但我们认为这只是半自动化,所谓的自动化应该是完全不需要人参与。目前,我们很多操作都还处于半自动化阶段,下一个阶段我们需要从半自动过渡到全自动。以MySQL系统为例,从运维角度看包括主从的高可用、服务过载的自我保护、容量自动诊断与评估以及集群的自动扩缩容等等。

现状和面临的挑战

下图是我们平台的现状,以关系数据库RDS平台为例,其中集成了很多管理的功能,例如主从的高可用、MGW的管理、DNS的变更、备份系统、升级流程、流量分配和切换系统、账号管理、数据归档、服务与资产的流转系统等等。

而且我们按照逻辑对平台设计进行了划分,例如以用户维度划分的RDS自助平台,DBA管理平台和测试环境管理平台;以功能维度划分的运维、运营和监控;以存储类型为维度划分的关系型数据库MySQL、分布式KV缓存、分布式KV存储,以及正在建设中的NewSQL数据库平台等等。未来,我们希望打造成“MySQL+NoSQL+NewSQL,存储+缓存的一站式服务平台”。

挑战一:RootCause定位难

即便我们打造了一个很强大的平台,但还是发现有很多问题难以搞定。第一个就是故障定位,如果是简单的故障,我们有类似天网、雷达这样的系统去发现和定位。但是如果故障发生在数据库内部,那就需要专业的数据库知识,去定位和查明到底是什么原因导致了故障。

通常来讲,故障的轨迹是一个链,但也可能是一个“多米诺骨牌”的连环。可能因为一些原因导致SQL执行变慢,引起连接数的增长,进而导致业务超时,而业务超时又会引发业务不断重试,结果会产生更多的问题。当我们收到一个报警时,可能已经过了30秒甚至更长时间,DBA再去查看时,已经错过了最佳的事故处理时机。所以,我们要在故障发生之后,制定一些应对策略,例如快速切换主库、自动屏蔽下线问题从库等等。除此之外,还有一个比较难的问题,就是如何避免相似的故障再次出现。

挑战二:人力和发展困境

第二个挑战是人力和发展的困境,当服务流量成倍增长时,其成本并不是以相同的速度对应增长的。当业务逻辑越来越复杂时,每增加一块钱的营收,其后面对应的数据库QPS可能是2倍甚至5倍,业务逻辑越复杂,服务支撑的难度越大。另外,传统的关系型数据库在容量、延时、响应时间以及数据量等方面很容易达到瓶颈,这就需要我们不断拆分集群,同时开发诉求也多种多样,当我们尝试使用平台化的思想去解决问题时,还要充分思考如何满足研发人员多样化的需求。

人力困境这一问题,从DBA的角度来说,时间被严重的碎片化,自身的成长就会遇到瓶颈,比如经常会做一些枯燥的重复操作;另外,业务咨询量暴增,尽管我们已经在尝试平台化的方法,但是还是跟不上业务发展的速度。还有一个就是专业的DBA越来越匮乏,越来越贵,关键是根本招聘不到人手。

在这种背景下,我们必须去思考:如何突破困局?如何朝着智能化转型?传统运维苦在哪里?智能化运维又能解决哪些问题?

首先从故障产生的原因来说,传统运维是故障触发,而智能运维是隐患驱动。换句话来说,智能运维不用报警,通过看报表就能知道可能要出事了,能够把故障消灭在“萌芽”阶段;第二,传统运维是被动接受,而智能运维是主动出击。但主动出击不一定是通过DBA去做,可能是系统或者机器人操作;第三,传统运维是由DBA发起和解决的,而智能运维是系统发起、RD自助;第四,传统运维属于“人肉救火”,而智能运维属于“智能决策执行”;最后一点,传统运维需要DBA亲临事故现场,而智能运维DBA只需要“隐身幕后”。

从自动化到智能化

那么,如何从半自动化过渡到自动化,进而发展到智能化运维呢?在这个过程中,我们会面临哪些痛点呢?

我们的目标是为整个公司的业务系统提供高效、稳定、快速的存储服务,这也是DBA存在的价值。业务并不关心后面是MySQL还是NoSQL,只关心数据是否没丢,服务是否可用,出了问题之后多长时间能够恢复等等。所以我们尽可能做到把这些东西对开发人员透明化,提供稳定高效快速的服务。而站在公司的角度,就是在有限的资源下,提升效率,降低成本,尽可能长远地解决问题。

上图是传统运维和智能运维的特点分析,左边属于传统运维,右边属于智能运维。传统运维在采集这一块做的不够,所以它没有太多的数据可供参考,其分析和预警能力是比较弱的。而智能运维刚好是反过来,重采集,很多功夫都在平时做了,包括分析、预警和执行,智能分析并推送关键报表。

而我们的目标,是让智能运维中的“报警+分析+执行”的比重占据的越来越少。

决策执行如何去做呢?我们都知道,预警重要但不紧急,但报警是紧急且重要的,如果你不能够及时去处理的话,事态可能会扩大,甚至会给公司带来直接的经济损失。

预警通常代表我们已经定位了一个问题,它的决策思路是非常清晰的,可以使用基于规则或AI的方式去解决,相对难度更小一些。而报警依赖于现场的链路分析,变量多、路径长,所以决策难,间接导致任何决策的风险可能都变大。所以说我们的策略就是全面的采集数据,然后增多预警,率先实现预警发现和处理的智能化。就像我们既有步枪,也有手枪和刺刀,能远距离解决敌人的,就尽量不要短兵相接、肉搏上阵。

数据采集,从数据库角度来说,我们产生的数据分成四块,Global Status、Variable,Processlist、InnoDB Status,Slow、Error、General Log和Binlog;从应用侧来说,包含端到端成功率、响应时间95线、99线、错误日志和吞吐量;从系统层面,支持秒级采样、操作系统各项指标;从变更侧来看,包含集群拓扑调整、在线DDL、DML变更、DB平台操作日志和应用端发布记录等等。

数据分析,首先是围绕集群分析,接着是实例、库,最后是表,其中每个对象都可以在多项指标上同比和环比,具体对比项可参考上图。

通过上面的步骤,我们基本可以获得数据库的画像,并且帮助我们从整体上做资源规划和服务治理。例如,有些集群实例数特别多且有继续增加的趋势,那么服务器需要scale up;读增加迅猛,读写比变大,那么应考虑存储KV化;利用率和分布情况会影响到服务器采购和预算制定;哪几类报警最多,就专项治理,各个击破。

从局部来说,我们根据分析到的一些数据,可以做一个集群的健康体检,例如数据库的某些指标是否超标、如何做调整等等。

数据库预警,通过分析去发现隐患,把报警转化为预警。上图是我们实际情况下的报警统计分析结果,其中主从延迟占比最大。假设load.1minPerCPU比较高,我们怎么去解决?那么,可能需要采购CPU单核性能更高的机器,而不是采用更多的核心。再比如说磁盘空间,当我们发现3T的磁盘空间普遍不够时,我们下次可以采购6T或更大空间的磁盘。

针对空间预警问题,什么时候需要拆分集群?MySQL数据库里,拆分或迁移数据库,花费的时间可能会很久。所以需要评估当前集群,按目前的增长速度还能支撑多长时间,进而反推何时要开始拆分、扩容等操作。

针对慢查询的预警问题,我们会统计红黑榜,上图是统计数据,也有利用率和出轨率的数据。假设这是一个金融事业群的数据库,假设有业务需要访问且是直连,那么这时就会产生几个问题:第一个,有没有数据所有者的授权;第二个,如果不通过服务化方式或者接口,发生故障时,它可能会导致整个金融的数据库挂,如何进行降级?所以,我们会去统计出轨率跟慢查询,如果某数据库正被以一种非法的方式访问,那么我们就会扫描出来,再去进行服务治理。

从运维的层面来说,我们做了故障快速转移,包括自动生成配置文件,自动判断是否启用监控,切换后自动重写配置,以及从库可自动恢复上线等等。

报警自动处理,目前来说大部分的处理工作还是基于规则,在大背景下拟定规则,触发之后,按照满足的前提条件触发动作,随着库的规则定义的逐渐完善和丰富,可以逐步解决很多简单的问题,这部分就不再需要人的参与。

展望

未来我们还会做一个故障诊断平台,类似于“扁鹊”,实现日志的采集、入库和分析,同时提供接口,供全链路的故障定位和分析、服务化治理。

展望智能运维,应该是在自动化和智能化上交叠演进,在ABC(AI、Big Data、Cloud Computing)三个方向上深入融合。在数据库领域,NoSQL和SQL界限正变得模糊,软硬结合、存储计算分离架构也被越来越多的应用,智能运维正当其时,我们也面临更多新的挑战。我们的目标是,希望通过DB平台的不断建设加固,平台能自己发现问题,自动定位问题,并智能的解决问题。

作者简介

应钢,美团点评研究员,数据库专家。曾就职于百度、新浪、去哪儿网等,10年数据库自动化运维开发、数据库性能优化、大规模数据库集群技术保障和架构优化经验。精通主流的SQL与NoSQL系统,现专注于公司业务在NewSQL领域的创新和落地。

来自:https://tech.meituan.com/Intelligent_Operation_Practice_in_meituan.html

LruCache在美团DSP系统中的应用演进(转载)

背景

DSP系统是互联网广告需求方平台,用于承接媒体流量,投放广告。业务特点是并发度高,平均响应低(百毫秒)。

为了能够有效提高DSP系统的性能,美团平台引入了一种带有清退机制的缓存结构LruCache(Least Recently Used Cache),在目前的DSP系统中,使用LruCache + 键值存储数据库的机制将远端数据变为本地缓存数据,不仅能够降低平均获取信息的耗时,而且通过一定的清退机制,也可以维持服务内存占用在安全区间。

本文将会结合实际应用场景,阐述引入LruCache的原因,并会在高QPS下的挑战与解决方案等方面做详细深入的介绍,希望能对DSP感兴趣的同学有所启发。

LruCache简介

LruCache采用的缓存算法为LRU(Least Recently Used),即最近最少使用算法。这一算法的核心思想是当缓存数据达到预设上限后,会优先淘汰近期最少使用的缓存对象。

LruCache内部维护一个双向链表和一个映射表。链表按照使用顺序存储缓存数据,越早使用的数据越靠近链表尾部,越晚使用的数据越靠近链表头部;映射表通过Key-Value结构,提供高效的查找操作,通过键值可以判断某一数据是否缓存,如果缓存直接获取缓存数据所属的链表节点,进一步获取缓存数据。LruCache结构图如下所示,上半部分是双向链表,下半部分是映射表(不一定有序)。双向链表中value_1所处位置为链表头部,value_N所处位置为链表尾部。

LruCache 初始结构

LruCache读操作,通过键值在映射表中查找缓存数据是否存在。如果数据存在,则将缓存数据所处节点从链表中当前位置取出,移动到链表头部;如果不存在,则返回查找失败,等待新数据写入。下图为通过LruCache查找key_2后LruCache结构的变化。

LruCache 查找

LruCache没有达到预设上限情况下的写操作,直接将缓存数据加入到链表头部,同时将缓存数据键值与缓存数据所处的双链表节点作为键值对插入到映射表中。下图是LruCache预设上限大于N时,将数据M写入后的数据结构。

LruCache 未达预设上限,添加数据

LruCache达到预设上限情况下的写操作,首先将链表尾部的缓存数据在映射表中的键值对删除,并删除链表尾部数据,再将新的数据正常写入到缓存中。下图是LruCache预设上限为N时,将数据M写入后的数据结构。

LruCache 达预设上限,添加数据

线程安全的LruCache在读写操作中,全部使用锁做临界区保护,确保缓存使用是线程安全的。

LruCache在美团DSP系统的应用场景

在美团DSP系统中广泛应用键值存储数据库,例如使用Redis存储广告信息,服务可以通过广告ID获取广告信息。每次请求都从远端的键值存储数据库中获取广告信息,请求耗时非常长。随着业务发展,QPS呈现巨大的增长趋势,在这种高并发的应用场景下,将广告信息从远端键值存储数据库中迁移到本地以减少查询耗时是常见解决方案。另外服务本身的内存占用要稳定在一个安全的区间内。面对持续增长的广告信息,引入LruCache + 键值存储数据库的机制来达到提高系统性能,维持内存占用安全、稳定的目标。

LruCache + Redis机制的应用演进

在实际应用中,LruCache + Redis机制实践分别经历了引入LruCache、LruCache增加时效清退机制、HashLruCache满足高QPS应用场景以及零拷贝机制四个阶段。各阶段的测试机器是16核16G机器。

演进一:引入LruCache提高美团DSP系统性能

在较低QPS环境下,直接请求Redis获取广告信息,可以满足场景需求。但是随着单机QPS的增加,直接请求Redis获取广告信息,耗时也会增加,无法满足业务场景的需求。

引入LruCache,将远端存放于Redis的信息本地化存储。LruCache可以预设缓存上限,这个上限可以根据服务所在机器内存与服务本身内存占用来确定,确保增加LruCache后,服务本身内存占用在安全范围内;同时可以根据查询操作统计缓存数据在实际使用中的命中率。

下图是增加LruCache结构前后,且增加LruCache后命中率高于95%的情况下,针对持续增长的QPS得出的数据获取平均耗时(ms)对比图:

引入LruCache前后平均耗时

根据平均耗时图显示可以得出结论:

  1. QPS高于250后,直接请求Redis获取数据的平均耗时达到10ms以上,完全无法满足使用的需求。
  2. 增加LruCache结构后,耗时下降一个量级。从平均耗时角度看,QPS不高于500的情况下,耗时低于2ms。

下图是增加LruCache结构前后,且增加LruCache后命中率高于95%的情况下,针对持续增长的QPS得出的数据获取Top999耗时(ms)对比图:

引入LruCache前后tp999耗时

根据Top999耗时图可以得出以下结论:

  1. 增加LruCache结构后,Top999耗时比平均耗时增长一个数量级。
  2. 即使是较低的QPS下,使用LruCache结构的Top999耗时也是比较高的。

引入LruCache结构,在实际使用中,在一定的QPS范围内,确实可以有效减少数据获取的耗时。但是QPS超出一定范围后,平均耗时和Top999耗时都很高。所以LruCache在更高的QPS下性能还需要进一步优化。

演进二:LruCache增加时效清退机制

在业务场景中,Redis中的广告数据有可能做修改。服务本身作为数据的使用方,无法感知到数据源的变化。当缓存的命中率较高或者部分数据在较长时间内多次命中,可能出现数据失效的情况。即数据源发生了变化,但服务无法及时更新数据。针对这一业务场景,增加了时效清退机制。

时效清退机制的组成部分有三点:设置缓存数据过期时间,缓存数据单元增加时间戳以及查询中的时效性判断。缓存数据单元将数据进入LruCache的时间戳与数据一起缓存下来。缓存过期时间表示缓存单元缓存的时间上限。查询中的时效性判断表示查询时的时间戳与缓存时间戳的差值超过缓存过期时间,则强制将此数据清空,重新请求Redis获取数据做缓存。

在查询中做时效性判断可以最低程度的减少时效判断对服务的中断。当LruCache预设上限较低时,定期做全量数据清理对于服务本身影响较小。但如果LruCache的预设上限非常高,则一次全量数据清理耗时可能达到秒级甚至分钟级,将严重阻断服务本身的运行。所以将时效性判断加入到查询中,只对单一的缓存单元做时效性判断,在服务性能和数据有效性之间做了折中,满足业务需求。

演进三:高QPS下HashLruCache的应用

LruCache引入美团DSP系统后,在一段时间内较好地支持了业务的发展。随着业务的迭代,单机QPS持续上升。在更高QPS下,LruCache的查询耗时有了明显的提高,逐渐无法适应低平响的业务场景。在这种情况下,引入了HashLruCache机制以解决这个问题。

LruCache在高QPS下的耗时增加原因分析:

线程安全的LruCache中有锁的存在。每次读写操作之前都有加锁操作,完成读写操作之后还有解锁操作。在低QPS下,锁竞争的耗时基本可以忽略;但是在高QPS下,大量的时间消耗在了等待锁的操作上,导致耗时增长。

HashLruCache适应高QPS场景:

针对大量的同步等待操作导致耗时增加的情况,解决方案就是尽量减小临界区。引入Hash机制,对全量数据做分片处理,在原有LruCache的基础上形成HashLruCache,以降低查询耗时。

HashLruCache引入某种哈希算法,将缓存数据分散到N个LruCache上。最简单的哈希算法即使用取模算法,将广告信息按照其ID取模,分散到N个LruCache上。查询时也按照相同的哈希算法,先获取数据可能存在的分片,然后再去对应的分片上查询数据。这样可以增加LruCache的读写操作的并行度,减小同步等待的耗时。

下图是使用16分片的HashLruCache结构前后,且命中率高于95%的情况下,针对持续增长的QPS得出的数据获取平均耗时(ms)对比图:

引入HashLruCache前后平均耗时

根据平均耗时图可以得出以下结论:

  1. 使用HashLruCache后,平均耗时减少将近一半,效果比较明显。
  2. 对比不使用HashLruCache的平均耗时可以发现,使用HashLruCache的平均耗时对QPS的增长不敏感,没有明显增长。

下图是使用16分片的HashLruCache结构前后,且命中率高于95%的情况下,针对持续增长的QPS得出的数据获取Top999耗时(ms)对比图:

引入HashLruCache前后tp999耗时

根据Top999耗时图可以得出以下结论:

  1. 使用HashLruCache后,Top999耗时减少为未使用时的三分之一左右,效果非常明显。
  2. 使用HashLruCache的Top999耗时随QPS增长明显比不使用的情况慢,相对来说对QPS的增长敏感度更低。

引入HashLruCache结构后,在实际使用中,平均耗时和Top999耗时都有非常明显的下降,效果非常显著。

HashLruCache分片数量确定:

根据以上分析,进一步提高HashLruCache性能的一个方法是确定最合理的分片数量,增加足够的并行度,减少同步等待消耗。所以分片数量可以与CPU数量一致。由于超线程技术的使用,可以将分片数量进一步提高,增加并行性。

下图是使用HashLruCache机制后,命中率高于95%,不同分片数量在不同QPS下得出的数据获取平均耗时(ms)对比图:

HashLruCache分片数量耗时

平均耗时图显示,在较高的QPS下,平均耗时并没有随着分片数量的增加而有明显的减少,基本维持稳定的状态。

下图是使用HashLruCache机制后,命中率高于95%,不同分片数量在不同QPS下得出的数据获取Top999耗时(ms)对比图:

HashLruCache分片tp99耗时

Top999耗时图显示,QPS为750时,分片数量从8增长到16再增长到24时,Top999耗时有一定的下降,并不显著;QPS为1000时,分片数量从8增长到16有明显下降,但是从16增长到24时,基本维持了稳定状态。明显与实际使用的机器CPU数量有较强的相关性。

HashLruCache机制在实际使用中,可以根据机器性能并结合实际场景的QPS来调节分片数量,以达到最好的性能。

演进四:零拷贝机制

线程安全的LruCache内部维护一套数据。对外提供数据时,将对应的数据完整拷贝一份提供给调用方使用。如果存放结构简单的数据,拷贝操作的代价非常小,这一机制不会成为性能瓶颈。但是美团DSP系统的应用场景中,LruCache中存放的数据结构非常复杂,单次的拷贝操作代价很大,导致这一机制变成了性能瓶颈。

理想的情况是LruCache对外仅仅提供数据地址,即数据指针。使用方在业务需要使用的地方通过数据指针获取数据。这样可以将复杂的数据拷贝操作变为简单的地址拷贝,大量减少拷贝操作的性能消耗,即数据的零拷贝机制。直接的零拷贝机制存在安全隐患,即由于LruCache中的时效清退机制,可能会出现某一数据已经过期被删除,但是使用方仍然通过持有失效的数据指针来获取该数据。

进一步分析可以确定,以上问题的核心是存放于LruCache的数据生命周期对于使用方不透明。解决这一问题的方案是为LruCache中存放的数据添加原子变量的引用计数。使用原子变量不仅确保了引用计数的线程安全,使得各个线程读取的引用计数一致,同时保证了并发状态最小的同步性能开销。不论是LruCache中还是使用方,每次获取数据指针时,即将引用计数加1;同理,不再持有数据指针时,引用计数减1。当引用计数为0时,说明数据没有被任何使用方使用,且数据已经过期从LruCache中被删除。这时删除数据的操作是安全的。

下图是使零拷贝机制后,命中率高于95%,不同QPS下得出的数据获取平均耗时(ms)对比图:

HashLruCache分片数量耗时

平均耗时图显示,使用零拷贝机制后,平均耗时下降幅度超过60%,效果非常显著。

下图是使零拷贝机制后,命中率高于95%,不同QPS下得出的数据获取Top999耗时(ms)对比图:

HashLruCache分片数量耗时

根据Top999耗时图可以得出以下结论:

  1. 使用零拷贝后,Top999耗时降幅将近50%,效果非常明显。
  2. 在高QPS下,使用零拷贝机制的Top999耗时随QPS增长明显比不使用的情况慢,相对来说对QPS的增长敏感度更低。

引入零拷贝机制后,通过拷贝指针替换拷贝数据,大量降低了获取复杂业务数据的耗时,同时将临界区减小到最小。线程安全的原子变量自增与自减操作,目前在多个基础库中都有实现,例如C++11就提供了内置的整型原子变量,实现线程安全的自增与自减操作。

在HashLruCache中引入零拷贝机制,可以进一步有效降低平均耗时和Top999耗时,且在高QPS下对于稳定Top999耗时有非常好的效果。

总结

下图是一系列优化措施前后,命中率高于95%,不同QPS下得出的数据获取平均耗时(ms)对比图:

HashLruCache分片数量耗时

平均耗时图显示,优化后的平均耗时仅为优化前的20%以内,性能提升非常明显。优化后平均耗时对于QPS的增长敏感度更低,更好的支持了高QPS的业务场景。

下图是一系列优化措施前后,命中率高于95%,不同QPS下得出的数据获取Top999耗时(ms)对比图:

HashLruCache分片数量耗时

Top999耗时图显示,优化后的Top999耗时仅为优化前的20%以内,对于长尾请求的耗时有非常明显的降低。

LruCache是一个非常常见的数据结构。在美团DSP的高QPS业务场景下,发挥了重要的作用。为了符合业务需要,在原本的清退机制外,补充了时效性强制清退机制。随着业务的发展,针对更高QPS的业务场景,使用HashLruCache机制,降低缓存的查询耗时。针对不同的具体场景,在不同的QPS下,不断尝试更合理的分片数量,不断提高HashLruCache的查询性能。通过引用计数的方案,在HashLruCache中引入零拷贝机制,进一步大幅降低平均耗时和Top999耗时,更好的服务于业务场景的发展。

作者简介

王粲,2018年11月加入美团,任职美团高级工程师,负责美团DSP系统后端基础架构的研发工作。

崔涛,2015年6月加入美团,任职资深广告技术专家,期间一手指导并从0到1搭建美团DSP投放平台,具备丰富的大规模计算引擎的开发和性能优化经验。

霜霜,2015年6月加入美团,任职美团高级工程师,美团DSP系统后端基础架构与机器学习架构负责人,全面负责DSP业务广告召回和排序服务的架构设计与优化。文章来自:https://tech.meituan.com/lrucache_practice_dsp.html

AI技术在智能海报设计中的应用(转载)

背景

在视觉设计领域中,设计师们往往会因为一些简单需求付出相当多的时间,比如修改文案内容,设计简单的海报版式,针对不同机型、展位的多尺寸拓展等。这些工作需要耗费大量的时间、人力成本(5~6张/人日),但对设计师的进步成长起到的作用却非常有限。另一方面,精准营销是未来的大趋势,在大流量背景下,首页的海报资源展位需要展示“千人千面”的效果,这对海报的生产效率也提出了非常高的要求。所以,我们美团外卖技术团队尝试结合AI技术,来协助设计师避免这种低收益、高重复的任务,同时低成本、高效率、高质量地完成海报图片的生成。本文以Banner(横版海报)为例,介绍我们在海报设计AI技术结合方面所进行的一些探索和研究。

分析

什么是Banner的设计过程?我们尝试总结了对Banner设计的理解,Banner的设计过程是一系列的具备某种特征属性的素材图层的有序叠加过程。这里的特征属性既包括颜色、形状、纹理、主题等视觉属性,也包括位置、大小、贴边等空间属性。在这个过程中,哪些环节可以被机器算法所探索呢?文献[1]研究了如何调整图像的颜色分布,使杂志封面的视觉效果更加符合人眼的视觉特性;文献[2]以此为基础,引入了基于显著性识别的图像裁剪,并使用优化方法来解决布局问题。阿里巴巴的鹿班系统在去年双十一当天,生成1.7亿张Banner;京东内部也在孵化玲珑和莎士比亚系统,更加智能地设计文案和Banner。

图1 封面配色&布局设计[2]

在设计领域的一些子问题上,可以用算法来挖掘出数据背后的规律(如图1所示)。那么,能否构建一个完整的学习算法和处理系统,统一解决Banner设计中所有的子问题(配色、布局、搭配、生成)呢?

技术方案

素材图层是Banner的基础元素,其本身可以被特征化,同时组成Banner的若干元素间的叠加顺序可以被序列化,因此,算法实际是在学习“在什么时候,选择某种素材,放在哪里”。

图2 流程框架

如图2所示,为了解决这个问题,我们设计规划器、优化器、生成器来共同构建海报设计的学习与生产过程。其中:

  1. 规划器从数据中学习设计师对不同风格下的设计习惯与规律;
  2. 优化器基于美学质量和设计原则,对前者的输出结果做精细化调整;
  3. 最后,由生成器选取/生成素材并渲染成图;
  4. 素材库作为这三个环节的基础,负责素材管理素材标签化

素材库

如何提取素材图片的特征属性,这是比较典型的分类问题。在计算机视觉领域,传统方案是提取图像的颜色、梯度等低级语义特征[3],结合传统的分类器(LR、SVM等)来实现分类。近年来,基于深度学习的方法因为能表达更为复杂的语义特征,逐渐成为主流方法[4]。如图3所示我们提取传统的低级语义特征,以及基于CNN的高级语义特征,来共同完成素材特征属性提取

图3 素材库-特征提取

规划器

完成素材的数据化工作后,怎样学习Banner的设计过程?

作为一种生成模型,对抗生成网络(GAN)[5]在近年广为应用,其优势是可以端到端地训练图像生成模型,但在我们的应用场景下,GAN存在以下两个问题:

  1. GAN的过程更像是“黑盒”的过程:输入方面,虽然Conditional-GAN之类的方法可以实现某种程度有条件地可控生成,但对于Banner设计任务来说,其输入信息(文案、目标风格、主体信息)仍然过于复杂;
  2. 输出方面,GAN直接生成源数据(即图像),但非常缺乏解释性。我们需要的是更加直观、更有解释性的信息,比如素材的类型、颜色、轮廓、位置等。

在上文中有提到,Banner设计过程是素材图层依次叠加的过程。因此,我们可以用序列生成模型来拟合这个过程[6]。在建模过程中,我们把素材视作词汇(Word),海报视作句子(Sentence),词汇索引视为离散化的特征索引,素材叠加顺序就可以视为句子中的词顺序[7]。

图4 规划器-序列生成

图4是我们使用的序列生成模型,输入主体信息和目标风格,输出素材特征的序列。为了增强预测过程中多条路径结果的多样性,我们在监督性地训练每个时刻的输出之外,还引入了评估整个序列合理性的Object loss。如图5所示,借鉴SeqGAN的思想,Object loss可以由判别器来提供[8]。

图5 SeqGAN[8]

优化器

规划器预测素材的量化特征,为了确保最终成图符合美学标准,需要一个后处理的过程(图6)。我们用优化器来解决这个问题。从本质上讲,这是一个优化过程。通过和设计师们的沟通,我们设计了一些基于常规设计理念和美学标准的目标函数,动作集合包括移动、缩放、亮度调整等,结合优化方法,提升Banner的视觉效果。

图6 优化器

生成器

优化后的素材特征序列,通过生成器来渲染成图。如图7所示,对于素材库检索不到符合某种特征属性的素材的情况,我们设计了图像风格迁移的方法来实现图像特征迁移。这里的特征可以是颜色、形状、纹理等低级特征,也可以是某种语义化的视觉风格特征,对后者来说,可以将源图像的内容Content和目标图像的风格Style在某种特征空间(CNN中的某一层)里做融合,实现风格迁移[9,10]。

图7 素材生成

应用场景及功能拓展

“千人千面”的精准营销是未来营销策略的趋势,这对商品素材的丰富性提出了非常高的要求;从为商家赋能的角度来说,也需要为商家提供更多样的海报版式,这也要求系统具备海报风格的快速学习能力和拓展能力。对此,在常规设计风格的研究之外,我们从以下3个方面做了一些拓展研究。

主体图片加工

商品素材的丰富度与美学质量是精细化营销及海报美学质量非常重要的一环。其中最核心的要求是图像分割的能力[11,12]。以全卷积网络(FCN)为基础,如图8所示,我们采取以下几个在目标分割领域常见的技巧,来实现商品图片的目标分割:

  1. Encoder-Decoder结构
  2. 空洞卷积
  3. 多尺度特征融合
  4. Two-Stage微调网络

图8 图像语义分割&抠图(结构图部分参考DeepLab v3+[12])

这种基于语义分割方法的结果,在专业设计师人工评审质量的过程中,发现主体边缘有时会出现明显的锯齿感。经过分析,我们认为有以下两个原因:

  1. 语义分割模型把问题建模为一个“像素级分类过程”,每一个像素的类别都天然地被假设为“非此即彼”,大多数的Segmentation模型都采用Cross-Entropy作为损失函数;
  2. 因此,无论是从模型结构(CNN)还是从损失函数的角度来说,分割模型会更倾向于全局信息从而丢弃局部结构信息,导致只能得到边缘比较粗糙的分割结果。

为此,如图8所示,我们在图像分割的输出结果之外,结合了Image Matting方法:

  1. 对分割模型的输出结果做形态学变换,生成三值Trimap,分别表示前景区、背景区、未知区;
  2. 应用常规的Matting方法,比如Bayesian、Close-Form等,以原图像和Trimap图像为输入,输出前景图的Alpha通道;
  3. Matting可以使前景主体的边缘更加平滑,视觉质量更高(图9)。

图9 商品主体抠图

另外,基于图像美学质量评分模型,我们会优先选择质量分高的图片作为主体素材来源。对中低分的图片,未来可以考虑借鉴Cycle-GAN[13]的思想,设计基于半监督和GAN的图像增强网络,美化后再经过图像分割产生主体素材。

海报模板拓展

上述的常规设计风格的学习属于一种广义的设计风格,需要设计师先期投入很多精力做风格划分以及数据收集、处理。为了快速适配热点场景,我们借鉴图像检索技术(如图10所示),提取素材图片的CNN特征及颜色特征,使用欧式距离来度量素材相似度,这能节省人工打标签的成本,实现基于固定模板的自动拓展及生成(海报临摹)。

图10 素材图像检索与模板拓展

多分辨率拓展

在日常工作中,设计师在设计出Banner后,往往要花费很长时间对不同展位、不同版本、不同机型做多尺寸适配(如图11所示)。能否用算法来协助人工提效?在素材已经确定,并且相互的位置关系近乎确定的条件下做多分辨率适配,这本质上是一个优化问题,我们在上文布局优化器的基础上,增加元素的局部相对位置与全局绝对位置的拓扑关系作为目标函数。目前,系统支持在某个固定宽高比±30%范围内的任意分辨率适配,未来会进一步扩大适配范围。

图11 多分辨率拓展

总结

目前,我们的Banner智能设计系统为钻展(外卖首页广告位)、商家店铺装修等业务提供稳定的设计能力;素材加工等子能力也在为外卖、闪购等商品图片提供技术支持。后续我们会从扩展常规设计风格、语义相关的颜色及素材挖掘、自动解析数据、构建自评估学习闭环等方面继续研究,进一步提高算法的设计能力和适用性,尽可能协助设计师提高效率,降低高重复性工作的时间和经济成本。

参考文献

[1] A. Jahanian, J. Liu, D. Tretter, Q. Lin, E. O’Brien-Strain, S. Lee, N. Lyons, and J. P. Allebach. “Automatic Design of Colors for Magazine Covers”. In Proc. IS&T/SPIE Electronic Imaging, International Society for Optics and Photonics, 2013
[2] X. Y. Yang, T. Mei, Y. Q. Xu, Y. Rui, S. P. Li. “Automatic Generation of Visual-Textual Presentation Layout”. ACM Transactions on Multimedia Computing, Communications, and Applications, 2017
[3] David G. Lowe. “Distinctive Image Features from Scale-Invariant Keypoints”. International journal of computer vision, 2004
[4] Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton. “ImageNet Classification with Deep Convolutional Neural Networks”. NIPS, 2012
[5] I. Goodfellow, J. Pouget-Abadie, M. Mirza, B. Xu, D. Warde-Farley, S. Ozair, A. Courville, Y. Bengio. “Generative Adversarial Networks”. NIPS, 2014
[6] K. Kawakami. “Supervised Sequence Labelling with Recurrent Neural Networks”. Studies in Computational Intelligence, 2008
[7] T. Mikolov. “Statistical Language Models based on Neural Networks”. 2012
[8] L. Yu, W. Zhang, J. Wang, Y. Yu. “SeqGAN: Sequence Generative Adversarial Nets with Policy Gradient”. AAAI, 2017
[9] L.A. Gatys, A.S. Ecker, M. Bethge. “Image Style Transfer Using Convolutional Neural Networks”. CVPR, 2016
[10] Y. Li, M.Y. Liu, X. Li, M.H. Yang, J. Kautz. “A Closed-form Solution to Photorealistic Image Stylization”. ECCV, 2018
[11] J. Long, E. Shelhamer, T. Darrell. “Fully Convolutional Networks for Semantic Segmentation”. CVPR, 2015
[12] L.C. Chen, Y. Zhu, G. Papandreou, F. Schroff, H. Adam. “Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation”. ECCV, 2018
[13] J.Y. Zhu, T. Park, P. Isola, A. A. Efros. “Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks”. ICCV, 2017

作者简介

晓星,2017年6月加入美团,目前主要负责美团外卖图像内容挖掘、增强、生成方面的相关工作,致力于图像相关技术的积累及落地。

文章来自:https://tech.meituan.com/AI_in_Banner_Design.html