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

2024-09-01 21:52

本文主要是介绍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 非常适合实现高效的签到统计功能,但在设计系统时需要根据具体需求权衡其优缺点。

这篇关于145. 利用 Redis Bitmap实践: 用户签到统计的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128144

相关文章

Android Paging 分页加载库使用实践

《AndroidPaging分页加载库使用实践》AndroidPaging库是Jetpack组件的一部分,它提供了一套完整的解决方案来处理大型数据集的分页加载,本文将深入探讨Paging库... 目录前言一、Paging 库概述二、Paging 3 核心组件1. PagingSource2. Pager3.

最新Spring Security的基于内存用户认证方式

《最新SpringSecurity的基于内存用户认证方式》本文讲解SpringSecurity内存认证配置,适用于开发、测试等场景,通过代码创建用户及权限管理,支持密码加密,虽简单但不持久化,生产环... 目录1. 前言2. 因何选择内存认证?3. 基础配置实战❶ 创建Spring Security配置文件

Redis MCP 安装与配置指南

《RedisMCP安装与配置指南》本文将详细介绍如何安装和配置RedisMCP,包括快速启动、源码安装、Docker安装、以及相关的配置参数和环境变量设置,感兴趣的朋友一起看看吧... 目录一、Redis MCP 简介二、安www.chinasem.cn装 Redis MCP 服务2.1 快速启动(推荐)2.

在Java中使用OpenCV实践

《在Java中使用OpenCV实践》用户分享了在Java项目中集成OpenCV4.10.0的实践经验,涵盖库简介、Windows安装、依赖配置及灰度图测试,强调其在图像处理领域的多功能性,并计划后续探... 目录前言一 、OpenCV1.简介2.下载与安装3.目录说明二、在Java项目中使用三 、测试1.测

MyBatis-Plus 自动赋值实体字段最佳实践指南

《MyBatis-Plus自动赋值实体字段最佳实践指南》MyBatis-Plus通过@TableField注解与填充策略,实现时间戳、用户信息、逻辑删除等字段的自动填充,减少手动赋值,提升开发效率与... 目录1. MyBATis-Plus 自动赋值概述1.1 适用场景1.2 自动填充的原理1.3 填充策略

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类

Olingo分析和实践之OData框架核心组件初始化(关键步骤)

《Olingo分析和实践之OData框架核心组件初始化(关键步骤)》ODataSpringBootService通过初始化OData实例和服务元数据,构建框架核心能力与数据模型结构,实现序列化、URI... 目录概述第一步:OData实例创建1.1 OData.newInstance() 详细分析1.1.1

Olingo分析和实践之ODataImpl详细分析(重要方法详解)

《Olingo分析和实践之ODataImpl详细分析(重要方法详解)》ODataImpl.java是ApacheOlingoOData框架的核心工厂类,负责创建序列化器、反序列化器和处理器等组件,... 目录概述主要职责类结构与继承关系核心功能分析1. 序列化器管理2. 反序列化器管理3. 处理器管理重要方

虚拟机Centos7安装MySQL数据库实践

《虚拟机Centos7安装MySQL数据库实践》用户分享在虚拟机安装MySQL的全过程及常见问题解决方案,包括处理GPG密钥、修改密码策略、配置远程访问权限及防火墙设置,最终通过关闭防火墙和停止Net... 目录安装mysql数据库下载wget命令下载MySQL安装包安装MySQL安装MySQL服务安装完成

SpringBoot整合(ES)ElasticSearch7.8实践

《SpringBoot整合(ES)ElasticSearch7.8实践》本文详细介绍了SpringBoot整合ElasticSearch7.8的教程,涵盖依赖添加、客户端初始化、索引创建与获取、批量插... 目录SpringBoot整合ElasticSearch7.8添加依赖初始化创建SpringBoot项