You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring Boot中实现支持类RDBMS查询的Redis分布式缓存

实现Redis支持类RDBMS范围查询的分布式二级缓存方案

首先得明确:Redis本身是键值型存储,原生并不支持SQL式的语法查询,但结合它的数据结构和扩展模块,完全可以实现你需要的id < 10这类范围查询,同时解决K8s多实例下的缓存同步问题。下面结合你的场景给出具体方案和代码示例:

方案一:基于Redis有序集合(Sorted Set)实现轻量范围查询

这个方案不需要额外安装Redis模块,利用Sorted Set的score特性就能轻松实现数值范围过滤,适合你的id范围查询需求。

核心思路

  1. 缓存结构设计:
    • 用Sorted Set存储学生ID的范围映射:键为students:sorted,score设为学生ID,value直接存学生ID(或者序列化后的Student对象,推荐存ID更高效)。
    • 用Hash存储单个学生的完整数据:键为students:hash,子键为student:{id},值为序列化后的Student对象。
  2. 数据同步逻辑:
    • 服务启动时:从DB加载所有学生数据,批量写入Sorted Set和Hash。
    • 更新/删除时:先操作DB,再同步更新Redis的Hash和Sorted Set(删除时要从两个结构中都移除对应数据)。
  3. 范围查询实现:利用Sorted Set的ZRANGEBYSCORE命令查询指定范围内的ID,再从Hash中批量获取完整数据。

代码示例(Spring Boot + RedisTemplate)

1. 启动时加载缓存

@Service
public class StudentCacheService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final StudentRepository studentRepository;

    public StudentCacheService(RedisTemplate<String, Object> redisTemplate, StudentRepository studentRepository) {
        this.redisTemplate = redisTemplate;
        this.studentRepository = studentRepository;
    }

    @PostConstruct
    public void loadCacheOnStartup() {
        List<Student> allStudents = studentRepository.findAll();
        
        // 批量写入Sorted Set(score=学生ID,value=学生ID)
        Set<ZSetOperations.TypedTuple<Object>> idTuples = allStudents.stream()
                .map(student -> new DefaultTypedTuple<>(student.getId(), student.getId().doubleValue()))
                .collect(Collectors.toSet());
        redisTemplate.opsForZSet().addAll("students:sorted", idTuples);
        
        // 批量写入Hash
        Map<String, Object> studentMap = allStudents.stream()
                .collect(Collectors.toMap(student -> "student:" + student.getId(), Function.identity()));
        redisTemplate.opsForHash().putAll("students:hash", studentMap);
    }
}

2. 范围查询(id ≤ 5)

public List<Student> getStudentsWithIdLessThanEqual(Long maxId) {
    // 从Sorted Set获取指定范围的ID
    Set<Object> studentIds = redisTemplate.opsForZSet().rangeByScore("students:sorted", 0, maxId);
    if (studentIds.isEmpty()) {
        return Collections.emptyList();
    }
    
    // 从Hash批量获取学生数据
    List<String> hashKeys = studentIds.stream()
            .map(id -> "student:" + id)
            .collect(Collectors.toList());
    List<Object> studentObjects = redisTemplate.opsForHash().multiGet("students:hash", hashKeys);
    
    return studentObjects.stream()
            .filter(Objects::nonNull)
            .map(obj -> (Student) obj)
            .collect(Collectors.toList());
}

3. 更新缓存同步

public void updateStudent(Student updatedStudent) {
    // 1. 先更新数据库
    studentRepository.save(updatedStudent);
    
    // 2. 更新Hash中的数据
    redisTemplate.opsForHash().put("students:hash", "student:" + updatedStudent.getId(), updatedStudent);
    
    // 3. 更新Sorted Set:先移除旧条目,再添加新条目(如果ID不变,其实可以跳过,但保险起见还是做)
    redisTemplate.opsForZSet().remove("students:sorted", updatedStudent.getId());
    redisTemplate.opsForZSet().add("students:sorted", updatedStudent.getId(), updatedStudent.getId().doubleValue());
}

方案二:RedisJSON + RediSearch 实现类SQL复杂查询

如果未来需要更复杂的查询(比如多条件过滤、模糊查询),可以用Redis的两个扩展模块:RedisJSON(存储JSON格式数据)和RediSearch(提供全文检索与结构化查询能力),这组合能让你实现近乎SQL的查询体验。

核心思路

  1. 数据存储:用RedisJSON将每个学生以JSON格式存储,键为student:{id}。
  2. 创建索引:用RediSearch为学生的id、name等字段创建索引。
  3. 查询实现:通过RediSearch的FT.SEARCH命令执行类似WHERE id <= 5的查询。

代码示例(Lettuce客户端)

1. 创建索引

public void createStudentIndex() {
    RedisCommands<String, String> syncCommands = redisConnection.sync();
    // 创建针对JSON数据的索引,指定前缀和要索引的字段
    syncCommands.ftCreate("idx:students",
            FTCreateParams.create().on(IndexDataType.JSON).prefix("student:"),
            Field.numeric("$.id").as("id"),
            Field.text("$.name").as("name")
    );
}

2. 范围查询

public List<Student> getStudentsWithIdLessThanEqual(Long maxId) throws JsonProcessingException {
    RedisCommands<String, String> syncCommands = redisConnection.sync();
    // 执行范围查询,语法类似SQL
    SearchResult result = syncCommands.ftSearch("idx:students", "@id:[0 " + maxId + "]");
    
    ObjectMapper objectMapper = new ObjectMapper();
    List<Student> students = new ArrayList<>();
    for (Document doc : result.getDocuments()) {
        String studentJson = doc.get("$");
        students.add(objectMapper.readValue(studentJson, Student.class));
    }
    return students;
}

K8s多实例缓存同步方案

因为你用K8s部署了多个Service A实例,修改数据后需要确保所有实例的缓存一致性,这里推荐用Redis Pub/Sub做事件通知:

  1. 当某个实例更新/删除缓存后,向Redis的student:change频道发布事件(比如携带学生ID)。
  2. 所有Service A实例订阅这个频道,收到事件后清除本地一级缓存(如果用了Caffeine等本地缓存),或者直接依赖Redis的分布式缓存一致性(因为Redis本身是单源,所以只要同步更新Redis,其他实例查询时就能拿到最新数据)。

订阅事件示例

@Component
public class StudentCacheSubscriber implements MessageListener {
    private final CacheManager cacheManager;
    private final RedisTemplate<String, Object> redisTemplate;

    public StudentCacheSubscriber(CacheManager cacheManager, RedisTemplate<String, Object> redisTemplate) {
        this.cacheManager = cacheManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        Long studentId = (Long) redisTemplate.getValueSerializer().deserialize(message.getBody());
        // 清除本地缓存中该学生的条目
        Cache studentCache = cacheManager.getCache("students");
        if (studentCache != null) {
            studentCache.evict(studentId);
        }
    }

    @Bean
    public RedisMessageListenerContainer messageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(this, new ChannelTopic("student:change"));
        return container;
    }
}

方案选择建议

  • 如果只需要简单的数值范围查询,方案一足够轻量,不需要额外依赖,维护成本低。
  • 如果未来有复杂查询需求,方案二更灵活,但需要在Redis服务器上安装RedisJSON和RediSearch模块。

内容的提问来源于stack exchange,提问作者Ganesh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:11:16