redis实现用户签到,统计活跃用户,用户在线状态,用户留存率

开发的过程中,可能会遇到用户签到、统计当天的活跃用户、以及每个用户的在线状态,用户留存率的开发需求,可能会用传统的方法,根据相应的需求设计数据库表等,但这样耗费的存储空间大,以及性能方面也不会太好,下面为大家介绍一些使用的方法
redis官方文档: http://www.redis.cn/documentation.html

一.用redis的set集合统计日活用户数

用户登录以后,把用户id添加到redis的set中,set会自动进行去重
127.0.0.1:6379> sadd users_2023_02_21 user1
(integer) 1
127.0.0.1:6379> sadd users_2023_02_21 user2
(integer) 1
127.0.0.1:6379> sadd users_2023_02_21 user3
(integer) 1
统计只需一条命令
127.0.0.1:6379> scard users_2023_02_21
(integer) 3

可以看出来,2023_02_21的用户数是3个,很简单,但是集合只适用于用户数比较少的场合,假如用户有100万,set存储100万个id号,如果一个id号占32个字节,总共就是差不多32M,一个月就是960M 差不多一个G了,用户量大的项目不适用

二.用redis中bitmap统计用户签到,活跃用户,用户在线状态,用户留存率等

思路:
1.设置一个key专门用来记录用户日活的,可以使用时间来翻滚
2.使用每个用户的唯一标识映射一个偏移量,比如使用id,这里可以把id换算成一个数字或直接使用id的二进制值作为该用户在当天是否活跃偏移量
3.用户登录则把该用户偏移量上的位值设置为1
4.每天按日期生成一个位图(bitmap)
5.计算日活则使用bitcount即可获得一个key的位值为1的量
6.计算月活(一个月内登陆的用户去重总数)即可把30天的所有bitmap做or计算,然后再计算bitcount
7.计算留存率(次日留存=昨天今天连续登录的人数/昨天登录的人数) 即昨天的bitmap与今天的bitmap做and计算就是连续登录的再做bitcount就得到连续登录人数,再bitcount得到昨天登录人数,就可以通过公式计算出次日留存

1.bitmap介绍

(1).BitMap是什么

就是通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身,我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间,但其位计算和位表示数值相对于局限,故如要用位来做业务数据记录,那么就不要在意value的值了

(2).Redis中的BitMap

Redis从2.2.0版本开始新增了setbit,getbit,bitcount等几个bitmap相关命令,虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展

(3).几个前提:

  • 数据在redis中都是二进制存储

  • setbit和getbit和bitcount是string数据类型的命令

  • 8bit表示一个ascll字符,因为是c写的redis

  • offset偏移量是从0开始

(4)空间占用、以及第一次分配空间需要的时间

在一台2010MacBook Pro上,offset为2^32-1(分配512MB)需要大约300ms,offset为2^30-1(分配128MB)需要大约80ms,offset为2^28-1(分配32MB)需要大约30ms,offset为2^26-1(分配8MB)大约需要8ms<来自官方文档>

大概的空间占用计算公式是:($offset/8/1024/1024)MB

(5)getbit命令

指令 GETBIT key offset
返回值:字符串值指定偏移量上的位(bit)。当偏移量 OFFSET 比字符串值的长度大,或者 key 不存在时,返回 0 。

对 key 所储存的字符串值,获取指定偏移量上的位(bit)

注:offset表示偏移量

127.0.0.1:6379> set A a
OK
127.0.0.1:6379> get A
"a"
127.0.0.1:6379> getbit A 0
(integer) 0
127.0.0.1:6379> getbit A 1
(integer) 1
127.0.0.1:6379> getbit A 2
(integer) 1
127.0.0.1:6379> getbit A 3
(integer) 0
127.0.0.1:6379> getbit A 4
(integer) 0
127.0.0.1:6379> getbit A 5
(integer) 0
127.0.0.1:6379> getbit A 6
(integer) 0

