基于Redis位图实现系统用户登录统计

Posted BNDong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Redis位图实现系统用户登录统计相关的知识,希望对你有一定的参考价值。

项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小。具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进!

需求

  • 实现记录用户哪天进行了登录,每天只记录是否登录过,重复登录状态算已登录。不需要记录用户的操作行为,不需要记录用户上次登录时间和IP地址(这部分以后需要可以单独拿出来存储)
  • 区分用户类型
  • 查询数据需要精确到天

分析

考虑到只是简单的记录用户是否登录,记录数据比较单一查询需要精确到天。以百万用户量为前提,前期考虑了几个方案

使用文件

使用单文件存储:文件占用空间增长速度快,海量数据检索不方便,Map/Reduce 操作也麻烦。

使用多文件存储:按日期对文件进行分割。每天记录当天日志,文件量过大。

使用数据库

不太认同直接使用数据库写入/读取

  • 频繁请求数据库做一些日志记录浪费服务器开销。
  • 随着时间推移数据急剧增大
  • 海量数据检索效率也不高,同时使用索引,易产生碎片,每次插入数据还要维护索引,影响性能

所以只考虑使用数据库做数据备份。

使用Redis位图(BitMap)

这也是在网上看到的方法,比较实用。也是我最终考虑使用的方法,

首先优点:

  • 数据量小:一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。1亿人每天的登陆情况,用1亿bit,约1200WByte,约10M 的字符就能表示。

  • 计算方便:实用Redis bit 相关命令可以极大的简化一些统计操作。常用命令 SETBITGETBITBITCOUNTBITOP

再说弊端:

  • 存储单一:位图上存储只是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,记录一个月数据。每次更新已存在的、插入没有的

暂定接口

  1.  设置用户登录
  2.  查询单个用户某天是否登录过
  3.     查询单个用户某月是否登录过
  4.  查询单个用户某个时间段是否登录过
  5.  查询单个用户某个时间段登录信息
  6.  指定用户类型:获取某个时间段内有效登录的用户
  7.  全部用户:获取某个时间段内有效登录的用户

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位图实现系统用户登录统计的主要内容,如果未能解决你的问题,请参考以下文章

数组与链表的应用-位图数组在Redis中的应用

redis中的Setbit位图法统计活跃用户

redis bitmap实现点赞的思路

基于express+redis高速实现实时在线用户数统计

《java精品毕设》基于javaweb宠物领养平台管理系统(源码+毕设论文+sql):主要实现:个人中心,信息修改,填写领养信息,交流论坛,新闻,寄养信息,公告,宠物领养信息,我的寄养信息等(代码片段

Redis位图实现用户签到功能