月度归档:2013年01月

mysql表分区创建使用学习

表分区的测试使用,主要内容来自于其他博客文章以及mysql5.1的参考手册

mysql测试版本:mysql5.5.28

mysql物理存储文件(有mysql配置的datadir决定存储路径)格式简介

数据库engine为MYISAM

frm表结构文件,myd表数据文件,myi表索引文件。

INNODB engine对应的表物理存储文件

innodb的数据库的物理文件结构为:

.frm文件

.ibd文件和.ibdata文件:

这两种文件都是存放innodb数据的文件,之所以用两种文件来存放innodb的数据,是因为innodb的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。

独享表空间存储方式使用.ibd文件,并且每个表一个ibd文件

共享表空间存储方式使用.ibdata文件,所有表共同使用一个ibdata文件

创建分区

分区的一些优点包括:

·         与单个磁盘或文件系统分区相比,可以存储更多的数据。

·         对于那些已经失去保存意义的数据,通常可以通过删除与那些数据有关的分区,很容易地删除那些数据。相反地,在某些情况下,添加新数据的过程又可以通过为那些新数据专门增加一个新的分区,来很方便地实现。

通常和分区有关的其他优点包括下面列出的这些。MySQL 分区中的这些功能目前还没有实现,但是在我们的优先级列表中,具有高的优先级;我们希望在5.1的生产版本中,能包括这些功能。

·         一些查询可以得到极大的优化,这主要是借助于满足一个给定WHERE 语句的数据可以只保存在一个或多个分区内,这样在查找时就不用查找其他剩余的分区。因为分区可以在创建了分区表后进行修改,所以在第一次配置分区方案时还不曾这么做时,可以重新组织数据,来提高那些常用查询的效率。

·         涉及到例如SUM() 和 COUNT()这样聚合函数的查询,可以很容易地进行并行处理。这种查询的一个简单例子如 “SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;”。通过“并行”, 这意味着该查询可以在每个分区上同时进行,最终结果只需通过总计所有分区得到的结果。

·         通过跨多个磁盘来分散数据查询,来获得更大的查询吞吐量。

简而言之就是 数据管理优化,查询更快,数据查询并行

检测mysql是否支持分区


mysql> show variables like
"%partition%";
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| have_partitioning | YES   |
+-------------------+-------+
1 row in set

RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区。


DROP TABLE IF EXISTS `p_range`;