(5)setbit命令

指令 SETBIT key offset value
返回值:指定偏移量原来储存的位

设置或者清除key的value(字符串)在offset处的bit值(只能只0或者1)

将上述A的值“a”的第6位修改为1,也就是相当于ASCLL码加2,从而值从a变成了c
127.0.0.1:6379> setbit A 6 1
(integer) 0
127.0.0.1:6379> get A
"c"
127.0.0.1:6379> 

(6).Bitcount 命令

指令BITCOUNT key [start] [end]
返回值:1比特位的数量

计算给定key的字符串值中,被设置为 1 的比特位的数量

不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0

值c的二进制数应该是01100011,故bitcount计算出来应该是4
127.0.0.1:6379> get A
"c"
127.0.0.1:6379> bitcount A
(integer) 4

(7).bitop 命令

指令 operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:
BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey
BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 - destkey
BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey
BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey
除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入

对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上

BITOP 的复杂度为 O(N) ,当处理大型矩阵(matrix)或者进行大数据量的统计时,最好将任务指派到附属节点(slave)进行,避免阻塞主节点

2.使用场景一:用户签到

需要展示最近一个月的签到情况,使用bitmap
<?php
$redis = new Redis();
$redis->connect('127.0.0.1');
//用户uid
$uid = 1;
//记录有uid的key
$cacheKey = sprintf("sign_%d", $uid);
//开始有签到功能的日期
$startDate = '2023-02-21';
//今天的日期
$todayDate = '2023-02-21';
//计算offset
$startTime = strtotime($startDate);
$todayTime = strtotime($todayDate);
$offset = floor(($todayTime - $startTime) / 86400);
echo "今天是第{$offset}天" . PHP_EOL;
//签到
//一年一个用户会占用多少空间呢?大约365/8=45.625个字节,好小
$redis->setBit($cacheKey, $offset, 1);
//查询签到情况
$bitStatus = $redis->getBit($cacheKey, $offset);
echo 1 == $bitStatus ? '今天已经签到啦' : '还没有签到呢';
echo PHP_EOL;
//计算总签到次数
echo $redis->bitCount($cacheKey) . PHP_EOL;

3.使用场景二:统计活跃用户

使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1
那么该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃)
命令 BITOP operation destkey key [key ...]
说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上
说明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数
$redis = Yii::$app->redis_sms;
//日期对应的活跃用户
$data = array(
'2023-02-21' => array(10000030, 10600031, 10050031, 10040031, 10200031, 10020031, 10001001, 10000011, 10000021, 10000131),
'2023-02-20' => array(10000030, 10600031, 10050031, 10040031, 10200031, 10020031, 10110001, 10000011),
'2023-02-19' => array(10000030, 10600031, 10050031, 10040031, 10200031),
'2023-02-18' => array(10000030, 10600031),
'2023-02-17' => array(10000030, 10600031, 10050031,)
);

//批量设置活跃状态
foreach ($data as $date => $uids) {
    $y = date("Y", strtotime($date));
    $m = date("m", strtotime($date));
    $d = date("d", strtotime($date));
    $cacheKey = sprintf("active_user:%s:%s:%s", $y, $m, $d);
    foreach ($uids as $uid) {
        $redis->setbit($cacheKey, $uid - 10000000, 1); // 偏移量,用户初始id较大时使用,减少redis计算
    }
}

$redis->bitop('AND', 'active_user:stat', 'active_user:2023:02:21', 'active_user:2023:02:200') . PHP_EOL;
//总活跃用户:6
echo "总活跃用户:" . $redis->bitcount('active_user:stat') . PHP_EOL;
$redis->bitop('AND', 'active_user:stat1', 'active_user:2023:02:19', 'active_user:2023:02:18', 'active_user:2023:02:12') . PHP_EOL;
//总活跃用户:2
echo "总活跃用户:" . $redis->bitcount('active_user:stat1') . PHP_EOL;
$redis->bitop('AND', 'active_user:stat2', 'active_user:2023:02:18', 'active_user:2023:02:12') . PHP_EOL;
//总活跃用户:8
echo "总活跃用户:" . $redis->bitcount('active_user:stat2') . PHP_EOL;

