在现代应用程序中,用户签到是一个常见的功能。我们通常使用 MySQL
数据库来存储用户的签到记录。然而,随着用户数量的增加,数据库中的记录将会随时间和用户量线性增长,这不仅增加了存储的负担,而且可能影响查询效率。在追求更高存储效率和查询性能的场景下,MySQL
可能不再是最佳选择。
这时,Redis
的 Bitmap
数据结构就显得尤为重要。利用 Redis Bitmap
,我们不仅可以大幅度降低存储空间的占用,还可以高效实现复杂的用户行为统计,如连续签到天数、月签到统计等。接下来,本文将详细介绍如何利用 Redis Bitmap
实现高效的用户签到统计功能。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
Redis
的 Bitmap
,也称为位图,是一种用于存储和处理二进制位(bit
)的数据结构。在 Redis
中,Bitmap
不是一种独立的数据类型,而是字符串类型的一种特殊使用方式。你可以通过特定的命令在字符串数据中处理二进制位。由于 Redis
中字符串的最大长度为 512
MB,每个字节有 8
位,因此一个字符串最多可以存储 512 * 1024 * 1024 * 8 = 2^32
个位。
Bitmap
的主要应用场景如下:
0
表示未签到,1
表示已签到。通过位图可以快速统计用户的连续签到天数、总签到天数等。bitmap
可以实现一个布隆过滤器,bitmap
可以用于高效地判断某个元素是否存在于一个集合中。通过多个哈希函数将元素映射到 bitmap
的不同位上,快速判断元素的存在性。Bitmap
记录用户是否在某一天活跃。例如,用户访问网站或者使用应用时,将相应的位设为 1
,通过统计位的数量可以快速计算活跃用户数。签到记录以年为单位,一个用户,对应一张位图(Bitmap
),表示用户在一年内的签到情况。
key
的设计:user:sign:%d:%d
,第一个占位符表示年份,第二个占位符表示用户的编号。bitmap
值的设计:由于一年只有 365 或 366 天,因此我们只需要 bitmap
里面的前 366 位,即 0-365 位。接下来将会结合 Go
语言和 Redis
中间件实现以下功能:
接下来的功能实现将会使用 Go
语言代码进行演示,因此我们需要先安装 Go Redis
依赖。
go get github.com/redis/go-redis/v9
要实现用户签到的功能,我们需要用到 Redis
的 SETBIT
命令。
SETBIT
命令用于设置或清除字符串值中的某个位(bit
)值,用法如下所示:
SETBIT key offset value
bit
)的位置。位的位置从 0 开始计数。示例代码:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/redis-bitmap-sign/sign/main.go
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func RedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
}
func main() {
rdb := RedisClient()
if rdb == nil {
panic("redis client is nil")
}
oldValue, err := rdb.SetBit(context.Background(), "user:2024:1", 0, 1).Result()
if err != nil {
panic(err)
}
if oldValue == 1 {
fmt.Println("重复签到")
} else {
fmt.Println(oldValue) // 0,表示这个位(`bit`)被设置新值之前的值。
}
}
在上述代码示例中,我们通过调用 Redis
客户端实例的 SetBit
方法,将 key
为 user:2024:1
对应的 bitmap
中第 0 位设为 1。这代表 ID
为 1 的用户在 2024-01-01 进行了签到。SetBit
方法的返回值为该位(bit
)被设置新值之前的值。
要实现查询用户签到的状态,我们需要用到 Redis
的 GETBIT
命令。
GETBIT
命令用于获取字符串值中的某个位(bit
)的值,用法如下所示:
GETBIT key offset
bit
)的位置。位的位置从 0 开始计数。示例代码:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/redis-bitmap-sign/sign-in-record/main.go
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func RedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
}
func main() {
rdb := RedisClient()
if rdb == nil {
panic("redis client is nil")
}
value, err := rdb.GetBit(context.Background(), "user:2024:1", 0).Result()
if err != nil {
panic(err)
}
fmt.Println(value) // 1
}
在上述代码示例中,我们通过调用 Redis
客户端实例的 GetBit
方法,获取到 key
为 user:2024:1
对应的 bitmap
中的第 0 位的值为 1,这代表 ID
为 1 的用户在 2024-01-01 已经签到过了。
要实现统计一年里的签到次数,我们需要用到 Redis
的 BITFIELD
命令。
Redis
的 BITFIELD
命令是一个非常强大的命令,它允许你执行多种位级操作,包括读取、设置、增加位字段。这个命令能够操作存储在字符串中的位数组,并可以看作是直接在字符串上执行复杂的位操作。用法如下所示:
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]
详情请参考:Redis BITFIRLED Command
示例代码:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/redis-bitmap-sign/cumulative-sign/main.go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
// RedisClient 初始化 Redis 客户端
func RedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
}
// GetCumulativeDays 获取指定年份的累计签到天数
func GetCumulativeDays(ctx context.Context, rdb *redis.Client, userID int, year int, dayOfYear int) (int, error) {
key := fmt.Sprintf("user:%d:%d", year, userID)
segmentSize := 63
cumulativeDays := 0
bitOps := make([]any, 0)
for i := 0; i < dayOfYear; i += segmentSize {
size := segmentSize
if i+segmentSize > dayOfYear {
size = dayOfYear - i
}
bitOps = append(bitOps, "GET", fmt.Sprintf("u%d", size), fmt.Sprintf("#%d", i))
}
values, err := rdb.BitField(ctx, key, bitOps...).Result()
if err != nil {
return 0, fmt.Errorf("failed to get bitfield: %w", err)
}
for idx, value := range values {
if value != 0 {
size := segmentSize
if (idx+1)*segmentSize > dayOfYear {
size = dayOfYear % segmentSize
}
for j := 0; j < size; j++ {
if (value & (1 << (size - 1 - j))) != 0 {
cumulativeDays++
}
}
}
}
return cumulativeDays, nil
}
func main() {
rdb := RedisClient()
if rdb == nil {
log.Fatal("redis client is nil")
}
now := time.Now()
// 获取当前的年份
year := now.Year()
// 获取当前日期是今年的第几天
dayOfYear := now.YearDay()
// 假设用户 ID 为 1
userID := 1
cumulativeDays, err := GetCumulativeDays(context.Background(), rdb, userID, year, dayOfYear)
if err != nil {
log.Fatalf("failed to get cumulative days: %v", err)
}
fmt.Printf("%d 年累计签到的天数: %d\n", year, cumulativeDays)
}
上述代码实现了统计今年累计签到天数的功能,流程如下:
Redis
客户端实例: 使用 redis.NewClient()
方法连接 Redis
至服务器,并获取一个客户端实例。year := now.Year()
获取。dayOfYear := now.YearDay()
获取。ID
为 1。ID
构建一个唯一的 Redis Key
,格式为 user:年份:用户ID
。BitField
的每个操作可以处理的最大长度是 63 位,定义 segmentSize := 63
来批量处理签到数据。一个区间表示 63 天的签到情况。BitField
命令的参数: 通过循环将从年初到当前日期的天数(dayOfYear
)分割为每段最多包含 63 天的多个区间,动态构建 BitField
命令的参数。rdb.BitField()
方法执行构建好的 BitField
命令,返回一个包含位二进制对应的十进制表示的 int64
类型切片。&
操作和位移操作)来检测签到情况,每发现一个 1 就将 cumulativeDays
增加 1。要实现统计某月的签到情况,同样我们也需要用到 Redis
的 BITFIELD
命令。
示例代码:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/redis-bitmap-sign/monthly-sign/main.go
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
func RedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
}
func main() {
rdb := RedisClient()
if rdb == nil {
panic("redis client is nil")
}
now := time.Now()
// 获取当前的年份
year := now.Year()
// 假设用户 ID 为 1
userID := 1
// 获取当前月的天数
days := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Add(-24 * time.Hour).Day()
// 获取本月初是今年的第几天
offset := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).YearDay()
signOfMonth, err := GetSignOfMonth(context.Background(), rdb, userID, year, days, offset)
if err != nil {
log.Fatal(err)
}
fmt.Println(signOfMonth)
}
func GetSignOfMonth(ctx context.Context, rdb *redis.Client, userID, year, days, offset int) ([]bool, error) {
typ := fmt.Sprintf("u%d", days)
key := fmt.Sprintf("user:%d:%d", year, userID)
s, err := rdb.BitField(ctx, key, "GET", typ, offset).Result()
if err != nil {
return nil, fmt.Errorf("failed to get bitfield: %w", err)
}
if len(s) != 0 {
signInBits := s[0]
signInSlice := make([]bool, days)
for i := 0; i < days; i++ {
signInSlice[i] = (signInBits & (1 << (days - 1 - i))) != 0
}
return signInSlice, nil
} else {
return nil, errors.New("no result returned from BITFIELD command")
}
}
上述代码实现了统计当月的签到情况的功能,流程如下:
redis.NewClient()
方法连接至 Redis
服务器,并获取一个客户端实例。year := now.Year()
获取。time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Add(-24 * time.Hour).Day()
计算。time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).YearDay()
获取。ID
:示例中假设用户 ID 为 1。Redis key
和 BitField
命令的参数:ID
构建一个唯一的 Redis Key
,格式为 user:年份:用户ID
。days
构建 type
参数 fmt.Sprintf("u%d", days)
,表示操作的位字段宽度。BitField
命令:通过 rdb.BitField()
方法执行 BitField
命令,返回一个包含位二进制对应的十进制表示的 int64
类型切片。true
表示签到,false
表示未签到。我们可以根据布尔切片的元素在用户端展示当月的签到情况,例如 签到日历。
本文详细介绍了如何利用 Redis Bitmap
类型实现高效的用户签到统计功能。内容包括 Redis Bitmap
数据类型的简单介绍及其应用场景,并通过 Go
语言程序简单实现了 用户签到、查询用户签到状态 和 统计今年累计签到天数 以及 统计当月的签到情况 的功能。
虽然 Redis bitmap
数据类型在统计用户签到情况方面具有显著优势,主要体现在以下两点:
然而,Redis Bitmap
数据类型也有其局限性。例如,使用 Bitmap
存储数据时,只能存储单一状态。如果需要存储额外的具体签到时间或其他相关信息,Bitmap
并不适用。
总的来说,Redis Bitmap
非常适合实现高效的签到统计功能,但在设计系统时需要根据具体需求权衡其优缺点。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。