基于Redis位图实现用户签到功能


Posted in Redis onMay 08, 2021

场景需求

适用场景如签到送积分、签到领取奖励等,大致需求如下:

  1. 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等。
  2. 如果连续签到中断,则重置计数,每月初重置计数。
  3. 当月签到满3天领取奖励1,满5天领取奖励2,满7天领取奖励3……等等。
  4. 显示用户某个月的签到次数和首次签到时间。
  5. 在日历控件上展示用户每月签到情况,可以切换年月显示……等等。

设计思路

对于用户签到数据,如果每条数据都用K/V的方式存储,当用户量大的时候内存开销是非常大的。而位图(BitMap)是由一组bit位组成的,每个bit位对应0和1两个状态,虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作位图,可以把它看作是一个bit数组,数组的下标就是偏移量。它的优点是内存开销小、效率高且操作简单,很适合用于签到这类场景。

Redis提供了以下几个指令用于操作位图:

SETBIT

GETBIT

BITCOUNT

BITPOS

BITOP

BITFIELD

考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。

例如u:sign:1000:201902表示ID=1000的用户在2019年2月的签到记录。

# 用户2月17号签到
SETBIT u:sign:1000:201902 16 1 # 偏移量是从0开始,所以要把17减1

# 检查2月17号是否签到
GETBIT u:sign:1000:201902 16 # 偏移量是从0开始,所以要把17减1

# 统计2月份的签到次数
BITCOUNT u:sign:1000:201902

# 获取2月份前28天的签到数据
BITFIELD u:sign:1000:201902 get u28 0

# 获取2月份首次签到的日期
BITPOS u:sign:1000:201902 1 # 返回的首次签到的偏移量,加上1即为当月的某一天

示例代码

import redis.clients.jedis.Jedis;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * 基于Redis位图的用户签到功能实现类
 * <p>
 * 实现功能:
 * 1. 用户签到
 * 2. 检查用户是否签到
 * 3. 获取当月签到次数
 * 4. 获取当月连续签到次数
 * 5. 获取当月首次签到日期
 * 6. 获取当月签到情况
 */
public class UserSignDemo {
    private Jedis jedis = new Jedis();

    /**
     * 用户签到
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 之前的签到状态
     */
    public boolean doSign(int uid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        return jedis.setbit(buildSignKey(uid, date), offset, true);
    }

    /**
     * 检查用户是否签到
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 当前的签到状态
     */
    public boolean checkSign(int uid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        return jedis.getbit(buildSignKey(uid, date), offset);
    }

    /**
     * 获取用户签到次数
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 当前的签到次数
     */
    public long getSignCount(int uid, LocalDate date) {
        return jedis.bitcount(buildSignKey(uid, date));
    }

    /**
     * 获取当月连续签到次数
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 当月连续签到次数
     */
    public long getContinuousSignCount(int uid, LocalDate date) {
        int signCount = 0;
        String type = String.format("u%d", date.getDayOfMonth());
        List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
        if (list != null && list.size() > 0) {
            // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 0; i < date.getDayOfMonth(); i++) {
                if (v >> 1 << 1 == v) {
                    // 低位为0且非当天说明连续签到中断了
                    if (i > 0) break;
                } else {
                    signCount += 1;
                }
                v >>= 1;
            }
        }
        return signCount;
    }

    /**
     * 获取当月首次签到日期
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 首次签到日期
     */
    public LocalDate getFirstSignDate(int uid, LocalDate date) {
        long pos = jedis.bitpos(buildSignKey(uid, date), true);
        return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
    }

    /**
     * 获取当月签到情况
     *
     * @param uid  用户ID
     * @param date 日期
     * @return Key为签到日期,Value为签到状态的Map
     */
    public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
        Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
        String type = String.format("u%d", date.lengthOfMonth());
        List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
        if (list != null && list.size() > 0) {
            // 由低位到高位,为0表示未签,为1表示已签
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = date.lengthOfMonth(); i > 0; i--) {
                LocalDate d = date.withDayOfMonth(i);
                signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
                v >>= 1;
            }
        }
        return signMap;
    }

    private static String formatDate(LocalDate date) {
        return formatDate(date, "yyyyMM");
    }

    private static String formatDate(LocalDate date, String pattern) {
        return date.format(DateTimeFormatter.ofPattern(pattern));
    }

    private static String buildSignKey(int uid, LocalDate date) {
        return String.format("u:sign:%d:%s", uid, formatDate(date));
    }

    public static void main(String[] args) {
        UserSignDemo demo = new UserSignDemo();
        LocalDate today = LocalDate.now();

        { // doSign
            boolean signed = demo.doSign(1000, today);
            if (signed) {
                System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
            } else {
                System.out.println("签到完成:" + formatDate(today, "yyyy-MM-dd"));
            }
        }

        { // checkSign
            boolean signed = demo.checkSign(1000, today);
            if (signed) {
                System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
            } else {
                System.out.println("尚未签到:" + formatDate(today, "yyyy-MM-dd"));
            }
        }

        { // getSignCount
            long count = demo.getSignCount(1000, today);
            System.out.println("本月签到次数:" + count);
        }

        { // getContinuousSignCount
            long count = demo.getContinuousSignCount(1000, today);
            System.out.println("连续签到次数:" + count);
        }

        { // getFirstSignDate
            LocalDate date = demo.getFirstSignDate(1000, today);
            System.out.println("本月首次签到:" + formatDate(date, "yyyy-MM-dd"));
        }

        { // getSignInfo
            System.out.println("当月签到情况:");
            Map<String, Boolean> signInfo = new TreeMap<>(demo.getSignInfo(1000, today));
            for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) {
                System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
            }
        }
    }

}

