Category: 系统设计

WEB监控体系之设备负载监控

By 深空, 2010年04月11日 19:14

  第一次写和工作密切相关的文章,却无从下手,胡乱写起,纯当总结。

  设备负载监控属于硬件级的基础监控,比设备基础监控粒度要粗一些,属于设备基础监控上一层的硬件监控,适合于数量较大、具有集群特性的硬件综合指标监控。当然,其监控数据来源仍为单机设备基础信息。

  单机基础硬件指标大概包括CPU使用率、内存使用率、磁盘I/O、磁盘空间使用率、网卡出入包量、网卡出入流量、平均负载等。那么各种业务逻辑可能对这些指标都会有所侧重,例如WEB服务器比较侧重CPU、包量、流量,而DB比较侧重磁盘I/O、CPU使用率,CACHE则更关注内存使用率、CPU使用率等。对于数量庞大、类型不一的服务器,不可能关注到这么细致的数据信息,所以必须在几个维度进行汇总以便更好实现服务器管理。

  那么设备负载监控系统的设计目标是什么呢?大概总结有以下几点:
  1、减少管理单元,提高维护效率;
  2、方便查看业务总体负载状况;
  3、尽快发现高负载设备以便及时增加设备缓解业务压力;
  4、减少空闲设备量,提高设备复用率,降低设备成本;
  5、发现负载均衡方面的问题

  要实现以上几个目标,首先需要将服务器分门别类。如WEB、DB、CACHE、业务逻辑等。上面提到,这些设备应该具备集群特性,其大概形式如下:

集群示意图

集群示意图


  如上图所示,除灰色部分外,该集群拥有4台一样的设备,每台设备上均安装有1、2、3三种软件,这样这些设备的正常运行状况应该基本一致。当该集群呈现负载较繁忙的状况的时候,可以比较容易复制1-4号设备以增加一台一样的5号设备来降低业务负载。而当该集群负载较空闲的时候,可以将第4号软件部署于该集群下以充分利用设备性能。

  在该集群负载均衡的状况下,单机的负载状况表现出来的特征,应该就是该集群的负载特征,通过管理集群即可映射到管理单机设备,假设有1000台设备,每个集群50台,那么只需要管理20个集群即可,管理单元明显减少。

  在现实情况下,其实无法达到百分百负载均衡,所以还是需要一些算法计算集群的指标。最基本的算法就是MAX、MIN、AVG了。这三个基本可以处理90%以上情况。我曾经设计过比较复杂的公式支持,后来发现基本上用不上。当然算法越粗暴误差越大。如使用MAX计算CPU使用率,那么假如该集群下某台设备由于特殊原因CPU一直占用较高,那么表现在集群上的CPU使用率也会较高,而实际情况可能这个集群相对空闲。而使用AVG求平均数值,那么一些异常设备将会被淹没不能及时发现,所以这里需要根据业务特性做一些权衡和取舍。当然不建议使用更复杂的算法,因为配置维护成本比较高,而且数值计算结果不直观。

  为了修正个别设备引起集群高负载的问题,引入了高负载设备数的指标。假如该集群负载较高且高负载设备数也高于某个比例(如50%)则认为该负载值准确描述集群压力状况。

  接下来看看实际指标的计算方法。

  首先是负载值的定义。考虑到单机指标太多,业务复杂,所以一律是用百分比来反映负载状况。如10%负载、80%负载、200%负载等。这样单位统一直观。而不需要去考虑具体单位和具体数值。以CPU使用率为例,假设当CPU使用率为80%的时候,负载为100%,那么将80定义为CPU使用率的基准值,当CPU使用率为40%的时候,计算出来的负载为50%,而当CPU使用率为100%时,计算出来的负载为125%。同样其他指标需要定义一些基准值做为负载100%的值。例如百M网卡定义80M为100%负载等。

  这样单机所有基础指标均可以使用百分比表示,CPU使用率、内存使用率、磁盘I/O、磁盘空间使用率、网卡出入包量、网卡出入流量等均换算成负载比例,根据设备所属类型(WEB、DB、CACHE、逻辑等)设计权重结合计算公式得到单机负载值,如:

  单机负载 = AVG(CPU使用率*权重/CPU使用率基准,出流量*权重/出流量基准…);

  实际上单机负载的作用只在于计算高负载设备数。因为这样的计算方式累加到集群中的负载值误差会偏大。为了修正这一问题,引入集群指标负载的概念,即:集群的CPU使用率负载、集群流量负载等,由于同一集群的各项指标较相近,这样将同类型指标进行叠加,减少误差,其公式如下:

  集群CPU使用率负载 = AVG(设备1CPU使用率/CPU使用率基准,设备2CPU使用率/CPU使用率基准,…);

  从业务结构上看,会有如下关系图:

逻辑结构示意图

逻辑结构示意图

  以上为现阶段使用的计算关系图,还有另外一种误差较大的关系图如下:

单机管理关系图

单机管理关系图


  上图设备负载计算主要用于单机负载管理上,实际从单机负载直接计算集群负载的误差会较大,所以一般会采用前一种计算逻辑。不过视图2还能较为直观反映某一集群的负载均衡问题。

  实际上负载监控的作用远远高于其起初设计目标。随着业务的增长,可以看到集群负载也随之增长,虽然有波动,但是通过计算后仍会随着业务增长表现出增长趋势,那么系统就可以根据近一段时间内的负载增长状况,结合业务实际增长状况,预测出未来该集群所要达到的负载值,当超过一个临界值的时候(如80%负载),可以有计划的实行扩容操作(增加设备),而不是等到业务突然呈现高负荷、稳定性降低的时候,才紧急进行设备扩充。

  关于负载预测,我在之前有提到过,我是使用线性回归方法计算的,其中最小二乘法计算公式如下:

最小二乘法公式

最小二乘法公式


  下面代码简单枚举历史10个点来计算该设备负载增长率:

//Y坐标值表示设备历史负载
$y = array(52.09, 52.4, 53.29, 54.22, 55.15, 55.83, 56.89, 56.98, 57.55, 57.8);

//X坐标值表示顺序天数
$x = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

//计算X和Y均值
$ax = array_sum($x)/count($x);
$ay = array_sum($y)/count($y);

//计算斜率公式中的分母(em)和分子(ez)
$em = 0;
$ez = 0;
for ($i = 0; $i < count($x); $i++) {
    //分母求和
    $em += (($x[$i] - $ax) * ($y[$i] - $ay));
    //分子求和
    $ez += pow(($x[$i] - $ax), 2);
}

//斜率0.69
echo $em/$ez;

