瑞士军刀

Redis

2020年11月28日

签到

需求分析

Redis应用之用户签到

在很多的互联网应用中,我们基本都存在签到送积分、签到领取奖励这样的需求,比如:

1、签到累计送积分

2、签到中断,重置计数,每月初重置计数

3、显示用户某个月的签到次数

4、在日历控件上展示用户每月签到的情况,可以切换年月显示

1.

MySQL实现方案

【】数据库表设计

在数据库保存用户的签到数据,每次签到均生成一条记录保存,对于用户量比较大的应用,数据库可能就会扛不住,比如一千万签到狂魔用户一个月会有三亿条数据,这是非常庞大的,并且,针对上面的需求分析,MySQL在判断用户某个月签到,和月份的切换上,需要写很多相对复杂的SQL语句,如果涉及到积分,甚至还会发生多表联查,这对于数据库来说压力会非常的大

2.

BitMap的实现方案

BitMaps位图,它是Redis中基于String类型的按位操作,高阶数据类型的一种,BitMap最大支持232位,512m的内存可以储存42.9亿的字节信息。

位图中,他是由一组bit组成的,每个bit位对应0和1两个状态,虽然内部还是采用String类型存储,但Redis提供了一些指令可以直接操作位图,可以把位图看做一个二进制的bit数组,数组的下标就是偏移量。

常见指令:

 我们的业务需求需要按月存储签到次数,最简单的方式是按每用户每月存一条签到数据,Key的格式为user:sign:userid:yyyyMM,位图的每一位代表一天的签到,例如:user:sign:98:202011表示用户98在2020年11月的签到情况。那么:

【这里放图】

3.

代码实现

用户签到Service

