Coder Social home page Coder Social logo

articles's Introduction

articles's People

Contributors

zhenfeng-zhu avatar

Watchers

 avatar

articles's Issues

#日常 每个都有...

#日常 每个都有短期、中期和长期的todo。

  • 短期的近一个个双月完成,架构组的人负责
  • 中期近两个双月完成,给组内其他同学技术需求
  • 长期的可以放未来架构

微信读书-正版书籍小说免费阅读
https://weread.qq.com/

java类加载机制

java类加载机制

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编程语言发展的一大步

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

1 类的生命周期

一个类从被加载到内存到卸载出内存,整个生命周期包括:

  • 加载loading
  • 验证verification
  • 准备preparation
  • 解析resolution
  • 初始化initialization
  • 使用using
  • 卸载unloading

其中验证、准备和解析,这三步合起来又被称为连接(liking)。

加载、验证、准备、初始化和卸载,这五个阶段的顺序是确定的,而解析不一定。某些情况下,解析可能在初始化之后再开始,这就是java动态绑定。

java虚拟机规范中严格规定了有且只有5种情况必须对类立即进行初始化:

  • 遇到new、getstatic、putstatic或invokestatic这四个指令时,必须进行初始化。

    生成这几个指令的场景有:

    • 使用new实例化一个对象时;
    • 读取或者设置一个类的静态字段时;
    • 调用一个类的静态方法时。
  • 使用reflect包的方法对类进行反射时,也触发初始化。

  • 初始化一个类的时候,若父类还未初始化,则首先进行父类的初始化。

  • 包含main方法的那个类,虚拟机启动时会首先初始化这个主类。

  • 当使用jdk1.7的动态语言支持时,

接口的加载和类加载的过程稍有些不同:

  • 接口和类一样都有初始化过程,虽然接口里面不能有static{}语句块,但是编译器仍然会为接口生成<clinit>()类构造器,用于初始化接口中所定义的成员变量。

    java接口中的变量必须得是final静态的,但接口里最好不要有变量。

  • 当一个类初始化时,必须要求父类全部都已经初始化,但是接口在初始化时并不要求其父接口也全部初始化,只有在使用到父接口时才会初始化。

2 类加载的过程

2.1 加载

加载是类加载的一个阶段。在加载阶段,虚拟机要完成3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。

2.2 验证

验证阶段是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证

    验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

  • 元数据验证

    对字节码描述的信息进行语义分析,以保证其描述信息符合java语言规范。

  • 字节码验证

    最复杂的一个阶段,通过对数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  • 符号引用验证

    对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。目的是为了确保解析动作能正常执行。

验证阶段是非常重要的,但不是一定必要的阶段。如果所运行的代码都已经被反复使用和验证过,就可以通过jvm参数来关闭大部分类验证措施。

2.3 准备

准备阶段是给类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

此时进行内存分配的变量仅包括类变量,而不包含实例变量,实例变量将在对象实例化时随着对象一起分配在java堆中。

这里所说的初始值是指数据类型的零值,比如:

public static int v = 123;

那v的值在准备阶段是0,而不是123。

数据类型 零值 数据类型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char '\u0000' reference null
byte (byte)0

如果一个变量是常量,或者final类型的,那么在准备阶段就被初始化为常量值,如:

public static final int v = 123;

此时v的值在准备阶段是123。

2.4 解析

解析阶段是虚拟机将常量池的符号引用替换成直接引用的过程。

  • 符号引用:是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定加载到内存中。各种虚拟机的内存布局可以各不相同,但是能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存相关的,如果有了直接引用,那么引用的目标必定已经在内存中了。

解析主要是针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行的。

2.5 初始化

类初始化时类加载过程的最后一步。前面的阶段中,除了加载的时候,可以由用户指定自定义类加载器之外,别的都是由虚拟机主导控制。初始化阶段才真正执行类中定义的java代码。

在准备阶段变量已经被赋过零值,而初始化阶段是根据程序里面的来初始化类变量和其他资源,可以理解为执行类构造器的<clinit>()方法的过程。

  • <clinit>()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的。编译器的收集顺序是由语句在源文件中出现的顺序来决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。如:

    public class Test{
      static{
        i=0;                          //这句话是给变量赋值,可以编译通过
        System.out.println(i);        //这句话是要访问i,编译器会提示“非法向前引用”编译不过。   
      }
      static int i = 1;
    }
  • <clinit>()方法和类的构造函数不同,它不需要显示调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此虚拟机第一个被执行<clinit>()方法的类肯定是java.lang.Object。

  • 由上一条可以得出结论,父类中定义的静态语句块要早于子类的变量赋值操作。

  • <clinit>()方法对类或者接口不是必须的,如果一个类中没有静态语句块,也没有对变量进行赋值操作,那么编译器就不会为类生成<clinit>()方法。

  • 前面加载的时候有说到,接口中不能有静态语句块,但是可以有变量的初始化赋值操作。接口和类都会生成<clinit>()方法,到那时接口执行<clinit>()方法时不需要先执行父接口的<clinit>()方法,只有当父接口定义的变量被使用的时候,父接口才被初始化。另外,接口的实现类在初始化的时候,也不用执行接口的<clinit>()方法。

  • 虚拟机会保证一个类的<clinit>()方法在多线程的环境中被正确的加锁、同步。

3 类加载器

类加载阶段的加载阶段,即“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到jvm外部实现,使得应用程序自己可以决定如何获取所需要的类。实现这个动作的代码模块称为“类加载器”。

对于任意一个类来说,需要加载它的类加载器和其类本身来保证唯一性。如果同一个Class文件,被不同的类加载器加载了,那么产生的两个类是不相同的。

3.1 类加载器的分类

对于java虚拟机来说,只有两种不同的类加载器:

  • 启动类加载器 Bootstrap ClassLoader:C++实现的,虚拟机的一部分。
  • 其他类加载器:java语言实现,独立于jvm外部。全部继承抽象类java.lang.ClassLoader。

从java程序员的角度来看,有三种系统提供的类加载器:

  • 启动类加载器 Bootstrap ClassLoader
    负责将放在JAVA_HOEM/lib目录里的,或者是被-Xbootclasspath参数指定的路径中的,并且可以被虚拟机识别的类库加载到虚拟机内存中。
    启动类加载器无法被java程序直接引用。如果是用户在编写自定义类加载器的时候,需要把加载请求委派给启动类加载器,返回null就行了。
  • 扩展类加载器 Extension ClassLoader
    负责加载JAVA_HOEM/lib/ext目录中的,或者被java.ext.dirs系统变量指定的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器 Application ClassLoader
    这个类加载器是ClassLoader中的getSystemClassLoader()方法中的返回值,所以也称为系统类加载器,负责加载用户类路径上指定的类库。
    开发者可以直接使用此类加载器,如果应用程序没有自定义自己的类加载器,一般情况下这个就是程序的默认类加载器。

开发者可以自己编写一些自定义类加载器,用来进行特定类的加载。他们的关系是:

双亲委派模型

3.1 双亲委派模型

双亲委派模型要求除了最顶层的启动类加载器外,其余的加载器都得有自己的父类加载器。这里的类加载器的父子关系不是通过继承来实现,而是使用组合关系来复用复加载器的代码。

双亲委派模型的工作过程是:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是这样。因此,所有的类加载请求最终都会传送到最顶层的启动类加载器,只有当父加载器反馈自己无法加载这个加载请求的时候,子加载器才会尝试自己去加载。

使用这个模型的好处就是java类随着它的加载器一起具备了一种带有优先级的层次关系。比如java.lang.Object,无论哪个类加载器要加载这个类的时候,最终都是委派给最顶端的启动类加载器进行加载,因此Object类在程序的各个类加载器环境中都是同一个类。如果不使用这个模型的话,由各个类加载器自己加载,就会出现多个Object类。

双亲委派模型的逻辑实现代码很简单:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
  //首先检查请求的类是否已经被加载过
    Class c = findLoadedClass(name);
    if(c==null){
        if(parent!=null){
            c=parent.loadClass(name, false);
        }else{
            c=findBootstrapClassOrNull(name);
        }
        //如果父类加载器无法加载的时候,就调用本身的方法去加载
        if(c==null){
            c=findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}

3.2 破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,在java世界中,大部分加载器都遵循这个模型,在java历史上有三种比较大的被破坏情况。

  • 第一次是jdk1.2发布的时候。由于双亲委派模型是在1.2才引入的,java.lang.ClassLoader是在1.0的时候就存在了,面对在此之前的用户自定义类加载器的代码,java设计者添加了一个findClass方法来作为妥协。
  • 第二次是JNDI服务。双亲委派模型很好地解决了各个类加载器的基础类统一问题,但是当基础类又回来调用用户的代码就没办法了。所以引入了线程上下文 类加载器(Thread Context ClassLaoder)。
  • 第三次就是热更新热部署的时候。代表就是OSGi,每一个程序模块都有一个自己的类加载器,当需要更换一个模块的时候,就把模块连同其加载器一起换掉。此时的类加载器的结构成了网状结构了。

4 写在最后

把书从后面往前面看还是挺有意思的。

Geth私链的创建

在上一篇文章《Geth入门》中,主要讲了开发环境下以太坊geth客户端的使用。今天简单说下私链的配置。

genesis.json

{
    "config": {
          "chainId": 10,
          "homesteadBlock": 0,
          "eip155Block": 0,
          "eip158Block": 0
    },
    "coinbase"   : "0x0000000000000000000000000000000000000000",
    "difficulty" : "0x40000",
    "extraData"  : "",
    "gasLimit"   : "0xffffffff",
    "nonce"      : "0x0000000000000042",
    "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
    "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
    "timestamp"  : "0x00",
    "alloc": { }
}
参数 描述
nonce nonce就是一个64位随机数,用于挖矿
mixhash 与nonce配合用于挖矿,由上一个区块的一部分生成的hash
difficulty 设置当前区块的难度,如果难度过大,cpu挖矿就很难,这里设置较小难度
alloc 用来预置账号以及账号的以太币数量,因为私有链挖矿比较容易,所以我们不需要预置有币的账号,需要的时候自己创建即可以
coinbase 矿工的账号,随便填
timestamp 设置创世块的时间戳
parentHash 上一个区块的hash值,因为是创世块,所以这个值是0
extraData 附加信息,随便填,可以填你的个性信息
gasLimit 该值设置对GAS的消耗总量限制,用来限制区块能包含的交易信息总和,因为我们是私有链,所以填最大。
config Fatal: failed to write genesis block: genesis has no chain configuration :这个错误信息,就是说,你的json文件中,缺少config部分。看到这个信息,我们不需要把geth退回到v1.5版本,而是需要加上config部分。

创建创世区块

打开终端,输入以下命令,在当前目录下创建创世区块。

geth --datadir "./" init genesis.json

可以发现在当前目录新增了两个文件夹:

  • geth中保存的是区块链的相关数据
  • keystore中保存的是该链条中的用户信息

启动私链

geth --datadir "./" --nodiscover console 2>>geth.log
  • --datadir:代表以太坊私链的创世区块的地址
  • --nodiscover:私链不要让公链上的节点发现

也可将此命令写入一个shell文件中,每次启动的时候执行脚本就可以了。

输入此命令后,就可以进入到geth的控制台中了,在这里可以进行挖矿,智能合约的编写。

大型分布式网站的思考(五):高可用

网站页面能够完整的展现在用户面前,需要经过很多环节,任何一个环节出问题都可能会导致页面不可用。一般情况下为了提高系统可用性,会采用较昂贵的硬件设备。但是硬件故障也是常态的,高可用架构设计的一个主要目的就是保证在硬件故障时服务依然可用、数据能够保存且被访问。主要手段就是的冗余备份失效转移

1,分层模型

典型的分层模型是三层,即应用层、服务层和数据层,各层之间相对独立。

  • 应用层:负责具体业务逻辑的处理。
  • 服务层:可复用的服务。
  • 数据层:数据的存储和访问。

2,应用层高可用

当负载均衡设备通过心跳检测等手段检测到某台服务器不能用的时候,就将其从集群列表中剔除,并将请求分发到集群中其他可用的服务器上。

应用的一个显著特点就是无状态性。所谓的无状态的应用是指:应用服务器不保存业务的上下文信息,而仅根据每次请求提交的数据进行相应的业务逻辑处理,多个服务器之间完全对等,请求提交到任意服务器,处理结果都是一样的。

事实上,业务总是有状态的。将多次请求修改使用的上下文对象称作会话(Session)。session和cookie总是成对出现,不过可以简单理解为:cookie数据存放在客户的浏览器上,session数据放在服务器上。

在集群情况下,负载均衡服务器可能会将请求分发到任何一台服务器上,保证每次请求都获得正确的session主要有以下几种手段。

  1. session复制

    应用服务器开启web容器的session复制功能,在集群中的服务器之间同步session对象,使得每台服务器都保存所有用户的session信息。

    • 优点
      • 简单。
      • 从本机读取,速度快。
    • 缺点
      • 集群规模较大时,集群之间进行同步session,占用网络等。
      • 由于session在每台服务器都有备份,当大量用户访问时,服务器内存可能不够session使用。
  2. session绑定

    利用负载均衡的源地址hash算法实现,负载均衡服务器总是将来源于同一个ip的请求分发到同一台服务器上。这样在整个会话期间用户的所有请求都被这台服务器获取,这种方法又称为会话黏滞。

    • 缺点

      当某台服务器挂了,那该机器的session就不复存在,用户请求切换到其他机器后因为没有session而无法完成业务处理。

  3. 利用cookie记录session

    将session记录通过cookie保存在客户端,每次请求服务器时,都将session放在请求中发送给服务器,服务器处理完之后再将修改的session响应给客户端。

    • 优点
      • cookie简单易用,可用性高,支持应用服务器线性伸缩,大部分session信息较小。
    • 缺点
      • cookie大小限制,能记录信息有限。
      • 每次请求都要传输cookie,影响性能。
      • 如果用户关闭cookie,访问就会不正常。
  4. session服务器

    利用独立部署的session服务器集群统一管理session。这种解决方案实际上是将应用服务器的状态分离,分为无状态应用服务器和有状态的session服务器。

    对于有状态的session服务器,一种比较简单的方法是利用分布式缓存、数据库等,在这些产品上进行包装,使其符合session的存储和访问要求。如果业务场景对session管理有比较高的要求,比如利用session服务集成单点登录(SSO)等,需要开发专用session服务管理平台。

3,服务层的高可用

可复用的服务模块通常独立分布式部署,被具体应用远程调用(RPC)。可复用的服务也是无状态的服务,因此可以使用类似负载均衡的失效转移策略。

除此之外还有如下几点策略:

  1. 分级管理

    运维上将服务器分级管理,核心应用和服务使用更好的硬件,运维响应速度也格外迅速。

    同时在部署上也进行必要隔离,防止故障的连锁反应。低优先级的服务通过启动不同的线程或者部署在不同的虚拟机上进行隔离,而高优先级的服务则需要部署在不同的物理机上,核心服务和数据甚至要部署在不同的数据中心。

  2. 超时设置

    由于服务器宕机、线程死锁等原因,可能导致应用程序对服务端的调用失去响应,进而导致用户长期得不到响应,同时还占用应用程序的资源,不利于将访问转移到正常的服务器上。

    因此需要设置服务调用的超时时间,一旦超时抛出异常。

  3. 异步调用

    应用对服务的调用通过消息队列等异步方式完成,避免一个服务失败导致整个应用请求失败的情况。

    对于获取用户信息的调用,采用异步的方式会延长响应时间。对于那些必须确认调用成功才能进行下一步的服务也不适合异步调用。

  4. 服务降级

    在访问高峰期,比如双十一等,服务器可能因为大量并发访问导致性能下降,为了保证核心应用和功能的正常运行,需要对服务进行降级。主要有两种手段:

    • 拒绝服务
      • 拒绝低优先级应用的调用,减少调用并发数。
      • 随机拒绝部分请求,节省资源,让另一部分请求成功。
    • 关闭功能
      • 关闭部分不重要的服务。
      • 服务内关闭部分不重要的功能。
  5. 幂等性设计

    应用调用服务失败后,会将请求重新发送到其他服务器,但是这个失败可能是虚假的失败。如服务处理成功了,但是因为网络故障导致应用没有收到相应,这时应用重新提交请求就导致服务重复调用。

    服务的重复调用是无法避免的,应用层也不关系服务是否真的失败,只要没收到成功响应,就可以认为调用失败并重试调用。因此必须在服务层保证重复调用和一次调用的结果相同,即服务具有幂等性。

    有些服务天然具有幂等性,如性别的设置。但是对于转账之类的交易,就比较复杂,需要通过交易编号等信息进行校验。

4,数据存储高可用

由于数据存储服务器上保存的数据一般都不相同,当某一台服务器宕机后,数据访问请求不能任意切换到集群中的其他服务器上。保证数据存储高可用的手段主要是数据备份和失效转移。

数据备份:保存的数据有多个副本,任意副本的失效都不会导致数据的永久丢失,从而实现数据完全的持久化。

失效转移:当一个数据副本不可访问时,可以快速切换数据的其他副本,保证系统可用性。

4.1,CAP

分布式系统有三个特性:

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本),换句话就是说,任何时刻,所用的应用程序都能访问得到相同的数据。
  • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性),换句话就是说,任何时候,任何应用程序都可以读写数据。
  • 分区容错性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择,换句话说,系统可以跨网络分区线性的伸缩和扩展。

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

高可用、数据一致是很多系统设计的目标,但是分区又是不可避免的事情,因此一般情况下满足P,C和A权衡考虑满足。

4.2,数据一致性

数据的一致性一般分为如下几点:

  • 数据强一致

    各个副本的数据在物理存储中总是一致的;数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定是没有被更新,而不是处于不确定的状态。

  • 数据用户一致

    数据在物理存储中各个副本的数据可能是不一致的,但是终端访问的时,通过纠错和校验机制,可以确定一个一致性的且正确的数据给用户。

  • 数据的最终一致

    这是一致性最弱的一种,即物理存储的数据可能是不一致的,终端用户访问到的数据也可能是不一致的(同一用户连续访问,结果不同;或者不同用户同时访问,结果不同),但是系统经过一段时间的调整,数据最终会一致。

因为很难满足数据的强一致性,综合考虑一般是达到存储系统的用户一致,保证最终用户访问数据的正确性。

4.3,数据备份

冷备

定期将数据复制到某种存储介质中并物理存档保管,如果系统存储损坏,就从冷备的设备中恢复。

  • 优点

    • 简单,成本和技术难度较低。
  • 缺点

    • 不能保证数据的最终一致,由于是定期备份,因此备份设备中的数据比系统中的陈旧,如果系统数据丢失,则从上个备份点之后的数据都丢失了。
    • 不能保证数据可用性,从冷备存储恢复数据需要较长时间,这段时间无法访问数据,系统不可用。

热备

  • 异步热备

多份数据副本的写入操作异步完成,应用程序收到数据服务系统的写入操作响应时,只写入了一份,存储系统会异步写入其他副本。

在异步写入下,存储服务器分为主存储服务器(master)和从存储服务器(slave),应用程序正常情况下只连接master,然后通过异步线程写入到slave中。

  • 同步热备

多份数据副本写入操作同步完成。

为了提高性能,应用程序并发的向多个存储服务器同时写入,然后等待所有存储服务器都返回成功之后,再通知写入成功。

传统的关系型数据库几乎都提供了数据实时同步备份的机制,关系型数据库热备机制就是通常所说的master-slave机制,实践中通常使用读写分离的方法来访问数据库。

4.4,失效转移

失效转移操作由三部分组成:失效确认、访问转移、数据恢复。

  • 失效确认

    系统确认一台服务器宕机的手段有两种:

    • 心跳检测
    • 应用程序访问失败,访问失败之后还要再发一次心跳,避免误判
  • 访问转移

    路由到可用的存储服务器

  • 数据恢复

    因为某台服务器宕机,所以存储的副本数会减少,必须将副本的数目恢复到系统的设定值,否则,再有服务器宕机时,可能出现无法访问转移,数据永久丢失。

5,待续

近期沉迷于《当你沉睡时》,周末的两天在去韶关的大巴上把剧给刷完了。不得不说,现在的韩剧已经不是当年那种车祸,失忆,兄妹一类的剧情了,题材越来越新颖,部分还反应社会现实,值得我们学习。国民初恋——秀智好漂亮啊啊啊啊啊啊啊啊!

刷完了剧又该好好学习了,要是我能在梦里学会这些知识,或者提前梦到自己的未来多好。

Stay Hungry, Stay Foolish.

大型分布式网站的思考(三):缓存

网站优化的第一定律:优先考虑使用缓存优化性能

1,基本原理

缓存的本质是一个内存hash表。

数据缓存以一对key,value的形式存在hash表中,hash表数据读写的时间复杂度为o(1)。把hashcode理解为对象的唯一键,然后通过key的hashcode来计算hash表的索引下标。

2,合理使用缓存

使用缓存对提高系统性能有很多好处,但是事务都有两面性,不合理的使用缓存,对于系统来说是个灾难。因此在使用缓存的时候,也要经过深思熟虑。

  • 不缓存频繁修改的数据

  • 及时清理没有热点访问的数据

  • 容忍数据不一致或脏读

  • 避免缓存雪崩

    缓存雪崩是指当缓存服务崩溃时,所有的请求都会去读数据库,数据库会因为承受不了压力而宕机,进而导致整个网站不能用。

    解决方案主要有两种:

    • 缓存热备:当某台缓存服务器宕机时,将缓存切换到热备服务器上。

    • 分布式缓存集群:将缓存数据分布到多台机器上,当一台宕机时,只有部分缓存数据丢失,从数据库中读取这些数据不会造成很大影响.

      第一种方案显然有违缓存初衷,因为缓存根本就不应该当做一个可靠的数据源来使用。

  • 缓存预热

    缓存中存放的都是热点数据,热点数据又是缓存系统通过lru(最近最久未用算法)对不断访问的数据淘汰筛选出来,这个过程需要花费很长的时间。因此可以在缓存系统启动时就将一些热点数据准备好,这种手段就叫做缓存预热。

  • 缓存穿透

    如果因为不恰当的业务,或者恶意攻击持续高并发的请求某个缓存中不存在的数据,所有的请求就会到数据库中,可能造成数据库崩溃。可以将不存在的数据也缓存起来,如其value设置为null。

3,分布式缓存

分布式缓存是指缓存部署在多个服务器组成的集群中,以集群的方式提供缓存服务。分布式缓存有两种架构,一种是以jboos cache为代表的需要更新同步的分布式缓存,一种是以memcached为代表的互不通信的分布式缓存。

jboos cache是集群中所有服务器都保存相同的数据,当某台服务器有数据更新时,通知集群中的其他服务器更新缓存数据。jboos cache一般将应用程序和缓存部署在同一台服务器上,应用程序可以很快的从本地获取缓存,但是当规模很大的时候更新缓存就会需要很大的代价。一般用于企业网站。

memcached采用一种集中式缓存集群管理,也被称作互不通信的分布式架构方式。缓存与应用分离部署,缓存系统部署在一组专门的服务器上,应用通过一致性hash等路由算法选择缓存服务器远程访问缓存数据,缓存服务器之间不进行通信,缓存集群的规模可以很容易的实现扩容,具有很好的可伸缩性。

4,写在最后

当网站遭遇性能瓶颈的时候,第一个想到的就是缓存。在整个网站应用中,缓存几乎无处不在。既存在于浏览器中,也存在于应用服务器和数据库服务器;既可以对数据缓存,也可以对文件缓存,还可以对整个页面进行缓存。合理使用缓存,对网站性能优化意义重大。

ISO-8583报文设计思路

ISO-8583报文设计思路

很早就听说了8583报文,之前看银联协议规范也是一知半解,这几天深入研究了一下,参考了一些网上的资料,豁然开朗。

金融行业其实涉及到的数据内容并不是成千上万,无法统计,恰恰相反,是比较少的。

我们都可以数得过来,如交易类型、帐号、帐户类型、密码、交易金额、交易手续费、日期时间、商户代码、2磁3磁数据、交易序列号等,把所有能够总结出来的都总结起来也就100个左右的数据。

简单设计ISO8583

