✅技术社区—使用Redis BitMap实现签到与查询历史签到以及签到统计功能

一、前言

签到是一个很常见的功能,如果使用数据库实现,那么用户一次签到,就是一条记录,假如有100万用户,平均每个用户每年签到次数为30次,则这张表一年的数据量为 3000 万条,一般签到记录字段不会太多一条数据按照30字节算,一年就是858.3MB左右,但是对于签到信息查询是比较频繁的,如查询当天是否签到、查询用户近7天签到记录、查询用户近30天签到记录、统计用户签到次数,如果这些查询都要去签到表查询那么数据库压力是非常大的,而且考虑到数据量会不断增长,这里使用Redis BitMap 实现高效的签到与统计。

二、Redis BitMap 位图原理

BitMap 在 Redis 中并不是一个新的数据类型,其底层是 Redis 实现,Redis 的位图(BitMap)是由多个二进制位组成的数组,只有两种状态,0和1, 数组中的每个二进制位都有与之对应的偏移量(从 0 开始),通过这些偏移量可以对位图中指定的一个或多个二进制位进行操作,由于采用一个bit 来存储一个数据,因此可以大大的节省空间。

  • 偏移量定义:偏移量是指位图中每个二进制位的位置编号,从0开始计数。每一个bit都有一个与之对应的偏移量,这个偏移量就是该bit在位图中的位置索引。
  • 偏移量作用:通过偏移量,我们可以直接访问、修改或查询位图中的任何一个或多个二进制位的状态(即将其设置为0或1)。这种直接通过偏移量操作单个bit的能力,使得位图非常适合于高效地处理大量的布尔值类型的数据,如在线状态、特性开关、用户签到记录等。

2.1、BitMap 能解决什么
  • BitMap 能解决很多问题,核心就是使用位数组节省存储空间,常见业务有用户签到、打卡、统计活跃用户、统计用户在线状态、实现布隆过滤器、数据去重、快速查找等。

  • BitMap是如何使用位数组节省存储空间的

在20亿个随机整数中找出某个数m是否存在其中,并假设32位操作系统,4G内存。

计算机分配给内存的最小单元是bit,在Java中,int占4字节,1字节=8位(1 byte = 8 bit)。

如果每个数字用int存储,那就是20亿个int,因而占用的空间约为 (2000000000*4/1024/1024/1024)≈7.45G

如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为 (2000000000/8/1024/1024/1024)≈0.23G

2.2、BitMap 存储空间计算
  • 在 Redis 中是使用字符串类型存储的,Redis 中字符串的最大长度是 512M,所以 BitMap 的 offset (偏移量)最大值为:512 * 1024 * 1024 * 8 = 2^32,也就是说一个BitMap只能存储2^32个位,差不多4.29亿。
  • 还注意一个问题,如果我们只在一个 BitMap 偏移量为99的位置存放了一个数据,那么这个 BitMap 也是会占用100个位的内存的,0-98这些位都会被隐式地初始化为 0。
2.3、BitMap 存在问题
  • 数据碰撞。比如将字符串映射到 BitMap 的时候会有碰撞的问题,那就可以考虑用 Bloom Filter 来解决,Bloom Filter 使用多个 Hash 函数来减少冲突的概率。
  • 数据稀疏。又比如要存入(10,100000,10000000)这三个数据,我们需要建立一个 9999999 长度的 BitMap ,但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入 Roaring BitMap 来解决。

三、Redis BitMap 操作基本语法和原生实现签到

3.1、基本语法
 
# 设置指定偏移量上的位的值(0 或 1),语法:SETBIT key offset value
## 示例:给mykey 偏移量为9的位置设置值为1
SETBIT mykey 9 1

# 获取指定偏移量上的位的值,语法:GETBIT key offset
## 示例:获取mykey 偏移量为9上的值
GETBIT mykey 9

# 统计指定范围内所有位为1的数量 如果不指定范围则统计整个key,这个范围是以字节为单位的比如start设置成1其实代表8bit,对应偏移量是8开始,语法:BITCOUNT key [start end]
## 示例:获取mykey 所有所有位为1的数量
BITCOUNT mykey

# 在指定范围内查找第一个被设置为 1 或 0 的位,语法:BITPOS key bit [start] [end]
## 示例:查找mykey中第一个被设置为 1 的位置
BITPOS mykey 1

