Redis List:打造高效消息队列的秘密武器【redis实战 一】
Redis Streams在Spring Boot中的应用:构建可靠的消息队列解决方案【redis实战 二】
Spring Boot和Redis Geo实现附近的人【redis实战 三】
在数字化时代,签到系统已成为许多应用的标准功能,它不仅帮助我们追踪参与度,还激励着用户的日常活动。但如何在保证响应速度和数据准确性的同时处理成千上万的用户请求呢?这正是Spring Boot遇上Redis Bitmap的舞台。通过这篇文章,我们将踏上一个既充满挑战又充满创新的旅程,一起探索如何利用这两个强大工具打造出一个既高效又可靠的签到系统。
Redis Bitmap是一种特殊的数据结构,它使用连续的内存来存储和操作位数据(bit data),每个位可以是0或1。这种结构特别适合于那些需要处理大量布尔值的场景,比如在线状态、签到系统等。Bitmap由于其高效的存储和计算能力,在处理大规模数据时特别有优势。下面是Redis Bitmap的一些基本概念和特性:
SETBIT & GETBIT:
SETBIT
用于在指定偏移量处设置位的值。GETBIT
用于获取指定偏移量处的位的值。BITCOUNT:
BITCOUNT
命令用于计数Bitmap中设置为1的位的数量。BITOP:
BITOP
命令用于对两个或多个Bitmap执行位运算(AND、OR、XOR和NOT)。BITFIELD:
BITFIELD
命令用于对Bitmap执行更复杂的操作,如将位作为整数进行递增。性能:
空间效率:
总结来说,Redis的Bitmap提供了一种高效、灵活的方法来处理大量的位级数据。它在存储效率和性能方面都有显著优势,特别适合于那些需要处理大量布尔值的场景。
使用Redis Bitmap相比于使用MySQL或其他关系型数据库实现签到系统有一系列的优势。这些优势不仅在于性能上的提升,还包括了存储效率、扩展性和特定场景下的操作便利性。下面详细说明这些优势:
SETBIT
、GETBIT
、BITCOUNT
等),使得处理像签到这样的布尔值数据更加方便和高效。在上面提到的实现中,签到系统的基础逻辑是使用Redis的Bitmap来跟踪用户的每日签到状态。这种实现方式利用了Bitmap的高效存储和快速位操作特性,适合处理大量用户和高频率的签到事件。以下是对这个实现的基础逻辑和关键组件的详细介绍:
每日签到:
统计查询:
在实现中,关键的是如何构造用于Bitmap的key。一个好的key设计既能确保访问的效率,又能方便与其他系统(如MySQL中的用户信息)进行交互。
用户标识:
时间信息:
前缀或命名空间:
signin:
。示例Key: signin:userId:2023
用户详细信息:
数据关联:
数据一致性:
通过以上的基础逻辑和key组成策略,你的签到系统将能够高效、灵活地处理用户的签到数据,并且能够方便地与存储在MySQL中的用户详细信息进行交互。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
package fun.bo.service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.util.BitSet;
/**
* @author xiaobo
*/
@Service
public class SignInService {
// 这里可以不写构造方法,直接用@RequiredArgsConstructor实现,可参考<a href="https://blog.csdn.net/Mrxiao_bo/article/details/135113649">一行注解,省却百行代码:深度解析@RequiredArgsConstructor的妙用</>
private final RedisTemplate<String, byte[]> redisTemplate;
public SignInService(RedisTemplate<String, byte[]> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 签到
public void signIn(long userId) {
LocalDate today = LocalDate.now();
int dayOfYear = today.getDayOfYear();
// 使用用户ID和年份作为key,确保每年的数据是独立的
String key = "sign_in:" + userId + ":" + today.getYear();
redisTemplate.opsForValue().setBit(key, dayOfYear, true);
}
// 获取今天签到的人数
public long countSignInToday() {
LocalDate today = LocalDate.now();
return countSignInOnDate(today);
}
// 获取本周签到的人数
public long countSignInThisWeek() {
LocalDate today = LocalDate.now();
// 获取本周的开始和结束日期
LocalDate startOfWeek = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
LocalDate endOfWeek = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
return countSignInBetweenDates(startOfWeek, endOfWeek);
}
// 获取本月签到的人数
public long countSignInThisMonth() {
// 获取本月的开始和结束日期
LocalDate startOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth());
LocalDate endOfMonth = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
return countSignInBetweenDates(startOfMonth, endOfMonth);
}
// 获取特定日期签到的人数
private long countSignInOnDate(LocalDate date) {
int dayOfYear = date.getDayOfYear();
String keyPattern = "sign_in:*:" + date.getYear();
return redisTemplate.keys(keyPattern).stream()
.filter(key -> redisTemplate.opsForValue().getBit(key, dayOfYear))
.count();
}
// 获取日期范围内签到的人数
private long countSignInBetweenDates(LocalDate start, LocalDate end) {
long count = 0;
// 遍历日期范围
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
count += countSignInOnDate(date);
}
return count;
}
// 获取用户当月签到次数
public long countUserSignInThisMonth(long userId) {
LocalDate startOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth());
LocalDate endOfMonth = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
String key = "sign_in:" + userId + ":" + LocalDate.now().getYear();
byte[] bitmap = redisTemplate.opsForValue().get(key);
BitSet bitSet = BitSet.valueOf(bitmap);
long count = 0;
for (int day = startOfMonth.getDayOfYear(); day <= endOfMonth.getDayOfYear(); day++) {
count += bitSet.get(day) ? 1 : 0;
}
return count;
}
}
有坑注意:RedisTemplate<String, byte[]>,这里要留意,如果你的bean中序列化Value的时候用的非字节数组,可能会报错如下
2023-12-25 11:12:55.163 ERROR 56398 --- [nio-7004-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Illegal character ((CTRL-CHAR, code 0)): only regular white space (\r, \n, \t) is allowed between tokens
at [Source: (byte[])"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001"; line: 1, column: 2]; nested exception is com.fasterxml.jackson.core.JsonParseException: Illegal character ((CTRL-CHAR, code 0)): only regular white space (\r, \n, \t) is allowed between tokens
at [Source: (byte[])"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001"; line: 1, column: 2]] with root cause
package fun.bo.controller;
import fun.bo.service.SignInService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SignInController {
private final SignInService signInService;
public SignInController(SignInService signInService) {
this.signInService = signInService;
}
// ... 保留之前的signIn和count方法 ...
@GetMapping("/signin/count/today")
public String countSignInToday() {
long count = signInService.countSignInToday();
return "Total sign-ins today: " + count;
}
@GetMapping("/signin/count/week")
public String countSignInThisWeek() {
long count = signInService.countSignInThisWeek();
return "Total sign-ins this week: " + count;
}
@GetMapping("/signin/count/month")
public String countUserSignInThisMonth(@RequestParam long userId) {
long count = signInService.countUserSignInThisMonth(userId);
return "Total sign-ins this month for user " + userId + ": " + count;
}
@PostMapping("/signin")
public String signIn(@RequestParam("userId") Long userId) {
// 调用签到服务来处理签到
signInService.signIn(userId);
return "User " + userId + " signed in successfully.";
}
}
<!DOCTYPE html>
<html>
<head>
<title>签到系统</title>
<meta charset="UTF-8"> <!-- 指定页面字符集为UTF-8 -->
</head>
<body>
<h1>欢迎来到签到系统</h1>
<div>
<h2>用户签到</h2>
用户ID:<input type="number" id="userIdInput" placeholder="输入用户ID">
<button onclick="signIn()">签到</button>
</div>
<div>
<h2>今日签到人数</h2>
<button onclick="countSignInToday()">查询</button>
<p id="todayCount"></p>
</div>
<div>
<h2>本周签到人数</h2>
<button onclick="countSignInThisWeek()">查询</button>
<p id="weekCount"></p>
</div>
<div>
<h2>本月用户签到次数</h2>
用户ID:<input type="number" id="userId">
<button onclick="countUserSignInThisMonth()">查询</button>
<p id="monthCount"></p>
</div>
<script>
function signIn() {
// 请替换为你的实际用户ID和API端点
let userId = document.getElementById('userIdInput').value;
if (!userId) {
alert("请先输入用户ID!");
return;
}
fetch('/signin?userId=' + userId, { method: 'POST' })
.then(response => alert('签到成功!'))
.catch(error => console.error('Error:', error));
}
function countSignInToday() {
fetch('/signin/count/today')
.then(response => response.text())
.then(data => document.getElementById('todayCount').innerText = data)
.catch(error => console.error('Error:', error));
}
function countSignInThisWeek() {
fetch('/signin/count/week')
.then(response => response.text())
.then(data => document.getElementById('weekCount').innerText = data)
.catch(error => console.error('Error:', error));
}
function countUserSignInThisMonth() {
let userId = document.getElementById('userId').value;
fetch('/signin/count/month?userId=' + userId)
.then(response => response.text())
.then(data => document.getElementById('monthCount').innerText = data)
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>