场景需求

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

  • 签到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月的签到记录。

  1. # 用户2月17号签到
  2. SETBIT u:sign:1000:201902 16 1 # 偏移量是从0开始,所以要把17减1
  3. # 检查2月17号是否签到
  4. GETBIT u:sign:1000:201902 16 # 偏移量是从0开始,所以要把17减1
  5. # 统计2月份的签到次数
  6. BITCOUNT u:sign:1000:201902 # 返回当月签到次数
  7. # 获取2月份前28天的签到数据
  8. BITFIELD u:sign:1000:201902 get u28 0
  9. # 获取2月份首次签到的日期
  10. BITPOS u:sign:1000:201902 0 # 返回的首次签到的偏移量,加上1即为当月的某一天
示例代码
  1. import redis.clients.jedis.Jedis;
  2. import java.time.LocalDate;
  3. import java.time.format.DateTimeFormatter;
  4. import java.util.HashMap;
  5. import java.util.List;
  6. import java.util.Map;
  7. import java.util.TreeMap;
  8. /**
  9. * 基于Redis位图的用户签到功能实现类
  10. * <p>
  11. * 实现功能:
  12. * 1. 用户签到
  13. * 2. 检查用户是否签到
  14. * 3. 获取用户签到次数
  15. * 4. 获取用户连续签到次数
  16. * 5. 获取用户每天的签到情况
  17. */
  18. public class UserSignDemo {
  19. private Jedis jedis = new Jedis();
  20. /**
  21. * 用户签到
  22. *
  23. * @param uid 用户ID
  24. * @param date 日期
  25. * @return 之前的签到状态
  26. */
  27. public boolean doSign(int uid, LocalDate date) {
  28. int offset = date.getDayOfMonth() - 1;
  29. return jedis.setbit(buildSignKey(uid, date), offset, true);
  30. }
  31. /**
  32. * 检查用户是否签到
  33. *
  34. * @param uid 用户ID
  35. * @param date 日期
  36. * @return 当前的签到状态
  37. */
  38. public boolean checkSign(int uid, LocalDate date) {
  39. int offset = date.getDayOfMonth() - 1;
  40. return jedis.getbit(buildSignKey(uid, date), offset);
  41. }
  42. /**
  43. * 获取用户签到次数
  44. *
  45. * @param uid 用户ID
  46. * @param date 日期
  47. * @return 当前的签到次数
  48. */
  49. public long getSignCount(int uid, LocalDate date) {
  50. return jedis.bitcount(buildSignKey(uid, date));
  51. }
  52. /**
  53. * 获取当月连续签到次数
  54. *
  55. * @param uid 用户ID
  56. * @param date 日期
  57. * @return 当月连续签到次数
  58. */
  59. public long getContinuousSignCount(int uid, LocalDate date) {
  60. int signCount = 0;
  61. String type = String.format("u%d", date.getDayOfMonth());
  62. List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
  63. if (list != null && list.size() > 0) {
  64. // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
  65. long v = list.get(0) == null ? 0 : list.get(0);
  66. for (int i = 0; i < date.getDayOfMonth(); i++) {
  67. if (v >> 1 << 1 == v) {
  68. // 低位为0且非当天说明连续签到中断了
  69. if (i > 0) break;
  70. } else {
  71. signCount += 1;
  72. }
  73. v >>= 1;
  74. }
  75. }
  76. return signCount;
  77. }
  78. /**
  79. * 获取当月首次签到日期
  80. *
  81. * @param uid 用户ID
  82. * @param date 日期
  83. * @return 首次签到日期
  84. */
  85. public LocalDate getFirstSignDate(int uid, LocalDate date) {
  86. long pos = jedis.bitpos(buildSignKey(uid, date), true);
  87. return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
  88. }
  89. /**
  90. * 获取当月的签到情况
  91. *
  92. * @param uid 用户ID
  93. * @param date 日期
  94. * @return Key为签到日期,Value为签到状态的Map
  95. */
  96. public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
  97. Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
  98. String type = String.format("u%d", date.lengthOfMonth());
  99. List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
  100. if (list != null && list.size() > 0) {
  101. // 由低位到高位,为0表示未签到,为1表示已签到
  102. long v = list.get(0) == null ? 0 : list.get(0);
  103. for (int i = date.lengthOfMonth(); i > 0; i--) {
  104. LocalDate d = date.withDayOfMonth(i);
  105. signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
  106. v >>= 1;
  107. }
  108. }
  109. return signMap;
  110. }
  111. private static String formatDate(LocalDate date) {
  112. return formatDate(date, "yyyyMM");
  113. }
  114. private static String formatDate(LocalDate date, String pattern) {
  115. return date.format(DateTimeFormatter.ofPattern(pattern));
  116. }
  117. private static String buildSignKey(int uid, LocalDate date) {
  118. return String.format("u:sign:%d:%s", uid, formatDate(date));
  119. }
  120. public static void main(String[] args) {
  121. UserSignDemo demo = new UserSignDemo();
  122. LocalDate today = LocalDate.now();
  123. { // doSign
  124. boolean signed = demo.doSign(1000, today);
  125. if (signed) {
  126. System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
  127. } else {
  128. System.out.println("签到完成:" + formatDate(today, "yyyy-MM-dd"));
  129. }
  130. }
  131. { // checkSign
  132. boolean signed = demo.checkSign(1000, today);
  133. if (signed) {
  134. System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
  135. } else {
  136. System.out.println("尚未签到:" + formatDate(today, "yyyy-MM-dd"));
  137. }
  138. }
  139. { // getSignCount
  140. long count = demo.getSignCount(1000, today);
  141. System.out.println("本月签到次数:" + count);
  142. }
  143. { // getContinuousSignCount
  144. long count = demo.getContinuousSignCount(1000, today);
  145. System.out.println("连续签到次数:" + count);
  146. }
  147. { // getFirstSignDate
  148. LocalDate date = demo.getFirstSignDate(1000, today);
  149. System.out.println("本月首次签到:" + formatDate(date, "yyyy-MM-dd"));
  150. }
  151. { // getSignMap
  152. System.out.println("当月签到情况:");
  153. Map<String, Boolean> signInfo = new TreeMap<>(demo.getSignInfo(1000, today));
  154. for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) {
  155. System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
  156. }
  157. }
  158. }
  159. }
