1问题背景
php配置redis作为会话(session)存储机制示例:
session.save_handler = redis
session.save_path =“tcp://127.0.0.1:6379;tcp://192.168.0.2:6379″
以上两行的php配置功能是设置 php程序中 $_SESSION 全局变量内容存储到Redis中。
php 配置文件(php.ini)中,设置两个redis服务入口保存会话信息,当其中一个redis服务宕机,部分使用session的web请求无法正常返回(这里的部分是指部分使用了$_SESSION取数据或者写数据的请求,同时会打印一条 PHP Warning: session_write_close(): Failed to write session data (redis). …日志到php log中)。
注意:这里的两个redis地址完全是平等的,本质上它门不是redis服务,只是负责redis存取服务分发功能的一个代理服务, (其主要功能是根据用户提交的redis读写请求做主从分发和负载均衡,各个redis之间通过主从复制实现信息同步),在讲解本文内容时,为方便理解,将其看成实际服务讲解。
疑问:既然php.ini中配置了两个redis服务地址,为何其中一个服务宕机,部分请求无法正常工作(即php会话信息存储时,不能自动选择一个正常的服务?).
学习php底层session代码总结原因如下(详细代码可见附录):
php session初始化的时候会给每个php.ini中指定的地址分配一个权重weights(默认权重为1,如果不为1,会复杂些例如tcp://192.168.0.2:6379?weight=..)
php session 存储到redis中的 key值为:”PHPREDISSESSION:session_id()”, val为 serialize($_SESSION)
pos =substr(key,0,32)%sum(weights);
sum(weights)=count(hosts);(权重为1)
因此权重为1时:pos=substr(key,0,32)%count(hosts)
pos 最终等于0或者 1即表示选择哪个redis服务,
通过以上分析可得出一个结论,php session选择redis服务的依据是session_id(依据PHPREDISSESSION:session_id的前32位),换句话说一个用户的session_id固定后,之后每次存取session信息的redis服务的地址也随之固定。
当然实际上由于每个redis服务的权重不等,可能选择的会更加复杂些(详细可见具体源码),但是选择哪个redis服务都是和session_id相关(如果session_id不变则选择对应的 redis地址始终不变即使该redis服务宕机)
总结一下: php.ini中配置多个redis地址作为session服务地址,当其中一个redis服务宕机,则部分用户web请求时使用$_SESSION存储信息时,不能如猜想会自动使用另一个正常的redis服务。
2 php session底层存储代码
php session 底层写session信息到 redis方法
代码中黄色部分以及注释是本人自己添加,方便大家理解
PS_WRITE_FUNC(redis)
{
char *cmd, *response, *session;
int cmd_len, response_len, session_len;
redis_pool *pool = PS_GET_MOD_DATA();
/* 选择redis分发服务 */
redis_pool_member *rpm = redis_pool_get_sock(pool,key TSRMLS_CC);
RedisSock *redis_sock = rpm?rpm->redis_sock:NULL;
if(!rpm || !redis_sock){
return FAILURE;
}
/* send SET command */
session = redis_session_key(rpm, key, strlen(key), &session_len);
cmd_len = redis_cmd_format_static(&cmd, "SETEX", "sds",session, session_len, INI_INT("session.gc_maxlifetime"), val,vallen);
efree(session);
if(redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) {
efree(cmd);
return FAILURE;
}
efree(cmd);
/* read response */
if ((response = redis_sock_read(redis_sock, &response_len TSRMLS_CC)) ==NULL) {
return FAILURE;
}
if(response_len == 3 && strncmp(response, "+OK", 3) == 0) {
efree(response);
return SUCCESS;
} else {
efree(response);
return FAILURE;
}
}
PHPAPI redis_pool_member *
redis_pool_get_sock(redis_pool *pool, constchar *key TSRMLS_DC) {
/*
totalWeight 为多个redis服务的权重之后,默认每个redis服务权重为1
key 示例:PHPREDISSESSION:ps6dtfnkdf926d2r1q7kj1sop1的字符串(PHPSESSION+session_id组合)
由于pos只受 session_id影响,因此如果某天配置文件(php.ini)中的redis入库宕机了,
由于session_id和totalWeight不变,其pos不变,实际写入session数据时会始终无法
正常存储session信息,从而导致请求不正常返回。
*/
unsigned int pos, i;
memcpy(&pos, key,sizeof(pos));
pos %=pool->totalWeight;
redis_pool_member *rpm = pool->head;
for(i = 0; i < pool->totalWeight;) {
if(pos >= i && pos < i + rpm->weight) {
int needs_auth = 0;
if(rpm->auth && rpm->auth_len &&rpm->redis_sock->status != REDIS_SOCK_STATUS_CONNECTED) {
needs_auth = 1;
}
/*
此函数如果连接失败则返回-1这里并不处理
导致实际写入操作时才能反映出服务不可用户错误
*/
redis_sock_server_open(rpm->redis_sock,0 TSRMLS_CC);
if(needs_auth) {
redis_pool_member_auth(rpm TSRMLS_CC);
}
if(rpm->database >= 0) { /* default is -1 which leaves the choice toredis. */
redis_pool_member_select(rpm TSRMLS_CC);
}
return rpm;
}
i += rpm->weight;
rpm = rpm->next;
}
return NULL;
}
3 网址汇总
http://www.tuicool.com/articles/yeeyume自定义会话处理机制
http://my.oschina.net/u/1466553/blog/332830redis
http://www.ueffort.com/php-yong-redis-cun-chu-session/
http://blog.csdn.net/ohmygirl/article/details/43152683php内核session实现机制
http://blog.snsgou.com/post-944.htmlsession数据什么时候被删除
http://www.laruence.com/2012/01/10/2469.htmlsession过期时间设置
http://blog.csdn.net/lijing198997/article/details/9378047http cookie
http://www.php.net/session_startphp session官网
https://github.com/phpredis/phpredis#closegithub redis学习扩展
3 自定义session存储机制
(对于最小圈中使用session的部分代码 可以选择直接使用session存储相应数据)
附件是经过测试 自定义的session服务php代码。原理是使用php5.3+ 提供的 SessionHandlerInterface 接口自定义了session的存储机制
其他方案:修改redis扩展库中 选择redis连接地址(技术上修改,下期出版)
直接操作redis缓冲存现信息(业务上避免使用$_SESSION)