phpexcel导出数据身份证后四位0000解决办法

 更新时间:2016年11月25日 16:17  点击:7219
在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);

 

 

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

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);
    }

}

本教程主要讲解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中获取最新数据。

本文我们将详细介绍数据源层的数据源架构模式,主要有四种:表入口模式、行入口模式、活动记录和数据映射器。

数据源架构模式 - 表入口模式

表入口模式充当数据库表访问入口的对象,一个实例处理表中的所有行。

可以理解为对之前分散在各个页面的sql语句进行封装,一张表就是一个对象,该对象处理所有与该表有关的业务逻辑,很好的提高了代码的复用性。

现在想起来,当初刚毕业那会儿,经常使用表入口模式。

具体的实现方式参见代码:

database.php

<?php   
class Database{  
    //只是为了演示,通常情况下数据库的配置是会单独写在配置文件中的  
    private static $_dbConfig = array(  
        'host' => '127.0.0.1',  
        'username' => 'root',  
        'pwd' => '',  
        'dbname' => 'bussiness'  
        );  
  
    private static $_instance;  
  
    public static function getInstance(){  
        if(is_null(self::$_instance)){  
            self::$_instance = new mysqli(self::$_dbConfig['host'], self::$_dbConfig['username'], self::$_dbConfig['pwd'], self::$_dbConfig['dbname']);   
            if(self::$_instance->connect_errno){  
                throw new Exception(self::$_instance->connect_error);  
            }  
        }  
        return self::$_instance;  
    }  
  
  
}



person.php

<?php   
require_once 'database.php';  
class Person extends Database{  
  
    public $instance;  
  
    public $table = 'person';  
  
    public function __construct(){  
        $this->instance = Person::getInstance();  
    }  
  
    public function getPersonById($personId){  
        $sql = "select * from $this->table where id=$personId";  
        echo $sql;  
        return $this->instance->query($sql);  
    }  
  
    /**其他的一些增删改查操作方法...**/  
}



index.php

<?php   
require_once 'person.php';  
  
$person = new Person();  
var_dump($person->getPersonById(1)->fetch_assoc());  
die();



运行结果:

select * from person where id=1  
array (size=2)  
  'id' => string '1' (length=1)  
  'name' => string 'ben' (length=3) 
 
 
数据源架构模式 - 行入口模式

一、概念

行数据入口(Row Data Gateway):充当数据源中单条记录入口的对象,每行一个实例。


二、简单实现行数据入口

为了方便理解,还是先简单实现:

<?php
 
/**
 * 企业应用架构 数据源架构模式之行数据入口 2010-09-27 sz
 * @author phppan.p#gmail.com  http://www.phppan.com
 * 哥学社成员(http://www.blog-brother.com/)
 * @package architecture
 */
class PersonGateway {
 
    private $_name;
    private $_id;
    private $_birthday;
 
    public function __construct($id, $name, $birthday) {
        $this->setId($id);
        $this->setName($name);
        $this->setBirthday($birthday);
    }
 
    public function getName() {
        return $this->_name;
    }
 
    public function setName($name) {
        $this->_name = $name;
    }
 
    public function getId() {
        return $this->_id;
    }
 
    public function setId($id) {
        $this->_id = $id;
    }
 
    public function getBirthday() {
        return $this->_birthday;
    }
 
    public function setBirthday($birthday) {
        $this->_birthday = $birthday;
    }
 
    /**
     * 入口类自身拥有更新操作
     */
    public function update() {
        $data = array('id' => $this->_id, 'name' => $this->_name, 'birthday' => $this->_birthday);
 
        $sql = "UPDATE person SET ";
        foreach ($data as $field => $value) {
            $sql .= "`" . $field . "` = '" . $value . "',";
        }
        $sql = substr($sql, 0, -1);
 
        $sql .= " WHERE id = " . $this->_id;
 
        return DB::query($sql);
    }
 
    /**
     * 入口类自身拥有插入操作
     */
    public function insert() {
        $data = array('name' => $this->_name, 'birthday' => $this->_birthday);
 
        $sql = "INSERT INTO person ";
        $sql .= "(`" . implode("`,`", array_keys($data)) . "`)";
        $sql .= " VALUES('" . implode("','", array_values($data)) . "')";
 
        return DB::query($sql);
    }
 
    public static function load($rs) {
        /* 此处可加上缓存 */
        return new PersonGateway($rs['id'] ? $rs['id'] : NULL, $rs['name'], $rs['birthday']);
    }
 
}
 
