当前位置: 移动技术网 > IT编程>数据库>Redis > 基于Redis位图实现系统用户登录统计

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

2020年11月24日  | 移动技术网IT编程  | 我要评论
项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小。具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进!1. 需求 实现记录用户哪天进行了登录,每天只记录是否登录过,重复登录状

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

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 相关命令可以极大的简化一些统计操作。常用命令 、、、

  再说弊端:

  存储单一:这也算不上什么缺点,位图上存储只是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. 参考资料

  

  

  

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

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网