基于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 11 Redis
redis实现共同好友的思路详解
May 26 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
基于Redis的List实现特价商品列表功能
Aug 30 Redis
Redis集群新增、删除节点以及动态增加内存的方法
Sep 04 Redis
SpringBoot集成Redis的思路详解
Oct 16 Redis
在Centos 8.0中安装Redis服务器的教程详解
Mar 21 Redis
Redis如何使用乐观锁(CAS)保证数据一致性
Mar 25 Redis
redis复制有可能碰到的问题汇总
Apr 03 Redis
Redis实现一个账号只能登录一个设备
Apr 19 Redis
Redis 异步机制
May 15 Redis
Redis实现短信验证码登录的示例代码
Jun 14 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
fetchAll()与mysql_fetch_array()的区别详解
2013/06/05 PHP
PHP序列化操作方法分析
2016/09/28 PHP
php通过header发送自定义数据方法
2018/01/18 PHP
基于jQuery的为attr添加id title等效果的实现代码
2011/04/20 Javascript
JavaScript实现简单的时钟实例代码
2013/11/23 Javascript
JQuery+Ajax实现数据查询、排序和分页功能
2015/09/27 Javascript
在JavaScript中对HTML进行反转义详解
2016/05/18 Javascript
分享jQuery网页元素拖拽插件
2020/12/01 Javascript
完美实现八种js焦点轮播图(下篇)
2020/04/20 Javascript
Bootstrap模态框调用功能实现方法
2016/09/19 Javascript
详解nodejs 文本操作模块-fs模块(四)
2016/12/22 NodeJs
前端面试知识点锦集(JavaScript篇)
2016/12/28 Javascript
React Native之TextInput组件解析示例
2017/08/22 Javascript
vue的.vue文件是怎么run起来的(vue-loader)
2018/12/10 Javascript
Echarts.js无法引入问题解决方案
2020/10/30 Javascript
Vue中使用wangeditor富文本编辑的问题
2021/02/07 Vue.js
python文件读写并使用mysql批量插入示例分享(python操作mysql)
2014/02/17 Python
Pyhton中防止SQL注入的方法
2015/02/05 Python
按日期打印Python的Tornado框架中的日志的方法
2015/05/02 Python
Python的Django框架中forms表单类的使用方法详解
2016/06/21 Python
Python代码实现KNN算法
2017/12/20 Python
用Python进行简单图像识别(验证码)
2018/01/19 Python
利用Pandas 创建空的DataFrame方法
2018/04/08 Python
python numpy数组的索引和切片的操作方法
2018/10/20 Python
Python自动采集微信联系人的实现示例
2020/02/28 Python
python代码xml转txt实例
2020/03/10 Python
记一次django内存异常排查及解决方法
2020/08/07 Python
高中数学教师求职信
2013/10/30 职场文书
日语专业毕业生自荐信
2013/11/11 职场文书
宠物店的创业计划书范文
2014/01/11 职场文书
团队经理竞聘书
2014/03/31 职场文书
工伤事故赔偿协议书
2014/04/15 职场文书
禁止酒驾标语
2014/06/25 职场文书
2014年关工委工作总结
2014/11/17 职场文书
综合办公室主任岗位职责
2015/04/01 职场文书
2016中秋节问候语
2015/11/11 职场文书