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

需求

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

分析

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

使用文件

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

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

使用数据库

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

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

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

使用Redis位图(BitMap)

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

首先优点:

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

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

再说弊端:

  • 存储单一:位图上存储只是0/1,所以需要存储其他信息就要别的地方单独记录,对于需要存储信息多的记录就需要使用别的方法了

设计

Redis BitMap

Key结构:前缀_年Y-月m_用户类型_用户ID

标准Key:KEYS loginLog_2017-10_client_1001
检索全部:KEYS loginLog_*
检索某年某月全部:KEYS loginLog_2017-10_*
检索单个用户全部:KEYS loginLog_*_client_1001
检索单个类型全部:KEYS loginLog_*_office_*
...

每条BitMap记录单个用户一个月的登录情况,一个bit位表示一天登录情况。

设置用户1001,217-10-25登录:SETBIT loginLog_2017-10_client_1001 25 1
获取用户1001,217-10-25是否登录:GETBIT loginLog_2017-10_client_1001 25
获取用户1001,217-10月是否登录:BITCOUNT loginLog_2017-10_client_1001
获取用户1001,217-10/9/7月是否登录:BITOP OR stat loginLog_2017-10_client_1001 loginLog_2017-09_client_1001 loginLog_2017-07_client_1001
...

关于获取登录信息,就得获取BitMap然后拆开,循环进行判断。特别涉及时间范围,需要注意时间边界的问题,不要查询出多余的数据

获取数据Redis优先级高于数据库,Redis有的记录不要去数据库获取

Redis数据过期:在数据同步中进行判断,过期时间自己定义(我定义的过期时间单位为“天”,必须大于31)。

在不能保证同步与过期一致性的问题,不要给Key设置过期时间,会造成数据丢失。

上一次更新时间:         2107-10-02
下一次更新时间: 2017-10-09
Redis BitMap 过期时间: 2017-10-05 这样会造成:2017-10-09同步的时候,3/4/5/6/7/8/9 数据丢失

所以我把Redis过期数据放到同步时进行判断  

我自己想的同步策略(定时每周一凌晨同步):

一、验证是否需要进行同步:
1. 当前日期 >= 8号,对本月所有记录进行同步,不对本月之前的记录进行同步
2. 当前日期 < 8号,对本月所有记录进行同步,对本月前一个月的记录进行同步,对本月前一个月之前的所有记录不进行同步
二、验证过期,如果过期,记录日志后删除

数据库,表结构

每周同步一次数据到数据库,表中一条数据对应一个BitMap,记录一个月数据。每次更新已存在的、插入没有的

暂定接口

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

Code

TP3中实现的代码,在接口服务器内部库中,Application\Lib\

  ├─LoginLog

  │ ├─Logs 日志目录,Redis中过期的记录删除写入日志进行备份

  │ ├─LoginLog.class.php 对外接口

  │ ├─LoginLogCommon.class.php 公共工具类

  │ ├─LoginLogDBHandle.class.php 数据库操作类

  │ ├─LoginLogRedisHandle.class.php Redis操作类

LoginLog.class.php

 <?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;
}
}

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;
}
}

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;
}
}
}
}
}

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);
}
}

参考资料

https://segmentfault.com/a/1190000008188655

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

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

