您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關使用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); } }
上述就是小編為大家分享的使用Redis數據庫如何統計系統登錄用戶了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。