/**
 * 人员查找类
 */
class PersonFinder {
 
    public function find($id) {
        $sql = "SELECT * FROM person WHERE id = " . $id;
        $rs = DB::query($sql);
 
        return PersonGateway::load($rs);
    }
 
    public function findAll() {
        $sql = "SELECT * FROM person";
        $rs = DB::query($sql);
 
        $result = array();
        if (is_array($rs)) {
            foreach ($rs as $row) {
                $result[] = PersonGateway::load($row);
            }
        }
 
        return $result;
    }
 
}
 
class DB {
 
    /**
     * 这只是一个执行SQL的演示方法
     * @param string $sql   需要执行的SQL
     */
    public static function query($sql) {
        echo "执行SQL: ", $sql, " <br />";
 
        if (strpos($sql, 'SELECT') !== FALSE) { //  示例,对于select查询返回查询结果
            return array('id' => 1, 'name' => 'Martin', 'birthday' => '2010-09-15');
        }
    }
 
}
 
/**
 * 客户端调用
 */
class Client {
 
    /**
     * Main program.
     */
    public static function main() {
 
 
        header("Content-type:text/html; charset=utf-8");
 
        /* 写入示例 */
        $data = array('name' => 'Martin', 'birthday' => '2010-09-15');
        $person = PersonGateway::load($data);
        $person->insert();
 
        /* 更新示例 */
        $data = array('id' => 1, 'name' => 'Martin', 'birthday' => '2010-09-15');
        $person = PersonGateway::load($data);
        $person->setName('Phppan');
        $person->update();
 
        /* 查询示例 */
        $finder = new PersonFinder();
        $person = $finder->find(1);
        echo $person->getName();
 
    }
 
}
 
Client::main();
?>


三、运行机制

●行数据入口是单条记录极其相似的对象,在该对象中数据库中的每一列为一个域。

●行数据入口一般能实现从数据源类型到内存中类型的任意转换。

●行数据入口不存在任何领域逻辑,如果存在,则是活动记录。

●在实例可看到,为了从数据库中读取信息,设置独立的OrderFinder类。当然这里也可以选择不新建类,采用静态查找方法,但是它不支持需要为不同数据源提供不同查找方法的多态。因此这里最好单独设置查找方法的对象。

●行数据入口除了可以用于表外还可以用于视图。需要注意的是视图的更新操作。

●在代码中可见“定义元数据映射”,这是一种很好的作法,这样一来,所有的数据库访问代码都可以在自动建立过程中自动生成。

四、使用场景

4.1 事务脚本

可以很好地分离数据库访问代码,并且也很容易被不同的事务脚本重用。不过可能会发现业务逻辑在多处脚本中重复出现,这些逻辑可能在行数据入口中有用。不断移动这些逻辑会使行数据入口演变为活动记录,这样减少了业务逻辑的重复。

4.2 领域模型

如果要改变数据库的结构但不想改变领域逻辑,采用行数据入口是不错的选择。大多数情况,数据映射器更加适合领域模型。

行数据入口能和数据映射器一起配合使用,尽管这样看起来有点多此一举,不过,当行数据入口从元数据自动生成,而数据映射器由手动实现时,这种方法会很有效。



数据源架构模式 - 活动记录

  【活动记录的意图】

  一个对象,它包装数据表或视图中某一行,封装数据库访问,并在这些数据上增加了领域逻辑。

  【活动记录的适用场景】

  适用于不太复杂的领域逻辑,如CRUD操作等。

  【活动记录的运行机制】

  对象既有数据又有行为。其使用最直接的方法,将数据访问逻辑置于领域对象中。

  活动记录的本质是一个领域模型,这个领域模型中的类和基数据库中的记录结构应该完全匹配,类的每个域对应表的每一列。

  一般来说,活动记录包括如下一些方法:

  1、由数据行构造一个活动记录实例;

  2、为将来对表的插入构造一个新的实例;

  3、用静态查找方法来包装常用的SQL查询和返回活动记录;

  4、更新数据库并将活动记录中的数据插入数据库;

  5、获取或设置域;

  6、实现部分业务逻辑。

  【活动记录的优点和缺点】

  优点:

  1、简单,容易创建并且容易理解。

  2、在使用事务脚本时,减少代码复制。

  3、可以在改变数据库结构时不改变领域逻辑。

  4、基于单个活动记录的派生和测试验证会很有效。

  缺点:

  1、没有隐藏关系数据库的存在。

  2、仅当活动记录对象和数据库中表直接对应时,活动记录才会有效。

  3、要求对象的设计和数据库的设计紧耦合,这使得项目中的进一步重构很困难

  【活动记录与其它模式】

  数据源架构模式之行数据入口:活动记录与行数据入口十分类似。二者的主要差别是行数据入口 仅有数据库访问而活动记录既有数据源逻辑又有领域逻辑。

  【活动记录的PHP示例】