// 获取活跃状态的用户id
$result = $this->get_bitmap_all("active_user:2023:02:19");

public function get_bitmap_all($key)
    {
        $redis = Yii::$app->redis;

        $result = [];
        $value = $redis->get($key);
        if ($value) {
            /**
             * 解包(redis返回来的是二进制字符串,我们需要把它解成对应的数字)
             * 关于unpack的用法,如果不了解,大家可以网上搜索学习,改天可以单独写篇文章分享
             */
            $bitmap = unpack('C*', $value);
            if ($bitmap) {
                foreach ($bitmap as $key => $number) {
                    // 下标是从1开始的; 1个字节8位
                    $offset = ($key - 1) * 8;
                    // 过滤没有标记的字节段
                    if ($number) {
                        for ($i = 0; $i < 8; $i++) {
                            // 遍历这个字节的每一位,是否有为1的值,如果有,那就记录这个位置的偏移量,就是用户id
                            if (($number >> $i & 1) == 1) {
                                // 8位范围是0~7,因为redis是高位到低位存储,所以要反过来计算偏移量
                                $result[] = $offset + (7 - $i);
                            }
                        }
                    }
                }
            }
        }
        return $result;
    }
   /**
     * 新增上一天的日活跃用户数,以及更新上一天所在的月份的月活跃用户数
     */
    public function actionActiveUser()
    {
        $day = 1;
        $date_key = date("Y:m:d", strtotime("-{$day} day"));
        $date = date("Y-m-d", strtotime("-{$day} day"));

        $redisUser = Yii::$app->redis_user;

        $platforms = array_keys(Common::fromPlatformText());//获取平台
        //保存对应日期平台活跃的用户到数据库
        foreach ($platforms as $platform) { //循环平台
            $key = sprintf("active_user:%s:%s", $date_key, $platform);
            //1.查询redis活跃用户id
            //2.查询到的用户id组成一个数组(该数组可能会很大)
            $userIds = $this->get_bitmap_all($key);
            if ($userIds) {
                //3.循环查询(使用yield生成器)用户注册表(t_reg)获取代理渠道相关信息
                $i_userIds = $this->activeUser($userIds);
                foreach ($i_userIds as $userId) {
                    $key = sprintf("active_user:%s:%s", $date_key, $platform);
                    //从用户注册表(reg)中获取对应用户渠道相关数据
                    $reg = Reg::getGameUserData(['id' => $userId], ['agency_from', 'time']);
                    if (!$reg) {
                        continue;
                    }
                    $agencyFromPId = $reg['id']; //渠道id,默认为0:0 无渠道
                    //4.重组key, 增加渠道id,把数据保存到redis服务器中
                    $key .= ":" . $agencyFromPId;
                    $redisUser->setbit($key, $userId, 1);
                    $redisUser->expire($key, 15552000);//半年过期时间
                }
            }
        }

        //5.添加数据到活跃用户数据表进行统计

        //获取代理渠道类型以及对应的代理渠道商
        $agency_from = Common::agencyFrom();
        //保存当前日期平台对应的所有渠道对应的活跃用户到数据库
        foreach ($platforms as $platform) { //循环平台
            foreach ($agency_from as $id) {   //循环渠道相关信息
                    //构建活跃用户key
                    $key = sprintf("active_user:%s:%s:%s", $date_key, $platform, $id);
                    if ($redisUser->exists($key)) { // 判断key是否存在
                        $num = $redisUser->bitcount($key);//获取当前日期对应平台对应的渠道的活跃用户
                        //5.添加用户活跃数据
                        ActiveUser::addInfo(['date' => $date, 'platform' => $platform, 'num' => $num, 'agency_from_id' => $id]);
                }
            }
        }

        //保存当前日期所在月份平台对应的所有渠道的活跃用户到数据库
        //生成月份数据
        $month = date("Y-m", strtotime($date));
        $timestamp = strtotime($month);
        $month_format = date("Y:m", $timestamp);    // 月份时间格式
        $m_days = date('t', $timestamp); //当前月份的天数
        $date_start = strtotime(date('Y-m-01', $timestamp)); //当前月份开始日期戳
        $date_end = strtotime(date('Y-m-' . $m_days, $timestamp));//当前月份结束日期戳
        for ($i = $date_end; $i >= $date_start; $i -= 86400) {  // 循环当前月份所在日期
            $dateKey = date("Y:m:d", $i);
            foreach ($platforms as $platform) { //循环平台,添加并更新当前月份活跃用户数据总数到redis服务器中
                foreach ($agency_from as $id) { //循环渠道相关信息
                        $monthKey = 'active_user_month_num:' . $month_format . ":" . $platform . ":" . $id; // 月份活跃用户数量key
                        $key = sprintf("active_user:%s:%s:%s", $dateKey, $platform, $id     );   //获取活跃用户key
                        if ($redisUser->exists($key)) {
                            //添加并更新当前月份活跃用户数据总数到后台redis服务器中
                            $redisUser->bitop('or', $monthKey, $monthKey, $key);    //bitop活跃用户
                            $redisUser->expire($monthKey, 15552000);//半年过期时间     
                    }
                }
            }
        }
        //保存当前月份平台对应的所有代理渠道商对应的活跃用户到数据库
        foreach ($platforms as $platform) { //循环渠道,bitOp活跃用户
            foreach ($agency_platform as $id) {
                    $monthKey = 'active_user_month_num:' . $month_format . ":" . $platform . ":" . $id; // 月份活跃用户数量key
                    if ($redisUser->exists($monthKey)) {
                        $num = $redisUser->bitcount($monthKey);    //bitop活跃用户
                        //保存前日期所在月份的渠道活跃的用户到数据库
                        ActiveUser::addOrUpdate(['date' => $month, 'platform' => $platform, 'num' => $num,  'agency_from_id' => $id]);
                }
            }
        }
    }

    /**
     * yield生成器
     * @param $userIds
     * @return \Generator
     */
    public function activeUser($userIds) {
        //使用yield生成器
        foreach ($userIds as $userId) {
            yield $userId;
        }
    }

