基于Redis的Bitmap位图配合前端组件实现用户签到功能

博客:https://www.mintimate.cn Mintimate’s Blog,只为与你分享

一位B站粉丝,问我后端Java和前端Vue,如何实现一个简单的签到功能,在吃了顿大餐后,顺便也把主要过程分享一下。

头图不能少呀

如果一个系统,想要实现签到功能,相信大多数人的第一反应都是Redis或者MySQL数据库。而使用Redis的Bitmap位图,主要是对资源的利用比较小,接下来就来详解一下啦。

为什么使用位图

位图,其实就是基于位的映射。BitMap 的基本原理就是用一个bit 位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的

举个例子,我们用bit的0和1来作为签到状态的有无,那么8天的签到数据就是8bit(1B),1个月的数据就是4B左右,也就是一个月一个用户的签到数据为4字节(4B)。

一个用户一个月的签到数据

前置依赖

总体上,我们将前后端分别部署在腾讯云的服务器上,中间件使用Redis进行签到信息的持久化存储,需要注意⚠️,Redis设置的有效期,我们设置为永不过期。

后端

这里介绍一下生产开发的环境,首先是后端:

  • JDK版本:ZuluOpenJDK 11
  • Maven骨架
  • Redis

我这里使用Maven进行项目依赖包的管理,并使用了SpringBoot自带的Redis依赖驱动:

<!--        Redis驱动-->
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

对Redis进行序列化:

/**
 * Redis设置
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

并且,我们创建一个签到的工具包,方便我们调用:

签到工具包

前端

而对于前端,我使用的目前还是Vue2,并且使用组件Buefy的日期:

buefy的日期组件

Redis签到

我们使用Redis的Bitmap进行签到,使用org.springframework.data.redis.core包下的opsForValue进行签到信息映射;

方法结构

其中,公共方法:

  • isSigned:传入用户Key和校验签到日期,判断是否有签到。
  • daSign:传入用户信息和需要签到的日期,返回签到结果(连续签到天数等)
  • monthSigned:传入用户Key和校验签到月份,返回当月签到情况详情。

而签到的信息,我们使用日期工具包构建用户的签到结果集合key,并设置Bitmap数值。

构建用户的签到key:

/**
 * 构建 Redis Key - user:sign:userId:yyyyMM
 *
 * @param userId 用户ID
 * @param date   日期
 * @return
 */
private String buildSignKey(String userId, Date date) {
    return String.format("img2d_user_daily_sign:%s:%s", userId,
            DateUtil.format(date, "yyyyMM"));
}

实际上,就是构建用户的Redis的key:

比如:2022年5月,用户雪花ID为1452998090465296386的key:

用户的key

而Redis内存储的value就是我们的Bitmap数据。

日期工具包

首先,在正式构建业务逻辑前,我们需要设计几个日期工具包的方法包,首先是用户获取当前的时间:

    /**
     * 获取日期
     *
     * @param dateStr yyyy-MM-dd
     * @return
     */
    private Date getDate(String dateStr) {
        return Objects.isNull(dateStr) ?
                new Date() : DateUtil.parseDate(dateStr);
    }