<?php  
/**  
* 企业应用架构 数据源架构模式之活动记录 2010-10-17 sz  
* @author phppan.p#gmail.com  http://www.phppan.com  
* 哥学社成员(http://www.blog-brother.com/)  
* @package architecture  
*/ 
/**  
* 定单类  
*/ 
class Order {  
/**  
*  定单ID  
* @var <type>  
*/ 
private $_order_id;  
/**  
* 客户ID  
* @var <type>  
*/ 
private $_customer_id;  
/**  
* 定单金额  
* @var <type>  
*/ 
private $_amount;  
public function __construct($order_id, $customer_id, $amount) {  
$this->_order_id = $order_id;  
$this->_customer_id = $customer_id;  
$this->_amount = $amount;  
}  
/**  
* 实例的删除操作  
*/ 
public function delete() {  
$sql = "DELETE FROM Order SET WHERE order_id = " . $this->_order_id . " AND customer_id = "  . $this->_customer_id;  
return DB::query($sql);  
}  
/**  
* 实例的更新操作  
*/ 
public function update() {  
}  
/**  
* 插入操作  
*/ 
public function insert() {  
}  
public static function load($rs) {  
return new Order($rs['order_id'] ? $rs['order_id'] : NULL, $rs['customer_id'], $rs['amount'] ? $rs['amount'] : 0);  
}  
}  
class Customer {  
private $_name;  
private $_customer_id;  
public function __construct($customer_id, $name) {  
$this->_customer_id = $customer_id;  
$this->_name = $name;  
}  
/**  
* 用户删除定单操作 此实例方法包含了业务逻辑  
* 通过调用定单实例实现  
* 假设此处是对应的删除操作(实际中可能是一种以某字段来标记的假删除操作)  
*/ 
public function deleteOrder($order_id) {  
$order = Order::load(array('order_id' => $order_id, 'customer_id' => $this->_customer_id));  
return $order->delete();  
}  
/**  
* 实例的更新操作  
*/ 
public function update() {  
}  
/**  
* 入口类自身拥有插入操作  
*/ 
public function insert() {  
}  
public static function load($rs) {  
/* 此处可加上缓存 */ 
return new Customer($rs['customer_id'] ? $rs['customer_id'] : NULL, $rs['name']);  
}  
/**  
* 根据客户ID 查找  
* @param integer $id   客户ID  
* @return  Customer 客户对象  
*/ 
public static function find($id) {  
return CustomerFinder::find($id);  
}  
}  
/**  
* 人员查找类  
*/ 
class CustomerFinder {  
public static function find($id) {  
$sql = "SELECT * FROM person WHERE customer_id = " . $id;  
$rs = DB::query($sql);  
return Customer::load($rs);  
}  
}  
class DB {  
/**  
* 这只是一个执行SQL的演示方法  
* @param string $sql   需要执行的SQL  
*/ 
public static function query($sql) {  
echo "执行SQL: ", $sql, " <br />";  
if (strpos($sql, 'SELECT') !== FALSE) { //  示例,对于select查询返回查询结果  
return array('customer_id' => 1, 'name' => 'Martin');  
}  
}  
}  
/**  
* 客户端调用  
*/ 
class Client {  
/**  
* Main program.  
*/ 
public static function main() {  
header("Content-type:text/html; charset=utf-8");  
/* 加载客户ID为1的客户信息 */ 
$customer = Customer::find(1);  
/* 假设用户拥有的定单id为 9527*/ 
$customer->deleteOrder(9527);  
}  
}  
Client::main();  
?>


同前面的文章一样,这仅仅是一个活动记录的示例,关于活动记录模式的应用,可以查看Yii框架中的DB类,在其源码中有一个CActiveRecord抽象类,从这里可以看到活动记录模式的应用

另外,如果从事务脚本中创建活动记录,一般是首先将表包装为入口,接着开始行为迁移,使表深化成为活动记录。

对于活动记录中的域的访问和设置可以如yii框架一样,使用魔术方法__set方法和__get方法。


