PHP云存储Redis的应用场景与Redis实现排行榜功能

 更新时间:2016年11月25日 16:17  点击:2607
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。本文我们来讲解Redis的应用场景实例。

CRS可作为缓存使用,在会话缓存、全页缓存、提升数据库查询性能等场景都有显著的优越性;CRS还可作为Key-Value存储使用,其保存的数据具备高可靠性,可每日生成冷备和通过流水日志回档。在排行榜、计数器应用、实时系统、阅后即焚类需要精准设定过期时间的应用、反垃圾系统等场景下可大大提高效率。

云存储Redis的使用场景,应用场景,产品特点和架构介绍:

使用场景

PHP云存储Redis的应用场景示例


应用场景

PHP云存储Redis的应用场景示例


产品特点

PHP云存储Redis的应用场景示例


架构介绍

PHP云存储Redis的应用场景示例


PHP运行前必备:

使用客户端phpredis,下载和参考地址:https://github.com/phpredis/phpredis

【示例代码】

<?php
  /**以下参数分别填写您的redis实例内网IP,端口号,实例id和密码*/
  $host = "192.168.0.2";
  $port = 6379;
  $instanceid = "c532952f-55dc-4c22-a941-63057e560788";
  $pwd = "1234567q";

  $redis = new Redis();
  //连接redis
  if ($redis->connect($host, $port) == false) {
    die($redis->getLastError());
  }
  //鉴权
  if ($redis->auth($instanceid . ":" . $pwd) == false) {
    die($redis->getLastError());
  }
 
  /**接下来可以愉快的开始操作redis实例,可以参考:https://github.com/phpredis/phpredis */
 
  //设置key
  if ($redis->set("redis", "piaoyi.org") == false) {
    die($redis->getLastError());
  }
  echo "set key redis OK, value is: piaoyi.org\n";
 
  //获取key
  $value = $redis->get("redis");
  echo "get key redis is:".$value."\n";
?>

 一个使用redis实现积分排行榜的小例子:

//积分排行榜类
class Ranks {
  const PREFIX = 'rank:';
  protected $redis = null;
  public function __construct( Redis $redis ) {
    $this->redis = $redis;
  }
  public function addScores( $member, $scores, $date="" ) {
    if ( $date=="" ) $key = self::PREFIX . date( 'Ymd' );
    else $key = self::PREFIX . $date;
    return $this->redis->zIncrBy( $key, $scores, $member );
  }
  function getOneDayRankings( $date, $start, $stop ) {
    $key = self::PREFIX . $date;
    return $this->redis->zRevRange( $key, $start, $stop, true );
  }
  protected function getMultiDaysRankings( $dates, $outKey, $start, $stop ) {
    $keys = array_map( function( $date ) {
        return self::PREFIX . $date;
      }, $dates );
    $weights = array_fill( 0, count( $keys ), 1 );
    $this->redis->zUnion( $outKey, $keys, $weights );//合集,相同key会求和累加
    return $this->redis->zRevRange( $outKey, $start, $stop, true );
  }
  public function getYesterdayTop10() {
    $date = date('Ymd' , strtotime('-1 day'));
    return $this->getOneDayRankings( $date, 0, 9 );
  }
  public static function getCurrentMonthDates() {
    $BeginDate=date('Y-m-01', strtotime(date("Y-m-d"))); //当前月第1天
    $curDate=(int)date('d', strtotime(date("Y-m-d")));
    $dates = array();
    for ( $day = 0; $day < $curDate; $day++ ) {
      $dates[] = date('Ymd' , strtotime("$BeginDate +$day day"));
    }
    return $dates;
  }
  public function getCurrentMonthTop10() {
    $dates = self::getCurrentMonthDates();
    return $this->getMultiDaysRankings( $dates, 'rank:current_month', 0, 9 );
  }
}

上面这个类使用:

//实例化
$rank = new Ranks( $redis );

$rank->addScores(1, 3, "20151106");
$rank->addScores(2, 2, "20151106");
$rank->addScores(3, 1, "20151106");

$rank->addScores(1, 1, "20151107");
$rank->addScores(2, 3, "20151107");
$rank->addScores(3, 2, "20151107");

print_r($rank->getYesterdayTop10());
print_r($rank->getCurrentMonthTop10());
print_r($rank->getOneDayRankings("20151106",0,-1));
print_r($rank->getOneDayRankings("20151107",0,-1));


一些支持的方法:

Connection

connect, open - Connect to a server
pconnect, popen - Connect to a server (persistent)
auth - Authenticate to the server
select - Change the selected database for the current connection
close - Close the connection
setOption - Set client option
getOption - Get client option
ping - Ping the server
echo - Echo the given string

Keys and Strings

Strings(字符串)

append - Append a value to a key
bitcount - Count set bits in a string
bitop - Perform bitwise operations between strings
decr, decrBy - Decrement the value of a key
get - Get the value of a key
getBit - Returns the bit value at offset in the string value stored at key
getRange - Get a substring of the string stored at a key
getSet - Set the string value of a key and return its old value
incr, incrBy - Increment the value of a key
incrByFloat - Increment the float value of a key by the given amount
mGet, getMultiple - Get the values of all the given keys
mSet, mSetNX - Set multiple keys to multiple values
set - Set the string value of a key
setBit - Sets or clears the bit at offset in the string value stored at key
setex, psetex - Set the value and expiration of a key
setnx - Set the value of a key, only if the key does not exist
setRange - Overwrite part of a string at key starting at the specified offset
strlen - Get the length of the value stored in a key

Keys(键)

del, delete - Delete a key
dump - Return a serialized version of the value stored at the specified key.
exists - Determine if a key exists
expire, setTimeout, pexpire - Set a key's time to live in seconds
expireAt, pexpireAt - Set the expiration for a key as a UNIX timestamp
keys, getKeys - Find all keys matching the given pattern
scan - Scan for keys in the keyspace (Redis >= 2.8.0)
migrate - Atomically transfer a key from a Redis instance to another one
move - Move a key to another database
object - Inspect the internals of Redis objects
persist - Remove the expiration from a key
randomKey - Return a random key from the keyspace
rename, renameKey - Rename a key
renameNx - Rename a key, only if the new key does not exist
type - Determine the type stored at key
sort - Sort the elements in a list, set or sorted set
ttl, pttl - Get the time to live for a key
restore - Create a key using the provided serialized value, previously obtained with dump.


Hashes(哈希表)

hDel - Delete one or more hash fields
hExists - Determine if a hash field exists
hGet - Get the value of a hash field
hGetAll - Get all the fields and values in a hash
hIncrBy - Increment the integer value of a hash field by the given number
hIncrByFloat - Increment the float value of a hash field by the given amount
hKeys - Get all the fields in a hash
hLen - Get the number of fields in a hash
hMGet - Get the values of all the given hash fields
hMSet - Set multiple hash fields to multiple values
hSet - Set the string value of a hash field
hSetNx - Set the value of a hash field, only if the field does not exist
hVals - Get all the values in a hash
hScan - Scan a hash key for members


Lists(列表)

blPop, brPop - Remove and get the first/last element in a list
brpoplpush - Pop a value from a list, push it to another list and return it
lIndex, lGet - Get an element from a list by its index
lInsert - Insert an element before or after another element in a list
lLen, lSize - Get the length/size of a list
lPop - Remove and get the first element in a list
lPush - Prepend one or multiple values to a list
lPushx - Prepend a value to a list, only if the list exists
lRange, lGetRange - Get a range of elements from a list
lRem, lRemove - Remove elements from a list
lSet - Set the value of an element in a list by its index
lTrim, listTrim - Trim a list to the specified range
rPop - Remove and get the last element in a list
rpoplpush - Remove the last element in a list, append it to another list and return it (redis >= 1.1)
rPush - Append one or multiple values to a list
rPushx - Append a value to a list, only if the list exists


Sets(集合)

sAdd - Add one or more members to a set
sCard, sSize - Get the number of members in a set
sDiff - Subtract multiple sets
sDiffStore - Subtract multiple sets and store the resulting set in a key
sInter - Intersect multiple sets
sInterStore - Intersect multiple sets and store the resulting set in a key
sIsMember, sContains - Determine if a given value is a member of a set
sMembers, sGetMembers - Get all the members in a set
sMove - Move a member from one set to another
sPop - Remove and return a random member from a set
sRandMember - Get one or multiple random members from a set
sRem, sRemove - Remove one or more members from a set
sUnion - Add multiple sets
sUnionStore - Add multiple sets and store the resulting set in a key
sScan - Scan a set for members


