使用 Redis BitMap 实现签到与查询历史签到以及签到统计功能(SpringBoot环境)

目录

    • 一、前言
    • 二、Redis BitMap 位图原理
      • 2.1、BitMap 能解决什么
      • 2.2、BitMap 存储空间计算
      • 2.3、BitMap 存在问题
    • 三、Redis BitMap 操作基本语法和原生实现签到
      • 3.1、基本语法
      • 3.2、Redis BitMap 实现签到操作指令
    • 四、SpringBoot 使用 Redis BitMap 实现签到与统计功能
      • 4.1、代码实现
      • 4.2、功能测试

一、前言

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

二、Redis BitMap 位图原理

      BitMap 在 Redis 中并不是一个新的数据类型,其底层是 Redis 实现,Redis 的位图(BitMap)是由多个二进制位组成的数组,只有两种状态,0和1, 数组中的每个二进制位都有与之对应的偏移量(从 0 开始),通过这些偏移量可以对位图中指定的一个或多个二进制位进行操作,由于采用一个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,其余日期不签到。

  • 1、添加用户签到位 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
    
  • 2、查看用户指定日期是否有签到(查看当天是否有签到同理),这里查看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:https://blog.csdn.net/weixin_44606481/article/details/133907103

我这里还会使用到hutool工具包操作时间解析,有需要可以引入。

<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/159234.html

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

相关文章

ClickHouse的分片和副本

1.副本 副本的目的主要是保障数据的高可用性&#xff0c;即使一台ClickHouse节点宕机&#xff0c;那么也可以从其他服务器获得相同的数据。 Data Replication | ClickHouse Docs 1.1 副本写入流程 1.2 配置步骤 &#xff08;1&#xff09;启动zookeeper集群 &#xff08;2&…

异常

文章目录 概念体系结构分类处理抛异常捕获异常throws 异常声明try-catch 异常捕获finally 异常处理流程自定义异常 概念 在Java中&#xff0c;将程序执行过程中发生的不正常行为称为异常。 比如: 算术异常 Exception in thread "main" java.lang.ArithmeticExcept…

C/C++---------------LeetCode第LCR. 024.反转链表

反转链表 题目及要求双指针 题目及要求 双指针 思路&#xff1a;遍历链表&#xff0c;并在访问各节点时修改 next 引用指向&#xff0c;首先&#xff0c;检查链表是否为空或者只有一个节点&#xff0c;如果是的话直接返回原始的头节点&#xff0c;然后使用三个指针来迭代整个…

最新自动定位版本付费进群系统源码

更新内容&#xff1a; 1.在网站首页增加了付款轮播功能。 2.新增了城市定位功能&#xff0c;方便用户查找所在城市的相关信息。 3.对域名库及支付设置进行了更新和优化。 4.增加了一种图模板设置模式&#xff0c;简化了后台模板设置流程。 5.此外还进行了前后台的其他优化…

二进制分析工具-radare2使用教程

二进制分析工具-radare2使用教程 按照如下执行命令 按照如下执行命令 r2 -A 二进制文件

智慧物流追踪:打造未来的物流网络

随着互联网和物流行业的深度融合&#xff0c;智慧物流已成为现代物流发展的新趋势。通过开发一款智能化的物流追踪app小程序&#xff0c;我们不仅可以提高物流效率&#xff0c;还可以为客户提供更加便捷的服务。本文将从市场需求、技术应用、竞争优势、行业前景等方面对智慧物流…

upload-labs(1-17关攻略详解)

upload-labs pass-1 上传一个php文件&#xff0c;发现不行 但是这回显是个前端显示&#xff0c;直接禁用js然后上传 f12禁用 再次上传&#xff0c;成功 右键打开该图像 即为位置&#xff0c;使用蚁剑连接 连接成功 pass-2 源码 $is_upload false; $msg null; if (isse…

达尔优EK87键盘说明书

EK87说明书连接说明&#xff1a; **有线模式&#xff1a;**开关拨到最右边&#xff0c;然后插线连接电脑即可使用 2.4G **接收器模式&#xff1a;**开关拨到中间&#xff0c;然后接收器插入电脑USB接口即可使用 **蓝牙模式&#xff1a;**开关拨到最左边&#xff0c;然后按FNQ长…