CREATE TABLE `p_range` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` char(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (id)
(PARTITION p0 VALUES LESS THAN (8) ENGINE = MyISAM) */;

range分区就是 partition by range(id) 表示按id 1-7的数据存储在p0分区;如果id大于7了则数据不能写入了,因为没有对应的数据分区来存储;
所以这时在创建分区时需要使用maxvalues关键字了

PARTITION BY RANGE (id)
(
PARTITION p0 VALUES LESS THAN (8),
PARTITION p1 VALUES LESS THAN MAXVALUE)

这样就表示,所有id大于7的数据记录存在在p1分区里。

RANGE分区在如下场合特别有用:

·         当需要删除“旧的”数据时。如果你使用上面最近的那个例子给出的分区方案,你只需简单地使用 “ALTER TABLE employees DROP PARTITION p0;”来删除所有在1991年前就已经停止工作的雇员相对应的所有行。对于有大量行的表,这比运行一个如“DELETE FROM employees WHERE YEAR(separated) <= 1990;”这样的一个DELETE查询要有效得多。

·         想要使用一个包含有日期或时间值,或包含有从一些其他级数开始增长的值的列。

·         经常运行直接依赖于用于分割表的列的查询。例如,当执行一个如“SELECT COUNT(*) FROM employees WHERE YEAR(separated) = 2000 GROUP BY store_id;”这样的查询时,MySQL可以很迅速地确定只有分区p2需要扫描,这是因为余下的分区不可能包含有符合该WHERE子句的任何记录。

LIST 分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择。

list分区可以理解为按一个键的id区间进行数据存储,比如类型表 1,2,3,4的所有记录存储在p0里面,5,6,7,8存在在p1分区里面

这里与range分区一样,如果现在有条记录typeid是9,那么这条记录是不能存入的;

需要注意的是:LIST分区没有类似如“VALUES LESS THAN MAXVALUE”这样的包含其他值在内的定义。将要匹配的任何值都必须在值列表中找到。


DROP TABLE IF EXISTS `p_list`;

CREATE TABLE `p_list` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`typeid` mediumint(10) NOT NULL DEFAULT '0',
`typename` char(20) DEFAULT NULL,
PRIMARY KEY (`id`,`typeid`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY LIST (typeid)
(PARTITION p0 VALUES IN (1,2,3,4) ENGINE = MyISAM,
PARTITION p1 VALUES IN (5,6,7,8) ENGINE = MyISAM) */;

HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL 中有效的、产生非负整数值的任何表达式。

HASH分区主要用来确保数据在预先确定数目的分区中平均分布。在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL 自动完成这些工作,你所要做的只是基于将要被哈希的列值指定一个列值或表达式,以及指定被分区的表将要被分割成的分区数量。

要使用HASH分区来分割一个表,要在CREATE TABLE 语句上添加一个“PARTITION BY HASH (expr)”子句,其中“expr”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL 整型的一列的名字。此外,你很可能需要在后面再添加一个“PARTITIONS num”子句,其中num 是一个非负的整数,它表示表将要被分割成分区的数量。如果没有包括一个PARTITIONS子句,那么分区的数量将默认为1。


DROP TABLE IF EXISTS `p_hash`;

CREATE TABLE `p_hash` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`storeid` mediumint(10) NOT NULL DEFAULT '0',
`storename` char(255) DEFAULT NULL,
PRIMARY KEY (`id`,`storeid`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (storeid)
PARTITIONS 4 */;

InnoDB引擎

简单点说就是数据的存入可以按 partition by hash(expr); 这里的expr可以是键名也可以是表达式比如YEAR(time),如果是表达式的情况下

“但是应当记住,每当插入或更新(或者可能删除)一行,这个表达式都要计算一次;这意味着非常复杂的表达式可能会引起性能问题,尤其是在执行同时影响大量行的运算(例如批量插入)的时候。 ”

在执行删除、写入、更新时这个表达式都会计算一次。

数据的分布采用基于用户函数结果的模数来确定使用哪个编号的分区。换句话,对于一个表达式“expr”,将要保存记录的分区编号为N ,其中“N = MOD(expr, num)”。

比如上面的storeid 为10;那么 N=MOD(10,4) ;N是等于2的,那么这条记录就存储在p2的分区里面。

如果插入一个表达式列值为’2005-09-15’的记录到表中,那么保存该条记录的分区确定如下:MOD(YEAR(‘2005-09-01’),4)  =  MOD(2005,4)  =  1 ;就存储在p1分区里面了。

“MySQL 5.1 还支持一个被称为“linear hashing(线性哈希功能)”的变量,它使用一个更加复杂的算法来确定新行插入到已经分区了的表中的位置。

线性哈希分区和常规哈希分区在语法上的唯一区别在于,在“PARTITION BY” 子句中添加“LINEAR”关键字;线性哈希功能使用的一个线性的2的幂(powers-of-two)运算法则

按照线性哈希分区的优点在于增加、删除、合并和拆分分区将变得更加快捷,有利于处理含有极其大量(1000GB)数据的表。

它的缺点在于,与使用常规HASH分区得到的数据分布相比,各个分区间数据的分布不大可能均衡。”

KEY 分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值。


DROP TABLE IF EXISTS `p_key`;

CREATE TABLE `p_key` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`keyname` char(20) DEFAULT NULL,
`keyval` varchar(1000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=12 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY KEY (id)
PARTITIONS 4 */;

按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的 哈希函数是由MySQL 服务器提供。MySQL 簇(Cluster)使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的 哈希函数,这些函数是基于与PASSWORD()一样的运算法则。

“CREATE TABLE … PARTITION BY KEY”的语法规则类似于创建一个通过HASH分区的表的规则。它们唯一的区别在于使用的关键字是KEY而不是HASH,并且KEY分区只采用一个或多个列名的一个列表。

与hash的区别就是,hash使用用户定义的表达式如YEAR(time) ;而key分区则是由mysql服务器提供的。同样KEY也是可以使用linear线性key的,与hash linear是相同的算法。

子分区:是分区表中每个分区的再次分割。


DROP TABLE IF EXISTS `p_subpartition`;

CREATE TABLE `p_subpartition` (
`id` int(10) DEFAULT NULL,
`title` char(255) NOT NULL,
`createtime` date NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8
/*!50100

PARTITION BY RANGE (YEAR(createtime))
SUBPARTITION BY HASH (MONTH(createtime))
(PARTITION p0 VALUES LESS THAN (2012)
(SUBPARTITION s1 ENGINE = MyISAM,
SUBPARTITION s2 ENGINE = MyISAM),
PARTITION p1 VALUES LESS THAN (2013)
(SUBPARTITION s3 ENGINE = MyISAM,
SUBPARTITION s4 ENGINE = MyISAM),
PARTITION p2 VALUES LESS THAN MAXVALUE
(SUBPARTITION s5 ENGINE = MyISAM,
SUBPARTITION s6 ENGINE = MyISAM)) */;

可以看到p_subpartition有三个分区p0,p1,p2;而这三个分区每一个又进一步分为2个分区。那么整个表都就分为6个小分区;

可以看到代表p_sobpartitionp0.myd的文件消失了,取代的是p_subpartition#p#p0#sp#s1.myd

在MySQL 5.1中,对于已经通过RANGE或LIST分区了的表再进行子分区是可能的。

子分区是分区表中每个分区的再次分割,子分区既可以使用HASH希分区,也可以使用KEY分区。这 也被称为复合分区(composite partitioning)。

1,如果一个分区中创建了子分区,其他分区也要有子分区

2,如果创建了了分区,每个分区中的子分区数必有相同

3,同一分区内的子分区,名字不相同,不同分区内的子分区名子可以相同(5.1.50不适用)

分区注意点

1、重新分区时,如果原分区里面存在maxvalue则新的分区里面也必须包含maxvalue否则就错误。
alter table p_range2x
reorganize partition p1,p2
into (partition p0 values less than (5), partition p1 values less than maxvalue);

[Err] 1520 – Reorganize of range partitions cannot change total ranges except for last partition where it can extend the range

2、分区删除时,数据也同样会被删除

alter table p_range drop partition p0;

3、如果range分区列表里面没有maxvalue则如有新数据大于现在分区range数据值那么这个数据是无法写入到数据库表的。

4、修改表名不需要 删除分区后在进行更改,修改表名后分区存储myd myi对应也会自动更改。

如果希望从所有分区删除所有的数据,但是又保留表的定义和表的分区模式,使用TRUNCATE TABLE命令。(请参见13.2.9节,“TRUNCATE语法”)。

如果希望改变表的分区而又不丢失数据,使用“ALTER TABLE … REORGANIZE PARTITION”语句。参见下面的内容,或者在13.1.2节,“ALTER TABLE语法” 中参考关于REORGANIZE PARTITION的信息。

5、对表进行分区时,不论采用哪种分区方式如果表中存在主键那么主键必须在分区列中。表分区的局限性。

6、list方式分区没有类似于range那种 less than maxvalue的写法,也就是说list分区表的所有数据都必须在分区字段的值列表集合中。

7、在MySQL 5.1版中,同一个分区表的所有分区必须使用同一个存储引擎;例如,不能对一个分区使用MyISAM,而对另一个使用InnoDB。

8、分区的名字是不区分大小写的,myp1与MYp1是相同的。

分区的管理

range与list分区的改变动作不能适用于hash与key方式的分区。删除与添加动作是都能使用的。

以下面的例子


DROP TABLE IF EXISTS `p_list`;

CREATE TABLE `p_list` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`typeid` mediumint(10) NOT NULL DEFAULT '0',
`typename` char(20) DEFAULT NULL,
PRIMARY KEY (`id`,`typeid`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY LIST (typeid)
(PARTITION p0 VALUES IN (1,2,3,4) ENGINE = MyISAM,
PARTITION p1 VALUES IN (5,6,7,8) ENGINE = MyISAM) */;

range与list分区的管理
删除分区
ALTER TABLE tr DROP PARTITION p1;
需要注意的是删除分区后,该分区的所有数据都没有了。同时删除后存在一个重大影响也就是typeid为5,6,7,8的记录是不能写入到该表了的!

清空数据

如果想要保留表结构与分区结构可以使用 TRUNCATE TABLE 清空表

更改分区保留数据

ALTER TABLE tbl_name REORGANIZE PARTITION partition_list INTO (partition_definitions);

如果想保留数据进行分区的更改

ALTER TABLE p_list REORGANIZE PARTITION p0 INTO (
 PARTITION s0 VALUES IN(1,2),
 PARTITION s1 VALUES IN(3,4),
);

这样就能进行分区的合并了,那怎么进行拆分呢

ALTER TABLE p_list REORGANIZE PARTITION s0,s1 INTO (
 PARTITION p0 VALUES IN(1,2,3,4),
); 使用 REORGANIZE PARTITION进行数据的合并与拆分,数据是没有丢失的。
在使用REORGANIZE进行重新分区时,需要注意几点:
1、用来确定新分区模式的PARTITION子句使用与用在CREATE TABLE中确定分区模式的PARTITION子句相同的规则。(partition 分区子句必须与创建原分区时的规则相同)
2、partition_definitions 列表中分区的合集应该与在partition_list 中命名分区的合集占有相同的区间或值集合。 (不管是合并还是拆分,s0,s1到p0;p0到s0,s1 里面的区间或者值都必须相同)
3、对于按照RANGE分区的表,只能重新组织相邻的分区;不能跳过RANGE分区。(比如按range年份 p0 1990,p1 2000 ,p2 2013三个分区;在合并时partition p0,p2 into()
   这样是不行的,因为这两个分区不是相邻的分区;)
4、不能使用REORGANIZE PARTITION来改变表的分区类型;也就是说,例如,不能把RANGE分区变为HASH分区,反之亦然。也不能使用该命令来改变分区表达式或列。

增加分区
ALTER TABLE p_list ADD PARTITION (PARTITION p2 VALUES IN (9, 10, 11));
但是不能使用
ALTER TABLE p_list ADD PARTITION (PARTITION p2 VALUES IN (9, 14));
这样mysql 会产生错误1465 (HY000): 在LIST分区中,同一个常数的多次定义
hash与key分区的管理在改变分区设置方面,按照HASH分区或KEY分区的表彼此非常相似,但是它们又与按照RANGE或LIST分区的表在很多方面有差别。
关于添加和删除按照RANGE或LIST进行分区的表的分区
不能使用与从按照RANGE或LIST分区的表中删除分区相同的方式,来从HASH或KEY分区的表中删除分区。但是,可以使用“ALTER TABLE ... COALESCE PARTITION”命令来合并HASH或KEY分区。
DROP TABLE IF EXISTS `p_hash`;

CREATE TABLE `p_hash` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`storeid` mediumint(10) NOT NULL DEFAULT '0',
`storename` char(255) DEFAULT NULL,
PRIMARY KEY (`id`,`storeid`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (storeid)
PARTITIONS 4 */;

如p_hash的分区数为4个; 要减少分区数为2个 ALTER TABLE p_hash COALESCE PARTITION 2 对于按照HASH,KEY,LINEAR HASH,或LINEAR KEY分区的表, COALESCE能起到同样的作用。

COALESCE不能用来增加分区的数量,如果你尝试这么做,结果会出现类似于下面的错误:

mysql> ALTER TABLE clients COALESCE PARTITION 18;
错误1478 (HY000): 不能移动所有分区,使用DROP TABLE代替

要增加顾客表的分区数量从12到18,使用“ALTER TABLE … ADD PARTITION”,具体如下:

ALTER TABLE clients ADD PARTITION PARTITIONS 18;

注释:“ALTER TABLE … REORGANIZE PARTITION”不能用于按照HASH或HASH分区的表。

分区维护

重建分区

这和先删除保存在分区中的所有记录,然后重新插入它们,具有同样的效果。它可用于整理分区碎片。

ALTER TABLE t1 REBUILD PARTITION (p0, p1);

优化分区

如果从分区中删除了大量的行,或者对一个带有可变长度的行(也就是说,有VARCHAR,BLOB,或TEXT类型的列)作了许多修改,

可以使用“ALTER TABLE … OPTIMIZE PARTITION”来收回没有使用的空间,并整理分区数据文件的碎片。

ALTER TABLE t1 OPTIMIZE PARTITION (p0, p1);

分析分区
读取并保存分区的键分布
ALTER TABLE t1 ANALYZE PARTITION (p3);

修补分区: 修补被破坏的分区。
ALTER TABLE t1 REPAIR PARTITION (p0,p1);

检查分区
可以使用几乎与对非分区表使用CHECK TABLE 相同的方式检查分区。
ALTER TABLE trb3 CHECK PARTITION (p1);
这个命令可以告诉你表t1的分区p1中的数据或索引是否已经被破坏。如果发生了这种情况,使用“ALTER TABLE ... REPAIR PARTITION”来修补该分区。

获取分区信息

在mysql服务器信息数据库里面的partitions存放着服务器所有表的分区信息。


-- explain partitions命令
explain partitions select * from p_hash
+----+-------------+--------+-------------+------+---------------+------+---------+------+------+-------+
| id | select_type | table  | partitions  | type | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+--------+-------------+------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | p_hash | p0,p1,p2,p3 | ALL  | NULL          | NULL | NULL    | NULL |   10 |       |
+----+-------------+--------+-------------+------+---------------+------+---------+------+------+-------+

-- 获取到p_list表的分区详细信息。

select * from information_schema.`PARTITIONS` where TABLE_NAME = 'p_list';
-- 分区的创建信息
show create table p_list;

 

更多阅读:

mysql5.5 reference manmul partition

mysql表分区
mysql分区功能详细介绍以及实例

discuzx2系列之session的实现机制

discuzx2系列文章

discuzx2系列之群组积分排行

我们知道discuzx是没有采用php默认的session机制的,而是自己采用数据库表自己进行了构建一套session用户状态识别机制;而不是简单的把php session采用存入数据库的方式。

直接看代码

几个顶级的control入口是引用了下面的class_core.php;也就是discuzx核心类文件。


require './source/class/class_core.php';
$discuz = & discuz_core::instance();

$discuz->init();

可以再discuz_core类里面的,初始化了一系列模块


function init() {
if(!$this->initated) {
$this->_init_db();
$this->_init_memory();
$this->_init_user();
$this->_init_session();    // 初始化session
$this->_init_setting();
$this->_init_mobile();
$this->_init_cron();
$this->_init_misc();
$this->_init_connect();
}
$this->initated = true;
}

那就来看看_init_session()  这个函数的主要作用初始化session,检测已有session或者创建新的session作为当前连接的唯一标识
其中附加功能包括,当前连接用户ip的白名单,黑名单检测;确定用户的详细属性信息,如是新登陆的用户则更新用户的状态信息和在线统计信息


function _init_session() {
$this->session = new discuz_session();  // 实例化数据库session类

if($this->init_session)    {
// $this->var 就是超全局变量$_G 可以看到$this->var = & $_G;是在 _init_env()中引用赋值的
// 将目前cookie为sid的值和用户个人信息传入init;discuz_session 的init方法始终会返回一个实例对象,来表示当前的用户或游客
$this->session->init($this->var['cookie']['sid'], $this->var['clientip'], $this->var['uid']);

$this->var['sid'] = $this->session->sid;
$this->var['session'] = $this->session->var; // 当前用户对象的session数据

// 如果当前返回sid 不等于浏览器中名为sig cookie的值,则设置新的sid值1天后过期
if($this->var['sid'] != $this->var['cookie']['sid']) {
dsetcookie('sid', $this->var['sid'], 86400);
}
// $isnew 表示是否为网站新的用户,也就是新创建的session
if($this->session->isnew) {
// 是新来访用户,检测用户的ip是否在系统的可访问名单($_G['setting']['ipaccess'])或是否在系统的黑名单(common_banned禁止访问表;cache_ipbanned.php)中
if(ipbanned($this->var['clientip'])) {
// ip不在可访问名单或者ip在系统的黑名单中;则将用户移入 groupid=6的“禁止ip”组;游客的groupid是7
$this->session->set('groupid', 6);
}
}

// 如果是“禁止ip”组,显示系统提示信息
// "抱歉,您的 IP 地址不在被允许,或您的账号被禁用,无法访问本站点"
if($this->session->get('groupid') == 6) {
$this->var['member']['groupid'] = 6;
sysmessage('user_banned');
}
// 存在当前登录用户;用户是新登录的或者lastactivity最后活动时间在十分钟以前了
if($this->var['uid'] && ($this->session->isnew || ($this->session->get('lastactivity') + 600) &lt; TIMESTAMP)) {
// 重新设置lastactivity,接下来在footer.html中调用updatesession();时更新当前用户session记录信息 主要作用就是更新lastactivity;避免按在线时间$_G['setting']['onlinehold']过期被清除掉
$this->session->set('lastactivity', TIMESTAMP);
if($this->session->isnew) {
// 新的登录用户,更新用户的状态
DB::update('common_member_status', array('lastip' => $this->var['clientip'], 'lastvisit' => TIMESTAMP), "uid='".$this->var['uid']."'");
}
}

}
}

discuz_session类;主要作用对session进行操作,对应的是common_session记录表。其中$discuz->session 返回的就是浏览器当前连接用户的session实例对象了。($discuz就是discuz_core的引用实例)


class discuz_session {

var $sid = null;
var $var;
var $isnew = false;
var $newguest = array('sid' => 0, 'ip1' => 0, 'ip2' => 0, 'ip3' => 0, 'ip4' => 0,
'uid' => 0, 'username' => '', 'groupid' => 7, 'invisible' => 0, 'action' => 0,
'lastactivity' => 0, 'fid' => 0, 'tid' => 0, 'lastolupdate' => 0, 'country' => 0, 'email' => ''); //add country ,email

var $old =  array('sid' =>  '', 'ip' =>  '', 'uid' =>  0);

// 实例化,初始化session默认数据
function discuz_session($sid = '', $ip = '', $uid = 0) {
$this->old = array('sid' =>  $sid, 'ip' =>  $ip, 'uid' =>  $uid);
$this->var = $this->newguest;
if(!empty($ip)) {
$this->init($sid, $ip, $uid);
}
}

function set($key, $value) {
if(isset($this->newguest[$key])) {
$this->var[$key] = $value;
} elseif ($key == 'ip') {
$ips = explode('.', $value);
$this->set('ip1', $ips[0]);
$this->set('ip2', $ips[1]);
$this->set('ip3', $ips[2]);
$this->set('ip4', $ips[3]);
}
}

function get($key) {
if(isset($this->newguest[$key])) {
return $this->var[$key];
} elseif ($key == 'ip') {
return $this->get('ip1').'.'.$this->get('ip2').'.'.$this->get('ip3').'.'.$this->get('ip4');
}
}
// 初始化返回session实例对象
function init($sid, $ip, $uid) {
$this->old = array('sid' =>  $sid, 'ip' =>  $ip, 'uid' =>  $uid);
$session = array();
// cookie sid存在;读取记录
if($sid) {
$session = DB::fetch_first("SELECT * FROM ".DB::table('common_session').
" WHERE sid='$sid' AND CONCAT_WS('.', ip1,ip2,ip3,ip4)='$ip'");
}
// 不存在或存在session记录但与新的uid值不匹配
if(empty($session) || $session['uid'] != $uid) {
// 重新创建session记录
$session = $this->create($ip, $uid);
}

$this->var = $session;
$this->sid = $session['sid'];
}

// 返回新设置的session对象
function create($ip, $uid) {

$this->isnew = true;
$this->var = $this->newguest;
$this->set('sid', random(6));
$this->set('uid', $uid);
$this->set('ip', $ip);

//写入区域信息
include_once libfile('function/misc');
$ipaddress = convertip($ip);
//$ipaddress = mb_convert_encoding($ipaddress, 'UTF-8', 'GB2312');
$country = mb_substr($ipaddress, 2, 2, 'UTF-8');  // 获取两个字存入数据库

include_once libfile('local', 'include');
if (array_key_exists($country, $local_name)) { //如果数据在数组中
$country = $local_name[$country];
} else {// 如果不是则随机获取一个
$country = $local_name[array_rand($local_name)];
}
$this->set('country', $country);

$uid && $this->set('invisible', getuserprofile('invisible'));
$this->set('lastactivity', time());
$this->sid = $this->var['sid'];

return $this->var;
}

// 清除session记录,也就是清除掉已过期的连接
function delete() {

global $_G;
// onlinehold 系统设置的用户在线时间(分钟) 15
// 这里的$_G['setting']['onlinehold']已经换算成秒了;build_cache_setting()
$onlinehold = $_G['setting']['onlinehold'];
// 游客活动时间 1分钟
$guestspan = 60;
$onlinehold = time() - $onlinehold;
$guestspan = time() - $guestspan;

$condition = " sid='{$this->sid}' ";
$condition .= " OR lastactivity&lt;$onlinehold ";  // 活动时间距离现在超过了15分钟了
$condition .= " OR (uid='0' AND ip1='{$this->var['ip1']}' AND ip2='{$this->var['ip2']}' AND ip3='{$this->var['ip3']}' AND ip4='{$this->var['ip4']}' AND lastactivity>$guestspan) "; // 同一ip,访问活动时间在1分钟以内
$condition .= $this->var['uid'] ? " OR (uid='{$this->var['uid']}') " : ''; // 存在uid 也可按uid删除
DB::delete('common_session', $condition);
}

// 更新记录
function update() {
global $_G;
if($this->sid !== null) {

$data = daddslashes($this->var);
// 新创建用户session记录;且对符合提交的session记录进行清除
if($this->isnew) {
$this->delete();
DB::insert('common_session', $data, false, false, true);
} else {
// 更新session记录
DB::update('common_session', $data, "sid='$data[sid]'");
}
$_G['session'] = $data;
// 更新sid过期时间,等于是每次刷新都会重新更新
dsetcookie('sid', $this->sid, 86400);
}
}

// 在线人数统计 登录用户/游客统计
function onlinecount($type = 0) {
$condition = $type == 1 ? ' WHERE uid>0 ' : ($type == 2 ? ' WHERE invisible=1 ' : '');
return DB::result_first("SELECT count(*) FROM ".DB::table('common_session').$condition);
}

}

对应的php session中的有效期,回收机制就是delete()方法;写入与读取对应的就是update();那用户在浏览器端刷新后是怎么进行session的验证与返回的呢?session开启后,用户每次请求服务器都会进行数据验证与返回的;所以discuzx每次请求都会执行这个update()

discuzx采用的是页面载入模板的形式,对应的 template/default/common/footer.htm是每个页面都可能会加载的;有一些公共内容在这里,其中就有用户的session更新函数调用

<!–{eval updatesession();}–>

updatesession()函数


function updatesession($force = false) {

global $_G;
static $updated = false;

if(!$updated) {
// 登录用户
if($_G['uid']) {
// 获取当前用户最后活动时间且authcode编码后写入到浏览器cookie中
if($_G['cookie']['ulastactivity']) {
$ulastactivity = authcode($_G['cookie']['ulastactivity'], 'DECODE');
} else {
$ulastactivity = getuserprofile('lastactivity');
// 有效期长达一年
dsetcookie('ulastactivity', authcode($ulastactivity, 'ENCODE'), 31536000);
}
}
// 实例化核心类
$discuz = & discuz_core::instance();
// $discuz->session 当前这个用户的session对象

// 用户在线时间更新时长(分钟) 也就是在线用户多久更新一次自己的“在线时间”
// Discuz! 可统计每个用户总共和当月的在线时间,本设置用以设定更新用户在线时间的时间频率。例如设置为 10,则用户每在线 10 分钟更新一次记录。本设置值越小,则统计越精确,但消耗资源越大。建议设置为 5~30 范围内,0 为不记录用户在线时间
$oltimespan = $_G['setting']['oltimespan'];
// session记录中的“用户在线时间更新”的最后时间
$lastolupdate = $discuz->session->var['lastolupdate'];

// 登录用户 且存在“在线时间更新”频率 且时间段已过则进行用户的个人在线时间更新。
// 等于是用户在线时间是按每次10分钟,这样叠加的;间隔越小越准确。
if($_G['uid'] && $oltimespan && TIMESTAMP - ($lastolupdate ? $lastolupdate : $ulastactivity) > $oltimespan * 60)      {
DB::query("UPDATE ".DB::table('common_onlinetime')."
SET total=total+'$oltimespan', thismonth=thismonth+'$oltimespan', lastupdate='" . TIMESTAMP . "'
WHERE uid='{$_G['uid']}'");
if(!DB::affected_rows()) {
DB::insert('common_onlinetime', array(
'uid' => $_G['uid'],
'thismonth' => $oltimespan,
'total' => $oltimespan,
'lastupdate' => TIMESTAMP,
));
}
// 重新设置 “用户在线时间更新”的最后更新时间
$discuz->session->set('lastolupdate', TIMESTAMP);
}
foreach($discuz->session->var as $k => $v) {
if(isset($_G['member'][$k]) && $k != 'lastactivity') {
$discuz->session->set($k, $_G['member'][$k]);
}
}

foreach($_G['action'] as $k => $v) {
$discuz->session->set($k, $v);
}

// 这里进行session状态更新
$discuz->session->update();

$updated = true;
// 更新当前登录用户的统计信息与用户状态信息
if($_G['uid'] && TIMESTAMP - $ulastactivity > 21600) {
if($oltimespan && TIMESTAMP - $ulastactivity > 43200) {
$total = DB::result_first("SELECT total FROM ".DB::table('common_onlinetime')." WHERE uid='$_G[uid]'");
// 换算为分钟写入用户个人统计信息表
DB::update('common_member_count', array('oltime' => round(intval($total) / 60)), "uid='$_G[uid]'", 1);
}
dsetcookie('ulastactivity', authcode(TIMESTAMP, 'ENCODE'), 31536000);
DB::update('common_member_status', array('lastip' => $_G['clientip'], 'lastactivity' => TIMESTAMP, 'lastvisit' => TIMESTAMP), "uid='$_G[uid]'", 1);
}
}
return $updated;
}

session在程序中的流程。用户访问页面,初始化discuz_session实例;读取客户端cookie sid 查找对应的session;已存在则返回记录信息,设置cookie sid的过期时间;不存在则创建新的session记录,也就是一个新的用户,在创建新用户前,对session记录表按系统设置的用户在线时间、sid、uid、同一ip1分钟内活动记录这几个条件进行清理;(很关键的一点是只有在创建新用户session记录时,才会进行session记录的删除整理) 登陆用户与session的关系这里是一个很简单的实现,也就是

_init_session(); // 也就是$this->var[‘uid’] 是否是有值得,也就是$_G[‘uid’]是否存在。uid等用户个人信息赋值的过程是在login成功后调用setloginstatus($member,$lifetime);函数(function_member.php)

$this->session->init($this->var[‘cookie’][‘sid’], $this->var[‘clientip’], $this->var[‘uid’]);

这样用户uid就直接与session记录关联起来了。同时登陆用户会进行“个人在线时间信息”“用户个人状态”的数据更新处理。

common_session表结构

CREATE TABLE pre_common_session (
sid char(6) NOT NULL default ” COMMENT ‘sid’,
ip1 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip2 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip3 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip4 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
uid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘会员id’,
username char(15) NOT NULL default ” COMMENT ‘会员名’,
groupid smallint(6) unsigned NOT NULL default ‘0’ COMMENT ‘会员组’,
invisible tinyint(1) NOT NULL default ‘0’ COMMENT ‘是否隐身登录’,
`action` tinyint(1) unsigned NOT NULL default ‘0’ COMMENT ‘当前动作’,
lastactivity int(10) unsigned NOT NULL default ‘0’ COMMENT ‘最后活动时间’,
lastolupdate int(10) unsigned NOT NULL default ‘0’ COMMENT ‘在线时间最后更新’,
fid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘论坛id’,
tid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘主题id’,
UNIQUE KEY sid (sid),
KEY uid (uid)
) ENGINE=MEMORY DEFAULT CHARSET=utf8 COMMENT=’会员认证表’;

需要注意的是session表采用的是memory引擎,优点是常驻内存存取速度快,方便操作与管理;缺点则是如果服务器重启则数据都被清空,只读或读为主的访问模式(短时间用户激增写入量会很大)

更多了解:

MySQL Memory存储引擎:优势及性能测试