基于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延迟队列的实现代码
May 13 Redis
Java Socket实现Redis客户端的详细说明
May 26 Redis
Redis主从配置和底层实现原理解析(实战记录)
Jun 30 Redis
厉害!这是Redis可视化工具最全的横向评测
Jul 15 Redis
Redis入门教程详解
Aug 30 Redis
详解redis在微服务领域的贡献
Oct 16 Redis
SpringBoot整合Redis入门之缓存数据的方法
Nov 17 Redis
聊聊redis-dump工具安装问题
Jan 18 Redis
Redis+Lua脚本实现计数器接口防刷功能(升级版)
Feb 12 Redis
浅谈Redis跟MySQL的双写问题解决方案
Feb 24 Redis
解决 Redis 秒杀超卖场景的高并发
Apr 12 Redis
Redis配置外网可访问(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中HTML标签过滤技巧
2014/01/07 PHP
yii实现创建验证码实例解析
2014/07/31 PHP
jQuery中与toggleClass等价的程序段 以及未来学习的方向
2010/03/18 Javascript
用jquery模仿的a的title属性(兼容ie6/7)
2013/01/21 Javascript
html文档中的location对象属性理解及常见的用法
2014/08/13 Javascript
javascript设计模式--策略模式之输入验证
2015/11/27 Javascript
jquery+json实现动态商品内容展示的方法
2016/01/14 Javascript
Javascript的表单验证-提交表单
2016/03/18 Javascript
详解MVC如何使用开源分页插件(shenniu.pager.js)
2016/12/16 Javascript
JavaScript简单生成 N~M 之间随机数的方法
2017/01/13 Javascript
深入理解Vue 单向数据流的原理
2017/11/09 Javascript
vue.js $refs和$emit 父子组件交互的方法
2017/12/20 Javascript
Javascript中JSON数据分组优化实践及JS操作JSON总结
2017/12/22 Javascript
ng-repeat指令在迭代对象时的去重方法
2018/10/02 Javascript
vue-cli3+typescript初体验小结
2019/02/28 Javascript
详解JavaScript 的执行机制
2020/09/18 Javascript
解决vue elementUI 使用el-select 时 change事件的触发问题
2020/11/17 Vue.js
[02:13] 完美世界DOTA2联赛PWL DAY5集锦
2020/11/03 DOTA
Python实例分享:快速查找出被挂马的文件
2014/06/08 Python
利用Python2下载单张图片与爬取网页图片实例代码
2017/12/25 Python
tensorflow学习教程之文本分类详析
2018/08/07 Python
Python实现的拉格朗日插值法示例
2019/01/08 Python
python时间序列按频率生成日期的方法
2019/05/14 Python
python实现对图片进行旋转,放缩,裁剪的功能
2019/08/07 Python
python-web根据元素属性进行定位的方法
2019/12/13 Python
Python 限定函数参数的类型及默认值方式
2019/12/24 Python
Pytorch 搭建分类回归神经网络并用GPU进行加速的例子
2020/01/09 Python
Python之多进程与多线程的使用
2021/02/23 Python
html5指南-4.使用Geolocation实现定位功能
2013/01/07 HTML / CSS
财务会计实习报告体会
2013/12/20 职场文书
幼儿教育感言
2014/02/05 职场文书
文案策划求职信
2014/03/18 职场文书
2014年志愿者工作总结
2014/11/20 职场文书
酒会邀请函
2015/01/31 职场文书
小孩不笨观后感
2015/06/03 职场文书
优秀教师主要事迹材料
2015/11/04 职场文书