Redis基于Bitmap实现用户签到功能


Posted in Redis onJune 20, 2021
目录
  • 功能分析
  • 更多应用场景
  • 总结
  • 参考资料

很多应用上都有用户签到的功能,尤其是配合积分系统一起使用。现在有以下需求:

  • 签到1天得1积分,连续签到2天得2积分,3天得3积分,3天以上均得3积分等。
  • 如果连续签到中断,则重置计数,每月重置计数。
  • 显示用户某月的签到次数和首次签到时间。
  • 在日历控件上展示用户每月签到,可以切换年月显示。
  • ...

 

功能分析

对于用户签到数据,如果直接采用数据库存储,当出现高并发访问时,对数据库压力会很大,例如双十一签到活动。这时候应该采用缓存,以减轻数据库的压力,Redis是高性能的内存数据库,适用于这样的场景。

如果采用String类型保存,当用户数量大时,内存开销就非常大。

如果采用集合类型保存,例如Set、Hash,查询用户某个范围的数据时,查询效率又不高。

Redis提供的数据类型BitMap(位图),每个bit位对应0和1两个状态。虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作BitMap,可以把它看作一个bit数组,数组的下标就是偏移量。

它的优点是内存开销小,效率高且操作简单,很适合用于签到这类场景。缺点在于位计算和位表示数值的局限。如果要用位来做业务数据记录,就不要在意value的值。

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

 

命令 说明 可用版本 时间复杂度
SETBIT 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 >= 2.2.0 O(1)
GETBIT 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。 >= 2.2.0 O(1)
BITCOUNT 计算给定字符串中,被设置为 1 的比特位的数量。 >= 2.6.0 O(N)
BITPOS 返回位图中第一个值为 bit 的二进制位的位置。 >= 2.8.7 O(N)
BITOP 对一个或多个保存二进制位的字符串 key 进行位元操作。 >= 2.6.0 O(N)
BITFIELD BITFIELD 命令可以在一次调用中同时对多个位范围进行操作。 >= 3.2.0 O(1)

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

例如 u:sign:1225:202101 表示ID=1225的用户在2021年1月的签到记录

# 用户1月6号签到
SETBIT u:sign:1225:202101 5 1 # 偏移量是从0开始,所以要把6减1

# 检查1月6号是否签到
GETBIT u:sign:1225:202101 5 # 偏移量是从0开始,所以要把6减1

# 统计1月份的签到次数
BITCOUNT u:sign:1225:202101

# 获取1月份前31天的签到数据
BITFIELD u:sign:1225:202101 get u31 0

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

示例代码

using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;

/**
* 基于Redis Bitmap的用户签到功能实现类
* 
* 实现功能:
* 1. 用户签到
* 2. 检查用户是否签到
* 3. 获取当月签到次数
* 4. 获取当月连续签到次数
* 5. 获取当月首次签到日期
* 6. 获取当月签到情况
*/
public class UserSignDemo
{
    private IDatabase _db;

    public UserSignDemo(IDatabase db)
    {
        _db = db;
    }

    /**
     * 用户签到
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 之前的签到状态
     */
    public bool DoSign(int uid, DateTime date)
    {
        int offset = date.Day - 1;
        return _db.StringSetBit(BuildSignKey(uid, date), offset, true);
    }

    /**
     * 检查用户是否签到
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 当前的签到状态
     */
    public bool CheckSign(int uid, DateTime date)
    {
        int offset = date.Day - 1;
        return _db.StringGetBit(BuildSignKey(uid, date), offset);
    }

    /**
     * 获取用户签到次数
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 当前的签到次数
     */
    public long GetSignCount(int uid, DateTime date)
    {
        return _db.StringBitCount(BuildSignKey(uid, date));
    }