//第十一个点预测负载值58.34
echo $em/$ez * 10 + $ay - ($em/$ez)*$ax;

  理想状况下,将负载维持到一个稳定的值,使得设备使用率达到最高,且业务稳定正常,是一个长期的调整过程,通过不断的增减设备--增加设备以降低负载,减少设备以提高设备使用率,降低设备成本。这是一个多方面的协调工作,不是一个负载监控系统能解决的,但是负载监控提供基础的数据支持,可大大提高业务稳定性,减少突发故障,从而提高业务可用性。这里基本需要一些辅助功能的支持:
  1、通过每日高、低负载设备数累计季度总高、低负载设备数来考核运维人员,以便及时处理高、低负载设备,将集群负载维持在一个相对稳定的范围,并消除明显的负载不均衡状况;
  2、每日处理重点业务高负载设备,或者负载异常设备,提高业务稳定性。通常负载异常往往能反应软件故障,如CPU使用率一直100%而流量非常低,则可能是软件BUG导致,及时推动开发人员处理软件BUG或者优化软件以提高设备使用率和增加业务可用性;
  3、定时、有计划的(如一个月一次)对高负载集群进行设备扩充,能大大降低临时扩充设备的维护成本,并减少临时资源池设备量以降低设备成本;
  4、定时对低负载集群进行合并、下线,减少集群数从而减少管理单元,并增加设备利用率以降低设备成本;
  5、系统建设初期还会涉及到覆盖率问题,及时分享业务的负载监控覆盖率,推动业务运维接入,以便能实现100%覆盖,以提高监控能力。

  那么,在系统建设过程中还会碰到哪些问题呢?可能基础数据准确性是个比较重要的问题。由于单机设备硬件基础指标在一天内波动可能会比较大,所以需要对一天所采集到的基础数据进行处理,如消峰、去毛刺,将一些明显异常点去除,取一个较高点做为采集结果数据,以免得到一些高得恐怖的负载值。负载监控在实时性要求上并没有特别的高,对于长期优化目标来看,只需要按天粒度即可,当然由于其计算的合理性,逐渐提高其监控及时性,如按小时监控粒度,以便能更及时的了解到业务运行状况,这种情况一般用在业务放量、新业务上线、设备扩充后的负载实时监控上,以便作出更快的反应。

  另外在集群的划分、归属、分类、合并、共享上,也就是基础配置、关系数据上需要比较细致的维护,目标是做到尽可能的标准化,各个集群既要功能单一独立,在横向上具有强的可扩充性(图1横向),又要能够提供公用以提高设备复用率,如多个集群的合并(图1纵向)。这个涉及到各个业务逻辑的设计,推动业务逻辑按照这种方向进行部署,这样在自动扩容上能达到更好的效果。当集群负载达到一定的数值,会自动调度设备缓冲池里的设备,根据已有的集群内设备的软件配置,自动初始化并接入业务运营,以达到设备自动调度扩充的能力。

  负载监控作为一个长期性和及时性兼顾的监控系统,不仅在提高业务长期稳定性上发挥重要作用,更在预算、成本优化上提供了非常有力的数据支持,而且在未来自动调度扩容的可行性上给出了明朗的答案,是一个衔接业务监控和基础硬件监控的重要基础监控系统。

(待续……)

VN:F [1.9.3_1094]
Rating: 7.8/10 (16 votes cast)
VN:F [1.9.3_1094]
Rating: +5 (from 11 votes)

《Head First 设计模式》代码之PHP版

By 深空, 2009年12月28日 09:49

  《Head First 设计模式》是本不错的讲解设计模式的书,不像F4写的那么枯燥,应该算是比较容易理解的好书。书中的例子都比较浅显易懂,不过由于是外国佬写的,所以例子的习惯不是很附合中国特色,可能偶尔看起来有些别扭,还有语言习惯也不是中国风。当然��看过这本书之后,你才能深刻理解设计模式到底能为你解决哪些问题,不能为你解决哪些问题(比如不能代替你的编码)。
  我将书中部分代码改成PHP,看下代码再配合概念应该是比较容易理解了。
  策略模式

<?php
/**
 * 策略模式
 * 定义了算法族,分别封装起来,让它们之间可以互相替换,
 * 此模式让算法的变化独立于使用算法的客户。
 */
//飞行行为接口
interface FlyBehavior {
    public function fly();
}
//呱呱叫行为接口
interface QuackBehavior {
    public function quack();
}
//翅膀飞行
class FlyWithWings implements FlyBehavior {
    public function fly() {
        echo "I'm flying!!\n";
    }
}
//不会飞
class FlyNoWay implements FlyBehavior {
    public function fly() {
        echo "I can't fly!\n";
    }
}
class FlyRocketPowered implements FlyBehavior {
    public function fly() {
        echo "I'm flying with a rocket!\n";
    }
}
class Qquack implements QuackBehavior {
    public function quack() {
        echo "Quack\n";
    }
}
class Squeak implements QuackBehavior {
    public function quack() {
        echo "Squeak\n";
    }
}
class MuteQuack implements QuackBehavior {
    public function quack() {
        echo "<< Silence >>\n";
    }
}
abstract class Duck {
    protected $quack_obj;
    protected $fly_obj;
    public abstract function display();

    public function performQuack() {
        $this->quack_obj->quack();
    }
    public function performFly() {
        $this->fly_obj->fly();
    }
    public function swim() {
        echo "All ducks float, even decoys!\n";
    }
    public function setFlyBehavior(FlyBehavior $fb) {
        $this->fly_obj = $fb;
    }
    public function setQuackBehavior(QuackBehavior $qb) {
        $this->quack_obj = $qb;
    }
}

class ModelDuck extends Duck {
    public function __construct() {
        $this->fly_obj = new FlyNoWay();
        $this->quack_obj = new MuteQuack();
    }
    public function display() {
        echo "I'm a model duck!\n";
    }
}

$model = new ModelDuck();
$model->performFly();
$model->performQuack();
//提供新的能力
$model->setFlyBehavior(new FlyRocketPowered());
$model->setQuackBehavior(new Squeak());
$model->performFly();
$model->performQuack();

?>

  单件模式

<?php
/**
 * 单件模式
 * 确保一个类只有一个实例,并提供一个全局访问点。
 */
class MyClass {
    private static $uniqueInstance;
    private function __construct() {

    }
    public static function getInstance() {
        if (self::$uniqueInstance == null) {
            self::$uniqueInstance = new MyClass();
        }
        return self::$uniqueInstance;
    }
}
$myClass = MyClass::getInstance();
var_dump($myClass);
$myClass = MyClass::getInstance();
var_dump($myClass);
?>

  工厂方法模式

<?php
abstract class PizzaStore {
    public function orderPizza($type) {
        $pizza = $this->createPizza($type);

        $pizza->prepare();
        $pizza->bake();
        $pizza->cut();
        $pizza->box();
        return $pizza;
    }

    public abstract function createPizza($type);
}
class NYPizzaStore extends PizzaStore {
    public function createPizza($type) {
        if ($type == "cheese") {
            return new NYStyleCheesePizza();
        } elseif ($type == "veggie") {
            return new NYStyleVeggiePizza();
        } elseif ($type == "clam") {
            return new NYStyleClamPizza();
        } elseif ($type == "papperoni") {
            return new NYStylePapperoniPizza();
        } else {
            return null;

        }
    }
}
class ChicagoPizzaStore extends PizzaStore {
    public function createPizza($type) {
        if ($type == "cheese") {
            return new ChicagoStyleCheesePizza();
        } elseif ($type == "veggie") {
            return new ChicagoStyleVeggiePizza();
        } elseif ($type == "clam") {
            return new ChicagoStyleClamPizza();
        } elseif ($type == "papperoni") {
            return new ChicagoStylePapperoniPizza();
        } else {
            return null;
        }
    }
}
abstract class Pizza {
    public $name;
    public $dough;
    public $sauce;
    public $toppings = array();