运行结果

您已签到:2019-02-18
您已签到:2019-02-18
本月签到次数:11
连续签到次数:8
本月首次签到:2019-02-02
当月签到情况:
2019-02-01: -
2019-02-02: √
2019-02-03: √
2019-02-04: -
2019-02-05: -
2019-02-06: √
2019-02-07: -
2019-02-08: -
2019-02-09: -
2019-02-10: -
2019-02-11: √
2019-02-12: √
2019-02-13: √
2019-02-14: √
2019-02-15: √
2019-02-16: √
2019-02-17: √
2019-02-18: √
2019-02-19: -
2019-02-20: -
2019-02-21: -
2019-02-22: -
2019-02-23: -
2019-02-24: -
2019-02-25: -
2019-02-26: -
2019-02-27: -
2019-02-28: -

参考链接

Redis 命令参考

Redis 深度历险:核心原理与应用实践

到此这篇关于基于Redis位图实现用户签到功能的文章就介绍到这了,更多相关Redis用户签到内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
Redis如何一键部署脚本
Apr 12 Redis
基于Redis延迟队列的实现代码
May 13 Redis
详解Redis主从复制实践
May 19 Redis
redis实现的四种常见限流策略
Jun 18 Redis
Redis缓存-序列化对象存储乱码问题的解决
Jun 21 Redis
在项目中使用redis做缓存的一些思路
Sep 14 Redis
redis缓存存储Session原理机制
Nov 20 Redis
解决redis批量删除key值的问题
Mar 23 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 Redis
Redis keys命令的具体使用
Jun 05 Redis
python中使用redis用法详解
Dec 24 Redis
基于Redis过期事件实现订单超时取消
May 08 #Redis
Redis实现订单自动过期功能的示例代码
May 08 #Redis
redis 限制内存使用大小的实现
使用Redis实现秒杀功能的简单方法
Redis6.0搭建集群Redis-cluster的方法
May 08 #Redis
浅谈Redis存储数据类型及存取值方法
Redis IP地址的绑定的实现
May 08 #Redis
You might like
php中的strpos使用示例
2014/02/27 PHP
强制PHP命令行脚本单进程运行的方法
2014/04/15 PHP
可以保证单词完整性的PHP英文字符串截取代码分享
2014/07/15 PHP
PHP实现上传图片到数据库并显示输出的方法
2018/05/31 PHP
使用jQuery插件创建常规模态窗口登陆效果
2013/08/23 Javascript
jquery foreach使用示例
2013/09/12 Javascript
可恶的ie8提示缺少id未定义
2014/03/20 Javascript
详谈nodejs异步编程
2014/12/04 NodeJs
Javascript中call和apply函数的比较和使用实例
2015/02/03 Javascript
jquery获取多个checkbox的值异步提交给php
2015/07/07 Javascript
基于jQuery通过jQuery.form.js插件实现异步上传
2015/12/13 Javascript
理解JS绑定事件
2016/01/19 Javascript
微信 java 实现js-sdk 图片上传下载完整流程
2016/10/21 Javascript
javascript 中的try catch应用总结
2017/04/01 Javascript
vue loadmore 组件滑动加载更多源码解析
2017/07/19 Javascript
实例详解JSON取值(key是中文或者数字)方式
2017/08/24 Javascript
iview给radio按钮组件加点击事件的实例
2017/09/30 Javascript
vue观察模式浅析
2018/09/25 Javascript
解决layui富文本编辑器图片上传无法回显的问题
2019/09/18 Javascript
python 实现文件的递归拷贝实现代码
2012/08/02 Python
python每次处理固定个数的字符的方法总结
2013/01/29 Python
使用python删除nginx缓存文件示例(python文件操作)
2014/03/26 Python
wxPython定时器wx.Timer简单应用实例
2015/06/03 Python
全面了解Python环境配置及项目建立
2016/06/30 Python
使用Python实现博客上进行自动翻页
2017/08/23 Python
python调用摄像头显示图像的实例
2018/08/03 Python
Python的条件表达式和lambda表达式实例
2019/01/31 Python
澳大利亚香水在线商店:City Perfume
2020/09/02 全球购物
好的自荐信的要求
2013/10/30 职场文书
业务员简历自我评价
2014/03/06 职场文书
教师专业自荐信
2014/05/31 职场文书
小学体育教学随笔
2015/08/14 职场文书
2019年世界儿童日宣传标语
2019/11/22 职场文书
导游词之淮安明祖陵
2019/11/25 职场文书
python opencv通过4坐标剪裁图片
2021/06/05 Python
用JS实现飞机大战小游戏
2021/06/09 Javascript