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连接被拒绝的解决方案
Apr 12 Redis
Redis安装启动及常见数据类型
Apr 14 Redis
redis三种高可用方式部署的实现
May 11 Redis
解析Redis Cluster原理
Jun 21 Redis
Redis源码阅读:Redis字符串SDS详解
Jul 15 Redis
redis击穿 雪崩 穿透超详细解决方案梳理
Mar 17 Redis
Redis监控工具RedisInsight安装与使用
Mar 21 Redis
Redis如何使用乐观锁(CAS)保证数据一致性
Mar 25 Redis
redis复制有可能碰到的问题汇总
Apr 03 Redis
详解Redis的三种常用的缓存读写策略步骤
May 06 Redis
Redis入门基础常用操作命令整理
Jun 01 Redis
Redis sentinel哨兵集群的实现步骤
Jul 15 Redis
redis实现的四种常见限流策略
Redis 哨兵集群的实现
Redis可视化客户端小结
Windows中Redis安装配置流程并实现远程访问功能
详解Redis复制原理
Windows下redis下载、redis安装及使用教程
深入理解redis中multi与pipeline
Jun 02 #Redis
You might like
理解php Hash函数,增强密码安全
2011/02/25 PHP
php 常用算法和时间复杂度
2013/07/01 PHP
PHP函数shuffle()取数组若干个随机元素的方法分析
2016/04/02 PHP
CentOS 7.2 下编译安装PHP7.0.10+MySQL5.7.14+Nginx1.10.1的方法详解(mini版本)
2016/09/01 PHP
php生出随机字符串
2017/07/06 PHP
ThinkPHP框架表单验证操作方法
2017/07/19 PHP
Jquery中Ajax 缓存带来的影响的解决方法
2011/05/19 Javascript
javascript中replace( )方法的使用
2015/04/24 Javascript
每天一篇javascript学习小结(RegExp对象)
2015/11/17 Javascript
微信小程序 获取相册照片实例详解
2016/11/16 Javascript
新闻上下滚动jquery 超简洁(必看篇)
2017/01/21 Javascript
有关JS中的0,null,undefined,[],{},'''''''',false之间的关系
2017/02/14 Javascript
三分钟学会用ES7中的Async/Await进行异步编程
2018/06/14 Javascript
Vue 菜单栏点击切换单个class(高亮)的方法
2018/08/22 Javascript
javascript实现计算器功能
2020/03/30 Javascript
微信小程序实现导航栏和内容上下联动功能代码
2020/06/29 Javascript
[01:44]Ti10举办地公布
2019/08/25 DOTA
python处理json数据中的中文
2014/03/06 Python
利用Python批量生成任意尺寸的图片
2016/08/29 Python
利用pyinstaller或virtualenv将python程序打包详解
2017/03/22 Python
python机器学习理论与实战(二)决策树
2018/01/19 Python
配置 Pycharm 默认 Test runner 的图文教程
2018/11/30 Python
Python解析json代码实例解析
2019/11/25 Python
pd.DataFrame统计各列数值多少的实例
2019/12/05 Python
python实现拼图小游戏
2020/02/22 Python
Python控制台实现交互式环境执行
2020/06/09 Python
css3选择器基本介绍
2014/12/15 HTML / CSS
英国家电直销:Appliances Direct
2016/09/22 全球购物
博朗(Braun)俄罗斯官方商店:德国小家电品牌
2019/09/24 全球购物
用C语言实现文件读写操作
2013/10/27 面试题
《小壁虎借尾巴》教学反思
2014/02/16 职场文书
yy生日主持词
2014/03/20 职场文书
卖车协议书
2014/04/21 职场文书
学校远程教育工作总结
2015/08/11 职场文书
2016保送生自荐信范文
2016/01/29 职场文书
2016年清明节网上祭英烈活动总结
2016/04/01 职场文书