/** * 用户签到 * * @param accessToken 登录用户 token * @param dateStr     查询的日期,默认当天 yyyy-MM-dd * @return 连续签到次数 */public int doSign(String accessToken, String dateStr) {    // 获取登录用户信息    SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);    // 获取日期    Date date = getDate(dateStr);    // 获取日期对应的天数,多少号    int offset = DateUtil.dayOfMonth(date) - 1; // 从 0 开始    // 构建 Key    String signKey = buildSignKey(signInDinerInfo.getId(), date);    // 查看是否已签到    boolean isSigned = redisTemplate.opsForValue().getBit(signKey, offset);    AssertUtil.isTrue(isSigned, "当前日期已完成签到,无需再签");    // 签到    redisTemplate.opsForValue().setBit(signKey, offset, true);    // 统计连续签到次数    int count = getContinuousSignCount(signInDinerInfo.getId(), date);    return count;}

获取登录用户信息

/** * 获取登录用户信息 * * @param accessToken * @return */private SignInDinerInfo loadSignInDinerInfo(String accessToken) {    // 登录校验    AssertUtil.mustLogin(accessToken);    // 获取登录用户信息    String url = oauthServerName + "user/me?access_token={accessToken}";    ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);    if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {        throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());    }    // 这里的data是一个LinkedHashMap,SignInDinerInfo    SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),            new SignInDinerInfo(), true);    if (dinerInfo == null) {        throw new ParameterException(ApiConstant.NO_LOGIN_CODE, ApiConstant.NO_LOGIN_MESSAGE);    }    return dinerInfo;}

获取日期

/** * 获取日期 * * @param dateStr yyyy-MM-dd 默认当天 * @return */private static Date getDate(String dateStr) {    if (StrUtil.isBlank(dateStr)) {        return new Date();    }    try {        return DateUtil.parse(dateStr);    } catch (Exception e) {        throw new ParameterException("请传入yyyy-MM-dd的日期格式");    }}

构建Key

/** * 考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。 * Key的格式为user:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。 * 位图的每一位代表一天的签到,1表示已签,0表示未签。 * 构建存储Key user:sign:dinerId:yyyyMM * e.g. user:sign:89:202011表示dinerId=89的食客在2020年11月的签到记录。 * * @param dinerId * @return */private static String buildSignKey(int dinerId, Date date) {    return String.format("user:sign:%d:%s", dinerId,            DateUtil.format(date, "yyyyMM"));}

统计用户某月签到次数

/** * 统计某月连续签到次数 * * @param dinerId 用户ID * @param date    日期 * @return 当月连续签到次数 */private int getContinuousSignCount(Integer dinerId, Date date) {    // 获取日期对应的天数,多少号    int dayOfMonth = DateUtil.dayOfMonth(date);    // 构建 Key    String signKey = buildSignKey(dinerId, date);    // 命令:bitfield key get [u/i]offset value    // 此命令就是get取出key对应的位图,指定value索引位开始,取offset位偏移量的二进制    BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()            .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))            .valueAt(0);    Listlist = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);    if (list == null || list.isEmpty()) {        return 0;    }    int signCount = 0;    long v = list.get(0) == null ? 0 : list.get(0);    // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况    for (int i = dayOfMonth; i > 0; i--) {// i 表示位移次数        // 右移再左移,如果等于自己说明最低位是 0,表示未签到        if (v >> 1 << 1 == v) {            // 低位为 0 且非当天说明连续签到中断了            if (i != dayOfMonth) break;        } else {            // 签到了 签到数加1            signCount += 1;        }        // 右移一位并重新赋值,相当于把最右边一位去除        v >>= 1;    }    return signCount;}

控制层Controller

/** * 签到,可以补签 * @param access_token * @param date 某个日期 yyyy-MM-dd 默认当天 * @return */@PostMappingpublic ResultInfosign(String access_token,                                @RequestParam(required = false) String date) {    int count = signService.doSign(access_token, date);    return ResultInfoUtil.buildSuccess(request.getServletPath(), count);}

获取用户签到情况

/** * 获取当月签到情况 * * @param accessToken 登录token * @param dateStr     查询的日期,默认当月 yyyy-MM-dd * @return Key为签到日期,Value为签到状态的Map */public Map<String, Boolean> getSignInfo(String accessToken, String dateStr) {    // 获取登录用户信息    SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);    // 获取日期    Date date = getDate(dateStr);    // 构建 Key    String signKey = buildSignKey(signInDinerInfo.getId(), date);    // 构建一个自动排序的 Map    Map<String, Boolean> signInfo = new TreeMap<>();    // 获取某月的总天数(考虑闰年)    int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) + 1,            DateUtil.isLeapYear(DateUtil.dayOfYear(date)));    // 命令:bitfield key get [u/i]offset value    // 此命令就是get取出key对应的位图,指定value索引位开始,取offset位偏移量的二进制    // 获取某月最后一天的数值(取无符号整数)    BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()            .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))            .valueAt(0);    List list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);    if (list == null || list.isEmpty()) {        return signInfo;    }    long v = list.get(0) == null ? 0 : list.get(0);    // 由低位到高位进行遍历,为 0 表示未签,为 1 表示已签    for (int i = dayOfMonth; i > 0; i--) {        // 获取日期时间,比如 i = 31,最终拿到 yyyyMM31        LocalDateTime dateTime = LocalDateTimeUtil.of(date).withDayOfMonth(i);        // 先右移一位再左移一位,如果还不变那只能证明低位是 0,否则低位就是 1        boolean flag = v >> 1 << 1 != v;        // 构建一个 Key 为日期,value 为是否签到标记的有序 Map        signInfo.put(DateUtil.format(dateTime, "yyyy-MM-dd"), flag);        // 右移一位并重新赋值,相当于把最右边一位去除        v >>= 1;    }    return signInfo;}
/** * 获取用户签到情况 默认当月 * * @param access_token * @param date         某个日期 yyyy-MM-dd * @return */@GetMappingpublic ResultInfo<Map<String, Boolean>> getSignInfo(String access_token, String date) {    Map<String, Boolean> map = signService.getSignInfo(access_token, date);    return ResultInfoUtil.buildSuccess(request.getServletPath(), map);}

测试结果

{    "code": 1,    "message": "Successful.",    "path": "/sign",    "data": {        "2020-11-01": false,        "2020-11-02": false,        "2020-11-03": false,        "2020-11-04": false,        "2020-11-05": false,        "2020-11-06": false,        "2020-11-07": false,        "2020-11-08": false,        "2020-11-09": false,        "2020-11-10": false,        "2020-11-11": false,        "2020-11-12": false,        "2020-11-13": false,        "2020-11-14": false,        "2020-11-15": false,        "2020-11-16": false,        "2020-11-17": false,        "2020-11-18": false,        "2020-11-19": false,        "2020-11-20": true,        "2020-11-21": true,        "2020-11-22": true,        "2020-11-23": true,        "2020-11-24": true,        "2020-11-25": true,        "2020-11-26": false,        "2020-11-27": false,        "2020-11-28": false,        "2020-11-29": false,        "2020-11-30": false    }}

很简单吧~~~就这样我们通过Redis的高阶数据类型Bitmaps位图,很简单的实现了用户签到和统计每月用户签到情况的业务开发,说实话,我第一次学习到这样的解法的时候,我也是很惊讶的,编程嘛,路漫漫其修远兮~~~

还是那句话,代码私信我获得咯,如果有代码问题,可以积极沟通呦,祝大家早日月入过万!!

mysql 连续签到天数_瑞士军刀Redis(二)用户签到相关推荐

  1. 实现 连续15签到记录_MySQL和Redis实现用户签到,你喜欢怎么实现?

    现在的网站和app开发中,签到是一个很常见的功能 如微博签到送积分,签到排行榜 微博签到 如移动app ,签到送流量等活动, 移动app签到 用户签到是提高用户粘性的有效手段,用的好能事半功倍! 下面 ...

  2. mysql 连续天数_mysql计算连续天数,mysql连续登录天数,连续天数统计

    mysql计算连续天数,mysql连续登录天数,连续天数统计 >>>>>>>>>>>>>>>>>& ...

  3. redis实现用户签到以及签到统计

    1:数据库实现. 最简单的做法就是创建一个表,专门来存取用户的签到信息. 但是呢,这么做会有一个问题? 用户一次签到,就是一条记录,假如有1000万用户,平均每人每年签到次数为10次,则这张表一年的数 ...

  4. mysql 连续签到天数_签到功能实现,没有你想的那么复杂(一)

    1 签到定义以及作用签到,指在规定的簿册上签名或写一"到"字,表示本人已经到达.在APP中使用此功能,可以增加用户粘性和活跃度.2 技术选型redis为主写入查询,mysql辅助查 ...

  5. mysql 连续签到天数_天天拿帮会通宝 帮会签到真给力

    在<天龙八部手游>中,绝大多数少侠都拥有自己的帮会,每天和帮会兄弟姐妹们做着各种帮会活动,大家彼此配合默契,荣辱与共,其乐融融.为了尽可能地满足少侠对帮会通宝的需求,现在帮会又多了一项帮会 ...

  6. mysql 连续签到天数_新版签到活动明天上线,福利活动抢鲜看~

    明天游戏中心app旧版福利签到活动马上就要结束啦,虽然很多的魅友反馈说积分签到结束的太早,积分不够换5折券,不过更早的开启新版签到才能更快的享受到更好的福利嘛~ 今晚0点新版签到活动即将上线,接下来就 ...

  7. mysql 连续签到天数_获取连续登陆天数,连续签到天数 ,方法优化

    获取连续登陆天数,连续签到天数,类似这样的需求应该是一个常见的需求,那么我们有没有一套成熟的解决方案呢 ?下面我来跟大家分享一下我的故事. 在猴年马月的一天,有个用户反馈个人中心打开缓慢,需要7.8秒 ...

  8. mysql 连续签到天数_最大连续签到天数-sql

    SELECT MIN(rq) as 起始日期 , MAX(rq) as 终止日期 , MAX(id1) - MIN(id1) + 1 as 持续天数 ,id3 as 累计签到天数 ,name FROM ...

  9. mysql 签到 存储,MySQL和Redis实现用户签到,你喜欢怎么实现?

    现在的网站和app开发中,签到是一个很常见的功能,用户签到是提高用户粘性的有效手段,用的好能事半功倍! 下面我们从技术方面看看常用的实现手段: 一. 方案1 直接存到数据库MySQL 用户表如下: l ...

  10. mysql 点赞数据库设计_基于redis实现的点赞功能设计思路详解

    点赞其实是一个很有意思的功能.基本的设计思路有大致两种, 一种自然是用mysql等 数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysq ...

最新文章

  1. ActiveMQ学习总结(10)——ActiveMQ采用Spring注解方式发送和监听
  2. 《神秘的程序员们》漫画47:这些年你读过的书
  3. select及触发方式,select监听多链接,select与epoll的实现区别
  4. Detectron-MaskRCnn: 用于抠图的FCNN
  5. Git常用命令与基本操作
  6. 2018年手机保值排行榜出炉:华为P20成最大赢家?
  7. 高校战役结束,中小学市场战斗吹响号角
  8. MySQL 面试必备 8 个知识点
  9. python debug run 结果不同_Python:PyCharm中运行与调试模式下的不同行为
  10. smartconfig配置模式
  11. 985硕士,入职八个月被通知裁员,领导哭着谈话,同事疯狂帮忙,但还是走了!...
  12. python 相对导入与绝对导入
  13. java判断是否英文_java如何判断字符串是否是英文
  14. 高等数学(第七版)同济大学 习题12-4 个人解答
  15. 我开发了一款软件,完成了舔狗的绝地反杀(代码开源)!
  16. 使用第三方应用打开pdf文件
  17. 并行计算范式-SIMD vs SIMT vs SMT: What’s the Difference Between Parallel Processing Models?
  18. 实现对文件夹进行加密
  19. mysql和ad的对接_对接mysql数据库遇见的一些问题
  20. XSS挑战之旅(1-9)

热门文章

  1. poj-1190 生日蛋糕 **
  2. 14.docker volumn
  3. 24.事务控制和锁定语句
  4. 24.Yii 组件行为
  5. python的内置函数 剖析
  6. java 修改字体大小
  7. 洛谷1309 瑞士轮 解题报告
  8. resize view from nib引起的子控制器视图(childviewcontroller)部分区域无响应
  9. Java概 述(新手专区)
  10. 向Yahoo Mail的主页学习