Redis 位图 (BitMap)
概述
Redis 位图是一种基于位操作的数据结构,主要用于处理大量的布尔值(0 或 1)或二进制标志。它的基本单元是一个位(bit),这些位可以用来表示各种状态信息。位图在存储和处理这些数据时非常高效,因为它们只使用了每个布尔值占用的单个位,而不是占用完整的字节或整数。
基本操作
1. 设置位的值 (SETBIT)
用于设置 key 对应的位图中指定偏移量(offset)处的位的值。
SETBIT mybitmap 7 1
- mybitmap 是位图的键名。
- 7 是偏移量(从 0 开始)。
- 1 是要设置的值(可以是 0 或 1)。
2. 获取位的值 (GETBIT)
用于获取 key 对应位图中指定偏移量处的位的值。
GETBIT mybitmap 7
- mybitmap 是位图的键名。
- 7 是偏移量。
- 返回值为 0 或 1。
3. 计算位图中值为 1 的位的数量 (BITCOUNT)
计算位图中所有值为 1 的位的数量,可以指定范围。
BITCOUNT mybitmap
- mybitmap 是位图的键名。
- 可以通过 BITCOUNT mybitmap 0 100 来统计从第 0 位到第 100 位的 1 的数量。
4. 按位操作 (BITOP)
对多个位图进行按位操作,并将结果存储在 destkey 中。
BITOP OR result bitmap1 bitmap2
- OR 是操作类型,可以是 AND、OR、XOR 或 NOT。
- result 是存储操作结果的键名。
- bitmap1 和 bitmap2 是操作的两个位图。
5. 查找位的第一个匹配位置 (BITPOS)
查找位图中第一个出现指定值的位置,可以指定范围。
BITPOS mybitmap 1
- mybitmap 是位图的键名。
- 1 是要查找的值(0 或 1)。
位图的应用场景
1. 用户签到记录
位图可以高效地记录用户的签到状态。例如,用用户 ID 作为偏移量,每次签到时就设置对应的位为 1。这样可以快速检查某个用户是否签到过,也可以统计签到人数。
2. 活跃用户统计
可以使用位图记录用户的活跃状态。例如,每天的活跃用户使用位图记录,通过计算位图中值为 1 的位数来统计活跃用户数量。
3. 权限标志
位图可以用来存储用户的权限标志。每一位代表一个特定的权限,用户拥有某个权限时,相应的位为 1。这样可以在短时间内高效地检查和操作用户的权限。
4. 广告投放
位图可以记录用户对广告的点击情况。每个用户对应一个位,通过设置位的值来标记用户是否点击了广告
示例: (每天签到)
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"log"
)
var ctx = context.Background()
// 创建 Redis 客户端
// 使用默认数据库 4 进行连接
func createClient() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 地址
DB: 4, // 使用的数据库索引(这里是 4)
})
return rdb
}
// 用户签到
// 使用位图(BitMap)来记录用户的签到状态
func signIn(rdb *redis.Client, userID int, date string) error {
// 生成 Redis 键,格式为 "sign_in:<date>"
key := fmt.Sprintf("sign_in:%s", date)
// 将用户 ID 对应的位设为 1,表示签到
_, err := rdb.SetBit(ctx, key, int64(userID), 1).Result()
return err
}
// 检查用户是否已签到
// 查询位图中的用户签到状态
func hasSignedIn(rdb *redis.Client, userID int, date string) (bool, error) {
// 生成 Redis 键,格式为 "sign_in:<date>"
key := fmt.Sprintf("sign_in:%s", date)
// 获取用户 ID 对应的位的值
bit, err := rdb.GetBit(ctx, key, int64(userID)).Result()
return bit == 1, err
}
// 统计签到人数
// 计算在特定日期签到的总人数
func countSignIns(rdb *redis.Client, date string) (int64, error) {
// 生成 Redis 键,格式为 "sign_in:<date>"
key := fmt.Sprintf("sign_in:%s", date)
// 使用 BitCount 命令统计位图中值为 1 的位的数量
count, err := rdb.BitCount(ctx, key, &redis.BitCount{
Start: 0, // 位图的起始位置
End: -1, // 位图的结束位置(-1 表示到最后一位)
}).Result()
return count, err
}
func main() {
// 创建 Redis 客户端
rdb := createClient()
// 示例用法
userID := 123
date := "2024-09-14"
// 用户签到
err := signIn(rdb, userID, date)
if err != nil {
log.Fatalf("Failed to sign in: %v", err)
}
// 检查签到状态
signedIn, err := hasSignedIn(rdb, userID, date)
if err != nil {
log.Fatalf("Failed to check sign-in status: %v", err)
}
fmt.Printf("User %d signed in: %v\n", userID, signedIn)
// 统计签到人数
count, err := countSignIns(rdb, date)
if err != nil {
log.Fatalf("Failed to count sign-ins: %v", err)
}
fmt.Printf("Total sign-ins on %s: %d\n", date, count)
}
示例2:(一周的签到)
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"log"
"strconv"
"time"
)
// 创建一个背景上下文对象,用于 Redis 操作
var ctx = context.Background()
// createClient 创建并返回一个 Redis 客户端
func createClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 服务器地址
DB: 0, // 默认数据库索引
})
}
// signIn 记录用户在指定日期的签到状态
func signIn(rdb *redis.Client, userID int, date string) error {
// 生成 Redis 键,格式为 "weekly_sign_in:日期"
key := fmt.Sprintf("weekly_sign_in:%s", date)
// 使用 SetBit 方法设置用户签到状态为 1
_, err := rdb.SetBit(ctx, key, int64(userID), 1).Result()
return err
}
// hasSignedIn 检查用户在指定日期是否签到
func hasSignedIn(rdb *redis.Client, userID int, date string) (bool, error) {
// 生成 Redis 键,格式为 "weekly_sign_in:日期"
key := fmt.Sprintf("weekly_sign_in:%s", date)
// 使用 GetBit 方法获取用户的签到状态
bit, err := rdb.GetBit(ctx, key, int64(userID)).Result()
// 如果 bit 为 1,表示用户已签到
return bit == 1, err
}
// countSignIns 统计指定日期的总签到人数
func countSignIns(rdb *redis.Client, date string) (int64, error) {
// 生成 Redis 键,格式为 "weekly_sign_in:日期"
key := fmt.Sprintf("weekly_sign_in:%s", date)
// 使用 BitCount 方法统计所有设置为 1 的位数
count, err := rdb.BitCount(ctx, key, &redis.BitCount{
Start: 0,
End: -1,
}).Result()
return count, err
}
// getWeekDates 获取指定日期所在周的所有日期
func getWeekDates(date string) ([]string, error) {
layout := "2006-01-02" // 日期格式
// 解析输入的日期字符串
d, err := time.Parse(layout, date)
if err != nil {
return nil, err
}
// 获取该日期的年份和周数
year, week := d.ISOWeek()
// 计算该周的第一天(周一)
startOfWeek := time.Date(year, time.January, 1, 0, 0, time.UTC, time.UTC, time.Weekday(time.Monday))
startOfWeek = startOfWeek.AddDate(0, 0, (week-1)*7)
// 创建存储一周日期的切片
days := make([]string, 7)
for i := 0; i < 7; i++ {
days[i] = startOfWeek.AddDate(0, 0, i).Format(layout)
}
return days, nil
}
func main() {
// 创建 Redis 客户端
rdb := createClient()
userID := 123 // 示例用户 ID
date := "2024-09-14" // 假设这个日期是我们要检查的日期
// 签到操作
if err := signIn(rdb, userID, date); err != nil {
log.Fatalf("Failed to sign in: %v", err) // 如果签到失败,输出错误信息并退出
}
// 检查用户是否已签到
signedIn, err := hasSignedIn(rdb, userID, date)
if err != nil {
log.Fatalf("Failed to check sign-in status: %v", err) // 如果检查签到状态失败,输出错误信息并退出
}
fmt.Printf("User %d signed in on %s: %v\n", userID, date, signedIn)
// 统计指定日期的总签到人数
count, err := countSignIns(rdb, date)
if err != nil {
log.Fatalf("Failed to count sign-ins: %v", err) // 如果统计签到人数失败,输出错误信息并退出
}
fmt.Printf("Total sign-ins on %s: %d\n", date, count)
// 获取本周的所有日期
weekDates, err := getWeekDates(date)
if err != nil {
log.Fatalf("Failed to get week dates: %v", err) // 如果获取本周日期失败,输出错误信息并退出
}
fmt.Println("Week Dates:", weekDates)
// 统计本周每一天的签到情况
for _, d := range weekDates {
count, err := countSignIns(rdb, d)
if err != nil {
log.Printf("Failed to count sign-ins on %s: %v", d, err) // 如果统计失败,输出错误信息
continue
}
fmt.Printf("Total sign-ins on %s: %d\n", d, count)
}
}