失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > redis实现用户签到 统计活跃用户 用户状态 用户留存率

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

时间:2020-03-13 17:37:06

相关推荐

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

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

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

用户登录以后,把用户id添加到redis的set中,set会自动进行去重

127.0.0.1:6379> sadd users__02_21 user1(integer) 1127.0.0.1:6379> sadd users__02_21 user2(integer) 1127.0.0.1:6379> sadd users__02_21 user3(integer) 1

统计只需一条命令

127.0.0.1:6379> scard users__02_21(integer) 3

可以看出来,_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)空间占用、以及第一次分配空间需要的时间

在一台MacBook 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 aOK127.0.0.1:6379> get A"a"127.0.0.1:6379> getbit A 0(integer) 0127.0.0.1:6379> getbit A 1(integer) 1127.0.0.1:6379> getbit A 2(integer) 1127.0.0.1:6379> getbit A 3(integer) 0127.0.0.1:6379> getbit A 4(integer) 0127.0.0.1:6379> getbit A 5(integer) 0127.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) 0127.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 = '-02-21';//今天的日期$todayDate = '-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('-02-21' => array(10000030, 10600031, 10050031, 10040031, 10200031, 1001, 10001001, 10000011, 10000021, 10000131),'-02-20' => array(10000030, 10600031, 10050031, 10040031, 10200031, 1001, 10110001, 10000011),'-02-19' => array(10000030, 10600031, 10050031, 10040031, 10200031),'-02-18' => array(10000030, 10600031),'-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::02:21', 'active_user::02:200') . PHP_EOL;//总活跃用户:6echo "总活跃用户:" . $redis->bitcount('active_user:stat') . PHP_EOL;$redis->bitop('AND', 'active_user:stat1', 'active_user::02:19', 'active_user::02:18', 'active_user::02:12') . PHP_EOL;//总活跃用户:2echo "总活跃用户:" . $redis->bitcount('active_user:stat1') . PHP_EOL;$redis->bitop('AND', 'active_user:stat2', 'active_user::02:18', 'active_user::02:12') . PHP_EOL;//总活跃用户:8echo "总活跃用户:" . $redis->bitcount('active_user:stat2') . PHP_EOL;// 获取活跃状态的用户id$result = $this->get_bitmap_all("active_user::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的值,如果有,那就记录这个位置的偏移量,就是用户idif (($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 ); //获取活跃用户keyif ($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; // 月份活跃用户数量keyif ($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('-02-14 05:00:00' => array(10000030, 10600031),'-02-15 05:00:00' => array(10000030, 10600031),'-02-15 13:00:00' => array(10000030, 10600031, 10050031, 10050031, 10040031, 10200031, 1001, 10110001),'-02-19 12:00:00' => array(10000030, 10600031, 10050031, 10040031, 10200031, 1001, 10001001, 10000011, 10000021, 10000131),'-02-18 13:00:00' => array(10000030, 10600031, 10050031, 10050031, 10040031, 10200031, 1001, 10110001),'-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::02:17:13', 'online_user::02:18:13') . PHP_EOL;//总在线用户:6echo "总在线用户:" . $redis->bitcount('online_user:stat') . PHP_EOL;$redis->bitop('AND', 'online_user:stat1', 'online_user::02:18:13', 'online_user::02:15:11') . PHP_EOL;//总在线用户:2echo "总在线用户:" . $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);}}}

参考:/maoyuanming0806/article/details/81813776

参考:/php-weizijiaocheng-387074.html

如果觉得《redis实现用户签到 统计活跃用户 用户状态 用户留存率》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。