2014年5月

什么是双机热备 ?

什么是双机热备技术?双机热备,英文”Hot Standby“,是一种常见的服务器高可用技术。顾名思义,A,B两台机器互相做备份,A歇菜了B顶上,B歇菜了A顶上。

showimage-10057963-10009581-c4d4df6cba4fb6fcf79206c9c6fcb774.jpg

双机热备典型应用环境就是:

对于关键性的业务系统,需要有两台或两台以上的服务器完成相同的功能,共同执行同一服务,让它们具有相同的配置,彼此互为备用,当一台服务器出现故障时,不需要人工介入,可以由另一台服务器自动接替工作,保证系统持续运转,实现高可用性。

双机热备的工作模式有三种

根据两台服务器的工作方式可以有三种不同的工作模式,即双机主从模式、双机互备模式和双机双工模式。下面分别予以简单介绍:

双机主从模式
最常见的双机热备模式,即目前通常所说的active/standby 方式,active服务器处于工作状态;而standby服务器处于监控准备状态。当active服务器出现故障的时候,通过软件诊测或手工方式将standby机器激活,保证应用在短时间内完全恢复正常使用。典型应用在证券资金服务器或行情服务器。这是目前采用较多的一种模式,但由于另外一台服务器长期处于后备的状态,从计算资源方面考量,就存在一定的浪费。

双机互备模式
两个相对独立的应用在两台机器同时运行,但彼此均设为备机,当某一台服务器出现故障时,另一台服务器可以在短时间内将故障服务器的应用接管过来,从而保证了应用的持续性,但对服务器的性能要求比较高。服务器配置相对要好。

双机双工模式
目前Cluster(集群)的一种形式,active/active两台服务器均为活动状态,同时运行相同的应用,保证整体的性能,也实现了负载均衡和互为备份。WEB服务器或FTP服务器等用此种方式比较多。

双机热备的实现方式有两种

双机热备有两种实现模式,一种是基于共享的存储设备的方式,另一种是没有共享的存储设备的方式,一般称为纯软件方式。

基于存储共享的双机热备是双机热备的最标准方案。这种方式采用两台(或多台)服务器,使用共享的存储设备(磁盘阵列柜或存储区域网SAN)。两台服务器可以采用热备(主从)、互备、双工(并行)等不同的方式。在工作过程中,两台服务器将以一个虚拟的IP地址对外提供服务,依工作方式的不同,将服务请求发送给其中一台服务器承担。同时,服务器通过心跳线(目前往往采用建立私有网络的方式)侦测另一台服务器的工作状况。当一台服务器出现故障时,另一台服务器根据心跳侦测的情况做出判断,并进行切换,接管服务。对于用户而言,这一过程是全自动的,在很短时间内完成,从而对业务不会造成影响。由于使用共享的存储设备,因此两台服务器使用的实际上是一样的数据,由双机或集群软件对其进行管理。

了解Yii中Log日志机制

想要知道用户在你的程序中做了些什么,我们可以通过用日志的形式记录下来,前提是用户是做的跟数据库有关的操作。我们可以在任何时候进行的增删改操作都可以记录下来,对于Yii中的AR模型我们可以使用behavior(行为)来达到此目的,这样很容易的就可以把日志功能加到AR类里。

首先我们需要建一张日志表:

CREATE TABLE ActiveRecordLog (
  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  description VARCHAR(255) NULL,
  action VARCHAR(20) NULL,
  model VARCHAR(45) NULL,
  idModel INTEGER UNSIGNED NULL,
  field VARCHAR(45) NULL,
  creationdate TIMESTAMP NOT NULL,
  userid VARCHAR(45) NULL,
  PRIMARY KEY(id)
)
TYPE=InnoDB;

接着我们就需要建立相应的model,可以使用gii或者shell工具等。

为了记录用户操作,我们需要创建一个behavior类,一般放在protected/behavior目录下,该类必须继承CActiveRecordBehavior类。