【面试经典150 | 数学】加一

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;加一 其他语言python3 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结…

一文看分布式锁

为什么会存在分布式锁&#xff1f; 经典场景-扣库存&#xff0c;多人去同时购买一件商品&#xff0c;首先会查询判断是否有剩余&#xff0c;如果有进行购买并扣减库存&#xff0c;没有提示库存不足。假如现在仅存有一件商品&#xff0c;3人同时购买&#xff0c;三个线程同时执…

Apache DolphinScheduler在通信行业的多集群统一建设与管理实践

背景介绍 为什么我们考虑构建统一的调度平台&#xff1f; 主要原因是&#xff1a;我们公司的大数据中心目前拥有七个大数据集群&#xff0c;这些集群分布在不同的机房&#xff0c;例如内蒙、南京、苏州和广州。而且&#xff0c;这些机房之间的网络并不互通。如果每个集群都独立…

基于SSM的项目管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

4核8G服务器价格选择轻量还是CVM合适?

腾讯云服务器4核8G配置优惠价格表&#xff0c;轻量应用服务器和CVM云服务器均有活动&#xff0c;云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元&#xff0c;5年6490.44元&#xff0c;轻量应用服务器4核8G12M带宽一年446元、529元15个月&#xff0c;腾讯云百科txybk.com分…

各机构如何加强网络渗透、“渗透”防御

数据渗透&#xff0c;例如黑客攻击和“渗透”&#xff0c;或未经授权的信息传输。 联邦调查局、国家安全局以及网络安全和基础设施安全局最近的联合报告证明&#xff0c;网络安全仍然是当今国防部门面临的两个最大的网络威胁。 所谓的零日攻击尤其有害&#xff0c;因为组织在…

DMA原理和应用

目录 1.什么是DMA 2.DMA的意义 3.DMA搬运的数据和方式 4.DMA 控制器和通道 5.DMA通道的优先级 6.DMA传输方式 7.DMA应用 实验一: 内存到内存搬运 CubeMX配置&#xff1a; ​编辑用到的库函数&#xff1a; 代码实现思路&#xff1a; 实验二: 内存到外设搬运 CubeMX…

VR智慧景区:VR赋能文旅产业,激活消费潜能

随着国家数字化战略的不断深入实施&#xff0c;文旅产业数字化转型的步伐也在逐渐加快&#xff0c;以VR技术赋能文旅产业&#xff0c;让文旅景区线上线下双渠道融合&#xff0c;进一步呈现文化底蕴、激活消费潜能。 VR智慧景区以沉浸式、互动式、科技感的方式&#xff0c;将景区…

Vellum —— Constraint 约束

目录 Stretch Bend Pin Drag 解算器对DOP外节点的约束属性&#xff0c;只会读取起始帧的值&#xff1b; Stretch 保持点间的初始距离&#xff1b; Stiffness 越高的stiffness&#xff0c;就需要越多的迭代来收敛&#xff0c;如constraint iterations或substeps(子步会更好)…

Codeforces Round 909 (Div. 3)(A~G)(启发式合并)

1899A - Game with Integers 题意&#xff1a;给定一个数 , 两个人玩游戏&#xff0c;每人能够执行 操作&#xff0c;若操作完是3的倍数则获胜&#xff0c;问先手的人能否获胜&#xff08;若无限循环则先手的人输&#xff09;。 思路&#xff1a;假如一个数模3余1或者2&#…

Java编程中,异步操作流程中,最终一致性以及重试补偿的设计与实现

一、背景 微服务设计中&#xff0c;跨服务的调用&#xff0c;由于网络或程序故障等各种原因&#xff0c;经常会出现调用失败而需要重试。另外&#xff0c;在异步操作中&#xff0c;我们提供接口让外部服务回调。回调过程中&#xff0c;也可能出现故障。 这就要求我们主动向外…

金融业务系统: Service Mesh用于安全微服务集成

随着云计算的不断演进&#xff0c;微服务架构变得日益复杂。为了有效地管理这种复杂性&#xff0c;人们开始采用服务网格。在本文中&#xff0c;我们将解释什么是Service Mesh&#xff0c;为什么它对现代云架构至关重要&#xff0c;以及它是如何解决开发人员今天面临的一些最紧…