基于Redis位图实现系统用户登录统计
Posted BNDong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Redis位图实现系统用户登录统计相关的知识,希望对你有一定的参考价值。
项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小。具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进!
需求
- 实现记录用户哪天进行了登录,每天只记录是否登录过,重复登录状态算已登录。不需要记录用户的操作行为,不需要记录用户上次登录时间和IP地址(这部分以后需要可以单独拿出来存储)
- 区分用户类型
- 查询数据需要精确到天
分析
考虑到只是简单的记录用户是否登录,记录数据比较单一,查询需要精确到天。以百万用户量为前提,前期考虑了几个方案
使用文件
使用单文件存储:文件占用空间增长速度快,海量数据检索不方便,Map/Reduce 操作也麻烦。
使用多文件存储
使用数据库
不太认同直接使用数据库写入/读取
- 频繁请求数据库做一些日志记录浪费服务器开销。
- 随着时间推移数据急剧增大
- 海量数据检索效率也不高,同时使用索引,易产生碎片,每次插入数据还要维护索引,影响性能
所以只考虑使用数据库做数据备份。
使用Redis位图(BitMap)
这也是在网上看到的方法,比较实用。也是我最终考虑使用的方法,
首先优点:
再说弊端:
-
存储单一:位图上存储只是0/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月是否登录: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. 当前日期 >= 8号,对本月所有记录进行同步,不对本月之前的记录进行同步 2. 当前日期 < 8号,对本月所有记录进行同步,对本月前一个月的记录进行同步,对本月前一个月之前的所有记录不进行同步
二、验证过期,如果过期,记录日志后删除
数据库,表结构
每周同步一次数据到数据库,表中一条数据对应一个BitMap,记录一个月数据。每次更新已存在的、插入没有的
暂定接口
- 设置用户登录
- 查询单个用户某天是否登录过
- 查询单个用户某月是否登录过
- 查询单个用户某个时间段是否登录过
- 查询单个用户某个时间段登录信息
- 指定用户类型:获取某个时间段内有效登录的用户
- 全部用户:获取某个时间段内有效登录的用户
Code
TP3中实现的代码,在接口服务器内部库中,Application\\Lib\\
├─LoginLog
│ ├─Logs 日志目录,Redis中过期的记录删除写入日志进行备份
│ ├─LoginLog.class.php 对外接口
│ ├─LoginLogCommon.class.php 公共工具类
│ ├─LoginLogDBHandle.class.php 数据库操作类
│ ├─LoginLogRedisHandle.class.php Redis操作类
LoginLog.class.php
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<以上是关于基于Redis位图实现系统用户登录统计的主要内容,如果未能解决你的问题,请参考以下文章
《java精品毕设》基于javaweb宠物领养平台管理系统(源码+毕设论文+sql):主要实现:个人中心,信息修改,填写领养信息,交流论坛,新闻,寄养信息,公告,宠物领养信息,我的寄养信息等(代码片段