class ActiveRecordLogableBehavior extends CActiveRecordBehavior
{
    private $_oldattributes = array();

    /**
     * save操作
     * @param  [type] $event [description]
     * @return [type]        [description]
     */
    public function afterSave($event)
    {
        //非新记录,即非插入
        if (!$this->Owner->isNewRecord) {

            $newattributes = $this->Owner->getAttributes();             //获得AR类中已修改的各字段值
            $oldattributes = $this->getOldAttributes();                 //之前的旧数据

            //比较新旧数据
            foreach ($newattributes as $name => $value) {
                if (!empty($oldattributes)) {
                    $old = $oldattributes[$name];
                } else {
                    $old = '';
                }

                //如果该字段旧数据与新数据不一样,则进行记录
                if ($value != $old) {
                    //$changes = $name . ' ('.$old.') => ('.$value.'), ';

                    $log=new ActiveRecordLog;                                               //实例log对象
                    $log->description=  'User ' . Yii::app()->user->Name                    //设置日志内容格式,描述具体操作
                                            . ' changed ' . $name . ' for ' 
                                            . get_class($this->Owner) 
                                            . '[' . $this->Owner->getPrimaryKey() .'].';
                    $log->action=       'CHANGE';                                           //设置操作类型为“修改”
                    $log->model=        get_class($this->Owner);
                    $log->idModel=      $this->Owner->getPrimaryKey();                      //获得修改的记录的主键
                    $log->field=        $name;                                              //修改的字段名
                    $log->creationdate= new CDbExpression('NOW()');                         //日志生成时间
                    $log->userid=       Yii::app()->user->id;                               //记录用户id
                    $log->save();                                                           //保存日至到数据库
                }
            }
        } else {//新纪录直接保存操作日志入库
            $log=new ActiveRecordLog;
            $log->description=  'User ' . Yii::app()->user->Name 
                                    . ' created ' . get_class($this->Owner) 
                                    . '[' . $this->Owner->getPrimaryKey() .'].';
            $log->action=       'CREATE';
            $log->model=        get_class($this->Owner);
            $log->idModel=      $this->Owner->getPrimaryKey();
            $log->field=        '';
            $log->creationdate= new CDbExpression('NOW()');
            $log->userid=       Yii::app()->user->id;
            $log->save();
        }
    }

    /**
     * 删除操作
     * @param  [type] $event [description]
     * @return [type]        [description]
     */
    public function afterDelete($event)
    {
        $log=new ActiveRecordLog;
        $log->description=  'User ' . Yii::app()->user->Name . ' deleted ' 
                                . get_class($this->Owner) 
                                . '[' . $this->Owner->getPrimaryKey() .'].';
        $log->action=       'DELETE';
        $log->model=        get_class($this->Owner);
        $log->idModel=      $this->Owner->getPrimaryKey();
        $log->field=        '';
        $log->creationdate= new CDbExpression('NOW()');
        $log->userid=       Yii::app()->user->id;
        $log->save();
    }

    public function afterFind($event)
    {
        //保存查询出来的数据
        $this->setOldAttributes($this->Owner->getAttributes());
    }

    public function getOldAttributes()
    {
        return $this->_oldattributes;
    }

    public function setOldAttributes($value)
    {
        $this->_oldattributes=$value;
    }
}

该behavior行为类中需要使用到ActiveRecordLo类来将日志记录到数据库中,它会为为每次插入、删除记录一条日志,也会为修改的每个字段记录一条日志。

设定的行为已经写好了,那么剩下的就是需要将其绑定到对应的model模型上。我们只需在对应的model里加入以下方法就完成绑定了:

public function behaviors()
{
    return array(
        // 行为类名 => 类文件别名路径
        'ActiveRecordLogableBehavior'=>
            'application.behaviors.ActiveRecordLogableBehavior',
    );
}

 

当然这些都是最基本的简单的记录日志操作,你还可以进行扩展,以满足更高级更多功能的需求。如有问题,留言探讨~

 

 