定义128个字段,将所有能够考虑到的类似上面提到的”账号“等金融数据类型,按照一个顺序排起来,分别对应128个字段中的一个字段。每个数据类型占固定的长度,这个顺序和长度我们都事先定义好。这样就简单了,要发送一个报文时,就将128个字段按照顺序接起来,然后将接起来的整串数据包发送出去。

任何金融软件收到ISO8583包后,直接按照我们定义的规范解包即可,因为整个报文的128个字段从哪一位到哪一位代表什么,大家都知道,只要知道你的数据包是ISO8583包即可,我们都已经定义好了。比如第1个字段是“交易类型”,长度为4位,第2个字段位是“帐号”,为19位等等。接收方就可以先取4位,再取接着的19位,依次类推,直到整个数据包128个字段都解完为止。

简单设计产生的问题

上面这种做法简单粗暴,基本上就可以满足需要了。不过我们有几个问题要思考下:

  • 我怎么知道每个字段的数据类型呢,是数字还是字符?
  • 每个传送的报文都把128个字段都传过去,那网络带宽能够承受得了,有时候我可能只需要其中5个字段,结果多收到了123个无用的字段。
  • 如果我某些字段的长度不固定,属于变长怎么办?

解决思路

问题一解决思路

第一个问题简单,在定义ISO8583时除了定义每个字段表示什么,还规定其内容是数字或是字符等即可。

考虑可能出现的类型不过有以下几种:字母、数字、特殊字符、年月日等时间、二进制数据。比如我对128个字段中的“商户类型”字段定义其长度是15,同时定义其类型为字母。再精细点,如果“商户类型”里面的数据同时包括数字和字母呢?那我们就定义其类型为字母也可,为数字也可,即一个字段可以同时属于多个类型。

问题二解决思路

第二个问题稍微复杂点。其本质就是如果我只传128个字段的5个字段,接收方怎么知道我传了哪几个字段给它了。

思路一

如果我们把剩下的123全部填成0或其他特殊标识,标明该字段不需要使用?这种处理方法没有半点用处,没有解决网络带宽的本质问题,还是要传128个字段。

思路二

换个思路,我在报文前面加上个报文头,头里面包含的信息能够让别人知道只传了5个字段。

  • 怎样设计这个报文头?

    可以这样,我们用16个字节,即128个bit(一个字节等于8bit)来表示128个字段中的某个字段是否存在。每个bit在计算机的二进制里面不是1就是0,如果是1就表示对应的字段在本次报文中存在,如果是0就是不存在。

    如果别人接收到了ISO8583报文,可以先根据最前面的报文头,就知道紧接着报文头后面的报文有哪些字段,没有哪些字段了。

    比如,我要发送5个字段,分别属于128个字段中的第2、3、6、8、9字段,我就可以将128bit的报文头填成011001011000000000………..,一共128个bit,后面就全是0了。注意其中第2、3、6、8、9位为1,其他都为0。

  • 怎样组织报文?

    先放上这128bit,即16个字节的头,然后在头后面放2、3、6、8、9字段,这些字段紧挨在一起,3和6之间也不需要填上4、5这两个字段了。接收方收到这个报文,它会根据128bit的报文头来解包,它自然知道把第3个字段取出后,就直接在第3字段的后面取第6个字段,每个字段的长度在ISO8583里面都定义好了,很轻松就把数据包解出来了。

位图

为了解决上面的第二问题,我们只是在报文中增加了16个字节的数据,就轻松搞定了,我们把这16个字节称为bit map,即位图,用来表示某个位是否存在。

优化

再稍微优化一下,考虑到很多时候报文不需要128个字段这么多,其一半64个字段都不一定能够用完。那可以将报文头由128bit减到64bit,只有在需要的时候才把剩下的64bit放到报文里面,这样报文长度又少了8个字节。

我们把ISO8583的128个字段中最常见的都放到前64个字段中,那我们可以将处理缩小一倍。这样我一般发送报文时只需发送64bit,即一个字节的报文头,再加上需要的几个字段就可以了。

如果有些报文用到64到128之间的字段时,把64bit报文头的第一位bit用来代表特殊含义,如果该bit为1,则表示64bit后面跟了剩下的64bit报文头;如果第一位bit为0,则表示64bit后面没有跟剩下的64bit报文头,直接是128个字段中的报文了。

因为报文头第二个64bit属于有时候有,所以我们叫它Extended bit map扩展位图,相应的报文头最开始的64bit我们叫它Primary bit map主位图。我们直接把扩展位图固定放到128个字段的第一个字段,而主位图每个数据包都有,就强制性放在所有128个字段的前面,即报文头,并不归入128个字段中去。

问题三解决思路

思路一

预先定义一个我们认为比较大的位数。

如果第2个字段是“帐号”,是不定长的,可能有的银行帐号是19位,有的是16位等。我们可以规定第2个字段是25位,这下足够将19和16的情况都包含进来,但是如果以后出现了30位的怎么办?那我们现在将字段定为100位。以后超过100位怎么办,况且如果你只有19位的帐号,我们定义了100位,那81位的数据不是浪费了网络的带宽。看来预先定义一个我们认为比较大的位数是不太好的。

思路二

可以在字段开头加上长度的方式来解决。

对于第2个字段“帐号”,在字段的开头加上“帐号”的长度。比如帐号是0123456789,一共10位,变成100123456789,注意前面多了个10,表示后面的10位为帐号。

接收方收到该字段后,它知道ISO8583规定第2个字段“帐号”是变长的,所以会先取前面的2位出来,获取其值,此时为长度,然后根据该长度值知道应该拷贝该字段后面哪几位数据,才是真正的帐号。

在规范里面如果定义某个字段的属性是“LLVAR”,其中的LL表示长度,VAR表示后面的数据,两个LL表示两位长,最大是99,如果是三位就是“LLLVAR”,最大是999。这样看我们定义的ISO8583规范文档时直接根据这几个字母就理解某个变长字段的意思了。

完善

剩下的工作就简单了,收集金融行业可能出现的数据字段类型,分成128个字段类型,如果没有到128个这么多就先保留一些下来,另外考虑到有些人有特殊的要求,我们规定可以将128个字段中的几个字段你自己来定义其内容,也算是一种扩展了。

docker常用命令

docker常用命令

docker

  1. 获取镜像

    docker pull

  2. 新建并启动

    docker run

  3. 列出镜像

    docker image ls

    docker images

  4. 删除虚悬镜像

    docker image prune

  5. 删除本地镜像

    docker iamge rm

  6. 查看应用信息

    docker logs

dockerfile

一般步骤:

  • 在一个目录里,新建一个文件,命名为Dockerfile
  • 在Dockerfile的目录内,执行docker build

常用指令

  1. FROM 指定基础镜像,且是第一条命令

  2. RUN 执行命令

    shell格式

    exec格式

  3. COPY和ADD指令是复制文件

  4. CMD指令和RUN类似,容器启动命令

    shell格式

    exec格式

    参数列表格式

  5. ENV 设置环境变量

  6. EXPOSE 声明对外暴露的端口

  7. WORKDIR 指定工作目录

compose

两个重要的概念

  • service 服务:一个应用的容器,实际上可以包括若干运行相同镜像的实例。
  • project 项目:由一组关联的容器组成一个完整业务单元,在docker-compose.yml文件中定义。

一般步骤:

  • 在一个项目目录里,新建一个Dockerfile

  • 新建一个文件docker-compose.yml

    模板格式

    version: 3.0
    services:
    	web:
    		build: .
    		ports:
    			- "5000:5000"
    			
    	redis:
    		images: "redis:alpine"
  • docker-compose up运行项目

常用命令:

  1. docker-compose build 重新构建项目中的服务容器
  2. config 验证compose文件格式是否正确
  3. down 停止up命令所启动的容器
  4. images 列出compose文件中包含的镜像
  5. exec 进入指定的容器
  6. kill 强制停止服务容器
  7. ps 列出目前所有容器
  8. rm 删除停止状态的容器
  9. top 显示所有容器的进程

compose模板文件:

每个服务都必须通过image指令指定镜像或者build指令(需要dockerfile)来构建生成的镜像。

  1. build

    指定dockerfile所在的文件夹路径,compose将会利用它来自动构建这个镜像,然后使用。

  2. depends_on

    解决容器的依赖和先后启动问题。但是不会等待完成启动之后再启动,而是在他们启动之后就去启动。

  3. environment

    设置环境变量,在这里指定程序或者容器启动时所依赖的环境参数。

  4. expose

    指定暴露的端口,只被连接的服务访问。

  5. image

    指定镜像名称,如果本地不存在则去拉取这个镜像。

  6. labels

    为容器添加docker元数据信息,即一些辅助说明。

  7. ports

    暴露端口信息,宿主端口:容器端口,或者只指定容器端口。

kotlin 拾遗

kotlin拾遗

elvis操作符

其实就是用于null检查中的,用?来代替

范围表达式

..

in

!in

解构

将一个对象解构为多个变量,类似于js的解构

val (name, age) = person

也可用在for循环中:

for((a,b) in collection){...}

示例:

从一个函数中返回两个值:

data class Result(val result: Int, val status: Status)
fun function(...): Result{
	return Result(result, status)
}

val (result, status)=function(...)

默认参数和命名参数

fun reformat(str: String, a1: Int=1, a2: Boolean=true)

调用的时候可以:

reformat("hello")

reformat("hello", a2=false)

sealed类

sealed类用来表示对类阶层的限制,可以限定一个值只允许是某些指定的类型之一,而不允许是别的类型。

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

使用封闭类的主要好处在于, 当使用 when expression 时, 可以验证分支语句覆盖了所有的可能情况, 因此就不必通过 else 分支来处理例外情况. 但是, 这种用法只适用于将 when 用作表达式(使用它的返回值)的情况, 而不能用于将 when 用作语句的情况.

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    // 不需要 `else` 分支, 因为我们已经覆盖了所有的可能情况
}

companion

静态方法

class Constants{
	companion object{
		val BASE_URL = 'http://example.com'
	}
}

d调用

Constants.Companion.getBASE_URL()

补翻译

通道

。。。。

接收数据

当你需要处理一系列相关值的时候,channel可能非常有用:

suspend fun handleTemperatureStream() {
  val stream = vertx.eventBus().consumer<Double>("temperature")
  val channel = stream.toChannel(vertx)

  var min = Double.MAX_VALUE
  var max = Double.MIN_VALUE

  // Iterate until the stream is closed
  // Non-blocking
  for (msg in channel) {
    val temperature = msg.body()
    min = Math.min(min, temperature)
    max = Math.max(max, temperature)
  }

  // The stream is now closed
}

他也可以用于解析协议。我们将构建一个非阻塞的http请求解析器来展示通道的功能。

我们将依靠RecordParser将以\r \n分割的buffer流进行分割。

这是解析器的初始版本,它只处理http的请求行。

val server = vertx.createNetServer().connectHandler { socket ->

  // The record parser provides a stream of buffers delimited by \r\n
  val stream = RecordParser.newDelimited("\r\n", socket)

  // Convert the stream to a Kotlin channel
  val channel = stream.toChannel(vertx)

  // Run the coroutine
  launch(vertx.dispatcher()) {

    // Receive the request-line
    // Non-blocking
    val line = channel.receive().toString().split(" ")
    val method = line[0]
    val uri = line[1]

    println("Received HTTP request ($method, $uri)")

    // Still need to parse headers and body...
  }
}

解析请求行就像在channel上调用receive一样简单。

下一步是通过接收块来解析http头,直到我们得到一个空块为止。

// Receive HTTP headers
val headers = HashMap<String, String>()
while (true) {

  // Non-blocking
  val header = channel.receive().toString()

  // Done with parsing headers
  if (header.isEmpty()) {
    break
  }

  val pos = header.indexOf(':')
  headers[header.substring(0, pos).toLowerCase()] = header.substring(pos + 1).trim()
}

println("Received HTTP request ($method, $uri) with headers ${headers.keys}")

最后,我们通过处理可选的请求体来终止解析器。

// Receive the request body
val transferEncoding = headers["transfer-encoding"]
val contentLength = headers["content-length"]

val body : Buffer?
if (transferEncoding == "chunked") {

  // Handle chunked encoding, e.g
  // 5\r\n
  // HELLO\r\n
  // 0\r\n
  // \r\n

  body = Buffer.buffer()
  while (true) {

    // Parse length chunk
    // Non-blocking
    val len = channel.receive().toString().toInt(16)
    if (len == 0) {
      break
    }

    // The stream is flipped to parse a chunk of the exact size
    stream.fixedSizeMode(len + 2)

    // Receive the chunk and append it
    // Non-blocking
    val chunk = channel.receive()
    body.appendBuffer(chunk, 0, chunk.length() - 2)

    // The stream is flipped back to the \r\n delimiter to parse the next chunk
    stream.delimitedMode("\r\n")
  }
} else if (contentLength != null) {

  // The stream is flipped to parse a body of the exact size
  stream.fixedSizeMode(contentLength.toInt())

  // Non-blocking
  body = channel.receive()
} else {
  body = null
}

println("Received HTTP request ($method, $uri) with headers ${headers.keys} and body with size ${body?.length() ?: 0}")

发送数据

使用channel发送数据也非常直接:

suspend fun sendChannel() {
  val stream = vertx.eventBus().publisher<Double>("temperature")
  val channel = stream.toChannel(vertx)

  while (true) {
    val temperature = readTemperatureSensor()

    // Broadcast the temperature
    // Non-blocking but could be suspended
    channel.send(temperature)

    // Wait for one second
    awaitEvent<Long> { vertx.setTimer(1000, it)  }
  }
}

SendChannel#send 和 WriteStream#write都是非阻塞操作。不像当channel满的时候SendChannel#send可以停止执行,而等效WriteStream#writ的无channel操作可能像这样:

// Check we can write in the stream
if (stream.writeQueueFull()) {

  // We can't write so we set a drain handler to be called when we can write again
  stream.drainHandler { broadcastTemperature() }
} else {

  // Read temperature
  val temperature = readTemperatureSensor()

  // Write it to the stream
  stream.write(temperature)

  // Wait for one second
  vertx.setTimer(1000) {
    broadcastTemperature()
  }
}

延迟,取消和超时

借助于vert.x的定时器,vert.x的调度器完全支持协程的delay函数:

launch(vertx.dispatcher()) {
  // Set a one second Vertx timer
  delay(1000)
}

定时器也支持取消:

val job = launch(vertx.dispatcher()) {
  // Set a one second Vertx timer
  while (true) {
    delay(1000)
    // Do something periodically
  }
}

// Sometimes later
job.cancel()

取消是合作的。

你也可以使用withTimeout函数安排超时。

launch(vertx.dispatcher()) {
  try {
    val id = withTimeout<String>(1000) {
      return awaitEvent<String> { anAsyncMethod(it) }
    }
  } catch (e: TimeoutCancellationException) {
    // Cancelled
  }
}

Vert.x支持所有的协程构建器:launch,async和runBlocking。runBlocking构建器不能再vert.x的时间循环线程中使用。

协程的互操作性

vert.x集成协程被设计为完全可以和kotlin协程互操作。

kotlinx.coroutines.experimental.sync.Mutex被执行在使用vert.x调度器的事件循环线程。

RxJava的互操作性

虽然vertx-lang-kotlin-coroutines模块没有与RxJava特定集成,但是kotlin协程提供了RxJava的集成。RxJava可以和vertx-lang-kotlin-coroutines很好的协同工作。

你可以阅读响应流和协程的指南。

#日常 (1)...

#日常
(1)首先以满足管理和运营的需要为前提,寻找需要追溯的事件,或者称为关键业务时刻。(2)根据这些需要追溯,寻找足迹以及相应的关键业务时刻对象。(3)寻找“关键业务时刻”对象周围的“人-事-物”对象。(4)从“人-事-物”中抽象出角色。(5)把一些描述信息用对象补足。

#日常 te... · Issue #40 · zhenfeng-zhu/articles
#40

Spring Boot操作Redis

Spring-data-redis为spring-data模块中对redis的支持部分,简称为“SDR”,提供了基于jedis客户端API的高度封装以及与spring容器的整合,

jedis客户端在编程实施方面存在如下不足:

  • connection管理缺乏自动化,connection-pool的设计缺少必要的容器支持。
  • 数据操作需要关注“序列化”/“反序列化”,因为jedis的客户端API接受的数据类型为string和byte,对结构化数据(json,xml,pojo等)操作需要额外的支持。
  • 事务操作纯粹为硬编码
  • pub/sub功能,缺乏必要的设计模式支持,对于开发者而言需要关注的太多。

1 spring-data-redis特性

  1. 连接池自动管理,提供了一个高度封装的“RedisTemplate”类
  2. 针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口
    • ValueOperations:简单K-V操作
    • SetOperations:set类型数据操作
    • ZSetOperations:zset类型数据操作
    • HashOperations:针对map类型的数据操作
    • ListOperations:针对list类型的数据操作
  3. 提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations:
    • BoundValueOperations
    • BoundSetOperations
    • BoundListOperations
    • BoundSetOperations
    • BoundHashOperations
  4. 将事务操作封装,有容器控制。
  5. 针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer)
    • JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。
    • StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。
    • JacksonJsonRedisSerializer:jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。
    • OxmSerializer:提供了将javabean与xml之间的转换能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存储的数据将是xml工具。不过使用此策略,编程将会有些难度,而且效率最低;不建议使用。
  6. 基于设计模式,和JMS开发思路,将pub/sub的API设计进行了封装,使开发更加便捷。
  7. spring-data-redis中,并没有对sharding提供良好的封装,如果你的架构是基于sharding,那么你需要自己去实现,这也是sdr和jedis相比,唯一缺少的特性。

2 引入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3 配置

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=root
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0

其中spring.redis.database的配置通常使用0即可,Redis在配置的时候可以设置数据库数量,默认为16,可以理解为数据库的schema

3.1 StringRedisTemplate

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@Test
	public void testRedis(){
		stringRedisTemplate.opsForValue().set("myKey", "hello redis");
		Assert.assertEquals("hello redis", stringRedisTemplate.opsForValue().get("myKey"));
	}

}

通过上面这段极为简单的测试案例演示了如何通过自动配置的StringRedisTemplate对象进行Redis的读写操作,该对象从命名中就可注意到支持的是String类型。如果有使用过spring-data-redis的开发者一定熟悉RedisTemplate<K, V>接口,StringRedisTemplate就相当于RedisTemplate<String, String>的实现。

除了String类型,我们还经常会在Redis中存储对象。

3.2 RedisTemplate<Object, Object>

3.2.1 新建User类

@Data
@AllArgsConstructor
public class User implements Serializable{
    private static final long serialVersionUID = 1L;
    private Integer id;
    private String username;
    private Integer age;
}

3.2.2 创建UserRepository

@Repository
public class UserRepository {
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    //
    @Resource(name = "redisTemplate")
    ValueOperations<Object, Object> valOps;

    /**
     * 保存
     * @param user
     */
    public void save(User user) {
        int id = user.getId();
        valOps.set(id, user);
    }

    /**
     * 获取
     * @param id
     * @return
     */
    public User getUserById(int id) {
        return (User) valOps.get(id);
    }

}

@resource注解和@Autowired一样,也可以标注在字段或属性的setter方法上,但它默认按名称装配。名称可以通过@resource的name属性指定,如果没有指定name属性,当注解标注在字段上,即默认取字段的名称作为bean名称寻找依赖对象,当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻找依赖对象。

3.2.3 单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
	@Autowired
	private UserRepository userRepository;

	@Test
	public void testRedis(){
		User user=new User(1, "hello", 12);
		userRepository.save(user);
		Assert.assertEquals("hello", userRepository.getUserById(1).getUsername());
	}
}

4 参考资料

SpringBoot之Redis的支持

Spring-data-redis特性与实例

线程安全

在之前学习编程的时候,有一个概念根深蒂固,即程序=算法+数据结构。数据代表问题空间中的客体,代码就用来处理这些数据,这种思维是站在计算机的角度去抽象问题和解决问题,称之为面向过程编程。后来逐渐的发展,诞生了面向对象的编程**。面向对象是站在现实世界的角度去抽象解决问题,把数据和行为都看成对象的一部分。

有了面向对象的编程模式,极大的地提升了现代软件的开发效率和规模,但是现实世界和计算机世界还是有很大的差异。比如人们很难想象在现实世界中进行一项工作的时候,不停的中断和切换,某些属性也会在中断期间改变,而这些事件在计算机里是很正常的。因此不得不妥协,首先在保证数据的准确性之后,才能来谈高效。

1 什么叫线程安全

我们谈论的线程安全,是限定在多个线程之间存在共享数据访问,因为如果一段代码根本不会与其他线程共享数据,那也就不会出现线程安全问题。

当多个线程访问一个对象的时候,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他协调操作的时候,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

也就是当一个对象可以安全的被多个线程同时使用,那么它就是线程安全对象。

2 java线程安全

按照线程安全的安全程度来分的话,java中的各种操作共享的数据主要分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

2.1 不可变

在java中,不可变(immutable)的对象一定是线程安全的。

对于final关键字可见性来说,只要一个不可变对象被正确构建出来,那其外部的可见状态永远也不会改变。不可变带来的安全性是最简单和最纯粹的。

对于基本数据类型来说,只需要用final修饰即可。

对于对象来说,将对象中带有状态的变量都设置为final。

在java api中复核不可变要求的类型主要有:

  • String
  • 枚举类型
  • Long和Double等数值包装类型
  • BigInteger和BigDecimal等大数据类型

AtomicLong和AtomInteger等原子类并非是不可变的类型。

2.2 绝对线程安全

一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”这个条件,通常是需要付出很大,甚至是有些不切实际的代价。

在java里标注自己是线程安全的类,大多都不是绝对线程安全,比如某些情况下Vector类在调用端也需要额外的同步措施。

2.3 相对线程安全

这个就是我们通常意义所说的线程安全。

它需要保证对这个对象单独的操作时线程安全的,我们在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。

如vector,hashtable等线程安全类都是属于这种的。

2.4 线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确使用同步手段来保证对象在并发环境中可以安全使用。

java中的ArrayList和HashMap就是这种。

2.5 线程对立

指无论调用端是否采用同步措施,都无法在多线程环境中并发使用代码。

一个例子就是Thread类的suspen方法和resume方法,如果有两个线程同时持有一个线程对象,一个去中断线程,一个去恢复线程,如果并发进行的话,无论调用是否采用了同步,都会存在锁死的风险。

常见的线程对立例子还有:

  • System.setIn()
  • System.setOut()
  • System.runFinalizersOnExit()

3 线程安全的实现方法

如何实现线程安全与代码编写有着很大的关系,但是虚拟机提供的同步和锁的机制也起到了非常重要的作用。

3.1 互斥同步

互斥同步是一种比较常见的并发正确性保障手段。

同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或是一些,使用信号量的时候是一些)线程使用。互斥是实现同步的一种手段,临界区、互斥量和信号量等都是主要的互斥手段。

  • synchronized

    java中最基本的互斥同步手段就是synchronized关键字。

    synchronized关键字在经过编译之后,会在同步块的前后形成monitorenter和monitorexit这两个字节码指令。这两个字节码都需要一个引用类型的参数来指明锁定和解锁的对象。如果在java程序中指明了这个对象,那么这个参数就是此对象的引用,如果没有指定,那就根据synchronized修饰的是实例方法还是类方法来取对应的对象实例或者Class对象来作为锁对象。

    • synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
    • synchronized同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
  • ReentrantLock

    java.util.concurrent包中提供了重入锁来实现同步。

    ReentrantLock在写的时候,使用lock()和unlock()方法配合try/finally来完成,相比synchronized增加了一些高级功能:

    • 等待可中断

      当持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待,改为处理别的事情。

    • 可实现公平锁

      公平锁是指多个线程在等待一个同一个锁的时候,必须按照申请锁的时间顺序来依次获得锁,也就是队列方式。非公平锁则是竞争获取。

      synchronized是非公平的,ReentrantLock默认也是非公平的,但是可以实现公平的。

    • 锁可以绑定多个事件

      一个ReentrantLock对象可以绑定多个Condition对象,而synchronized的锁对象的wait、notify等方法只能实现一个隐含的条件。

    如果要用到上面三个高级功能的话,建议使用ReentrantLock,但是如果基于性能考虑的话,优先考虑使用synchronized来进行同步。

    在jdk1.6之前,synchronized在多线程下吞吐量下降很严重,ReentrantLock表现稳定。但是1.6之后,性能就差不多了,而且以后虚拟机的优化也是偏向synchronized。

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也是阻塞同步。从处理问题角度来讲,互斥同步是一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出问题,无论共享数据是否会出现竞争,它都会去加锁同步。

