基于Redis位图实现系统用户登录统计
项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小。具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进!
1. 需求
实现记录用户哪天进行了登录,每天只记录是否登录过,重复登录状态算已登录。不需要记录用户的操作行为,不需要记录用户上次登录时间和ip地址(这部分以后需要可以单独拿出来存储) 区分用户类型 查询数据需要精确到天
2. 分析
考虑到只是简单的记录用户是否登录,记录数据比较单一,查询需要精确到天。以百万用户量为前提,前期考虑了几个方案
2.1 使用文件
使用单文件存储:文件占用空间增长速度快,海量数据检索不方便,map/reduce操作也麻烦
使用多文件存储:按日期对文件进行分割。每天记录当天日志,文件量过大
2.2 使用数据库
不太认同直接使用数据库写入/读取
- 频繁请求数据库做一些日志记录浪费服务器开销。
- 随着时间推移数据急剧增大
- 海量数据检索效率也不高,同时使用索引,易产生碎片,每次插入数据还要维护索引,影响性能
所以只考虑使用数据库做数据备份。
2.3 使用redis位图(bitmap)
这也是在网上看到的方法,比较实用。也是我最终考虑使用的方法,
首先优点:
数据量小:一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。我们知道8个bit可以组成一个byte,所以bitmap本身会极大的节省储存空间。1亿人每天的登陆情况,用1亿bit,约1200wbyte,约10m 的字符就能表示。
计算方便:实用redis bit 相关命令可以极大的简化一些统计操作。常用命令 setbit、getbit、bitcount、bitop
再说弊端:
存储单一:这也算不上什么缺点,位图上存储只是0/1,所以需要存储其他信息就要别的地方单独记录,对于需要存储信息多的记录就需要使用别的方法了
3. 设计3.1 redis bitmap
key结构:前缀_年y-月m_用户类型_用户id
标准key: keys loginlog_2017-10_client_1001
检索全部: keys loginlog_*
检索某年某月全部: keys loginlog_2017-10_*
检索单个用户全部: keys loginlog_*_client_1001
检索单个类型全部: keys loginlog_*_office_*
...
每条bitmap记录单个用户一个月的登录情况,一个bit位表示一天登录情况。
设置用户1001,217-10-25登录: setbit loginlog_2017-10_client_1001 25 1
获取用户1001,217-10-25是否登录:getbit loginlog_2017-10_client_1001 25
获取用户1001,217-10月是否登录: getcount loginlog_2017-10_client_1001
获取用户1001,217-10/9/7月是否登录:bitop or stat loginlog_2017-10_client_1001 loginlog_2017-09_client_1001 loginlog_2017-07_client_1001
...
关于获取登录信息,就得获取bitmap然后拆开,循环进行判断。特别涉及时间范围,需要注意时间边界的问题,不要查询出多余的数据
获取数据redis优先级高于数据库,redis有的记录不要去数据库获取
redis数据过期:在数据同步中进行判断,过期时间自己定义(我定义的过期时间单位为“天”,必须大于31)。
在不能保证同步与过期一致性的问题,不要给key设置过期时间,会造成数据丢失。
上一次更新时间: 2107-10-02
下一次更新时间: 2017-10-09
redis bitmap 过期时间: 2017-10-05
这样会造成:2017-10-09同步的时候,3/4/5/6/7/8/9 数据丢失
所以我把redis过期数据放到同步时进行判断
我自己想的同步策略(定时每周一凌晨同步):
一、验证是否需要进行同步:
1. 当前日期 >= 8号,对本月所有记录进行同步,不对本月之前的记录进行同步
2. 当前日期 < 8号,对本月所有记录进行同步,对本月前一个月的记录进行同步,对本月前一个月之前的所有记录不进行同步
二、验证过期,如果过期,记录日志后删除[/code]3.2 数据库,表结构
每周同步一次数据到数据库,表中一条数据对应一个bitmap,记录一个月数据。每次更新已存在的、插入没有的
3.3 暂定接口
- 设置用户登录
- 查询单个用户某天是否登录过
- 查询单个用户某月是否登录过
- 查询单个用户某个时间段是否登录过
- 查询单个用户某个时间段登录信息
- 指定用户类型:获取某个时间段内有效登录的用户
- 全部用户:获取某个时间段内有效登录的用户
4. code
tp3中实现的代码,在接口服务器内部库中,application\lib\
├─loginlog
│├─logs 日志目录,redis中过期的记录删除写入日志进行备份
│├─loginlog.class.php 对外接口
│├─loginlogcommon.class.php 公共工具类
│├─loginlogdbhandle.class.php 数据库操作类
│├─loginlogredishandle.class.php redis操作类
4.1 loginlog.class.php
<?php namespace lib\loginlog; use lib\clogfilehandler; use lib\hobject; use lib\log; use lib\tools; /** * 登录日志操作类 * user: dbn * date: 2017/10/11 * time: 12:01 * ------------------------ * 日志最小粒度为:天 */ class loginlog extends hobject { private $_redishandle; // redis登录日志处理 private $_dbhandle; // 数据库登录日志处理 public function __construct() { $this->_redishandle = new loginlogredishandle($this); $this->_dbhandle = new loginlogdbhandle($this); // 初始化日志 $loghandler = new clogfilehandler(__dir__ . '/logs/del.log'); log::init($loghandler, 15); } /** * 记录登录:每天只记录一次登录,只允许设置当月内登录记录 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 时间戳 * @return boolean */ public function setlogging($type, $uid, $time) { $key = $this->_redishandle->getloginlogkey($type, $uid, $time); if ($this->_redishandle->checkloginlogkey($key)) { return $this->_redishandle->setlogging($key, $time); } return false; } /** * 查询用户某一天是否登录过 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 时间戳 * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function getdatewhetherlogin($type, $uid, $time) { $key = $this->_redishandle->getloginlogkey($type, $uid, $time); if ($this->_redishandle->checkloginlogkey($key)) { // 判断redis中是否存在记录 $isredisexists = $this->_redishandle->checkredislogexists($key); if ($isredisexists) { // 从redis中进行判断 return $this->_redishandle->datewhetherlogin($key, $time); } else { // 从数据库中进行判断 return $this->_dbhandle->datewhetherlogin($type, $uid, $time); } } return false; } /** * 查询用户某月是否登录过 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 时间戳 * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function getdatemonthwhetherlogin($type, $uid, $time) { $key = $this->_redishandle->getloginlogkey($type, $uid, $time); if ($this->_redishandle->checkloginlogkey($key)) { // 判断redis中是否存在记录 $isredisexists = $this->_redishandle->checkredislogexists($key); if ($isredisexists) { // 从redis中进行判断 return $this->_redishandle->datemonthwhetherlogin($key); } else { // 从数据库中进行判断 return $this->_dbhandle->datemonthwhetherlogin($type, $uid, $time); } } return false; } /** * 查询用户在某个时间段是否登录过 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function gettimerangewhetherlogin($type, $uid, $starttime, $endtime){ $result = $this->getusertimerangelogin($type, $uid, $starttime, $endtime); if ($result['haslog']['count'] > 0) { return true; } return false; } /** * 获取用户某时间段内登录信息 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @return array 参数错误或未查询到返回array() * ------------------------------------------------- * 查询到结果: * array( * 'haslog' => array( * 'count' => n, // 有效登录次数,每天重复登录算一次 * 'list' => array('2017-10-1', '2017-10-15' ...) // 有效登录日期 * ), * 'notlog' => array( * 'count' => n, // 未登录次数 * 'list' => array('2017-10-1', '2017-10-15' ...) // 未登录日期 * ) * ) */ public function getusertimerangelogin($type, $uid, $starttime, $endtime) { $hascount = 0; // 有效登录次数 $notcount = 0; // 未登录次数 $haslist = array(); // 有效登录日期 $notlist = array(); // 未登录日期 $successflg = false; // 查询到数据标识 if ($this->checktimerange($starttime, $endtime)) { // 获取需要查询的key $keylist = $this->_redishandle->gettimerangerediskey($type, $uid, $starttime, $endtime); if (!empty($keylist)) { foreach ($keylist as $key => $val) { // 判断redis中是否存在记录 $isredisexists = $this->_redishandle->checkredislogexists($val['key']); if ($isredisexists) { // 存在,直接从redis中获取 $loginfo = $this->_redishandle->getusertimerangelogin($val['key'], $starttime, $endtime); } else { // 不存在,尝试从数据库中读取 $loginfo = $this->_dbhandle->getusertimerangelogin($type, $uid, $val['time'], $starttime, $endtime); } if (is_array($loginfo)) { $hascount += $loginfo['haslog']['count']; $haslist = array_merge($haslist, $loginfo['haslog']['list']); $notcount += $loginfo['notlog']['count']; $notlist = array_merge($notlist, $loginfo['notlog']['list']); $successflg = true; } } } } if ($successflg) { return array( 'haslog' => array( 'count' => $hascount, 'list' => $haslist ), 'notlog' => array( 'count' => $notcount, 'list' => $notlist ) ); } return array(); } /** * 获取某段时间内有效登录过的用户 统一接口 * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @param array $typearr 用户类型,为空时获取全部类型 * @return array 参数错误或未查询到返回array() * ------------------------------------------------- * 查询到结果:指定用户类型 * array( * 'type1' => array( * 'count' => n, // type1 有效登录总用户数 * 'list' => array('111', '222' ...) // type1 有效登录用户 * ), * 'type2' => array( * 'count' => n, // type2 有效登录总用户数 * 'list' => array('333', '444' ...) // type2 有效登录用户 * ) * ) * ------------------------------------------------- * 查询到结果:未指定用户类型,全部用户,固定键 'all' * array( * 'all' => array( * 'count' => n, // 有效登录总用户数 * 'list' => array('111', '222' ...) // 有效登录用户 * ) * ) */ public function getorientedtimerangelogin($starttime, $endtime, $typearr = array()) { if ($this->checktimerange($starttime, $endtime)) { // 判断是否指定类型 if (is_array($typearr) && !empty($typearr)) { // 指定类型,验证类型合法性 if ($this->checktypearr($typearr)) { // 依据类型获取 return $this->getspecifytypetimerangelogin($starttime, $endtime, $typearr); } } else { // 未指定类型,统一获取 return $this->getspecifyalltimerangelogin($starttime, $endtime); } } return array(); } /** * 指定类型:获取某段时间内登录过的用户 * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @param array $typearr 用户类型 * @return array */ private function getspecifytypetimerangelogin($starttime, $endtime, $typearr) { $data = array(); $successflg = false; // 查询到数据标识 // 指定类型,根据类型单独获取,进行整合 foreach ($typearr as $typearrval) { // 获取需要查询的key $keylist = $this->_redishandle->getspecifytypetimerangerediskey($typearrval, $starttime, $endtime); if (!empty($keylist)) { $data[$typearrval]['count'] = 0; // 该类型下有效登录用户数 $data[$typearrval]['list'] = array(); // 该类型下有效登录用户 foreach ($keylist as $keylistval) { // 查询kye,验证redis中是否存在:此处为单个类型,所以直接看redis中是否存在该类型key即可判断是否存在 // 存在的数据不需要去数据库中去查看 $standardkeylist = $this->_redishandle->getkeys($keylistval['key']); if (is_array($standardkeylist) && count($standardkeylist) > 0) { // redis存在 foreach ($standardkeylist as $standardkeylistval) { // 验证该用户在此时间段是否登录过 $redischecklogin = $this->_redishandle->getusertimerangelogin($standardkeylistval, $starttime, $endtime); if ($redischecklogin['haslog']['count'] > 0) { // 同一个用户只需记录一次 $uid = $this->_redishandle->getloginlogkeyinfo($standardkeylistval, 'uid'); if (!in_array($uid, $data[$typearrval]['list'])) { $data[$typearrval]['count']++; $data[$typearrval]['list'][] = $uid; } $successflg = true; } } } else { // 不存在,尝试从数据库中获取 $dbresult = $this->_dbhandle->gettimerangeloginsuccessuser($keylistval['time'], $starttime, $endtime, $typearrval); if (!empty($dbresult)) { foreach ($dbresult as $dbresultval) { if (!in_array($dbresultval, $data[$typearrval]['list'])) { $data[$typearrval]['count']++; $data[$typearrval]['list'][] = $dbresultval; } } $successflg = true; } } } } } if ($successflg) { return $data; } return array(); } /** * 全部类型:获取某段时间内登录过的用户 * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @return array */ private function getspecifyalltimerangelogin($starttime, $endtime) { $count = 0; // 有效登录用户数 $list = array(); // 有效登录用户 $successflg = false; // 查询到数据标识 // 未指定类型,直接对所有数据进行检索 // 获取需要查询的key $keylist = $this->_redishandle->getspecifyalltimerangerediskey($starttime, $endtime); if (!empty($keylist)) { foreach ($keylist as $keylistval) { // 查询kye $standardkeylist = $this->_redishandle->getkeys($keylistval['key']); if (is_array($standardkeylist) && count($standardkeylist) > 0) { // 查询到key,直接读取数据,记录类型 foreach ($standardkeylist as $standardkeylistval) { // 验证该用户在此时间段是否登录过 $redischecklogin = $this->_redishandle->getusertimerangelogin($standardkeylistval, $starttime, $endtime); if ($redischecklogin['haslog']['count'] > 0) { // 同一个用户只需记录一次 $uid = $this->_redishandle->getloginlogkeyinfo($standardkeylistval, 'uid'); if (!in_array($uid, $list)) { $count++; $list[] = $uid; } $successflg = true; } } } // 无论redis中存在不存在都要尝试从数据库中获取一遍数据,来补充redis获取的数据,保证检索数据完整(redis类型缺失可能导致) $dbresult = $this->_dbhandle->gettimerangeloginsuccessuser($keylistval['time'], $starttime, $endtime); if (!empty($dbresult)) { foreach ($dbresult as $dbresultval) { if (!in_array($dbresultval, $list)) { $count++; $list[] = $dbresultval; } } $successflg = true; } } } if ($successflg) { return array( 'all' => array( 'count' => $count, 'list' => $list ) ); } return array(); } /** * 验证开始结束时间 * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return boolean */ private function checktimerange($starttime, $endtime) { return $this->_redishandle->checktimerange($starttime, $endtime); } /** * 批量验证用户类型 * @param array $typearr 用户类型数组 * @return boolean */ private function checktypearr($typearr) { $flg = false; if (is_array($typearr) && !empty($typearr)) { foreach ($typearr as $val) { if ($this->_redishandle->checktype($val)) { $flg = true; } else { $flg = false; break; } } } return $flg; } /** * 定时任务每周调用一次:从redis同步登录日志到数据库 * @param int $existsday 一条记录在redis中过期时间,单位:天,必须大于31 * @return string * 'null': redis中无数据 * 'fail': 同步失败 * 'success':同步成功 */ public function cronweeklysync($existsday) { // 验证生存时间 if ($this->_redishandle->checkexistsday($existsday)) { $likekey = 'loginlog_*'; $keylist = $this->_redishandle->getkeys($likekey); if (!empty($keylist)) { foreach ($keylist as $keyval) { if ($this->_redishandle->checkloginlogkey($keyval)) { $keytime = $this->_redishandle->getloginlogkeyinfo($keyval, 'time'); $thismonth = date('y-m'); $beforemonth = date('y-m', strtotime('-1 month')); // 验证是否需要进行同步: // 1. 当前日期 >= 8号,对本月所有记录进行同步,不对本月之前的记录进行同步 // 2. 当前日期 < 8号,对本月所有记录进行同步,对本月前一个月的记录进行同步,对本月前一个月之前的所有记录不进行同步 if (date('j') >= 8) { // 只同步本月数据 if ($thismonth == $keytime) { $this->redis2db($keyval); } } else { // 同步本月或本月前一个月数据 if ($thismonth == $keytime || $beforemonth == $keytime) { $this->redis2db($keyval); } } // 验证是否过期 $existssecond = $existsday * 24 * 60 * 60; if (strtotime($keytime) + $existssecond < time()) { // 过期删除 $bitmap = $this->_redishandle->getloginlogbitmap($keyval); log::info('删除过期数据[' . $keyval . ']:' . $bitmap); $this->_redishandle->delloginlog($keyval); } } } return 'success'; } return 'null'; } return 'fail'; } /** * 将记录同步到数据库 * @param string $key 记录key * @return boolean */ private function redis2db($key) { if ($this->_redishandle->checkloginlogkey($key) && $this->_redishandle->checkredislogexists($key)) { $time = $this->_redishandle->getloginlogkeyinfo($key, 'time'); $data['id'] = tools::generateid(); $data['user_id'] = $this->_redishandle->getloginlogkeyinfo($key, 'uid'); $data['type'] = $this->_redishandle->getloginlogkeyinfo($key, 'type'); $data['year'] = date('y', strtotime($time)); $data['month'] = date('n', strtotime($time)); $data['bit_log'] = $this->_redishandle->getloginlogbitmap($key); return $this->_dbhandle->redis2db($data); } return false; } }
4.2 loginlogcommon.class.php
<?php namespace lib\loginlog; use lib\redisdata; use lib\status; /** * 公共方法 * user: dbn * date: 2017/10/11 * time: 13:11 */ class loginlogcommon { protected $_loginlog; protected $_redis; public function __construct(loginlog $loginlog) { $this->_loginlog = $loginlog; $this->_redis = redisdata::getredis(); } /** * 验证用户类型 * @param string $type 用户类型 * @return boolean */ protected function checktype($type) { if (in_array($type, array( status::login_log_type_admin, status::login_log_type_carrier, status::login_log_type_driver, status::login_log_type_office, status::login_log_type_client, ))) { return true; } $this->_loginlog->seterror('未定义的日志类型:' . $type); return false; } /** * 验证唯一标识 * @param string $uid * @return boolean */ protected function checkuid($uid) { if (is_numeric($uid) && $uid > 0) { return true; } $this->_loginlog->seterror('唯一标识非法:' . $uid); return false; } /** * 验证时间戳 * @param string $time * @return boolean */ protected function checktime($time) { if (is_numeric($time) && $time > 0) { return true; } $this->_loginlog->seterror('时间戳非法:' . $time); return false; } /** * 验证时间是否在当月中 * @param string $time * @return boolean */ protected function checktimewhetherthismonth($time) { if ($this->checktime($time) && $time > strtotime(date('y-m')) && $time < strtotime(date('y-m') . '-' . date('t'))) { return true; } $this->_loginlog->seterror('时间未在当前月份中:' . $time); return false; } /** * 验证时间是否超过当前时间 * @param string $time * @return boolean */ protected function checktimewhetherfuturetime($time) { if ($this->checktime($time) && $time <= time()) { return true; } return false; } /** * 验证开始/结束时间 * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return boolean */ protected function checktimerange($starttime, $endtime) { if ($this->checktime($starttime) && $this->checktime($endtime) && $starttime < $endtime && $starttime < time() ) { return true; } $this->_loginlog->seterror('时间范围非法:' . $starttime . '-' . $endtime); return false; } /** * 验证时间是否在指定范围内 * @param string $time 需要检查的时间 * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return boolean */ protected function checktimewithintimerange($time, $starttime, $endtime) { if ($this->checktime($time) && $this->checktimerange($starttime, $endtime) && $starttime <= $time && $time <= $endtime ) { return true; } $this->_loginlog->seterror('请求时间未在时间范围内:' . $time . '-' . $starttime . '-' . $endtime); return false; } /** * 验证redis日志记录标准key * @param string $key * @return boolean */ protected function checkloginlogkey($key) { $pattern = '/^loginlog_\d{4}-\d{1,2}_\s+_\d+$/'; $result = preg_match($pattern, $key, $match); if ($result > 0) { return true; } $this->_loginlog->seterror('rediskey非法:' . $key); return false; } /** * 获取月份中有多少天 * @param int $time 时间戳 * @return int */ protected function getdaysinmonth($time) { return date('t', $time); } /** * 对没有前导零的月份或日设置前导零 * @param int $num 月份或日 * @return string */ protected function setdateleadingzero($num) { if (is_numeric($num) && strlen($num) <= 2) { $num = (strlen($num) > 1 ? $num : '0' . $num); } return $num; } /** * 验证过期时间 * @param int $existsday 一条记录在redis中过期时间,单位:天,必须大于31 * @return boolean */ protected function checkexistsday($existsday) { if (is_numeric($existsday) && ctype_digit(strval($existsday)) && $existsday > 31) { return true; } $this->_loginlog->seterror('过期时间非法:' . $existsday); return false; } /** * 获取开始日期边界 * @param int $time 需要判断的时间戳 * @param int $starttime 起始时间 * @return int */ protected function getstarttimeborder($time, $starttime) { $initday = 1; if ($this->checktime($time) && $this->checktime($starttime) && date('y-m', $time) === date('y-m', $starttime) && false !== date('y-m', $time)) { $initday = date('j', $starttime); } return $initday; } /** * 获取结束日期边界 * @param int $time 需要判断的时间戳 * @param int $endtime 结束时间 * @return int */ protected function getendtimeborder($time, $endtime) { $border = $this->getdaysinmonth($time); if ($this->checktime($time) && $this->checktime($endtime) && date('y-m', $time) === date('y-m', $endtime) && false !== date('y-m', $time)) { $border = date('j', $endtime); } return $border; } }
4.3 loginlogdbhandle.class.php
<?php namespace lib\loginlog; use think\model; /** * 数据库登录日志处理类 * user: dbn * date: 2017/10/11 * time: 13:12 */ class loginlogdbhandle extends loginlogcommon { /** * 从数据库中获取用户某月记录在指定时间范围内的用户信息 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 需要查询月份时间戳 * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @return array * array( * 'haslog' => array( * 'count' => n, // 有效登录次数,每天重复登录算一次 * 'list' => array('2017-10-1', '2017-10-15' ...) // 有效登录日期 * ), * 'notlog' => array( * 'count' => n, // 未登录次数 * 'list' => array('2017-10-1', '2017-10-15' ...) // 未登录日期 * ) * ) */ public function getusertimerangelogin($type, $uid, $time, $starttime, $endtime) { $hascount = 0; // 有效登录次数 $notcount = 0; // 未登录次数 $haslist = array(); // 有效登录日期 $notlist = array(); // 未登录日期 if ($this->checktype($type) && $this->checkuid($uid) && $this->checktimewithintimerange($time, $starttime, $endtime)) { $timeym = date('y-m', $time); // 设置开始时间 $initday = $this->getstarttimeborder($time, $starttime); // 设置结束时间 $border = $this->getendtimeborder($time, $endtime); $bitmap = $this->getbitmapfind($type, $uid, date('y', $time), date('n', $time)); for ($i = $initday; $i <= $border; $i++) { if (!empty($bitmap)) { if ($bitmap[$i-1] == '1') { $hascount++; $haslist[] = $timeym . '-' . $this->setdateleadingzero($i); } else { $notcount++; $notlist[] = $timeym . '-' . $this->setdateleadingzero($i); } } else { $notcount++; $notlist[] = $timeym . '-' . $this->setdateleadingzero($i); } } } return array( 'haslog' => array( 'count' => $hascount, 'list' => $haslist ), 'notlog' => array( 'count' => $notcount, 'list' => $notlist ) ); } /** * 从数据库获取用户某月日志位图 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $year 年y * @param int $month 月n * @return string */ private function getbitmapfind($type, $uid, $year, $month) { $model = d('home/statloginlog'); $map['type'] = array('eq', $type); $map['user_id'] = array('eq', $uid); $map['year'] = array('eq', $year); $map['month'] = array('eq', $month); $result = $model->field('bit_log')->where($map)->find(); if (false !== $result && isset($result['bit_log']) && !empty($result['bit_log'])) { return $result['bit_log']; } return ''; } /** * 从数据库中判断用户在某一天是否登录过 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 时间戳 * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function datewhetherlogin($type, $uid, $time) { if ($this->checktype($type) && $this->checkuid($uid) && $this->checktime($time)) { $timeinfo = getdate($time); $bitmap = $this->getbitmapfind($type, $uid, $timeinfo['year'], $timeinfo['mon']); if (!empty($bitmap)) { if ($bitmap[$timeinfo['mday']-1] == '1') { return true; } } } return false; } /** * 从数据库中判断用户在某月是否登录过 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 时间戳 * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function datemonthwhetherlogin($type, $uid, $time) { if ($this->checktype($type) && $this->checkuid($uid) && $this->checktime($time)) { $timeinfo = getdate($time); $userarr = $this->getmonthloginsuccessuser($timeinfo['year'], $timeinfo['mon'], $type); if (!empty($userarr)) { if (in_array($uid, $userarr)) { return true; } } } return false; } /** * 获取某月所有有效登录过的用户id * @param int $year 年y * @param int $month 月n * @param string $type 用户类型,为空时获取全部类型 * @return array */ public function getmonthloginsuccessuser($year, $month, $type = '') { $data = array(); if (is_numeric($year) && is_numeric($month)) { $model = d('home/statloginlog'); $map['year'] = array('eq', $year); $map['month'] = array('eq', $month); $map['bit_log'] = array('like', '%1%'); if ($type != '' && $this->checktype($type)) { $map['type'] = array('eq', $type); } $result = $model->field('user_id')->where($map)->select(); if (false !== $result && count($result) > 0) { foreach ($result as $val) { if (isset($val['user_id'])) { $data[] = $val['user_id']; } } } } return $data; } /** * 从数据库中获取某月所有记录在指定时间范围内的用户id * @param int $time 查询的时间戳 * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @param string $type 用户类型,为空时获取全部类型 * @return array */ public function gettimerangeloginsuccessuser($time, $starttime, $endtime, $type = '') { $data = array(); if ($this->checktimewithintimerange($time, $starttime, $endtime)) { $timeinfo = getdate($time); // 获取满足时间条件的记录 $model = d('home/statloginlog'); $map['year'] = array('eq', $timeinfo['year']); $map['month'] = array('eq', $timeinfo['mon']); if ($type != '' && $this->checktype($type)) { $map['type'] = array('eq', $type); } $result = $model->where($map)->select(); if (false !== $result && count($result) > 0) { // 设置开始时间 $initday = $this->getstarttimeborder($time, $starttime); // 设置结束时间 $border = $this->getendtimeborder($time, $endtime); foreach ($result as $val) { $bitmap = $val['bit_log']; for ($i = $initday; $i <= $border; $i++) { if ($bitmap[$i-1] == '1' && !in_array($val['user_id'], $data)) { $data[] = $val['user_id']; } } } } } return $data; } /** * 将数据更新到数据库 * @param array $data 单条记录的数据 * @return boolean */ public function redis2db($data) { $model = d('home/statloginlog'); // 验证记录是否存在 $map['user_id'] = array('eq', $data['user_id']); $map['type'] = array('eq', $data['type']); $map['year'] = array('eq', $data['year']); $map['month'] = array('eq', $data['month']); $count = $model->where($map)->count(); if (false !== $count && $count > 0) { // 存在记录进行更新 $savedata['bit_log'] = $data['bit_log']; if (!$model->create($savedata, model::model_update)) { $this->_loginlog->seterror('同步登录日志-更新记录,创建数据对象失败:' . $model->geterror()); logger()->error('同步登录日志-更新记录,创建数据对象失败:' . $model->geterror()); return false; } else { $result = $model->where($map)->save(); if (false !== $result) { return true; } else { $this->_loginlog->seterror('同步登录日志-更新记录,更新数据失败:' . json_encode($data)); logger()->error('同步登录日志-更新记录,更新数据失败:' . json_encode($data)); return false; } } } else { // 不存在记录插入一条新的记录 if (!$model->create($data, model::model_insert)) { $this->_loginlog->seterror('同步登录日志-插入记录,创建数据对象失败:' . $model->geterror()); logger()->error('同步登录日志-插入记录,创建数据对象失败:' . $model->geterror()); return false; } else { $result = $model->add(); if (false !== $result) { return true; } else { $this->_loginlog->seterror('同步登录日志-插入记录,插入数据失败:' . json_encode($data)); logger()->error('同步登录日志-插入记录,插入数据失败:' . json_encode($data)); return false; } } } } }
4.4 loginlogredishandle.class.php
<?php namespace lib\loginlog; /** * redis登录日志处理类 * user: dbn * date: 2017/10/11 * time: 15:53 */ class loginlogredishandle extends loginlogcommon { /** * 记录登录:每天只记录一次登录,只允许设置当月内登录记录 * @param string $key 日志记录key * @param int $time 时间戳 * @return boolean */ public function setlogging($key, $time) { if ($this->checkloginlogkey($key) && $this->checktimewhetherthismonth($time)) { // 判断用户当天是否已经登录过 $whetherloginresult = $this->datewhetherlogin($key, $time); if (!$whetherloginresult) { // 当天未登录,记录登录 $this->_redis->setbit($key, date('d', $time), 1); } return true; } return false; } /** * 从redis中判断用户在某一天是否登录过 * @param string $key 日志记录key * @param int $time 时间戳 * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function datewhetherlogin($key, $time) { if ($this->checkloginlogkey($key) && $this->checktime($time)) { $result = $this->_redis->getbit($key, date('d', $time)); if ($result === 1) { return true; } } return false; } /** * 从redis中判断用户在某月是否登录过 * @param string $key 日志记录key * @return boolean 参数错误或未登录过返回false,登录过返回true */ public function datemonthwhetherlogin($key) { if ($this->checkloginlogkey($key)) { $result = $this->_redis->bitcount($key); if ($result > 0) { return true; } } return false; } /** * 判断某月登录记录在redis中是否存在 * @param string $key 日志记录key * @return boolean */ public function checkredislogexists($key) { if ($this->checkloginlogkey($key)) { if ($this->_redis->exists($key)) { return true; } } return false; } /** * 从redis中获取用户某月记录在指定时间范围内的用户信息 * @param string $key 日志记录key * @param int $starttime 开始时间戳 * @param int $endtime 结束时间戳 * @return array * array( * 'haslog' => array( * 'count' => n, // 有效登录次数,每天重复登录算一次 * 'list' => array('2017-10-1', '2017-10-15' ...) // 有效登录日期 * ), * 'notlog' => array( * 'count' => n, // 未登录次数 * 'list' => array('2017-10-1', '2017-10-15' ...) // 未登录日期 * ) * ) */ public function getusertimerangelogin($key, $starttime, $endtime) { $hascount = 0; // 有效登录次数 $notcount = 0; // 未登录次数 $haslist = array(); // 有效登录日期 $notlist = array(); // 未登录日期 if ($this->checkloginlogkey($key) && $this->checktimerange($starttime, $endtime) && $this->checkredislogexists($key)) { $keytime = $this->getloginlogkeyinfo($key, 'time'); $keytime = strtotime($keytime); $timeym = date('y-m', $keytime); // 设置开始时间 $initday = $this->getstarttimeborder($keytime, $starttime); // 设置结束时间 $border = $this->getendtimeborder($keytime, $endtime); for ($i = $initday; $i <= $border; $i++) { $result = $this->_redis->getbit($key, $i); if ($result === 1) { $hascount++; $haslist[] = $timeym . '-' . $this->setdateleadingzero($i); } else { $notcount++; $notlist[] = $timeym . '-' . $this->setdateleadingzero($i); } } } return array( 'haslog' => array( 'count' => $hascount, 'list' => $haslist ), 'notlog' => array( 'count' => $notcount, 'list' => $notlist ) ); } /** * 面向用户:获取时间范围内可能需要的key * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return array */ public function gettimerangerediskey($type, $uid, $starttime, $endtime) { $list = array(); if ($this->checktype($type) && $this->checkuid($uid) && $this->checktimerange($starttime, $endtime)) { $data = $this->getspecifyuserkeyhandle($type, $uid, $starttime); if (!empty($data)) { $list[] = $data; } $temym = strtotime('+1 month', strtotime(date('y-m', $starttime))); while ($temym <= $endtime) { $data = $this->getspecifyuserkeyhandle($type, $uid, $temym); if (!empty($data)) { $list[] = $data; } $temym = strtotime('+1 month', $temym); } } return $list; } private function getspecifyuserkeyhandle($type, $uid, $time) { $data = array(); $key = $this->getloginlogkey($type, $uid, $time); if ($this->checkloginlogkey($key)) { $data = array( 'key' => $key, 'time' => $time ); } return $data; } /** * 面向类型:获取时间范围内可能需要的key * @param string $type 用户类型 * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return array */ public function getspecifytypetimerangerediskey($type, $starttime, $endtime) { $list = array(); if ($this->checktype($type) && $this->checktimerange($starttime, $endtime)) { $data = $this->getspecifytypekeyhandle($type, $starttime); if (!empty($data)) { $list[] = $data; } $temym = strtotime('+1 month', strtotime(date('y-m', $starttime))); while ($temym <= $endtime) { $data = $this->getspecifytypekeyhandle($type, $temym); if (!empty($data)) { $list[] = $data; } $temym = strtotime('+1 month', $temym); } } return $list; } private function getspecifytypekeyhandle($type, $time) { $data = array(); $temuid = '11111111'; $key = $this->getloginlogkey($type, $temuid, $time); if ($this->checkloginlogkey($key)) { $arr = explode('_', $key); $arr[count($arr)-1] = '*'; $key = implode('_', $arr); $data = array( 'key' => $key, 'time' => $time ); } return $data; } /** * 面向全部:获取时间范围内可能需要的key * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return array */ public function getspecifyalltimerangerediskey($starttime, $endtime) { $list = array(); if ($this->checktimerange($starttime, $endtime)) { $data = $this->getspecifyallkeyhandle($starttime); if (!empty($data)) { $list[] = $data; } $temym = strtotime('+1 month', strtotime(date('y-m', $starttime))); while ($temym <= $endtime) { $data = $this->getspecifyallkeyhandle($temym); if (!empty($data)) { $list[] = $data; } $temym = strtotime('+1 month', $temym); } } return $list; } private function getspecifyallkeyhandle($time) { $data = array(); $temuid = '11111111'; $temtype = 'office'; $key = $this->getloginlogkey($temtype, $temuid, $time); if ($this->checkloginlogkey($key)) { $arr = explode('_', $key); array_pop($arr); $arr[count($arr)-1] = '*'; $key = implode('_', $arr); $data = array( 'key' => $key, 'time' => $time ); } return $data; } /** * 从redis中查询满足条件的key * @param string $key 查询的key * @return array */ public function getkeys($key) { return $this->_redis->keys($key); } /** * 从redis中删除记录 * @param string $key 记录的key * @return boolean */ public function delloginlog($key) { return $this->_redis->del($key); } /** * 获取日志标准key:前缀_年-月_用户类型_唯一标识 * @param string $type 用户类型 * @param int $uid 唯一标识(用户id) * @param int $time 时间戳 * @return string */ public function getloginlogkey($type, $uid, $time) { if ($this->checktype($type) && $this->checkuid($uid) && $this->checktime($time)) { return 'loginlog_' . date('y-m', $time) . '_' . $type . '_' . $uid; } return ''; } /** * 获取日志标准key上信息 * @param string $key key * @param string $field 需要的参数 time,type,uid * @return mixed 返回对应的值,没有返回null */ public function getloginlogkeyinfo($key, $field) { $param = array(); if ($this->checkloginlogkey($key)) { $arr = explode('_', $key); $param['time'] = $arr[1]; $param['type'] = $arr[2]; $param['uid'] = $arr[3]; } return $param[$field]; } /** * 获取key记录的登录位图 * @param string $key key * @return string */ public function getloginlogbitmap($key) { $bitmap = ''; if ($this->checkloginlogkey($key)) { $time = $this->getloginlogkeyinfo($key, 'time'); $maxday = $this->getdaysinmonth(strtotime($time)); for ($i = 1; $i <= $maxday; $i++) { $bitmap .= $this->_redis->getbit($key, $i); } } return $bitmap; } /** * 验证日志标准key * @param string $key * @return boolean */ public function checkloginlogkey($key) { return parent::checkloginlogkey($key); } /** * 验证开始/结束时间 * @param string $starttime 开始时间 * @param string $endtime 结束时间 * @return boolean */ public function checktimerange($starttime, $endtime) { return parent::checktimerange($starttime, $endtime); } /** * 验证用户类型 * @param string $type * @return boolean */ public function checktype($type) { return parent::checktype($type); } /** * 验证过期时间 * @param int $existsday 一条记录在redis中过期时间,单位:天,必须大于31 * @return boolean */ public function checkexistsday($existsday) { return parent::checkexistsday($existsday); } }
5. 参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。