Yii的CDbCriteria中addSearchCondition与compare的区别

在使用Yii过程中发现CDbCriteria类中addSearchCondition与compare的功能似乎都是用于增加搜索条件的,但仔细看其代码之后还是有区别的。

这里我就讲一下使用方法吧,不讲原理了,讲原理要分析其代码涉及很多。

addSearchCondition第一个参数为搜索的字段名

第二个参数为搜索关键词

第三个参数是设置是否过滤掉你关键词中的“%"百分号和”_“下划线这两个通配符,默认是true,即过滤这两个通配符,并在关键词前后自动加上"%"以进行匹配。第四个参数就是条件连接符,默认为AND,最后一个是Sql 关键字”LIKE",还可以设置为“NOT LIKE"。基本上最后两个参数都不需要自己设置,都使用默认值就好了。

addSearchCondition就大致理解为增加一个LIKE搜索条件即可。

compare

第一个参数一样为搜索的字段名

第二个参数也是关键词,但不同点也在于此,你可以在关键词开头加"<"、">"、"<="、">="、"<>"这四个比较符号,组成的sql语句就类似[字段名][比较符号][搜索关键词],如我搜一个商品价格小于5的东西就这么写:compare('price','<5'),查询相当于这样"price < 5",它会自动提取开头的比较符号,如果你没写比较符号,那默认的就是"="等于符号,写成这样compare('price','5')就相当于'price=5'这种效果。

第三个参数是开启模糊匹配,默认是false关闭的,开启后,会调用上面的addSearchCondition方法,也就又使用了LIKE。compare('price','5',true)即price LIKE %5%,compare('price','<>5',true)即price NOT LIKE %5%,如果使用其他比较符号如compare('price','<=5',true)、compare('price','>5',true)等,使不会增加addSearchCondition的。

第四个参数是条件连接符AND,当然可以设置为其他的。

第五个参数是设置addSearchCondition中第三个条件的,当然你前面的参数要确保能调用addSearchCondition这个方法。

compare可以这么理解,相当于使用"<"、">"、"<="、">="、"<>"来比较值,启用模糊匹配就使用LIKE方式。

 

差不多就是这些吧,如果还有不明白的可以提出,我再讲细一点。

了解PHP设计模式之观察者模式

观察者模式是一种事件系统,意味着这一模式允许某个类观察另一个类的状态,当被观察的类状态发生改变的时候,观察类可以收到通知并且做出相应的动作,观察者模式提供了避免组件之间紧密耦合的另一种方式。

在观察者模式中,被观察者称为subject,观察者称为observer,为了表达这些内容,SPL(Standard PHP Libaray)提供了SplSubject(被观察者)和SplObserver(观察者)两个接口。在编写观察者模式时,只要实现这两个接口即可。接口如下:

 //被观察者接口
interface SplSubject{
   public function attach(SplObserver $observer);//注册观察者(注册的观察者:当我(被观察者)的某个状态改变时,需要通知的对象)
   public function detach(SplObserver $observer);//释放观察者
   public function notify();//通知所有注册的观察者的方法
}
  //观察者接口
  interface SplObserver{
   public function update(SplSubject $subject);//观察者进行更新状态
  }

这一模式的概念是SplSubject类维护了一个特定状态,当这个状态发生变化时,它就会调用notify()方法。调用notify()方法时,所有之前使用attach()方法注册的SplObserver实例的update方法都会被调用。

以下是实现代码:

  <?php
//被观察者实现类
class DemoSubject implements SplSubject {

  private $observers;    //存放观察者的类
  private $value;

  public function __construct() {
    $this->observers = array();
  }
//注册观察者
  public function attach(SplObserver $observer) {
    $this->observers[] = $observer;
  }

 //释放观察者
  public function detach(SplObserver $observer) {
    if($idx = array_search($observer,$this->observers,true)) {
      unset($this->observers[$idx]);
    }
  }