    /**
     * 获取当月连续签到次数
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 当月连续签到次数
     */
    public long GetContinuousSignCount(int uid, DateTime date)
    {
        int signCount = 0;
        string type = $"u{date.Day}";   // 取1号到当天的签到状态

        RedisResult result = _db.Execute("BITFIELD", (RedisKey)BuildSignKey(uid, date), "GET", type, 0);
        if (!result.IsNull)
        {
            var list = (long[])result;
            if (list.Length > 0)
            {
                // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
                long v = list[0];
                for (int i = 0; i < date.Day; 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 DateTime? GetFirstSignDate(int uid, DateTime date)
    {
        long pos = _db.StringBitPosition(BuildSignKey(uid, date), true);
        return pos < 0 ? null : date.AddDays(date.Day - (int)(pos + 1));
    }

    /**
     * 获取当月签到情况
     *
     * @param uid  用户ID
     * @param date 日期
     * @return Key为签到日期,Value为签到状态的Map
     */
    public Dictionary<string, bool> GetSignInfo(int uid, DateTime date)
    {
        Dictionary<string, bool> signMap = new Dictionary<string, bool>(date.Day);
        string type = $"u{GetDayOfMonth(date)}";
        RedisResult result = _db.Execute("BITFIELD", (RedisKey)BuildSignKey(uid, date), "GET", type, 0);
        if (!result.IsNull)
        {
            var list = (long[])result;
            if (list.Length > 0)
            {
                // 由低位到高位,为0表示未签,为1表示已签
                long v = list[0];
                for (int i = GetDayOfMonth(date); i > 0; i--)
                {
                    DateTime d = date.AddDays(i - date.Day);
                    signMap.Add(FormatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
                    v >>= 1;
                }
            }
        }
        return signMap;
    }

    private static string FormatDate(DateTime date)
    {
        return FormatDate(date, "yyyyMM");
    }

    private static string FormatDate(DateTime date, string pattern)
    {
        return date.ToString(pattern);
    }

    /**
     * 构建签到Key
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 签到Key
     */
    private static string BuildSignKey(int uid, DateTime date)
    {
        return $"u:sign:{uid}:{FormatDate(date)}";
    }

    /**
     * 获取月份天数
     *
     * @param date 日期
     * @return 天数
     */
    private static int GetDayOfMonth(DateTime date)
    {
        if (date.Month == 2)
        {
            return 28;
        }
        if (new int[] { 1, 3, 5, 7, 8, 10, 12 }.Contains(date.Month))
        {
            return 31;
        }
        return 30;
    }

    static void Main(string[] args)
    {
        ConnectionMultiplexer connection = ConnectionMultiplexer.Connect("192.168.0.104:7001,password=123456");

        UserSignDemo demo = new UserSignDemo(connection.GetDatabase());
        DateTime today = DateTime.Now;
        int uid = 1225;

        { // doSign
            bool signed = demo.DoSign(uid, today);
            if (signed)
            {
                Console.WriteLine("您已签到:" + FormatDate(today, "yyyy-MM-dd"));
            }
            else
            {
                Console.WriteLine("签到完成:" + FormatDate(today, "yyyy-MM-dd"));
            }
        }

        { // checkSign
            bool signed = demo.CheckSign(uid, today);
            if (signed)
            {
                Console.WriteLine("您已签到:" + FormatDate(today, "yyyy-MM-dd"));
            }
            else
            {
                Console.WriteLine("尚未签到:" + FormatDate(today, "yyyy-MM-dd"));
            }
        }

        { // getSignCount
            long count = demo.GetSignCount(uid, today);
            Console.WriteLine("本月签到次数:" + count);
        }

        { // getContinuousSignCount
            long count = demo.GetContinuousSignCount(uid, today);
            Console.WriteLine("连续签到次数:" + count);
        }

        { // getFirstSignDate
            DateTime? date = demo.GetFirstSignDate(uid, today);
            if (date.HasValue)
            {
                Console.WriteLine("本月首次签到:" + FormatDate(date.Value, "yyyy-MM-dd"));
            }
            else
            {
                Console.WriteLine("本月首次签到:无");
            }
        }

        { // getSignInfo
            Console.WriteLine("当月签到情况:");
            Dictionary<string, bool> signInfo = new Dictionary<string, bool>(demo.GetSignInfo(uid, today));
            foreach (var entry in signInfo)
            {
                Console.WriteLine(entry.Key + ": " + (entry.Value ? "√" : "-"));
            }
        }
    }
}

运行结果

 Redis基于Bitmap实现用户签到功能

 

更多应用场景

  • 统计活跃用户:把日期作为Key,把用户ID作为offset,1表示当日活跃,0表示当日不活跃。还能使用位计算得到日活、月活、留存率等数据。
  • 用户在线状态:跟统计活跃用户一样。

 

总结

  • 位图优点是内存开销小,效率高且操作简单;缺点是位计算和位表示数值的局限。
  • 位图适合二元状态的场景,例如用户签到、在线状态等场景。
  • String类型最大长度为512M。 注意SETBIT时的偏移量,当偏移量很大时,可能会有较大耗时。 位图不是绝对的好,有时可能更浪费空间。
  • 如果位图很大,建议分拆键。如果要使用BITOP,建议读取到客户端再进行位计算。

 

参考资料

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

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

Redis:Bitmap的setbit,getbit,bitcount,bitop等使用与应用场景

BITFIELD SET command is not working

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

Redis 相关文章推荐
redis 查看所有的key方式
May 07 Redis
基于Redis实现分布式锁的方法(lua脚本版)
May 12 Redis
浅谈Redis主从复制以及主从复制原理
May 29 Redis
详解Redis基本命令与使用场景
Jun 01 Redis
redis 存储对象的方法对比分析
Aug 02 Redis
详解Redis在SpringBoot工程中的综合应用
Oct 16 Redis
Springboot/Springcloud项目集成redis进行存取的过程解析
Dec 04 Redis
Redis监控工具RedisInsight安装与使用
Mar 21 Redis
Redis超详细讲解高可用主从复制基础与哨兵模式方案
Apr 07 Redis
Redis 异步机制
May 15 Redis
Redis实现订单过期删除的方法步骤
Jun 05 Redis
redis实现的四种常见限流策略
Redis 哨兵集群的实现
Redis可视化客户端小结
Windows中Redis安装配置流程并实现远程访问功能
详解Redis复制原理
Windows下redis下载、redis安装及使用教程
深入理解redis中multi与pipeline
Jun 02 #Redis
You might like
用php+mysql一个名片库程序
2006/10/09 PHP
一键生成各种尺寸Icon的php脚本(实例)
2017/02/08 PHP
PHP crc32()函数讲解
2019/02/14 PHP
javascript document.execCommand() 常用解析
2009/12/14 Javascript
12个非常有创意的JavaScript小游戏
2010/03/18 Javascript
jquery实现的让超出显示范围外的导航自动固定屏幕最顶上
2011/09/22 Javascript
jquery Mobile入门—多页面切换示例学习
2013/01/08 Javascript
使用javascript控制cookie显示和隐藏背景图
2014/02/12 Javascript
用Jquery选择器计算table中的某一列某一行的合计
2014/08/13 Javascript
jQuery内部原理和实现方式浅析
2015/02/03 Javascript
JS实现从表格中动态删除指定行的方法
2015/03/31 Javascript
详解iframe与frame的区别
2016/01/13 Javascript
jQuery基于$.ajax设置移动端click超时处理方法
2016/05/14 Javascript
使用BootStrap实现标签切换原理解析
2017/03/14 Javascript
你可能不知道的CORS跨域资源共享
2019/03/13 Javascript
微信小程序下拉框搜索功能的实现方法
2019/07/31 Javascript
element-ui中Table表格省市区合并单元格的方法实现
2019/08/07 Javascript
VUE中setTimeout和setInterval自动销毁案例
2020/09/07 Javascript
python实现在pickling的时候压缩的方法
2014/09/25 Python
python+pyqt实现12306图片验证效果
2017/10/25 Python
解决nohup重定向python输出到文件不成功的问题
2018/05/11 Python
python爬虫框架scrapy实现模拟登录操作示例
2018/08/02 Python
Python3实现的判断回文链表算法示例
2019/03/08 Python
在python3中实现更新界面
2020/02/21 Python
Python接口测试结果集实现封装比较
2020/05/01 Python
python 读取串口数据的示例
2020/11/09 Python
英国时尚家具、家居饰品及礼品商店:Graham & Green
2016/09/15 全球购物
《争吵》教学反思
2014/02/15 职场文书
大班幼儿评语大全
2014/04/30 职场文书
个人租房协议书样本
2014/10/01 职场文书
文明单位汇报材料
2014/12/24 职场文书
2015年百日安全活动总结
2015/03/26 职场文书
老干部局2015年度工作总结
2015/10/22 职场文书
2016国庆节67周年寄语
2015/12/07 职场文书
Python实现抖音热搜定时爬取功能
2022/03/16 Python
Javascript中Microtask和Macrotask鲜为人知的知识点
2022/04/02 Javascript