4.使用场景三:实现用户上线次数统计

需求:
假设希望记录开发网站上的用户上线的频率,比如:计算用户 A 上线了多少天,用户 B 上线了多少天,从而决定让哪些用户参加 某个活动,这个功能可以使用 SETBIT 和 BITCOUNT 来实现。
每当用户在某一天上线的时候,我们就使用 SETBIT ,以用户id作为 key ,将那天所代表的网站的上线日作为 offset 参数,并将这个 offset 上的为设置为 1
案例:
如果今天是网站上线的第365天,而用户10001在今天浏览过网站,那么执行命令 SETBIT 10001 365 1 ;如果明天用户10001 也继续浏览网站,那么执行命令 SETBIT 10001 366 1 ,以此类推。当要计算用户10001 总共以来的上线次数时,就使用 BITCOUNT 命令:执行 BITCOUNT 10001 ,得出的结果就是 用户10001上线的总天数

5.使用场景四:用户在线状态

思路:
查询当前用户是否在线,使用bitmap是一个节约空间效率又高的一种方法,设置一个key,用户ID为offset,如果在线就设置为1,不在线就设置为0
 $redis = Yii::$app->redis;
        //时段对应的在线用户
        $data = array(
            '2023-02-14 05:00:00' => array(10000030, 10600031),
            '2023-02-15 05:00:00' => array(10000030, 10600031),
            '2023-02-15 13:00:00' => array(10000030, 10600031, 10050031, 10050031, 10040031, 10200031, 10020031, 10110001),
            '2023-02-19 12:00:00' => array(10000030, 10600031, 10050031, 10040031, 10200031, 10020031, 10001001, 10000011, 10000021, 10000131),
            '2023-02-18 13:00:00' => array(10000030, 10600031, 10050031, 10050031, 10040031, 10200031, 10020031, 10110001),
            '2023-02-16 13:00:00' => array(10000030, 10600031, 10050031, 10040031, 10200031),
        );

        //批量设置在线用户状态
        foreach ($data as $date => $uids) {
            $y = date("Y", strtotime($date));
            $m = date("m", strtotime($date));
            $d = date("d", strtotime($date));
            $H = date("H", strtotime($date));
            $cacheKey = sprintf("online_user:%s:%s:%s:%s", $y, $m, $d, $H);
            foreach ($uids as $uid) {
                $redis->setbit($cacheKey, $uid - 10000000, 1);
            }
        }
        $redis->bitop('AND', 'online_user:stat', 'online_user:2023:02:17:13', 'online_user:2023:02:18:13') . PHP_EOL;
        //总在线用户:6
        echo "总在线用户:" . $redis->bitcount('online_user:stat') . PHP_EOL;

        $redis->bitop('AND', 'online_user:stat1', 'online_user:2023:02:18:13', 'online_user:2023:02:15:11') . PHP_EOL;
        //总在线用户:2
        echo "总在线用户:" . $redis->bitcount('online_user:stat1') . PHP_EOL;