 //通知所有观察者,执行观察者类中的update方法
  public function notify() {
    foreach($this->observers as $observer) {
      $observer->update($this);
    }
  }

/*设置状态,当状态发生任何变化时,都会调用notify方法通知所有的观察者,即调用观察者类中的update方法*/
  public function setState($value) {
    $this->value = $value;
    $this->notify();
  }
//
  public function getValue() {
    return $this->value;
  }

}

//观察者简单类
class DemoObserver implements SplObserver {

  //接收被观察者发送的通知
  public function update(SplSubject $subject) {
    echo 'The new value is '. $subject->getValue();
  }

}

$subject = new DemoSubject();//初始化被观察者
$observer = new DemoObserver();//初始化一个观察者
$subject->attach($observer);//添加一个观察者
$subject->setState(5);//被观察者修改状态。
?>;

输出结果为:

The new value is 5

观察者模式的优点在于,挂接到被观察者上的观察者可多可少,并且不需要提前知道哪个观察者会响应被观察者发出的响应事件。

这种设计模式在很多PHP框架中都被使用到,如Yii等。

了解PHP中crypt加密函数

曾经在分析Yii框架的博客demo中看到使用了这个crypt函数,于是就大致的了解了一下。

crypt() 返回一个基于标准 UNIX DES 算法或系统上其他可用的替代算法的散列字符串,这是手册中写到的,可能有些同学不是很理解这句话,我的理解就是,crypt回返回不同类型的散列字符串,即使用了不同算法返回的。而具体使用哪个算法,还要看所以来的操作系统。

PHP中有几个常量,分别是CRYPT_STD_DES 、CRYPT_EXT_DES、CRYPT_MD5、CRYPT_BLOWFISH、CRYPT_SHA256、CRYPT_SHA512。这些常量不是1就是0,也意味着其所对应的算法是否可用。

我们可以看一下下面代码的输出结果:

if (CRYPT_STD_DES == 1) {
    echo 'Standard DES: ' . crypt('rasmuslerdorf', 'rl') . "<br>";
}

if (CRYPT_EXT_DES == 1) {
    echo 'Extended DES: ' . crypt('rasmuslerdorf', '_J9..rasm') . "<br>";
}

if (CRYPT_MD5 == 1) {
    echo 'MD5:          ' . crypt('rasmuslerdorf', '$1$rasmusle$') . "<br>";
}

if (CRYPT_BLOWFISH == 1) {
    echo 'Blowfish:     ' . crypt('rasmuslerdorf', '$2a$07$usesomesillystringforsalt$') . "<br>";
}

if (CRYPT_SHA256 == 1) {
    echo 'SHA-256:      ' . crypt('rasmuslerdorf', '$5$rounds=5000$usesomesillystringforsalt$') . "<br>";
}

if (CRYPT_SHA512 == 1) {
    echo 'SHA-512:      ' . crypt('rasmuslerdorf', '$6$rounds=5000$usesomesillystringforsalt$') . "<br>";
}

输出结果:

Standard DES: rl.3StKT.4T8M
Extended DES: _J9..rasmBYk8r9AiWNc
MD5: $1$rasmusle$rISCgZzpwk3UhDidwXvin0
Blowfish: $2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi
SHA-256: $5$rounds=5000$usesomesillystri$KqJWpanXZHKq2BOB43TSaYhEWsQ1Lr5QNyPCDH/Tp.6
SHA-512: $6$rounds=5000$usesomesillystri$D4IrlXatmP7rx3P3InaxBeoomnAihCKRVQP22JZ6EY47Wc6BkroIuUUBOov1i.S5KPgErtP/EN5mcO.ChWQW21

第一个结果是基于标准 DES 算法的散列,使用 "./0-9A-Za-z" 字符中的两个字符作为盐值,本例中使用了"rl",你可以试一下添加更多字符如"rlsdfs",输出结果还是不变的。手册中说,如果不提供salt,默认使用该算法,但我试过之后发现是使用的下面md5。