# 对位图的指定偏移量进行位级别的读写操作:语法:BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]
## GET type offset 用于获取指定偏移量上的位,type 可以是 u<n>(无符号整数)或 i<n>(有符号整数),offset 是位图的偏移量。
## SET type offset value 用于设置指定偏移量上的位,type 是位的类型,offset 是位图的偏移量,value 是要设置的值。
## INCRBY type offset increment 用于递增或递减指定偏移量上的位,type 是位的类型,offset 是位图的偏移量,increment 是递增或递减的值。
## 示例:获取mykey 偏移量从 0 开始的4位无符号整数(u4 表示 4 位的无符号整数)
BITFIELD mykey GET u4 0

# 对一个或多个位图执行指定的位运算操作(AND、OR、XOR、NOT),语法:BITOP operation destkey key [key ...]
## 示例:将key1和key1进行AND运算(对应位都为 1 时结果位为 1,否则为 0),将运算后的结果保存到新的key:destkey 
BITOP AND destkey key1 key2
3.2、Redis BitMap 实现签到操作指令

这里模拟一个月签到,日期上选择2023年11月的1 3 5号这三天签到,因为偏移量是从0开始,所以对应偏移量就是0、2、4,其余日期不签到。

  • 添加用户签到位 key = USER_SIGN_IN:U0001:202311,其中U0001代表用户编号,202311代表对应年和月
127.0.0.1:6379> SETBIT USER_SIGN_IN:U0001:202311 0 1
(integer) 0
127.0.0.1:6379> SETBIT USER_SIGN_IN:U0001:202311 2 1
(integer) 0
127.0.0.1:6379> SETBIT USER_SIGN_IN:U0001:202311 4 1
(integer) 0
  • 查看用户指定日期是否有签到(查看当天是否有签到同理),这里查看5号是否有签到偏移量为4,返回1则代表有签到
127.0.0.1:6379> GETBIT USER_SIGN_IN:U0001:202311 4
(integer) 1

3、查看用户2023年11月一共签到了几天

127.0.0.1:6379> BITCOUNT USER_SIGN_IN:U0001:202311
(integer) 3

4、查看用户2023年11月那些日期签到了,11月一共有30天

  • 通过BITFIELD获取30位的无符号十进制整数,从偏移量0开始
127.0.0.1:6379> BITFIELD USER_SIGN_IN:U0001:202311 GET u30 0
1) (integer) 704643072
  • 将获取到的无符号十进制整数转换成二进制,这里可以看到从左到右二进制的第1 3 5位置值都是1,对应偏移量0 2 4,这里不是从右到左的,然后通过业务代码判断判断这个二进制对应位为1则代表有签到,具体代码会在下面做实现。
# 十进制
704643072
# 二进制
101010000000000000000000000000

四、SpringBoot 使用 Redis BitMap 实现签到与统计功能

这里会使用SpringBoot环境RedisTemplate来操作Redis,需要集成文章可以查看,SpringBoot集成Lettuce客户端操作Redis:SpringBoot集成Lettuce客户端操作Redis-CSDN博客

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.17</version>
</dependency>
4.1、代码实现

代码里注释比较完整这里就不做额外介绍了

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.YearMonth;
import java.util.*;

/**
 * 签到业务
 */