Sorted sets(有序集合)

zAdd - Add one or more members to a sorted set or update its score if it already exists
zCard, zSize - Get the number of members in a sorted set
zCount - Count the members in a sorted set with scores within the given values
zIncrBy - Increment the score of a member in a sorted set
zInter - Intersect multiple sorted sets and store the resulting sorted set in a new key
zRange - Return a range of members in a sorted set, by index
zRangeByScore, zRevRangeByScore - Return a range of members in a sorted set, by score
zRangeByLex - Return a lexigraphical range from members that share the same score
zRank, zRevRank - Determine the index of a member in a sorted set
zRem, zDelete - Remove one or more members from a sorted set
zRemRangeByRank, zDeleteRangeByRank - Remove all members in a sorted set within the given indexes
zRemRangeByScore, zDeleteRangeByScore - Remove all members in a sorted set within the given scores
zRevRange - Return a range of members in a sorted set, by index, with scores ordered from high to low
zScore - Get the score associated with the given member in a sorted set
zUnion - Add multiple sorted sets and store the resulting sorted set in a new key
zScan - Scan a sorted set for members


Transactions(事务)

multi, exec, discard - Enter and exit transactional mode
watch, unwatch - Watches a key for modifications by another client.


Server(服务器端命令)

bgrewriteaof - Asynchronously rewrite the append-only file
bgsave - Asynchronously save the dataset to disk (in background)
config - Get or Set the Redis server configuration parameters
dbSize - Return the number of keys in selected database
flushAll - Remove all keys from all databases
flushDB - Remove all keys from the current database
info - Get information and statistics about the server
lastSave - Get the timestamp of the last disk save
resetStat - Reset the stats returned by info method.
save - Synchronously save the dataset to disk (wait to complete)
slaveof - Make the server a slave of another instance, or promote it to master
time - Return the current server time
slowlog - Access the Redis slowlog entries




使用 Redis 实现排行榜功能



排行榜功能是一个很普遍的需求。使用 Redis 中有序集合的特性来实现排行榜是又好又快的选择。

一般排行榜都是有实效性的,比如“用户积分榜”。如果没有实效性一直按照总榜来排,可能榜首总是几个老用户,对于新用户来说,那真是太令人沮丧了。

首先,来个“今日积分榜”吧,排序规则是今日用户新增积分从多到少。

那么用户增加积分时,都操作一下记录当天积分增加的有序集合。
假设今天是 2015 年 04 月 01 日,UID 为 1 的用户因为某个操作,增加了 5 个积分。
Redis 命令如下:

ZINCRBY rank:20150401 5 1

假设还有其他几个用户也增加了积分:

ZINCRBY rank:20150401 1 2
ZINCRBY rank:20150401 10 3

看看现在有序集合 rank:20150401 中的数据(withscores 参数可以附带获取元素的 score):

ZRANGE rank:20150401 0 -1 withscores

1) "2"
2) "1"
3) "1"
4) "5"
5) "3"
6) "10"

按照分数从高到低,获取 top10:

ZREVRANGE rank:20150401 0 9 withscores

1) "3"
2) "10"
3) "1"
4) "5"
5) "2"
6) "1"

因为只有三个元素,所以就查询出了这些数据。

如果每天记录当天的积分排行榜,那么其他花样百出的榜单也就简单了。
比如“昨日积分榜”:

ZREVRANGE rank:20150331 0 9 withscores

利用并集实现多天的积分总和,实现“上周积分榜”:

ZUNIONSTORE rank:last_week 7 rank:20150323 rank:20150324 rank:20150325 rank:20150326 rank:20150327 rank:20150328 rank:20150329 WEIGHTS 1 1 1 1 1 1 1

这样就将 7 天的积分记录合并到有序集合 rank:last_week 中了。权重因子 WEIGHTS 如果不给,默认就是 1。为了不隐藏细节,特意写出。
那么查询上周积分榜 Top10 的信息就是:

ZREVRANGE rank:last_week  0 9 withscores

“月度榜”、“季度榜”、“年度榜”等等就以此类推。

下面给出一个 PHP 版的简单实现。使用 Redis 依赖于 PHP 扩展 PhpRedis,代码还依赖于 Carbon 库,用于处理时间。代码量很少,所以就不敲注释了。

<?php

namespace Blog\Redis;

use \Redis;
use Carbon\Carbon;


class Ranks {

    const PREFIX = 'rank:';

    protected $redis = null;


    public function __construct(Redis $redis) {
        $this->redis = $redis;
    }


    public function addScores($member, $scores) {
        $key = self::PREFIX . date('Ymd');
        return $this->redis->zIncrBy($key, $scores, $member);
    }


    protected function getOneDayRankings($date, $start, $stop) {
        $key = self::PREFIX . $date;
        return $this->redis->zRevRange($key, $start, $stop, true);
    }


    protected function getMultiDaysRankings($dates, $outKey, $start, $stop) {
        $keys = array_map(function($date) {
            return self::PREFIX . $date;
        }, $dates);

        $weights = array_fill(0, count($keys), 1);
        $this->redis->zUnion($outKey, $keys, $weights);
        return $this->redis->zRevRange($outKey, $start, $stop, true);
    }


    public function getYesterdayTop10() {
        $date = Carbon::now()->subDays(1)->format('Ymd');
        return $this->getOneDayRankings($date, 0, 9);
    }


    public static function getCurrentMonthDates() {
        $dt = Carbon::now();
        $days = $dt->daysInMonth;

        $dates = array();
        for ($day = 1; $day <= $days; $day++) {
            $dt->day = $day;
            $dates[] = $dt->format('Ymd');
        }
        return $dates;
    }


    public function getCurrentMonthTop10() {
        $dates = self::getCurrentMonthDates();
        return $this->getMultiDaysRankings($dates, 'rank:current_month', 0, 9);
    }

}

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。

一、什么是单例模式?

1、含义   

   作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统全局地提供这个实例。它不会创建实例副本,而是会向单例类内部存储的实例返回一个引用。

2、单例模式的三个要点:

(1). 需要一个保存类的唯一实例的静态成员变量:
private static $_instance;  
 

(2). 构造函数和克隆函数必须声明为私有的,防止外部程序new类从而失去单例模式的意义:
private function __construct()   
{   
    $this->_db = pg_connect('xxxx');  
}   
private function __clone()  
{  
}//覆盖__clone()方法,禁止克隆
 
   
(3). 必须提供一个访问这个实例的公共的静态方法(通常为getInstance方法),从而返回唯一实例的一个引用
public static function getInstance()    
{    
    if(! (self::$_instance instanceof self) )   
    {    
        self::$_instance = new self();    
    }  
    return self::$_instance;    
 
}   


二、为什么要使用单例模式?

多数人都是从单例模式的字面上的意思来理解它的用途, 认为这是对系统资源的节省, 可以避免重复实例化, 是一种"计划生育".   而PHP每次执行完页面都是会从内存中清理掉所有的资源. 因而PHP中的单例实际每次运行都是需要重新实例化的, 这样就失去了单例重复实例化的意义了. 单单从这个方面来说, PHP的单例的确有点让各位失望. 但是单例仅仅只有这个功能和应用吗? 答案是否定的,我们一起来看看。

php的应用主要在于数据库应用, 所以一个应用中会存在大量的数据库操作, 在使用面向对象的方式开发时(废话), 如果使用单例模式, 则可以避免大量的new 操作消耗的资源。
如果系统中需要有一个类来全局控制某些配置信息, 那么使用单例模式可以很方便的实现. 这个可以参看zend Framework的FrontController部分。
在一次页面请求中, 便于进行调试, 因为所有的代码(例如数据库操作类db)都集中在一个类中, 我们可以在类中设置钩子, 输出日志,从而避免到处var_dump, echo。


1、PHP缺点:        

PHP语言是一种解释型的脚本语言,这种运行机制使得每个PHP页面被解释执行后,所有的相关资源都会被回收。也就是说,PHP在语言级别上没有办法让某个对象常驻内存,这和asp.net、Java等编译型是不同的,比如在Java中单例会一直存在于整个应用程序的生命周期里,变量是跨页面级的,真正可以做到这个实例在应用程序生命周期中的唯一性。然而在PHP中,所有的变量无论是全局变量还是类的静态成员,都是页面级的,每次页面被执行时,都会重新建立新的对象,都会在页面执行完毕后被清空,这样似乎PHP单例模式就没有什么意义了,所以PHP单例模式我觉得只是针对单次页面级请求时出现多个应用场景并需要共享同一对象资源时是非常有意义的。

