您现在的位置是:网站首页> 编程资料编程资料
基于Redis位图实现系统用户登录统计_Redis_
2023-05-27
571人已围观
简介 基于Redis位图实现系统用户登录统计_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
_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[
相关内容
- Redis密码设置与访问限制实现方法_Redis_
- Redis分布式锁的使用和实现原理详解_Redis_
- redission分布式锁防止重复初始化问题_Redis_
- 全网最完整的Redis新手入门指导教程_Redis_
- Windows安装Redis并添加本地自启动服务的实例详解_Redis_
- Redis客户端及服务端的安装教程详解_Redis_
- Redis集群水平扩展、集群中添加以及删除节点的操作_Redis_
- redis keys与scan命令的区别说明_Redis_
- redis通过lua脚本,获取满足key pattern的所有值方式_Redis_
- Redis swap空间(虚拟内存)的使用详解_Redis_
点击排行
本栏推荐