    public function prepare() {
        echo "Preparing " . $this->name . "\n";
        echo "Yossing dough...\n";
        echo "Adding sauce...\n";
        echo "Adding toppings: \n";
        for ($i = 0; $i < count($this->toppings); $i++) {
            echo "    " . $this->toppings[$i] . "\n";
        }
    }

    public function bake() {
        echo "Bake for 25 minutes at 350\n";
    }

    public function cut() {
        echo "Cutting the pizza into diagonal slices\n";
    }

    public function box() {
        echo "Place pizza in official PizzaStore box\n";
    }

    public function getName() {
        return $this->name;
    }
}

class NYStyleCheesePizza extends Pizza {
    public function __construct() {
        $this->name = "NY Style Sauce and cheese Pizza";
        $this->dough = "Thin Crust Dough";
        $this->sauce = "Marinara Sauce";

        $this->toppings[] = "Grated Reggiano Cheese";
    }
}

class NYStyleVeggiePizza extends Pizza {
    public function __construct() {
        $this->name = "NY Style Sauce and veggie Pizza";
        $this->dough = "Thin Crust Dough";
        $this->sauce = "Marinara Sauce";

        $this->toppings[] = "Grated Reggiano veggie";
    }
}
class NYStyleClamPizza extends Pizza {
    public function __construct() {
        $this->name = "NY Style Sauce and clam Pizza";
        $this->dough = "Thin Crust Dough";
        $this->sauce = "Marinara Sauce";

        $this->toppings[] = "Grated Reggiano clam";
    }
}
class NYStylePapperoniPizza extends Pizza {
    public function __construct() {
        $this->name = "NY Style Sauce and papperoni Pizza";
        $this->dough = "Thin Crust Dough";
        $this->sauce = "Marinara Sauce";

        $this->toppings[] = "Grated Reggiano papperoni";
    }
}

class ChicagoStyleCheesePizza extends Pizza {
    public function __construct() {
        $this->name = "Chicago Style Deep Dish Cheese Pizza";
        $this->dough = "Extra Thick Crust Dough";
        $this->sauce = "Plum Tomato Sauce";

        $this->toppings[] = "Shredded Mozzarella Cheese";
    }

    public function cut() {
        echo "Cutting the pizza into square slices\n";
    }
}

$myStore = new NYPizzaStore();
$chicagoStore = new ChicagoPizzaStore();
$pizza = $myStore->orderPizza("cheese");
echo "Ethan ordered a " . $pizza->getName() . "\n";

$pizza = $chicagoStore->orderPizza("cheese");
echo "Ethan ordered a " . $pizza->getName() . "\n";

?>

  工厂模式

<?php
abstract class PizzaStore {
    public function orderPizza($type) {
        $pizza = $this->createPizza($type);

        $pizza->prepare();
        $pizza->bake();
        $pizza->cut();
        $pizza->box();
        return $pizza;
    }

    public abstract function createPizza($type);
}
class NYPizzaStore extends PizzaStore {
    public function createPizza($type) {
        $pizza = null;
        $ingredientFactory = new NYPizzaIngredientFactory();
        if ($type == "cheese") {
            $pizza = new CheesePizza($ingredientFactory);
            $pizza->setName('New York Style Cheese Pizza');
        } elseif ($type == "veggie") {
            $pizza = new VeggiePizza($ingredientFactory);
            $pizza->setName('New York Style Veggie Pizza');
        } elseif ($type == "clam") {
            $pizza = new ClamPizza($ingredientFactory);
            $pizza->setName('New York Style Clam Pizza');
        } elseif ($type == "papperoni") {
            $pizza = new PapperoniPizza($ingredientFactory);
            $pizza->setName('New York Style Papperoni Pizza');
        }
        return $pizza;
    }
}
class ChicagoPizzaStore extends PizzaStore {
    public function createPizza($type) {
        if ($type == "cheese") {
            return new ChicagoStyleCheesePizza();
        } elseif ($type == "veggie") {
            return new ChicagoStyleVeggiePizza();
        } elseif ($type == "clam") {
            return new ChicagoStyleClamPizza();
        } elseif ($type == "papperoni") {
            return new ChicagoStylePapperoniPizza();
        } else {
            return null;
        }
    }
}
interface PizzaIngredientFactory {
    public function createDough();
    public function createSauce();
    public function createCheese();
    public function createVeggies();
    public function createPepperoni();
    public function createClam();
}
class NYPizzaIngredientFactory implements PizzaIngredientFactory {
    public function createDough() {
        return new ThinCrustDough();
    }
    public function createSauce() {
        return new MarinaraSauce();
    }
    public function createCheese() {
        return new ReggianoCheese();
    }
    public function createVeggies() {
        $veggies = array(
        new Garlic(),
        new Onion(),
        new Mushroom(),
        new RedPepper(),
        );
        return $veggies;
    }
    public function createPepperoni() {
        return new SlicedPepperoni();
    }
    public function createClam() {
        return new FreshClams();
    }
}
class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory {
    public function createDough() {
        return new ThickCrustDough();
    }
    public function createSauce() {
        return new PlumTomatoSauce();
    }
    public function createCheese() {
        return new Mozzarella();
    }
    public function createVeggies() {
        $veggies = array(
        new BlackOlives(),
        new Spinach(),
        new EggPlant(),
        );
        return $veggies;
    }
    public function createPepperoni() {
        return new SlicedPepperoni();
    }
    public function createClam() {
        return new FrozenClams();
    }
}
abstract class Pizza {
    public $name;
    public $dough;
    public $sauce;
    public $veggies = array();
    public $cheese;
    public $pepperoni;
    public $clam;

    public abstract function prepare();

    public function bake() {
        echo "Bake for 25 minutes at 350\n";
    }

    public function cut() {
        echo "Cutting the pizza into diagonal slices\n";
    }

    public function box() {
        echo "Place pizza in official PizzaStore box\n";
    }

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function __toString() {

    }
}

class CheesePizza extends Pizza {
    public $ingredientFactory;

    public function __construct(PizzaIngredientFactory $ingredientFactory) {
        $this->ingredientFactory = $ingredientFactory;
    }

    public function prepare() {
        echo "Preparing " . $this->name . "\n";
        $this->dough = $this->ingredientFactory->createDough();
        $this->sauce = $this->ingredientFactory->createSauce();
        $this->cheese = $this->ingredientFactory->createCheese();
    }
}

class ClamPizza extends Pizza {
    public $ingredientFactory;

    public function __construct(PizzaIngredientFactory $ingredientFactory) {
        $this->ingredientFactory = $ingredientFactory;
    }

    public function prepare() {
        echo "Preparing " . $this->name . "\n";
        $this->dough = $this->ingredientFactory->createDough();
        $this->sauce = $this->ingredientFactory->createSauce();
        $this->cheese = $this->ingredientFactory->createCheese();
        $clam = $this->ingredientFactory->createClam();
    }
}