2、单例模式在PHP中的应用场合:

(1)、应用程序与数据库交互

一个应用中会存在大量的数据库操作,比如过数据库句柄来连接数据库这一行为,使用单例模式可以避免大量的new操作,因为每一次new操作都会消耗内存资源和系统资源。

(2)、控制配置信息

如果系统中需要有一个类来全局控制某些配置信息, 那么使用单例模式可以很方便的实现.

三、如何实现单例模式?

1、普通的数据库访问例子:

<?php  
......  
//初始化一个数据库句柄  
$db = new DB(...);  
  
//添加用户信息  
$db->addUserInfo(...);  
  
......  
  
//在函数中访问数据库,查找用户信息  
function getUserInfo()  
{  
    $db = new DB(...);//再次new 数据库类,和数据库建立连接  
    $db = query(....);//根据查询语句访问数据库  
}  
  
?>




2、应用单例模式对数据库进行操作:

<?php 
class DB    
{    
    private $_db;    
    private static $_instance;    
    
    private function __construct(...)    
    {    
        $this->_db = pg_connect(...);//postgrsql    
    }    
    
    private function __clone() {};  //覆盖__clone()方法,禁止克隆    
    
    public static function getInstance()    
    {    
        if(! (self::$_instance instanceof self) ) {    
            self::$_instance = new self();    
        }    
        return self::$_instance;    
    }    
    
    public function addUserInfo(...)  
    {  
    }  
     public function getUserInfo(...)  
    {   
    }  
  
}  
  
//test    
$db = DB::getInstance();    
$db->addUserInfo(...);    
$db->getUserInfo(...);   
  
?>

下面的代码是PDO操作数据库类的一个封装,采用了单例模式:

<?php
/**
 * MyPDO
 */
class MyPDO
{
    protected static $_instance = null;
    protected $dbName = '';
    protected $dsn;
    protected $dbh;
    
    /**
     * 构造
     * 
     * @return MyPDO
     */
    private function __construct($dbHost, $dbUser, $dbPasswd, $dbName, $dbCharset)
    {
        try {
            $this->dsn = 'mysql:host='.$dbHost.';dbname='.$dbName;
            $this->dbh = new PDO($this->dsn, $dbUser, $dbPasswd);
            $this->dbh->exec('SET character_set_connection='.$dbCharset.', character_set_results='.$dbCharset.', character_set_client=binary');
        } catch (PDOException $e) {
            $this->outputError($e->getMessage());
        }
    }
    
    /**
     * 防止克隆
     * 
     */
    private function __clone() {}
    
    /**
     * Singleton instance
     * 
     * @return Object
     */
    public static function getInstance($dbHost, $dbUser, $dbPasswd, $dbName, $dbCharset)
    {
        if (self::$_instance === null) {
            self::$_instance = new self($dbHost, $dbUser, $dbPasswd, $dbName, $dbCharset);
        }
        return self::$_instance;
    }
    
    /**
     * Query 查询
     *
     * @param String $strSql SQL语句
     * @param String $queryMode 查询方式(All or Row)
     * @param Boolean $debug
     * @return Array
     */
    public function query($strSql, $queryMode = 'All', $debug = false)
    {
        if ($debug === true) $this->debug($strSql);
        $recordset = $this->dbh->query($strSql);
        $this->getPDOError();
        if ($recordset) {
            $recordset->setFetchMode(PDO::FETCH_ASSOC);
            if ($queryMode == 'All') {
                $result = $recordset->fetchAll();
            } elseif ($queryMode == 'Row') {
                $result = $recordset->fetch();
            }
        } else {
            $result = null;
        }
        return $result;
    }
    
    /**
     * Update 更新
     *
     * @param String $table 表名
     * @param Array $arrayDataValue 字段与值
     * @param String $where 条件
     * @param Boolean $debug
     * @return Int
     */
    public function update($table, $arrayDataValue, $where = '', $debug = false)
    {
        $this->checkFields($table, $arrayDataValue);
        if ($where) {
            $strSql = '';
            foreach ($arrayDataValue as $key => $value) {
                $strSql .= ", `$key`='$value'";
            }
            $strSql = substr($strSql, 1);
            $strSql = "UPDATE `$table` SET $strSql WHERE $where";
        } else {
            $strSql = "REPLACE INTO `$table` (`".implode('`,`', array_keys($arrayDataValue))."`) VALUES ('".implode("','", $arrayDataValue)."')";
        }
        if ($debug === true) $this->debug($strSql);
        $result = $this->dbh->exec($strSql);
        $this->getPDOError();
        return $result;
    }
    
    /**
     * Insert 插入
     *
     * @param String $table 表名
     * @param Array $arrayDataValue 字段与值
     * @param Boolean $debug
     * @return Int
     */
    public function insert($table, $arrayDataValue, $debug = false)
    {
        $this->checkFields($table, $arrayDataValue);
        $strSql = "INSERT INTO `$table` (`".implode('`,`', array_keys($arrayDataValue))."`) VALUES ('".implode("','", $arrayDataValue)."')";
        if ($debug === true) $this->debug($strSql);
        $result = $this->dbh->exec($strSql);
        $this->getPDOError();
        return $result;
    }
    
    /**
     * Replace 覆盖方式插入
     *
     * @param String $table 表名
     * @param Array $arrayDataValue 字段与值
     * @param Boolean $debug
     * @return Int
     */
    public function replace($table, $arrayDataValue, $debug = false)
    {
        $this->checkFields($table, $arrayDataValue);
        $strSql = "REPLACE INTO `$table`(`".implode('`,`', array_keys($arrayDataValue))."`) VALUES ('".implode("','", $arrayDataValue)."')";
        if ($debug === true) $this->debug($strSql);
        $result = $this->dbh->exec($strSql);
        $this->getPDOError();
        return $result;
    }
    
    /**
     * Delete 删除
     *
     * @param String $table 表名
     * @param String $where 条件
     * @param Boolean $debug
     * @return Int
     */
    public function delete($table, $where = '', $debug = false)
    {
        if ($where == '') {
            $this->outputError("'WHERE' is Null");
        } else {
            $strSql = "DELETE FROM `$table` WHERE $where";
            if ($debug === true) $this->debug($strSql);
            $result = $this->dbh->exec($strSql);
            $this->getPDOError();
            return $result;
        }
    }
    
    /**
     * execSql 执行SQL语句
     *
     * @param String $strSql
     * @param Boolean $debug
     * @return Int
     */
    public function execSql($strSql, $debug = false)
    {
        if ($debug === true) $this->debug($strSql);
        $result = $this->dbh->exec($strSql);
        $this->getPDOError();
        return $result;
    }
    
    /**
     * 获取字段最大值
     * 
     * @param string $table 表名
     * @param string $field_name 字段名
     * @param string $where 条件
     */
    public function getMaxValue($table, $field_name, $where = '', $debug = false)
    {
        $strSql = "SELECT MAX(".$field_name.") AS MAX_VALUE FROM $table";
        if ($where != '') $strSql .= " WHERE $where";
        if ($debug === true) $this->debug($strSql);
        $arrTemp = $this->query($strSql, 'Row');
        $maxValue = $arrTemp["MAX_VALUE"];
        if ($maxValue == "" || $maxValue == null) {
            $maxValue = 0;
        }
        return $maxValue;
    }
    
    /**
     * 获取指定列的数量
     * 
     * @param string $table
     * @param string $field_name
     * @param string $where
     * @param bool $debug
     * @return int
     */
    public function getCount($table, $field_name, $where = '', $debug = false)
    {
        $strSql = "SELECT COUNT($field_name) AS NUM FROM $table";
        if ($where != '') $strSql .= " WHERE $where";
        if ($debug === true) $this->debug($strSql);
        $arrTemp = $this->query($strSql, 'Row');
        return $arrTemp['NUM'];
    }
    
    /**
     * 获取表引擎
     * 
     * @param String $dbName 库名
     * @param String $tableName 表名
     * @param Boolean $debug
     * @return String
     */
    public function getTableEngine($dbName, $tableName)
    {
        $strSql = "SHOW TABLE STATUS FROM $dbName WHERE Name='".$tableName."'";
        $arrayTableInfo = $this->query($strSql);
        $this->getPDOError();
        return $arrayTableInfo[0]['Engine'];
    }
    
    /**
     * beginTransaction 事务开始
     */
    private function beginTransaction()
    {
        $this->dbh->beginTransaction();
    }
    