@Service
public class SignInService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static String yyyy_MM_dd = "yyyy-MM-dd";
    private static String yyyy_MM = "yyyy-MM";

    /**
     * 用户签到
     * @param userNo 用户编号
     * @param date   日期 格式yyyy-MM-dd
     */
    public boolean signIn(String userNo, String date) {
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 获取日期
        DateTime dateTime = DateUtil.parse(date, yyyy_MM_dd);
        int day = dateTime.dayOfMonth();
        // 设置给BitMap对应位标记 其中offset为0表示第一天所以要day-1
        Boolean result = redisTemplate.opsForValue().setBit(cacheKey, day - 1, true);
        // 如果响应true则代表之前已经签到,在Redis指令操作setbit 设置对应位为1的时候,如果之前是0或者不存在会响应0,如果为1则响应1
        if (result) {
            System.out.println("用户userNo=" + userNo + " date=" + date + "  已签到");
        }
        return result;
    }

    /**
     * 查看用户指定日期是否签到(查看当天是否有签到同理)
     * @param userNo
     * @param date   日期 格式yyyy-MM-dd
     */
    public boolean isSignIn(String userNo, String date) {
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 获取日期
        DateTime dateTime = DateUtil.parse(date, yyyy_MM_dd);
        int day = dateTime.dayOfMonth();
        return redisTemplate.opsForValue().getBit(cacheKey, day - 1);
    }

    /**
     * 统计用户指定年月签到次数
     * @param userNo
     * @param date   格式yyyy-MM
     */
    public Long getSignInCount(String userNo, String date) {
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 不知道是那个版本才有的下面这个方法,我的现在使用的spring-data-redis是2.3.9.RELEASE 是没有这个方法的,改用connection直接调用bitCount
//        Long count = redisTemplate.opsForValue().bitCount(key, start, end);
        Long count = redisTemplate.execute(connection -> connection.bitCount(cacheKey.getBytes()), true);
        return count;
    }

    /**
     * 获取用户指定年月签到列表,也可以通过这种方式获取用户月签到次数
     *
     * @param userNo
     * @param date   格式yyyy-MM
     */
    public List<Map> getSignInList(String userNo, String date) {
        // 获取缓存key
        String cacheKey = getCacheKey(userNo, date);
        // 获取传入月份有多少天
        DateTime dateTime = DateUtil.parse(date, yyyy_MM);
        YearMonth yearMonth = YearMonth.of(dateTime.year(), dateTime.monthBaseOne());
        int days = yearMonth.lengthOfMonth();
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(days)).valueAt(0);
        // 获取位图的无符号十进制整数
        List<Long> list = redisTemplate.opsForValue().bitField(cacheKey, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return null;
        }
        // 获取位图的无符号十进制整数值
        long bitMapNum = list.get(0);
        // 进行位运算判断组装那些日期有签到
        List<Map> result = new ArrayList<>();
        for (int i = days; i > 0; i--) {
            Map<String, Object> map = new HashMap<>();
            map.put("day", i);
            //先 右移,然后在 左移,如果得到的结果仍然与本身相等,则 最低位是0 所以是未签到
            if (bitMapNum >> 1 << 1 == bitMapNum) {
                map.put("active", false);
            } else {
                //与本身不等,则最低位是1 表示已签到
                map.put("active", true);
            }
            result.add(map);
            // 将位图的无符号十进制整数右移一位,准备下一轮判断
            bitMapNum >>= 1;
        }
        Collections.reverse(result);
        return result;
    }


    /**
     * 获取缓存key
     */
    private static String getCacheKey(String userNo, String date) {
        DateTime dateTime = DateUtil.parse(date, yyyy_MM);
        return String.format("USER_SIGN_IN:%s:%s", userNo, dateTime.year() + "" + dateTime.monthBaseOne());
    }
}
4.2、功能测试
import com.redisscene.service.SignInService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
import java.util.Map;