第二个结果是扩展的基于 DES 算法的散列。其盐值为 9 个字符的字符串,由 1 个下划线后面跟着 4 字节循环次数和 4 字节盐值组成。它们被编码成可打印字符,每个字符 6 位,有效位最少的优先。0 到 63 被编码为 "./0-9A-Za-z"。

第三个结果就是使用了md5后的散列,使用一个以 $1$ 开始的 12 字符的字符串盐值。

第四个Blowfish 算法使用如下盐值:“$2a$”,一个两位 cost 参数,“$” 以及 64 位由 “./0-9A-Za-z” 中的字符组合而成的字符串。在盐值中使用此范围之外的字符将导致 crypt() 返回一个空字符串。两位 cost 参数是循环次数以 2 为底的对数,它的范围是 04-31,超出这个范围将导致 crypt() 失败。

第五个结果是 SHA-256 算法,使用一个以 $5$ 开头的 16 字符字符串盐值进行散列。如果盐值字符串以 “rounds=<N>$” 开头,N 的数字值将被用来指定散列循环的执行次数,这点很像 Blowfish 算法的 cost 参数。默认的循环次数是 5000,最小是 1000,最大是 999,999,999。超出这个范围的 N 将会被转换为最接近的值。

第六个是 SHA-512 算法,使用一个以 $6$ 开头的 16 字符字符串盐值进行散列。如果盐值字符串以 “rounds=<N>$” 开头,N 的数字值将被用来指定散列循环的执行次数,这点很像 Blowfish 算法的 cost 参数。默认的循环次数是 5000,最小是 1000,最大是 999,999,999。超出这个范围的 N 将会被转换为最接近的值。


大致的了解了一下crypt几种散列类型,但最终如何应用?

使用crypt加密就是为了即使加密列表落入他人手中,也无法获得其明文,因为该加密是不可逆的。

单纯的使用crypt($input_pwd)会生成不同的随机值。(本人在win系统和centos上获得的都是基于md5的散列值,即$1$开头的随机值)

使用crypt加密方式有很多,你或许可以使用:

<?php

$pwd = 'veitor';		//注册密码明文
crypt($pwd);

?php

将注册密码明文用crypt随机生成的散列值存入数据库。

验证登陆时:

<?php
$input_pwd = 'veitor';		      //表单输入密码明文
crypt($input_pwd, $pwd) == $pwd       //$pwd为上一步数据库中的加密暗文
?>

你需要使用库中的加密暗文作为salt,这样做是指定crypt使用的散列类型。比如我上一步注册生成的暗文是基于MD5的,则暗文以$1$开头多位字符串,那么在这一步中,使用该暗文作为salt,crypt判断$1$就知道使用的散列类型是md5了。(另外,改成crypt($input_pwd, sub_str($pwd,0,12)) == $pwd也是正确的,因为基于md5散列时,salt只使用到前12个字符)

当然你还可以使用crypt和md5两个函数的结合,使得密码更安全,如md5(crypt($pwd))这种方式,生成后的依旧是32位字符,初看就以为是md5加密而已,即使再庞大的数据系统也难以进行暴力破解。

以上只是本人的一点小理解,或许存在偏差,如果你有更好的想法,不妨留个言一起探讨。

 

再谈javascript图片预加载技术

lightbox类效果为了让图片居中显示而使用预加载,需要等待完全加载完毕才能显示,体验不佳(如filick相册的全屏效果)。javascript无法获取img文件头数据,真的是这样吗?本文通过一个巧妙的方法让javascript获取它。

这是大部分人使用预加载获取图片大小的例子:

var imgLoad = function (url, callback) {
	var img = new Image();

	img.src = url;
	if (img.complete) {
		callback(img.width, img.height);
	} else {
		img.onload = function () {
			callback(img.width, img.height);
			img.onload = null;
		};
	};

};

 

可以看到上面必须等待图片加载完毕才能获取尺寸,其速度不敢恭维,我们需要改进。