3.2 非阻塞同步

随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略。

通俗来讲就是,先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。

最常见的补偿措施就是不断的重试,直到成功为止。

对于非阻塞同步来讲,最重要的一个硬件指令是比较并交换(CAS)。java的Unsafe类里面的某些方法被编译之后,就成了一条平台相关的处理器CAS指令,没有方法调用的过程。

Unsafe类不是提供给用户程序调用的类,不使用反射的话,只能通过使用其他java api来间接使用。java的concurrent包里的AtomicInteger整数原子类的compareAndSet和getAndIncrement方法使用了Unsafe类的CAS操作。

CAS操作会出现“ABA”问题:如果一个变量初始被读取是A,最终被赋值的时候检查到仍然是A,但是在读取和赋值这段时间里,有可能被其他线程改为B,后来又改成了A。那么CAS就认为没有改变过。大部分情况下ABA也不会影响程序并发的正确性。

3.3 无同步方案

要保证线程安全,不一定就得需要数据的同步,两者没有因果关系。如果一个方法不涉及共享数据,那它自然就不用同步,有些代码天生就是线程安全的,比如:

  • 可重入代码

    也叫纯代码,可以在代码执行的任何时刻中断它,转而去执行别的代码(包括递归调用自己),而控制权返回后,原来的程序不会出现错误。

    所有可重入的代码都是线程安全的,但是线程安全的代码不一定是可重入的。

    可重入代码的一些特征是:

    • 不依赖存储在堆上的数据和公用系统资源
    • 用的状态量都是参数传入
    • 不调用不可重入代码

    如果一个方法是结果是可预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性要求,当然也就是线程安全的。

  • 线程本地存储

    如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码能不能在同一个线程中运行?如果把这些共享数据的可见范围放在同一个线程之内,这样无需进行同步也可以做到线程安全。

    符合这种特点的应用有很多,比如:

    • 大部分的消息队列的生产者——消费者模式。
    • web交互模型中的一个请求对应一个服务器线程的处理方式。

    在java中,如果一个变量要被某个线程独享,就可以用ThreadLocal来实现线程本地存储的功能。

4 写在最后

通过对线程安全的仔细研究,终于理解了函数式编程为什么是天然的支持高并发了。函数式编程里的不可变对象和可重入代码,都不会出现线程安全的问题。这也是为什么现在函数式编程越来越火的一个重要原因。

spring boot多数据源配置

spring boot多数据源配置

在单数据源的情况下,Spring Boot的配置非常简单,只需要在application.properties文件中配置连接参数即可。但是往往随着业务量发展,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源。

1 准备

1.1 禁止DataSourceAutoConfiguration

首先要将spring boot自带的DataSourceAutoConfiguration禁掉,因为它会读取application.properties文件的spring.datasource.*属性并自动配置单数据源。在@SpringBootApplication注解中添加exclude属性即可:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(JpaDemoApplication.class, args);
	}
}

1.2 配置数据库连接

然后在application.properties中配置多数据源连接信息:

spring.datasource.primary.url=jdbc:mysql://localhost:3306/test
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.secondary.url=jdbc:mysql://localhost:3306/test1
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver

1.3 手段创建数据源

由于我们禁掉了自动数据源配置,因些下一步就需要手动将这些数据源创建出来:

@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
//    @Qualifier(value = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource(){
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "secondaryDataSource")
//    @Qualifier(value = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

2 jdbcTemplate多数据源

2.1 jdbcTemplate的数据源配置

新建jdbcTemplate的数据源配置:

@Configuration
public class JdbcTemplateConfig {
    @Bean(name = "primaryJdbcTemplate")
    public JdbcTemplate primaryJdbcTemplate(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean(name = "secondaryJdbcTemplate")
    public JdbcTemplate secondaryJdbcTemplate(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

2.2 单元测试

然后编写单元测试用例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaDemoApplicationTests {
    @Autowired
    @Qualifier("primaryJdbcTemplate")
    protected JdbcTemplate jdbcTemplate1;

    @Autowired
    @Qualifier("secondaryJdbcTemplate")
    protected JdbcTemplate jdbcTemplate2;

    @Test
    public void testJdbc() {
        // 往第一个数据源中插入两条数据
        jdbcTemplate1.update("insert into users(id,name,age) values(?, ?, ?)", 1, "aaa", 20);
        jdbcTemplate1.update("insert into users(id,name,age) values(?, ?, ?)", 2, "bbb", 30);

        // 往第二个数据源中插入一条数据,若插入的是第一个数据源,则会主键冲突报错
        jdbcTemplate2.update("insert into users(id,name,age) values(?, ?, ?)", 1, "aaa", 20);

        // 查一下第一个数据源中是否有两条数据,验证插入是否成功
        Assert.assertEquals("2", jdbcTemplate1.queryForObject("select count(1) from users", String.class));

        // 查一下第一个数据源中是否有两条数据,验证插入是否成功
        Assert.assertEquals("1", jdbcTemplate2.queryForObject("select count(1) from users", String.class));

    }


    @Test
    public void contextLoads() {
    }
}

3 mybatis多数据源配置

3.1 自定义SqlSessionFactory

新建两个mybatis的SqlSessionFactory配置:

@Configuration
@MapperScan(basePackages = {"com.example.jpademo.primary.mapper"}, sqlSessionFactoryRef = "sqlSessionFactory1")
public class MybatisPrimaryConfig {
    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory1() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        // 使用primaryDataSource数据源
        factoryBean.setDataSource(primaryDataSource);
        return factoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
        // 使用上面配置的Factory
        SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory1());
        return template;
    }
}

这样,com.example.jpademo.primary.mapper包下的所有mapper就会用sqlSessionFactory1。同理可以创建

sqlSessionFactory2

@Configuration
@MapperScan(basePackages = {"com.example.jpademo.secondary.mapper"}, sqlSessionFactoryRef = "sqlSessionFactory2")
public class MybatisSecondaryConfig {
    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory2() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        // 使用primaryDataSource数据源
        factoryBean.setDataSource(secondaryDataSource);
        return factoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
        // 使用上面配置的Factory
        SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory2());
        return template;
    }
}

3.2 mapper和实体类

然后编写mapper和实体类:

@Data
public class User {
    private Integer id;
    private String name;
    private Integer age;
}
package com.example.jpademo.primary.mapper;

import com.example.jpademo.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.beans.factory.annotation.Qualifier;

@Mapper
@Qualifier("userMapper1")
public interface UserMapper1 {
    @Select("select * from users where id=#{id}")
    User findById(@Param("id") Integer id);
}
package com.example.jpademo.secondary.mapper;

import com.example.jpademo.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.beans.factory.annotation.Qualifier;

@Mapper
@Qualifier("userMapper2")
public interface UserMapper2 {
    @Select("select * from users where id=#{id}")
    User findById(Integer id);
}

3.3 单元测试

编写单元测试用例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaDemoApplicationTests {

    @Autowired
    private UserMapper1 userMapper1;

    @Autowired
    private UserMapper2 userMapper2;

    @Test
    public void testMybatis() {
        User user1 = userMapper1.findById(1);
        User user2 = userMapper2.findById(1);

        Assert.assertEquals("aaa", user1.getName());
        Assert.assertEquals("ccc", user2.getName());
    }
}

4 参考资料

Spring Boot + Mybatis多数据源和动态数据源配置

Spring Boot 两种多数据源配置:JdbcTemplate、Spring-data-jpa

Express 快速入门

Express 快速入门

安装

npm init

npm install --save express

hello world

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});

执行命令运行应用程序

node app.js

然后,在浏览器中输入 http://localhost:3000/ 以查看输出。

express程序生成器

安装

npm install -g express-generator

示例

以下语句在当前工作目录中创建名为 myapp 的 Express 应用程序:

express --view=pug myapp

在 MacOS 或 Linux 上,采用以下命令运行此应用程序:

DEBUG=myapp:* npm start

然后在浏览器中输入 http://localhost:3000/ 以访问此应用程序。

路由

基本路由

路由用于确定应用程序如何响应对特定端点的客户机请求,包含一个 URI(或路径)和一个特定的 HTTP 请求方法(GET、POST 等)。

每个路由可以具有一个或多个处理程序函数,这些函数在路由匹配时执行。

路由定义采用以下结构:

app.METHOD(PATH, HANDLER)

其中:

  • appexpress 的实例。
  • METHODHTTP 请求方法
  • PATH 是服务器上的路径。
  • HANDLER 是在路由匹配时执行的函数。

比如简单的Hello world:

app.get('/', function (req, res) {
  res.send('Hello World!');
});

响应方法

下表中响应对象 (res) 的方法可以向客户机发送响应,并终止请求/响应循环。如果没有从路由处理程序调用其中任何方法,客户机请求将保持挂起状态。

方法 描述
res.download() 提示将要下载文件。
res.end() 结束响应进程。
res.json() 发送 JSON 响应。
res.jsonp() 在 JSONP 的支持下发送 JSON 响应。
res.redirect() 重定向请求。
res.render() 呈现视图模板。
res.send() 发送各种类型的响应。
res.sendFile 以八位元流形式发送文件。
res.sendStatus() 设置响应状态码并以响应主体形式发送其字符串表示。

app.route()

可以使用 app.route() 为路由路径创建可链接的路由处理程序。 因为在单一位置指定路径,所以可以减少冗余和输入错误。

app.route('/book')
  .get(function(req, res) {
    res.send('Get a random book');
  })
  .post(function(req, res) {
    res.send('Add a book');
  })
  .put(function(req, res) {
    res.send('Update the book');
  });

express.Router

使用 express.Router 类来创建可安装的模块化路由处理程序。Router 实例是完整的中间件和路由系统;因此,常常将其称为“微型应用程序”。

以下示例将路由器创建为模块,在其中装入中间件,定义一些路由,然后安装在主应用程序的路径中。

在应用程序目录中创建名为 birds.js 的路由器文件,其中包含以下内容:

var express = require('express');
var router = express.Router();

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// define the home page route
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
  res.send('About birds');
});

module.exports = router;

接着,在应用程序中装入路由器模块:

var birds = require('./birds');
...
app.use('/birds', birds);

此应用程序现在可处理针对 /birds/birds/about 的请求,调用特定于此路由的 timeLog 中间件函数。

中间件

中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。

next() 函数不是 Node.js 或 Express API 的一部分,而是传递给中间件函数的第三自变量。next() 函数可以命名为任何名称,但是按约定,始终命名为“next”。

中间件函数可以执行以下任务:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求/响应循环。
  • 调用堆栈中的下一个中间件。

如果当前中间件函数没有结束请求/响应循环,那么它必须调用 next(),以将控制权传递给下一个中间件函数。否则,请求将保持挂起状态。

Express 应用程序可以使用以下类型的中间件:

模板引擎

在 Express 可以呈现模板文件之前,必须设置以下应用程序设置:

  • views:模板文件所在目录。例如:app.set('views', './views')
  • view engine:要使用的模板引擎。例如:app.set('view engine', 'pug')

然后安装对应的模板引擎 npm 包:

npm install pug --save

views 目录中创建名为 index.pug 的 Pug 模板文件,其中包含以下内容:

html
  head
    title!= title
  body
    h1!= message

随后创建路由以呈现 index.pug 文件。如果未设置 view engine 属性,必须指定 view 文件的扩展名。否则,可以将其忽略。

app.get('/', function (req, res) {
  res.render('index', { title: 'Hey', message: 'Hello there!'});
});

向主页发出请求时,index.pug 文件将呈现为 HTML。

常见的web攻击手段和防范

常见的web攻击手段和防范

XSS

XSS攻击即跨站点脚本攻击(Cross Site Script),指黑客通过篡改网页,注入恶意html脚本,在用户浏览网页的时候,控制用户浏览器进行恶意操作的一种攻击方式。

  • 手段

    • 反射型:诱导用户点击一个嵌入恶意脚本的链接。
    • 持久型:黑客提交含有恶意脚本的请求,保存在被攻击的web站点数据库中,用户浏览网页时,恶意脚本被包含在正常的页面中,达到攻击目的。
  • 防范

    • 消毒:对某些危险的html标签进行转义处理,如将"<"转义为"&lt"。

    • HTTPOnly:对于存放敏感信息的cookie,添加HttpOnly属性,避免被攻击脚本窃取。

SQL注入

通过把SQL命令伪装成正常的HTTP请求参数,传递到服务端,欺骗服务器最终执行恶意的sql命令,达到入侵目的。

  • 手段

    sql注入攻击需要对数据结构有所了解才能进行,获取数据库表结构信息的手段主要有如下几种:

    • 开源:开源的软件的数据库结构是公开的,容易获取。
    • 错误回显:服务器内部的500错误会显示到浏览器上。通过故意构造非法参数,使服务器异常信息显示到浏览器上,猜测数据库结构。
    • 盲注:根据页面变化猜测。
  • 防范

    • 消毒:通过正则匹配,过滤请求中可能注入的恶意SQL语句。

    • 使用预编译语句:java的PrepareStatement等一类的预编译语句使用参数占位符替代需要动态传入的参数,这样攻击的sql就无法改变sql语句的结构。

    • 使用orm框架:常见的orm框架都对关键字进行了转义处理

    • 避免密码明文存放

    • 处理好异常:系统异常后,重定向到相应的错误处理页面,不能让其简单的暴露给用户。

CSRF

CSRF(Cross Site Request Forgery,跨站点请求伪造),通过跨站请求,以合法的用户身份进行非法操作。

  • 手段

    利用用户跨站请求,利用用户浏览器的cookie和服务器的session策略,盗取用户身份,伪造请求。

  • 防范

    • HTTPOnly:防止脚本盗取cookie。
    • 表单token:在页面表单中增加一个随机数作为token,每次响应的token都不相同,从正常页面的请求会包含该token,而伪造的请求无法获得该值,服务器检查请求参数中token值是否存在即可。
    • 验证码:请求提交时,用户输入验证码,避免在用户不知情的情况下被攻击者伪造请求。
    • Referer check:http请求头的referer域中记录了请求来源,通过检查请求来源来验证是否合法。

文件上传漏洞

文件上传攻击是指利用一些站点没有对文件类型做很好的校验,上传了可执行文件或脚本,并通过脚本获得服务器上的相应权利,或者是通过诱导外部用户访问、下载上传的病毒或木马文件,达到攻击的目的。

  • 防范
    • 文件白名单
    • 限制上传文件的大小
    • 上传文件重命名
    • 通过魔数来判断文件类型:很多类型的文件,起始的几个字节是固定的,因此可以通过这几个字节来确定文件类型,而不能简单的通过后缀名来判断。

DDos攻击

DDos(Distributed Denial of Service)即分布式拒绝服务攻击,是目前最为强大、最难防御的攻击方式之一。

最基本的Dos攻击是利用合理的客户端请求来占用过多的服务器资源,从而使合法的用户无法得到服务器的响应。DDos的原理是攻击者借助公共网络,将数量庞大的计算机联合起来作为攻击平台,对一个或多个平台发动攻击,从而达到瘫痪目标主机的目的。

  • 手段

    常用的攻击手段主要有SYN Flood,DNS Query Flood,CC等。

    • SYN Flood

      TCP三次握手过程中设置了一些异常处理,第三步中如果服务器没有收到客户端的ACK报文,一方面服务端一般会进行3~5次的重试,每个30秒论云一遍等待队列;另一方面,服务器发出SYN+ACK报文之后,会分配一部分资源给即将建立的连接,这个资源在重试期间一直保留,当维护等待列表超过极限后就不再接受新的TCP连接。

      SYN Flood正式利用TCP三次握手的过程来攻击。伪造大量IP地址给服务器发送SYN报文,但是由于伪造的IP地址几乎不可能存在,也就不可能从客户端收到回应,导致服务器会维护一个非常大的半连接等待列表,并且不断的从这个列表中的IP地址进行遍历和重试,占用大量服务器资源。

    • DNS Query Flood

      采用的方法是向被攻击的服务器发送海量的域名解析请求。通常请求解析的域名是随机生成的,大部分都不存在,并且通过伪造端口和客户端IP,防止请求被过滤。由于域名随机生成, 几乎不可能查到相应的缓存信息,此DNS域名服务器不能解析时就向上层DNS服务器查询,直到最顶级的13台根DNS服务器。大量不存在的域名解析给服务器带来很大的负载,造成正常的DNS域名解析失败,达到了攻击的目的。

    • CC攻击

      CC(Challenge Collapsar)攻击是基于应用层http协议发起的,也被称为HTTP Flood。

      攻击者通过控制大量的“肉鸡”(被劫持的普通用户电脑)或者利用从网上收集的大量http代理,模拟正常用户给网站发起请求直到该网站拒绝服务为止。

  • 防御

    没有啥好办法,耐心等待吧。

    • 拼带宽:或者说拼软妹币,这不是一点点钱能搞定的。
    • 流量清洗&封IP要这么做的前提是攻击包至少要到你的机房。而机房自保的措施导致了数据包根本到不了机房,无解。
    • CDN服务:现代CDN提供商还没有完善的动态网页加速技术,所以结果就是,你充其量利用CDN保住静态化的主页可以访问,其他任何动态网站功能就只能呵呵了。

其他攻击手段

其他还有比较常见的DNS域名劫持、CDN回源攻击、服务器权限提升、缓冲区溢出等等,防御手段的额滞后性导致攻击手段永远比防御手段多。

型变:搞不懂的"协变"和"逆变"

先来看一个定义,Animal(简称F,Father)类型是Dog(简称C,Child)类型的父类型,我们将这种父子关系简写成 F <| C,而对于List<Animal>List<Dog>,简记为 f(F)f(C) 。那么我们可以这样描述协变和逆变:

当 F <| C 时, 如果有 f(F) <| f(C) ,那么 f 叫做协变(Convariant);
当 F <| C 时, 如果有 f(C) <| f(F) ,那么 f 叫做逆变(Contravariance); 
如果上面两种关系都不成立则叫做不可变。

通俗来讲,协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性。 协变就是用一个窄类型替代宽类型,逆变则用宽类型覆盖窄类型。

协变和逆变都是类型安全的。

java中的泛型是不可变的,可是有时候需要实现逆变和协变,怎么办呢?这时就需要用到通配符

1 通配符

java泛型的通配符有两种形式:

  • ? extends T

    子类型上届限定符,指定参数类型的上限(该类型必须是T或者它的子类型)

  • ? super T

    超类型下届限定符,指定类型参数的下限(该类型必须是T或者它的父类型)

public class Animal {
    public void act(List<? extends Animal> list){
        for(Animal animal : list){
            System.out.println("acting");
        }
    }

    public void aboutTeddyDog(List<? super TeddyDog> list){
        System.out.println("teddy dog");
    }
}
public class Cat extends Animal { }
public class Dog extends Animal { }
public class TeddyDog extends Dog { }

我们在act(List<? extends Animal> list)方法中,这个list可以是如下几个类型:

List<Animal>
List<Dog>
List<TeddydDog>
List<Cat>

虽然TeddyDog的子类,List<TeddydDog>并不是List<Dog>的子类。对于任何的List<X>,这里的X只要是Animal的子类型,那么List<? extends Animal> 就是List<X>的父类型。

使用通配符 List<? extends Animal> 的引用, 我们不可以往这个List中添加Animal类型以及其子类型的元素。因为对于set方法, 编译器无法知道具体的类型, 所以会拒绝这个调用。 但是, 如果是get方法形式的调用, 则是允许的:

List<? extends Animal> list1 = new ArrayList<>();
list1. add(new Dog()); //报错。因为编译器无法知道具体类型
list1. add(new Animal()); //报错。因为编译器无法知道具体类型
List<? extends Animal> list1 = new ArrayList<>();
List<Dog> list4 = new ArrayList<>();
list1 = list4; // 不报错,因为编译器知道可以把返回的对象转换为一个Animal类型

在Java中, 还有一个无界通配符, 即单独一个 ?? 可以代表任意类型。

? get() // 可正常调用
void set(?) //调用set方法报错,编译器无法确定参数类型

2 <? extends T> 实现了泛型的协变

假设有:

List<? extends Number> list = new ArrayList<>();

? extends Number 表示的是Number类或其子类。

这里的父类型 F 是Number,子类型 C 是 ? extends Number < Number,而且 List<? extends Number> 是 List的父类型。那么如下是代码是正确的实现了协变:

List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Float>();

但是这里不能向list1、 list2添加除null以外的任意对象。因为, List可以添加Interger及其子类, List可以添加Float及其子类, list1、 list2 都是 List<? extends Number> 的子类型, 如果能将Float的子类添加到 List<? extends Number> 中, 那么也能将Integer的子类添加到 List<? extends Number> 中。这时候 List<? extends Number> 里面将会持有各种Number子类型的对象(Byte, Integer, Float, Double等等) 。 Java为了保护其类型一致, 禁止向List<? extends Number>添加任意对象, 不过可以添加null。

list1.add(null);
list2.add(null);

list1.add(new Integer(1)); // error
list2.add(new Float(1. 1f)); // error

3 <? super T> 实现了泛型的逆变

假设有:

List<? super Number> list = new ArrayList<>();

? super Number 通配符则表示的类型下界为Number.

4 PECS

Netty

Netty是一个高性能、异步事件驱动的NIO框架。Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。Netty的主要构件块是:

  • Channel
  • 回调
  • Future
  • 事件和ChannelHandler

Channel

NIO的一个基本的通道。

回调

一个回调其实就是一个方法,一个指向已经被提供给另一个方法的引用。使得后者可以在适当的时候去调用前者。Netty的内部使用回调来处理事件。

Future

Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的占位符,它将在未来某个时刻完成,并提供对其结果的访问。

Netty自己封装了一个ChannelFuture,用于异步操作的时候使用。每个Netty的出站IO操作都返回一个ChannelFuture,也就意味着他们不会阻塞。

事件和ChannelHandler

Netty使用不同的事件来通知状态的改变或者操作的状态,使得我们可以基于已经发生的事件来触发适当的动作。

每个事件都可以被分发给ChannelHandler类中的某个用户的实现方法。

  • ChannelHandler

    充当了所有出站和入站数据的应用程序逻辑容器。

    ChannelInboundHandler是一个经常使用的子接口,这种Handler接收入站数据。应用程序的业务逻辑通常驻留在一个或多个ChannelInboundHandler中。

  • ChannelPipeline

    是ChannelHandler链容器。当channel创建的时候,会被自动分配到专属的pipline中:

  • 编码器和解码器

    当通过Netty发送或者接受消息的时候,就会发生一次数据转换。需要编码器和解码器来处理,netty提供了几种常用的编码器和解码器。

    TCP以流的方式传输数据,容易出现粘包问题,上层的应用协议为了对消息进行区分,经常采用的方式有如下4种:

    • 消息长度固定,累计读取的长度总和为约定的定长长度后,就认为读到了一个完整的消息;将计数器置位,重新开始读取下一条报文;
    • 将回车换行符作为消息结束符,例如:FTP协议,这种方式在文本协议中应用比较广泛;
    • 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符;
    • 通过在消息头中定义长度段来标示报文的总长度(在银行系统中应用最为广泛);

    因此netty提供了相对应的几种解码器:

    • FixedLengthFrameDecoder 定长解码器来解决定长消息的黏包问题
    • LineBasedFrameDecoder和StringDecoder来解决以回车换行符作为消息结束符的TCP黏包的问题
    • DelimiterBasedFrameDecoder 特殊分隔符解码器来解决以特殊符号作为消息结束符的TCP黏包问题
    • LengthFieldBasedFrameDecoder 自定义长度解码器解决TCP黏包问题

EventLoop

EventLoop本身只由一个线程驱动,他处理了一个Channel的所有IO事件。包括:

  • 注册感兴趣的事件
  • 将事件分发给ChannelHandler
  • 安排进一步的动作

多个EventLoop形成一个EventLoopGroup。这种设计中,一个给定的Channel的IO操作都是由相同的Thread来执行了,实际上消除了同步的需要。

引导Bootstrap

引导类为应用程序的网络层配置提供了容器,主要是用于将一个进程绑定到某个指定的端口,或者某个进程连接到另一个运行在某个主机上的某个端口。

  • Bootstrap

    客户端的引导器,只需要一个EventLoop。

  • ServerBootstrap

    服务端的引导器,需要两个EventLoop。一个用来服务器自身已经绑定到本地端口正在监听的套接字。另一组用来处理客户端的连接。

