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

相关文章

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

hdu1496(用hash思想统计数目)

作为一个刚学hash的孩子,感觉这道题目很不错,灵活的运用的数组的下标。 解题步骤:如果用常规方法解,那么时间复杂度为O(n^4),肯定会超时,然后参考了网上的解题方法,将等式分成两个部分,a*x1^2+b*x2^2和c*x3^2+d*x4^2, 各自作为数组的下标,如果两部分相加为0,则满足等式; 代码如下: #include<iostream>#include<algorithm

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

hdu4267区间统计

题意:给一些数,有两种操作,一种是在[a,b] 区间内,对(i - a)% k == 0 的加value,另一种操作是询问某个位置的值。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import

hdu4417区间统计

给你一个数列{An},然后有m次查询,每次查询一段区间 [l,r] <= h 的值的个数。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamRead

hdu3333区间统计

题目大意:求一个区间内不重复数字的和,例如1 1 1 3,区间[1,4]的和为4。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;

实例:如何统计当前主机的连接状态和连接数

统计当前主机的连接状态和连接数 在 Linux 中,可使用 ss 命令来查看主机的网络连接状态。以下是统计当前主机连接状态和连接主机数量的具体操作。 1. 统计当前主机的连接状态 使用 ss 命令结合 grep、cut、sort 和 uniq 命令来统计当前主机的 TCP 连接状态。 ss -nta | grep -v '^State' | cut -d " " -f 1 | sort |

【Kubernetes】K8s 的安全框架和用户认证

K8s 的安全框架和用户认证 1.Kubernetes 的安全框架1.1 认证:Authentication1.2 鉴权:Authorization1.3 准入控制:Admission Control 2.Kubernetes 的用户认证2.1 Kubernetes 的用户认证方式2.2 配置 Kubernetes 集群使用密码认证 Kubernetes 作为一个分布式的虚拟