在上一篇文章中,我们介绍了如何使用 go-redis 操作 Redis 的 SortedSet 有序集合数据类型。如果你还没有阅读,可以点击这里查看。在本篇文章中,我们将重点介绍 Redis 中的 bitmap 数据结构,以及如何使用 go-redis v9 来进行操作。
👉 点击查看 go-redis 使用指南目录
在《go-redis 使用指南》系列文章中,我们将详细介绍如何在 Golang 项目中使用 redis/go-redis 库与 Redis 进行交互。以下是该系列文章的全部内容:
- Golang 操作 Redis:快速上手 - go-redis 使用指南
- Golang 操作 Redis:连接设置与参数详解 - go-redis 使用指南
- Golang 操作 Redis:基础的字符串键值操作 - go-redis 使用指南
- Golang 操作 Redis:如何设置 key 的过期时间 - go-redis 使用指南
- Golang 操作 Redis:Hash 哈希数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:Set 集合数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:为 Hash 中的字段设置过期时间 - go-redis 使用指南
- Golang 操作 Redis:List 列表数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:SortedSet 有序集合数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:bitmap 数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:事务处理操作用法 - go-redis 使用指南
- Golang 操作 Redis:地理空间数据类型操作用法 - go-redis 使用指南
- Golang 操作 Redis:HyperLogLog 操作用法 - go-redis 使用指南
- Golang 操作 Redis:Pipeline 操作用法 - go-redis 使用指南
- Golang 操作 Redis:PubSub发布订阅用法 - go-redis 使用指南
- Golang 操作 Redis:布隆过滤器(Bloom Filter)操作用法 - go-redis 使用指南
- Golang 操作 Redis:Cuckoo Filter操作用法 - go-redis 使用指南
- Golang 操作 Redis:Stream操作用法 - go-redis 使用指南
Redis bitmap 数据结构简介
Redis 的 bitmap 是一种位数组,通常用于高效地处理大量二进制数据。bitmap 可以实现以下常见使用场景:
- 用户活跃状态记录:比如每天用户是否登录、用户签到等。
- 统计唯一用户数量:利用 bitmap 记录用户访问情况。
- 位运算:如布隆过滤器的实现。
go-redis 中 bitmap 操作的方法
以下是 go-redis v9 中 bitmap 操作的方法及其功能描述:
GetBit
- 获取位图中指定偏移量的值SetBit
- 设置位图中指定偏移量的值BitCount
- 计算位图中值为 1 的位数BitOpAnd
- 对一个或多个位图执行 AND 操作,并将结果存储在目标位图中BitOpOr
- 对一个或多个位图执行 OR 操作,并将结果存储在目标位图中BitOpXor
- 对一个或多个位图执行 XOR 操作,并将结果存储在目标位图中BitOpNot
- 对位图执行 NOT 操作,并将结果存储在目标位图中BitPos
- 查找位图中第一个设置为指定值的位的位置BitPosSpan
- 查找指定范围内第一个设置为指定值的位的位置 (Redis 6.0 以上)BitField
- 执行多个位域操作BitFieldRO
- 执行多个只读位域操作
go-redis bitmap 操作方法详细讲解及示例代码
GetBit:获取位图中指定偏移量的值。
方法签名:
GetBit(ctx context.Context, key string, offset int64) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:字符串,位图的键。offset
:整数,位图的偏移量。
返回结果说明:
返回一个整数命令结果,表示偏移量处的位的值(0 或 1)。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一个位,位置 7 的位设置为 1
rdb.SetBit(ctx, "mybitmap", 7, 1)
// 获取位置 7 的位的值
val, err := rdb.GetBit(ctx, "mybitmap", 7).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Value at offset 7:", val) // 输出应为 1
}
SetBit:设置位图中指定偏移量的值。
方法签名:
SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:字符串,位图的键。offset
:整数,位图的偏移量。value
:整数,要设置的值(0 或 1)。
返回结果说明:
返回一个整数命令结果,表示原偏移量处的位的旧值。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一个位,位置 7 的位设置为 1
oldVal, err := rdb.SetBit(ctx, "mybitmap", 7, 1).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Old value at offset 7:", oldVal) // 输出应为 0,因为之前该位置未设置
}
BitCount:计算位图中值为 1 的位数。
方法签名:
BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:字符串,位图的键。bitCount
:指针,包含可选的开始和结束范围。
返回结果说明:
返回一个整数命令结果,表示位图中值为 1 的位数。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位
rdb.SetBit(ctx, "mybitmap", 1, 1)
rdb.SetBit(ctx, "mybitmap", 3, 1)
rdb.SetBit(ctx, "mybitmap", 5, 1)
// 计算位图中值为 1 的位数,指定范围为 0 到 7
bitCount := &redis.BitCount{
Start: 0,
End: 7,
}
count, err := rdb.BitCount(ctx, "mybitmap", bitCount).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Number of set bits:", count) // 输出应为 3
}
BitOpAnd:对一个或多个位图执行按位与(AND)操作,并将结果存储在目标位图中。
方法签名:
BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。destKey
:字符串,目标位图的键。keys
:字符串,可变参数,源位图的键。
返回结果说明:
返回一个整数命令结果,表示目标位图的长度(即其字节数)。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位
rdb.SetBit(ctx, "bitmap1", 1, 1) // bitmap1: 01000000
rdb.SetBit(ctx, "bitmap2", 1, 1)
rdb.SetBit(ctx, "bitmap2", 2, 1) // bitmap2: 01100000
// 对位图执行 AND 操作 resultBitmap: 01000000
byteLength, err := rdb.BitOpAnd(ctx, "resultBitmap", "bitmap1", "bitmap2").Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result bitmap byteLength:", byteLength) // 输出应为 1
for i := 0; i < 8; i++ {
bit, err := rdb.GetBit(ctx, "resultBitmap", int64(i)).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Bit %d: %d\n", i, bit)
}
}
BitOpOr:对一个或多个位图执行按位或(OR)操作,并将结果存储在目标位图中。
方法签名:
BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。destKey
:字符串,目标位图的键。keys
:字符串,可变参数,源位图的键。
返回结果说明:
返回一个整数命令结果,表示目标位图的长度(即其字节数)。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位
rdb.SetBit(ctx, "bitmap1", 1, 1) // 01000000
rdb.SetBit(ctx, "bitmap2", 2, 1) // 00100000
rdb.SetBit(ctx, "bitmap3", 8, 1) // 0000000010000000
// 对位图执行 OR 操作 resultBitmap: 0110000010000000
byteLength, err := rdb.BitOpOr(ctx, "resultBitmap", "bitmap1", "bitmap2", "bitmap3").Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result bitmap byteLength:", byteLength) // 输出应为 2
}
BitOpXor:对一个或多个位图执行按位异或(XOR)操作,并将结果存储在目标位图中。
方法签名:
BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。destKey
:字符串,目标位图的键。keys
:字符串,可变参数,源位图的键。
返回结果说明:
异或操作的规则是:当两个对应的位不同时,结果位为 1,否则结果位为 0。
返回一个整数命令结果,表示目标位图的长度(即其字节数)。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位
rdb.SetBit(ctx, "bitmap1", 1, 1) // bitmap1: 01000000
rdb.SetBit(ctx, "bitmap2", 1, 1)
rdb.SetBit(ctx, "bitmap2", 2, 1) // bitmap2: 01100000
// 对位图执行 XOR 操作 resultBitmap: 00100000
length, err := rdb.BitOpXor(ctx, "resultBitmap", "bitmap1", "bitmap2").Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result bitmap length:", length) // 输出应为 1
}
BitOpNot:对位图执行取反(NOT)操作,并将结果存储在目标位图中。
方法签名:
BitOpNot(ctx context.Context, destKey string, key string) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。destKey
:字符串,目标位图的键。key
:字符串,源位图的键。
返回结果说明:
返回一个整数命令结果,表示目标位图的长度(即其字节数)。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位 bitmap1: 01000000
rdb.SetBit(ctx, "bitmap1", 1, 1)
// 对位图执行 NOT 操作 resultBitmap: 10111111
length, err := rdb.BitOpNot(ctx, "resultBitmap", "bitmap1").Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result bitmap length:", length) // 输出应为 1
}
BitPos:查找位图中第一个设置为指定值的位的位置。
方法签名:
BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:字符串,位图的键。bit
:整数,指定的位值(0 或 1)。pos
:可选参数,表示查找的开始和结束位置。
返回结果说明:
返回一个整数命令结果,表示位图中第一个设置为指定值的位的位置。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位 00010100
rdb.SetBit(ctx, "mybitmap", 3, 1)
rdb.SetBit(ctx, "mybitmap", 5, 1)
// 查找第一个设置为 1 的位的位置
pos, err := rdb.BitPos(ctx, "mybitmap", 1).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Position of first set bit:", pos) // 输出应为 3
}
BitPosSpan:查找指定范围内第一个设置为指定值的位的位置 (Redis 7.0 以上)。
方法签名:
BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:字符串,位图的键。bit
:整数,指定的位值(0 或 1)。start
:整数,范围的开始位置。end
:整数,范围的结束位置。span
:字符串,表示跨度(byte | bit
,redis 7.0.0)。
返回结果说明:
返回一个整数命令结果,表示位图中第一个设置为指定值的位的位置。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 设置一些位 00010100
rdb.SetBit(ctx, "mybitmap", 3, 1)
rdb.SetBit(ctx, "mybitmap", 5, 1)
// 查找范围内第一个设置为 1 的位的位置
pos, err := rdb.BitPosSpan(ctx, "mybitmap", 1, 0, 7, "bit").Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Position of first set bit in range:", pos) // 输出应为 3
}
BitField:执行多个位域操作。
方法签名:
BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd
参数说明:
ctx
:上下文,用于控制请求的生命周期。key
:字符串,位图的键。values
:接口,可变参数,表示多个位域操作。
返回结果说明:
返回一个整数切片命令结果,表示位域操作的结果。
示例代码:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 执行多个位域操作
results, err := rdb.BitField(ctx, "mybitmap",
"SET", "i5", 0, 1,
"INCRBY", "u4", 2, 1,
"GET", "u4", 2).Result()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("BitField results:", results) // 输出应为 [0 3 3]
}
使用 Redis bitmaps 实现用户签到示例代码
以下是一个实现用户签到,以及统计累计连续签到天数的示例(谨慎直接用于生产环境):
package main
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
const businessStartDate = "2024-07-31" // 业务上线日期
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 示例用户ID
userID := "user123"
// 使用一个位图键记录所有签到状态
signinKey := fmt.Sprintf("user:signin:%s", userID)
// 标记某天为签到日期
today := time.Now()
date := today.Format("2006-01-02") // 使用 YYYY-MM-DD 格式
offset := getDayOffset(date)
// 将位图中对应的位设置为 1,表示签到
_, err := rdb.SetBit(ctx, signinKey, offset, 1).Result()
if err != nil {
fmt.Println("Error setting bit:", err)
return
}
fmt.Println("User signed in today.")
// 获取用户连续签到天数
consecutiveDays, err := getConsecutiveSignins(ctx, rdb, userID)
if err != nil {
fmt.Println("Error getting consecutive days:", err)
return
}
fmt.Printf("User has %d consecutive sign-ins.\n", consecutiveDays)
}
// 根据日期获取位图偏移量
func getDayOffset(date string) int64 {
startDate, err := time.Parse("2006-01-02", businessStartDate)
if err != nil {
fmt.Println("Error parsing business start date:", err)
return 0
}
t, err := time.Parse("2006-01-02", date)
if err != nil {
fmt.Println("Error parsing date:", err)
return 0
}
// 计算从业务上线日期到指定日期的天数
return int64(t.Sub(startDate).Hours() / 24)
}
// 获取用户连续签到天数
func getConsecutiveSignins(ctx context.Context, rdb *redis.Client, userID string) (int, error) {
consecutiveDays := 0
signinKey := fmt.Sprintf("user:signin:%s", userID)
bitmapString := rdb.Get(ctx, signinKey).Val()
// 将位图字节转换为二进制字符串
bitStr := ""
for _, b := range bitmapString {
bitStr += fmt.Sprintf("%08b", b)
}
fmt.Println("bitStr:", bitStr)
// 根据今天的offset的长度截取这个二进制字符串
today := time.Now()
date := today.Format("2006-01-02")
offset := getDayOffset(date)
fmt.Println("offset:", offset)
dayLength := offset + 1
if int64(len(bitStr)) >= dayLength {
bitStr = bitStr[:dayLength]
}
fmt.Println("cut bitStr:", bitStr)
// 从后往前计算是否有连续1出现,统计连续1出现的次数就是连续签到天数
for i := len(bitStr) - 1; i >= 0; i-- {
if bitStr[i] == '1' {
consecutiveDays++
} else {
break
}
}
return int(consecutiveDays), nil
}
结语
通过本文,我们详细介绍了 Redis 中 bitmap 数据类型的使用及 go-redis v9 提供的相关操作方法。希望这些内容对你在使用 Golang 操作 Redis 时有所帮助,点击 go-redis 使用指南 可查看更多相关教程。