大型分布式网站的思考(二):Web前端性能优化

一般来说web前端是指网站业务逻辑之前的部分,比如:浏览器加载、网站视图模型、图片服务、CDN服务等等。web前端优化主要从如下三个方面入手:

浏览器访问优化

  1. 减少http请求

    http协议是一个无状态的,每次请求都需要建立通信链路进行传输,在服务器端,一般每个请求都会分配一个线程去处理。

    减少http请求的主要手段是合并CSS合并js合并图片

  2. 使用浏览器缓存

    css、js、Logo、图标等静态资源文件更新频率较低,可以将这些文件缓存在浏览器中。

    在更新js等文件的时候,一般不是将文件内容更新,而是生成一个新的文件,然后更新html的引用。

    更新静态资源的时候,也是要逐量更新,以避免用户浏览器的大量缓存失效,造成服务器负载增加、网络堵塞。

  3. 启用压缩

    在服务器对文件压缩,然后在浏览器端解压缩,可以减少通信传输的数据量。

  4. CSS放在页面最上面,js放在页面最下面

    浏览器会在下载完全部CSS之后才对整个页面进行渲染,而浏览器是在加载js之后就立即执行,有可能会阻塞整个页面。因此最好的做法就是把CSS放在最上面,js放在最下面。但是如果是页面解析的时候就用到js,也是要相应的js放在上面。

  5. 减少cookie传输

    cookie会包含在每次请求和响应中,太大的cookie会影响数据传输,需要慎重考虑哪些数据写入cookie中。

    对于某些静态资源的访问,如css和js等,发送cookie没意义,可以考虑静态资源使用独立域名访问,避免请求静态资源时发送cookie。

CDN加速

CDN(content distribute network,内容分发网络)的本质仍然是一个缓存。将缓存放在离用户最近的地方,使得用户可以以最快的速度获取数据。

CDN缓存的一般是静态资源,如图片、文件、CSS、js、静态网页等。

反向代理

反向代理服务器位于网站中心机房的一侧,代理网站web服务器接收http请求。

反向代理可以在一定程度上保护网站安全,来自互联网的访问请求必须经过代理服务器,相当于在web服务器和攻击之间加了一个屏障。

反向代理也可以通过配置缓存,静态资源被缓存在反向代理服务器,当用户访问时,可以从反向代理服务器上返回。有些网站也会将部分动态内容缓存在代理服务器上,通过内部通知机制,更新缓存。

反向代理也可以实现负载均衡的功能。

写在最后

可以发现,在web前端性能优化的时候,提到最多的就是缓存。

网站性能优化第一定律:优先考虑使用缓存!

分布式理论和协议

分布式理论和协议

随着计算机的规模越来越大,所有的业务集中部署在一个或者多台大型机上的体系结构,已经越来越不能满足当前的计算机系统。同时随着微型计算机的发展,越来越多的廉价PC成为各大企业架构的首选。

1 分布式系统概念

分布式系统是一个硬件或软件组件分布在不同网络的计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。

一个标准的分布式系统在没有任何特定业务逻辑约束的情况下,都会有如下的特征:

  • 分布性

    这个好理解,指的是多台计算机在空间上随意分布,同时计算机的分布情况也会随时变动。

  • 对等性

    分布式系统中的计算机没有主从之分,所有计算机节点都是对等的。

    副本是一个比较常用的概念,是对数据和服务的一种冗余方式,用来解决数据丢失和服务节点不可用的情况。

  • 并发性

    程序运行中的并发操作是非常常见的额行为。

  • 缺乏全局时钟

    由于多个计算机的空间上是分离的,没有一个全局的时钟序列,因此很难断定两个事件的先后问题。

  • 故障总会发生

当一个系统是分布式的时候,要保证系统是高可用的时候,就会面临很多问题,比如如下几个典型的问题:

  • 通信异常

    每次网络通信都会伴随着网络不可用的风险。即使网络通信正常,相对于单机系统从内存中获取数据,分布式也会出现延时的问题。

  • 网络分区

    当网络由于发生异常情况,导致部分节点之间的网络延时不断增大,最终可能只有部分节点之间可以正常通信,另外一部分则不能。这种现象被称为网络分区,也就是“脑裂”。

    极端情况下,这些局部小集群会对立完成原本需要整个系统才能完成的功能,这就会产生数据的一致性问题。

  • 三态

    每次请求响应,可能出现三种情况,成功、失败和超时,这就是三态。

    传统单机应用,调用一个函数能够得到明确的成功或者失败。但是在分布式的系统中,由于网络不可靠,就会出现超时的情况:

    • 一个是消息并没有被发到接收方,在发送过程中就丢失了。
    • 另一种情况是接收方已经处理并回应了,但是在接收过程中消息丢失。

    由于这样的情况,发起方是无法确定当前的请求是否被成功处理。

  • 节点故障

2 分布式事务

狭义的事务一般特指数据库的事务,但是广义上来讲:

事务是由一系列对系统中数据进行访问和更新的操作所组成的一个程序执行逻辑单元。

我们经常说到,对于事务来说,有4个特征ACID:

  • 原子性Atomicity
  • 一致性Consistency
  • 隔离性Isolation
  • 持久性Durability

对于分布式系统来说,数据是分散在不同的机器上,为了保证分布式程序的可靠性,出现了分布式事务的概念。

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。

一个分布式事务可以看作是由多个分布式的操作序列组成,通常把这一系列分布式的操作称为子事务。因此分布式事务可以被定义成一种嵌套型的事务,同时也就具备了ACID事务特性。由于各个子事务的执行是分布式的,因此实现起来比较复杂。

对于一个高访问量和高并发的互联网分布式系统来讲,如果严格按照ACID的特性来实现一套系统,则可能出现系统的可用性和严格一致性的冲突。但是有一点毋庸置疑,可用性是所有用户不允许我们讨价还价的系统属性,因此如何构建兼顾可用性和一致性分布式系统这一难题出现了诸如CAP和BASE这样的分布式经典理论。

3 经典理论

3.1 CAP

CAP理论告诉我们,一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能满足其中的两项。

从这三个词的字面意思就可以很容易理解CAP的是在讲什么。

  • 一致性是指数据一致性。
  • 可用性是指服务必须处于一直可用的状态。
  • 分区容错是指系统在遇到任何网络分区故障时,仍然可以对外提供满足一致性和可用性的服务。

既然无法同时满足上述三个需求,我们就需要抛弃一项。

放弃 说明
放弃P 如果要避免分区,一个简单的做法就是将所有数据和服务都放在一个节点上。在放弃P的同时也就放弃了系统的可扩展性。
放弃A 一旦系统遇到网络分区或者其他故障,受到影响的服务在等待恢复的时间内是无法对外提供正常的服务的,即不可用。
放弃C 这里所放弃的一致性是指数据的强一致性。这里放弃强一致性,保留最终的一致性。这样的系统无法保证数据的实时一致性,但是在一定的时间窗口之后就会达到一个最终一致的状态。

对于一个分布式系统而言,分区容错性必须要满足,因此一个分布式系统往往在数据一致性和可用性之间寻求平衡。

3.2 BASE

BASE是**Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)**的缩写。BASE理论是对CAP中的一致性和可用性权衡的结果。

其核心**是即使无法做到强一致性,但是每一个应用都可以根据自身业务的特点,采用适当的方式来使系统达到最终一致性。

  • 基本可用

    在系统出现不可预知故障的时候,允许损失部分可用性,这不等价于系统不可用。一般情况下有如下两种策略:

    • 响应时间的损失:比如原来需要0.5s返回,当出现故障,可以允许2~3s返回数据。
    • 部分功能的损失:正常情况下用户可以顺利使用所有功能,当出现故障时,允许某些低优先级的功能不可用。
  • 弱状态

    弱状态也称之为软状态,和应状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

  • 最终一致性

    最终一致性强调的是系统中的所有数据副本,在经过一段时间同步后,最后能够达到一个一致的状态,不要求实时保证数据的强一致性。

    最终一致性是一种特殊的弱一致性:系统能够保证在没有其他新的跟新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问都能够获取到最新的值。同时在没有发生故障的前提下,数据达到一致状态的时间延迟,取决于网络延迟、系统负载和数据复制方案的设计等因素。

    在实际的工程实践中,最终一致性存在以下五类变种:

    • 因果一致性

      在A进程更新完某数据之后通知B进程,那么B之后对该数据的访问都应该能够获取到A更新后的值,并且如果B要更新该数据,则必须基于A更新之后的最新值。

    • 读己之所写

      A进程更新一个数据之后,它自己总能访问到被更新过后的最新值而不会看到旧值。

    • 会话一致性

      将系统对数据的访问过程框定在一个会话中:在此会话中实现“读己之所写”的一致性。

    • 单调读一致性

      如果一个进程在某一时刻从系统中读取某数据的值之后,那么系统对于该数据后续时间的任何访问都不应该获取到旧的值。

    • 单调写一致性

      对同一进程的写操作是顺序的。

    在实际工程中,可以将这若干个变种结合起来,构建一个具有最终一致性的分布式系统。

    总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,传统事务的ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终是一致的状态。在实际的业务场景中,对于数据的一致性的要求是不同的,因此这两个理论又往往结合在一起使用。

4 一致性协议

4.1 2PC:二阶段提交

4.2 3PC:三阶段提交

4.3 Paxos算法

go项目的构建

鉴于golang项目在CI构建时需要下载远程包,最简单一种解决思路是将vendor目录上传上来。

这里提供一下解决办法:

建项目的时候,自定义gopath的路径,然后在goland中设置当前项目为gopath。

.
├── bin
├── pkg
├── Dockerfile
├── .drone.yml
└── src
    └── swan-product
        ├── api
        ├── config
        ├── db
        ├── logger
        ├── middleware
        ├── service
        ├── utils
        └── vendor
        ├── Gopkg.lock
        ├── Gopkg.toml

这样就能保证每个项目的gopath是纯净且一致的。使用dep构建工具,管理go的依赖,在.gitignore中忽略掉bin和pkg目录。

在drone构建的时候,只需要将gopath指定为当前项目的根目录即可,即:

 commands:
   - export GOPATH=`pwd`
   - cd src/swan-product
   - go build

因为这里已经在vendor中包含了所有的第三方依赖,故不用再go get安装。

在dockerfile中,只需要把构建好的可执行文件放在opt目录之下即可:

FROM docker.finogeeks.club/base/alpine
MAINTAINER "[email protected]"

WORKDIR /opt

COPY src/swan-product/swan-product /opt/swan-product
ENTRYPOINT /opt/swan-product

Java NIO

Java NIO

Java NIO提供了与标准IO不同的IO工作方式:

  • Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  • Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
  • Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

Channel

Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。

channel有几种实现:

  • FileChannel:从文件中读写数据。FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。

  • DatagramChannel:能通过UDP读写网络中的数据。

  • SocketChannel:能通过TCP读写网络中的数据。客户端。

  • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。服务端。

Buffer

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

Java NIO 有以下Buffer类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

如你所见,这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。

使用buffer读写数据一般要经过如下几个步骤:

  • 写入数据到Buffer
  • 调用flip()方法
  • 从Buffer中读取数据
  • 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

为了理解Buffer的工作原理,需要熟悉它的三个属性:

  • capacity

    作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

  • position

    当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。

    当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

  • limit

    在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

    当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position) 。

Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。

使用Selector的步骤:

  • 创建一个selector

  • 向Selector注册通道

    注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。

  • SelectionKey

    当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些感兴趣的属性:

    • interest集合

      感兴趣的事件集合。

    • ready集合

      是通道已经准备就绪的操作的集合。

    • Channel

    • Selector

    • 附加的对象(可选)

      可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。

  • 通过Selector选择通道

    一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。

分散(Scatter)/聚集(Gather)

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。

聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样可以方便的处理消息头和消息体。

管道(Pipe)

管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

RESTful API

RESTful API

REST这个词,是Roy Thomas Fielding在他2000年的博士论文——《Architectural Styles and the Design of Network-based Software Architectures》中提出的。Fielding是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注,并且立即对互联网开发产生了深远的影响。

Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。

1 基本概念

1.1 简介

REST,即我们常说的RESTful,全称是Representational State Transfer。一般被翻译为表现层状态转移

如果一个架构符合REST原则,就称它为RESTful架构。

要理解RESTful架构,最好的方法就是去理解Representational State Transfer这个词组到底是什么意思,它的每一个词代表了什么涵义。

  • 资源Resources

    REST的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是**"资源"(Resources)的"表现层"**。

    所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

  • 表现层Representation

    "资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。

    比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。

    URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而URI应该只代表"资源"的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对"表现层"的描述。

  • 状态转移State Transfer

    访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

    HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转移"(State Transfer)。而这种转移是建立在表现层之上的,所以就是"表现层状态转移"。

    HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

1.2 http的幂等性

在HTTP/1.1规范中幂等性的定义是:

Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴,正如编译器只能帮助检查语法错误一样,HTTP规范也没有办法通过消息格式等语法手段来定义它,这可能是它不太受到重视的原因之一。但实际上,幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。

我们先从一个例子说起,假设有一个从账户取钱的远程API(可以是HTTP的,也可以不是),我们暂时用类函数的方式记为:

bool withdraw(account_id, amount)

withdraw的语义是从account_id对应的账户中扣除amount数额的钱;如果扣除成功则返回true,账户余额减少amount;如果扣除失败则返回false,账户余额不变。

值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的情况是withdraw请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被掉丢了,导致客户端无法得知处理结果。如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了withdraw被调用两次,账户也被多扣了一次钱。

这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证withdraw功能的事务性。分布式事务的优点是对于调用者很简单,复杂性都交给了中间件来管理。缺点则是一方面架构太重量级,容易被绑在特定的中间件上,不利于异构系统的集成;另一方面分布式事务虽然能保证事务的ACID性质,而但却无法提供性能和可用性的保证。

另一种更轻量级的解决方案是幂等设计。我们可以通过一些技巧把withdraw变成幂等的,比如:

int create_ticket() 
bool idempotent_withdraw(ticket_id, account_id, amount)

create_ticket的语义是获取一个服务器端生成的唯一的处理号ticket_id,它将用于标识后续的操作。idempotent_withdraw和withdraw的区别在于关联了一个ticket_id,一个ticket_id表示的操作至多只会被处理一次,每次调用都将返回第一次调用时的处理结果。这样,idempotent_withdraw就符合幂等性了,客户端就可以放心地多次调用。

基于幂等性的解决方案中一个完整的取钱流程被分解成了两个步骤:1.调用create_ticket()获取ticket_id;2.调用idempotent_withdraw(ticket_id, account_id, amount)。虽然create_ticket不是幂等的,但在这种设计下,它对系统状态的影响可以忽略,加上idempotent_withdraw是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以重试,直到获得结果。

和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用,幂等设计往往是唯一的选择。

HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。

  • HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。
  • HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。
  • POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。
  • 对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。

1.3 设计指南

这部分内容主要参考阮一峰老师的《RESTful API 设计指南》,为了方便以后查阅,搬运过来。

一、协议

API与用户的通信协议,总是使用HTTPs协议

二、域名

应该尽量将API部署在专用域名之下。

https://api.example.com

如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。

https://example.org/api/

三、版本(Versioning)

应该将API的版本号放入URL。

https://api.example.com/v1/

另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。

四、路径(Endpoint)

