
spring boot通过其核心框架的 cache abstraction 提供了声明式缓存支持,通常通过 @cacheable 等注解实现。然而,对于“先检查缓存中部分数据,再从数据库获取缺失数据”这种细粒度需求,spring开箱即用的 @cacheable 机制存在一些局限性。
Spring的缓存抽象机制本质上是对昂贵的服务方法或数据访问调用进行装饰(通过AOP)。这意味着,一个被 @Cacheable 注解的方法,其行为是“全有或全无”的:
默认情况下,Spring Cache Abstraction使用方法的所有参数来生成缓存键。例如,对于 findByIds(Set<Integer> ids) 方法:
@Cacheable("Students")
List<Student> findByIds(Set<Integer> ids) {
  // ...
  return repository.findByIds(ids);
}此时,缓存中存储的将是:
缓存键 | 缓存值 ----------------|-------------- Set<Integer> | List<Student>
这意味着整个 Set<Integer> 将作为单个键,对应的缓存值是整个 List<Student>。这与我们期望的按单个 Student ID进行缓存(例如:ID 1 -> Student A,ID 2 -> Student B)的粒度不符。虽然可以自定义键生成策略,但对于这种“批查询、单缓存”的需求,默认注解仍然难以直接实现。
Spring的 org.springframework.cache.Cache 接口是对底层缓存提供商(如EhCache, Redis, Hazelcast等)的适配器。该接口主要提供单键的 get、put、evict 等操作。这意味着,即使我们想手动查询缓存,也需要对每个ID进行单独的 get 调用:
Cache cache = cacheManager.getCache("Students");
Student student = cache.get(id, Student.class); // 逐个获取对于需要查询大量ID的场景,这种逐个访问的方式可能会导致性能问题,尤其是在分布式缓存环境中。虽然可以通过 Cache.getNativeCache() 获取底层缓存提供商的原生API(例如Hazelcast的 IMap.getAll(:Set<K>)),但这会引入与特定缓存提供商的强耦合,降低代码的可移植性。
鉴于Spring Cache Abstraction的上述局限性,要实现先验证缓存中现有数据,再查询数据库获取缺失数据,并缓存新结果的需求,通常需要手动管理缓存逻辑。以下是一个详细的实现示例:
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class StudentService {
    private final StudentRepository studentRepository;
    private final Cache studentsCache; // 直接获取特定缓存实例
    public StudentService(StudentRepository studentRepository, CacheManager cacheManager) {
        this.studentRepository = studentRepository;
        this.studentsCache = cacheManager.getCache("Students"); // 假设缓存名为"Students"
    }
    /**
     * 根据给定的学生ID集合,从缓存或数据库中获取学生信息。
     * 优先从缓存获取,缺失部分再从数据库查询并更新缓存。
     *
     * @param studentIds 待查询的学生ID集合
     * @return 匹配的学生列表
     */
    public List<Student> findStudentsByIds(Set<Integer> studentIds) {
        List<Student> result = new ArrayList<>();
        Set<Integer> idsToQueryDb = new HashSet<>(studentIds); // 存储需要从数据库查询的ID
        // 1. 尝试从缓存中加载学生数据
        // 注意:这里是逐个ID查询缓存,可能存在性能瓶颈
        for (Integer id : studentIds) {
            Cache.ValueWrapper valueWrapper = studentsCache.get(id);
            if (valueWrapper != null) {
                Object cachedObject = valueWrapper.get();
                if (cachedObject instanceof Student) {
                    result.add((Student) cachedObject);
                    idsToQueryDb.remove(id); // 从待查询数据库的ID集合中移除已缓存的ID
                }
            }
        }
        // 2. 查询数据库获取缓存中缺失的学生数据
        if (!idsToQueryDb.isEmpty()) {
            List<Student> studentsFromDb = studentRepository.findByIdIn(idsToQueryDb);
            // 3. 将从数据库获取的数据存入缓存,并添加到结果列表
            for (Student student : studentsFromDb) {
                studentsCache.put(student.getId(), student); // 缓存单个学生
                result.add(student);
            }
        }
        // 确保返回的结果只包含请求的ID,并且顺序不固定
        // 如果需要特定顺序,可能需要额外处理,例如按原始 studentIds 排序
        return result.stream()
                .filter(s -> studentIds.contains(s.getId())) // 过滤掉可能存在的意外数据
                .collect(Collectors.toList());
    }
    // 示例:StudentRepository 接口
    public interface StudentRepository {
        List<Student> findByIdIn(Set<Integer> ids);
    }
    // 示例:Student 实体类
    public static class Student {
        private int id;
        private String name;
        public Student(int id, String name) {
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return id == student.id;
        }
        @Override
        public int hashCode() {
            return Objects.hash(id);
        }
    }
}// 示例:使用Hazelcast原生API进行批量获取
// 假设studentsCache的底层实现是HazelcastCache
if (studentsCache.getNativeCache() instanceof IMap) {
    IMap<Integer, Student> nativeMap = (IMap<Integer, Student>) studentsCache.getNativeCache();
    Map<Integer, Student> cachedStudentsMap = nativeMap.getAll(studentIds); // 批量获取
    // ... 然后处理 cachedStudentsMap
}但请注意,这种方式会牺牲缓存抽象带来的灵活性和可移植性。
Spring Cache Abstraction 的 @Cacheable 注解适用于“方法级别”的整体缓存,即要么整个方法的输入参数对应的结果在缓存中,要么整个方法执行并缓存结果。它不直接支持在方法内部进行细粒度的“部分缓存命中,部分数据库查询”逻辑。
要实现这种复杂需求,开发者需要:
在实现过程中,应权衡代码的复杂性、性能需求以及与特定缓存提供商的耦合程度。对于高性能要求的场景,可以考虑利用底层缓存提供商的批量操作API,但需注意可能牺牲的抽象优势。
以上就是Spring Boot缓存中数据验证与缺失键处理策略的详细内容,更多请关注php中文网其它相关文章!
                        
                        每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
                Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号