$nyPizzaStore = new NYPizzaStore();
$nyPizzaStore->orderPizza('cheese');
?>

  观察者模式

<?php
/**
 * 观察者模式
 * 定义了对象之间的一对多依赖,当一个对象改变状态时,
 * 它的所有依赖者都会收到通知并自动更新。
 */
interface Subject {
    public function registerObserver(Observer $o);
    public function removeObserver(Observer $o);
    public function notifyObservers();
}
interface Observer {
    public function update($temperature, $humidity, $pressure);
}
interface DisplayElement {
    public function display();
}
class WeatherData implements Subject {
    private $observers = array();
    private $temperature;
    private $humidity;
    private $pressure;
    public function __construct() {
        $this->observers = array();
    }
    public function registerObserver(Observer $o) {
        $this->observers[] = $o;
    }
    public function removeObserver(Observer $o) {
        if (($key = array_search($o, $this->observers)) !== false) {
            unset($this->observers[$key]);
        }
    }
    public function notifyObservers() {
        foreach ($this->observers as $observer) {
            $observer->update($this->temperature, $this->humidity, $this->pressure);
        }
    }
    public function measurementsChanged() {
        $this->notifyObservers();
    }
    public function setMeasurements($temperature, $humidity, $pressure) {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->pressure = $pressure;
        $this->measurementsChanged();
    }
}
class CurrentConditionsDisplay implements Observer, DisplayElement {
    private $temperature;
    private $humidity;
    private $weatherData;
    public function __construct(Subject $weatherData) {
        $this->weatherData = $weatherData;
        $weatherData->registerObserver($this);
    }
    public function update($temperature, $humidity, $pressure) {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->display();
    }
    public function display() {
        echo "温度:" . $this->temperature . "; 湿度:" . $this->humidity . "%\n";
    }
}
class StatistionsDisplay implements Observer, DisplayElement {
    private $temperature;
    private $humidity;
    private $pressure;
    private $weatherData;
    public function __construct(Subject $weatherData) {
        $this->weatherData = $weatherData;
        $weatherData->registerObserver($this);
    }
    public function update($temperature, $humidity, $pressure) {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->pressure = $pressure;
        $this->display();
    }
    public function display() {
        echo "温度:" . $this->temperature . "; 湿度:" . $this->humidity . "%; 气压:" . $this->pressure . "\n";
    }
}
$weatherData = new WeatherData();
$currentDisplay = new CurrentConditionsDisplay($weatherData);
$statistionDisplay = new StatistionsDisplay($weatherData);
$weatherData->setMeasurements(10, 20, 30);
$weatherData->removeObserver($currentDisplay);
$weatherData->setMeasurements(30, 40, 50);
?>

  命令模式

<?php

class Light {
    public function __construct() {

    }

    public function on() {
        echo "Light On\n";
    }

    public function off() {
        echo "Light Off\n";
    }
}

interface Command {
    public function execute();
}

class LightOnCommand implements Command {
    public $light;

    public function __construct(Light $light) {
        $this->light = $light;
    }

    public function execute() {
        $this->light->on();
    }
}

class SimpleRemoteControl {
    public $slot;

    public function __construct() {

    }

    public function setCommand(Command $command) {
        $this->slot = $command;
    }

    public function buttonWasPressed() {
        $this->slot->execute();
    }
}

class RemoteControlTest {
    public static function main() {
        $remote = new SimpleRemoteControl();
        $light = new Light();
        $lightOn = new LightOnCommand($light);
        $remote->setCommand($lightOn);
        $remote->buttonWasPressed();
    }
}

RemoteControlTest::main();

?>

  装饰者模式

<?php
/**
 * 装饰着模式
 * 动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
 */
abstract class Beverage {
    public $description = "Unknown Beverage";

    public function getDescription() {
        return $this->description;
    }

    public abstract function cost();
}

abstract class CondimentDecorator extends Beverage {
    //JAVA代码里这里是个抽象类,PHP不允许这么做
    public function getDescription() {
        return $this->description;
    }
}

class Espresso extends Beverage {
    public function __construct() {
        $this->description = "Espresso";
    }

    public function cost() {
        return 1.99;
    }
}

class HouseBlend extends Beverage {
    public function __construct() {
        $this->description = "HouseBlend";
    }

    public function cost() {
        return .89;
    }
}

class DarkRoast extends Beverage {
    public function __construct() {
        $this->description = "DarkRoast";
    }

    public function cost() {
        return .99;
    }
}

class Mocha extends CondimentDecorator {
    public $beverage;

    public function __construct(Beverage $beverage) {
        $this->beverage = $beverage;
    }
    public function getDescription() {
        return $this->beverage->getDescription() . ", Mocha";
    }
    public function cost() {
        return .20 + $this->beverage->cost();
    }
}

class Whip extends CondimentDecorator {
    public $beverage;

    public function __construct(Beverage $beverage) {
        $this->beverage = $beverage;
    }
    public function getDescription() {
        return $this->beverage->getDescription() . ", Whip";
    }
    public function cost() {
        return .10 + $this->beverage->cost();
    }
}

class Soy extends CondimentDecorator {
    public $beverage;

    public function __construct(Beverage $beverage) {
        $this->beverage = $beverage;
    }
    public function getDescription() {
        return $this->beverage->getDescription() . ", Soy";
    }
    public function cost() {
        return .15 + $this->beverage->cost();
    }
}

$beverage = new Espresso();
echo $beverage->getDescription() . "\n";
$beverage2 = new DarkRoast();
$beverage2 = new Mocha($beverage2);
$beverage2 = new Mocha($beverage2);
$beverage2 = new Whip($beverage2);
echo $beverage2->getDescription() . " $" . $beverage2->cost() . "\n";

$beverage3 = new HouseBlend();
$beverage3 = new Soy($beverage3);
$beverage3 = new Mocha($beverage3);
$beverage3 = new Whip($beverage3);
echo $beverage3->getDescription() . " $" . $beverage3->cost() . "\n";
?>

  状态模式

<?php

class GumballMachine {
    const SOLD_OUT = 0;
    const NO_QUARTER = 1;
    const HAS_QUARTER = 2;
    const SOLD = 3;

    public $state = self::SOLD_OUT;
    public $count = 0;

    public function __construct($count) {
        $this->count = $count;
        if ($count > 0) {
            $this->state = self::NO_QUARTER;
        }
    }

    public function insertQuarter() {
        if ($this->state == self::HAS_QUARTER) {
            echo "You can't insert another quarter!\n";
        } else if ($this->state == self::NO_QUARTER) {
            $this->state = self::HAS_QUARTER;
            echo "You inserted a quarter!\n";
        } else if ($this->state == self::SOLD_OUT) {
            echo "You can't insert a quarter, the machine is sold out!\n";
        } else if ($this->state == self::SOLD) {
            echo "Please wait, we're already giving you a gumball!\n";
        }
    }
}

$obj = new GumballMachine(0);
print_r($obj)

