目标:如何使用Redis实现用户签到功能
设计思路
使用Mysql的话1000w用户一个月就是3亿条数据,存储压力太大。
使用Redis的Bitmaps完成
Bitmaps叫位图,它不是Redis的基本数据类型(比如Strings、Lists、Sets、Hashes这类实际的数据类型),而是基于String数据类型的按位操作,高阶数据类型的一种。Bitmaps支持的最大位数是232位。使用512M内存就可以存储多达42.9亿的字节信息(232 = 4,294,967,296)
它是由一组bit位组成的,每个bit位对应0和1两个状态,虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作位图,可以把它看作是一个bit数组,数组的下标就是偏移量。它的优点是内存开销小、效率高且操作简单,很适合用于签到这类场景。比如按月进行存储,一个月最多31天,那么我们将该月用户的签到缓存二进制就是00000000000000000000000000000000,当某天签到将0改成1即可,而且Redis提供对bitmap的很多操作比如存储、获取、统计等指令,使用起来非常方便。
BitMaps常用指令
- SETBIT key offset value: 将指定键(key)的位图中偏移量(offset)处的位设置为给定的值(value)。其中,offset 表示位图中的位索引,value 可以是 0 或 1。
- GETBIT key offset: 返回指定键(key)的位图中偏移量(offset)处的位的值(0 或 1)。
- BITCOUNT key [start end]: 统计指定键(key)的位图中指定范围内的位为 1 的个数。可选的 start 和 end 参数用于指定范围,如果不指定,则统计整个位图。
- BITOP operation destkey key [key ...]: 对指定键(key)的位图进行位操作,并将结果存储到 destkey 中。operation 可以是 AND、OR、XOR 或 NOT,用于指定位操作的类型。
- BITPOS key bit [start] [end]: 在指定键(key)的位图中查找第一个设置为指定 bit(0 或 1)的位的索引。可选的 start 和 end 参数用于指定查找范围。
- BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]: BITFIELD 指令允许您在一个命令中执行多个位级操作,包括获取、设置和增减位值。
功能开发
需求说明
用户签到,默认是当天,但可以通过传入日期补签,返回用户连续签到次数(后续如果有积分规则,就会返回用户此次签到积分)
代码实现
SignService层关注方法
- 获取登录用户信息
- 根据日期获取当前是多少号(使用BITSET指令关注时,offset从0开始计算,0就代表1号)
- 构建用户按月存储key(user:sign:用户id:月份)
- 判断用户是否签到(GETBIT指令)
- 用户签到(SETBIT)
- 返回用户连续签到次数(BITFIELD key GET [u/i] type offset value, 获取从用户从当前日期开始到1号的所有签到状态,然后进行位移操作,获取连续签到天数)
PHP示例
// 示例参数 假设今天是21号,所以数据只有到21号为止
$redis = new Redis();
$redis->connect('redis', 6379);
$redis->setbit('user:sign:5:202306', 0, 1); // 这是1号的数据
$redis->setbit('user:sign:5:202306', 1, 1);
$redis->setbit('user:sign:5:202306', 2, 1);
$redis->setbit('user:sign:5:202306', 3, 1);
$redis->setbit('user:sign:5:202306', 4, 1);
$redis->setbit('user:sign:5:202306', 5, 1);
$redis->setbit('user:sign:5:202306', 6, 1);
$redis->setbit('user:sign:5:202306', 7, 1);
$redis->setbit('user:sign:5:202306', 8, 1);
$redis->setbit('user:sign:5:202306', 9, 1);
$redis->setbit('user:sign:5:202306', 10, 1);
$redis->setbit('user:sign:5:202306', 11, 1);
$redis->setbit('user:sign:5:202306', 12, 1);
$redis->setbit('user:sign:5:202306', 13, 1);
$redis->setbit('user:sign:5:202306', 14, 1);
$redis->setbit('user:sign:5:202306', 15, 1);
$redis->setbit('user:sign:5:202306', 16, 0); // 没有签到的日子
$redis->setbit('user:sign:5:202306', 17, 1);
$redis->setbit('user:sign:5:202306', 18, 1);
$redis->setbit('user:sign:5:202306', 19, 1);
$redis->setbit('user:sign:5:202306', 20, 1); // 这是21号的数据
// user:sign:5:202306 用户ID为5 2023年06月的签到数据
$sign_days = 0;
$today = date('j'); // 今天的日期 可以手动设置为21
$decimal = $redis->rawCommand('BITFIELD', 'user:sign:5:202306', ...['GET', 'u' . $today, '0']);
$binary = decbin($decimal[0]);
$sign_days = 0;
$length = strlen($binary);
for ($i = $length - 1; $i >= 0; $i--) {
$bit = $binary[$i];
if ($bit === '1') {
$sign_days++;
} else {
break;
}
}
echo "二进制数: " . $binary;
echo "
";
echo "连续签到 $sign_days 天";
echo "
";
echo "一共签到 {$redis->bitCount('user:sign:5:202306')}天";
# 结果
二进制数: 111111111111111101111
连续签到 4 天
一共签到 20天
Comments NOTHING