Java Spring Boot 基于Redis存储学生列表并实现多条件过滤查询
Redis存储自定义Student列表并实现多条件检索的Java实现
选型逻辑
因为检索条件包含精确匹配课程ID、ID范围查询、报名日期范围查询三类要求,不能采用将全量Student列表序列化后单key存储的方案——数据量稍大就会出现全量拉取、反序列化的性能损耗。实际落地时搭配Redis原生Hash存储全量实体、ZSet做维度索引的结构,即可在不引入额外组件的前提下完成需求。
实体类定义
直接匹配给出的Student结构,注意日期类型的序列化处理:
import java.util.Date; public class Student { private int id; private int course_id; private Date addmisson_date; private String name; // 省略全参构造、getter、setter方法 }
存储结构设计
所有Key统一采用固定前缀做隔离,避免和其他业务数据冲突:
- 学生全量信息:
student:info:{studentId}类型为Hash,逐字段存储学生的id、course_id、报名日期时间戳、姓名 - 课程维度ID索引:
student:index:course:{courseId}类型为ZSet,score存学生id(整数原生适配范围查询),value存学生id - 课程维度报名日期索引:
student:index:course:{courseId}:date类型为ZSet,score存报名日期的毫秒时间戳(适配时间范围查询),value存学生id
代码实现
列表存储逻辑
遍历传入的Student列表,逐次写入全量Hash数据,同步更新两个维度的ZSet索引:
import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import java.util.List; public void saveStudentData(List<Student> studentList, RedisTemplate<String, Object> redisTemplate) { HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet(); for (Student student : studentList) { // 写入学生全量Hash数据 String infoKey = String.format("student:info:%d", student.getId()); hashOps.put(infoKey, "id", student.getId()); hashOps.put(infoKey, "course_id", student.getCourse_id()); hashOps.put(infoKey, "addmisson_date", student.getAddmisson_date().getTime()); hashOps.put(infoKey, "name", student.getName()); // 更新课程下的ID范围索引 String courseIdIndexKey = String.format("student:index:course:%d", student.getCourse_id()); zSetOps.add(courseIdIndexKey, student.getId(), student.getId()); // 更新课程下的报名日期范围索引 String courseDateIndexKey = String.format("student:index:course:%d:date", student.getCourse_id()); zSetOps.add(courseDateIndexKey, student.getId(), student.getAddmisson_date().getTime()); } }
多条件检索逻辑
检索入参为:指定课程ID、ID区间上下限、报名日期区间上下限,流程为按维度过滤取交集、拉取匹配学生的全量信息:
import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; public List<Student> queryMatchedStudents(int targetCourseId, int idMin, int idMax, Date dateStart, Date dateEnd, RedisTemplate<String, Object> redisTemplate) { HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet(); List<Student> result = new ArrayList<>(); // 第一步:按ID范围过滤匹配的学生 String courseIdIndexKey = String.format("student:index:course:%d", targetCourseId); Set<Object> idRangeMatched = zSetOps.rangeByScore(courseIdIndexKey, idMin, idMax); // 第二步:按报名日期范围过滤匹配的学生 String courseDateIndexKey = String.format("student:index:course:%d:date", targetCourseId); Set<Object> dateRangeMatched = zSetOps.rangeByScore(courseDateIndexKey, dateStart.getTime(), dateEnd.getTime()); // 取两个集合的交集,得到同时满足所有条件的学生ID idRangeMatched.retainAll(dateRangeMatched); // 遍历交集ID,拉取全量学生信息组装返回 for (Object idObj : idRangeMatched) { int studentId = (int) idObj; String infoKey = String.format("student:info:%d", studentId); Map<Object, Object> studentInfo = hashOps.entries(infoKey); Student student = new Student(); student.setId((Integer) studentInfo.get("id")); student.setCourse_id((Integer) studentInfo.get("course_id")); student.setAddmisson_date(new Date((Long) studentInfo.get("addmisson_date"))); student.setName((String) studentInfo.get("name")); result.add(student); } return result; }
落地注意事项
- 别图省事把整个Student列表序列化成存在单个key的JSON里,只要数据量破万,每次检索都要拉全量反序列化,性能会掉的非常明显
- 单课程下学生规模超过10万时,可以切换为Redis Search的结构化文档索引替代自定义ZSet索引,减少手动维护集合交集的内存开销
- 日期字段统一存为毫秒时间戳作为ZSet的score,别直接存格式化字符串,不然范围查询会完全失效
- 检索过程中如果生成了临时的交集中间集合,记得设置10秒以内的过期时间,别让无意义的结果占Redis内存
内容的提问来源于stack exchange,提问作者THE ANKIT VERMA
相关产品推荐
相关产品推荐