    /**
     * commit 事务提交
     */
    private function commit()
    {
        $this->dbh->commit();
    }
    
    /**
     * rollback 事务回滚
     */
    private function rollback()
    {
        $this->dbh->rollback();
    }
    
    /**
     * transaction 通过事务处理多条SQL语句
     * 调用前需通过getTableEngine判断表引擎是否支持事务
     *
     * @param array $arraySql
     * @return Boolean
     */
    public function execTransaction($arraySql)
    {
        $retval = 1;
        $this->beginTransaction();
        foreach ($arraySql as $strSql) {
            if ($this->execSql($strSql) == 0) $retval = 0;
        }
        if ($retval == 0) {
            $this->rollback();
            return false;
        } else {
            $this->commit();
            return true;
        }
    }
    /**
     * checkFields 检查指定字段是否在指定数据表中存在
     *
     * @param String $table
     * @param array $arrayField
     */
    private function checkFields($table, $arrayFields)
    {
        $fields = $this->getFields($table);
        foreach ($arrayFields as $key => $value) {
            if (!in_array($key, $fields)) {
                $this->outputError("Unknown column `$key` in field list.");
            }
        }
    }
    
    /**
     * getFields 获取指定数据表中的全部字段名
     *
     * @param String $table 表名
     * @return array
     */
    private function getFields($table)
    {
        $fields = array();
        $recordset = $this->dbh->query("SHOW COLUMNS FROM $table");
        $this->getPDOError();
        $recordset->setFetchMode(PDO::FETCH_ASSOC);
        $result = $recordset->fetchAll();
        foreach ($result as $rows) {
            $fields[] = $rows['Field'];
        }
        return $fields;
    }
    
    /**
     * getPDOError 捕获PDO错误信息
     */
    private function getPDOError()
    {
        if ($this->dbh->errorCode() != '00000') {
            $arrayError = $this->dbh->errorInfo();
            $this->outputError($arrayError[2]);
        }
    }
    
    /**
     * debug
     * 
     * @param mixed $debuginfo
     */
    private function debug($debuginfo)
    {
        var_dump($debuginfo);
        exit();
    }
    
    /**
     * 输出错误信息
     * 
     * @param String $strErrMsg
     */
    private function outputError($strErrMsg)
    {
        throw new Exception('MySQL Error: '.$strErrMsg);
    }
    
    /**
     * destruct 关闭数据库连接
     */
    public function destruct()
    {
        $this->dbh = null;
    }
}
?>



调用方法:

<?php
require 'MyPDO.class.php';
$db = MyPDO::getInstance('localhost', 'root', '123456', 'test', 'utf8');
$db->query("select count(*) frome table");
$db->destruct();
?>


在php中我们如果要导入excel数据我们通常会使用phpexcel插件了,但是有朋友会发与使用phpexcel导出数据出现身份证后四位是0000情况了,下面我们就来看解决办法。


最近做一个php项目的时候,遇到一个问题。

功能:使用phpexcel导出数据。

问题描述:导出身份证后四位是0000

这是因为在excel中如果在一个默认的格中输入或复制超长数字字符串,它会显示为科学计算法。

 

方法一:设置单元格为文本

$objPHPExcel = new PHPExcel();

$objPHPExcel->setActiveSheetIndex(0);

$objPHPExcel->getActiveSheet()->setTitle('Simple');

//设置A3单元格为文本

$objPHPExcel->getActiveSheet()->getStyle('A3')->getNumberFormat()

->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_TEXT);

//也可以设置整行或整列的style

/*

//E 列为文本

$objPHPExcel->getActiveSheet()->getStyle('E')->getNumberFormat()

->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_TEXT);

//第三行为文本

$objPHPExcel->getActiveSheet()->getStyle('3')->getNumberFormat()

->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_TEXT);

*/

 

更多的格式可以在PHPExcel/Style/NumberFormat.php中找到。

注意:上述的设置对长数字字符串还是以文本方式来显示科学计数法的结果。

 

方法二:在设置值的时候显示的指定数据类型

$objPHPExcel = new PHPExcel();

$objPHPExcel->setActiveSheetIndex(0);

$objPHPExcel->getActiveSheet()->setTitle('Simple');

$objPHPExcel->getActiveSheet()->setCellValueExplicit('D1',123456789033, PHPExcel_Cell_DataType::TYPE_STRING);

 

方法三:在数字字符串前加一个空格使之成为字符串

$objPHPExcel = new PHPExcel();

$objPHPExcel->setActiveSheetIndex(0);

$objPHPExcel->getActiveSheet()->setTitle('Simple');

$objPHPExcel->getActiveSheet()->setCellValue('D1', ' ' . 123456789033);

 

 

推荐使用第二、三种,第一种没有根本解决问题。

本教程主要讲解PHP项目如何用实现memcache分布式,配置使用memcache存储session数据,以及memcache的SESSION数据如何同步。

至于Memcache的安装配置,我们就不讲了,以前有两篇比较好的文章如下:
windows 64位系统配置安装Memcache缓存 http://www.111cn.net/sys/Windows/61708.htm
Linux系统Memcache安装配置详解 http://www.111cn.net/sys/linux/81920.htm

PHP分布式中使用Memcache来同步存储SESSION的步骤如下:

1、直接修改php.ini配置文件

session.save_handler = memcache
session.save_path = "tcp://127.0.0.100:11211"

2、利用目录下的 .htaccess 文件

php_value session.save_handler "memcache"
php_value session.save_path "tcp://127.0.0.1:11211"

说明:这个只是针对Apache的,目前使用Nginx的比较多,也不推荐此方式。

3、项目中修改配置

ini_set("session.save_handler", "memcache");
ini_set("session.save_path", "tcp://127.0.0.100:11211");

上面只是一些简单的说明,其实说点题外话,一般来说,做分布式,那肯定是有服务器权限的,所以推荐第一种。





memcache分布式实现、memcache分布式的数据同步、memcache保存session数据的实现


Memcache的分布式介绍

memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端仅包括内存存储功能,其实现非常简单。至于memcached的分布式,则是完全由客户端程序库实现的。这种分布式是memcached的最大特点。

Memcached的分布式是什么意思?

这里多次使用了“分布式”这个词,但并未做详细解释。现在开始简单地介绍一下其原理,各个客户端的实现基本相同。

下面假设memcached服务器有node1~node3三台,应用程序要保存键名为“tokyo”“kanagawa”“chiba”“saitama”“gunma”的数据。

首先向memcached中添加“tokyo”。将“tokyo”传给客户端程序库后,客户端实现的算法就会根据“键”来决定保存数据的memcached服务器。服务器选定后,即命令它保存“tokyo”及其值。

同样,“kanagawa”“chiba”“saitama”“gunma”都是先选择服务器再保存。接下来获取保存的数据。获取时也要将要获取的键“tokyo”传递给函数库。函数库通过与数据保存时相同的算法,根据“键”选择服务器。使用的算法相同,就能选中与保存时相同的服务器,然后发送get命令。只要数据没有因为某些原因被删除,就能获得保存的值。

这样,将不同的键保存到不同的服务器上,就实现了memcached的分布式。 memcached服务器增多后,键就会分散,即使一台memcached服务器发生故障无法连接,也不会影响其他的缓存,系统依然能继续运行。

Php+memcache实现分布式:

我们PHP的PECL中的Memcache扩展能够有效的解决Memcache的分布式问题,主要的接口就是 addServer() 函数,具体关于addServer()函数的实现可以参考该扩展源代码。那么现在就存在第二个问题,就是说无法同步数据,可以理解为MySQL中Master/Slave的机制,就是说如果我们有多台的Memcache服务器,使用addServer函数的话,每个服务器存储的数据都是唯一的,也就是说每个memcached服务器上存储的数据不是统一的,而是各自保存了不通的数据。


配置使用memcache存储session数据

session.save_handler = memcache

session.save_path ="tcp://127.0.0.1:11211"

或者某个目录下的 .htaccess :

php_value session.save_handler"memcache"

php_value session.save_path  "tcp://127.0.0.1:11211"

再或者在某个一个应用中:

ini_set("session.save_handler","memcache");

ini_set("session.save_path","tcp://127.0.0.1:11211");

使用多个 memcached server 时用逗号","隔开,并且和Memcache::addServer() 文档中说明的一样,可以带额外的参数"persistent"、"weight"、"timeout"、"retry_interval"等等,类似这样的:"tcp://host1:port1?persistent=1&weight=2,tcp://host2:port2"。

存session:

<?php
session_start();
if (!isset($_SESSION['TEST'])) {
   $_SESSION['TEST'] = time();
}
$_SESSION['TEST3'] = time();
print $_SESSION['TEST'];
print "<br><br>";
print $_SESSION['TEST3'];
print "<br><br>";
print session_id();
?>



从memcache中取回session数据

<?php
$memcache = memcache_connect('localhost',11211);
var_dump($memcache->get('19216821213c65cedec65b0883238c278eeb573e077'));
?>



会有看到

string(37)"TEST|i:1177556731;TEST3|i:1177556881;"

这样的输出,证明 session 正常工作

用 memcache 来存储 session 在读写速度上会比 files 时快很多,而且在多个服务器需要共用 session 时会比较方便,将这些服务器都配置成使用同一组 memcached 服务器就可以,减少了额外的工作量。缺点是 session 数据都保存在 memory 中,持久化方面有所欠缺,但对 session 数据来说也不是很大的问题。

另外,WS Memcached Session Handler for PHP 提供一种用session_set_save_handler 来利用 memcached 的方法。。


Linux下的Memcache安装

1. 如果通过下载源码进行安装,则需要下载最新版本http://memcached.googlecode.com/files/memcached-1.4.13.tar.gz。

如果通过apt-get方式安装,则无需下载。

2. 进行memcache的安装:
1).下载源码,进行编译安装

Memcache用到了libevent这个库用于Socket的处理,所以还需要安装libevent,libevent的最新版本是https://github.com/downloads/libevent/libevent/libevent-2.0.19-stable.tar.gz,如果你的系统已经安装了libevent,则不需要安装。

# cd /tmp
# wget http://www.danga.com/memcached/dist/memcached-1.2.0.tar.gz
# wgethttp://www.monkey.org/~provos/libevent-1.2.tar.gz


2.先安装libevent:

# tar zxvf libevent-1.2.tar.gz
# cd libevent-1.2
# ./configure --prefix=/usr
# make
# make install



测试libevent是否已经安装。

# ls -al /usr/lib | grep libevent
lrwxrwxrwx 1 root root 21 11?? 12 17:38libevent-1.2.so.1 -> libevent-1.2.so.1.0.3
-rwxr-xr-x 1 root root 263546 11?? 12 17:38libevent-1.2.so.1.0.3
-rw-r--r-- 1 root root 454156 11?? 12 17:38 libevent.a
-rwxr-xr-x 1 root root 811 11?? 12 17:38 libevent.la
lrwxrwxrwx 1 root root 21 11?? 12 17:38 libevent.so-> libevent-1.2.so.1.0.3


安装memcache

安装memcached,同时需要安装中指定libevent的安装位置:

# cd /tmp
# tar zxvf memcached-1.2.0.tar.gz
# cd memcached-1.2.0
# ./configure --with-libevent=/usr
# make
# make install

如果中间出现报错,请仔细检查错误信息,按照错误信息来配置或者增加相应的库或者路径。

安装完成后会把memcached放到 /usr/local/bin/memcached 。

测试是否成功安装memcached:

# ls -al /usr/local/bin/mem*

-rwxr-xr-x 1 root root 137986 11?? 12 17:39/usr/local/bin/memcached
-rwxr-xr-x 1 rootroot 140179 11?? 12 17:39 /usr/local/bin/memcached-debug

2)通过apt-get进行安装:

sudo apt-get install memcached,推荐通过该方式进行安装,比较简单,不容易出现错误。

启动memcache服务器:

memcached -d -m 50 -p 11211 -uroot

memcached常用启动参数描述:

-d:启动一个守护进程,
-m:分配给Memcache使用的内存数量,单位是MB,默认是64MB,
-u:运行Memcache的用户
-l:监听的服务器IP地址
-p:设置Memcache监听的端口,默认是11211注:-p(p为小写)
-c:设置最大并发连接数,默认是1024
-P:设置保存Memcache的pid文件注:-P(P为大写)
-h 显示帮助

 
安装Memcache的PHP扩展

通过地址http://pecl.php.net/get/memcache-2.2.6.tgz下载memcache最新的稳定版本。

安装php的memcache扩展有两种方式:
在linux下安装memcache扩展

可以使用php自带的pecl安装程序
# pecl install memcache

或apt-get安装php的memcache扩展

sudo apt-get installphp5-memcache

也可以从源码安装

# tar zxf memcache-2.2.3.tgz
# cd memcache-2.2.3
# /usr/local/servers/php5/bin/phpize
#./configure --enable-memcache=/usr/local/servers/memcached--with-php-config=/usr/local/servers/php5/bin/php-config--with-apxs2=/usr/sbin/apxs
# make && make inst

安装完后会有类似这样的提示:

Installing shared extensions:/usr/local/servers/php5/lib/php/extensions/no-debug-non-zts-20060922/


把这个记住,然后修改php.ini,把

extension_dir ="./"

修改为

extension_dir ="/usr/local/servers/php5/lib/php/extensions/"


并添加一行

extension="no-debug-non-zts-20060922/memcache.so"

 
在windows下安装memcache扩展

下载时要弄清楚自己的php版本,下载相对应的memcache扩展的版本,我用的是php5.2.6,下载地址为http://museum.php.net/php5/pecl-5.2.6-Win32.zip,解压后找到它的php_memcache.dll文件,放在php目录下的extension目录下,并在php.ini 加入一行 ‘extension=php_memcache.dll’。phpinfo下,看是否安装成功。

Php的memcache

使用实例:

< ?php
//连接
$mem = new Memcache;
$mem->connect("192.168.0.200", 12000);
//保存数据
$mem->set('key1', 'This is first value', 0, 60);
$val = $mem->get('key1');
echo "Get key1 value: " . $val ."<br/>";
//替换数据
$mem->replace('key1', 'This is replace value', 0,60);
$val = $mem->get('key1');
echo "Get key1 value: " . $val ."<br />";
//保存数组
$arr = array('aaa', 'bbb', 'ccc', 'ddd');
$mem->set('key2', $arr, 0, 60);
$val2 = $mem->get('key2');
echo "Get key2 value: ";
print_r($val2);
echo "<br />";
//删除数据
$mem->delete('key1');
$val = $mem->get('key1');
echo "Get key1 value: " . $val ."<br />";
//清除所有数据
$mem->flush();
$val2 = $mem->get('key2');
echo "Get key2 value: ";
print_r($val2);
echo "<br />";
//关闭连接
$mem->close();
?>


Memcached集群


Magent软件介绍

magent是一款开源的memcached代理服务器软件

地址: http://code.google.com/p/memagent/

安装magent到/usr/local/下

cd /usr/local
mkdir magent
cd magent/
wgethttp://memagent.googlecode.com/files/magent-0.5.tar.gz
tar zxvf magent-0.5.tar.gz
/sbin/ldconfig
sed -i "s/LIBS = -levent/LIBS =-levent -lm/g" Makefile
make



magent命令参数:
-hthis message
-u uid
-g gid
-p port, default is 11211. (0 to disable tcpsupport)
-s ip:port, set memcached server ip and port
-b ip:port, set backup memcached server ip andport
-l ip, local bind ip address, default is 0.0.0.0
-n number, set max connections, default is 4096
-D don't go to background
-k use ketama key allocation algorithm
-f file, unix socket path to listen on. defaultis off
-i number, max keep alive connections for onememcached server, default is 20
-v verbose

启动magent服务

magent -u root -n 4096 -l 127.0.0.1 -p12000           -s127.0.0.1:8086           -s 127.0.0.2:8086            -b 127.0.0.1:11213


magent的hash算法

magent采用的是:Consistent Hashing原理,Consistent Hashing如下所示:首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。 然后用同样的方法求出存储数据的键的哈希值,并映射到圆上。 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。

从上面的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在continuum上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。

利用magent实现对memecache的分布式管理,搭建一套memcache集群服务

1、前端php对magent的访问跟对memcache访问相同,不需要做任何更改,对于插入的key,magent会把值散列到各个memcache服务上,只操作magent,不用关心后端处理
2、公司项目应用:南北各10台前端,南北各部署一套magent服务,主要是考虑电信网通的跨网访问.以北方为例,每个前端安装memcached服务(大内存机器可以启动多个服务),每个前端都安装magent服务,后端挂载全部机器的 memcached服务,启动参数:

magent-p 12000 -s 127.0.0.1:8086 -s 127.0.0.2:8086 -s 127.0.0.3:8086.......-s127.0.0.10:8086

,所有前端配置都是相同的,任何一个前端只需访问本地端口的magent,这样的memcache集群对应用带来很大便利.