基于Redis位图实现系统用户登录统计的更多相关文章

  1. 基于Redis位图实现用户签到功能

    场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...

  2. springcloud微服务基于redis集群的单点登录

    springcloud微服务基于redis集群的单点登录 yls 2019-9-23 简介 本文介绍微服务架构中如何实现单点登录功能 创建三个服务: 操作redis集群的服务,用于多个服务之间共享数据 ...

  3. SSH——基于BaseDao和BaseAction实现用户登录

           基于BaseDao和BaseAction实现用户登录  1. 首先修改login.jsp页面,点击登录按钮,提交表单 <a onclick="document.forms ...

  4. 基于Python实现的系统SLA可用性统计

    基于Python实现的系统SLA可用性统计 1. 介绍 SLA是Service Level Agreement的英文缩写,也叫服务质量协议.根据SRE Google运维解密一书中的定义: SLA是服务 ...

  5. Redis缓存Mysql模拟用户登录Java实现实例[www]

    Redis缓存Mysql模拟用户登录Java实现实例 https://jingyan.baidu.com/article/09ea3ede1dd0f0c0aede3938.html redis+mys ...

  6. 查看Linux系统用户登录日志

    日志: /var/log/wtmp说明: 此文件是二进制文件,查看方法如下:该日志文件永久记录每个用户登录.注销及系统的启动.停机的事件.因此随着系统正常运行时间的增加,该文件的大小也会越来越大,增加 ...

  7. 基于Servlet的MVC模式用户登录实例

    关于MVC模式的简单解释 M Model,模型层,例如登录实例中,用于处理登录操作的类: V View,视图层,用于展示以及与用户交互.使用html.js.css.jsp.jQuery等前端技术实现: ...

  8. 使用系统用户登录Oracle

    如果数据库安装不在本机上,@后面加的是服务名或IP地址 如果是sys用户的话,它具有管理员的权限,要使用sysdba或sysoper权限来登录oracle工具.

  9. Django Redis配合Mysql验证用户登录

    1.redis_check.py # coding:utf-8 import pymysql import redis import sys def con_mysql(sql): db = pymy ...

随机推荐

  1. CCNA基础知识摘录

    cisco设备的启动要点: 1.检测硬件(保存在rom) 2.载入软件(IOS)(保存在Flash) 3.调入配置文件(密码,IP地址,路由协议都保存在此)(此文件保存在NVRAM) 0x2102:正 ...

  2. 交叉编译器安装 gcc version 4.3.3 (Sourcery G++ Lite 2009q1-203)

    安装环境    :ubuntu 14.04 安装包       :toolchain.tar.gz 编译器版本:gcc version 4.3.3 (Sourcery G++ Lite 2009q1- ...

  3. 1~N任意三个数最大的最小公倍数(Java版)

    最大最小公倍数 如题 话不多说,直接上代码 public class MaxCommonMultiple{ public static void main(String[] args) { Scann ...

  4. 一些LVS实验配置、工具和方案

    最近做了一些LVS配置和方案的验证实验,将过程中用到的一些配置.工具和具体的解决方案记录一下.使用DR模式.验证一种不中断业务的RealServer升级或者重启方案. 网络规划: 节点 IP地址 ce ...

  5. Matlab生成.exe可执行程序

    由于在教学过程中需要演示Matlab程序,而教学机又未安装Matlab程序,因此有必要将Matlab程序生成.exe可执行程序,便于直接执行. 在Matlab中提供了Complier,可直接使用. ( ...

  6. 201521123071 《JAVA程序设计》第四周学习总结

    1. 本周学习总结 1.1 1.2 在本周的学习中,主要学习了以下几点: 注释的应用,并能在Eclipse中查看 继承的基本定义,关键字super的用法,覆盖与重载 多态与is-a,instanceo ...

  7. 201521123032 《Java程序设计》第1周学习总结

    #1. 本周学习总结 下载熟悉eclipse,了解java的入门.用notepad++和eclipse编写Java程序.复习到了十进制转化为二进制,八进制与十六进制. #2. 书面作业 ##2.1为什 ...

  8. #黑客社会工程学攻防演练#[Chapter 1]

    1.1 什么是社会工程学 社会工程学(Social Engineering)是关于建立理论通过自然的.社会的和制度上的途径并特别强调根据现实的双向计划和设计经验来一步一步地解决各种社会问题.社会工程学 ...

  9. We Talk -- 团队博客

    WeTalk --在线群聊程序 团队博客 服务器一直在运行,使用客户端可直接登入使用.(做得很粗糙...) 客户端下载(java环境下直接运行) 0.项目介绍 现在我们网上交流离不开微信和QQ,当然在 ...

  10. 201521123044 《Java程序设计》第10周学习总结

    1. 本章学习总结 2. 书面作业 本次PTA作业题集异常丶多线程 1.finally题目4-2 1.1 截图你的提交结果 1.2 4-2中finally中捕获异常需要注意什么? 1.无论try-ca ...