mysql 连续签到天数_瑞士军刀Redis(二)用户签到
瑞士军刀
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(二)用户签到相关推荐
- 实现 连续15签到记录_MySQL和Redis实现用户签到,你喜欢怎么实现?
现在的网站和app开发中,签到是一个很常见的功能 如微博签到送积分,签到排行榜 微博签到 如移动app ,签到送流量等活动, 移动app签到 用户签到是提高用户粘性的有效手段,用的好能事半功倍! 下面 ...
- mysql 连续天数_mysql计算连续天数,mysql连续登录天数,连续天数统计
mysql计算连续天数,mysql连续登录天数,连续天数统计 >>>>>>>>>>>>>>>>>& ...
- redis实现用户签到以及签到统计
1:数据库实现. 最简单的做法就是创建一个表,专门来存取用户的签到信息. 但是呢,这么做会有一个问题? 用户一次签到,就是一条记录,假如有1000万用户,平均每人每年签到次数为10次,则这张表一年的数 ...
- mysql 连续签到天数_签到功能实现,没有你想的那么复杂(一)
1 签到定义以及作用签到,指在规定的簿册上签名或写一"到"字,表示本人已经到达.在APP中使用此功能,可以增加用户粘性和活跃度.2 技术选型redis为主写入查询,mysql辅助查 ...
- mysql 连续签到天数_天天拿帮会通宝 帮会签到真给力
在<天龙八部手游>中,绝大多数少侠都拥有自己的帮会,每天和帮会兄弟姐妹们做着各种帮会活动,大家彼此配合默契,荣辱与共,其乐融融.为了尽可能地满足少侠对帮会通宝的需求,现在帮会又多了一项帮会 ...
- mysql 连续签到天数_新版签到活动明天上线,福利活动抢鲜看~
明天游戏中心app旧版福利签到活动马上就要结束啦,虽然很多的魅友反馈说积分签到结束的太早,积分不够换5折券,不过更早的开启新版签到才能更快的享受到更好的福利嘛~ 今晚0点新版签到活动即将上线,接下来就 ...
- mysql 连续签到天数_获取连续登陆天数,连续签到天数 ,方法优化
获取连续登陆天数,连续签到天数,类似这样的需求应该是一个常见的需求,那么我们有没有一套成熟的解决方案呢 ?下面我来跟大家分享一下我的故事. 在猴年马月的一天,有个用户反馈个人中心打开缓慢,需要7.8秒 ...
- mysql 连续签到天数_最大连续签到天数-sql
SELECT MIN(rq) as 起始日期 , MAX(rq) as 终止日期 , MAX(id1) - MIN(id1) + 1 as 持续天数 ,id3 as 累计签到天数 ,name FROM ...
- mysql 签到 存储,MySQL和Redis实现用户签到,你喜欢怎么实现?
现在的网站和app开发中,签到是一个很常见的功能,用户签到是提高用户粘性的有效手段,用的好能事半功倍! 下面我们从技术方面看看常用的实现手段: 一. 方案1 直接存到数据库MySQL 用户表如下: l ...
- mysql 点赞数据库设计_基于redis实现的点赞功能设计思路详解
点赞其实是一个很有意思的功能.基本的设计思路有大致两种, 一种自然是用mysql等 数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysq ...
最新文章
- ActiveMQ学习总结(10)——ActiveMQ采用Spring注解方式发送和监听
- 《神秘的程序员们》漫画47:这些年你读过的书
- select及触发方式,select监听多链接,select与epoll的实现区别
- Detectron-MaskRCnn: 用于抠图的FCNN
- Git常用命令与基本操作
- 2018年手机保值排行榜出炉:华为P20成最大赢家?
- 高校战役结束,中小学市场战斗吹响号角
- MySQL 面试必备 8 个知识点
- python debug run 结果不同_Python:PyCharm中运行与调试模式下的不同行为
- smartconfig配置模式
- 985硕士,入职八个月被通知裁员,领导哭着谈话,同事疯狂帮忙,但还是走了!...
- python 相对导入与绝对导入
- java判断是否英文_java如何判断字符串是否是英文
- 高等数学(第七版)同济大学 习题12-4 个人解答
- 我开发了一款软件,完成了舔狗的绝地反杀(代码开源)!
- 使用第三方应用打开pdf文件
- 并行计算范式-SIMD vs SIMT vs SMT: What’s the Difference Between Parallel Processing Models?
- 实现对文件夹进行加密
- mysql和ad的对接_对接mysql数据库遇见的一些问题
- XSS挑战之旅(1-9)