比如项目的基本配置信息,早期策略只能在中控机生成配置文件,同步到各个前端,没有办法把配置信息放到缓存中,因为各个前端的memcache是不共享 的,一台机器缓存更新,其它机器是不更新的,用程序去控制更新,还是存在不稳定因素,而且随着服务增多,也不便于管理,部署了magent后,就可以解决 这个问题,任何一个前端更新数据=全局更新
这种部署还可以解决的应用:session共享

Magent使用举例

启动两个memcached进程,端口分别为11211和11212:

memcached -m 1 -u root -d -l 127.0.0.1 -p 11211

memcached -m 1 -u root -d -l 127.0.0.1 -p 11212


启动两个magent进程,端口分别为10000和11000:

magent -u root -n 51200 -l 127.0.0.1 -p 10000 -s127.0.0.1:11211 -b 127.0.0.1:11212

magent -u root -n 51200 -l 127.0.0.1 -p 11000 -s127.0.0.1:11212 -b 127.0.0.1:11211

-s 为要写入的memcached, -b 为备份用的memcached。

说明:测试环境用magent和memached的不同端口来实现,在生产环境中可以将magent和memached作为一组放到两台服务器上。

也就是说通过magent能够写入两个memcached。

magent使用

1、安装libevent:

wget http://monkey.org/~provos/libevent-1.4.13-stable.tar.gz
tar -xzvf libevent-1.4.13-stable.tar.gz
cd libevent-1.4.13-stable
./configure --prefix=/usr/local/libevent
make
make install

2、安装Memcached:

wget http://memcached.googlecode.com/files/memcached-1.4.4.tar.gz
tar -xzvf memcached-1.4.4.tar.gz
cd memcached-1.4.4
./configure --prefix=/usr/local/memcached --with-libevent=/usr/local/libevent
make
make install
ln -s /usr/local/libevent/lib/libevent-1.4.so.2 /usr/lib/

3、编译安装magent:

mkdir magent
cp magent-0.5.tar.gz magent
cd magent
tar -xzvf magent-0.5.tar.gz
/sbin/ldconfig
sed -i "s#LIBS = -levent#LIBS = -levent -lm#g" Makefile
vi magent.c 添加
    #include <limits.h>
make

一、使用实例:

memcached -m 1 -u root -d -l 192.168.1.219 -p 11211
memcached -m 1 -u root -d -l 192.168.1.219 -p 11212
memcached -m 1 -u root -d -l 192.168.1.219 -p 11213


magent -u root -n 51200 -l 192.168.1.219 -p 12000 -s 192.168.1.219:11211 -s 192.168.1.219:11212 -b 192.168.1.219:11213


1、分别在11211、11212、11213端口启动3个Memcached进程,在12000端口开启magent代理程序;

2、11211、11212端口为主Memcached,11213端口为备份Memcached;

3、连接上12000的magent,set key1和set key2,根据哈希算法,key1被写入11212和11213端口的Memcached,key2被写入11212和11213端口的Memcached;

4、当11211、11212端口的Memcached死掉,连接到12000端口的magent取数据,数据会从11213端口的Memcached取出。


三、整个测试流程:

# telnet 192.168.1.219 12000
Trying 1192.168.1.219...
Connected to 192.168.1。219.
Escape character is '^]'.
stats
memcached agent v0.4
matrix 1 -> 192.168.1.219:11211, pool size 0
matrix 2 -> 192.168.1.219:11212, pool size 0
END
set key1 0 0 5
reesun
STORED
set key2 0 0 6
reesun1
STORED
quit
Connection closed by foreign host.


# telnet 192.168.1.219 11211
Trying 192.168.1.219...
Connected to 192.168.1.219.
Escape character is '^]'.
get key1
END
get key2
VALUE key2 0 6
reesun1
END
quit
Connection closed by foreign host.


# telnet 192.168.1.219 11212
Trying 192.168.1.219...
Connected to 1192.168.1.219.
Escape character is '^]'.
get key1
VALUE key1 0 5
reesun
END
get key2
END
quit
Connection closed by foreign host.


# telnet 192.168.1.219 11213
Trying 192.168.1.219...
Connected to 1192.168.1.219.
Escape character is '^]'.
get key1
VALUE key1 0 5
reesun
END
get key2
VALUE key2 0 6
reesun1
END
quit
Connection closed by foreign host.

本文我们将详细介绍PHP中关于Session的知识,主要内容有: Session 和 Cookie 有什么关系,为什么不推荐使用 PHP 自带的 files 型 Session 处理器,如何设置一个严格30分钟过期的Session,为什么不能用memcached存储Session

Session 和 Cookie 有什么关系

Cookie 也是由于 HTTP 无状态的特点而产生的技术。也被用于保存访问者的身份标识和一些数据。每次客户端发起 HTTP 请求时,会将 Cookie 数据加到 HTTP header 中,提交给服务端。这样服务端就可以根据 Cookie 的内容知道访问者的信息了。

可以说,Session 和 Cookie 做着相似的事情,只是 Session 是将数据保存在服务端,通过客户端提交来的 session_id 来获取对应的数据;而 Cookie 是将数据保存在客户端,每次发起请求时将数据提交给服务端的。

上面提到,session_id 可以通过 URL 或 cookie 来传递,由于 URL 的方式比 cookie 的方式更加不安全且使用不方便,所以一般是采用 cookie 来传递 session_id。

服务端生成 session_id,通过 HTTP 报文发送给客户端(比如浏览器),客户端收到后按指示创建保存着 session_id 的 cookie。cookie 是以 key/value 形式保存的,看上去大概就这个样子的:

    PHPSESSID=e4tqo2ajfbqqia9prm8t83b1f2

在 PHP 中,保存 session_id 的 cookie 名称默认叫作 PHPSESSID,这个名称可以通过 php.ini 中 session.name 来修改,也可以通过函数 session_name() 来修改。

 

为什么不推荐使用 PHP 自带的 files 型 Session 处理器

在 PHP 中,默认的 Session 处理器是 files,处理器可以用户自己实现(参见:自定义会话管理器)。我知道的成熟的 Session 处理器还有很多:Redis、Memcached、MongoDB……为什么不推荐使用 PHP 自带的 files 类型处理器,PHP 官方手册中给出过这样一段 Note:

    无论是通过调用函数 session_start() 手动开启会话, 还是使用配置项 session.auto_start 自动开启会话, 对于基于文件的会话数据保存(PHP 的默认行为)而言, 在会话开始的时候都会给会话数据文件加锁, 直到 PHP 脚本执行完毕或者显式调用 session_write_close() 来保存会话数据。 在此期间,其他脚本不可以访问同一个会话数据文件。

上述引用参见:Session 的基本用法

为了证明这段话,我们创建一下 2 个文件:

文件:session1.php

<?php
session_start();
sleep(5);
var_dump($_SESSION);
?>


文件:session2.php

<?php
session_start();
var_dump($_SESSION);
?>


在同一个浏览器中,先访问 http://127.0.0.1/session1.php,然后在当前浏览器新的标签页立刻访问 http://127.0.0.1/session2.php。实验发现,session1.php 等了 5 秒钟才有输出,而 session2.php 也等到了将近 5 秒才有输出。而单独访问 session2.php 是秒开的。在一个浏览器中访问 session1.php,然后立刻在另外一个浏览器中访问 session2.php。结果是 session1.php 等待 5 秒钟有输出,而 session2.php 是秒开的。

分析一下造成这个现象的原因:上面例子中,默认使用 Cookie 来传递 session_id,而且 Cookie 的作用域是相同。这样,在同一个浏览器中访问这 2 个地址,提交给服务器的 session_id 就是相同的(这样才能标记访问者,这是我们期望的效果)。当访问 session1.php 时,PHP 根据提交的 session_id,在服务器保存 Session 文件的路径(默认为 /tmp,通过 php.ini 中的 session.save_path 或者函数 session_save_path() 来修改)中找到了对应的 Session 文件,并对其加锁。如果不显式调用 session_write_close(),那么直到当前 PHP 脚本执行完毕才会释放文件锁。如果在脚本中有比较耗时的操作(比如例子中的 sleep(5)),那么另一个持有相同 session_id 的请求由于文件被锁,所以只能被迫等待,于是就发生了请求阻塞的情况。

既然如此,在使用完 Session 后,立刻显示调用 session_write_close() 是不是就解决问题了哩?比如上面例子中,在 sleep(5) 前面调用 session_write_close()。

