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

news2024/12/25 9:04:58

文章目录

  • 一、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 main

import (
    "context"
    "fmt"

    "github.com/redis/go-redis/v9"
)

func RedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       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 main

import (
    "context"
    "fmt"

    "github.com/redis/go-redis/v9"
)

func RedisClient() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       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 main

import (
    "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 set
        DB:       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 := 63
    consecutiveDays := 0
    bitOps := make([]any, 0)

    for i := 0; i < dayOfYear; i += segmentSize {
        size := segmentSize
        if 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 := segmentSize
            if (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 为 1
    userID := 1

    consecutiveDays, 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 main

import (
    "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 set
        DB:       0,  // use default DB
    })
}

func main() {
    rdb := RedisClient()
    if rdb == nil {
        panic("redis client is nil")
    }
    now := time.Now()
    // 获取当前的年份
    year := now.Year()
    // 假设用户 ID 为 1
    userID := 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 非常适合实现高效的签到统计功能,但在设计系统时需要根据具体需求权衡其优缺点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2094963.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++11新特性-智能指针

1. 智能指针的概念及原理 1.1 什么是智能指针 智能指针RAII(Resource Acquisition Is Initialization)&#xff0c;是一种利用对象的生命周期来管理资源的技术。如果我们采用传统的new/delete来申请和释放资源&#xff0c;如果忘记调用delete&#xff0c;或者在调用delete之前…

World of Warcraft [CLASSIC][80][Grandel] Call to Arms: Arathi Basin

PVP战场阿拉希盆地15人 /i 开局队伍分配&#xff1a;圣骑士飙车光环 /i [铁匠铺]坦克、治疗3个、输出6个&#xff08;10人组&#xff09; /i [伐木场]坦克、治疗、输出2&#xff08;4个人组&#xff09; /i [农场]留一个守&#xff08;1个人组&#xff09; /i 不要恋战&#x…

如何准确物理定位EMC Unity存储的磁盘位置

上周收到一个客户的咨询&#xff0c;问题是想主动更换一个Unity存储的磁盘&#xff0c;但不知道这个盘具体在存储的什么位置&#xff0c;有没有命令或者方法准确找到这个磁盘的物理位置&#xff1f; 以前也碰到过过类似的问题&#xff0c;但大部分是来自VNX存储。在现场让客户…

ChatGPT-4o:多领域创新应用的智能助手

ChatGPT-4o&#xff1a;多领域创新应用的智能助手 前言1. 数学建模&#xff1a;ChatGPT-4o的精确计算1.1 专业术语简介1.2 代码示例&#xff1a;线性规划问题问题描述代码实现运行结果 2. AI绘画&#xff1a;ChatGPT-4o的视觉创造力2.1 角色设计示例&#xff1a;火焰魔法师角色…

Leangoo领歌敏捷管理:助力敏捷高效协作,轻松实现Scrum敏捷转型

在当今快速变化的商业环境中&#xff0c;企业面临着前所未有的挑战。如何在激烈的竞争中保持领先&#xff1f;如何快速响应市场需求&#xff1f;答案就在于敏捷转型。而在这一过程中&#xff0c;有一个高效的敏捷工具至关重要——Leangoo领歌&#xff08;Leangoo领歌 - 免费一站…

盛京银行营收、利润双降下的负重难行,症结在哪儿?

撰稿|芋圆 来源|贝多财经 盛京银行自2020开年始&#xff0c;经营业绩除了在2022年稍有回暖外&#xff0c;均处于营收、利润双降的局面。 2024年半年报显示&#xff0c;盛京银行的资产总额为10683亿元&#xff0c;规模较2023年末收缩1.1%&#xff1b;营业收入46亿元&#xff0…

【前缀和算法】--- 进阶题目赏析

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 算法Journey 本篇我们来赏析前缀和算法的进阶题目。 &#x1f3e0; 和可被K整除的子数组 &#x1f4cc; 题目解析 和可被k整除的子数组 &#x1f4cc; …

软件单元测试工程模版化

一、简介 在汽车领域混了这么多年也做了不少项目&#xff0c;发现很多公司对软件单元测试和代码覆盖率测试根本不重视&#xff0c;或者开发流程就没有单元测试这个流程。但是有的客户需要评审单元测试这个流程&#xff0c;需要有相关的单元测试报告和代码覆盖率统计的报告。如…

百度 AI Studio 脚本任务篇,它不同于notebook任务是支持免费的, 脚本任务是需要算力卡的,更好的算力 支持四张显卡,

aistudio 脚本任务是需要算力卡的&#xff0c;是收费的一个项目&#xff0c;估计是运行效率更高&#xff0c;支持4张显卡&#xff0c;同时计算。 # -*- coding: utf-8 -*- """ 空白模板 """ ###### 欢迎使用脚本任务&#xff0c;首先让我们熟悉…

计算机毕设选题推荐-基于python的豆瓣电子图书数据可视化分析

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、基于python的豆瓣电子图书数…

插入排序代码实现(java)

简介&#xff1a; 也是一种简单的排序方法&#xff0c;其基本操作是将一条记录插入到已排好的有序表中&#xff0c;从而得到一个新的、记录数量增的有序表 说明&#xff1a; 拿一维数组来说&#xff0c;可以把第一个元素看成一个有序表&#xff0c;后面的元素看成无序表&am…

《中文Python穿云箭量化平台二次开发技术08》获取大盘涨跌家数、平均股价数据等来判断市场涨跌趋势,并在策略中自动控制多空交易

《中文Python穿云箭量化平台》是纯Python开发的量化平台&#xff0c;因此其中很多Python模块&#xff0c;我们可以自己设计新的量化工具&#xff0c;例如自己新的行情软件、新的量化平台、以及各种量化研究工具。 穿云箭自带指标公式源码运行模块&#xff0c;可以为其他量化平台…

莫比乌斯反演总结

目录 前置知识1.1 线性筛 (欧拉筛)1.2 整除分块 (数论分块)引理 1引理 2引理 3实现例 1例 2例 3例 4 1.3 数学知识积性函数莫比乌斯函数狄利克雷(Dirichlet)卷积 莫比乌斯反演2.1 公式2.2 常用~(唯一)~结论2.3 例题例 1例 2例 3例 4例 5练习 1练习 2练习 3练习 4 懵逼乌斯反演总…

配置nginx安全连接ssl(购买域名、获取ssl证书)

以前了解过ssl配置比较麻烦&#xff0c;需要弄挺多东西。 1、购买域名、获取ssl证书 2、安装nginx的ssl模块 3、配置config 1、购买域名、获取ssl证书 可以在腾讯云、阿里云购买域名&#xff0c;然后申请免费的ssl证书&#xff0c;因为免费的证书需要域名才能申请&#xff0…

docker安装配置、docker命令

一、CentOS7安装docker 1、安装 Docker CE 支持 64 位版本 CentOS 7&#xff0c;并且要求内核版本不低于 3.10&#xff0c; CentOS 7 满足最低内核的要求&#xff0c;所以我们在CentOS 7安装Docker。 卸载旧docker 如果之前安装过旧版本的Docker&#xff0c;可以使用下面命令…

Codeforces Round 964 (Div. 4) A-E Java题解

比赛地址 Dashboard - Codeforces Round 964 (Div. 4) - Codeforces A题 签到题 给一个两位数 求各位上的数字和 直接对10取余加上本来的数除以10 // 注意类名必须为 Main, 不要有任何 package xxx 信息 // package Dduo; import java.io.*; import java.math.*; import j…

22:差分线规则

1.那些线是差分对&#xff1a; ①有些特定模块就是差分线&#xff1a;USB&#xff0c;HDMI, 以太网口&#xff0c;LEDS等 设置差分对 Panel打开PCB 输入&#xfe62;和- 点击执行 对90欧姆差分对和100Ω差分对进行分类 设置差分对线宽 ①90ohm 由excel可知&a…

孩子自闭症的主要表现:探寻理解之门

自闭症&#xff0c;也称为孤独症&#xff0c;是一种复杂的神经发展障碍&#xff0c;它影响着孩子的社交互动、沟通能力以及行为模式。当家长注意到孩子出现自闭症倾向时&#xff0c;及时识别并寻求专业帮助至关重要。以下是孩子自闭症的一些主要表现&#xff0c;希望能为家长提…

西安电子科技大学研究生新生大数据

西安电子科技大学研究生新生大数据&#xff0c;来自卓越工程学院—杭州研究院 杭研不少来自双非院校&#xff0c;西电也不怎么歧视双非的

游戏开发设计模式之模板方法模式

目录 模板方法模式在游戏开发中的具体应用案例是什么&#xff1f; 如何在不同类型的游戏&#xff08;如角色扮演游戏、策略游戏等&#xff09;中实现模板方法模式&#xff1f; 模板方法模式与其他设计模式&#xff08;如观察者模式、状态模式等&#xff09;相比&#xff0c;…