• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    基于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);
    }
    }

    5. 参考资料

      https://segmentfault.com/a/1190000008188655

      http://blog.csdn.net/rdhj5566/article/details/54313840

      http://www.redis.net.cn/tutorial/3508.html

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    您可能感兴趣的文章:
    • PHP使用redis位图bitMap 实现签到功能
    • Redis精确去重计数方法(咆哮位图)
    • redis通过位图法记录在线用户的状态详解
    • java redis 实现简单的用户签到功能
    • 基于Redis位图实现用户签到功能
    上一篇:Redis密码设置与访问限制实现方法
    下一篇:window环境redis通过AOF恢复数据的方法
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

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