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

末蓝、 2022-03-12 09:18 312阅读 0赞

更多Redis学习请访问 www.itkc8.com

场景需求

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

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

设计思路

对于用户签到数据,如果每条数据都用K/V的方式存储,当用户量大的时候内存开销是非常大的。而位图(BitMap)是由一组bit位组成的,每个bit位对应0和1两个状态,虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作位图,可以把它看作是一个bit数组,数组的下标就是偏移量。它的优点是内存开销小、效率高且操作简单,很适合用于签到这类场景。

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

  • SETBIT
  • GETBIT
  • BITCOUNT
  • BITPOS
  • BITOP
  • BITFIELD

考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,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. * 6. 获取当月签到情况
  18. */
  19. public class UserSignDemo {
  20. private Jedis jedis = new Jedis();
  21. /**
  22. * 用户签到
  23. *
  24. * @param uid 用户ID
  25. * @param date 日期
  26. * @return 之前的签到状态
  27. */
  28. public boolean doSign(int uid, LocalDate date) {
  29. int offset = date.getDayOfMonth() - 1;
  30. return jedis.setbit(buildSignKey(uid, date), offset, true);
  31. }
  32. /**
  33. * 检查用户是否签到
  34. *
  35. * @param uid 用户ID
  36. * @param date 日期
  37. * @return 当前的签到状态
  38. */
  39. public boolean checkSign(int uid, LocalDate date) {
  40. int offset = date.getDayOfMonth() - 1;
  41. return jedis.getbit(buildSignKey(uid, date), offset);
  42. }
  43. /**
  44. * 获取用户签到次数
  45. *
  46. * @param uid 用户ID
  47. * @param date 日期
  48. * @return 当前的签到次数
  49. */
  50. public long getSignCount(int uid, LocalDate date) {
  51. return jedis.bitcount(buildSignKey(uid, date));
  52. }
  53. /**
  54. * 获取当月连续签到次数
  55. *
  56. * @param uid 用户ID
  57. * @param date 日期
  58. * @return 当月连续签到次数
  59. */
  60. public long getContinuousSignCount(int uid, LocalDate date) {
  61. int signCount = 0;
  62. String type = String.format("u%d", date.getDayOfMonth());
  63. List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
  64. if (list != null && list.size() > 0) {
  65. // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
  66. long v = list.get(0) == null ? 0 : list.get(0);
  67. for (int i = 0; i < date.getDayOfMonth(); i++) {
  68. if (v >> 1 << 1 == v) {
  69. // 低位为0且非当天说明连续签到中断了
  70. if (i > 0) break;
  71. } else {
  72. signCount += 1;
  73. }
  74. v >>= 1;
  75. }
  76. }
  77. return signCount;
  78. }
  79. /**
  80. * 获取当月首次签到日期
  81. *
  82. * @param uid 用户ID
  83. * @param date 日期
  84. * @return 首次签到日期
  85. */
  86. public LocalDate getFirstSignDate(int uid, LocalDate date) {
  87. long pos = jedis.bitpos(buildSignKey(uid, date), true);
  88. return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
  89. }
  90. /**
  91. * 获取当月签到情况
  92. *
  93. * @param uid 用户ID
  94. * @param date 日期
  95. * @return Key为签到日期,Value为签到状态的Map
  96. */
  97. public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
  98. Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
  99. String type = String.format("u%d", date.lengthOfMonth());
  100. List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
  101. if (list != null && list.size() > 0) {
  102. // 由低位到高位,为0表示未签,为1表示已签
  103. long v = list.get(0) == null ? 0 : list.get(0);
  104. for (int i = date.lengthOfMonth(); i > 0; i--) {
  105. LocalDate d = date.withDayOfMonth(i);
  106. signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
  107. v >>= 1;
  108. }
  109. }
  110. return signMap;
  111. }
  112. private static String formatDate(LocalDate date) {
  113. return formatDate(date, "yyyyMM");
  114. }
  115. private static String formatDate(LocalDate date, String pattern) {
  116. return date.format(DateTimeFormatter.ofPattern(pattern));
  117. }
  118. private static String buildSignKey(int uid, LocalDate date) {
  119. return String.format("u:sign:%d:%s", uid, formatDate(date));
  120. }
  121. public static void main(String[] args) {
  122. UserSignDemo demo = new UserSignDemo();
  123. LocalDate today = LocalDate.now();
  124. { // doSign
  125. boolean signed = demo.doSign(1000, today);
  126. if (signed) {
  127. System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
  128. } else {
  129. System.out.println("签到完成:" + formatDate(today, "yyyy-MM-dd"));
  130. }
  131. }
  132. { // checkSign
  133. boolean signed = demo.checkSign(1000, today);
  134. if (signed) {
  135. System.out.println("您已签到:" + formatDate(today, "yyyy-MM-dd"));
  136. } else {
  137. System.out.println("尚未签到:" + formatDate(today, "yyyy-MM-dd"));
  138. }
  139. }
  140. { // getSignCount
  141. long count = demo.getSignCount(1000, today);
  142. System.out.println("本月签到次数:" + count);
  143. }
  144. { // getContinuousSignCount
  145. long count = demo.getContinuousSignCount(1000, today);
  146. System.out.println("连续签到次数:" + count);
  147. }
  148. { // getFirstSignDate
  149. LocalDate date = demo.getFirstSignDate(1000, today);
  150. System.out.println("本月首次签到:" + formatDate(date, "yyyy-MM-dd"));
  151. }
  152. { // getSignInfo
  153. System.out.println("当月签到情况:");
  154. Map<String, Boolean> signInfo = new TreeMap<>(demo.getSignInfo(1000, today));
  155. for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) {
  156. System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
  157. }
  158. }
  159. }
  160. }

运行结果

  1. 您已签到:2019-02-18
  2. 您已签到:2019-02-18
  3. 本月签到次数:11
  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: -

参考链接

  • http://redisdoc.com/

更多Redis学习请访问 www.itkc8.com

发表评论

表情:
评论列表 (有 0 条评论,312人围观)

还没有评论,来说两句吧...

相关阅读

    相关 使用redis bitmap实现签到功能

    1.签到功能的实现思路 最近有研究到用户的签到功能,对功能进行设计的时候想到使用msyql存储用户的签到记录,将用户的每日签到记录存储到表中,然后又想到每次签到就往表里面

    相关 利用redis实现每日签到功能

    今天给大家介绍一个简单的应用场景,我们迷你喵小程序最近新增了一个签到功能,但是每天只能签到一次,我们如何实现每日只签到一次呢? 想学习分布式、微服务、JVM、多线程、架构、j