路径又称"终点"(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

五、HTTP动词

对于资源的具体操作类型,由HTTP动词表示。

常用的HTTP动词有下面五个(括号里是对应的SQL命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

还有两个不常用的HTTP动词。

  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

下面是一些例子。

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物
  • DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
六、过滤信息(Filtering)

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。

下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。

七、状态码(Status Codes)

服务器向用户返回的状态码和提示信息,常见的有以下一些(方括号中是该状态码对应的HTTP动词)。

  • 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  • 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  • 204 NO CONTENT - [DELETE]:用户删除数据成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  • 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  • 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  • 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  • 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  • 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

状态码的完全列表参见这里

八、错误处理(Error handling)

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

{
    error: "Invalid API key"
}

九、返回结果

针对不同操作,服务器向用户返回的结果应该符合以下规范。

  • GET /collection:返回资源对象的列表(数组)
  • GET /collection/resource:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档
十、Hypermedia API

RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。

{"link": {
  "rel":   "collection https://www.example.com/zoos",
  "href":  "https://api.example.com/zoos",
  "title": "List of zoos",
  "type":  "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。

Hypermedia API的设计被称为HATEOAS。Github的API就是这种设计,访问api.github.com会得到一个所有可用API的网址列表。

{
  "current_user_url": "https://api.github.com/user",
  "authorizations_url": "https://api.github.com/authorizations",
  // ...
}

从上面可以看到,如果想获取当前用户的信息,应该去访问api.github.com/user,然后就得到了下面结果。

{
  "message": "Requires authentication",
  "documentation_url": "https://developer.github.com/v3"
}

上面代码表示,服务器给出了提示信息,以及文档的网址。

十一、其他

(1)API的身份认证应该使用OAuth 2.0框架。

(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

2 spring boot实践

2.1 注解

在spring boot中提供了如下几个注解来快速编写restful api

  • @Controller:修饰class,用来创建处理http请求的对象

    @RestController:Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。

  • @RequestMapping:配置url映射

    @GetMapping @PostMapping @PutMapping @DeleteMapping:相当于@RequestMapping中的Method的配置。

  • @PathVariable @ModelAttribute @RequestParam:参数绑定注解

2.2 具体代码

首先创建一个实体类

// lombok的注解,自动生成get和set方法
@Data
public class User {
    private Integer id;
    private String name;
}

然后创建处理的repository

@Repository
public class UserRepository {

    /**
     * 不使用数据库,采用map的方式来存数据
     */
    private final Map<Integer, User> userMap = new ConcurrentHashMap<>(16);

    /**
     * id生成器
     */
    private final static AtomicInteger idGen = new AtomicInteger();

    public boolean save(User user) {
        Integer id = idGen.getAndIncrement();
        user.setId(id);
        return userMap.put(id, user) == null;
    }

    public Collection<User> findAll() {
        return userMap.values();
    }

    public User findById(Integer id){
        return userMap.get(id);
    }

    public boolean delete(Integer id){
        return userMap.remove(id) == null;
    }

}

最后是controller

@RestController
@CommonsLog//lombok的注解,生成log对象
public class UserController {
    private final UserRepository userRepository;

    @Autowired
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostMapping("/set")
    public User save(@RequestParam String name) {
        User user = new User();
        user.setName(name);
        if (userRepository.save(user)) {
            log.info("success: " + user.toString());
        }
        return user;
    }

    @GetMapping("/get/all")
    public Collection<User> getAll() {
        return userRepository.findAll();
    }

    @GetMapping("/get/{id}")
    public User getId(@PathVariable Integer id) {
        return userRepository.findById(id);
    }

    @PutMapping("/put")
    public User put(@RequestParam String name) {
        User user = new User();
        user.setName(name);
        userRepository.save(user);
        return user;
    }

    @DeleteMapping("/delete/{id}")
    public Boolean delete(@PathVariable Integer id) {
        return userRepository.delete(id);
    }

}

2.3 测试

可以通过postman来进行测试。

也可以通过编写单元测试代码的方式对controller进行测试。

新版本的spring boot的测试使用的注解是:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyAppApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;

    @Test
    public void saves() {
        User user = new User();
        user.setName("zhu");
        assertTrue("true", userRepository.save(user));
    }

    @Test
    public void controllerTest() throws Exception {
        RequestBuilder request = null;

        request = get("/get/all");
        mockMvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(content().string("[]"));

        request = post("/set").param("name", "zhu");
        mockMvc.perform(request)
                .andExpect(status().isOk());
    }
}

3 构建RESTful API文档

我们构建RESTful API的目的通常都是由于多终端的原因,这些终端会共用很多底层业务逻辑,因此我们会抽象出这样一层来同时服务于多个移动端或者Web前端。

这样一来,我们的RESTful API就有可能要面对多个开发人员或多个开发团队:IOS开发、Android开发或是Web开发等。为了减少与其他团队平时开发期间的频繁沟通成本,传统做法我们会创建一份RESTful API文档来记录所有接口细节,然而这样的做法有以下几个问题:

  • 由于接口众多,并且细节复杂(需要考虑不同的HTTP请求类型、HTTP头部信息、HTTP请求内容等),高质量地创建这份文档本身就是件非常吃力的事,下游的抱怨声不绝于耳。
  • 随着时间推移,不断修改接口实现的时候都必须同步修改接口文档,而文档与代码又处于两个不同的媒介,除非有严格的管理机制,不然很容易导致不一致现象。

为了解决上面这样的问题,接下来将介绍RESTful API的重磅好伙伴Swagger2,它可以轻松的整合到Spring Boot中,并与Spring MVC程序配合组织出强大RESTful API文档。它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。

3.1 pom依赖

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.4.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.4.0</version>
</dependency>

3.2 创建配置类

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.zzf.myapp"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Spring Boot中使用Swagger2构建RESTful APIs")
                .description("这一个是测试")
                .termsOfServiceUrl("https://github.com/zhenfeng-zhu")
                .contact(new Contact("Lucas", "https://github.com/zhenfeng-zhu", "[email protected]"))
                .version("1.0")
                .build();
    }
}

如上代码所示,通过@Configuration注解,让Spring来加载该类配置。再通过@EnableSwagger2注解来启用Swagger2

再通过createRestApi函数创建DocketBean之后,apiInfo()用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。select()函数返回一个ApiSelectorBuilder实例用来控制哪些接口暴露给Swagger来展现,本例采用指定扫描的包路径来定义,Swagger会扫描该包下所有Controller定义的API,并产生文档内容(除了被@ApiIgnore指定的请求)。

3.3 改造UserController类

在完成了上述配置后,其实已经可以生产文档内容,但是这样的文档主要针对请求本身,而描述主要来源于函数等命名产生,对用户并不友好,我们通常需要自己增加一些说明来丰富文档内容。如下所示,我们通过@ApiOperation注解来给API增加说明、通过@ApiImplicitParams、@ApiImplicitParam注解来给参数增加说明。

@RestController
@CommonsLog//lombok的注解,生成log对象
public class UserController {
    @Autowired
    private UserRepository userRepository;


    @ApiOperation(value = "设置用户", notes = "设置用户")
    @ApiImplicitParam(name = "name", value = "user的用户名", required = true)
    @PostMapping("/set")
    public User save(@RequestParam(value = "name") String name) {
        User user = new User();
        user.setName(name);
        if (userRepository.save(user)) {
            log.info("success: " + user.toString());
        }
        return user;
    }

    @ApiOperation(value = "获取所用用户", notes = "获取所用用户")
    @GetMapping("/get/all")
    public Collection<User> getAll() {
        return userRepository.findAll();
    }

    @ApiOperation(value = "通过id查找用户")
    @ApiImplicitParam(name = "id", value = "用户的id", dataType = "int", required = true)
    @GetMapping("/get/{id}")
    public User getId(@PathVariable Integer id) {
        return userRepository.findById(id);
    }

    @ApiOperation(value = "put方法测试")
    @ApiImplicitParam(name = "id", value = "用户的id", required = true)
    @PutMapping("/put")
    public User put(@RequestParam String name) {
        User user = new User();
        user.setName(name);
        userRepository.save(user);
        return user;
    }

    @ApiOperation(value = "通过id删除用户")
    @ApiImplicitParam(name = "id", value = "用户的id", required = true)
    @DeleteMapping("/delete/{id}")
    public Boolean delete(@PathVariable Integer id) {
        return userRepository.delete(id);
    }

}

3.4 访问swagger-ui

完成上述代码添加上,启动Spring Boot程序,访问:http://localhost:8080/swagger-ui.html。

相比为这些接口编写文档的工作,我们增加的配置内容是非常少而且精简的,对于原有代码的侵入也在忍受范围之内。因此,在构建RESTful API的同时,加入swagger来对API文档进行管理,是个不错的选择。

4 参考资料

阮一峰的文章《理解RESTful架构》

小马哥的spring boot系列《Java 微服务实践 - Spring Boot 系列(三)Web篇(中)》

程序员DD的spring boot系列文章Spring Boot中使用Swagger2构建强大的RESTful API文档

大型分布式网站的思考(四):负载均衡

今年的双十一顺利的度过了,信用卡的standin系统轻松的扛过了双十一。交易被花呗截流了不少,双十一当天tps峰值是6000左右,之前跟支付宝压测到15000,没有看到过万的tps,心里还是有些小失望,卡中心的领导可能比我更忧虑。在这一年多里,进行不断的系统调优,听到比较多的一个词是负载均衡。参考了相关资料加上自己的理解,写了这篇文章,留作记录。

负载均衡(Load Balance)是一种服务器或网络设备的集群技术。负载均衡将特定的业务(网络服务、网络流量等)分担给多个服务器或网络设备,从而提高了业务处理能力,保证了业务的高可用性。

1,实现负载均衡的基础技术手段

1.1,HTTP重定向负载均衡

利用http重定向协议实现负载均衡。

http重定向服务器是一台普通的应用服务器,其唯一的功能就是根据用户的http请求计算一台真实的web服务器地址,并将web服务器地址写入http重定向响应中(状态码是302)返回给浏览器,浏览器自动重新请求到实际的物理服务器地址,完成访问。

  • 优点:

    • 简单
  • 缺点:

    • 浏览器需要两次请求才能完成一次访问,性能较差。

    • 重定向服务器本身会成为瓶颈,集群伸缩规模有限。

    • 使用302响应吗重定向,可能使搜索引擎判断为seo作弊,降低搜索排名。

1.2,DNS域名解析负载均衡

利用DNS处理域名解析请求的同时进行负载均衡。

在DNS服务器中配置多个A记录,如:

www.mysite.com IN A 10.0.0.1
www.mysite.com IN A 10.0.0.2
www.mysite.com IN A 10.0.0.3
...

每次域名解析请求都会根据负载均衡算法计算出一个不同的IP地址返回,这样A记录中配置的多个服务器就成为一个集群,并且实现了负载均衡。

  • 优点
    • 将负载均衡工作交给DNS,省掉了维护负载均衡服务器的麻烦
    • DNS支持基于地理位置的域名解析,即会将域名解析成离用户地理最近的服务器地址,加快访问速度,改善性能。
  • 缺点
    • DNS是多级解析,每一级都可能缓存A记录,当下线某台服务器之后,即使修改了DNS的A记录,要使其生效需要很长时间,这段时间DNS会依然将域名解析到已经下线的服务器。
    • DNS负载均衡的控制权在域名服务商那里,网站一般无法做更多的改善和管理。

1.3,反向代理负载均衡

利用反向代理服务器实现负载均衡。

反向代理服务器将请求根据负载均衡算法转发到不同的web服务器上,web服务器处理完成的响应也要通过反向代理服务器返回给用户。由于web服务器不直接对外部提供访问,因此web服务器不需要使用外部IP地址,而反向代理服务器需要配置双网卡和内部外部两套IP地址。

由于反向代理服务器的转发请求都是在http协议层面,因此也叫应用层负载均衡。

  • 优点
    • 和反向代理服务器集成在一起,部署简单。
  • 缺点
    • 反向代理服务器是所有请求和响应的中转站,性能可能成为瓶颈。

1.4,IP负载均衡

在网络层通过修改请求目标地址进行负载均衡。

用户请求包到达负载均衡服务器114.100.0.1后,在操作系统内核进程获取网络数据包,根据算法得到一台真实的web服务器IP地址10.0.0.1,然后将数据包的目的IP地址修改为10.0.0.1,不需要用户进程处理。web服务器处理完成后,响应数据包到了负载均衡服务器后,再将数据包的源地址改为自己的IP地址114.100.0.1,返回给用户浏览器。

关键点是真实的web服务器响应数据包如何返回给负载均衡服务器,常用的有两种方案:

  1. 负载均衡服务器在修改目的IP地址时,同时修改源地址,将数据包源地址设为自身的IP地址(SNAT:源地址转换),这样web服务器的响应就会回到负载均衡服务器。
  2. 将负载均衡服务器同时作为真实物理服务器集群的网关服务器,这样所有的响应数据都会到达负载均衡服务器。
  • 优点
    • 在内核进程完成数据分发,比反向代理负载均衡(在应用程序中分发数据)性能更好。
  • 缺点
    • 所有请求响应都需要经过负载均衡服务器,集群的最大响应数据吞吐量受制于负载均衡服务器的网卡带宽,对于提供下载服务或者视频服务,难以满足需求。

1.5,数据链路层负载均衡

在通信协议的数据链路层修改mac地址进行负载均衡。

又称为三角传输模式,负载均衡数据分发过程中不修改IP地址,只修改mac地址,通过配置真实物理服务器集群所有虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包的源地址和目的地址就可以进行数据分发的目的。

由于集群对外暴露的IP和负载均衡服务器的IP是一致,因此可以将响应数据包直接返回给浏览器。这种又被称之为直接路由(DR)。

这种方案是用的最为广泛的,比较好用的产品时LVS(Linux Virtual Server)。

2,负载均衡的算法

2.1,轮询(Round Robin,RR)

所有的请求被依次分发到每台应用服务器上,每台服务器需要处理的请求数目都相同。适用于所有服务器硬件都相同的场景。

2.2,加权轮询(weighted Round Robin,WRR)

根据服务器硬件性能状况,在轮询的基础上,按照配置的权重将请求分发到每个服务器。高性能的服务器处理更多的请求。

2.3,随机(Random)

请求被随机分配到各个应用服务器。若硬件配置不同,可以使用加权随机算法。

2.4,最少连接(Least Connection)

记录每个服务器正在处理的连接数,将新到的请求分发到最少连接的服务器上。也可以实现加权最少连接。

2.5,源地址散列(Source Hashing)

根据请求来源的IP地址进行hash计算,得到应用服务器。这样来自同一个ip的请求总在同一个服务器被处理,该请求的上下文可以存储在这台服务器上,在一个会话周期内重复使用。

3,负载均衡的优化

3.1,TCP连接复用

连接复用功能通过使用连接池技术,可以将前端大量的客户的HTTP请求复用到后端与服务器建立的少量的TCP长连接上,大大减小服务器的性能负载,减小与服务器之间新建TCP连接所带来的延时,并最大限度减少后端服务器的并发连接数,降低服务器的资源占用。

3.2,SSL卸载

为了避免明文传输出现的安全问题,对于敏感信息,一般采用SSL协议,如HTTPS协议。SSL是需要耗费大量CPU资源的一种安全技术,如果由后端的服务器来承担,则会消耗很大的处理能力。

应用交付设备为了提升用户的体验,分担服务器的处理压力,将SSL加解密集中在自身的处理上,相对于服务器来说负载均衡服务器能提供更高的SSL处理性能,还能够简化对证书的管理,减少日常管理的工作量,该功能又称为SSL卸载。

4,写在最后

负载均衡技术不管应用于用户访问服务器资源,还是应用于多链路出口,均大大提高了对资源的高效利用,显著降低了用户的网络布署成本,提升了用户的网络使用体验。随着云计算的发展,负载均衡的技术实现还将与云计算相结合,在虚拟化和NFV软件定义网关等方面持续发展。

领域实体类

在看项目代码的时候,发现了entity包和dto包,里面都是只保存数据的类,仔细查了资料,才发现java对于只保存数据的类有好几个分类。

  • pojo类:这是普通的java类,具有一部分的get和set方法。
  • dto类:data transfer object 数据传输对象类,泛指用于展示层与服务层之间传输的对象。
  • vo类:vo有两种说法,一种是view object,一种是value object。
  • po类:persisent object 持久对象。和pojo类一样,也是只有get set方法,但是这种类一般是用于持久层。
  • bo类:business object,业务对象,表示应用程序领域内事物的所有实体类。
  • do类:domain object,领域对象,就是从现实中抽象出来的有形或者无形的业务实体。

根据我的经验来看,大部分人都没有分那么清楚,一般是把数据类放在domain包,或者entity包里。再细分一下的话,可以把dto类单独提取到一个包里。

一致性哈希算法

当我们在做数据库分库分表或者做分布式缓存的时候,不可避免的都会遇到一个问题:

如何将数据均匀的分散到各个节点中,并且尽量的在加减节点的时能使受影响的数据最少。

1 hash取模

随机放置就不多说了。通常最容易想到的方案是哈希取模了。

可以将传入的key按照
$$
index=hash(key) % N
$$
这样来计算出需要存放的节点。

这样可以满足数据的均匀分配,但是这个算法的容错性和扩展性比较差。比如增加或者删除一个节点的时候,所有的key都要重新计算,显然这样的成本比较高,为此需要一个算法来满足均匀的同时也要有良好的容错性和扩展性。

2 一致性hash算法

一致性hash算法是将所有的哈希值构成了一个环,其范围是0~2^32-1。如图:

哈希环

之后将各个服务器节点散列到这个环上,可以用节点的IP,hostname这样唯一性的字段作为key进行hash。散列之后如下:

之后需要将数据定位到对应的节点上,使用同样的hash函数将key也映射到这个环上。

这样就按照顺时针方向就可以将k1定位到N1节点,k2定位到N3节点,k3定位到N2节点。

2.1 容错性

假设N1宕机了:

依然根据顺时针方向,k2和k3保持不变,只有k1被重新映射到了N3。这样就很好的保证了容错性,当一个节点宕机时只会影响到少部分数据。

2.2 扩展性

当新增一个节点时:

在N2和N3之间新增了一个节点N4,这时受影响的数据只有k3,其余的数据也是保持不变。

2.3 虚拟节点

到目前为止,该算法也有一些问题:

当节点较少的时候可能出现数据不均匀的情况:

这样会导致大部分数据都在N1节点,只有少量的数据在N2节点。

为了解决这个问题,一致性哈希算法引入了虚拟节点。

将每一个节点进行多次哈希,生成的节点放置在环上成为虚拟节点

计算时可以在 IP 后加上编号来生成哈希值。

这样只需要在原有的基础上多一步由虚拟节点映射到实际节点的步骤即可让少量节点也能满足均匀性。

3 参考

https://crossoverjie.top/2018/01/08/Consistent-Hash/#more

最终一致性的实现手段

最终一致性的实现手段

实现最终一致性有三种手段:可靠事件模式、业务补偿模式和TCC模式

1 可靠事件模式

可靠事件模式属于事件驱动架构,当某件重要的事情发生时,比如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会将订阅事件的微服务推送事件。

要实现这种模式需要消息队列实现事件的持久化和at least once的可靠事件投递模式。

1.1 本地事件表

本地事件表方法是将事件和业务数据保存在同一个数据库中,使用一个额外的事件恢复服务来恢复事件,由本地事物保证更新业务和发布事件的原子性。

但是业务系统和事件系统耦合比较紧密,额外的事件数据库操作也会给数据库带来额外的压力,可能成为瓶颈。

1.2 外部事件

此方法是将事件持久化到外部的事件系统,事件系统需要提供实时事件服务以接受微服务发布的事件,同时事件系统还需要提供事件恢复服务来确认恢复事件。

1.3 不足

此过程可能出现重复消费的情况。

2 补偿模式

一般来讲,异常一般是由以下两种情况造成的:

业务异常:业务逻辑产生的错误,比如余额不足、库存不足等。

技术异常:非业务逻辑产生的异常,比如网络连接异常、超时等。

补偿模式就是使用一个额外的协调服务来协调各个需要保证一致性的其他服务。协调服务按顺序调用每一个服务,如果某个服务调用异常就取消之前所有已经调用成功的服务。

建议仅用于技术异常的情况。对于业务异常来讲,应该尽可能的去优化业务模式,以避免要求补偿事务。

2.1 常用手段

在实现补偿模式时应该做到两点:

  • 首先要确定失败的步骤和状态,从而确定要补偿的范围。
  • 其次要能提供补偿操作使用的业务数据。

可以通过记录完整的业务流水的方法来实现上面两点要求。但是对于一个通用的补偿框架来说,预先知道微服务需要记录的业务要素是不可能的,那么就需要一种办法来保证业务流水的可扩展性,实践中主要有两种方法:大表和关联表。

  • 大表,顾明思议就是设计时除了必须的字段外,还需要预留大量的备用字段,框架可以提供辅助工具来将业务数据映射到备用字段中。大表对于框架层实现起来比较简单,但是也有一些难点,比如预留多少个字段合适,每个字段又需要预留多长。还有一个难点是如果仅从数据层面来查询数据,很难一眼看出备用字段的业务含义,维护过程不友好。
  • 关联表,分为技术表和业务表。技术表中保存为实现补偿操作所需要的技术数据,业务表中保存业务数据。通过在技术表中增加业务表名和业务表主键来建立和业务数据的关联。关联表更灵活,能支持不同业务类型记录不同的业务要素。但是在框架的实现上难度较高,每次查询都需要复杂的关联动作,性能会受到影响。

2.2 重试

补偿过程作为一个服务,在调用的时候也会出现不成功的情况,这时就要通过重试机制来保证补偿的成功率。因此要求补偿操作具有幂等性。

但是也不是盲目的重试,我们需要根据服务执行失败的原因来选择不同的策略:

  • 因业务因素导致失败,需要停止重试。
  • 罕见的异常,如网络中断,传输过程中数据丢失,应该立即重试。
  • 如果是因为系统繁忙,此时需要等待一段时间再重试。

2.3 不足

在补偿模式中有一个明显的缺陷是隔离性,从第一个服务开始一直到补偿完成,不一致性是对其他服务可见的。另外补偿模式过分依赖协调服务的健壮性,如果协调服务异常,则没办法达到一致性。

3 TCC模式

TCC,是Try,Confirm和Cancel的缩写。一个完整的TCC业务一般是由一个主业务和若干个从业务组成。

  • Try
    • 完成所有业务检查
    • 预留必须的业务资源
  • Confirm
    • 真正执行业务
    • 不做任何业务检查
    • 只使用Try阶段预留的业务资源
    • 满足幂等性
  • Cancel
    • 释放Try阶段预留的业务资源
    • 满足幂等性

3.1 实现过程

整个TCC业务分成两个阶段完成:

第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。

第二阶段:活动管理器根据第一阶段的执行结果来执行Confirm或Cancel操作。如果第一阶段所有的try操作是成功的,则调用所有从业务的Confirm操作,否则都调用Cancel操作。

TCC模式不再需要记录详细的业务流水,在一定程度上弥补了补偿模式的缺陷,在TCC模式中,直到明确的Confirm动作,所有的业务操作都是隔离的。而且还可以通过指定try的超时时间,主动的Cancel预留的资源,从而实现了自治。

3.2 不足

TCC模式不能百分百保证一致性,如果某服务提交了Confirm成功,但是由于网络故障,导致主服务收到的失败,那么就会出现不一致性,这被称为heuristic exception。因此为保证成功率,都需要支持重试。

heuristic exception是不可杜绝的,但是通过设置合理的超时时间、重试频率以及监控,可以使此异常的可能性降到很低,另外如果出现了此异常,还可通过人工手段补救。

Java内存模型和线程

java内存模型和线程

并发不一定依赖多线程,但是在java里面谈论并发,大多与线程脱不开关系。

线程是大多是面试都会问到的问题。我们都知道,线程是比进程更轻量级的调度单位,线程之间可以共享内存。之前面试的时候,也是这样回答,迷迷糊糊,没有一个清晰的概念。

大学的学习的时候,写C和C++,自己都没有用过多线程,看过一个Windows编程的书,里面讲多线程的时候,一大堆大写的字母,看着一点都不爽,也是惭愧。后来的实习,写unity,unity的C#使用的是协程。只有在做了java后端之后,才知道线程到底是怎么用的。了解了java内存模型之后,仔细看了一些资料,对java线程有了更深入的认识,整理写成这篇文章,用来以后参考。

1 Java内存模型

Java虚拟机规范试图定义一种java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致性内存访问的效果。

java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。(这里所说的变量包括了实例字段、静态字段和数组等,但不包括局部变量与方法参数,因为这些是线程私有的,不被共享。)

1.1 主内存和工作内存

java规定所有的变量都存储在主内存。每条线程有自己的工作内存

线程的工作内存中的变量是主内存中该变量的副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

1.2 内存之间的交互

关于主内存和工作内存之间的具体交互协议,java内存模型定义了8中操作来完成,虚拟机实现的时候必须保证每个操作都是原子的,不可分割的(对于long和double有例外)

  • lock锁定:作用于主内存变量,代表一个变量是一条线程独占。
  • unlock解锁:作用于主内存变量,把锁定的变量解锁。
  • read读取:作用于主内存变量,把变量值从主内存传到线程的工作内存中,供load使用。
  • load载入:作用工作内存变量,把上一个read到的值放入到工作内存中的变量中。
  • use使用:作用于工作内存变量,把工作内存中的一个变量的值传递给执行引擎。
  • assign:作用于工作内存变量,把执行引擎执行过的值赋给工作内存中的变量。
  • store存储:作用于工作内存变量,把工作内存中的变量值传给主内存,供write使用。

这些操作要满足一定的规则。

1.3 volatile

volatile可以说是java的最轻量级的同步机制。

当一个变量被定义为volatile之后,他就具备两种特性:

  • 保证此变量对所有线程都是可见的

    这里的可见性是指当一个线程修改了某变量的值,新值对于其他线程来讲是立即得知的。而普通变量做不到,因为普通变量需要传递到主内存中才可以做到这点。

  • 禁止指令重排

    对于普通变量来说,仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执性顺序一致。

    若用volatile修饰变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性。

1.4 long和double

long和double是一个64位的数据类型。

虚拟机允许将没有被volatile修饰的64位变量的读写操作分为两次32位的操作来进行。因此当多个线程操作一个没有声明为volatile的long或者double变量,可能出现操作半个变量的情况。

但是这种情况是罕见的,一般商用的虚拟机都是讲long和double的读写当成原子操作进行的,所以在写代码时不需要将long和double专门声明为volatile。

1.5 原子性、可见性和有序性

java的内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性。

原子性

基本数据类型的访问读写是剧本原子性的。

如果需要一个更大范围的原子性保证,java提供了lock和unlock操作,对应于写代码时就是synchronized关键字,因此在synchronized块之间的操作也是具备原子性的。

可见性

可见性是指当一个线程修改到了一个共享变量的值,其他的线程能够立即得知这个修改。共享变量的读写都是通过主内存作为媒介来处理可见性的。

volatile的特殊规则保证了新值可以立即同步到主内存,每次使用前立即从主内存刷新。

synchronized同步块的可见性是由”对于一个变量unlock操作之前,必须先把此变量同步回内存中“来实现的。

final的可见性是指被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值。

有序性

如果在本线程内观察,所有的操作都是有序的;如果在一个线程内观察另一个线程,所有的操作都是无序的。
volatile关键字本身就包含了禁止指令重排的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来实现有序性的。

1.6 先行发生原则

如果java内存模型中的所有有序性都是靠着volatile和synchronized来完成,那有些操作将会变得很繁琐,但是我们在写java并发代码的时候没有感受到这一点,都是因为java有一个“先行发生”原则。

先行发生是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先发生于操作B,其实就是说在发生B之前,A产生的影响都能被B观察到,这里的影响包括修改了内存**享变量的值、发送了消息、调用了方法等等。

  • 程序次序规则

    在一个线程内,按程序代码控制流顺序执行。

  • 管程锁定规则

    unlock发生在后面时间同一个锁的lock操作。

  • volatile变量规则

    volatile变量的写操作发生在后面时间的读操作。

  • 线程启动规则

  • 线程终止规则

  • 线程中断规则

  • 对象终结规则

    一个对象的初始化完成在finalize方法之前。

  • 传递性

    如果A先行发生B,B先行发生C,那么A先行发生C。

由于指令重排的原因,所以一个操作的时间上的先发生,不代表这个操作就是先行发生;同样一个操作的先行发生,也不代表这个操作必定在时间上先发生。

2 Java线程

2.1 线程的实现

主流的操作系统都提供了线程的实现,java则是在不同的硬件和操作系统的平台下,对线程的操作提供了统一的处理,一个Thread类的实例就代表了一个线程。Thread类的关键方法都是native的,所以java的线程实现也都是依赖于平台相关的技术手段来实现的。

实现线程主要有3种方式:使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程实现。

2.1.1 使用内核线程实现

内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程的切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

程序一般不会直接去调用内核线程,而是使用内核线程的一个高级接口——轻量级进程(Light Weigh Process),LWP就是我们通常意义上所说的线程。

由于每个轻量级进程都由一个内核线程支持,这种轻量级进程与内核线程之间1:1的关系成为一对一线程模型。

局限性

虽然由于内核线程的支持,每个轻量级进程都成为了一个独立的调度单元,即使有一个阻塞,也不影响整个进程的工作,但是还是有一定的局限性:

  • 系统调用代价较高

    由于基于内核线程实现,所以各种线程的操作都要进行系统调用。而系统调用的代价比较高,需要在用户态和内核态来回切换。

  • 系统支持数量有限

    每个轻量级进程都需要一个内核线程支持,需要消耗一定的内核资源,所以支持的线程数量是有限的。

2.1.2 使用用户线程实现

指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同布、销毁和调度完全在用户态中完成,不需要内核帮助。

如果程序实现得当,则这些线程都不需要切换到内核态,操作非常快速消耗低,可以支持大规模线程数量。这种进程和用户线程之间1:N的关系成为一对多线程模型。

局限性

不需要系统内核的,既是优势也是劣势。由于没有系统内核支援,所有的操作都需要程序去处理,由于操作系统只是把处理器资源分给进程,那“阻塞如何处理”、“多处理器系统如何将线程映射到其他处理器上”这类问题的解决十分困难,所以现在使用用户线程的越来越少了。

2.1.3 使用用户线程加轻量级进程混合实现

在这种混合模式下,既存在用户线程,也存在轻量级进程。

用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,而且支持大规模用户线程并发、而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度和处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。

在这种模式下,用户线程和轻量级进程数量比不固定N:M,这种模式就是多对多线程模型。

2.1.4 java线程的实现

目前的jdk版本中,操作系统支持怎样的线程模型,很大程度上就决定了jvm的线程是怎么映射的,这点在不同的平台没办法打成一致。线程模型只对线程的并发规模和操作成本产生影响,对编码和运行都没什么差异。

windows和linux都是一对一的线程模型。

2.2 线程调度

线程的调度是指系统为线程分配处理器使用权的过程,主要的调度方式有两种:协同式线程调度和抢占式线程调度。

2.2.1 协同式线程调度

线程的执性时间由线程本身来控制,线程把自己的工作执性完了之后,要主动通知系统切换到另外一个线程上。Lua的协程就是这样。

好处

协同式多线程最大的好处就是实现简单。

由于线程要把自己的事情干完之后才进行线程切换,切换操作对线程是克制的,所以没有什么线程同步的问题。

坏处

坏处也很明显,线程执行时间不可控。甚至如果一个线程写的问题,一直不告诉系统切换,那程序就会一直阻塞。

2.2.2 抢占式线程调度

每个线程由系统分配执行时间,线程的切换不是又线程本身来决定。

使用yield方法是可以让出执行时间,但是要获取执行时间,线程本身是没有什么办法的。

在这种调度模式下,线程的执行时间是系统可控的,也就不会出现一个线程导致整个进程阻塞。

2.2.3 java线程调度

java使用的是抢占式线程调度。

虽然java的线程调度是系统来控制的,但是可以通过设置线程优先级的方式,让某些线程多分配一些时间,某些线程少分配一些时间。

不过线程优先级还是不太靠谱,原因就是java的线程是通过映射到系统的原生线程来实现的,所以线程的调度还是取决于操作系统,操作系统的线程优先级不一定和java的线程优先级一一对应。而且优先级还可能被系统自行改变。所以我们不能在程序中通过优先级来准确的判断先执行哪一个线程。

2.3 线程的状态转换

看到网上有好多种说法,不过大致也都是说5种状态:新建(new)、可运行(runnable)、运行(running)、阻塞(blocked)和死亡(dead)。

而深入理解jvm虚拟机中说java定义了5种线程状态,在任一时间点,一个线程只能有其中的一种状态:

  • 新建new

  • 运行runnable

    包括了操作系统线程状态的running和ready,也就是说处于此状态的线程可能正在执行,也可能正在等待cpu给分配执行时间。

  • 无限期等待waiting

    处于这种状态的线程不会被cpu分配执行时间,需要被其他线程显示唤醒,能够导致线程陷入无限期等待的方法有:

    • 没有设置timeout参数的wait方法。
    • 没有设置timeout参数的join方法。
    • LockSupport.park方法。
  • 限期等待timed waiting

    处于这种状态的线程也不会被cpu分配执行时间,不过不需要被其他线程显示唤醒,是经过一段时间之后,被操作系统自动唤醒。能够导致线程陷入限期等待的方法有:

    • sleep方法。
    • 设置timeout参数的wait方法。
    • 设置参数的join方法。
    • LockSupport.parkNanos方法。
    • LockSupport.parkUntil方法。
  • 阻塞blocked

    线程被阻塞了。在线程等待进入同步区域的时候是这个状态。

    阻塞和等待的区别是:阻塞是排队等待获取一个排他锁,而等待是指等一段时间或者一个唤醒动作。

  • 结束terminated

    已经终止的线程。

3 写在最后

并发处理的广泛应用是使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力的最有力武器。有些问题使用越多的资源就能越快地解决——越多的工人参与收割庄稼,那么就能越快地完成收获。但是另一些任务根本就是串行化的——增加更多的工人根本不可能提高收割速度。

我们使用线程的重要原因之一是为了支配多处理器的能力,我们必须保证问题被恰当地进行了并行化的分解,并且我们的程序有效地使用了这种并行的潜能。有时候良好的设计原则不得不向现实做出一些让步,我们必须让计算机正确无误的运行,首先保证并发的正确性,才能够在此基础上谈高效,所以线程的安全问题是一个很值得考虑的问题。

虽然一直说java不好,但是java带给我的影响确实最大的,从java这个平台里学到了很多有用的东西。现在golang,nodejs,python等语言,每个都是在一方面能秒java,可是java生态和java对软件行业的影响,是无法被超越的,java这种语言,从出生到现在几十年了,基本上每次软件技术的革命都没有落下,每次都觉得要死的时候,忽然间柳暗花明,枯木逢春。咳咳,扯远了。

nats简介

nats是一个开源的,云原生的消息系统。Apcera,百度,西门子,VMware,HTC和爱立信等公司都有在使用。

核心基于EventMachine开发,原理是基于消息发布订阅机制,每台服务器上的每个模块会根据自己的消息类别向MessageBus发布多个消息主题,而同时也向自己需要交互的模块,按照需要的主题订阅消息。能够达到每秒8-11百万个消息,整个程序很小只有3M Docker image,它不支持持久化消息,如果你离线,你就不能获得消息。使用nats streaming可以做到持久化,缓存等功能。

NATS server

nats提供了一个go编写的轻量级服务器。发行版包括二进制和docker镜像

NATS clients

nats官方提供的客户端有Go,Node,Ruby,Java,C,C#,NGINX等。

NATS 设计目标

核心原则是性能,可伸缩和易用性。

  • 高效
  • 始终在线和可用
  • 非常轻巧
  • 支持多种质量的服务
  • 支持各种消息传递模型和使用场景

NATS 使用场景

nats是一个简单且强大的消息系统,为支持现代云原生架构设计。由于可伸缩性的复杂性,nats旨在容易使用和实现,且能提供多种质量的服务。

一些适用nats的场景有:

  • 高吞吐量的消息分散 —— 少数的生产者需要将数据发送给很多的消费者。
  • 寻址和发现 —— 将数据发送给特定的应用实例,设备或者用户,也可用于发现并连接到基础架构中的实例,设备或用户。
  • 命令和控制(控制面板)—— 向程序或设备发送指令,并从程序/设备中接收状态,如SCADA,卫星遥感,物联网等。
  • 负载均衡 —— 主要应用于程序会生成大量的请求,且可动态伸缩程序实例。
  • N路可扩展性 —— 通信基础架构能够充分利用go的高效并发/调度机制,以增强水平和垂直的扩展性。
  • 位置透明 —— 程序在各个地理位置上分布者大量实例,且你无法了解到程序之间的端点配置详情,及他们所生产或消费的数据。
  • 容错

使用nats-streaming的附加场景有:

  • 从特定时间或顺序消费
  • 持久性
  • 有保证的消息投递

NATS消息传递模型

  • 发布订阅
  • 请求回复
  • 排队

NATS的特点

nats的独特功能有:

  • 纯净的pub-sub
  • 集群模式的server
  • 订阅者的自动裁剪
  • 基于文本的协议
  • 多种服务质量
    • 最多一次投递
    • 至少一次投递
  • 持久
  • 缓存

Spring Data Jpa

为了解决抽象各个Java实体基本的“增删改查”操作,我们通常会以泛型的方式封装一个模板Dao来进行抽象简化,但是这样依然不是很方便,我们需要针对每个实体编写一个继承自泛型模板Dao的接口,再编写该接口的实现。虽然一些基础的数据访问已经可以得到很好的复用,但是在代码结构上针对每个实体都会有一堆Dao的接口和实现。

由于模板Dao的实现,使得这些具体实体的Dao层已经变的非常“薄”,有一些具体实体的Dao实现可能完全就是对模板Dao的简单代理,并且往往这样的实现类可能会出现在很多实体上。Spring-data-jpa的出现正可以让这样一个已经很“薄”的数据访问层变成只是一层接口的编写方式。

1 工程配置

1.1 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>jpa-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>jpa-demo</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.9.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

1.2 application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop

spring.jpa.properties.hibernate.hbm2ddl.auto是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:

  • create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  • create-drop:每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  • update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
  • validate:每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

2 实体类

创建一个User实体,包含id(主键)、name(姓名)、age(年龄)属性,通过ORM框架其会被映射到数据库表中,由于配置了hibernate.hbm2ddl.auto,在应用启动的时候框架会自动去数据库中创建对应的表。

@Data
@NoArgsConstructor
@Entity
public class Users {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer age;

    public Users(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

3 repository

针对User实体创建对应的Repository接口实现对该实体的数据访问:

@Repository
public interface UsersRepository extends JpaRepository<Users, Long> {
    Users findByName(String name);
    Users findByNameAndAge(String name, Integer age);

    @Query("from Users u where u.name=:name")
    Users findUser(@Param("name") String name);
}

在Spring-data-jpa中,只需要编写类似上面这样的接口就可实现数据访问。不再像我们以往编写了接口时候还需要自己编写接口实现类,直接减少了我们的文件清单。

下面对上面的UserRepository做一些解释,该接口继承自JpaRepository,通过查看JpaRepository接口的API文档,可以看到该接口本身已经实现了创建(save)、更新(save)、删除(delete)、查询(findAll、findOne)等基本操作的函数,因此对于这些基础操作的数据访问就不需要开发者再自己定义。

在上例中,我们可以看到下面两个函数:

  • User findByName(String name)
  • User findByNameAndAge(String name, Integer age)

它们分别实现了按name查询User实体和按name和age查询User实体,可以看到我们这里没有任何类SQL语句就完成了两个条件查询方法。这就是Spring-data-jpa的一大特性:通过解析方法名创建查询

除了通过解析方法名来创建查询外,它也提供通过使用@query 注解来创建查询,您只需要编写JPQL语句,并通过类似“:name”来映射@param指定的参数,就像例子中的第三个findUser函数一样。

4 单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaDemoApplicationTests {

    @Autowired
    private UsersRepository usersRepository;

    @Test
    public void contextLoads() {
    }

    @Test
    public void testJPA() {
        // 创建10条记录
        usersRepository.save(new Users("AAA", 10));
        usersRepository.save(new Users("BBB", 20));
        usersRepository.save(new Users("CCC", 30));
        usersRepository.save(new Users("DDD", 40));
        usersRepository.save(new Users("EEE", 50));
        usersRepository.save(new Users("FFF", 60));
        usersRepository.save(new Users("GGG", 70));
        usersRepository.save(new Users("HHH", 80));
        usersRepository.save(new Users("III", 90));
        usersRepository.save(new Users("JJJ", 100));

        // 测试findAll, 查询所有记录
        Assert.assertEquals(10, usersRepository.findAll().size());

        // 测试findByName, 查询姓名为FFF的User
        Assert.assertEquals(60, usersRepository.findByName("FFF").getAge().longValue());

        // 测试findUser, 查询姓名为FFF的User
        Assert.assertEquals(60, usersRepository.findUser("FFF").getAge().longValue());

        // 测试findByNameAndAge, 查询姓名为FFF并且年龄为60的User
        Assert.assertEquals("FFF", usersRepository.findByNameAndAge("FFF", 60).getName());

        // 测试删除姓名为AAA的User
        usersRepository.delete(usersRepository.findByName("AAA"));

        // 测试findAll, 查询所有记录, 验证上面的删除是否成功
        Assert.assertEquals(9, usersRepository.findAll().size());
    }
}

5 参考资料

Spring Boot 使用Spring-data-jpa简化数据访问层

大型分布式网站的思考(一):大型网站发展历程

前几天跟一个朋友聊了一些关于网站缓存分布式的一些东西,发现自己的知识还是太过贫瘠。理论+协议,这是现在我亟待加强的。这个周末买了两本关于分布式网站的书,本着好记性不如烂笔头,便有了这样一系列的文章。希望一同分享,也请多指教。

code less, play more!

前言

这个世界上没有哪个网站从诞生起就是大型网站;也没有哪个网站第一次发布的时候就拥有庞大的用户,高并发的访问,海量的数据;大型网站都是从小型网站发展而来。网站的价值在于它能给用户提供什么家宅,在于网站能做什么,而不在于它是怎么做的,所以网站在小的时候就去追求网站的架构是舍本逐末,得不偿失的。小型网站最需要做的就是为用户提供更好的服务来创造价值,得到用户认可,活下去,野蛮生长。

大型网站软件系统的特点

  • 高并发,大流量
  • 高可用
  • 海量数据
  • 用户分布广泛,网络情况复杂
  • 安全环境恶劣
  • 需求快速变更,发布平频繁
  • 渐进式发展

大型网站的发展历程

  1. 初始阶段的网站架构

    最开始没有多少人访问,所以应用程序,数据库,文件都在同一台机器上。

  2. 应用服务器和数据服务分离

    应用和数据分离之后,一般需要三台服务器。应用服务器,文件服务器和数据库服务器,这三种服务器对于硬件要求各不相同。

    • 应用服务器:更强大的CPU
    • 数据库服务器:更快速的磁盘和更大的内存
    • 文件服务器:容量更大的硬盘
  3. 使用缓存改善性能

    网站的访问也遵循二八定律:80%的业务集中在20%的数据上。因此可以把这一小部分数据缓存在内存中,减少数据库的访问压力。

    网站的缓存可以分为两种:

    • 本地缓存:缓存在应用服务器上。本地缓存访问速度快,但是受制于内存限制,缓存数量有限,而且也会出现和应用程序争抢内存的情况。
    • 远程分布式缓存:以集群的方式,缓存在大内存的专用缓存服务器。可以在理论上做到不受内存容量限制。
  4. 使用应用服务器集群提高并发能力

    当一台服务器的处理能力和存储空间不足的时候,不要企图更换更强大的服务器。对于大型网站来说,不管多么强大的服务器,都满足不了网站持续增长的业务需求。此时就可以考虑集群的方式,通过负载均衡调度服务器,可以将来自用户的请求分发到应用服务器集群中的任何一台服务器上。

  5. 数据库读写分离

    使用缓存后,大部分的数据读操作访问都可以不通过数据库完成,但是仍有部分读操作(如缓存过期,缓存不命中)和全部的写操作需要访问数据库。

    目前大部分数据库都提供主从热备的功能,在写数据的时候,访问主库,主库通过主从复制机制将数据更新同步至从数据库,在读的时候就可以通过从数据库获取数据。

  6. 使用反向代理和CDN加速网站响应

    在《web性能权威指南》中有讲到,网站性能的瓶颈,大部分时间都浪费在TCP的握手和传输上。因此可以通过CDN和反向代理的方式来加快响应。

    CDN和反向代理的本质都是通过缓存,不同的主要是:

    • CDN部署在服务器器上的机房,用户在请求时,从距离自己最近的机房获取数据。
    • 反向代理是部署在中心机房,用户请求到达中心机房之后,首先访问的服务器是反向代理的拂去其,如果反向代理服务器中缓存着用户请求的额资源,就将其返回给用户。
  7. 使用分布式文件系统和分布式数据库系统

    随着业务的发展,依旧不能满足的时候,就采用分布式的文件和分布式的数据库系统。

    分布式数据库是数据库拆分的最后手段,只用在单表数据规模特别庞大的时候才使用。更常用的拆分手段是业务分库,将不同的业务数据存储在不同的数据库中。

  8. 使用NoSQL和搜索引擎

    对数据检索和存储越来越复杂的时候,就可以采用一些非关系型数据库如HBase和非数据库查询技术如ElasticSearch等等

  9. 业务拆分

    业务场景复杂的时候,一般讲整个网站业务分为不同的产品线,如首页,订单,买家,卖家等等。

    技术上也会根据产品线划分,将一个网站分为许多不同的应用,每个应用独立部署维护,应用之间可以通过一个超链接建立联系,也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。

  10. 分布式服务

    随着业务越拆越小,存储越来越大,维护越来越困难。此时就可以将相同业务操作的提取出来,独立部署。应用系统只需要管理用户界面,通过分布式服务调用共同的业务服务完成具体的业务操作。也就是最近概念越来越火的——微服务。

  11. 云计算

    大型网站架构解决了海量数据库管理和高并发事务处理,可以将这些解决方案应用到网站自身以外的业务上。现在像阿里云,亚马逊等云计算平台,将计算作为一种基础资源出售,中小网站不需要关系技术架构等问题,只需要按需付费,就可以使网站随着业务的增长获得更大的存储和计算资源。

  12. 未来

    未来还能变成什么样子,我也不清楚,也许以后都不是开发人员来维护了,所有的这些都是AI来完成,程序员要做的就是如何完善AI。也许AI发展到最后,人类都不需要存在了吧。

结语

网站的技术是为业务而存在的,除此以外毫无意义。在技术选型和架构设计中,脱离业务发展实际,一味的追求新技术,可能会把技术发展引入一个歪路。

技术是用来解决业务的问题,而技术不可能将所有问题都解决掉,涉及业务自身的问题,还是要通过业务手段去解决。

redis和memcached的比较

memcached和redis是现在分布式缓存中最常用到的,二者具有一定的相似性。这里将自己了解到的东西,简单总结一下:

基本架构和**

  • memcached采用客户端-服务器的架构,客户端和服务器端的通讯使用自定义的协议标准,只要满足协议格式要求,客户端可以用任何语言实现。

    服务器维护了一个键-值关系的数据表,服务器之间相互独立,互相之间不共享数据也不做任何通讯操作。客户端需要知道所有的服务器,并自行负责管理数据在各个服务器间的分配。

  • redis的基本应用模式和memcached的基本相似,redis内部的数据结构最终也会落实到key-value对应的形式,不过从暴露给用户的数据结构来看,要比memcached丰富,除了标准的通常意义的键值对,redis还支持list,set, hashes,sorted set等数据结构。

内存管理机制

  • memcached默认使用slab allocation机制管理内存,其主要**是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。slab allocation机制只为存储外部数据而设计,也就是说所有的key-value数据都存储在slab allocation系统里,而memcached的其它内存请求则通过普通的malloc/free来申请,因为这些请求的数量和频率决定了它们不会对整个系统的性能造成影响。

  • 在redis中,并不是所有的数据都一直存储在内存中的。这是和memcached相比一个最大的区别。当物理内存用完时,redis可以将一些很久没用到的value交换到磁盘,redis只会缓存所有的key的信息。

    redis为了方便内存的管理,在分配一块内存之后,会将这块内存的大小存入内存块的头部。通过定义一个数组来记录所有的内存分配情况,且内存块的大小为该元素的下标。总的来看,redis采用的是包装的mallc/free,相较于memcached的内存管理方法来说,要简单很多。

事务

  • memcached没有事务的概念,但是可以通过cas协议来保证数据的完整性,一致性。redis引入数据库中的事务概念来保证数据的完整性和一致性。

数据备份,有效性,持久化等

  • memcached不保证存储的数据的有效性,slab内部基于lru也会自动淘汰旧数据,客户端不能假设数据在服务器端的当前状态。memcached也不做数据的持久化工作。
  • redis可以以master-slave的方式配置服务器,slave节点对数据进行备份,slave节点也可以充当只读的节点分担数据读取的工作。redis内建支持两种持久化方案,snapshot快照和aof 增量log方式

性能

  • 通常在memcached里,需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络io的次数和数据体积。

    memcached自身并不主动定期检查和标记哪些数据需要被淘汰,只有当再次读取相关数据时才检查时间戳,或者当内存不够使用需要主动淘汰数据时进一步检查lru数据。

  • redis支持服务器端的数据操作,拥有更多的数据结构和并支持更丰富的数据操作。在redis中,这些复杂的操作通常和一般的get/set一样高效。redis为了减少大量小数据命令操作的网络通讯时间开销,还支持批量和脚本技术。

  • 对于kv的操作,memcached和redis都支持multiple的get和set命令,同样可以提高性能。

  • 由于redis只使用单核,而memcached可以使用多核,所以平均每一个核上redis在存储小数据时比memcached性能更高。而在100k以上的数据中,memcached性能要高于redis。

集群和分布式

  • memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现memcached的分布式存储。

    当客户端向memcached集群发送数据之前,首先会通过内置的分布式算法计算出该条数据的目标节点,然后数据会直接发送到该节点上存储。但客户端查询数据时,同样要计算出查询数据所在的节点,然后直接向该节点发送查询请求以获取数据。

  • redis更偏向于在服务器端构建分布式存储。redis cluster是一个实现了分布式且允许单点故障的redis高级版本,它没有中心节点,具有线性可伸缩的功能。节点与节点之间通过二进制协议进行通信,节点与客户端之间通过ascii协议进行通信。

Docker(二):Dockerfile

Docker(二):Dockerfile

dockerfile是一个文本文件,其中包含了若干条指令,这些指令描述了构建镜像的细节。

1 hello world

首先从一个hello world开始.

  1. 在一个目录下面新建一个文件并命名为Dockerfile,然后编辑:

    从官方镜像上下载nginx镜像,然后输出hello world。

    FROM和RUN都是dockerfile的基本指令。

    FROM nginx
    RUN echo 'hello world' > /usr/share/nginx/html/index.html
  2. 当前路径执行以下命令构建镜像

    docker build -t nginx:my .
    

    最后的点用于路径参数传递,表示当前路径。

  3. 启动容器

    docker run -d -p 92:80 nginx:my
    
  4. 访问http://localhost:92/

    即可出现helloworld。

2 dockerfile指令

指令的一般格式为:指令名称 参数

  • ADD 复制文件

    ADD src dest
  • ARG 设置构建参数

    ARG user1=someuser

    设置的是构建时的环境变量,在容器运行时是不会存在这些变量的。

  • CMD 容器启动命令

    用于为执行容器提供默认值,只能有一个CMD命令。如果指定了多个,则只用最后一个CMD会被执行。如果启动容器时指定了运行时命令,则会覆盖CMD指定的命令。

    CMD echo "This is a test" | wc -
  • COPY复制文件

    COPY src dest
  • ENTRYPOINT入口点

    和CMD一样,指定docker启动时执行的命令,可多次设置,只有最后一个生效。

    ENTRYPOINT command param1 param2
  • ENV设置环境变量

    ENV key value
  • EXPOSE生命暴露的端口

    #声明暴露一个
    EXPOSE port1
    
    #声明暴露多个
    EXPOSE port1 port2 port3
  • FROM指定基础镜像

    FROM image
    FROM image:tag
    FROM image@digest
  • LABEL为镜像添加元数据

    LABEL key=value key=value
  • MAINTAINER指定维护者信息

    MAINTAINER name
  • RUN执行命令

    RUN command
    RUN ["语句","param1","param2"]
  • USER设置用户

    USER name
  • VOLUME指定挂载点

    VOLUME dir
  • WORKDIR指定工作目录

    WORKDIR dir

3 使用Docker Registry管理docker镜像

构建好自己的镜像之后,就可以上传到仓库中,下次使用的时候,直接拉取下来即可。

4 使用Maven插件构建docker镜像

参考http://blog.csdn.net/qq_22841811/article/details/67369530

kotlin快速入门

快速浏览一下 Kotlin 的语法。

基本语法

包定义和引用

在源文件头部:

package my.demo
import java.util.*

方法定义

  • 带有方法体,并且返回确定类型数据的定义方式,例如接受 Int 类型的参数并返回 Int 类型的值:
fun sum(a: Int, b: Int): Int {
    return a + b
}
  • 带有方法体,返回推断类型数据的定义方式,例如:
fun sum(a: Int, b: Int) = a + b
  • 返回无意义类型的定义方式:
fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

或者省略 Unit

fun printSum(a: Int, b: Int) {
    println("sum of $a and $b is ${a + b}")
}

变量定义

  • 只赋值一次(只读)本地变量,val
val a:Int = 1    // 指定初始值
val b = 2        // 类型自推断为 `Int`
val c:Int        // 当不指定初始值时需要指定类型
c = 3            // 延迟赋值
  • 可变变量, var
var x = 5    // 类型自推断为 `Int`
x += 1
  • 顶层变量
val PI = 3.14
var x = 0

fun incrementX() {
    x += 1
}

注释

与 Java 和 JavaScript 一样,Kotlin 支持行尾注释和块注释:

// 行尾注释
/* 多行
   块注释 */

与 Java 不同,Kotlin 中的块注释可以嵌套。

string 模板

var a = 1
val s1 = "a is $a"

a = 2
val s2 = "${s1.replace("is", "was")}, but now is $a}" 

条件表达式

fun maxOf(a:Int, b:Int): Int {
    if (a > b) {
        return a
    } else {
        return b
    }
}

使用 if 做为表达式:

fun maxOf(a:Int, b:Int) = if (a > b) a else b

可能为 null 的值,检查是否为 null

如果值可能为 null 时,必须显示的指出。
例如:

fun parseInt(str: String): Int? {
    // ...
}

使用上面定义的方法:

fun printProduct(arg1: String, arg2: String) {
    val x = parseInt(arg1)
    val y = parseInt(arg2)

    if (x != null && y != null) {
        println(x * y)
    } else {
        println(either '$arg1' or '$arg2' is not a number)
}

或者:

if (x == null) {
    println("Wrong number format in arg1: '$arg1'")
    return
}
if (y == null) {
    println("Wrong number format in arg2: '$arg2'")
    return
}

println(x * y)

类型检查和自动转换

is 操作符用于检查某个实例是否为某种类型。如果一个不可变本地变量或属性已经做过类型检查,那么可以不必显示的进行类型转换就可以使用对应类型的属性或方法。

fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        return obj.length    // 在这个类型检查分支中,`obj` 自动转换为 `String`
    }

    return null              // 在类型检查分支外,`obj` 仍然为 `Any`
}

或者:

fun getStringLength(obj: Any): Int? {
    if (obj !is String) return null
    
    return obj.length    // 在这个分支中,`obj` 自动转换为 `String`
}

再或者:

fun getStringLength(obj: Any): Int? {
    if (obj is String && obj.length > 0) {    //  在 `&&` 操作符的右侧,`obj` 自动转换为 `String`
        return obj.length
    }

    return null 
}

for 循环

val items = listOf("apple", "banana", "kiwi")
for (item in items) {
    println(item)
}

或者:

val items = listOf("apple", "banana", "kiwi")
for (index in items.indices) {
    println("item at $index is ${items[index]}")
}

while 循环

val items = listOf("apple", "banana", "kiwi")
var index = 0
while (index < items.size) {
    println("item at $index is ${items[index]}")
    index ++
}

when 表达式

fun describe(obj: Any): String = 
when (obj) {
    1          -> "one"
    "hello"    -> "Greeting"
    is Long    -> "Long"
    !is String -> "Not a String"
    else       -> "Unknown"
}

区间

  • 使用 in 操作符检查数字是否在区间内:
val x = 10
val y = 9
if (x in 1..y+1) {
    println("fits in range")
}
  • 检查数字是否在范围外:
val list = listOf("a", "b", "c")

if (-1 !in 0..list.lastIndex) {
    println("-1 is out of range")
}
if (list.size !in list.indices) {
    println("list size is out of valid list indices range too")
}
  • 区间遍历
for (x in 1..5) {
    print(x)
}

集合

  • 遍历集合:
for (item in items) {
    println(item)
}
  • 使用 in 操作符判断集合中是否包含某个对象:
when {
    "orange" in items  -> println("juicy")
    "apple" in items   -> println("apple is fine too")
}
  • 使用 lambda 表达式过滤和 map 集合:
val fruits = listOf("banana", "avocado", "apple", "kiwi")
fruits
.filter {it.startWith("a")}
.sortedBy {it}
.map {it.upperCase()}
.forEach {println(it)}

创建基本类和实例

fun main(args: Array<String>) {
    val rectangle = Rectangle(5.0, 2.0) // 不需要使用 'new' 关键词
    val triangle = Triangle(3.0, 4.0, 5.0)
    println("Area of rectangle is ${rectangle.calculateArea()}, its perimeter is ${rectangle.perimeter}")
    println("Area of triangle is ${triangle.calculateArea()}, its perimeter is ${triangle.perimeter}")
}

abstract class Shape(val sides: List<Double>) {
    val perimeter: Double get() = sides.sum()
    abstract fun calculateArea(): Double
}

interface RectangleProperties {
    val isSquare: Boolean
}

class Rectangle(
    var height: Double,
    var length: Double
) : Shape(listOf(height, length, height, length)), RectangleProperties {
    override val isSquare: Boolean get() = height == length
    override fun calculateArea(): Double = height * length
}

class Triangle(
    var sideA: Double,
    var sideB: Double,
    var sideC: Double
) : Shape(listOf(sideA, sideB, sideC)) {
    override fyb calculateArre(): Double {
        val s = perimeter / 2
        return Math.sqrt(s * (s - sideA) * (s - sideB) * (s - sideC))
    }
}

以上引自:

http://kotlinlang.org/docs/reference/basic-syntax.html

Reactive微服务

Reactive微服务

分布式系统构建起来很困难,因为它们容易出问题,运行缓慢,并且被CAP和FLP理论所限制。换句话说,它们的构建和运维都特别复杂。为了解决这个问题,reactive便出现了。

Reactive编程:一种开发模型,其专注于数据流向、对变化的反馈,以及传播他们。

在reactive编程中,刺激信号是数据的转移,叫做streams。其实很像生产者——消费者模式,消费者对值进行订阅并响应。

Reactive系统:一种架构风格,其基于异步消息来构建响应式的分布式系统。

reactive系统使用了消息驱动的方法。所有的构建通过异步消息的发送和接收来交互。消息投递的逻辑由底层的实现决定。发送者不会阻塞着等待回复,它们可能会稍后才接收到回复。

reactive系统会有两个重要的特征:

  • 伸缩性——可以横向伸缩

    伸缩性来自消息传递的解耦。消息被发送到一个地址之后,可以被一组消费者按照一种负载均衡方法消费。当reactive系统遇到负载高峰时,它可以创造出新的消费者,并在此之后销毁它们。

  • 恢复性——可以处理错误并且恢复

    首先,这种消息交互模式允许组件在其本地处理错误,组件不需要等待消息,因此当一个组件发生错误时,其他组件仍然会正常工作。其次,当一个处理消息的组件发生错误后,消息可以可以传递给在相同地址注册的其他组件。

reactive微服务系统是由reactive微服务组成的。这些微服务有下面四个特征:

  • 自治性
  • 异步性
  • 恢复性
  • 伸缩性

Reactive微服务是可自治的。他们可以根据周围的服务是否可用来调整自己的行为。自治性往往伴随着孤立性;Reactive微服务可以在本地处理错误、独立地完成任务,并在必要时和其他服务合作。它们使用异步消息传递的机制和其他服务沟通;它们也会接收消息并且对其作出回应。

得益于异步消息机制,reactive微服务可以处理错误并根据情况调整自己的行为。错误不会被扩散,而是在靠近错误源头的地方被处理掉。当一个微服务挂掉之后,它的消费者微服务要能够处理错误并避免扩散。这一孤立原则是避免错误逐层上浮而毁掉整个系统的关键。可恢复性不只是关于处理错误,它还涉及到自愈性;一个reactive微服务应该能够从错误中恢复并且对错误进行补救。

最后,reactive微服务必须是可伸缩的,这样系统才可以根据负载情况来调整节点数量。这一特性意味着将会有一系列的限制,比如不能有在内存中的状态,要能够在必要时同步状态信息,或者要能够将消息路由到状态信息相同的节点。

Vert.x

Vert.x是一个用于构建reactive和分布式系统的工具箱,其使用了异步非阻塞编程模型。当使用Vert.x构建微服务的时候,微服务会自然地带上一个核心特征:所有事情都是异步的。

传统编程模式

int res = compute(1, 2);

在这段代码中,是在等待compute函数计算出来结果之后再进行剩下的操作。而在异步非阻塞的编程模式中,将会创建一个handler:

compute(1, 2, res -> {
    // called with the result
});

在上述代码中,compute函数不再返回一个结果,而是传一个handler,当结果准备好时调用就可以了。得益于这种开发模型,可以使用很少的线程去处理高并发工作。在vert.x中,到处都可以看到这种形式的代码,比如创建http服务器时:

vertx.createHttpServer()
    .requestHandler(request -> {
        request.response().end("hello vert.x");
    })
    .listen(8080);

这个例子中,我们让一个requestHandler接收HTTP请求(事件)并且返回"hello vert.x"。Handler是一个函数,当事件发生时,它会被调用。在我们的例子中,handler代码会在每次请求进来时被调用执行。要注意的是,Handler并不会返回一个结果,但是它可以提供一个结果;这个结果是怎样被提供的,这个要看是哪种交互行为。在上面的代码段中,它只是向一个HTTP response写入了结果。这个Handler后面跟了一个方法令其监听8080端口。调用这个HTTP服务它会返回一个简单的response。

event loop

绝大多数情况,Vert.x会用一个叫做event loop的线程来调用所有的handler。

基于消息循环的线程模型有一个很大的优点:它简化了并发。因为只有一个线程存在,因此永远都只被一个线程调用而不存在并发的情况。但是同样也有一个限制:

不要阻塞消息循环

因为没有阻塞,一个消息循环线程可以短时间内分发巨量的事件,这个模式就叫做reactor模式。

verticles

Verticles是被Vert.x部署和运行的代码块。一个微服务的应用,是由运行在同一个Vert.x实例上的若干verticle组成的。一个verticle通常会创建服务器或客户端、注册一组Handler,以及封装一部分系统的业务处理逻辑。

标准的verticle

标准的verticle会在Vert.x的消息循环中被执行,并且永远不会阻塞。Vert.x保证了每一个verticle都会只被同一个线程执行而不会有并发发生,从而避免同步工作。

Worker Verticle

和标准的verticle不同,worker verticle不是在消息循环中执行的,这就意味着他们可以执行阻塞代码。但是,这会限制你的可扩展性。

Verticle可以访问vertx成员变量(是由AbstractVerticle类提供的)来创建服务器和客户端,以及和其他的verticle交互。Verticle还可以部署其他的verticle,对它们进行配置,并设置创建实例的数量。这些实例会和不同的消息循环线程绑定,Vert.x通过这些实例来均衡负载。

从Callbacks到Observables

我们可以发现,Vert.x开发模式使用回调方法。在组织管理多个异步动作时,这种基于回调的开发模式容易产生复杂的代码,陷入callback hell。

Vert.x提供了解决这个开发难题的答案——RxJava API。

Geth入门

Geth简介

go-ethereum

go-ethereum客户端通常被称为geth,它是个命令行界面,执行在Go上实现的完整以太坊节点。通过安装和运行geth,可以参与到以太坊前台实时网络并进行以下操作:

  • 挖掘真的以太币
  • 在不同地址间转移资金
  • 创建合约,发送交易
  • 探索区块历史
  • 及很多其他

网站: http://ethereum.github.io/go-ethereum/

Github: https://github.com/ethereum/go-ethereum

维基百科: https://github.com/ethereum/go-ethereum/wiki/geth

Gitter: https://gitter.im/ethereum/go-ethereum

mac下安装geth

  1. 首先安装homebrew,
  2. 使用brew安装即可。在安装geth的时候,会将go也安装上。
brew tap ethereum/ethereum
brew install ethereum
  1. 在命令行输入geth —help,如果出现

    zhuzhenengdeMBP:blog zhuzhenfeng$ geth --help
    NAME:
       geth - the go-ethereum command line interface
    
       Copyright 2013-2017 The go-ethereum Authors
    
    USAGE:
       geth [options] command [command options] [arguments...]
       
    VERSION:
       1.7.3-unstable-eea996e4

    证明安装成功。

使用Geth

  1. 打开终端,输入以下命令,以开发的方式启动geth

    geth  --datadir “~/Documents/github/ethfans/ethdev” --dev
    

    --datadir 是指定geth的开发目录,引号的路径可以随便设置

  2. 新开一个终端,执行以下命令,进入geth的控制台

    geth --dev console 2>>file_to_log_output
    

    该命令会将在console中执行的命令,生成一个文本保存在file_to_log_output文件中。

  3. 再新开一个终端,查看打印出来的日志

    tail -f file_to_log_output
    

切换到geth控制台终端,geth有如下常用的命令

  • eth.accounts

    查看有什么账户

  • personal.newAccount('密码')

    创建一个账户

  • user1=eth.accounts[0]

    可以把账户赋值给某一个变量

  • eth.getBalance(user1)

    获取某一账户的余额

  • miner.start()

    启动挖矿程序

  • miner.stop()

    停止挖矿程序

  • eth.sendTransaction({from: user1,to: user2,value: web3.toWei(3,"ether")})

    从user1向user2转以太币

  • personal.unlockAccount(user1, '密码')

    解锁账户

以太坊启动挖矿程序的时候,头结点会产生以太币,在进行转账操作之后,必须进行挖矿才会使交易成功。

Guice快速入门

Guice快速入门

接手的新项目主要是使用kotlin+vert.x来写的,使用gradle构建,依赖注入框架使用了guice。这段时间都是在熟悉代码的过程,恶补一些知识。

guice是谷歌推出的一个轻量级的依赖注入框架,当然spring也可以实现依赖注入,只是spring太庞大了。

1 基本使用

引入依赖

使用gradle或者maven,引入guice。

maven:

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>4.1.0</version>
</dependency>

Gradle:

compile "com.google.inject:guice:4.1.0"

项目骨架

首先需要一个业务接口,包含一个方法来执行业务逻辑,它的实现非常简单:

package com.learning.guice;
public interface UserService {
    void process();
}


package com.learning.guice;
public class UserServiceImpl implements UserService {
    @Override
    public void process() {
        System.out.println("我需要做一些业务逻辑");
    }
}

然后写一个日志的接口:

package com.learning.guice;
public interface LogService {
    void log(String msg);
}

package com.learning.guice;
public class LogServiceImpl implements LogService {
    @Override
    public void log(String msg) {
        System.out.println("------LOG: " + msg);
    }
}

最后是一个系统接口和相应的实现,在实现中使用了业务接口和日志接口处理业务逻辑和打印日志信息:

package com.learning.guice;
public interface Application {
    void work();
}


package com.learning.guice;
import com.google.inject.Inject;
public class MyApp implements Application {
    private UserService userService;
    private LogService logService;

    @Inject
    public MyApp(UserService userService, LogService logService) {
        this.userService = userService;
        this.logService = logService;
    }

    @Override
    public void work() {
        userService.process();
        logService.log("程序正常运行");
    }
}

配置依赖注入

guice是使用java代码来配置依赖。继承AbstractModule类,并重写其中的config方法。在config方法中,调用AbstractModule类中提供的方法来配置依赖关系。最常用的是bind(接口).to(实现类)。

package com.learning.guice;

import com.google.inject.AbstractModule;

public class MyAppModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(LogService.class).to(LogServiceImpl.class);
        bind(UserService.class).to(UserServiceImpl.class);
        bind(Application.class).to(MyApp.class);
    }
}

