在上一篇文章中,我们介绍了如何使用 go-redis 操作 Redis 的 SortedSet 有序集合数据类型。如果你还没有阅读,可以点击这里查看。在本篇文章中,我们将重点介绍 Redis 中的 bitmap 数据结构,以及如何使用 go-redis v9 来进行操作。

👉 点击查看《go-redis使用指南》系列文章目录

在《go-redis使用指南》系列文章中,我们将详细介绍如何在 Golang 项目中使用 redis/go-redis 库与 Redis 进行交互。以下是该系列文章的全部内容:

  1. Golang 操作 Redis:快速上手 - go-redis 使用指南
  2. Golang 操作 Redis:连接设置与参数详解 - go-redis 使用指南
  3. Golang 操作 Redis:基础的字符串键值操作 - go-redis 使用指南
  4. Golang 操作 Redis:如何设置 key 的过期时间 - go-redis 使用指南
  5. Golang 操作 Redis:Hash 哈希数据类型操作用法 - go-redis 使用指南
  6. Golang 操作 Redis:Set 集合数据类型操作用法 - go-redis 使用指南
  7. Golang 操作 Redis:为 Hash 中的字段设置过期时间 - go-redis 使用指南
  8. Golang 操作 Redis:List 列表数据类型操作用法 - go-redis 使用指南
  9. Golang 操作 Redis:SortedSet 有序集合数据类型操作用法 - go-redis 使用指南
  10. Golang 操作 Redis:bitmap 数据类型操作用法 - go-redis 使用指南
  11. Golang 操作 Redis:事务处理操作用法 - go-redis 使用指南
  12. Golang 操作 Redis:地理空间数据类型操作用法 - go-redis 使用指南
  13. Golang 操作 Redis:HyperLogLog 操作用法 - go-redis 使用指南
  14. Golang 操作 Redis:Pipeline 操作用法 - go-redis 使用指南
  15. Golang 操作 Redis:PubSub发布订阅用法 - go-redis 使用指南
  16. Golang 操作 Redis:布隆过滤器(Bloom Filter)操作用法 - go-redis 使用指南
  17. Golang 操作 Redis:Cuckoo Filter操作用法 - go-redis 使用指南
  18. Golang 操作 Redis:Stream操作用法 - go-redis 使用指南
golang redis go-redis

Redis bitmap 数据结构简介

Redis 的 bitmap 是一种位数组,通常用于高效地处理大量二进制数据。bitmap 可以实现以下常见使用场景:

  1. 用户活跃状态记录:比如每天用户是否登录、用户签到等。
  2. 统计唯一用户数量:利用 bitmap 记录用户访问情况。
  3. 位运算:如布隆过滤器的实现。

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 使用指南 可查看更多相关教程。


也可以看看