DateUtil是我自己写的日期方法:

 /**
     * 格式化日期
     *
     * @param StrDate
     * @return
     */
    public static Date parseDate(String StrDate) {
        // e.g. 获得2022年02月15日 的Date对象
        DateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
        Date myDate1 = null;
        try {
            myDate1 = dateFormat1.parse(StrDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return myDate1;
    }

    /**
     * 格式化日期
     *
     * @param date
     * @param format
     * @return
     */
    public static String format(Date date, String format) {
        // 获得2009年06月01日 的Date对象
        DateFormat dateFormat1 = new SimpleDateFormat(format);
        String myDate1 = dateFormat1.format(date);

        return myDate1;
    }

这样,就可以获取当天时间的yyyy-MM-dd格式了。当然,我们使用Bitmap进行数据存储,就需要判断签到月份有几个天数,进而生成Bitmap类型的String(Redis内,Bitmap本质使用String进行存储),所以在DateUtil工具包内追加:

    /**
     * 根据日期获取日期所在月份的天数
     *
     * @param date
     * @return
     */
    public static int dayOfMonth(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar.get(Calendar.DATE);
    }

最后的结果:

工具包结果

用户签到

我们使用刚刚构建的工具包,记得完成签到业务,并且可以进行补签:

    /**
     * 用户签到,可以补签
     *
     * @param userId  用户ID
     * @param dateStr 查询的日期,默认当天 yyyy-MM-dd
     * @return 连续签到次数和总签到次数
     */
    public Map<String, Object> doSign(String userId, String dateStr) {
        Map<String, Object> result = new HashMap<>();
        // 获取日期
        Date date = getDate(dateStr);
        // 获取日期对应的天数,多少号
        int day = DateUtil.dayOfMonth(date) - 1; // 从 0 开始
        // 构建 Redis Key
        String signKey = buildSignKey(userId, date);
        // 查看指定日期是否已签到
        if (isSigned(userId,dateStr)) {
            result.put("message", "当前日期已完成签到,无需再签");
            result.put("code", 400);
            return result;
        }
        // 签到
        redisTemplate.opsForValue().setBit(signKey, day, true);
        // 根据当前日期统计签到次数
        Date today = new Date();
        // 统计连续签到次数
        int continuous = getContinuousSignCount(userId, today);
        // 统计总签到次数
        long count = getSumSignCount(userId, today);
        result.put("message", "签到成功");
        result.put("code", 200);
        result.put("continuous", continuous);
        result.put("count", count);
        return result;
    }

我这里并没有封装结果集,所以使用Map进行回传。

连续判断

如何判断用户连续签到几天呢?有一个简单的方法:位移计算。

/**
     * 统计连续签到次数
     *
     * @param userId 用户ID
     * @param date   查询的日期
     * @return
     */
    private int getContinuousSignCount(String userId, Date date) {
        // 获取日期对应的天数,多少号,假设是 31
        int dayOfMonth = DateUtil.dayOfMonth(date);
        // 构建 Redis Key
        String signKey = buildSignKey(userId, date);
        // e.g. bitfield user:sign:5:202103 u31 0
        BitFieldSubCommands bitFieldSubCommands =
                BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        // 获取用户从当前日期开始到 1 号的所有签到状态
        List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return 0;
        }
        // 连续签到计数器
        int signCount = 0;
        long v = list.get(0) == null ? 0 : list.get(0);
        // 位移计算连续签到次数
        for (int i = dayOfMonth; i > 0; i--) {// i 表示位移操作次数
            // 右移再左移,如果等于自己说明最低位是 0,表示未签到
            if (v >> 1 << 1 == v) {
                // 用户可能当前还未签到,所以要排除是否是当天的可能性
                // 低位 0 且非当天说明连续签到中断了
                if (i != dayOfMonth) break;
            } else {
                // 右移再左移,如果不等于自己说明最低位是 1,表示签到
                signCount++;
            }
            // 右移一位并重新赋值,相当于把最低位丢弃一位然后重新计算
            v >>= 1;
        }
        return signCount;
    }

再写一个方法,方便我们调用:

    /**
     * 统计总签到次数
     *
     * @param userId 用户ID
     * @param date   查询的日期
     * @return
     */
    private Long getSumSignCount(String userId, Date date) {
        // 构建 Redis Key
        String signKey = buildSignKey(userId, date);
        // e.g. BITCOUNT user:sign:5:202103
        return (Long) redisTemplate.execute(
                (RedisCallback<Long>) con -> con.bitCount(signKey.getBytes())
        );
    }

最后结果:

最后结果

签到详情

这里我们还需获取月份对应的签到详情,我们可以这样:

    public String monthSigned(String userId,String dateStr){
        // 获取日期
        Date date = getDate(dateStr);
        String signKey = buildSignKey(userId, date);
        // 获取日期对应的天数,多少号,假设是 31
        int dayOfMonth = DateUtil.dayOfMonth(date);
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        // 获取月份datOfMonth到1号的所有签到状态
        // (也就是:如果签到情况为003,则显示3;签到情况为1003,则显示1003)
        List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
        String total=Long.toBinaryString(list.get(0));
        return total;
    }

需要注意List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);获取的数值,是会去除前面的零。

效果

我们编写一个测试类,打印输出试试看:

测试代码

运行后:

运行输出

Redis内存储:

Redis内的存储效果

到此,后端的Redis就写好了。

前端渲染

后端怎么设计API,前端怎么请求API数据,这类基础方法,这里就不再赘述。直接处理,前端怎么渲染签到天数。

我们这里根据后端写的代码,请求的月份签到,可以直接用前文的签到详情获取。

数据处理

因为,我们获取的数据,会自动忽略前面的零,举个例子,二月份我们只在15号签到,那么我们在2022-02-15这天获取的数据“签到详情”就是:

获取的详情

因为15号前并没有签到,全部为0,获取的数据就只有1了。

相对的,前端就需要给1前面补零:

const today = new Date().getDate()
for (let len = (dateList + "").length; len < today; len = dateList.length) {
            dateList = "0" + dateList;
}

当然,我这个是只统计当前日期所在月份当天前的签到情况,如果你想改成历史统计,注意修改代码。

之后,就是一段0和1组成的数据,比如:

000000000000001

数据渲染

我们使用Buefy的日期组件:

        <b-datepicker
          class="is-centered"
          expanded
          inline
          v-model="date"
          :events="events"
          :min-date="new Date()"
          :max-date="new Date()"
          indicators="bars"
        >
        </b-datepicker>

使用效果:

组件效果

在将刚刚的数据处理后结果二次处理:

for (let [index, value] of dateList.split("").entries()) {
  if (value == 1) {
   if ((index + 1) == today) {
     this.isDisabled = true
   }
   this.events.push({
    date: new Date(thisYear, thisMonth, index + 1),
    type: 'is-success'
  })
 }
}

最后效果:

签到效果

END

到此,我们的签到功能就设计好啦~~是不是还是挺简单的。

当然,有更好的完善方法,就要看自己的业务需求进行更改了。其实Bitmap位图,在布隆过滤器里用的更频繁,有机会也和大家分享一下。

原文地址:https://cloud.tencent.com/developer/article/2056469

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340