web应用程序区别于桌面应用程序,响应速度才是最好的用户体验。如果想要速度与优雅兼得,那就必须提前获得图片尺寸,如何在图片没有加载完毕就能获取图片尺寸?

十多年的上网经验告诉我:浏览器在加载图片的时候你会看到图片会先占用一块地然后才慢慢加载完毕,并且不需要预设width与height属性,因为浏览器能够获取图片的头部数据。基于此,只需要使用javascript定时侦测图片的尺寸状态便可得知图片尺寸就绪的状态。

当然实际中会有一些兼容陷阱,如width与height检测各个浏览器的不一致,还有webkit new Image()建立的图片会受以处在加载进程中同url图片影响,经过反复测试后的最佳处理方式:

// 更新:
// 05.27: 1、保证回调执行顺序:error > ready > load;2、回调函数this指向img本身
// 04-02: 1、增加图片完全加载后的回调 2、提高性能

/**
 * 图片头数据加载就绪事件 - 更快获取图片尺寸
 * @version	2011.05.27
 * @author	TangBin
 * @see		http://www.planeart.cn/?p=1121
 * @param	{String}	图片路径
 * @param	{Function}	尺寸就绪
 * @param	{Function}	加载完毕 (可选)
 * @param	{Function}	加载错误 (可选)
 * @example imgReady('http://www.google.com.hk/intl/zh-CN/images/logo_cn.png', function () {
		alert('size ready: width=' + this.width + '; height=' + this.height);
	});
 */
var imgReady = (function () {
	var list = [], intervalId = null,

	// 用来执行队列
	tick = function () {
		var i = 0;
		for (; i < list.length; i++) {
			list[i].end ? list.splice(i--, 1) : list[i]();
		};
		!list.length && stop();
	},

	// 停止所有定时器队列
	stop = function () {
		clearInterval(intervalId);
		intervalId = null;
	};

	return function (url, ready, load, error) {
		var onready, width, height, newWidth, newHeight,
			img = new Image();

		img.src = url;

		// 如果图片被缓存,则直接返回缓存数据
		if (img.complete) {
			ready.call(img);
			load && load.call(img);
			return;
		};

		width = img.width;
		height = img.height;

		// 加载错误后的事件
		img.onerror = function () {
			error && error.call(img);
			onready.end = true;
			img = img.onload = img.onerror = null;
		};

		// 图片尺寸就绪
		onready = function () {
			newWidth = img.width;
			newHeight = img.height;
			if (newWidth !== width || newHeight !== height ||
				// 如果图片已经在其他地方加载可使用面积检测
				newWidth * newHeight > 1024
			) {
				ready.call(img);
				onready.end = true;
			};
		};
		onready();

		// 完全加载完毕的事件
		img.onload = function () {
			// onload在定时器时间差范围内可能比onready快
			// 这里进行检查并保证onready优先执行
			!onready.end && onready();

			load && load.call(img);

			// IE gif动画会循环执行onload,置空onload即可
			img = img.onload = img.onerror = null;
		};

		// 加入队列中定期执行
		if (!onready.end) {
			list.push(onready);
			// 无论何时只允许出现一个定时器,减少浏览器性能损耗
			if (intervalId === null) intervalId = setInterval(tick, 40);
		};
	};
})();

调用例子:

imgReady('http://pic1.hualongxiang.com/attachment/photo/Mon_1405/343656_7b0313991240910abfa97883b96b9.jpg', function () {
	alert('size ready: width=' + this.width + '; height=' + this.height);
});

是不是很简单?这样的方式获取摄影级别照片尺寸的速度往往是onload方式的几十多倍,而对于web普通(800×600内)浏览级别的图片能达到秒杀效果。看了这个再回忆一下你见过的web相册,是否绝大部分都可以重构一下呢?好了,请观赏令人愉悦的 DEMO :

http://www.planeart.cn/demo/imgReady/

(通过测试的浏览器:Chrome、Firefox、Safari、Opera、IE6、IE7、IE8)

 

原文地址:http://www.planeart.cn/?p=1121