单元测试

guice配置完之后,我们需要调用Guice.createInjector方法传入配置类来创建一个注入器,然后使用注入器中的getInstance方法获取目标类。

package com.learning.guice;

import com.google.inject.Guice;
import com.google.inject.Injector;
import org.junit.BeforeClass;
import org.junit.Test;

public class MyAppTest {
    private static Injector injector;

    @BeforeClass
    public static void init(){
        injector= Guice.createInjector(new MyAppModule());
    }

    @Test
    public void testMyApp(){
        Application application=injector.getInstance(Application.class);
        application.work();
    }
}

程序执行结果是:

/Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/bin/java -ea -...
我需要做一些业务逻辑
------LOG: 程序正常运行

Process finished with exit code 0

2 基本概念

2.1 Bingdings 绑定

  • 链式绑定

    在绑定依赖的时候不仅可以将父类和子类绑定,还可以将子类和子类的子类进行绑定。

    public class BillingModule extends AbstractModule {
      @Override 
      protected void configure() {
        bind(TransactionLog.class).to(DatabaseTransactionLog.class);
        bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
      }
    }

    在这种情况下,injector 会把所有 TransactionLog 替换为 MySqlDatabaseTransactionLog。

  • 注解绑定

    当我们需要将多个同一类型的对象注入不同对象的时候,就需要使用注解区分这些依赖了。最简单的办法就是使用@nAmed注解进行区分。

    首先需要在要注入的地方添加@nAmed注解。

    public class RealBillingService implements BillingService {
    
      @Inject
      public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
          TransactionLog transactionLog) {
        ...
      }

    然后在绑定中添加annotatedWith方法指定@nAmed中指定的名称。由于编译器无法检查字符串,所以Guice官方建议我们保守地使用这种方式。

    bind(CreditCardProcessor.class)
            .annotatedWith(Names.named("Checkout"))
            .to(CheckoutCreditCardProcessor.class);
  • 实例绑定

    有时候需要直接注入一个对象的实例,而不是从依赖关系中解析。如果我们要注入基本类型的话只能这么做。

    bind(String.class)
            .annotatedWith(Names.named("JDBC URL"))
            .toInstance("jdbc:mysql://localhost/pizza");
    bind(Integer.class)
            .annotatedWith(Names.named("login timeout seconds"))
            .toInstance(10);
  • @Privides方法

    当一个对象很复杂,无法使用简单的构造器来生成的时候,我们可以使用@provides方法,也就是在配置类中生成一个注解了@provides的方法。在该方法中我们可以编写任意代码来构造对象。

    @provides方法也可以应用@nAmed和自定义注解,还可以注入其他依赖,Guice会在调用方法之前注入需要的对象。

    public class BillingModule extends AbstractModule {
      @Override
      protected void configure() {
        ...
      }
    
      @Provides
      TransactionLog provideTransactionLog() {
        DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
        transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
        transactionLog.setThreadPoolSize(30);
        return transactionLog;
      }
    }
  • Provider绑定

    如果项目中存在多个比较复杂的对象需要构建,使用@provide方法会让配置类变得比较乱。我们可以使用Guice提供的Provider接口将复杂的代码放到单独的类中。办法很简单,实现Provider接口的get方法即可。在Provider类中,我们可以使用@Inject任意注入对象。

    public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
      private final Connection connection;
    
      @Inject
      public DatabaseTransactionLogProvider(Connection connection) {
        this.connection = connection;
      }
    
      public TransactionLog get() {
        DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
        transactionLog.setConnection(connection);
        return transactionLog;
      }
    }

    然后在config方法中,调用.toProvider方法:

    public class BillingModule extends AbstractModule {
      @Override
      protected void configure() {
        bind(TransactionLog.class)
            .toProvider(DatabaseTransactionLogProvider.class);
      }
    }
  • 无目标绑定

    无目标绑定没有to子句

  • 构造器绑定

    某些场景下,你能需要把某个类型绑定到任意一个构造函数上。以下情况会有这种需求:1、 @Inject 注解无法被应用到目标构造函数;2、目标类是一个第三方类;3、目标类有多个构造函数参与DI。

    为了解决这个问题,guice 提供了 toConstructor()绑定 ,它需要你指定要使用的确切的某个目标构造函数,并处理 "constructor annot be found" 异常:

    public class BillingModule extends AbstractModule {
      @Override 
      protected void configure() {
        try {
          bind(TransactionLog.class).toConstructor(
              DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
        } catch (NoSuchMethodException e) {
          addError(e);
        }
      }
    }
  • 内置绑定

    除了显示绑定和即时绑定 just-in-time bindings,剩下的绑定都属于injector的内置绑定。这些绑定只能由injector自己创建,不允许外部调用。

  • 即时绑定

    当 injector 需要某一个类型的实例的时候,它需要获取一个绑定。在Module类中的绑定叫做显式绑定,只要他们可用,injector 就会在任何时候使用它们。如果需要某一类型的实例,但是又没有显式绑定,那么injector将会试图创建一个即时绑定(Just-in-time Bindings),也被称为JIT绑定 或 隐式绑定。

2.2 作用域

默认情况下Guice会在每次注入的时候创建一个新对象。如果希望创建一个单例依赖的话,可以在实现类上应用@singleton注解。

@Singleton
public class InMemoryTransactionLog implements TransactionLog {
  /* everything here should be threadsafe! */
}

或者也可以在配置类中指定。

bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);