?>
VN:F [1.9.3_1094]
Rating: 7.7/10 (15 votes cast)
VN:F [1.9.3_1094]
Rating: +6 (from 8 votes)

基于HTTP长连接的“服务器推”技术的简易聊天室

By 深空, 2009年10月28日 19:43

  关于HTTP长连接的“服务器推”技术原理可以查看IBM的这篇文章,我简单的做了个DEMO:
  首先是首页,包含一个文本输入和一个显示聊天内容的iframe,还有一个隐藏iframe用来提交form表单:

<?php
//chat.php
header('cache-control: private');
header('Content-Type: text/html; charset=utf-8');
?>
<html>
<script type="text/javascript">
function submitChat(obj) {
    obj.submit();
    document.getElementsByName('content')[0].value = '';
}
</script>
<iframe src="./chat_content.php" height="300" width="100%"></iframe>
<iframe name="say" height="0" width="0"></iframe>
<form method="POST" target="say" action="./say.php" onsubmit="submitChat(this)">
<input type="text" size="30" name="content" /> <input type="button" value="say" onclick="submitChat(this.form)" />
</form>
</html>

  另外一个就是保存用户提交的聊天内容了,我简单的写一下文本,而且没有做什么锁定,这个只是简易版本:

<?php
$content = trim($_POST['content']);
if ($content) {
    $fp = fopen('./chat.txt', 'a');
    fwrite($fp, $content . "\n");
    fclose($fp);
    clearstatcache();
}
?>

  接下来看主要的HTTP长连接部分,也就是chat_content.php文件:

<?php
header('cache-control: private');
header('Content-Type: text/html; charset=utf-8');

//测试设置30秒超时,一般会设置比较长时间。
set_time_limit(30);

//这一行是为了搞定IE这个BT
echo str_repeat(' ', 256);

ob_flush();
flush();
$fp = new SplFileObject('./chat.txt', 'r+');
$line = 0;
$totalLine = 0;
while (!$fp->eof()) {
    $fp->current();
    $totalLine++;
    $fp->next();
}
$fp->seek($totalLine);
$i = $totalLine - 1;
while (true) {
    if (!$fp->eof()) {
        if ($content = trim($fp->current())) {
            echo '<div>';
            echo htmlspecialchars($content);
            echo "</div>";
            flush();
            $fp->next();
            $i++;
        }
    } else {
        $fp->seek($i - 1);
        $fp->next();
    }

    {
        //这里可以添加心跳检测后退出循环
    }
    usleep(1000);
}
?>

  我一行行解释一下,其实也比较容易理解:
  06. 设置一个超时时间,由于要保持HTTP长连接,这个时间肯定要比较长,可能要几个小时吧,上面提到的文章里也有说明,这种HTTP长连接只能打开两个,由于浏览器的限制。另外其实即使你设置了一个永不超时,其实上服务器部分(如Apache)的配置文件也可能对HTTP请求设置了最长等待时间,所以也可能效果会不是你想的,一般默认可能都是15分钟超时。如果有兴趣可以自己尝试修改。

  09. 这里输出了一段空白,主要是手册上已经说明了,IE浏览器在前面256个字符是不会直接输出的,所以我们先随便输出些空白,以便让后面的内容输出来,可能其他浏览器也有其他浏览器的设置,具体可以查看PHP手册的frush函数的说明。接下去11、12行就是强制把这些空白符丢给浏览器输出。

  13. ~ 20. 这里主要是为了计算文件行数,以便从这一行后面开始读内容。

  接下去的while循环就是一个死循环了,就是循环输出文件内容,每次判断是否到达文件末尾,如果有用户写入文件,则当前检测肯定不是文件末尾,就将该行读取出来输出,否则将指针往前移动一行,继续循环,每次等待1000微秒,

  39. 如果一直保持长连接,那么即使客户端断开,服务端也不一定能知道客户端已经断开,所以这里可能还需要做一些心跳记录,比如每个用户保持一个心跳flag,每格几秒更新一下最后心跳时间,当检测最后时间很久没更新后,推出这个死循环,关闭这个HTTP连接。

  OK,基本上原理就是这样了,当然这个性能不清楚,有兴趣的自己试试,欢迎交流。

VN:F [1.9.3_1094]
Rating: 7.5/10 (34 votes cast)
VN:F [1.9.3_1094]
Rating: +12 (from 14 votes)

PHP MVC及模板引擎