/**
 * 签到功能测试
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class SignInTest {

    @Autowired
    private SignInService signInService;

    /**
     * 测试用户签到
     */
    @Test
    public void t1() {
        boolean b1 = signInService.signIn("U0001", "2023-11-01");
        boolean b2 = signInService.signIn("U0001", "2023-11-03");
        boolean b3 = signInService.signIn("U0001", "2023-11-05");
        boolean b4 = signInService.signIn("U0001", "2023-11-01");
        System.out.println("b1=" + b1 + " b2=" + b2 + " b3=" + b3 + " b4=" + b4);
    }

    /**
     * 测试查看用户指定日期是否签到(查看当天是否有签到同理)
     */
    @Test
    public void t2() {
        boolean b1 = signInService.isSignIn("U0001", "2023-11-01");
        System.out.println(b1 ? "b1已签到" : "b1未签到");
        boolean b2 = signInService.isSignIn("U0001", "2023-11-06");
        System.out.println(b2 ? "b2已签到" : "b2未签到");
    }

    /**
     * 测试统计用户指定年月签到次数
     */
    @Test
    public void t3() {
        Long count = signInService.getSignInCount("U0001", "2023-11");
        System.out.println("签到次数count=" + count);
    }

    /**
     * 测试获取用户指定年月签到列表,也可以通过这种方式获取用户月签到次数
     */
    @Test
    public void t4() {
        List<Map> list = signInService.getSignInList("U0001", "2023-11");
        if (list != null && !list.isEmpty()) {
            list.forEach(item -> System.out.println(item));
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

揭秘项目管理多重益处:加速产品上市、优化资源利用,提升质量与满意度等秘诀

一、引言 在飞速发展的商业环境中&#xff0c;项目管理已经成为企业成功的关键要素之一。正如项目管理大师哈罗德科兹纳所言&#xff1a;“项目管理不仅仅是关于完成任务的工具&#xff0c;更是一种战略武器&#xff0c;能够帮助企业获得竞争优势。”项目管理的好处多种多样&am…

Apache FtpServer在Windows上下载安装与使用

Apache FtpServer在Windows上下载安装与使用 1、Apache Ftp Server下载 进入apache官网 https://mina.apache.org/ftpserver-project/old-downloads.html 下载自己使用的版本。 Apache FtpServer 1.1.1及以下的版本需要JDK1.7的支持 Apache FtpServer 1.1.1以上的版本需要JDK…

Linux--任务管理与守护进程

目录 任务管理 进程组概念 作业概念 会话概念 补充 守护进程 基本概念 守护进程的查看 守护进程的创建 自己手写守护进程 使用系统调用函数创建守护进程 任务管理 进程组概念 每一个进程除了有一个进程ID之外&#xff0c;还有一个进程组ID&#xff0c;进程组是一个或…

新零售SaaS架构:线上商城系统架构设计

零售商家为什么要建设线上商城&#xff1f; 传统的实体门店服务范围有限&#xff0c;只能吸引周边500米以内的消费者。因此&#xff0c;如何拓展服务范围&#xff0c;吸引更多的消费者到店&#xff0c;成为了店家迫切需要解决的问题。 缺乏忠实顾客&#xff0c;客户基础不稳&a…

clickhouse突然启动不起来问题排查

场景&#xff1a; 在实现postgreql数据迁移到clickhouse中&#xff0c;想使用MaterializedPostgreSQL的功能实现&#xff0c;但是中途clickhouse突然挂了&#xff0c;就再启动不了了。 现象&#xff1a; systemctl start clcikhouse-server启动报错 [rootlocalhost clickhous…

python矢量算法-三角形变化寻找对应点

1.算法需求描述 现有随机生成的两个三角形A与B&#xff0c;在三角形A中存在Pa&#xff0c;使用算法计算出三角形B中对应的点Pb 2.python代码 import numpy as np # 计算三角形A的面积 def area_triangle(vertices): return 0.5 * np.abs(np.dot(vertices[0] - vertices[…

用python爬取CSDN博客的总字数

一、下载pycahrm 此处推荐博客&#xff1a;PyCharm安装教程&#xff0c;图文教程&#xff08;超详细&#xff09;-CSDN博客 二、安装相应的库 pycharm安装库的步骤&#xff1a; 1、打开pycharm&#xff1b; 2、在菜单栏中&#xff0c;选择 "file">"setti…

d3dcompiler43.dll缺失怎么修复,教你五个方法快速搞定

在数字世界的深渊中&#xff0c;有一个名为d3dcompiler_43.dll的神秘文件&#xff0c;它就像一把打开现代科技之门的钥匙。这个文件是DirectX 12的一部分&#xff0c;由微软公司开发&#xff0c;用于编译和处理图形数据。 d3dcompiler_43.dll是一个动态链接库&#xff08;DLL&…

代码随想录算法训练营第三十一天 | 455. 分发饼干、376. 摆动序列、53. 最大子数组和

代码随想录算法训练营第三十一天 | 455. 分发饼干、376. 摆动序列、53. 最大子数组和 455. 分发饼干题目解法 376. 摆动序列题目解法 53. 最大子数组和题目解法 感悟 455. 分发饼干 题目 解法 class Solution { public:int findContentChildren(vector<int>& g, vec…

AcWing 1250. 格子游戏 (并查集,坐标变换)

记录此题的目的&#xff1a; 明确二维的坐标可以映射到一维&#xff1a;在x和y都是从0开始的前提下&#xff0c;假如图形列数为n&#xff0c;(x,y)映射到一维可以写成x * n y。并查集并不好存储二维数据&#xff0c;如果遇到二维数据可以将其映射到一维。 Alice和Bob玩了一个…

Amazon Bedrock 实践 | 动手玩转 Claude 3

生成式 AI 和大模型在 2024 年已经进入落地实践阶段。因此&#xff0c;围绕开发者在生成式应用程序开发中的主要痛点和需求&#xff0c;我们组织了这个 “Amazon Bedrock 实践” 的系列&#xff0c;希望可以帮助开发者高效地上手生成式 AI 和大模型的应用开发&#xff0c;本篇为…

Spring:面试八股

文章目录 参考Spring模块CoreContainerAOP 参考 JavaGuide Spring模块 CoreContainer Spring框架的核心模块&#xff0c;主要提供IoC依赖注入功能的支持。内含四个子模块&#xff1a; Core&#xff1a;基本的核心工具类。Beans&#xff1a;提供对bean的创建、配置、管理功能…

第十三届蓝桥杯省赛真题 Java B 组【原卷】

文章目录 发现宝藏【考生须知】试题 A: 星期计算试题 B: 山试题 C: 字符统计试题 D: 最少刷题数试题 E \mathrm{E} E : 求阶乘试题 F : \mathrm{F}: F: 最大子矩阵试题 G: 数组切分试题 H: 回忆迷宫试题 I: 红绿灯试题 J 拉箱子 发现宝藏 前些天发现了一个巨牛的人工智能学习…

C++ 侯捷 程序设计(Ⅱ)兼谈对象模型 笔记

Conversion function 转换函数 侯捷老师使用分数 Fraction举例&#xff0c;分数理应可以被看作是小数 提供了Fraction类对象一个转换为double的方法&#xff0c;当碰到需要转换为double的情况下&#xff0c;会调用该方法。 黄色的就是转换函数&#xff0c;没有return type&am…

【免费】基于扩展(EKF)和无迹卡尔曼滤波(UKF)的电力系统动态状态估计

目录 1 主要内容 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序对应文章《Power System Dynamic State Estimation Using Extended and Unscented Kalman Filters》&#xff0c;电力系统状态的准确估计对于提高电力系统的可靠性、弹性、安全性和稳定性具有重要意义&a…

RIPGeo代码理解(五)utils.py( 辅助函数)第一部分

​ 代码链接:RIPGeo代码实现 ├── lib # 包含模型(model)实现文件 │ |── layers.py # 注意力机制的代码。 │ |── model.py # TrustGeo的核心源代码。 │ |── sublayers.py # layer.py的支持文件。 │ |── utils.p…

详解Python的函数嵌套

Python语言允许在定义函数的时候&#xff0c;其函数体内又包含另外一个函数的完整定义&#xff0c;这就是我们通常所说的嵌套定义。 实例1&#xff1a; def OutFun(): #定义函数OutFun()&#xff0c;m3 #定义变量m3;def InFun(): #在OutFun内定义函…

python学生作业管理系统flask-django-nodejs-php

课题主要分为三大模块&#xff1a;即管理员模块和学生、教师模块&#xff0c;主要功能包括&#xff1a;学生、教师、作业信息、学习模块、教学评价、学习情况等&#xff1b; 关键词&#xff1a;学生作业管理系统&#xff1b;作业信息 目录 摘 要 I Abstrac II 目录 III 1绪论 1…

jmeter接口导入方式

curl直接导入 1、操作页面后&#xff0c;F12查看接口&#xff0c;右击接口-copy-copy as cURL 2、jmeter 工具-import from cURL&#xff0c;粘贴上面复制的curl 根据接口文档导入 1、接口文档示例如下&#xff1a; Path&#xff1a; /api/jobs/xps/exec Method&#xf…

图像几何变换(仿射变换和透视变换...)及python-opencv实现

文章目录 图像变换类型仿射变换透视变换python-opencv实现参考文献 图像变换类型 图像几何变换主要包括以下几种类型&#xff1a; 平移&#xff08;Translation&#xff09;&#xff1a;将图像在水平或垂直方向上移动&#xff0c;不改变图像的尺寸和形状。缩放&#xff08;Sca…