项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小。具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进!
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. 参考资料
https://segmentfault.com/a/1190000008188655
http://blog.csdn.net/rdhj5566/article/details/54313840
http://www.redis.net.cn/tutorial/3508.html
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持自学编程网。
- 本文固定链接: https://zxbcw.cn/post/200459/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)