当前位置: 首页 > news >正文

145. 利用 Redis Bitmap实践: 用户签到统计

文章目录

  • 一、Redis Bitmap简介
  • 二、Bitmap 的主要应用
  • 三、Go使用Redis实现签到统计
    • 用户签到
    • 查询用户签到状态
    • 统计今年累计签到天数
    • 统计当月的签到情况
  • 总结

在现代应用程序中,用户签到是一个常见的功能。我们通常使用 MySQL 数据库来存储用户的签到记录。然而,随着用户数量的增加,数据库中的记录将会随时间和用户量线性增长,这不仅增加了存储的负担,而且可能影响查询效率。在追求更高存储效率和查询性能的场景下,MySQL 可能不再是最佳选择。

这时,RedisBitmap 数据结构就显得尤为重要。利用 Redis Bitmap,我们不仅可以大幅度降低存储空间的占用,还可以高效实现复杂的用户行为统计,如连续签到天数、月签到统计等。接下来,本文将详细介绍如何利用 Redis Bitmap 实现高效的用户签到统计功能。

一、Redis Bitmap简介

在这里插入图片描述

RedisBitmap,也称为位图,是一种用于存储和处理二进制位(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 值的设计:由于一年只有 365366 天,因此我们只需要 bitmap 里面的前 366 位,即 0-365 位。
在这里插入图片描述

三、Go使用Redis实现签到统计

接下来将会结合 Go 语言和 Redis 中间件实现以下功能:

  • 用户签到
  • 查询用户签到状态
  • 统计今年累计签到天数
  • 统计当月的签到情况

在 Go 程序里安装 Redis 依赖
接下来的功能实现将会使用 Go 语言代码进行演示,因此我们需要先安装 Go Redis 依赖。

go get github.com/redis/go-redis/v9

用户签到

要实现用户签到的功能,我们需要用到 RedisSETBIT 命令。

SETBIT 命令用于设置或清除字符串值中的某个位(bit)值,用法如下所示:

SETBIT key offset value

  • key: 键名。
  • offset: 位偏移量,表示要设置或清除的位(bit)的位置。位的位置从0开始计数。
  • value: 要设置的位值,可以是0 1

示例代码:

package mainimport ("context""fmt""github.com/redis/go-redis/v9"
)func RedisClient() *redis.Client {return redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "", // no password setDB:       0,  // use default DB})
}func main() {rdb := RedisClient()if rdb == nil {panic("redis client is nil")}// 返回值为这个位(`bit`)被设置新值之前的值。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 方法,将 keyuser:2024:1 对应的 bitmap 中第 0 位设为 1。这代表 ID1 的用户在 2024-01-01 进行了签到。SetBit 方法的返回值为该位(bit)被设置新值之前的值。

查询用户签到状态

要实现查询用户签到的状态,我们需要用到 RedisGETBIT 命令。

GETBIT 命令用于获取字符串值中的某个位(bit)的值,用法如下所示:

GETBIT key offset

  • key: 键名。
  • offset: 位偏移量,表示要设置或清除的位(bit)的位置。位的位置从 0 开始计数。

示例代码:

package mainimport ("context""fmt""github.com/redis/go-redis/v9"
)func RedisClient() *redis.Client {return redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "", // no password setDB:       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 方法,获取到 keyuser:2024:1 对应的bitmap中的第0位的值为 1,这代表 ID 1 的用户在 2024-01-01 已经签到过了。

统计今年累计签到天数

要实现统计一年里的签到次数,我们需要用到 RedisBITFIELD 命令。

RedisBITFIELD 命令是一个非常强大的命令,它允许你执行多种位级操作,包括读取、设置、增加位字段。这个命令能够操作存储在字符串中的位数组,并可以看作是直接在字符串上执行复杂的位操作。用法如下所示:

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]

  • type:表示操作的位字段宽度。
  • offset:表示从该偏移量开始

详情请参考:Redis BITFIRLED Command

示例代码:

package mainimport ("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 setDB:       0,  // use default DB})
}// GetConsecutiveDays 计算连续签到天数
func GetConsecutiveDays(ctx context.Context, rdb *redis.Client, userID int, year int, dayOfYear int) (int, error) {key := fmt.Sprintf("user:%d:%d", year, userID)segmentSize := 63consecutiveDays := 0bitOps := make([]any, 0)for i := 0; i < dayOfYear; i += segmentSize {size := segmentSizeif i+segmentSize > dayOfYear {size = dayOfYear - i}// 表示从offset开始,获取指定位字段宽度的值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 := segmentSizeif (idx+1)*segmentSize > dayOfYear {size = dayOfYear % segmentSize}for j := 0; j < size; j++ {if (value & (1 << (size - 1 - j))) != 0 {consecutiveDays++}}}}return consecutiveDays, nil
}func main() {rdb := RedisClient()if rdb == nil {log.Fatal("redis client is nil")}now := time.Now()// 获取当前的年份year := now.Year()// 获取当前日期是今年的第几天dayOfYear := now.YearDay()// 假设用户 ID 为 1userID := 1consecutiveDays, err := GetConsecutiveDays(context.Background(), rdb, userID, year, dayOfYear)if err != nil {log.Fatalf("failed to get consecutive days: %v", err)}fmt.Printf("%d 年累计签到的天数: %d\n", year, consecutiveDays)
}

上述代码实现了统计今年累计签到天数的功能,流程如下:

  • 获取 Redis 客户端实例: 使用 redis.NewClient() 方法连接 Redis 至服务器,并获取一个客户端实例。
  • 获取时间因子:
    • 当前年份: 通过 year := now.Year() 获取。
    • 今天是今年的第几天: 通过dayOfYear := now.YearDay() 获取。
  • 设定用户 ID: 示例中假设用户 ID1
  • 构建 Redis Key:使用年份和用户ID构建一个唯一的 Redis Key,格式为 user:年份:用户ID
  • 定义位操作的区间大小: 由于位域命令BitField的每个操作可以处理的最大长度是 63 位,定义 segmentSize := 63 来批量处理签到数据。一个区间表示 63 天的签到情况。
  • 封装 BitField 命令的参数: 通过循环将从年初到当前日期的天数(dayOfYear)分割为每段最多包含63天的多个区间,动态构建 BitField 命令的参数。
  • 执行BitField命令: 使用rdb.BitField()方法执行构建好的 BitField 命令,返回一个包含位二进制对应的十进制表示的int64类型切片。
  • 统计累计签到天数: 遍历结果数组,针对每个非零的结果使用位运算(& 操作和位移操作)来检测签到情况,每发现一个1就将consecutiveDays增加 1。

统计当月的签到情况

要实现统计某月的签到情况,同样我们也需要用到 RedisBITFIELD 命令。

示例代码:

package mainimport ("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 setDB:       0,  // use default DB})
}func main() {rdb := RedisClient()if rdb == nil {panic("redis client is nil")}now := time.Now()// 获取当前的年份year := now.Year()// 假设用户 ID 为 1userID := 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 客户端实例:使用 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:示例中假设用户 ID1
  • 构建 Redis keyBitField 命令的参数:
  • 使用年份和用户 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 非常适合实现高效的签到统计功能,但在设计系统时需要根据具体需求权衡其优缺点。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【ArcGIS Pro原理第一期】各种空间插值原理:GPI、LPI、IDW等
  • 七月刚入职字节跳动的测试开发面试题,附答案
  • 【全网最全】《2024高教社杯/国赛》 C题 思路+代码+文献 蒙特卡洛+遗传算法 一到三问 农作物的种植策略
  • Linux系统运行模式以及链接文件
  • 高级java每日一道面试题-2024年9月04日-前端篇-前端的框架分类有哪些?
  • Google Research 推出高效的Prompt Tuning方法
  • pointer-events: auto; 是一个 CSS 属性,
  • CSS基础:浮动(float)如何使用清楚以及代替方法
  • 使用CJson编写多个节点嵌套的程序代码
  • 尚品汇-延迟插件实现订单超时取消(四十五)
  • Markdown转换成公众号、知乎、今日头条格式,已开源
  • 已经30岁了,想转行从头开始现实吗?什么样的工作算好工作?
  • List 集合指定值升序降序排列Comparator实现
  • 【学习笔记】5G-A时代物联网应用及策略研究
  • C++设计模式——Template Method模板方法模式
  • 【EOS】Cleos基础
  • docker-consul
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • HTML5新特性总结
  • Java方法详解
  • Laravel5.4 Queues队列学习
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • unity如何实现一个固定宽度的orthagraphic相机
  • webgl (原生)基础入门指南【一】
  • web标准化(下)
  • 诡异!React stopPropagation失灵
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 如何利用MongoDB打造TOP榜小程序
  • 数组大概知多少
  • 详解移动APP与web APP的区别
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • 如何正确理解,内页权重高于首页?
  • 智能情侣枕Pillow Talk,倾听彼此的心跳
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • #define、const、typedef的差别
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (二)JAVA使用POI操作excel
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (接口封装)
  • (一)RocketMQ初步认识
  • (转)Linux下编译安装log4cxx
  • (转)关于多人操作数据的处理策略
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .NET C# 操作Neo4j图数据库
  • .Net Core 微服务之Consul(二)-集群搭建
  • .NET Framework 和 .NET Core 在默认情况下垃圾回收(GC)机制的不同(局部变量部分)
  • .Net 垃圾回收机制原理(二)
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • .NET开源项目介绍及资源推荐:数据持久层
  • @antv/x6 利用interacting方法来设置禁止结点移动的方法实现。