@Provides方法中也可以指定单例。

@Provides @Singleton
  TransactionLog provideTransactionLog() {
    ...
  }

如果一个类型上存在多个冲突的作用域,Guice会使用bind()方法中指定的作用域。如果不想使用注解的作用域,可以在bind()方法中将对象绑定为Scopes.NO_SCOPE。

Guice和它的扩展提供了很多作用域,和spring一样,有单例Singleton,Session作用域SessionScoped,Request请求作用域RequestScoped等等。我们可以根据需要选择合适的作用域。

2.3 注入

guice的注入和spring类似,而且还做了一些扩展。

  • 构造器注入

    使用 @Inject 注解标记类的构造方法,这个构造方法需要接受类依赖作为参数。大多数构造子将会把接收到的参数分派给内部成员变量。

  • 方法注入

    Guice 可以向标注了 @Inject 的方法中注入依赖。依赖项以参数的形式传给方法,Guice 会在调用注入方法前完成依赖项的构建。注入方法可以有任意数量的参数,并且方法名对注入操作不会有任何影响。

  • 字段注入

    使用 @Inject 注解标记字段。这是最简洁的注入方式。

    注意:不能给final字段加@Inject注解。

  • 可选注入

    有的时候,可能需要一个依赖项存在则进行注入,不存在则不注入。此时可以使用方法注入或字段注入来做这件事,当依赖项不可用的时候Guice 就会忽略这些注入。如果你需要配置可选注入的话,使用 @Inject(optional = true) 注解就可以了。

  • 按需注入

    方法注入和字段注入可以可以用来初始化现有实例,你可以使用 Injector.injectMembers。

    这个不常用。

  • 静态注入

    不建议使用静态注入。

  • 自动注入

    Guice 会对以下情形做自动注入:

    • 在绑定语句里,通过 toInstance() 注入实例。
    • 在绑定语句里,通过 toProvider() 注入 Provider 实例。这些对象会在注入器创建的时候被创建并注入容器。如果它们需要满足其他启动注入,Guice 会在它们被使用前将他们注入进去。

2.4 AOP

guice的aop功能较弱,时间原因还没研究透,后续继续写。

#日常 产品经...

#日常
产品经理的三个核心职责范围:需求洞察力、抽象设计能力、未来洞见和规划决策能力。产品经理的第一课就是分清什么是反馈,什么是需求。产品经理要具备需求洞察力的前提就是领域知识的积累。如果一个用户主动想来用你,但你连这种用户都留不下的时候,这个产品就很危险了。时间流产品一定是在当下满足了很多用户的需求,但同时会让一波用户感到焦虑,想远离。找到核心指标是产品需要关注的,语雀的核心指标是自然留存率和付费客户数。

08 | 产品经理能力进阶:用户洞察、抽象设计到看到远方
https://time.geekbang.org/column/article/599309

我所理解的微服务

微服务

前几天写前端的哥们聊天的时候,得出了这样的一个结论:

前端框架多,后端概念多。

这几天在看微服务相关的东西,spring boot真的是简单了很多。感觉大部分人对java的认识还是停留在10年前的ejb或者ssh的时代,认为写一个java web应用,要写好多接口和配置文件。有了spring boot,开发web应用简直不要太简单。一说到java的微服务,肯定少不了spring boot,spring cloud,这篇文章也只是总结记录了一下自己对于微服务概念的认识,对于spring cloud也是出于学习的状态,以后有了深入的了解,再来修改这篇文章。

1 微服务的概念

先看维基百科上的定义:

Microservices are a more concrete and modern interpretation of service-oriented architectures (SOA) used to build distributed software systems.

引用知乎最高赞的一个答案:

微服务架构强调的第一个重点就是业务系统需要彻底的组件化和服务化,

原有的单个业务系统会拆分为多个可以独立开发,设计,运行和运维的小应用。这些小应用之间通过服务完成交互和集成。每个小应用从前端web ui,到控制层,逻辑层,数据库访问,数据库都完全是独立的一套。

在这里我们不用组件而用小应用这个词更加合适,每个小应用除了完成自身本身的业务功能外,重点就是还需要消费外部其它应用暴露的服务,同时自身也将自身的能力朝外部发布为服务。

用简单的话来概括就是: 微服务架构是一种分布式系统架构,系统通常由多个小的、内部独立的、相互调用的服务组成。每个内部服务都是一个微服务。

微服务诞生的一个目的就是解决单体应用规模增加时所带来的问题。

可以发现,想要了解一个微服务概念,就引出了SOA,单体应用,分布式等抽象的概念,绝望。

2 单体应用 vs SOA vs 微服务

其实架构的演进是 单体应用架构 ——> SOA ——> 微服务。

2.1 单体应用 vs 微服务

单体应用就是一个包或者一个归档中,包含了应用所有的功能,所有的功能都在一个单一的进程中运行。在进行扩展的时候,是通过在多个服务器上复制这些单体应用进行扩展。

一个微服务架构就是把每个功能元素放进一个独立的服务中。在进行扩展的时候,是通过跨服务器分发这些服务进行扩展,只在需要时才复制。

不能说单体应用相对微服务来讲就是不好的,他也有自己的优势:

  • 开发简单
  • 稳定
    所有功能耦合在一起,彼此之间不相互依赖,十分稳定。
  • 性能好
    相对于微服务来说,由于所有的功能都在一个进程中,通讯简单,性能当然强劲。
  • 部署简单

但是随着功能越做越多,网站越来越大,单体应用的一些缺点就暴露了出来:

  • 中心化
  • 耦合
  • 学习成本
    所有的功能都是在一个项目中,造成项目庞大,代码复杂,对于学习维护来说成本较高。
  • 伸缩
    衡量伸缩性的主要标准就是是否可以用多台服务器构建集群,是否容易向集群中添加新的服务器。加入新的服务器是否可以提供和原来服务器无差别的服务、集群中可容纳的总的服务器是否有限制。
    而单体应用如果是无状态的,伸缩当然很容易,但是应用很少能做到无状态的,所以伸缩性比较差。
  • 持续交付
    持续交付描述的软件开发,是从原始需求识别到最终产品部署到生产环境这个过程中,需求以小批量形式在团队的各个角色间顺畅流动,能够以较短地周期完成需求的小粒度频繁交付。
    对于微服务来说,各个服务松耦合,弱化了原来迭代(部分)需要解决的问题,所以有些人就喊“敏捷已死”,但是个人觉得,敏捷开发是一种调优软件开发的方法,现在已经被DevOps,微服务的方式给实践了。

2.2 SOA vs 微服务

  • 面向服务架构(Service-oriented architecture,SOA)

SOA的提出是在企业计算领域,就是要将紧耦合的系统,划分为面向业务的,粗粒度,松耦合,无状态的服务。服务发布出来供其他服务调用,一组互相依赖的服务就构成了SOA架构下的系统。

在企业计算领域,如果不是交易系统的话,并发量都不是很大的,所以大多数情况下,一台服务器就容纳将许许多多的服务,这些服务采用统一的基础设施,可能都运行在一个应用服务器的进程中。虽然说是面向服务了,但还是单一的系统。

  • 微服务

微服务架构大体是从互联网企业兴起的,由于大规模用户,对分布式系统的要求很高,如果像企业计算那样的系统,伸缩就需要多个容纳续续多多的服务的系统实例,前面通过负载均衡使得多个系统成为一个集群。

