基于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 IP地址的绑定的实现
May 08 Redis
redis哨兵常用命令和监控示例详解
May 27 Redis
redis cluster支持pipeline的实现思路
Jun 23 Redis
redis客户端实现高可用读写分离的方式详解
Jul 04 Redis
Redis源码阅读:Redis字符串SDS详解
Jul 15 Redis
解析redis hash应用场景和常用命令
Aug 04 Redis
Redis读写分离搭建的完整步骤
Sep 14 Redis
解决linux下redis数据库overcommit_memory问题
Feb 24 Redis
面试分析分布式架构Redis热点key大Value解决方案
Mar 13 Redis
Redis实现一个账号只能登录一个设备
Apr 19 Redis
Redis唯一ID生成器的实现
Jul 07 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基础知识:类与对象(5) static
2006/12/13 PHP
fleaphp下不确定的多条件查询的巧妙解决方法
2008/09/11 PHP
解析PHP对现有搜索引擎的调用
2013/06/25 PHP
PHP几个实用自定义函数小结
2016/01/25 PHP
php通过curl添加cookie伪造登陆抓取数据的方法
2016/04/02 PHP
php在windows环境下获得cpu内存实时使用率(推荐)
2018/02/08 PHP
javascript 数组的方法集合
2008/06/05 Javascript
JavaScript 学习笔记(十三)Dom创建表格
2010/01/21 Javascript
Jquery中获取iframe的代码
2011/01/11 Javascript
window.dialogArguments 使用说明
2011/04/11 Javascript
jQuery检测鼠标左键和右键点击的方法
2015/03/17 Javascript
JavaScript数组_动力节点Java学院整理
2017/06/26 Javascript
JavaScript强制类型转换和隐式类型转换操作示例
2019/05/01 Javascript
JavaScript 预解析的4种实现方法解析
2019/09/03 Javascript
Postman动态获取返回值过程详解
2020/06/30 Javascript
解决vue 退出动画无效的问题
2020/08/09 Javascript
python使用PyCharm进行远程开发和调试
2017/11/02 Python
Python将8位的图片转为24位的图片实现方法
2018/10/24 Python
python GUI库图形界面开发之PyQt5 MDI(多文档窗口)QMidArea详细使用方法与实例
2020/03/05 Python
基于python代码批量处理图片resize
2020/06/04 Python
瑞典Happy Socks美国官网:购买色彩斑斓的快乐袜子
2016/10/19 全球购物
英国领先的NHS批准的在线药店:Pharmacy2U
2017/01/06 全球购物
生物化工专业个人自荐信
2013/09/26 职场文书
《我不是最弱小的》教学反思
2014/02/23 职场文书
国贸专业自荐信范文
2014/03/02 职场文书
规划编制实施方案
2014/03/15 职场文书
高中生第一学年自我鉴定
2014/09/12 职场文书
2014年行风建设工作总结
2014/12/01 职场文书
乡镇一岗双责责任书
2015/01/29 职场文书
自荐信格式模板
2015/03/27 职场文书
不同意离婚答辩状
2015/05/22 职场文书
周一给客户的问候语
2015/11/10 职场文书
三好学生竞选稿
2015/11/21 职场文书
Java多条件判断场景中规则执行器的设计
2021/06/26 Java/Android
Python批量解压&压缩文件夹的示例代码
2022/04/04 Python
MySQL 原理与优化之Update 优化
2022/08/14 MySQL