6.使用场景五:计算用户留存率

    /**
     * 计算用户留存率并保存到mysql:30天用户留存率
     * @return string
     */
    public function actionRetentionRateUser()
    {
        $date_start = date('Y-m-d', strtotime('-1 month'));
        $date_end = date('Y-m-d');

        $date_start = strtotime($date_start);
        $date_end = strtotime($date_end);

        $redis = Yii::$app->redis_user;
        $platforms = array_keys(Common::fromPlatformText());    //平台
        //循环日期,获取日期范围对应的用户留存率并保存到数据库
        for ($i = $date_end; $i >= $date_start; $i -= 86400) {
            //当前日期
            $date = date("Y:m:d", $i);
            //次日
            $date2 = date("Y:m:d", strtotime("+1 day", $i));
            //三日
            $date3 = date("Y:m:d", strtotime("+3 day", $i));
            //四日
            $date4 = date("Y:m:d", strtotime("+4 day", $i));
            //五日
            $date5 = date("Y:m:d", strtotime("+5 day", $i));
            //六日
            $date6 = date("Y:m:d", strtotime("+6 day", $i));
            //七日
            $date7 = date("Y:m:d", strtotime("+7 day", $i));
            //十五日
            $date15 = date("Y:m:d", strtotime("+15 day", $i));
            //三十日
            $date30 = date("Y:m:d", strtotime("+30 day", $i));

            //平台
            foreach ($platforms as $platform) {
                        $key1 = 'active_user:' . $date . ':' . $platform;
                        $key2 = $platform;
                        $dest_day2 = 'retention_rate_user:' . $date . ':2day:' . $key2;
                        $dest_day3 = 'retention_rate_user:' . $date . ':3day:' . $key2;
                        $dest_day4 = 'retention_rate_user:' . $date . ':4day:' . $key2;
                        $dest_day5 = 'retention_rate_user:' . $date . ':5day:' . $key2;
                        $dest_day6 = 'retention_rate_user:' . $date . ':6day:' . $key2;
                        $dest_day7 = 'retention_rate_user:' . $date . ':7day:' . $key2;
                        $dest_day15 = 'retention_rate_user:' . $date . ':15day:' . $key2;
                        $dest_day30 = 'retention_rate_user:' . $date . ':30day:' . $key2;
                        //获取当前日期,上一天连续登录的人数
                        $redis->bitop('AND', $dest_day2, $key1, 'active_user:' . $date2 . ':' . $key2);
                        //获取当前日期,第三天连续登录的人数
                        $redis->bitop('AND', $dest_day3, $key1, 'active_user:' . $date3 . ':' . $key2);
                        //获取当前日期,第四天连续登录的人数
                        $redis->bitop('AND', $dest_day4, $key1, 'active_user:' . $date4 . ':' . $key2);
                        //获取当前日期,第五天连续登录的人数
                        $redis->bitop('AND', $dest_day5, $key1, 'active_user:' . $date5 . ':' . $key2);
                        //获取当前日期,第六天连续登录的人数
                        $redis->bitop('AND', $dest_day6, $key1, 'active_user:' . $date6 . ':' . $key2);
                        //获取当前日期,第七天连续登录的人数
                        $redis->bitop('AND', $dest_day7, $key1, 'active_user:' . $date7 . ':' . $key2);
                        //获取当前日期,第十五天连续登录的人数
                        $redis->bitop('AND', $dest_day15, $key1, 'active_user:' . $date15 . ':' . $key2);
                        //获取当前日期,第三十天连续登录的人数
                        $redis->bitop('AND', $dest_day30, $key1, 'active_user:' . $date30 . ':' . $key2);

                        //计算登录人数
                        //当前日期登录人数
                        $curCount = $redis->bitcount($key1);
                        //当前日期,上一天连续登录的人数
                        $nextCount = $redis->bitcount($dest_day2);
                        //当前日期,第三天连续登录的人数
                        $threeCount = $redis->bitcount($dest_day3);
                        //当前日期,第四天连续登录的人数
                        $fourCount = $redis->bitcount($dest_day4);
                        //当前日期,第五天连续登录的人数
                        $fiveCount = $redis->bitcount($dest_day5);
                        //当前日期,第六天连续登录的人数
                        $sixCount = $redis->bitcount($dest_day6);
                        //当前日期,第七天连续登录的人数
                        $sevenCount = $redis->bitcount($dest_day7);
                        //当前日期,第十五天连续登录的人数
                        $fifteenCount = $redis->bitcount($dest_day15);
                        //当前日期,第三十天连续登录的人数
                        $thirtyCount = $redis->bitcount($dest_day30);
                        //设置半年过期时间
                        $redis->expire($dest_day2, 15552000);
                        $redis->expire($dest_day3, 15552000);
                        $redis->expire($dest_day4, 15552000);
                        $redis->expire($dest_day5, 15552000);
                        $redis->expire($dest_day6, 15552000);
                        $redis->expire($dest_day7, 15552000);
                        $redis->expire($dest_day15, 15552000);
                        $redis->expire($dest_day30, 15552000);
                        //判断登录人数是否为0,为0的就不插入数据库了
                        $count = $curCount + $nextCount + $threeCount + $fourCount + $fiveCount + $sixCount
                            + $sevenCount + $fifteenCount + $thirtyCount;

                        if ($count == 0) {
                            continue;
                        }
                        $params = [
                            'cur_day' => $curCount,
                            'next_day' => $nextCount,
                            'three_day' => $threeCount,
                            'four_day' => $fourCount,
                            'five_day' => $fiveCount,
                            'six_day' => $sixCount,
                            'seven_day' => $sevenCount,
                            'fifteen_day' => $fifteenCount,
                            'thirty_day' => $thirtyCount,
                            'date' => $date,
                            'from_platform' => $platform,
                        ];
                        //插入用户留存统计表
                        RetentionRateUser::saveInfo($params);      
            }
        }
    }

参考:https://blog.csdn.net/maoyuanming0806/article/details/81813776

参考:https://www.php.cn/php-weizijiaocheng-387074.html

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值