运行结果
  1. 您已签到:2019-02-18
  2. 您已签到:2019-02-18
  3. 本月签到次数:12
  4. 连续签到次数:8
  5. 本月首次签到:2019-02-02
  6. 当月签到情况:
  7. 2019-02-01: -
  8. 2019-02-02:
  9. 2019-02-03:
  10. 2019-02-04: -
  11. 2019-02-05: -
  12. 2019-02-06:
  13. 2019-02-07: -
  14. 2019-02-08: -
  15. 2019-02-09: -
  16. 2019-02-10: -
  17. 2019-02-11:
  18. 2019-02-12:
  19. 2019-02-13:
  20. 2019-02-14:
  21. 2019-02-15:
  22. 2019-02-16:
  23. 2019-02-17:
  24. 2019-02-18:
  25. 2019-02-19: -
  26. 2019-02-20: -
  27. 2019-02-21: -
  28. 2019-02-22: -
  29. 2019-02-23: -
  30. 2019-02-24: -
  31. 2019-02-25: -
  32. 2019-02-26: -
  33. 2019-02-27: -
  34. 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. C++中函数的形参为数组时,实质形参是指针

    C++中函数的形参如果为数组的话,那么进行实参传递时,实参实际上换转化成指针.参考下面的例子: #include<iostream> using namespace std; void f ...

  2. gitlab 10.8.1 迁移

    参考官网: https://docs.gitlab.com/ee/raketasks/backup_restore.html    Backing up and restoring GitLab 及 ...

  3. js实现照片墙效果

    本次要实现的是一个照片墙的效果,如下图,很多图片随机的摆放在窗口中,当点击到某一张的时候,该张图片出现出现在窗口的水平垂直居中的位置. 首先,我们需要简单的结构处理图片,为了方便操作,引用了一个js库 ...

  4. 学习笔记之DevOps

    DevOps - Wikipedia https://en.wikipedia.org/wiki/DevOps DevOps (a clipped compound of "developm ...

  5. 判断浏览器是否IE(IE11可用)

    function isIE() { //ie?         if (!!window.ActiveXObject || "ActiveXObject" in window) { ...

  6. AMQP & JMS对比(转载)

    AMQP & JMS对比 原文地址:https://blog.csdn.net/hpttlook/article/details/23391967 初次接触消息队列时,在网上搜索,总是会提到如 ...

  7. vue 父组件主动获取子组件的数据和方法 子组件主动获取父组件的数据和方法

    Header.vue <template> <div> <h2>我是头部组件</h2> <button @click="getParen ...

  8. DNS污染

    参考链接:http://blog.csdn.net/charleslei/article/details/50117761 DNS污染: DNS污染,又称域名服务器缓存污染(DNS cache pol ...

  9. 发送短信验证码倒计时,CountDownTimer;

    1.声明CountDownTimer的成员变量: private CountDownTimer countDownTimer; 2.设置倒计时总时间和间隔时间: countDownTimer = ne ...

  10. Hive快捷查询:不启用Mapreduce job启用Fetch task

    启用MapReduce Job是会消耗系统开销的.对于这个问题,从Hive0.10.0版本开始,对于简单的不需要聚合的类似SELECT <col> from <table> L ...