基于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 14 Redis
redis 限制内存使用大小的实现
May 08 Redis
基于Redis延迟队列的实现代码
May 13 Redis
Redis Cluster 字段模糊匹配及删除
May 27 Redis
浅谈Redis的几个过期策略
May 27 Redis
Redis可视化客户端小结
Jun 10 Redis
Redis缓存-序列化对象存储乱码问题的解决
Jun 21 Redis
Redis分布式锁Redlock的实现
Aug 07 Redis
浅谈Redis跟MySQL的双写问题解决方案
Feb 24 Redis
Redis监控工具RedisInsight安装与使用
Mar 21 Redis
Redis实战高并发之扣减库存项目
Apr 14 Redis
Redis 限流器
May 15 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 无法加载mcrypt.dll的解决办法
2013/04/03 PHP
PHP函数import_request_variables()用法分析
2016/04/02 PHP
用jscript实现列出安装的软件列表
2007/06/18 Javascript
JS延迟加载加快页面打开速度示例代码
2013/12/30 Javascript
jquery删除指定的html标签并保留标签内文本内容的方法
2014/04/02 Javascript
轻量级的原生js日历插件calendar.js使用指南
2015/04/28 Javascript
js仿3366小游戏选字游戏
2016/04/14 Javascript
详解vue 配合vue-resource调用接口获取数据
2017/06/22 Javascript
Bootstrap Table 删除和批量删除
2017/09/22 Javascript
利用vue+elementUI实现部分引入组件的方法详解
2017/11/22 Javascript
在vue组件中使用axios的方法
2018/03/16 Javascript
Vue 监听列表item渲染事件方法
2018/09/06 Javascript
AngularJs1.x自定义指令独立作用域的函数传入参数方法
2018/10/09 Javascript
微信小程序配置服务器提示验证token失败的解决方法
2019/04/03 Javascript
使用vue-cli3 创建vue项目并配置VS Code 自动代码格式化 vue语法高亮问题
2019/05/14 Javascript
vue路由中前进后退的一些事儿
2019/05/18 Javascript
详解element-ui级联菜单(城市三级联动菜单)和回显问题
2019/10/02 Javascript
vue使用一些外部插件及样式的配置代码
2019/11/18 Javascript
node.JS的crypto加密模块使用方法详解(MD5,AES,Hmac,Diffie-Hellman加密)
2020/02/06 Javascript
python实现多线程暴力破解登陆路由器功能代码分享
2015/01/04 Python
Python进程间通信之共享内存详解
2017/10/30 Python
解决tensorflow1.x版本加载saver.restore目录报错的问题
2018/07/26 Python
python global和nonlocal用法解析
2020/02/03 Python
tensorflow 初始化未初始化的变量实例
2020/02/06 Python
Python将list元素转存为CSV文件的实现
2020/11/16 Python
乐天旅游台湾网站:Rakuten Travel TW
2017/06/01 全球购物
教师实习自我鉴定
2013/12/14 职场文书
职业规划书如何设计?
2014/01/09 职场文书
师德师风个人反思
2014/04/28 职场文书
服务承诺口号
2014/05/22 职场文书
老公保证书
2015/01/17 职场文书
2015年招商引资工作总结
2015/04/25 职场文书
搞笑结婚保证书
2015/05/08 职场文书
2016年离婚协议书范文
2016/03/18 职场文书
致男子1500米运动员的广播稿
2019/11/08 职场文书
Java完整实现记事本代码
2022/06/16 Java/Android