场景需求

适用场景如签到送积分、签到领取奖励等,大致需求如下:

  • 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等。
  • 如果连续签到中断,则重置计数,每月初重置计数。
  • 当月签到满3天领取奖励1,满5天领取奖励2,满7天领取奖励3……等等。
  • 显示用户某个月的签到次数和首次签到日期。
  • 在日历控件上展示用户每月签到情况,可以切换年月显示……等等。

设计思路

对于用户签到数据,如果每条数据都用Key/Value的方式存储,当用户量大的时候内存开销是非常大的。而位图(BitMap)是由一组bit位组成的,在Redis内部虽然是采用String类型存储的,但Redis提供了一些命令可以直接操作位图的每一位。
它的优点是内存占用小、效率高且操作简单,很适合签到这类场景。

Redis提供了以下几个命令用于操作位图:

考虑到每月初需要重置连续签到次数,所以最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(因为月份最大只有31天)。位图的每一位代表一天的签到情况,1表示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 0 # 返回的首次签到的偏移量,加上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. 获取用户每天的签到情况
*/
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"));
} { // getSignMap
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
本月签到次数:12
连续签到次数: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位图实现用户签到功能的更多相关文章

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

    场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 当月签到满 ...

  2. Redis实战篇(二)基于Bitmap实现用户签到功能

    很多应用上都有用户签到的功能,尤其是配合积分系统一起使用.现在有以下需求: 签到1天得1积分,连续签到2天得2积分,3天得3积分,3天以上均得3积分等. 如果连续签到中断,则重置计数,每月重置计数. ...

  3. 利用redis的bitmap实现用户签到功能

    一.场景需求 适用场景如签到送积分.签到领取奖励等,大致需求如下: 比如签到1天送1积分,连续签到2天送2积分,3天送3积分,3天以上均送3积分等. 如果连续签到中断,则重置计数,每月初重置计数. 显 ...

  4. java redis 实现用户签到功能(很普通简单的签到功能)

    业务需求是用户每天只能签到一次,而且签到后用户增加积分,所以把用户每次签到时放到redis 缓存里面,然后每天凌晨时再清除缓存,大概简单思想是这样的 直接看代码吧如下 @Transactional @ ...

  5. 基于Redis位图实现系统用户登录统计

    项目需求,试着写了一个简单登录统计,基本功能都实现了,日志数据量小.具体性能没有进行测试~ 记录下开发过程与代码,留着以后改进! 1. 需求 1. 实现记录用户哪天进行了登录,每天只记录是否登录过,重 ...

  6. 签到功能,用 MySQL 还是 Redis ?

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

  7. 用Redis实现签到功能

    一.场景 在很多时候我们会遇到用户签到的场景,每天用户进入应用时,需要获取用户当天的签到状态,如果没签到,用户可以进行签到,并且得到相关的奖励.我们可能需要每天的签到情况,必要的时候可能还需要统计一下 ...

  8. Redis位图法记录在线用户的状态

    Redis位图法记录在线用户的状态 位图 Redis官方文档对于位图的介绍如下: 位图不是一个真实的数据类型,而是定义在字符串类型上的面向位的操作的集合.由于字符串类型是二进制安全的二进制大对象,并且 ...

  9. redis位图巧用,节约内存

    最近要做一个圣诞抽奖活动,需要记录每天用户签到的记录,以前一般都是用普通的字符串数据类型,每个用户的签到用一个 key // 用户10在活动第一天的签到key为record:1:10 $key = & ...

随机推荐

  1. shiro登录实现自定义路径跳转

    一.实现需求 登录框架采用shiro,需根据不同客户端类型实现相对应定义页面跳转. 二.登录页jsp表单 <div class="input-prepend" title=& ...

  2. vue之自定义组件

    除了核心功能默认内置的指令外,vue也允许用户注册自定义指令.虽然在vue2.0中,代码复用和抽象的主要形式是组件,但是有些情况下,我们仍需要对普通DOM元素进行底层操作,这个时候就需要用到自定义指令 ...

  3. python接口自动化20-requests获取响应时间(elapsed)与超时(timeout) ok试了 获取响应时间的

    前言 requests发请求时,接口的响应时间,也是我们需要关注的一个点,如果响应时间太长,也是不合理的.如果服务端没及时响应,也不能一直等着,可以设置一个timeout超时的时间 关于request ...

  4. java的应用包的方法,及调用类里面函数的原理

    selenium官网下载的selenium包 包导入eclipse 见:https://www.cnblogs.com/kaibindirver/p/10674604.html 代码

  5. python-selenium 并发执行用例的问题

    看了虫师的多进程执行测试用例一直都执行错误,最后解决了 解决方法如下: 使用threading模块 import threading 使用threading.Thread的方法 ,执行用例成功

  6. Java-学习-喜欢-品牌:互联网公司成为动物园,拟人化品牌形象真的那么有意思?

    ylbtech-Java-学习-喜欢-品牌:互联网公司成为动物园,拟人化品牌形象真的那么有意思?  1.返回顶部 1. 当我们在思考如何在这个碎片化.多元化的时代找到真实的.不被标签的自我时,互联网中 ...

  7. 1125 Chain the Ropes (25 分)

    1125 Chain the Ropes (25 分) Given some segments of rope, you are supposed to chain them into one rop ...

  8. docker构建tomcat镜像

    下载centos镜像 # docker pull daocloud.io/centos:7 [root@localhost ~]# docker pull daocloud.io/centos: : ...

  9. Linux配置IP,安装yum源

    ip addr 查看IP地址 通过 ip 命令加参数 addr 则是查看当前网卡的配置信息, 从下图中可以看出, 当前系统的 ens33 网卡并没有 ipv4 及 ipv6, 没有 ip地址 则只能说 ...

  10. USB-IF协会公布最新PD3.0(PPS)协议认证芯片和产品名单

    原文: http://www.chongdiantou.com/wp/archives/25510.html 2017年的骁龙技术峰会高通带来了第一款兼容USB PD3.0(PPS)的QC4+充电器, ...