考慮到只是簡單的記錄用戶是否登陸,記錄數據比較單一,查詢須要精確到天。以百萬用戶量爲前提,前期考慮了幾個方案php
使用單文件存儲:文件佔用空間增加速度快,海量數據檢索不方便,Map/Reduce操做也麻煩html
使用多文件存儲:按日期對文件進行分割。天天記錄當天日誌,文件量過大git
不太認同直接使用數據庫寫入/讀取redis
因此只考慮使用數據庫作數據備份。數據庫
這也是在網上看到的方法,比較實用。也是我最終考慮使用的方法,json
首先優勢:segmentfault
數據量小:一個bit位來表示某個元素對應的值或者狀態,其中的key就是對應元素自己。咱們知道8個bit能夠組成一個Byte,因此bitmap自己會極大的節省儲存空間。1億人天天的登錄狀況,用1億bit,約1200WByte,約10M 的字符就能表示。數組
計算方便:實用Redis bit 相關命令能夠極大的簡化一些統計操做。經常使用命令 SETBIT、GETBIT、BITCOUNT、BITOP服務器
再說弊端:ide
存儲單一:這也算不上什麼缺點,位圖上存儲只是0/1,因此須要存儲其餘信息就要別的地方單獨記錄,對於須要存儲信息多的記錄就須要使用別的方法了
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月是否登陸:BITCOUNT 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、驗證是否須要進行同步: 1. 當前日期 >= 8號,對本月全部記錄進行同步,不對本月以前的記錄進行同步 2. 當前日期 < 8號,對本月全部記錄進行同步,對本月前一個月的記錄進行同步,對本月前一個月以前的全部記錄不進行同步
2、驗證過時,若是過時,記錄日誌後刪除
每週同步一次數據到數據庫,表中一條數據對應一個BitMap,記錄一個月數據。每次更新已存在的、插入沒有的
TP3中實現的代碼,在接口服務器內部庫中,Application\Lib\
├─LoginLog
│ ├─Logs 日誌目錄,Redis中過時的記錄刪除寫入日誌進行備份
│ ├─LoginLog.class.php 對外接口
│ ├─LoginLogCommon.class.php 公共工具類
│ ├─LoginLogDBHandle.class.php 數據庫操做類
│ ├─LoginLogRedisHandle.class.php Redis操做類
1 <?php 2 3 namespace Lib\LoginLog; 4 use Lib\CLogFileHandler; 5 use Lib\HObject; 6 use Lib\Log; 7 use Lib\Tools; 8 9 /** 10 * 登陸日誌操做類 11 * User: dbn 12 * Date: 2017/10/11 13 * Time: 12:01 14 * ------------------------ 15 * 日誌最小粒度爲:天 16 */ 17 18 class LoginLog extends HObject 19 { 20 private $_redisHandle; // Redis登陸日誌處理 21 private $_dbHandle; // 數據庫登陸日誌處理 22 23 public function __construct() 24 { 25 $this->_redisHandle = new LoginLogRedisHandle($this); 26 $this->_dbHandle = new LoginLogDBHandle($this); 27 28 // 初始化日誌 29 $logHandler = new CLogFileHandler(__DIR__ . '/Logs/del.log'); 30 Log::Init($logHandler, 15); 31 } 32 33 /** 34 * 記錄登陸:天天只記錄一次登陸,只容許設置當月內登陸記錄 35 * @param string $type 用戶類型 36 * @param int $uid 惟一標識(用戶ID) 37 * @param int $time 時間戳 38 * @return boolean 39 */ 40 public function setLogging($type, $uid, $time) 41 { 42 $key = $this->_redisHandle->getLoginLogKey($type, $uid, $time); 43 if ($this->_redisHandle->checkLoginLogKey($key)) { 44 return $this->_redisHandle->setLogging($key, $time); 45 } 46 return false; 47 } 48 49 /** 50 * 查詢用戶某一天是否登陸過 51 * @param string $type 用戶類型 52 * @param int $uid 惟一標識(用戶ID) 53 * @param int $time 時間戳 54 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 55 */ 56 public function getDateWhetherLogin($type, $uid, $time) 57 { 58 $key = $this->_redisHandle->getLoginLogKey($type, $uid, $time); 59 if ($this->_redisHandle->checkLoginLogKey($key)) { 60 61 // 判斷Redis中是否存在記錄 62 $isRedisExists = $this->_redisHandle->checkRedisLogExists($key); 63 if ($isRedisExists) { 64 65 // 從Redis中進行判斷 66 return $this->_redisHandle->dateWhetherLogin($key, $time); 67 } else { 68 69 // 從數據庫中進行判斷 70 return $this->_dbHandle->dateWhetherLogin($type, $uid, $time); 71 } 72 } 73 return false; 74 } 75 76 /** 77 * 查詢用戶某月是否登陸過 78 * @param string $type 用戶類型 79 * @param int $uid 惟一標識(用戶ID) 80 * @param int $time 時間戳 81 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 82 */ 83 public function getDateMonthWhetherLogin($type, $uid, $time) 84 { 85 $key = $this->_redisHandle->getLoginLogKey($type, $uid, $time); 86 if ($this->_redisHandle->checkLoginLogKey($key)) { 87 88 // 判斷Redis中是否存在記錄 89 $isRedisExists = $this->_redisHandle->checkRedisLogExists($key); 90 if ($isRedisExists) { 91 92 // 從Redis中進行判斷 93 return $this->_redisHandle->dateMonthWhetherLogin($key); 94 } else { 95 96 // 從數據庫中進行判斷 97 return $this->_dbHandle->dateMonthWhetherLogin($type, $uid, $time); 98 } 99 } 100 return false; 101 } 102 103 /** 104 * 查詢用戶在某個時間段是否登陸過 105 * @param string $type 用戶類型 106 * @param int $uid 惟一標識(用戶ID) 107 * @param int $startTime 開始時間戳 108 * @param int $endTime 結束時間戳 109 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 110 */ 111 public function getTimeRangeWhetherLogin($type, $uid, $startTime, $endTime){ 112 $result = $this->getUserTimeRangeLogin($type, $uid, $startTime, $endTime); 113 if ($result['hasLog']['count'] > 0) { 114 return true; 115 } 116 return false; 117 } 118 119 /** 120 * 獲取用戶某時間段內登陸信息 121 * @param string $type 用戶類型 122 * @param int $uid 惟一標識(用戶ID) 123 * @param int $startTime 開始時間戳 124 * @param int $endTime 結束時間戳 125 * @return array 參數錯誤或未查詢到返回array() 126 * ------------------------------------------------- 127 * 查詢到結果: 128 * array( 129 * 'hasLog' => array( 130 * 'count' => n, // 有效登陸次數,天天重複登陸算一次 131 * 'list' => array('2017-10-1', '2017-10-15' ...) // 有效登陸日期 132 * ), 133 * 'notLog' => array( 134 * 'count' => n, // 未登陸次數 135 * 'list' => array('2017-10-1', '2017-10-15' ...) // 未登陸日期 136 * ) 137 * ) 138 */ 139 public function getUserTimeRangeLogin($type, $uid, $startTime, $endTime) 140 { 141 $hasCount = 0; // 有效登陸次數 142 $notCount = 0; // 未登陸次數 143 $hasList = array(); // 有效登陸日期 144 $notList = array(); // 未登陸日期 145 $successFlg = false; // 查詢到數據標識 146 147 if ($this->checkTimeRange($startTime, $endTime)) { 148 149 // 獲取須要查詢的Key 150 $keyList = $this->_redisHandle->getTimeRangeRedisKey($type, $uid, $startTime, $endTime); 151 152 if (!empty($keyList)) { 153 foreach ($keyList as $key => $val) { 154 155 // 判斷Redis中是否存在記錄 156 $isRedisExists = $this->_redisHandle->checkRedisLogExists($val['key']); 157 if ($isRedisExists) { 158 159 // 存在,直接從Redis中獲取 160 $logInfo = $this->_redisHandle->getUserTimeRangeLogin($val['key'], $startTime, $endTime); 161 } else { 162 163 // 不存在,嘗試從數據庫中讀取 164 $logInfo = $this->_dbHandle->getUserTimeRangeLogin($type, $uid, $val['time'], $startTime, $endTime); 165 } 166 167 if (is_array($logInfo)) { 168 $hasCount += $logInfo['hasLog']['count']; 169 $hasList = array_merge($hasList, $logInfo['hasLog']['list']); 170 $notCount += $logInfo['notLog']['count']; 171 $notList = array_merge($notList, $logInfo['notLog']['list']); 172 $successFlg = true; 173 } 174 } 175 } 176 } 177 178 if ($successFlg) { 179 return array( 180 'hasLog' => array( 181 'count' => $hasCount, 182 'list' => $hasList 183 ), 184 'notLog' => array( 185 'count' => $notCount, 186 'list' => $notList 187 ) 188 ); 189 } 190 191 return array(); 192 } 193 194 /** 195 * 獲取某段時間內有效登陸過的用戶 統一接口 196 * @param int $startTime 開始時間戳 197 * @param int $endTime 結束時間戳 198 * @param array $typeArr 用戶類型,爲空時獲取所有類型 199 * @return array 參數錯誤或未查詢到返回array() 200 * ------------------------------------------------- 201 * 查詢到結果:指定用戶類型 202 * array( 203 * 'type1' => array( 204 * 'count' => n, // type1 有效登陸總用戶數 205 * 'list' => array('111', '222' ...) // type1 有效登陸用戶 206 * ), 207 * 'type2' => array( 208 * 'count' => n, // type2 有效登陸總用戶數 209 * 'list' => array('333', '444' ...) // type2 有效登陸用戶 210 * ) 211 * ) 212 * ------------------------------------------------- 213 * 查詢到結果:未指定用戶類型,所有用戶,固定鍵 'all' 214 * array( 215 * 'all' => array( 216 * 'count' => n, // 有效登陸總用戶數 217 * 'list' => array('111', '222' ...) // 有效登陸用戶 218 * ) 219 * ) 220 */ 221 public function getOrientedTimeRangeLogin($startTime, $endTime, $typeArr = array()) 222 { 223 if ($this->checkTimeRange($startTime, $endTime)) { 224 225 // 判斷是否指定類型 226 if (is_array($typeArr) && !empty($typeArr)) { 227 228 // 指定類型,驗證類型合法性 229 if ($this->checkTypeArr($typeArr)) { 230 231 // 依據類型獲取 232 return $this->getSpecifyTypeTimeRangeLogin($startTime, $endTime, $typeArr); 233 } 234 } else { 235 236 // 未指定類型,統一獲取 237 return $this->getSpecifyAllTimeRangeLogin($startTime, $endTime); 238 } 239 } 240 return array(); 241 } 242 243 /** 244 * 指定類型:獲取某段時間內登陸過的用戶 245 * @param int $startTime 開始時間戳 246 * @param int $endTime 結束時間戳 247 * @param array $typeArr 用戶類型 248 * @return array 249 */ 250 private function getSpecifyTypeTimeRangeLogin($startTime, $endTime, $typeArr) 251 { 252 $data = array(); 253 $successFlg = false; // 查詢到數據標識 254 255 // 指定類型,根據類型單獨獲取,進行整合 256 foreach ($typeArr as $typeArrVal) { 257 258 // 獲取須要查詢的Key 259 $keyList = $this->_redisHandle->getSpecifyTypeTimeRangeRedisKey($typeArrVal, $startTime, $endTime); 260 if (!empty($keyList)) { 261 262 $data[$typeArrVal]['count'] = 0; // 該類型下有效登陸用戶數 263 $data[$typeArrVal]['list'] = array(); // 該類型下有效登陸用戶 264 265 foreach ($keyList as $keyListVal) { 266 267 // 查詢Kye,驗證Redis中是否存在:此處爲單個類型,因此直接看Redis中是否存在該類型Key便可判斷是否存在 268 // 存在的數據不須要去數據庫中去查看 269 $standardKeyList = $this->_redisHandle->getKeys($keyListVal['key']); 270 if (is_array($standardKeyList) && count($standardKeyList) > 0) { 271 272 // Redis存在 273 foreach ($standardKeyList as $standardKeyListVal) { 274 275 // 驗證該用戶在此時間段是否登陸過 276 $redisCheckLogin = $this->_redisHandle->getUserTimeRangeLogin($standardKeyListVal, $startTime, $endTime); 277 if ($redisCheckLogin['hasLog']['count'] > 0) { 278 279 // 同一個用戶只需記錄一次 280 $uid = $this->_redisHandle->getLoginLogKeyInfo($standardKeyListVal, 'uid'); 281 if (!in_array($uid, $data[$typeArrVal]['list'])) { 282 $data[$typeArrVal]['count']++; 283 $data[$typeArrVal]['list'][] = $uid; 284 } 285 $successFlg = true; 286 } 287 } 288 289 } else { 290 291 // 不存在,嘗試從數據庫中獲取 292 $dbResult = $this->_dbHandle->getTimeRangeLoginSuccessUser($keyListVal['time'], $startTime, $endTime, $typeArrVal); 293 if (!empty($dbResult)) { 294 foreach ($dbResult as $dbResultVal) { 295 if (!in_array($dbResultVal, $data[$typeArrVal]['list'])) { 296 $data[$typeArrVal]['count']++; 297 $data[$typeArrVal]['list'][] = $dbResultVal; 298 } 299 } 300 $successFlg = true; 301 } 302 } 303 } 304 } 305 } 306 307 if ($successFlg) { return $data; } 308 return array(); 309 } 310 311 /** 312 * 所有類型:獲取某段時間內登陸過的用戶 313 * @param int $startTime 開始時間戳 314 * @param int $endTime 結束時間戳 315 * @return array 316 */ 317 private function getSpecifyAllTimeRangeLogin($startTime, $endTime) 318 { 319 $count = 0; // 有效登陸用戶數 320 $list = array(); // 有效登陸用戶 321 $successFlg = false; // 查詢到數據標識 322 323 // 未指定類型,直接對全部數據進行檢索 324 // 獲取須要查詢的Key 325 $keyList = $this->_redisHandle->getSpecifyAllTimeRangeRedisKey($startTime, $endTime); 326 327 if (!empty($keyList)) { 328 foreach ($keyList as $keyListVal) { 329 330 // 查詢Kye 331 $standardKeyList = $this->_redisHandle->getKeys($keyListVal['key']); 332 333 if (is_array($standardKeyList) && count($standardKeyList) > 0) { 334 335 // 查詢到Key,直接讀取數據,記錄類型 336 foreach ($standardKeyList as $standardKeyListVal) { 337 338 // 驗證該用戶在此時間段是否登陸過 339 $redisCheckLogin = $this->_redisHandle->getUserTimeRangeLogin($standardKeyListVal, $startTime, $endTime); 340 if ($redisCheckLogin['hasLog']['count'] > 0) { 341 342 // 同一個用戶只需記錄一次 343 $uid = $this->_redisHandle->getLoginLogKeyInfo($standardKeyListVal, 'uid'); 344 if (!in_array($uid, $list)) { 345 $count++; 346 $list[] = $uid; 347 } 348 $successFlg = true; 349 } 350 } 351 } 352 353 // 不管Redis中存在不存在都要嘗試從數據庫中獲取一遍數據,來補充Redis獲取的數據,保證檢索數據完整(Redis類型缺失可能致使) 354 $dbResult = $this->_dbHandle->getTimeRangeLoginSuccessUser($keyListVal['time'], $startTime, $endTime); 355 if (!empty($dbResult)) { 356 foreach ($dbResult as $dbResultVal) { 357 if (!in_array($dbResultVal, $list)) { 358 $count++; 359 $list[] = $dbResultVal; 360 } 361 } 362 $successFlg = true; 363 } 364 } 365 } 366 367 if ($successFlg) { 368 return array( 369 'all' => array( 370 'count' => $count, 371 'list' => $list 372 ) 373 ); 374 } 375 return array(); 376 } 377 378 /** 379 * 驗證開始結束時間 380 * @param string $startTime 開始時間 381 * @param string $endTime 結束時間 382 * @return boolean 383 */ 384 private function checkTimeRange($startTime, $endTime) 385 { 386 return $this->_redisHandle->checkTimeRange($startTime, $endTime); 387 } 388 389 /** 390 * 批量驗證用戶類型 391 * @param array $typeArr 用戶類型數組 392 * @return boolean 393 */ 394 private function checkTypeArr($typeArr) 395 { 396 $flg = false; 397 if (is_array($typeArr) && !empty($typeArr)) { 398 foreach ($typeArr as $val) { 399 if ($this->_redisHandle->checkType($val)) { 400 $flg = true; 401 } else { 402 $flg = false; break; 403 } 404 } 405 } 406 return $flg; 407 } 408 409 /** 410 * 定時任務每週調用一次:從Redis同步登陸日誌到數據庫 411 * @param int $existsDay 一條記錄在Redis中過時時間,單位:天,必須大於31 412 * @return string 413 * 'null': Redis中無數據 414 * 'fail': 同步失敗 415 * 'success':同步成功 416 */ 417 public function cronWeeklySync($existsDay) 418 { 419 420 // 驗證生存時間 421 if ($this->_redisHandle->checkExistsDay($existsDay)) { 422 $likeKey = 'loginLog_*'; 423 $keyList = $this->_redisHandle->getKeys($likeKey); 424 425 if (!empty($keyList)) { 426 foreach ($keyList as $keyVal) { 427 428 if ($this->_redisHandle->checkLoginLogKey($keyVal)) { 429 $keyTime = $this->_redisHandle->getLoginLogKeyInfo($keyVal, 'time'); 430 $thisMonth = date('Y-m'); 431 $beforeMonth = date('Y-m', strtotime('-1 month')); 432 433 // 驗證是否須要進行同步: 434 // 1. 當前日期 >= 8號,對本月全部記錄進行同步,不對本月以前的記錄進行同步 435 // 2. 當前日期 < 8號,對本月全部記錄進行同步,對本月前一個月的記錄進行同步,對本月前一個月以前的全部記錄不進行同步 436 if (date('j') >= 8) { 437 438 // 只同步本月數據 439 if ($thisMonth == $keyTime) { 440 $this->redis2db($keyVal); 441 } 442 } else { 443 444 // 同步本月或本月前一個月數據 445 if ($thisMonth == $keyTime || $beforeMonth == $keyTime) { 446 $this->redis2db($keyVal); 447 } 448 } 449 450 // 驗證是否過時 451 $existsSecond = $existsDay * 24 * 60 * 60; 452 if (strtotime($keyTime) + $existsSecond < time()) { 453 454 // 過時刪除 455 $bitMap = $this->_redisHandle->getLoginLogBitMap($keyVal); 456 Log::INFO('刪除過時數據[' . $keyVal . ']:' . $bitMap); 457 $this->_redisHandle->delLoginLog($keyVal); 458 } 459 } 460 } 461 return 'success'; 462 } 463 return 'null'; 464 } 465 return 'fail'; 466 } 467 468 /** 469 * 將記錄同步到數據庫 470 * @param string $key 記錄Key 471 * @return boolean 472 */ 473 private function redis2db($key) 474 { 475 if ($this->_redisHandle->checkLoginLogKey($key) && $this->_redisHandle->checkRedisLogExists($key)) { 476 $time = $this->_redisHandle->getLoginLogKeyInfo($key, 'time'); 477 $data['id'] = Tools::generateId(); 478 $data['user_id'] = $this->_redisHandle->getLoginLogKeyInfo($key, 'uid'); 479 $data['type'] = $this->_redisHandle->getLoginLogKeyInfo($key, 'type'); 480 $data['year'] = date('Y', strtotime($time)); 481 $data['month'] = date('n', strtotime($time)); 482 $data['bit_log'] = $this->_redisHandle->getLoginLogBitMap($key); 483 return $this->_dbHandle->redis2db($data); 484 } 485 return false; 486 } 487 }
1 <?php 2 3 namespace Lib\LoginLog; 4 5 use Lib\RedisData; 6 use Lib\Status; 7 8 /** 9 * 公共方法 10 * User: dbn 11 * Date: 2017/10/11 12 * Time: 13:11 13 */ 14 class LoginLogCommon 15 { 16 protected $_loginLog; 17 protected $_redis; 18 19 public function __construct(LoginLog $loginLog) 20 { 21 $this->_loginLog = $loginLog; 22 $this->_redis = RedisData::getRedis(); 23 } 24 25 /** 26 * 驗證用戶類型 27 * @param string $type 用戶類型 28 * @return boolean 29 */ 30 protected function checkType($type) 31 { 32 if (in_array($type, array( 33 Status::LOGIN_LOG_TYPE_ADMIN, 34 Status::LOGIN_LOG_TYPE_CARRIER, 35 Status::LOGIN_LOG_TYPE_DRIVER, 36 Status::LOGIN_LOG_TYPE_OFFICE, 37 Status::LOGIN_LOG_TYPE_CLIENT, 38 ))) { 39 return true; 40 } 41 $this->_loginLog->setError('未定義的日誌類型:' . $type); 42 return false; 43 } 44 45 /** 46 * 驗證惟一標識 47 * @param string $uid 48 * @return boolean 49 */ 50 protected function checkUid($uid) 51 { 52 if (is_numeric($uid) && $uid > 0) { 53 return true; 54 } 55 $this->_loginLog->setError('惟一標識非法:' . $uid); 56 return false; 57 } 58 59 /** 60 * 驗證時間戳 61 * @param string $time 62 * @return boolean 63 */ 64 protected function checkTime($time) 65 { 66 if (is_numeric($time) && $time > 0) { 67 return true; 68 } 69 $this->_loginLog->setError('時間戳非法:' . $time); 70 return false; 71 } 72 73 /** 74 * 驗證時間是否在當月中 75 * @param string $time 76 * @return boolean 77 */ 78 protected function checkTimeWhetherThisMonth($time) 79 { 80 if ($this->checkTime($time) && $time > strtotime(date('Y-m')) && $time < strtotime(date('Y-m') . '-' . date('t'))) { 81 return true; 82 } 83 $this->_loginLog->setError('時間未在當前月份中:' . $time); 84 return false; 85 } 86 87 /** 88 * 驗證時間是否超過當前時間 89 * @param string $time 90 * @return boolean 91 */ 92 protected function checkTimeWhetherFutureTime($time) 93 { 94 if ($this->checkTime($time) && $time <= time()) { 95 return true; 96 } 97 return false; 98 } 99 100 /** 101 * 驗證開始/結束時間 102 * @param string $startTime 開始時間 103 * @param string $endTime 結束時間 104 * @return boolean 105 */ 106 protected function checkTimeRange($startTime, $endTime) 107 { 108 if ($this->checkTime($startTime) && 109 $this->checkTime($endTime) && 110 $startTime < $endTime && 111 $startTime < time() 112 ) { 113 return true; 114 } 115 $this->_loginLog->setError('時間範圍非法:' . $startTime . '-' . $endTime); 116 return false; 117 } 118 119 /** 120 * 驗證時間是否在指定範圍內 121 * @param string $time 須要檢查的時間 122 * @param string $startTime 開始時間 123 * @param string $endTime 結束時間 124 * @return boolean 125 */ 126 protected function checkTimeWithinTimeRange($time, $startTime, $endTime) 127 { 128 if ($this->checkTime($time) && 129 $this->checkTimeRange($startTime, $endTime) && 130 $startTime <= $time && 131 $time <= $endTime 132 ) { 133 return true; 134 } 135 $this->_loginLog->setError('請求時間未在時間範圍內:' . $time . '-' . $startTime . '-' . $endTime); 136 return false; 137 } 138 139 /** 140 * 驗證Redis日誌記錄標準Key 141 * @param string $key 142 * @return boolean 143 */ 144 protected function checkLoginLogKey($key) 145 { 146 $pattern = '/^loginLog_\d{4}-\d{1,2}_\S+_\d+$/'; 147 $result = preg_match($pattern, $key, $match); 148 if ($result > 0) { 149 return true; 150 } 151 $this->_loginLog->setError('RedisKey非法:' . $key); 152 return false; 153 } 154 155 /** 156 * 獲取月份中有多少天 157 * @param int $time 時間戳 158 * @return int 159 */ 160 protected function getDaysInMonth($time) 161 { 162 return date('t', $time); 163 } 164 165 /** 166 * 對沒有前導零的月份或日設置前導零 167 * @param int $num 月份或日 168 * @return string 169 */ 170 protected function setDateLeadingZero($num) 171 { 172 if (is_numeric($num) && strlen($num) <= 2) { 173 $num = (strlen($num) > 1 ? $num : '0' . $num); 174 } 175 return $num; 176 } 177 178 /** 179 * 驗證過時時間 180 * @param int $existsDay 一條記錄在Redis中過時時間,單位:天,必須大於31 181 * @return boolean 182 */ 183 protected function checkExistsDay($existsDay) 184 { 185 if (is_numeric($existsDay) && ctype_digit(strval($existsDay)) && $existsDay > 31) { 186 return true; 187 } 188 $this->_loginLog->setError('過時時間非法:' . $existsDay); 189 return false; 190 } 191 192 /** 193 * 獲取開始日期邊界 194 * @param int $time 須要判斷的時間戳 195 * @param int $startTime 起始時間 196 * @return int 197 */ 198 protected function getStartTimeBorder($time, $startTime) 199 { 200 $initDay = 1; 201 if ($this->checkTime($time) && $this->checkTime($startTime) && 202 date('Y-m', $time) === date('Y-m', $startTime) && false !== date('Y-m', $time)) { 203 $initDay = date('j', $startTime); 204 } 205 return $initDay; 206 } 207 208 /** 209 * 獲取結束日期邊界 210 * @param int $time 須要判斷的時間戳 211 * @param int $endTime 結束時間 212 * @return int 213 */ 214 protected function getEndTimeBorder($time, $endTime) 215 { 216 $border = $this->getDaysInMonth($time); 217 if ($this->checkTime($time) && $this->checkTime($endTime) && 218 date('Y-m', $time) === date('Y-m', $endTime) && false !== date('Y-m', $time)) { 219 $border = date('j', $endTime); 220 } 221 return $border; 222 } 223 }
1 <?php 2 3 namespace Lib\LoginLog; 4 use Think\Model; 5 6 /** 7 * 數據庫登陸日誌處理類 8 * User: dbn 9 * Date: 2017/10/11 10 * Time: 13:12 11 */ 12 class LoginLogDBHandle extends LoginLogCommon 13 { 14 15 /** 16 * 從數據庫中獲取用戶某月記錄在指定時間範圍內的用戶信息 17 * @param string $type 用戶類型 18 * @param int $uid 惟一標識(用戶ID) 19 * @param int $time 須要查詢月份時間戳 20 * @param int $startTime 開始時間戳 21 * @param int $endTime 結束時間戳 22 * @return array 23 * array( 24 * 'hasLog' => array( 25 * 'count' => n, // 有效登陸次數,天天重複登陸算一次 26 * 'list' => array('2017-10-1', '2017-10-15' ...) // 有效登陸日期 27 * ), 28 * 'notLog' => array( 29 * 'count' => n, // 未登陸次數 30 * 'list' => array('2017-10-1', '2017-10-15' ...) // 未登陸日期 31 * ) 32 * ) 33 */ 34 public function getUserTimeRangeLogin($type, $uid, $time, $startTime, $endTime) 35 { 36 $hasCount = 0; // 有效登陸次數 37 $notCount = 0; // 未登陸次數 38 $hasList = array(); // 有效登陸日期 39 $notList = array(); // 未登陸日期 40 41 if ($this->checkType($type) && $this->checkUid($uid) && $this->checkTimeWithinTimeRange($time, $startTime, $endTime)) { 42 43 $timeYM = date('Y-m', $time); 44 45 // 設置開始時間 46 $initDay = $this->getStartTimeBorder($time, $startTime); 47 48 // 設置結束時間 49 $border = $this->getEndTimeBorder($time, $endTime); 50 51 $bitMap = $this->getBitMapFind($type, $uid, date('Y', $time), date('n', $time)); 52 for ($i = $initDay; $i <= $border; $i++) { 53 54 if (!empty($bitMap)) { 55 if ($bitMap[$i-1] == '1') { 56 $hasCount++; 57 $hasList[] = $timeYM . '-' . $this->setDateLeadingZero($i); 58 } else { 59 $notCount++; 60 $notList[] = $timeYM . '-' . $this->setDateLeadingZero($i); 61 } 62 } else { 63 $notCount++; 64 $notList[] = $timeYM . '-' . $this->setDateLeadingZero($i); 65 } 66 } 67 } 68 69 return array( 70 'hasLog' => array( 71 'count' => $hasCount, 72 'list' => $hasList 73 ), 74 'notLog' => array( 75 'count' => $notCount, 76 'list' => $notList 77 ) 78 ); 79 } 80 81 /** 82 * 從數據庫獲取用戶某月日誌位圖 83 * @param string $type 用戶類型 84 * @param int $uid 惟一標識(用戶ID) 85 * @param int $year 年Y 86 * @param int $month 月n 87 * @return string 88 */ 89 private function getBitMapFind($type, $uid, $year, $month) 90 { 91 $model = D('Home/StatLoginLog'); 92 $map['type'] = array('EQ', $type); 93 $map['user_id'] = array('EQ', $uid); 94 $map['year'] = array('EQ', $year); 95 $map['month'] = array('EQ', $month); 96 97 $result = $model->field('bit_log')->where($map)->find(); 98 if (false !== $result && isset($result['bit_log']) && !empty($result['bit_log'])) { 99 return $result['bit_log']; 100 } 101 return ''; 102 } 103 104 /** 105 * 從數據庫中判斷用戶在某一天是否登陸過 106 * @param string $type 用戶類型 107 * @param int $uid 惟一標識(用戶ID) 108 * @param int $time 時間戳 109 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 110 */ 111 public function dateWhetherLogin($type, $uid, $time) 112 { 113 if ($this->checkType($type) && $this->checkUid($uid) && $this->checkTime($time)) { 114 115 $timeInfo = getdate($time); 116 $bitMap = $this->getBitMapFind($type, $uid, $timeInfo['year'], $timeInfo['mon']); 117 if (!empty($bitMap)) { 118 if ($bitMap[$timeInfo['mday']-1] == '1') { 119 return true; 120 } 121 } 122 } 123 return false; 124 } 125 126 /** 127 * 從數據庫中判斷用戶在某月是否登陸過 128 * @param string $type 用戶類型 129 * @param int $uid 惟一標識(用戶ID) 130 * @param int $time 時間戳 131 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 132 */ 133 public function dateMonthWhetherLogin($type, $uid, $time) 134 { 135 if ($this->checkType($type) && $this->checkUid($uid) && $this->checkTime($time)) { 136 137 $timeInfo = getdate($time); 138 $userArr = $this->getMonthLoginSuccessUser($timeInfo['year'], $timeInfo['mon'], $type); 139 if (!empty($userArr)) { 140 if (in_array($uid, $userArr)) { 141 return true; 142 } 143 } 144 } 145 return false; 146 } 147 148 /** 149 * 獲取某月全部有效登陸過的用戶ID 150 * @param int $year 年Y 151 * @param int $month 月n 152 * @param string $type 用戶類型,爲空時獲取所有類型 153 * @return array 154 */ 155 public function getMonthLoginSuccessUser($year, $month, $type = '') 156 { 157 $data = array(); 158 if (is_numeric($year) && is_numeric($month)) { 159 $model = D('Home/StatLoginLog'); 160 $map['year'] = array('EQ', $year); 161 $map['month'] = array('EQ', $month); 162 $map['bit_log'] = array('LIKE', '%1%'); 163 if ($type != '' && $this->checkType($type)) { 164 $map['type'] = array('EQ', $type); 165 } 166 $result = $model->field('user_id')->where($map)->select(); 167 if (false !== $result && count($result) > 0) { 168 foreach ($result as $val) { 169 if (isset($val['user_id'])) { 170 $data[] = $val['user_id']; 171 } 172 } 173 } 174 } 175 return $data; 176 } 177 178 /** 179 * 從數據庫中獲取某月全部記錄在指定時間範圍內的用戶ID 180 * @param int $time 查詢的時間戳 181 * @param int $startTime 開始時間戳 182 * @param int $endTime 結束時間戳 183 * @param string $type 用戶類型,爲空時獲取所有類型 184 * @return array 185 */ 186 public function getTimeRangeLoginSuccessUser($time, $startTime, $endTime, $type = '') 187 { 188 $data = array(); 189 if ($this->checkTimeWithinTimeRange($time, $startTime, $endTime)) { 190 191 $timeInfo = getdate($time); 192 193 // 獲取知足時間條件的記錄 194 $model = D('Home/StatLoginLog'); 195 $map['year'] = array('EQ', $timeInfo['year']); 196 $map['month'] = array('EQ', $timeInfo['mon']); 197 if ($type != '' && $this->checkType($type)) { 198 $map['type'] = array('EQ', $type); 199 } 200 201 $result = $model->where($map)->select(); 202 if (false !== $result && count($result) > 0) { 203 204 // 設置開始時間 205 $initDay = $this->getStartTimeBorder($time, $startTime); 206 207 // 設置結束時間 208 $border = $this->getEndTimeBorder($time, $endTime); 209 210 foreach ($result as $val) { 211 212 $bitMap = $val['bit_log']; 213 for ($i = $initDay; $i <= $border; $i++) { 214 215 if ($bitMap[$i-1] == '1' && !in_array($val['user_id'], $data)) { 216 $data[] = $val['user_id']; 217 } 218 } 219 } 220 } 221 } 222 return $data; 223 } 224 225 /** 226 * 將數據更新到數據庫 227 * @param array $data 單條記錄的數據 228 * @return boolean 229 */ 230 public function redis2db($data) 231 { 232 $model = D('Home/StatLoginLog'); 233 234 // 驗證記錄是否存在 235 $map['user_id'] = array('EQ', $data['user_id']); 236 $map['type'] = array('EQ', $data['type']); 237 $map['year'] = array('EQ', $data['year']); 238 $map['month'] = array('EQ', $data['month']); 239 240 $count = $model->where($map)->count(); 241 if (false !== $count && $count > 0) { 242 243 // 存在記錄進行更新 244 $saveData['bit_log'] = $data['bit_log']; 245 246 if (!$model->create($saveData, Model::MODEL_UPDATE)) { 247 248 $this->_loginLog->setError('同步登陸日誌-更新記錄,建立數據對象失敗:' . $model->getError()); 249 logger()->error('同步登陸日誌-更新記錄,建立數據對象失敗:' . $model->getError()); 250 return false; 251 } else { 252 253 $result = $model->where($map)->save(); 254 255 if (false !== $result) { 256 return true; 257 } else { 258 $this->_loginLog->setError('同步登陸日誌-更新記錄,更新數據失敗:' . json_encode($data)); 259 logger()->error('同步登陸日誌-更新記錄,更新數據失敗:' . json_encode($data)); 260 return false; 261 } 262 } 263 } else { 264 265 // 不存在記錄插入一條新的記錄 266 if (!$model->create($data, Model::MODEL_INSERT)) { 267 268 $this->_loginLog->setError('同步登陸日誌-插入記錄,建立數據對象失敗:' . $model->getError()); 269 logger()->error('同步登陸日誌-插入記錄,建立數據對象失敗:' . $model->getError()); 270 return false; 271 } else { 272 273 $result = $model->add(); 274 275 if (false !== $result) { 276 return true; 277 } else { 278 $this->_loginLog->setError('同步登陸日誌-插入記錄,插入數據失敗:' . json_encode($data)); 279 logger()->error('同步登陸日誌-插入記錄,插入數據失敗:' . json_encode($data)); 280 return false; 281 } 282 } 283 } 284 } 285 }
1 <?php 2 3 namespace Lib\LoginLog; 4 5 /** 6 * Redis登陸日誌處理類 7 * User: dbn 8 * Date: 2017/10/11 9 * Time: 15:53 10 */ 11 class LoginLogRedisHandle extends LoginLogCommon 12 { 13 /** 14 * 記錄登陸:天天只記錄一次登陸,只容許設置當月內登陸記錄 15 * @param string $key 日誌記錄Key 16 * @param int $time 時間戳 17 * @return boolean 18 */ 19 public function setLogging($key, $time) 20 { 21 if ($this->checkLoginLogKey($key) && $this->checkTimeWhetherThisMonth($time)) { 22 23 // 判斷用戶當天是否已經登陸過 24 $whetherLoginResult = $this->dateWhetherLogin($key, $time); 25 if (!$whetherLoginResult) { 26 27 // 當天未登陸,記錄登陸 28 $this->_redis->setBit($key, date('d', $time), 1); 29 } 30 return true; 31 } 32 return false; 33 } 34 35 /** 36 * 從Redis中判斷用戶在某一天是否登陸過 37 * @param string $key 日誌記錄Key 38 * @param int $time 時間戳 39 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 40 */ 41 public function dateWhetherLogin($key, $time) 42 { 43 if ($this->checkLoginLogKey($key) && $this->checkTime($time)) { 44 $result = $this->_redis->getBit($key, date('d', $time)); 45 if ($result === 1) { 46 return true; 47 } 48 } 49 return false; 50 } 51 52 /** 53 * 從Redis中判斷用戶在某月是否登陸過 54 * @param string $key 日誌記錄Key 55 * @return boolean 參數錯誤或未登陸過返回false,登陸過返回true 56 */ 57 public function dateMonthWhetherLogin($key) 58 { 59 if ($this->checkLoginLogKey($key)) { 60 $result = $this->_redis->bitCount($key); 61 if ($result > 0) { 62 return true; 63 } 64 } 65 return false; 66 } 67 68 /** 69 * 判斷某月登陸記錄在Redis中是否存在 70 * @param string $key 日誌記錄Key 71 * @return boolean 72 */ 73 public function checkRedisLogExists($key) 74 { 75 if ($this->checkLoginLogKey($key)) { 76 if ($this->_redis->exists($key)) { 77 return true; 78 } 79 } 80 return false; 81 } 82 83 /** 84 * 從Redis中獲取用戶某月記錄在指定時間範圍內的用戶信息 85 * @param string $key 日誌記錄Key 86 * @param int $startTime 開始時間戳 87 * @param int $endTime 結束時間戳 88 * @return array 89 * array( 90 * 'hasLog' => array( 91 * 'count' => n, // 有效登陸次數,天天重複登陸算一次 92 * 'list' => array('2017-10-1', '2017-10-15' ...) // 有效登陸日期 93 * ), 94 * 'notLog' => array( 95 * 'count' => n, // 未登陸次數 96 * 'list' => array('2017-10-1', '2017-10-15' ...) // 未登陸日期 97 * ) 98 * ) 99 */ 100 public function getUserTimeRangeLogin($key, $startTime, $endTime) 101 { 102 $hasCount = 0; // 有效登陸次數 103 $notCount = 0; // 未登陸次數 104 $hasList = array(); // 有效登陸日期 105 $notList = array(); // 未登陸日期 106 107 if ($this->checkLoginLogKey($key) && $this->checkTimeRange($startTime, $endTime) && $this->checkRedisLogExists($key)) { 108 109 $keyTime = $this->getLoginLogKeyInfo($key, 'time'); 110 $keyTime = strtotime($keyTime); 111 $timeYM = date('Y-m', $keyTime); 112 113 // 設置開始時間 114 $initDay = $this->getStartTimeBorder($keyTime, $startTime); 115 116 // 設置結束時間 117 $border = $this->getEndTimeBorder($keyTime, $endTime); 118 119 for ($i = $initDay; $i <= $border; $i++) { 120 $result = $this->_redis->getBit($key, $i); 121 if ($result === 1) { 122 $hasCount++; 123 $hasList[] = $timeYM . '-' . $this->setDateLeadingZero($i); 124 } else { 125 $notCount++; 126 $notList[] = $timeYM . '-' . $this->setDateLeadingZero($i); 127 } 128 } 129 } 130 131 return array( 132 'hasLog' => array( 133 'count' => $hasCount, 134 'list' => $hasList 135 ), 136 'notLog' => array( 137 'count' => $notCount, 138 'list' => $notList 139 ) 140 ); 141 } 142 143 /** 144 * 面向用戶:獲取時間範圍內可能須要的Key 145 * @param string $type 用戶類型 146 * @param int $uid 惟一標識(用戶ID) 147 * @param string $startTime 開始時間 148 * @param string $endTime 結束時間 149 * @return array 150 */ 151 public function getTimeRangeRedisKey($type, $uid, $startTime, $endTime) 152 { 153 $list = array(); 154 155 if ($this->checkType($type) && $this->checkUid($uid) && $this->checkTimeRange($startTime, $endTime)) { 156 157 $data = $this->getSpecifyUserKeyHandle($type, $uid, $startTime); 158 if (!empty($data)) { $list[] = $data; } 159 160 $temYM = strtotime('+1 month', strtotime(date('Y-m', $startTime))); 161 162 while ($temYM <= $endTime) { 163 $data = $this->getSpecifyUserKeyHandle($type, $uid, $temYM); 164 if (!empty($data)) { $list[] = $data; } 165 166 $temYM = strtotime('+1 month', $temYM); 167 } 168 } 169 return $list; 170 } 171 private function getSpecifyUserKeyHandle($type, $uid, $time) 172 { 173 $data = array(); 174 $key = $this->getLoginLogKey($type, $uid, $time); 175 if ($this->checkLoginLogKey($key)) { 176 $data = array( 177 'key' => $key, 178 'time' => $time 179 ); 180 } 181 return $data; 182 } 183 184 /** 185 * 面向類型:獲取時間範圍內可能須要的Key 186 * @param string $type 用戶類型 187 * @param string $startTime 開始時間 188 * @param string $endTime 結束時間 189 * @return array 190 */ 191 public function getSpecifyTypeTimeRangeRedisKey($type, $startTime, $endTime) 192 { 193 $list = array(); 194 195 if ($this->checkType($type) && $this->checkTimeRange($startTime, $endTime)) { 196 197 $data = $this->getSpecifyTypeKeyHandle($type, $startTime); 198 if (!empty($data)) { $list[] = $data; } 199 200 $temYM = strtotime('+1 month', strtotime(date('Y-m', $startTime))); 201 202 while ($temYM <= $endTime) { 203 $data = $this->getSpecifyTypeKeyHandle($type, $temYM); 204 if (!empty($data)) { $list[] = $data; } 205 206 $temYM = strtotime('+1 month', $temYM); 207 } 208 } 209 return $list; 210 } 211 private function getSpecifyTypeKeyHandle($type, $time) 212 { 213 $data = array(); 214 $temUid = '11111111'; 215 216 $key = $this->getLoginLogKey($type, $temUid, $time); 217 if ($this->checkLoginLogKey($key)) { 218 $arr = explode('_', $key); 219 $arr[count($arr)-1] = '*'; 220 $key = implode('_', $arr); 221 $data = array( 222 'key' => $key, 223 'time' => $time 224 ); 225 } 226 return $data; 227 } 228 229 /** 230 * 面向所有:獲取時間範圍內可能須要的Key 231 * @param string $startTime 開始時間 232 * @param string $endTime 結束時間 233 * @return array 234 */ 235 public function getSpecifyAllTimeRangeRedisKey($startTime, $endTime) 236 { 237 $list = array(); 238 239 if ($this->checkTimeRange($startTime, $endTime)) { 240 241 $data = $this->getSpecifyAllKeyHandle($startTime); 242 if (!empty($data)) { $list[] = $data; } 243 244 $temYM = strtotime('+1 month', strtotime(date('Y-m', $startTime))); 245 246 while ($temYM <= $endTime) { 247 $data = $this->getSpecifyAllKeyHandle($temYM); 248 if (!empty($data)) { $list[] = $data; } 249 250 $temYM = strtotime('+1 month', $temYM); 251 } 252 } 253 return $list; 254 } 255 private function getSpecifyAllKeyHandle($time) 256 { 257 $data = array(); 258 $temUid = '11111111'; 259 $temType = 'office'; 260 261 $key = $this->getLoginLogKey($temType, $temUid, $time); 262 if ($this->checkLoginLogKey($key)) { 263 $arr = explode('_', $key); 264 array_pop($arr); 265 $arr[count($arr)-1] = '*'; 266 $key = implode('_', $arr); 267 $data = array( 268 'key' => $key, 269 'time' => $time 270 ); 271 } 272 return $data; 273 } 274 275 /** 276 * 從Redis中查詢知足條件的Key 277 * @param string $key 查詢的Key 278 * @return array 279 */ 280 public function getKeys($key) 281 { 282 return $this->_redis->keys($key); 283 } 284 285 /** 286 * 從Redis中刪除記錄 287 * @param string $key 記錄的Key 288 * @return boolean 289 */ 290 public function delLoginLog($key) 291 { 292 return $this->_redis->del($key); 293 } 294 295 /** 296 * 獲取日誌標準Key:前綴_年-月_用戶類型_惟一標識 297 * @param string $type 用戶類型 298 * @param int $uid 惟一標識(用戶ID) 299 * @param int $time 時間戳 300 * @return string 301 */ 302 public function getLoginLogKey($type, $uid, $time) 303 { 304 if ($this->checkType($type) && $this->checkUid($uid) && $this->checkTime($time)) { 305 return 'loginLog_' . date('Y-m', $time) . '_' . $type . '_' . $uid; 306 } 307 return ''; 308 } 309 310 /** 311 * 獲取日誌標準Key上信息 312 * @param string $key key 313 * @param string $field 須要的參數 time,type,uid 314 * @return mixed 返回對應的值,沒有返回null 315 */ 316 public function getLoginLogKeyInfo($key, $field) 317 { 318 $param = array(); 319 if ($this->checkLoginLogKey($key)) { 320 $arr = explode('_', $key); 321 $param['time'] = $arr[1]; 322 $param['type'] = $arr[2]; 323 $param['uid'] = $arr[3]; 324 } 325 return $param[$field]; 326 } 327 328 /** 329 * 獲取Key記錄的登陸位圖 330 * @param string $key key 331 * @return string 332 */ 333 public function getLoginLogBitMap($key) 334 { 335 $bitMap = ''; 336 if ($this->checkLoginLogKey($key)) { 337 $time = $this->getLoginLogKeyInfo($key, 'time'); 338 $maxDay = $this->getDaysInMonth(strtotime($time)); 339 for ($i = 1; $i <= $maxDay; $i++) { 340 $bitMap .= $this->_redis->getBit($key, $i); 341 } 342 } 343 return $bitMap; 344 } 345 346 /** 347 * 驗證日誌標準Key 348 * @param string $key 349 * @return boolean 350 */ 351 public function checkLoginLogKey($key) 352 { 353 return parent::checkLoginLogKey($key); 354 } 355 356 /** 357 * 驗證開始/結束時間 358 * @param string $startTime 開始時間 359 * @param string $endTime 結束時間 360 * @return boolean 361 */ 362 public function checkTimeRange($startTime, $endTime) 363 { 364 return parent::checkTimeRange($startTime, $endTime); 365 } 366 367 /** 368 * 驗證用戶類型 369 * @param string $type 370 * @return boolean 371 */ 372 public function checkType($type) 373 { 374 return parent::checkType($type); 375 } 376 377 /** 378 * 驗證過時時間 379 * @param int $existsDay 一條記錄在Redis中過時時間,單位:天,必須大於31 380 * @return boolean 381 */ 382 public function checkExistsDay($existsDay) 383 { 384 return parent::checkExistsDay($existsDay); 385 } 386 }
http://www.javashuo.com/article/p-pgugovbw-gc.html