但这是很不方便的,互联网企业迭代的周期很短,一周可能发布一个版本,甚至可能每天一个版本,而不同的子系统的发布周期是不一样的。而且,不同的子系统也不像原来企业计算那样采用集中式的存储,使用昂贵的Oracle存储整个系统的数据,二是使用MongoDB,HBase,Cassandra等NOSQL数据库和Redis,memcache等分布式缓存。

那么就倾向采用以子系统为分割,不同的子系统采用自己的架构,那么各个服务运行自己的Web容器中,当需要增加计算能力的时候,只需要增加这个子系统或服务的实例就好了,当升级的时候,可以不影响别的子系统。这种组织方式大体上就被称作微服务架构。

说实话,我确实不明白soa和微服务的本质区别,两者说到底都是对外提供接口的一种架构设计方式。微服务与SOA相比,更强调分布式系统的特性,比如横向伸缩性,服务发现,负载均衡,故障转移,高可用。互联网开发对服务治理提出了更多的要求,比如多版本,比如灰度升级,比如服务降级,比如分布式跟踪,这些都是在SOA实践中重视不够的。

Docker容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似Node.js或Spring Boot的技术跑在自己的进程中。可能在几十台计算机中运行成千上万个Docker容器,每个容器都运行着服务的一个实例。随时可以增加某个服务的实例数,或者某个实例崩溃后,在其他的计算机上再创建该服务的新的实例。

待续...

想起一个段子,一个写前端的每天要既要写js和css,还要切图,偶尔还要用node写一下页面的渲染,天天加班,而隔壁的后端们就是简单的数据的增删改查,但是工资却比前端的要高一些,所以心里很不平衡。还有一个阿里出来的人的面试的时候,说自己的主要工作内容就是CRUD,面试官就说那不行,然后这哥们很装逼反问的说,阿里的增删改查能是简单的增删改查吗?

想想也是,后端这些东西,概念一大堆,但是相对于前端来讲还是比较稳定的,我司用的spring版本号还停留在2的阶段。而前端的3个月不学习,再看js,就像新语言一样。

回家写...

kafka

kafka

kafka是一种分布式的、基于发布/订阅的消息系统。我司的很多系统都用kafka来做消息队列,之所以有越来越多的公司在生产环境中使用kafka,主要跟它的关键特性有关。

1 关键特性

  • 近乎实时性的消息处理能力,即使面对海量消息也能高效的存储和查询消息。
  • kafka将消息保存在磁盘中,以顺序读写的方式访问磁盘,避免了随机读写导致的性能瓶颈。
  • 支持批量读写消息,并对消息进行压缩,提高了网络利用率。
  • 支持消息分区,每个分区中的消息保证顺序传输,分区之间可以并发操作。
  • 支持在线增加分区和在线水平扩展。
  • 每个分区有多个副本,一个leader负责读写,其他副本负责与leader同步,提高了容灾能力。

2 核心概念

2.1 Topic

topic是用于存储消息的逻辑概念,可以将其理解为一个队列。

2.2 生产者

生产者的主要工作是生产消息,并将消息按照一定的规则推送到topic中。

2.3 消费者

消费者的主要工作是从topic中拉取消息,并对消息进行消费。

2.4 Broker

一个单独的kafka服务器就是一个broker。broker的主要工作就是接收生产者发过来的消息,分配一个offset,之后保存在磁盘中。

2.5 Cluster和Controller

多个broker可以做成一个集群对外提供服务,每个集群当中会选举出一个broker来担任controller,controller是集群的指挥中心。

controller负责管理分区的状态,管理每个分区的副本状态,监听zookeeper中数据的变化等工作。controller也是一主多从的实现,所有的broker都会监听controller的状态,当leader controller出现问题时,就重新选举一个新的。

2.6 Consumer Group

每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。

kafka 的分配单位是 patition。每个 consumer 都属于一个 group,一个 partition 只能被同一个 group 内的一个 consumer 所消费(也就保障了一个消息只能被 group 内的一个 consuemr 所消费),但是多个 group 可以同时消费这个 partition。

同一个组的消费者可以消费相同的消息,这样就实现了广播的机制。

2.7 partition和Log

  • 分区

    每个topic被划分多个分区,同一个topic下不同分区包含的消息是不同的。同一个topic的不同分区会被分配在不同的broker上。因此可以通过增加服务器并在其上分配partition的方式来增加kafka的并行处理能力。

    每个消息在被添加到分区时,都会被分配一个offset。这个offset是消息在此分区中的唯一编号。kafka通过offset来保证消息在分区中的顺序,offset的顺序性不跨分区,即kafka只保证在同一个分区内的消息是有序的。

  • Log

    Log是一个逻辑概念,可以对应到磁盘上的一个文件夹。Log是由多个segment组成,每个segment对应一个日志文件和索引文件。

    分区在逻辑上对应着一个Log,当生产者将消息写入分区时,实际上是写入到了分区对应的Log中。

    在面对海量的数据时,为了避免出现超大文件,每个日志的文件大小是由限制了,超过了限制,就会创建新的segment。

2.8 副本

kafka对消息进行了冗余备份,每个partition可以用多个副本,每个副本包含的消息是一样的。

每个分区的副本集合中都会选取一个副本作为leader,其他都是follower,follower仅仅是从leader中把数据拉到本地,同步保存到自己的log中。

2.9 zookeeper

kafka 通过 zookeeper 来存储集群的信息。

2.10 ISR集合

目前可用且消息量和leader相差不多的副本集合。isr集合中的副本必须满足两个条件:

  • 副本所在的节点必须维持着与zookeeper的连接。
  • 副本的最后一条消息的offset和leader的最后一条消息的offset的差值不能超过一定的阀值。

3 使用

3.1 Topic的创建和删除

创建

  • controller 在 ZooKeeper 的 /brokers/topics 节点上注册 watcher,当 topic 被创建,则 controller 会通过 watch 得到该 topic 的 分区和副本的分配。
  • controller从 /brokers/ids 读取当前所有可用的 broker 列表,对于 set_p 中的每一个 partition:
    • 从分配给该 partition 的所有 replica(称为AR)中任选一个可用的 broker 作为新的 leader,并将AR设置为新的 ISR。
    • 将新的 leader 和 ISR 写入 /brokers/topics/[topic]/partitions/[partition]/state
  • controller 通过 RPC 向相关的 broker 发送 LeaderAndISRRequest。

删除

  • controller 在 zooKeeper 的 /brokers/topics 节点上注册 watcher,当 topic 被删除,则 controller 会通过 watch 得到该 topic 的 partition/replica 分配。
  • 若 delete.topic.enable=false,结束;否则 controller 注册在 /admin/delete_topics 上的 watch 被 fire,controller 通过回调向对应的 broker 发送 StopReplicaRequest。

3.2 Producer生产消息

写入方式

producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)。

路由

producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个 partition。其路由机制为:

  • 指定了 patition,则直接使用;
  • 未指定 patition 但指定 key,通过对 key 的 value 进行hash 选出一个 patition。
  • patition 和 key 都未指定,使用轮询选出一个 patition。

写入流程

  • producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader。
  • producer 将消息发送给该 leader。
  • leader 将消息写入本地 log。
  • followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK
  • leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK。

生产者发送确认

producer delivery guarantee。

当 producer 向 broker 发送消息时,一旦这条消息被 commit,由于副本的存在,它就不会丢。但是如果 producer 发送数据给 broker 后,遇到网络问题而造成通信中断,那 Producer 就无法判断该条消息是否已经 commit。

虽然 Kafka 无法确定网络故障期间发生了什么,但是 producer 可以生成一种类似于主键的东西,发生故障时幂等性的重试多次,这样就做到了 Exactly once,但目前还并未实现。所以目前默认情况下一条消息从 producer 到 broker 是确保了 At least once,可通过设置 producer 异步发送实现At most once。

3.3 broker 保存消息

存储方式

物理上把 topic 分成一个或多个 patition,每个 patition 物理上对应一个文件夹。

存储策略

无论消息是否被消费,kafka 都会保留所有消息。有两种策略可以删除旧数据:

  • 基于时间
  • 基于文件大小

除此之外,kafka还会进行日志压缩。kafka会在后台启动一个线程,定期将相同key的消息进行合并,只保留最新的value值。

3.4 consumer 消费消息

消费方式

consumer 采用 pull 模式从 broker 中读取数据。

push 模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。

消费者消费确认

consumer delivery guarantee

如果将 consumer 设置为 autocommit,consumer 一旦读到数据立即自动 commit。如果只讨论这一读取消息的过程,那 Kafka 确保了 Exactly once。

但实际使用中应用程序并非在 consumer 读取完数据就结束了,而是要进行进一步处理,而数据处理与 commit 的顺序在很大程度上决定了consumer delivery guarantee:

  • 读完消息先 commit 再处理消息。
  • 读完消息先处理再 commit。
  • 如果一定要做到 Exactly once,就需要协调 offset 和实际操作的输出。

消费者再平衡consumer rebalance

当有 consumer 加入或退出、以及 partition 的改变(如 broker 加入或退出)时会触发 rebalance。

4 kafka高可用的手段

4.1 副本

同一个 partition 可能会有多个副本。没有副本的情况下,一旦 broker 宕机,其上所有 patition 的数据都不可被消费,同时 producer 也不能再将数据存于其上的 patition。引入副本之后,同一个 partition 可能会有多个 备份,而这时需要在这些 副本之间选出一个 leader,producer 和 consumer 只与这个 leader 交互,其它副本作为 follower 从 leader 中复制数据。

4.2 Leader failover

当 partition 对应的 leader 宕机时,需要从 follower 中选举出新 leader。在选举新leader时,一个基本的原则是,新的 leader 必须拥有旧 leader commit 过的所有消息。

kafka 在 zookeeper 中(/brokers/.../state)动态维护了一个 ISR,只有 ISR 里面的成员才能选为 leader。

4.3 broker failover

  • controller 在 zookeeper 的 /brokers/ids/[brokerId] 节点注册 Watcher,当 broker 宕机时 zookeeper 会 fire watch。
  • controller 从 /brokers/ids 节点读取可用broker。
  • controller决定set_p,该集合包含宕机 broker 上的所有 partition。
  • 对 set_p 中的每一个 partition
    • 从/brokers/topics/[topic]/partitions/[partition]/state 节点读取 ISR
    • 决定新 leader。
    • 将新 leader、ISR、controller_epoch 和 leader_epoch 等信息写入 state 节点。
  • 通过 RPC 向相关 broker 发送 leaderAndISRRequest 命令。

4.4 controller failover

当 controller 宕机时会触发 controller failover。

每个 broker 都会在 zookeeper 的 "/controller" 节点注册 watcher,当 controller 宕机时 zookeeper 中的临时节点消失,所有存活的 broker 收到 fire 的通知,每个 broker 都尝试创建新的 controller path,只有一个竞选成功并当选为 controller。

Docker(一):基本命令

Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、bare metal、OpenStack 集群和其他的基础应用平台。 ——《Docker入门教程》

容器技术的发展,使开发模式发生了进一步的变化。前段时间忙着项目的事情,断更了半个月的文章。一直觉得docker就是个工具,所以也没有深入的去研究它,只是掌握了几个命令。这次梳理一下docker,总结一些常用的知识点,以供参考。

对于docker来说,有三个方面是要掌握的:

  • 基本命令

    不必多说,对于一些常用的命令肯定是要掌握的。

  • dockerfile

    是一个文本文件,其中包含了若干条指令,描述构建镜像的细节。

  • docker compose

    由于一个微服务架构可能包含多个实例,一个一个的手动启停肯定不现实。compose就是一个用于定义和运行多容器的工具。

1 Docker架构

docker架构图

docker daemon守护进程

client客户端

images镜像

container容器

registry集中存储与分发镜像的服务

2 安装和卸载

对一个技术的学习,首先最重要的是安装配置环境。想到自己以前大学的时候,总是倒在了配环境这一步,明明按照书上写的来配置,最后还是运行不起来。环境的配置是一个玄学问题。

其实由于技术的发展更新较快,书上讲的东西有可能就和现在的安装配置方式不一样,所以环境配置一定要看官方的文档。

参考官网

3 配置镜像加速器

参考阿里云

4 基本命令

  • 搜索镜像

    docker search java
    
  • 下载镜像

    默认从docker registry上下载最新java镜像

    docker pull java
    

    从reg.itmuch.com上下载标签为7的java镜像

    docker pull reg.itmuch.com/java:7
    
  • 列出镜像

    docker images
    
  • 删除镜像

    docker rmi java
    
  • 删除所有镜像

docker rmi -f ${docker images}
  • 新建并启动一个容器

    启动java镜像并打印

    docker run java /bin/echo 'Hello world'
    

    在后台启动了一个Nginx容器,并进行了端口映射

    -d:后台运行
    
    -P: 随机端口映射
    
    -p:指定端口映射
    
    --network:指定网络模式
    
    docker run -d -p 91:80 nginx d
    
  • 列出容器

    docker ps
    
  • 停止容器

    docker stop 容器ID
    
  • 强行停止容器

    docker kill 容器ID
    
  • 启动动已经停止的容器

    docker start 容器ID
    
  • 重启容器

    docker restart 容器ID
    
  • 进入容器

    docker attach 容器ID
    

    当多个窗口同时attach到同一个容器时,所有窗口都会同步显示,当一个窗口阻塞,其他窗口也无法操作。

  • 使用nsenter进入容器

    docker inspect --format "{{.State.Pid}}" $容器ID
    nsenter --target "$PID" --mount --uts --ipc --net pid
    
  • 删除容器

    docker rm 容器ID
    
  • 删除所有容器

    docker rm -f $(docker ps -a -q)
    

RxJava2快速入门

RxJava2快速入门

引入依赖

compile 'io.reactivex.rxjava2:rxjava:2.0.1'

写法

简单版本

	private static void helloSimple() {
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {
                System.out.println("consumer accept is " + s);
            }
        };

        Observable.just("hello world").subscribe(consumer);
	}

复杂版本

	private static void helloComplex() {
        Observer<String> observer = new Observer<String>() {
            @Override
            public void onSubscribe(Disposable d) {
                System.out.println("onSubscribe: " + d);
            }

            @Override
            public void onNext(String s) {
                System.out.println("onNext: " + s);
            }

            @Override
            public void onError(Throwable e) {
                System.out.println("onError: " + e);
            }

            @Override
            public void onComplete() {
                System.out.println("onComplete: ");
            }
        };

        Observable.just("Hello world").subscribe(observer);
    }

变态版本

	private static void helloPlus() {
        Observer<String> observer = new Observer<String>() {
            @Override
            public void onSubscribe(Disposable d) {
                System.out.println("onSubscribe: " + d);
            }

            @Override
            public void onNext(String s) {
                System.out.println("onNext: " + s);
            }

            @Override
            public void onError(Throwable e) {
                System.out.println("onError: " + e);
            }

            @Override
            public void onComplete() {
                System.out.println("onComplete: ");
            }
        };

        Observable<String> observable = Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> e) throws Exception {
                e.onNext("hello world");
                e.onComplete();
            }
        });

        observable.subscribe(observer);
    }

常用操作符

filter

你早上去吃早餐,师傅是被观察者,说咱这有"包子", "馒头", "花生", "牛奶", "饺子", "春卷", "油条",你仔细想了想,发现你是最喜欢饺子的,所以把其他的都排除掉,
于是你就吃到了饺子。

	private static void helloFilter() {
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {
                System.out.println("accept: " + s);
            }
        };

        Observable.just("包子", "馒头", "花生", "牛奶", "饺子", "春卷", "油条")
                .filter(new Predicate<String>() {
                    @Override
                    public boolean test(String s) throws Exception {
                        System.out.println("test: " + s);
                        return s.equals("饺子");
                    }
                })
                .subscribe(consumer);
    }

Map

map操作符能够完成数据类型的转换。

将String类型转换为Integer类型。

	private static void helloMap() {
        // 观察者观察Integer
        Observer<Integer> observer = new Observer<Integer>() {
            @Override
            public void onSubscribe(Disposable d) {
                System.out.println("onSubscribe: " + d);
            }

            @Override
            public void onNext(Integer s) {
                System.out.println("onNext: " + s);
            }

            @Override
            public void onError(Throwable e) {
                System.out.println("onError: " + e);
            }

            @Override
            public void onComplete() {
                System.out.println("onComplete: ");
            }
        };

        Observable.just("100")
                .map(new Function<String, Integer>() {
                    @Override
                    public Integer apply(String s) throws Exception {
                        return Integer.valueOf(s);
                    }
                })
                .subscribe(observer);
    }

FlatMap

flatmap能够链式地完成数据类型的转换和加工。

遍历一个学校所有班级所有组的所有学生

private void flatmapClassToGroupToStudent() {
    Observable.fromIterable(new School().getClasses())
            //输入是Class类型,输出是ObservableSource<Group>类型
            .flatMap(new Function<Class, ObservableSource<Group>>() {
                @Override
                public ObservableSource<Group> apply(Class aClass) throws Exception {
                    Log.d(TAG, "apply: " + aClass.toString());
                    return Observable.fromIterable(aClass.getGroups());
                }
            })
            //输入类型是Group,输出类型是ObservableSource<Student>类型
            .flatMap(new Function<Group, ObservableSource<Student>>() {
                @Override
                public ObservableSource<Student> apply(Group group) throws Exception {
                    Log.d(TAG, "apply: " + group.toString());
                    return Observable.fromIterable(group.getStudents());
                }
            })
            .subscribe(
                    new Observer<Student>() {
                        @Override
                        public void onSubscribe(Disposable d) {
                            Log.d(TAG, "onSubscribe: ");
                        }

                        @Override
                        public void onNext(Student value) {
                            Log.d(TAG, "onNext: " + value.toString());
                        }

                        @Override
                        public void onError(Throwable e) {

                        }

                        @Override
                        public void onComplete() {

                        }
                    });
}

线程调度

关于RxJava的线程调度,初学者只需要掌握两个api就够够的啦。

subscribeOn

指定Observable在一个指定的线程调度器上创建。只能指定一次,如果指定多次则以第一次为准

observeOn

指定在事件传递,转换,加工和最终被观察者接受发生在哪一个线程调度器。可指定多次,每次指定完都在下一步生效。

常用线程调度器类型

  • Schedulers.single() 单线程调度器,线程可复用
  • Schedulers.newThread() 为每个任务创建新的线程
  • Schedulers.io() 处理io密集型任务,内部是线程池实现,可自动根据需求增长
  • Schedulers.computation() 处理计算任务,如事件循环和回调任务
  • AndroidSchedulers.mainThread() Android主线程调度器

Spring Boot启动原理分析

Spring Boot启动原理分析

我们在开发spring boot应用的时候,一般会遇到如下的启动类:

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

从这段代码可以看出,注解@SpringBootApplication和SpringApplication.run()是比较重要的两个东西。

1 @SpringApplication注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}

在这段代码里,比较重要的只有三个注解:

其实,我们使用这三个注解来修饰springboot的启动类也可以正常运行,如下所示:

@ComponentScan
@EnableAutoConfiguration
@Configuration
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

每次写这三个注解的话,比较繁琐,所以就spring团队就封装了一个@SpringBootApplication

1.1 @configuration

@configuration就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@configuration,SpringBoot社区推荐使用基于JavaConfig的配置形式,所以,这里的启动类标注了@configuration之后,本身其实也是一个IoC容器的配置类。

XML跟config配置方式的区别可以从如下几个方面来说:

  • 表达形式层面
    基于xml的配置方式是这样的:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
         default-lazy-init="true">
      <!--bean定义-->
    </beans>

    基于java config配置方式是这样的:

    @Configuration
    public class MockConfiguration{
        //bean定义
    }
  • 注册bean定义层面
    基于XML的配置形式是这样:

    <bean id="mockService" class="..MockServiceImpl">
        ...
    </bean>

    而基于Java config的配置形式是这样的:

    @Configuration
    public class MockConfiguration{
    	@Bean
    	public MockService mockService(){
        	return new MockServiceImpl();
    	}
    }

    任何一个标注了@bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。

  • 表达依赖注入关系层面
    为了表达bean与bean之间的依赖关系,在XML形式中一般是这样:

    <bean id="mockService" class="..MockServiceImpl">
        <propery name ="dependencyService" ref="dependencyService" />
    </bean>
    
    <bean id="dependencyService" class="DependencyServiceImpl"></bean>

    而基于Java config的配置形式是这样的:

    @Configuration
    public class MockConfiguration{
        @Bean
        public MockService mockService(){
            return new MockServiceImpl(dependencyService());
        }
        
        @Bean
        public DependencyService dependencyService(){
            return new DependencyServiceImpl();
        }
    }

    如果一个bean的定义依赖其他bean,则直接调用对应的JavaConfig类中依赖bean的创建方法就可以了。

1.2 @componentscan

@componentscan的功能其实就是自动扫描并加载符合条件的组件(比如@component@repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。

我们可以通过basePackages等属性来细粒度的定制@componentscan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@componentscan所在类的package进行扫描。

所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages。

1.3 @EnableAutoConfiguration

Spring框架提供了各种名字为@enable开头的Annotation定义,比如@EnableScheduling、@EnableCaching、@EnableMBeanExport等。@EnableAutoConfiguration的理念和做事方式其实一脉相承,简单概括一下就是,借助@import的支持,收集和注册特定场景相关的bean定义。

@EnableAutoConfiguration也是借助@import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器。

@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}

@EnableAutoConfiguration作为一个复合Annotation,

其中,最关键的要属@import(EnableAutoConfigurationImportSelector.class),借助EnableAutoConfigurationImportSelector,@EnableAutoConfiguration借助于SpringFactoriesLoader的支持可以帮助SpringBoot应用将所有符合条件的@configuration配置都加载到当前SpringBoot创建并使用的IoC容器。SpringFactoriesLoader的支持。

1.4 SpringFactoriesLoader

SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。

public abstract class SpringFactoriesLoader {
    public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
        ...
    }

    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader){
        ....
    }
}

配合@EnableAutoConfiguration使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找的Key,获取对应的一组@configuration类。

@EnableAutoConfiguration自动配置流程就是:

  • 从classpath中搜寻所有的META-INF/spring.factories配置文件;
  • 并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@configuration的JavaConfig形式的IoC容器配置类;
  • 然后汇总为一个并加载到IoC容器。

2 SpringApplication

SpringApplication的run该方法的主要流程大体可以归纳如下:

1) 如果我们使用的是SpringApplication的静态run方法,那么,这个方法里面首先要创建一个SpringApplication对象实例,然后调用这个创建好的SpringApplication的实例方法。在SpringApplication实例初始化的时候,它会提前做几件事情:

  • 根据classpath里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为Web应用使用的ApplicationContext类型。
  • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationContextInitializer。
  • 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationListener。
  • 推断并设置main方法的定义类。

2) SpringApplication实例初始化完成并且完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行所有通过SpringFactoriesLoader可以查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。

3) 创建并配置当前Spring Boot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile)。

4) 遍历调用所有SpringApplicationRunListener的environmentPrepared()的方法,告诉他们:“当前SpringBoot应用使用的Environment准备好了咯!”。

5) 如果SpringApplication的showBanner属性被设置为true,则打印banner。

6) 根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext并创建完成,然后根据条件决定是否添加ShutdownHook,决定是否使用自定义的BeanNameGenerator,决定是否使用自定义的ResourceLoader,当然,最重要的,将之前准备好的Environment设置给创建好的ApplicationContext使用。

7) ApplicationContext创建好之后,SpringApplication会再次借助Spring-FactoriesLoader,查找并加载classpath中所有可用的ApplicationContext-Initializer,然后遍历调用这些ApplicationContextInitializer的initialize(applicationContext)方法来对已经创建好的ApplicationContext进行进一步的处理。

8) 遍历调用所有SpringApplicationRunListener的contextPrepared()方法。

9) 最核心的一步,将之前通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext。

10) 遍历调用所有SpringApplicationRunListener的contextLoaded()方法。

11) 调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。

12) 查找当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们。

13) 正常情况下,遍历执行SpringApplicationRunListener的finished()方法、(如果整个过程出现异常,则依然调用所有SpringApplicationRunListener的finished()方法,只不过这种情况下会将异常信息一并传入处理)

去除事件通知点后,整个流程如下图所示:

3 参考资料

Spring Boot干货系列:(三)启动原理解析
SpringBoot揭秘快速构建为服务体系

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.