By 深空, 2009年09月16日 23:00

  模板引擎,这四个字听起来很高深的样子,一般用到“引擎”两字都会感觉比较高级,类似游戏3D引擎、Zend引擎等,其实都是唬人的,骗外行人的。所以在我初学PHP的那会,也因为这四个字导致了我觉得很难而没有去看他到底是什么样一个东西,直到很长时间以后使用Smarty才真正了解模板引擎的原理和作用。Smarty(http://smarty.php.net),PHP官方模板引擎,看名字给人感觉应该很快,其实很慢,即使他有预编译(另一个看起来很高级的名词,同样也是唬人的,下面我会讲到这个)。[注:我刚才点开Smarty发现他说他已经不是一个PHP子项目了,汗,看来确实唬人,哈玩笑^_^]。其实在PHP里,模板引擎扮演着View(其实通俗说就是页面,看英文有时候会给人很高级的错觉)的角色,这是一个很重要的角色,因为用户的交互啊,界面效果啊等等都在这里,这是最终用户看到的你的系统的样子。

  开头就说模板引擎,只是跟大家说明一下这个东西其实没有什么难理解的,明白其原理以后你会发现他是纸老虎,所以你要有信心你会很轻松看完此文。

  为了更好的说明模板引擎所扮演的角色,我不得不也谈谈MVC。这个话题恐怕互联网上谈及的很多,我也只能根据我的理解来描述,可能有不恰当的地方,欢迎讨论。通常的MVC是指Model、View和Controller。也就是模型、视图和控制器。我理解MVC也是在学了PHP不短时间后了,当时请教老廖(http://qeephp.com),才恍然大悟。

  先来说说Controller,也就是控制器,控制器是个什么东西呢?在PHP里他是扮演一个接收用户请求,把用户请求定位到指定数据模型的角色。解释起来感觉不是很好解释,来看一个简单的留言本的例子:

//用户请求可能是 http://www.example.com/guest.php?module=list
$module = $_GET['module'];

switch ($module) {
    case 'list':
        require_once 'list.php';
        break;
    case 'add':
        require_once 'add.php';
        break;
    case 'del':
        require_once 'del.php';
        break;
    default:
        require_once 'list.php';
        break;
}

  是不是看起来很简单好像没什么东西呀,只是根据用户的请求参数包含不同的文件而已。没错,确实很容易,这个switch语句其实就一个最简单的控制器的实现。他控制什么?他控制你根据不同的用户请求参数调用不同的数据模型处理用户请求。那么这里的list可能是一个留言列表,add是添加留言,del是删除留言。Controller的传统实现可以这么简单,当然现在的很多技巧包括根据不同的用户请求包含不同的业务逻辑处理类,比如list自动定位到/model/List.class.php这样的一些技巧性操作等。

  再来说说Model,其实我们一般花比较长时间设计和编写的也是这块内容,也就是具体的业务逻辑实现。比如一个留言列表要处理些什么,都是在这里实现。还是直接看一个Model例子比较直观:

//Guest_List.class.php
class Guest_List {
    public $page = 1;
    public function __construct() {
        $this->db = DB::init($GLOBALS['dsn']);
        $this->page = (int) $_GET['page'];
    }

    public function getList() {
        $begin = $this->page * 10;
        $sql = "SELECT * FROM guest ORDER BY addTime DESC LIMIT $begin, 10";
        return $this->db->getAll($sql);
    }
}

  这里的Guest_List就是一个简单的Model实现,构造函数取得页数page参数,getList方法查询留言列表并返回结果集。那么在list.php里可能是这样调用的:

//list.php
require_once 'Guest_List.class.php';
$model = new Guest_List();
$lists = $model->getList();

  嗯,其实很多MVC框架都是这么实现的,只不过可能加了一些自动调用的机制,会根据用户请求自动调用类,自动执行方法,呵呵。Model大功告成。这里需要明确一点就是,Model只是返回视图上所可能需要用到的数据,他不负责任何和显示有关的事情,那么显示相关的就交给View来做了。我们是不是不知不觉已经把表现和业务逻辑分离了?没错,分离就是这么简单。

  好了,来看看View怎么利用Model返回的数据来显示页面吧。最简单的例子,我们只需要在list.php里增加一行即可。

//list.php
require_once 'Guest_List.class.php';
$model = new Guest_List();
$lists = $model->getList();
//上面是Model,那么下面就是View
require_once 'list.html';

  来看看View都做些什么吧,我们用list.html来表示留言列表所展现给用户的界面文件,用html来命名看起来会更直观一些,他好像是个html文件,负责输出html代码给浏览器。来看看list.html可能长什么样子:


<table>
  <?php foreach ($lists as $value) { ?>
  <tr>
    <td><?php echo htmlspecialchars($value['guest_user_name']);?></td>
    <td><?php echo date('Y-m-d H:i', $value['guest_date_time']);?></td>
    <td><?php echo htmlspecialchars($value['guest_content']);?></td>
  </tr>
  <?php } ?>
</table>

  不难看出来这个文件所做的只不过是遍历留言数组$lists,然后输出每一行的留言,对留言的内容处理做了htmlspecialchars和date转换(与显示相关的处理),除了和显示相关的操作,他没有再做任何业务逻辑了(也不应该有)。

  我发现写到这里真的没有什么好写的了,MVC就是这些(或者再做一些扩展),至于怎么做到表现和业务分离,那么就是在你的Model里只返回数据,也就是你View所需要用到的数据,而你的View拿到这些数据后负责去显示他就可以了,不应该在你的Model里做显示和视觉相关的操作,也不应该在你的View里做一些业务逻辑相关的操作,把这两者分清楚,就自然而然的表现与业务分离了。

  接下来说说负责View的模板引擎吧,其实你在上面应该已经看到了一个最简陋的模板引擎,那就是View部分的 require_once 语句。厄,实在是太简单了,模板引擎其实是调度并解析模板的东西,其中调度模板由 require_once 搞定了,那么解析呢?这里由 PHP 引擎本身来搞定了。哈,没错,我一直都认为 PHP 是个最好的模板引擎。

  不过还是不得不说说传统的模板引擎的实现原理,一般来说会有这么几个步骤:
  1、注册变量,也就是把从Model返回的数据注册到模板引擎中,告诉模板引擎这个变量可以使用,其实所谓的注册也只不过是不得不这么做,因为一般引擎内部函数是没办法直接访问Model返回的变量的(变量作用域的问题),所以不得不加一个注册操作,把这些变量转换从模板引擎类的属性等。
  2、模板解析,就是读取模板文件,按照模板语法将标签解析成 PHP 语法,或者执行一些替换操作,用变量内容替换掉模板标签,其实效果都差不多。
  3、如果不是将变量内容替换掉模板标签,那么基本上第三步就是将注册的变量和解析完的模板融合在一起输出,类似于上面的list.html,是个解析完的文件,然后输出。

  一般模板引擎还会提供不少用于显示内容处理的插件,比如日期转换、字符串处理、生成表格、生成select等,这些给页面制作提供了一些方便。Smarty还包含了一些页面缓存机制,也很不错。

  很多模板引擎都顶着语法简单的嚎头,美其名曰降低美工的学习门槛。其实我不得不问,有多少模板是由美工来做的呢?而且对比两种语法,不觉得 PHP 的简单循环和输出有什么难以理解的,对比下面两种语法:

<!-- <?php foreach ($lists as $value) { ?> -->
<?=value['userName']?>
<!-- <?php } ?> -->

  和

<!-- loop lists value -->
{value['userName']}
<!-- loop -->

  我左看右看都觉得他们差不多,呵呵,与其再学习一套语法,还不如直接用你已经非常熟悉的PHP呢,为什么要虐待自己呢?而且从可维护性的角度来讲,维护PHP语法和维护模板语法,哪种更容易呢?PHP是标准,只要会PHP都知道他怎么写,表示什么,但是模板引擎千奇百怪的,各种语法都有,不是一个统一的标准,我想谁维护一个从来没有用过的模板,都需要花不少时间去学习引擎语法。更何况即使模板可以那样写,最终还是需要一堆正则替换成PHP语法。我敢肯定,前面写的哪种模板引擎语法最终会被转换成它上面那种PHP。其实模板引擎的解析也就是将模板语法转换成PHP语法的过程。抛开效率来说,多此一举。就象《C专家编程》作者说的,即使你能用宏把C写成看起来好像另外一种语言,但是你不要这么做,同样的这句告诫是否适合于模板引擎呢,它看起来很像另外一种语言。当然我这篇文章不是来批判模板引擎的,哈。它既然存在,也有其存在的道理,某些场合还是不得不用的,比如如果你把模板提供给用户去制作和使用,那么你不得不采用标签以限制用户使用PHP语法,来增强系统安全性。

  再来说效率问题,由于模板引擎要解析模板语法,会用到很多正则匹配和替换,那么在实际运行中是比较消耗系统资源的,而且当模板标签非常复杂或者嵌套多层的时候,效率是比较低的,因为有了一种处理方法,就是预编译。所谓的预编译,就是把带有模板语法的模板,通过处理,转换成 PHP 语法的文件,只要模板文件没有被修改,那么直接包含编译后的文件即可,这样就不需要再次替换和匹配,可以大大提高效率,不过由于模板引擎的复杂性,导致编译后的结果文件仍然比我们一般写出来的PHP文件复杂得多。所以其实效率还是远低于直接编写PHP模板的。有兴趣的可以打开一个Smarty编译过的文件,看看其嵌套,其实要比直接循环来得复杂。

  本文写到这里也差不多了,具体模板引擎如何编译如何处理,各个模板引擎的方式都不一样,有兴趣的可以去下载几个比较经典的引擎看看,比如Smarty。随后我附上我自己用的PHP模板引擎。

  PS:其实我觉得MVC应该叫做CMV是不是更符合逻辑呢?没有考证过这个词的由来^_^。

我的真正PHP模板引擎:

<?php
/**
 * 模板引擎
 *
 * Copyright(c) 2005-2008 by 陈毅鑫(深空). All rights reserved
 *
 * To contact the author write to {@link mailto:shenkong@php.net}
 *
 * @author 陈毅鑫(深空)
 * @version $Id: Template.class.php 1687 2008-07-07 01:16:07Z skchen $
 * @package Template
 */

defined('FW') || exit(header('HTTP/1.0 400 Bad Request'));

class Template {
    protected static $obj;

    public $vars;
    public $includeFiles;
    public $includeFile;
    public $templates;
    public $template;
    public $contents;
    protected $_content;
    protected $_contents;
    protected $_path;

    protected function __construct() {
        $this->vars = array();
        require_once ROOT_PATH . "lib/template.func.php";
    }

    /**
     * 初始化模板引擎
     *
     * @return object 模板引擎对象
     */
    public static function &init() {
        if (is_null(self::$obj)) {
            self::$obj = new Template();
        }
        return self::$obj;
    }

    /**
     * 注册模板变量
     *
     * 注册模板变量后在模板里就可以直接使用该变量,注册与被注册变量名不一定要一样
     * 如:$template->assign('var', $value);
     * 意思是将当前变量$value注册成模板变量var,在模板里就可以直接调用$val
     *
     * @param string $var 注册到模板里的变量名的字符串形式,不包含$
     * @param mixed $value 需要注册的变量
     */
    public function assign($var, $value) {
        if (is_array($var)) {
            foreach ($var as $key => $val) {
                $this->vars[$key] = $val;
            }
        } else {
            $this->vars[$var] = $value;
        }
    }

    /**
     * 解析模板文件
     *
     * 解析模板,并将变量植入模板,解析完后返回字符串结果
     *
     * @param unknown_type $templates
     * @return unknown
     */
    public function fetch($templates) {
        if (is_array($templates)) {
            $this->templates = $templates;
        } else {
            $this->templates = func_get_args();
        }
        extract($this->vars);

        $this->_contents = '';
        foreach ($this->templates as $this->template) {
            ob_end_clean();
            ob_start();
            $this->_path = $this->getPath($this->template);
            require $this->_path;
            $this->_content = ob_get_contents();
            ob_end_clean();
            ob_start();
            $this->_contents .= $this->_content;
            $this->contents[$this->template] = $this->_content;
        }
        return $this->_contents;
    }

    public function getPath($path) {
        $path = explode(".", $path);
        $num = count($path);
        if ($num == 1) {
            return ROOT_PATH . "template" . DIRECTORY_SEPARATOR . $path[0] . ".html";
        } elseif ($num > 1) {
            $templatePath = '';
            $templatePath = $path[$num - 1];
            array_pop($path);
            $templatePath = ROOT_PATH . implode(DIRECTORY_SEPARATOR, $path) . DIRECTORY_SEPARATOR . 'template' . DIRECTORY_SEPARATOR . $templatePath . ".html";
            return $templatePath;
        } else {
            return false;
        }
    }

    public function display($templates = array()) {
        if (!is_array($templates)) {
            $templates = func_get_args();
        }
        if (empty($templates)) {
            foreach ($this->templates as $this->template) {
                echo $this->contents[$this->template];
            }
        } else {
            echo $this->fetch($templates);
        }
    }
}

//end of script
<?php
/**
 * 模板扩充函数
 *
 * Copyright(c) 2005 by 陈毅鑫(深空). All rights reserved
 *
 * To contact the author write to {@link mailto:shenkong@php.net}
 *
 * @author 陈毅鑫(深空)
 * @version $Id: template.func.php 1687 2008-07-07 01:16:07Z skchen $
 * @package Template
 */

defined('FW') || exit(header('HTTP/1.0 400 Bad Request'));

/**
 * 包含模板
 *
 * 当你需要在主模板文件里(有些模板引擎称之为layout布局模板,其实不是所有模板都是布局)
 * 再包含其他公共模板的时候,使用该函数进行包含,则所有已注册的变量均可在被包含文件里使
 * 用,貌似支持多层嵌套,没有测试过,参数可以使用数组,也可以使用多个参数,如:
 * <?=includeFile('user.header', 'user.main', 'user.footer')?> 或者
 * <?=includeFile(array('user.header', 'user.main', 'user.footer'))?>
 *
 * @param string|array $filename 模板名(module.templateName形式)
 */
function includeFile($templates) {
    $template = Template::init();
    if (is_array($templates)) {
        $template->includeFiles = $templates;
    } else {
        $template->includeFiles = func_get_args();
    }
    extract($template->vars);
    foreach ($template->includeFiles as $template->includeFile) {
        require $template->getPath($template->includeFile);
    }
}

//end of script
VN:F [1.9.3_1094]
Rating: 8.9/10 (30 votes cast)
VN:F [1.9.3_1094]
Rating: +12 (from 20 votes)

浅谈权限设计

By 深空, 2009年09月13日 21:45

  PHPChina的专家版在谈权限设计,苦于没有权限回帖,特发此博文谈谈简单的权限设计。讨论在这里
  最简单的权限验证,应该是登录态的验证,如果登录,则可以怎样,没有登录,则不能怎样:

if ($isLogin === true) {
    //do something
} else {
    //do nothing
}

  一般使用会话或者Cookie来保存登录态,具体实现不在此文讨论范围。一般权限都和人挂勾,首先识别你是谁,然后看你有能力做什么,然后再确认你的能力在这个地方是否可以使,一个权限验证算是基本上完成。我们围绕这几点来看权限如何去设计。
  首先要能识别操作者是何许人,我们需要一张保存操作者信息的表,也就是通常所说的用户表。简单的用户表如下:

CREATE TABLE user (
    userId int(10) unsigned NOT NULL,
    username varchar(255) NOT NULL,
    PRIMARY KEY (userId)
)

  一般使用一个用户ID来标识一个唯一的用户,可以使用数字,或者直接使用用户名作为主键(如果用户名不重复)。这里我们使用userId来唯一标识一个用户。
  有了用户以后,接下来需要确认这个用户所具有的能力,也就是权限,那么首先我们需要列一下我们的系统总共需要几个权限,比如增、删、改、查等。增加一张权限表:

CREATE TABLE permission (
    permissionId int(10) unsigned NOT NULL ,
    permissionName varchar(255) NOT NULL ,
    PRIMARY KEY (permissionId)
)

  同样的我们以permissionId作为主键来唯一标识一个权限,当然也可以使用permissionName来标识(如果你能确定唯一的话)。我们新增几条记录在这张表里:

+--------------+----------------+
| permissionId | permissionName |
+--------------+----------------+
|            1 | add            |
|            2 | del            |
|            3 | modify         |
|            4 | select         |
+--------------+----------------+

  这里列举了4个权限,简单的表示我们的用户在系统里可能具有的增、删、改、查4种不同的能力。
  接下来把这些能力赋给用户,需要一张对应表来保存:

CREATE TABLE userPermission (
    userId int(10) unsigned NOT NULL,
    permissionId int(10) unsigned NOT NULL,
    PRIMARY KEY (userId, permissionId)
)

  其中将userId和permissionId设置为主键,表示某个用户具有某种权限。表内容可能如下:

+--------+--------------+
| userId | permissionId |
+--------+--------------+
|      1 |            1 |
|      1 |            4 |
|      2 |            1 |
|      2 |            2 |
|      2 |            3 |
|      2 |            4 |
+--------+--------------+

  以上权限配置表明用户1具有增、查权限,用户2具有增、删、改、查权限(嗯可以猜想用户1是个普通用户,用户2是个管理员)。
  写到这里,我发现基本的用户权限系统雏型已经完成了。这么简单?看起来好像确实就是这么简单。一个用户拥有哪些权限,那么只需要勾选相应的权限分配给这个用户。在验证权限的时候,取出用户所拥有的所有权限,然后判断是否存在该权限即可。
  实际上的权限设计要比这个复杂一些,到底复杂在哪里呢?我们接下来分析。当用户比较多而且权限数量比较多的时候,你是不是要每个用户都去勾选一堆权限呢?如何简化这个操作?OK,用户组的概念推出。所谓用户组,就是具有某些权限的一类人的集合。我们赋予用户组某些权限,然后把这个用户加到这个用户组里即可,来看看用户组长什么样子:

CREATE TABLE group (
    groupId int(10) unsigned NOT NULL,
    groupName varchar(255) NOT NULL,
    PRIMARY KEY (groupId)
)

  和用户表类似,我们需要标识一个唯一的组,这里分配一个组ID作为主键来标识。内容可能如下:

+---------+-----------+
| groupId | groupName |
+---------+-----------+
|       1 | user      |
|       2 | admin     |
+---------+-----------+

  有了用户组表后,我们需要把一些权限赋给用户组,就需要一张用户组权限表:

CREATE TABLE groupPermission (
    groupId int(10) unsigned NOT NULL,
    permissionId int(10) unsigned NOT NULL,
    PRIMARY KEY (groupId, permissionId)
)

  我们分配增、查权限给user组,分配增、删、改、查权限给admin组:

+---------+--------------+
| groupId | permissionId |
+---------+--------------+
|       1 |            1 |
|       1 |            4 |
|       2 |            1 |
|       2 |            2 |
|       2 |            3 |
|       2 |            4 |
+---------+--------------+

  用户组表和用户组所对应的权限表有了,那么要把用户分配给一个用户组,就需要一张用户和组的对应关系表:

CREATE TABLE userGroup (
    userId int(10) unsigned NOT NULL,
    groupId int(10) unsigned NOT NULL,
    PRIMARY KEY (userId, groupId)
)

  把用户1赋给user组,把用户2赋给admin组,和一开始我们直接分配权限一样:

+--------+---------+
| userId | groupId |
+--------+---------+
|      1 |       1 |
|      2 |       2 |
+--------+---------+

  很明显这里的配置信息相对比userPermission表少很多,这里只需要记录用户属于什么组就可以了,那么检测用户权限,就需要查出用户所在的组,然后再查出这个组(或者这些组)所拥有的权限,就可以得出用户所具备的权限,再进行验证即可。当然会比直接查询userPermission表绕一点点,不过相对比维护成本,这点点消耗不算什么。更何况我们其实仍然可以保存userPermission表,在分配用户组的时候,同时更新userPermission表即可。
  这里可以看到,除了在分配用户权限方便以外,当你需要更改某类用户权限的时候,你只需要更改其所在组的权限,那么这个组下所有成员的权限也会随之更改,非常方便。
  到这里用户、用户组的权限构成基本完成。它能解决大部分问题,可是我发现它仍然有一些小的问题。比如如果某个用户只有查权限,我不得不再新增一个用户组,搞得用户组也很多,怎么办呢?如果这个用户属于普通用户组,其实可以考虑也分配普通用户组给这个用户,然后再从普通用户组里“扣掉”增权限。要达到这样的效果,怎么处理呢?
  其实很简单,我们刚才没有去掉的userPermission表派上用场,这个表存储了用户的实际单个权限,我们只需要增加一个字段标识用户是拥有这个权限,还是没有这个权限即可,这样可以解决两个问题:一是从现有用户组中扣掉某些权限,二是在现有用户组中,再给这个用户增加用户组以外的权限。来看一下userPermission表:

CREATE TABLE userPermission (
  userId int(10) unsigned NOT NULL,
  permissionId int(10) unsigned NOT NULL,
  has enum('yes','no') NOT NULL,
  PRIMARY KEY (userId, permissionId)
)

  把用户1的增权限去掉,那么内容可能像这样:

+--------+--------------+-----+
| userId | permissionId | has |
+--------+--------------+-----+
|      1 |            1 | no  |
|      1 |            4 | yes |
|      2 |            1 | yes |
|      2 |            2 | yes |
|      2 |            4 | yes |
|      2 |            3 | yes |
+--------+--------------+-----+

  这个用户依然在user组里,只不过user组所拥有的2个权限(add, select),他少了个add(ID为1被标记为no)权限而已。
  嗯,这样做有解决了这个小问题,不过这个功能增强会让分配权限代码更复杂一些,不仅要给用户分配组,还可能需要操作具体权限,让他有或者让他没有相应的权限。
  OK,简单的权限设计全部完成,只不过,细心的读者,你是否意识到,还少点什么呢?没错,即使到这里,整个权限验证还少了一块很重要的部分,那就是用户拥有这些权限,那么他能在哪里使用这些权限。考虑一个例子,一个论坛版主在他所管理的论坛版块里拥有删、改帖子的权限,他在其他非他所管理的版块就没有这些权限,可能在其他论坛版块他像是一个普通用户一样,我们上面讨论的权限设计如何做到这个验证呢?那么我个人觉得,权限设计到上面已经完成了,接下去这种情况,属于业务逻辑层的验证,我们从权限系统中已经获得用户的权限,那么在具体业务逻辑中,和权限进行绑定即可,以上例子可以用一个版块用户关系表来解决这个问题:

CREATE TABLE userBoard (
  userId int(10) unsigned NOT NULL,
  boardId int(10) unsigned NOT NULL,
  PRIMARY KEY (userId, boardId)
)

  标记用户拥有哪些版块的管理权限,那么在严重用户拥有管理权限的时候,还要看当前版块是否是用户管辖内的版块,最终确定用户是否有操作权限。那么我将这类和业务逻辑相关的权限分配归到用户角色里,同样可以创建一系列角色,来管理用户所管辖的范围,比如超级版主,他也是具有管理删、改权限,只不过他的权限作用于全论坛,那么普通版主就需要指定论坛,这样来区分用户组和角色组我想会使整个权限系统更加清晰。
  到这里,权限验证全部完成,以上没有具体实现代码,我相信这样已经足够了,具体的实现代码和业务逻辑由具体的应用实现吧。

VN:F [1.9.3_1094]
Rating: 8.5/10 (25 votes cast)
VN:F [1.9.3_1094]
Rating: +8 (from 12 votes)

京ICP备05002071号 ©2003-2010 深空