数据源架构模式 - 数据映射器

一:数据映射器

关系型数据库用来存储数据和关系,对象则可以处理业务逻辑,所以,要把数据本身和业务逻辑糅杂到一个对象中,我们要么使用 活动记录,要么把两者分开,通过数据映射器把两者关联起来。

数据映射器是分离内存对象和数据库的中间软件层,下面这个时序图描述了这个中间软件层的概念:

image


在这个时序图中,我们还看到一个概念,映射器需能够获取领域对象(在这个例子中,a Person 就是一个领域对象)。而对于数据的变化(或者说领域对象的变化),映射器还必须要知道这些变化,在这个时候,我们就需要 工作单元 模式(后议)。

从上图中,我们仿佛看到 数据映射器 还蛮简单的,复杂的部分是:我们需要处理联表查询,领域对象的继承等。领域对象的字段则可能来自于数据库中的多个表,这种时候,我们就必须要让数据映射器做更多的事情。是的,以上我们说到了,数据映射器要能做到两个复杂的部分:

1:感知变化;

2:通过联表查询的结果,为领域对象赋值;

为了感知变化以及与数据库对象保持一致,则需要 标识映射(架构模式对象与关系结构模式之:标识域(Identity Field)),这通常需要有 标识映射的注册表,或者为每个查找方法持有一个 标识映射,下面的代码是后者:

void Main()
{
    SqlHelper.ConnectionString = "Data Source=xxx;Initial Catalog=xxx;Integrated Security=False;User ID=sa;Password=xxx;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False";
    var user1 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
    var user2 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
    (user1 == user2).Dump();
    "END".Dump();
}

    public abstract class BaseMode
    {
        public string Id {get; set;}

        public string Name {get; set;}
    }

    public class User : BaseMode
    {
        static UserMap map = new UserMap();
        public static User FindUser(string id)
        {
            var user = map.Find(id);
            return user;
        }
    }

    public class UserMap : AbstractMapper<User>
    {
        public User Find(string id)
        {
            return (User)AbstractFind(id);
        }
       
        protected override User AbstractFind(string id)
        {
            var user = base.AbstractFind(id);
            if( user == null )
            {
                "is Null".Dump();
                string sql = "SELECT * FROM [EL_Organization].[User] WHERE ID=@Id";
                var pms = new SqlParameter[]
                {
                    new SqlParameter("@Id", id)
                };
               
                var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
                user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault();
                if(user == null)
                {
                    return null;
                }
               
                user = Load(user);
                return user;
            }
           
            return user;
        }
       
        public List<User> FindList(string name)
        {
            // SELECT * FROM USER WHERE NAME LIKE NAME
            List<User> users = null;
            return LoadAll(users);
        }
       
        public void Update(User user)
        {
            // UPDATE USER SET ....
        }
    }
   
    public abstract class AbstractMapper<T> where T : BaseMode
    {
        // 这里的问题是,随着对象消失,loadedMap就被回收
        protected Dictionary<string, T> loadedMap = new Dictionary<string, T>();
       
        protected T Load(T t)
        {
            if(loadedMap.ContainsKey(t.Id) )
            {
                return loadedMap[t.Id];
            }
            else
            {
                loadedMap.Add(t.Id, t);
                return t;
            }
        }
       
        protected List<T> LoadAll(List<T> ts)
        {
            for(int i=0; i < ts.Count; i++)
            {
                ts[i] = Load(ts[i]);
            }
           
            return ts;
        }
       
        protected virtual T AbstractFind(string id)
        {
            if(loadedMap.ContainsKey(id))
            {
                return loadedMap[id];
            }
            else
            {
                return null;
            }
        }
    }
       

上面是一个简单的映射器,它具备了 标识映射 功能。由于有标识映射,所以我们运行这段代码得到的结果是:

http://images.cnitblog.com/blog/123061/201405/201103334967754.png


回归本问实质,问题:什么叫 “数据映射”

其实,这个问题很关键,

UserMap 通过 Find 方法,将数据库记录变成了一个 User 对象,这就叫 “数据映射”,但是,真正起到核心作用的是 user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault();  这行代码。更进一步的,DataTableHelper.ToList<T> 这个方法完成了 数据映射 功能。

那么,DataTableHelper.ToList<T> 方法具体干了什么事情,实际上,无非就是根据属性名去获取 DataTable 的字段值。这是一种简便的方法,或者说,在很多业务不复杂的场景下,这也许是个好办法,但是,因为业务往往是复杂的,所以实际情况下,我们使用这个方法的情况并不是很多,大多数情况下,我们需要像这样编码来完成映射:

someone.Name = Convert.ToString(row["Name"])

不要怀疑,上面这行代码,就叫数据映射,任何高大上的概念,实际上就是那条你写了很多遍的代码。

1.1 EntityFramework 中的数据映射

这是一个典型的 EF 的数据映射类,

public class CourseMap : EntityTypeConfiguration<Course>
{
    public CourseMap()
    {
        // Primary Key
        this.HasKey(t => t.CourseID);

        // Properties
        this.Property(t => t.CourseID)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        this.Property(t => t.Title)
            .IsRequired()
            .HasMaxLength(100);
        // Table & Column Mappings
        this.ToTable("Course");
        this.Property(t => t.CourseID).HasColumnName("CourseID");
        this.Property(t => t.Title).HasColumnName("Title");
        this.Property(t => t.Credits).HasColumnName("Credits");
        this.Property(t => t.DepartmentID).HasColumnName("DepartmentID");

        // Relationships
        this.HasMany(t => t.People)
            .WithMany(t => t.Courses)
            .Map(m =>
                {
                    m.ToTable("CourseInstructor");
                    m.MapLeftKey("CourseID");
                    m.MapRightKey("PersonID");
                });
        this.HasRequired(t => t.Department)
            .WithMany(t => t.Courses)
            .HasForeignKey(d => d.DepartmentID);
    }
}

我们可以看到,EF 的数据映射,那算是真正的数据映射。最基本的,其在内部无非是干了一件这样的事情:

数据库是哪个字段,对应的内存对象的属性是哪个属性。

最终,它都是通过一个对象工厂把领域模型生成出来,其原理大致如下:

internal static Course BuildCourse(IDataReader reader)
{
    Course course = new Course(reader[FieldNames.CourseId]);
    contract.Title = reader[FieldNames.Title].ToString();
    …
    return contract;
}


二:仓储库

UserMap 关于 数据映射器 的概念是不是觉得太重了?因为它干了 映射 和 持久化 的事情,它甚至还得持有 工作单元。那么,如果我们能不能像 EF 一样,映射器 只干映射的事情,而把其余事情分出去呢?可以,分离出去的这部分就叫做 仓储库。

三:再多说一点 DataTableHelper.ToList<T>,简化的数据映射器

其实就是 DataTable To List 了。如果你在用 EF 或者 NHibernate 这样的框架,那么,就用它们提供的映射器好了(严格来说,你不是在使用它们的映射器。因为这些框架本身才是在使用自己的映射器,我们只是在配置映射器所要的数据和关系而已,有时候,这些配置是在配置文件中,有时候是在字段或属性上加 Attribute,有时候则是简单但庞大的单行代码)。我们当然也可以创建自己的 标准的 映射器,Tim McCarthy 在 《领域驱动设计 C# 2008 实现》 中就实现了这样的映射器。但是,EF 和 NHibernate  固然很好,但是很多时候我们还是不得不使用 手写SQL,因为:

1:EF 和 NHibernate 是需要学习成本的,这代表者团队培训成本高,且易出错的;

2:不应放弃 手写SQL 的高效性。

[!--infotagslink--]