确实,这样 session2.php 就不会被 session1.php 所阻塞。但是,显示调用了 session_write_close() 就意味着将数据写到文件中并结束当前会话。那么,在后面代码中要使用 Session 时,必须重新调用 session_start()。

例如:

<?php
session_start();
$_SESSION['name'] = 'Jing';
var_dump($_SESSION);
session_write_close();
sleep(5);
session_start();
$_SESSION['name'] = 'Mr.Jing';
var_dump($_SESSION);
?>



官方给出的方案:

    对于大量使用 Ajax 或者并发请求的网站而言,这可能是一个严重的问题。 解决这个问题最简单的做法是如果修改了会话中的变量, 那么应该尽快调用 session_write_close() 来保存会话数据并释放文件锁。 还有一种选择就是使用支持并发操作的会话保存管理器来替代文件会话保存管理器。

我推荐的方式是使用 Redis 作为 Session 的处理器。

Session 数据是什么时候被删除的

这是一道经常被面试官问起的问题。

先看看官方手册中的说明:

    session.gc_maxlifetime 指定过了多少秒之后数据就会被视为"垃圾"并被清除。 垃圾搜集可能会在 session 启动的时候开始( 取决于 session.gc_probability 和 session.gc_divisor)。 session.gc_probability 与 session.gc_divisor 合起来用来管理 gc(garbage collection 垃圾回收)进程启动的概率。此概率用 gc_probability/gc_divisor 计算得来。例如 1/100 意味着在每个请求中有 1% 的概率启动 gc 进程。session.gc_probability 默认为 1,session.gc_divisor 默认为 100。

继续用我上面那个不太恰当的比方吧:如果我们把物品放在超市的储物箱中而不取走,过了很久(比如一个月),那么保安就要清理这些储物箱中的物品了。当然并不是超过期限了保安就一定会来清理,也许他懒,又或者他压根就没有想起来这件事情。

再看看两段手册的引用:

    如果使用默认的基于文件的会话处理器,则文件系统必须保持跟踪访问时间(atime)。Windows FAT 文件系统不行,因此如果必须使用 FAT 文件系统或者其他不能跟踪 atime 的文件系统,那就不得不想别的办法来处理会话数据的垃圾回收。自 PHP 4.2.3 起用 mtime(修改时间)来代替了 atime。因此对于不能跟踪 atime 的文件系统也没问题了。

    GC 的运行时机并不是精准的,带有一定的或然性,所以这个设置项并不能确保旧的会话数据被删除。某些会话存储处理模块不使用此设置项。

对于这种删除机制,我是存疑的。

比如 gc_probability/gc_divisor 设置得比较大,或者网站的请求量比较大,那么 GC 进程启动就会比较频繁。

还有,GC 进程启动后都需要遍历 Session 文件列表,对比文件的修改时间和服务端的当前时间,判断文件是否过期而决定是否删除文件。

这也是我觉得不应该使用 PHP 自带的 files 型 Session 处理器的原因。而 Redis 或 Memcached 天生就支持 key/value 过期机制的,用于作为会话处理器很合适。或者自己实现一个基于文件的处理器,当根据 session_id 获取对应的单个 Session 文件时判断文件是否过期。

 
为什么重启浏览器后 Session 数据就取不到了

    session.cookie_lifetime 以秒数指定了发送到浏览器的 cookie 的生命周期。值为 0 表示"直到关闭浏览器"。默认为 0。

其实,并不是 Session 数据被删除(也有可能是,概率比较小,参见上一节)。只是关闭浏览器时,保存 session_id 的 Cookie 没有了。也就是你弄丢了打开超市储物箱的钥匙(session_id)。

同理,浏览器 Cookie 被手动清除或者其他软件清除也会造成这个结果。

为什么浏览器开着,我很久没有操作就被登出了

这个是称为“防呆”,为了保护用户账户安全的。

这个小节放进来,是因为这个功能的实现可能和 Session 的删除机制有关(之所以说是可能,是因为这个功能不一定要借住 Session 实现,用 Cookie 也同样可以实现)。

说简单一点,就是长时间没有操作,服务端的 Session 文件过期被删除了。

 
一个有意思的事情

在我试验的过程中,发现了小有意思的事情:我把 GC 启动的概率设置为 100%。如果只有一个访问者请求,该访问者即使过了很久(超过了过期时间)后才发起第二次请求,那么 Session 数据也还是存在的('session.save_path' 目录下面的 Session 文件存在)。是的,明明就超过了过期时间,却没有被 GC 删除。这时,我用另外一个浏览器访问时(相对于另一个访问者),这次请求生成了新的 Session 文件,而上一个浏览器请求生成的那个 Session 文件终于没有了(之前那个 Session 文件在 'session.save_path' 目录下面的消失了)。

还有,发现 Session 文件被删除后,再次请求,还是会生成和之前文件名相同的 Session 文件(因为浏览器并没有关闭,再次请求发送的 session_id 是相同的,所以重新生成的 Session 文件的文件名还是一样的)。但是,我不理解的是:这个重新出现的文件的创建时间竟然是第一次的那个创建时间,难道它是从回收站中回来的?(确实,我做这个试验时是在 window 下进行的)

我猜测的原因是这样:当启动会话后,PHP 根据 session_id 找到并打开了对应的 Session 文件,然后才启动 GC 进程。GC 进程就只检查除了当前这个 Session 文件外的其他文件,发现过期的就干掉。所有,即使当前这个 Session 文件已经过期了,GC 也没有删除它。

我认为这个不合理的。

由于发生这种情况影响也不大(毕竟线上请求很多,当前请求的过期文件被其他请求唤起的 GC 干掉的可能性是比较大的),我没有信心去看 PHP 源代码,我并不在线上使用 PHP 自带的 files 型 Session 处理器。所以,这个问题我就没有深入研究了,请谅解。

<?php
// 过期时间设置为 30 秒
ini_set('session.gc_maxlifetime', '30');
// GC 启动概率设置为 100%
ini_set('session.gc_probability', '100');
ini_set('session.gc_divisor', '100');
session_start();
$_SESSION['name'] = 'Jing';
var_dump($_SESSION);
?>





如何设置一个严格30分钟过期的Session

第一种回答

那么, 最常见的一种回答是: 设置Session的过期时间, 也就是session.gc_maxlifetime, 这种回答是不正确的, 原因如下:

1. 首先, 这个PHP是用一定的概率来运行session的gc的, 也就是session.gc_probability和session.gc_divisor(介绍参看 深入理解PHP原理之Session Gc的一个小概率Notice), 这个默认的值分别是1和100, 也就是有1%的机会, PHP会在一个Session启动时, 运行Session gc. 不能保证到30分钟的时候一定会过期.

2. 那设置一个大概率的清理机会呢? 还是不妥, 为什么? 因为PHP使用stat Session文件的修改时间来判断是否过期, 如果增大这个概率一来会降低性能, 二来, PHP使用”一个”文件来保存和一个会话相关的Session变量, 假设我5分钟前设置了一个a=1的Session变量, 5分钟后又设置了一个b=2的Seesion变量, 那么这个Session文件的修改时间为添加b时刻的时间, 那么a就不能在30分钟的时候, 被清理了. 另外还有下面第三个原因.

3. PHP默认的(Linux为例), 是使用/tmp 作为Session的默认存储目录, 并且手册中也有如下的描述:

    Note: 如果不同的脚本具有不同的 session.gc_maxlifetime 数值但是共享了同一个地方存储会话数据,则具有最小数值的脚本会清理数据。此情况下,与 session.save_path 一起使用本指令。

也就是说, 如果有俩个应用都没有指定自己独立的save_path, 一个设置了过期时间为2分钟(假设为A), 一个设置为30分钟(假设为B), 那么每次当A的Session gc运行的时候, 就会同时删除属于应用B的Session files.

所以, 第一种答案是不”完全严格”正确的.

第二种答案

还有一种常见的答案是: 设置Session ID的载体, Cookie的过期时间, 也就是session.cookie_lifetime. 这种回答也是不正确的, 原因如下:

这个过期只是Cookie过期, 换个说法这点就考察Cookie和Session的区别, Session过期是服务器过期, 而Cookie过期是客户端(浏览器)来保证的, 即使你设置了Cookie过期, 这个只能保证标准浏览器到期的时候, 不会发送这个Cookie(包含着Session ID), 而如果通过构造请求, 还是可以使用这个Session ID的值.

第三种答案

使用memcache, redis等, okey, 这种答案是一种正确答案. 不过, 很显然出题者肯定还会接着问你, 如果只是使用PHP呢?

第四种答案

