侧边栏壁纸
博主头像
极简笔记博主等级

极简笔记,书写你的精彩程序人生!

  • 累计撰写 147 篇文章
  • 累计创建 24 个标签
  • 累计收到 8 条评论

目 录CONTENT

文章目录

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

极简笔记
2022-07-28 / 0 评论 / 0 点赞 / 880 阅读 / 2,636 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-07-28,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Mintimate’s Blog 更新于:2022年7月27日 晚上

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

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

1.为什么使用位图

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

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

image-1658986217615

2.前置依赖

总体上,我们将前后端分别部署在腾讯云的服务器上,中间件使用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;
    }
}

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

image-1658986370006

前端

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

image-1658986400701

Redis签到

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

image-1658986464593

其中,公共方法:

  • 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:

image-1658986497153

而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);
}

最后的结果:

image-1658986587749

用户签到

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

/**
 * 用户签到,可以补签
 *
 * @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())
    );
}

最后结果:

image-1658986732645

签到详情

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

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);获取的数值,是会去除前面的零。

效果

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

image-1658986797429

运行后:

image-1658986807601

Redis内存储:

image-1658987178604

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

前端渲染

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

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

数据处理

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

image-1658987222803

因为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>

使用效果:

image-1658987342611

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

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'
  })
 }
}

最后效果:

image-1658986103210

END

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

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


本文转自 mintimate,如有侵权,请联系删除。

0

评论区