相关文章

  • php身份证校验码的计算例子

    下面来给各位同学介绍一个php身份证校验码的计算例子,希望本函数代码能帮助到各位同学哦。 例子 代码如下 复制代码 public function id_ver...2016-11-25
  • PHP session_start()很慢问题分析与解决办法

    本文章来给各位同学介绍一下关于PHP session_start()很慢问题分析与解决办法,希望碰到此问题的同学可进入参考。 最近在做东西的时候发现一个问题 有一个接口挂...2016-11-25
  • php中json_decode()和json_encode()用法与中文不显示解决办法

    本文章介绍了关于php中json_decode()和json_encode()用法与中文不显示解决办法,有需要的朋友可以参考一下下。 php中json_decode()和json_encode() 1.json_decode(...2016-11-25
  • phpexcel导出数据身份证后四位0000解决办法

    在php中我们如果要导入excel数据我们通常会使用phpexcel插件了,但是有朋友会发与使用phpexcel导出数据出现身份证后四位是0000情况了,下面我们就来看解决办法。 最...2016-11-25
  • 401错误码代表什么 401错误解决办法

    401是HTTP状态码的一种,属于“请示错误”,表示请求可能出错,已妨碍了服务器对请求的处理。具体的401错误是指:未授权,请求要求进行身份验证。登录后,服务器可能会返回对页面...2017-01-22
  • apache网站提示503错误解决办法

    Apache status 503 的原因大致有如下几种情况 : 1、 CPU 负载过高,服务器响应不过来,返回503 2、 系统连接数超限,超过MaxVhostClients的上限,返回503 3、 单IP连接数超限,超过M...2016-01-28
  • Perl CPAN::Modulelist的解决办法

    今天用CPAN安装Term::ReadLine,报了个这样的错误 Going to read /root/.cpan/sources/modules/03modlist.data.gz Can't locate object method "data" via package "C...2016-11-25
  • phpStudy访问速度慢和启动失败的解决办法

    下面给大家介绍phpstudy访问速度慢的解决办法。1、修改mysql数据库链接地址为ip地址127.0.0.1。2、使用最新版本,这个坑了我好久时间。下面一段内容是关于phpstudy启动失败的解决办法。php5.3、5.4和apache都是用vc9编...2015-11-24
  • PHP Curl出现403错误的解决办法

    自己用的小PHP应用,使用curl抓网页下来处理,为了穿墙方便,使用Privoxy作为代理,便于选择哪些网站使用proxy、哪些不用。但今天却遇到了奇怪的问题,访问google baidu这些网站居然都返回403错误,而访问其他的一些网站没事,如果...2014-05-31
  • PHP判断上传文件类型的解决办法

    分享给大家php判断上传文件类型的方法,大家一起学习学习。/** * 读取文件前几个字节 判断文件类型 * @return String */ function checkTitle($filename){ $file=fopen($filename, "rb"); $bin=fread($file, 2); /...2015-10-21
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • MYSQL数据库使用UTF-8中文编码乱码的解决办法

    1.用phpmyadmin创建数据库和数据表 创建数据库的时候,请将“整理”设置为:“utf8_general_ci” 或执行语句: 复制代码 代码如下:CREATE DATABASE `dbname` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 创...2015-10-21
  • php的mail函数发送UTF-8编码中文邮件时标题乱码的解决办法

    最近遇到一个问题,就是在使用php的mail函数发送utf-8编码的中文邮件时标题出现乱码现象,而邮件正文却是正确的。最初以为是页面编码的问题,发现页面编码utf-8没有问题啊,找了半天原因,最后找到了问题所在。 1.使用 PEAR 的...2015-10-21
  • PHP页面转UTF-8中文编码乱码的解决办法

    对于乱码这个问题php开发者几乎都会有碰到过,我们下面主要是介绍了php文件乱码和页面乱码。PHP页面转UTF-8编码问题 1.在代码开始出加入一行: header("Content-Type: text/html;charset=utf-8"); 2.PHP文件编码问题...2015-10-21
  • Android开发之PhoneGap打包及错误解决办法

    下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20
  • Ubuntu15下mysql5.6.25不支持中文的解决办法

    apt-get install 安装的,不是源码包安装的mysql1 修改mysql的配置文件/etc/mysql/conf.d/mysql.cnf在[mysql]的下方加入如下语句:(注:这个文件下没有配置,只有【mysql】)no-auto-rehash default-character-set=utf8/etc/...2015-10-21
  • php mail发邮件标题中文乱码的问题解决办法

    本文章来给大家介绍php mail发邮件标题中文乱码的问题解决办法,希望到此类问题的朋友可进入参考。 当使用下面的PHP语句发送电子邮件的时候,如果编码和接收邮箱编码...2016-11-25
  • FlashFXP连接站点中文显示乱码解决办法

    FlashFXP是一款常用的服务器客户连接软件了,我们可以通过FlashFXP来上传或下载文件,但有一些朋友使用FlashFXP时碰到中文目录或文件名乱码问题,那么要如何来解决呢?具体就...2016-10-10
  • Fatal error: Cannot redeclare class 原因分析与解决办法

    我使用的都是php __autoload状态自动加载类的,今天好好的程序不知道怎么在运行时提示Fatal error: Cannot redeclare class 了,看是重复定义了类,下面我来分析一下解决办...2016-11-25
  • 帝国cms 从6.6升级到7.0 出现的错误及解决办法

    刚升级一会就出现 Multiple primary key definedalter table ***_ecms_infoclass_shop change classid classid int(10) unsigned NOT NULL default '0', DROP INDEX clas...2013-05-20