当然, 面试不是为了难道你, 而是为了考察思考的周密性. 在这个过程中我会提示出这些陷阱, 所以一般来说, 符合题意的做法是:

1. 设置Cookie过期时间30分钟, 并设置Session的lifetime也为30分钟.
2. 自己为每一个Session值增加Time stamp.
3. 每次访问之前, 判断时间戳.

最后, 有同学问, 为什么要设置30分钟的过期时间: 这个, 首先这是为了面试, 第二, 实际使用场景的话, 比如30分钟就过期的优惠??



为什么不能用memcached存储Session

Titas Norkūnas是DevOps咨询服务提供商Bear Mountain的联合创始人。由于看到Ruby/Rails社区忽略了Dormando那两篇文章所指出的问题,所以他近日撰文对此进行了进一步的阐述。他认为问题的根本在于,memcached是一个设计用于缓存数据而不是存储数据的系统,因此不应该用于存储Session。

对于Dormando的那两篇文章,他认为第一篇文章给出的原因很容易理解,而人们经常会对第二篇文章给出的原因认识不足。因此他对这个原因进行了详细地阐述:

    Memcached使用“最近最少使用(LRU)”算法回收缓存。但memcached的LRU算法针对每个slab类执行,而不是针对整体。

    这意味着,如果所有Session的大小大致相同,那么它们会分成两三个slab类。所有其它大小大致相同的数据也会放入同一些slab,与Session争用存储空间。一旦slab满了,即使更大的slab中还有空间,数据也会被回收,而不是放入更大的slab中……在特定的slab中,Session最老的用户将会掉线。用户将会开始随机掉线,而最糟糕的是,你很可能甚至都不会注意到它,直至用户开始抱怨……

另外,Norkūnas提到,如果Session中增加了新数据,那么Session变大也可能会导致掉线问题出现。

有人提出将Session和其它数据分别使用单独的memcached缓存。不过,由于memcached的LRU算法是局部的,那种方式不仅导致内存使用率不高,而且也无法消除用户因为Session回收而出现随机掉线的风险。

如果读者非常希望借助memcached提高Session读取速度,那么可以借鉴Norkūnas提出的memcached+RDBMS(在有些情况下,NoSQL也可以)的模式:

    当用户登录时,将Session “set”到memcached,并写入数据库;
    在Session中增加一个字段,标识Session最后写入数据库的时间;
    每个页面加载的时候,优先从memcached读取Session,其次从数据库读取;
    每加载N页或者Y分钟后,再次将Session写入数据库;
    从数据库中获取过期Session,优先从memcached中获取最新数据。

[!--infotagslink--]

相关文章

  • php语言实现redis的客户端

    php语言实现redis的客户端与服务端有一些区别了因为前面介绍过服务端了这里我们来介绍客户端吧,希望文章对各位有帮助。 为了更好的了解redis协议,我们用php来实现...2016-11-25
  • jQuery+jRange实现滑动选取数值范围特效

    有时我们在页面上需要选择数值范围,如购物时选取价格区间,购买主机时自主选取CPU,内存大小配置等,使用直观的滑块条直接选取想要的数值大小即可,无需手动输入数值,操作简单又方便。HTML首先载入jQuery库文件以及jRange相关...2015-03-15
  • JS实现的简洁纵向滑动菜单(滑动门)效果

    本文实例讲述了JS实现的简洁纵向滑动菜单(滑动门)效果。分享给大家供大家参考,具体如下:这是一款纵向布局的CSS+JavaScript滑动门代码,相当简洁的手法来实现,如果对颜色不满意,你可以试着自己修改CSS代码,这个滑动门将每一...2015-10-21
  • jQuery+slidereveal实现的面板滑动侧边展出效果

    我们借助一款jQuery插件:slidereveal.js,可以使用它控制面板左右侧滑出与隐藏等效果,项目地址:https://github.com/nnattawat/slideReveal。如何使用首先在页面中加载jquery库文件和slidereveal.js插件。复制代码 代码如...2015-03-15
  • PHP+jQuery翻板抽奖功能实现

    翻板抽奖的实现流程:前端页面提供6个方块,用数字1-6依次表示6个不同的方块,当抽奖者点击6个方块中的某一块时,方块翻转到背面,显示抽奖中奖信息。看似简单的一个操作过程,却包含着WEB技术的很多知识面,所以本文的读者应该熟...2015-10-21
  • SQLMAP结合Meterpreter实现注入渗透返回shell

    sqlmap 是一个自动SQL 射入工具。它是可胜任执行一个广泛的数据库管理系统后端指印, 检索遥远的DBMS 数据库等,下面我们来看一个学习例子。 自己搭建一个PHP+MYSQ...2016-11-25
  • PHP实现今天是星期几的几种写法

    复制代码 代码如下: // 第一种写法 $da = date("w"); if( $da == "1" ){ echo "今天是星期一"; }else if( $da == "2" ){ echo "今天是星期二"; }else if( $da == "3" ){ echo "今天是星期三"; }else if( $da == "4"...2013-10-04
  • 原生js实现fadein 和 fadeout淡入淡出效果

    js里面设置DOM节点透明度的函数属性:filter= "alpha(opacity=" + value+ ")"(兼容ie)和opacity=value/100(兼容FF和GG)。 先来看看设置透明度的兼容性代码: 复制代码 代码如下: function setOpacity(ele, opacity) { if (...2014-06-07
  • Android中用HttpClient实现Http请求通信

    本文我们需要解决的问题是如何实现Http请求来实现通信,解决Android 2.3 版本以后无法使用Http请求问题,下面请看正文。 Android开发中使用HttpClient来开发Http程序...2016-09-20
  • mysql存储过程实现split示例

    复制代码 代码如下:call PROCEDURE_split('分享,代码,片段',',');select * from splittable;复制代码 代码如下:drop PROCEDURE if exists procedure_split;CREATE PROCEDURE `procedure_split`( inputstring varc...2014-05-31
  • JS跨浏览器解析XML应用过程详解

    这篇文章主要介绍了JS跨浏览器解析XML应用过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-10-16
  • vivo X9如何查出后台偷跑流量应用?vivo X9查出后台偷跑流量应用的方法

    vivo X9如何查看后台流量偷跑的情况?小编教你轻松查到!还不了解的小伙伴快来看看吧! 1)打开手机自带的【i管家】应用,打开后点击【流量监控】选项。(如下图) 2)接着选...2016-12-31
  • PHP云存储Redis的应用场景与Redis实现排行榜功能

    Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。本文我们来讲解Redis的应用场景实例。 C...2016-11-25
  • PHP explode()函数的几个应用和implode()函数有什么区别

    explode()函数介绍explode() 函数可以把字符串分割为数组。语法:explode(separator,string,limit)。 参数 描述 separator 必需。规定在哪里分割字符串。 string...2015-11-08
  • PHP+Mysql+Ajax+JS实现省市区三级联动

    基本思想就是:在JS动态创建select控件的option,通过Ajax获取在PHP从SQL数据库获取的省市区信息,代码有点长,但很多都是类似的,例如JS中省、市、区获取方法类似,PHP中通过参数不同执行不同的select语句。index.html代码:复制...2014-05-31
  • PHP Libevent扩展安装配置及简单应用

    Libevent 是一个用C语言编写的、轻量级的开源高性能网络库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,下文我们就一起来看PHP Libevent扩展安装...2016-11-25
  • php中echo <<< 的应用

    <? $a="变量的值将被带入"; echo <<< help <pre> php中echo <<< 的应用 虽然echo "...";可以断行,但若其中如出现",则仍需做转义 处理。需写做: echo " ...2016-11-25
  • JS实现程序暂停与继续功能代码解读

    下面代码用JS实现了程序的暂停与继续 复制代码 代码如下: <script type="text/javascript"> /*Javascript中暂停功能的实现 Javascript本身没有暂停功能(sleep不能使用)同时 vbscript也不能使用doEvents,故编写此函数实...2013-10-13
  • PHPCMS实现自动推送URL到百度站长平台

    我们一起来看一篇关于PHPCMS实现自动推送URL到百度站长平台,希望此教程能够帮助到各位朋友。 百度站长平台开放url推送接口,可以使用调用接口的形式主动及时推送u...2016-11-25
  • PHP-GTK 介绍及其应用

    1. PHP-GTK介绍 1.1 PHP-GTK PHP-GTK是PHP的延伸模组,它可以让程式设计师写出在客户端执行的、且独立的GUI的程式。这个模组不允许在浏览器上显视GTK+的程式,它一开始就...2016-11-25