欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-14 22:29:09
项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小。具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进!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 相关命令可以极大的简化一些统计操作。常用命令 setbitgetbitbitcountbitop

  再说弊端:

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

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

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

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. 参考资料

  

  

  

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