??作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。
??多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。
??欢迎 ??点赞?评论?收藏
?? SpringBoot 领域知识 ??
链接 | 专栏 |
---|---|
SpringBoot 专业知识学习一 | SpringBoot专栏 |
SpringBoot 专业知识学习二 | SpringBoot专栏 |
SpringBoot 专业知识学习三 | SpringBoot专栏 |
SpringBoot 专业知识学习四 | SpringBoot专栏 |
SpringBoot 专业知识学习五 | SpringBoot专栏 |
SpringBoot 专业知识学习六 | SpringBoot专栏 |
SpringBoot 专业知识学习七 | SpringBoot专栏 |
SpringBoot 专业知识学习八 | SpringBoot专栏 |
SpringBoot 专业知识学习九 | SpringBoot专栏 |
SpringBoot 专业知识学习十 | SpringBoot专栏 |
SpringBoot 专业知识学习十一 | SpringBoot专栏 |
SpringBoot 专业知识学习十二 | SpringBoot专栏 |
SpringBoot 专业知识学习十三 | SpringBoot专栏 |
SpringBoot 专业知识学习十四 | SpringBoot专栏 |
文章目录
-
- ?? Java 注解 @Cacheable 学习(2)
-
- ?? 01. @Cacheable 注解的缓存过期策略有哪些?如何设置?
- ?? 02. @Cacheable 注解的缓存淘汰策略有哪些?如何设置?
- ?? 03. 如果在使用 @Cacheable 注解时,缓存中无数据,如何保证并发请求只有一个真正去查询数据库?
- ?? 04. 如果缓存中的数据被修改,如何更新缓存中的数据?
- ?? 05. 如何手动清空缓存中的数据?
- ?? 06. 如何在使用 @Cacheable 注解时,同时设置多个缓存策略?
- ?? 07. 如何实现一个基于 @Cacheable 注解的缓存管理器?
- ?? 08. @Cacheable 注解适用于哪些场景,哪些场景不适用?
- ?? 09. 如何处理缓存穿透和缓存膨胀的问题?
- ?? 10. 在使用 @Cacheable 注解时,如何优化缓存的命中率?
?? Java 注解 @Cacheable 学习(2)
?? 01. @Cacheable 注解的缓存过期策略有哪些?如何设置?
1.TTL 策略:TTL 机制是基于缓存对象在缓存中的存储时间来设置过期时间的。当缓存对象超过了这个时间,缓存提供商就会清理该缓存对象。使用 TTL 策略时,需要确保缓存提供商支持该机制。
@Cacheable(value = "userCache", key = "#userId", ttl = 3600) public User getUserById(String userId) { // ... // 查询用户信息的具体逻辑 // ... }
在上述示例中,
2.TTI 策略:TTI 机制是基于缓存对象的访问时间来设置过期时间的。当缓存对象在一段时间内没有被访问,缓存提供商就会将其清理。使用 TTI 策略时,需要确保缓存提供商支持该机制。
@Cacheable(value = "userCache", key = "#userId", tti = 3600) public User getUserById(String userId) { // ... // 查询用户信息的具体逻辑 // ... }
在上述示例中,
3.Spring 自带过期策略:Spring 提供了一些内置的过期策略,可以通过
@Cacheable(value = "userCache", key = "#userId", expire = "3600,1800") public User getUserById(String userId) { // ... // 查询用户信息的具体逻辑 // ... }
在上述示例中,
4.静态过期策略:在使用静态过期策略时,缓存对象被缓存后,就不再变化。因此,可以通过在
@Cacheable(value = "userCache", key = "#userId", unless="#result == null", cacheManager="cacheManager1") public User getUserById(String userId) { // ... // 查询用户信息的具体逻辑 // ... }
在上述示例中,我们使用了
5.变化时间策略:变化时间策略是一种动态过期策略,缓存对象的过期时间和变化时间相关。如果缓存对象在规定时间内没有发生变化,就会被强制剔除。
@Cacheable(value = "userCache", key = "#userId", condition= "#age < 30") public User getUserById(String userId, int age) { // ... // 查询用户信息的具体逻辑 // ... }
在上述示例中,我们使用了
以上是常用的缓存过期策略,根据实际情况选择并合理设置缓存过期策略,可以实现缓存的高效使用。同时,需要注意避免缓存数据的膨胀和失效,以避免不必要的资源浪费和程序异常。
?? 02. @Cacheable 注解的缓存淘汰策略有哪些?如何设置?
-
LRU(Least Recently Used):最近最少使用策略,即淘汰最长时间没有被使用的缓存。
-
LFU(Least Frequently Used):最不经常使用策略,即淘汰最不经常使用的缓存。
-
FIFO(First In First Out):先进先出策略,即淘汰最早进入缓存的缓存。
-
Random:随机策略,即随机淘汰一个缓存。
-
Size-based Eviction:基于缓存大小的淘汰策略,即根据缓存项的数量来决定淘汰哪些缓存项。可以通过设置最大缓存项数来限制缓存的大小,并根据需要淘汰最旧或最少使用的缓存项。
-
Time-to-Live (TTL):时间到期淘汰策略,即为缓存项设置一个固定的生存时间,一旦缓存项超过设定的时间就会被淘汰。
-
Time-to-Idle (TTI):空闲时间淘汰策略,即为缓存项设置一个空闲时间阈值,一旦缓存项在设定的时间内没有被访问过,就会被淘汰。
-
Custom Eviction Strategy:自定义淘汰策略,即根据业务需求自定义实现淘汰规则。可以根据一定的逻辑判断来决定哪些缓存项需要被淘汰。
缓存淘汰策略的选择会影响缓存的效果和性能。需要根据实际场景进行选择。例如,如果缓存数据的访问频次是非常有规律的,那么 LFU 可能是比较好的选择;如果数据访问频次没有规律,可以使用 LRU 或 FIFO 等策略来进行数据淘汰。
在 Spring 中,常用的缓存提供商(如 Ehcache 和 Redis)自带了多种缓存淘汰策略,并且还可以进行自定义淘汰策略。在使用 Spring 的 Cache 技术时,可以在注解上使用
@CacheConfig(cacheNames = "users", eviction = CacheEvictionPolicy.LRU) @Service public class UserServiceImpl implements UserService { // ... }
在上述代码中,我们使用了
?? 03. 如果在使用 @Cacheable 注解时,缓存中无数据,如何保证并发请求只有一个真正去查询数据库?
在使用
下面是一个示例代码:
@Cacheable(value = "userCache", key = "#userId") public User getUserById(String userId) { User user = cacheManager.getUserFromCache(userId); if (user == null) { synchronized (this) { user = cacheManager.getUserFromCache(userId); if (user == null) { user = userDao.getUserById(userId); cacheManager.putUserInCache(user); } } } return user; }
在上述示例中,首先检查缓存中是否存在对应的用户数据。如果不存在,使用双重检查锁定的方式进行并发控制。其中,cacheManager 是负责缓存管理的对象,userDao 是负责数据库操作的对象。
使用双重检查锁定的方式可以保证只有一个线程去查询数据库并将结果放入缓存,其他并发请求可以等待该线程完成后直接从缓存中获取数据,提高了性能和并发访问的效率。
需要注意的是,双重检查锁定在一些特定的情况下可能会存在问题,例如在多线程环境下可能会有可见性问题。因此,也可以考虑使用其他并发控制方式,如使用分布式锁或使用缓存中的原子操作来保证并发请求只有一个真正查询数据库。在实际应用中,需要根据具体需求和环境选择合适的方式来保证并发访问的正确性和性能。
?? 04. 如果缓存中的数据被修改,如何更新缓存中的数据?
如果缓存中的数据被修改,需要更新缓存中的数据,可以通过使用 Spring 的
下面是一个示例代码:
@CachePut(value = "userCache", key = "#user.userId") public User updateUser(User user) { // update user in database userDao.updateUser(user); User updatedUser = userDao.getUserById(user.getUserId()); return updatedUser; }
在上述示例中,
在更新用户数据时,首先会将数据更新到数据库中,然后再查询更新后的用户数据。最后返回更新后的用户数据,并将其放入缓存中。这样,下一次访问该用户数据时就可以从缓存中获取到更新后的数据,而不是从数据库中查询。
需要注意的是,当使用
当缓存中的数据被修改时,还可以使用以下方式更新缓存中的数据:
1.手动更新缓存:在数据被修改后,手动更新缓存中对应的数据。可以通过调用缓存管理器的方法,如
2.自动更新缓存:可以通过使用缓存框架提供的自动缓存更新机制,如使用 Spring Cache 中的
下面是一个使用
@CacheEvict(value = "userCache", key = "#user.userId") public void updateUser(User user) { // update user in database userDao.updateUser(user); }
在上述示例中,
需要注意的是,自动更新缓存的方式需要确保在数据修改后能够及时更新缓存,以避免缓存中的数据和数据库中的数据不一致。同时,还需注意不要过度频繁地更新缓存,以避免对系统性能造成负面影响。根据具体业务场景,可以根据数据修改的频率和对数据一致性的要求来选择合适的缓存更新方式。
?? 05. 如何手动清空缓存中的数据?
要手动清空缓存中的数据,可以使用缓存管理器提供的方法来实现。具体的实现方式取决于使用的缓存框架和缓存管理器。
以下是一种常用的手动清空缓存的方法:
1.获取缓存管理器:首先需要获取到缓存管理器的实例,可以通过依赖注入或其他方式获取。例如,在使用 Spring Cache 框架时,可以注入
2.清空缓存:使用缓存管理器的方法来清空缓存中的数据。根据具体的缓存框架和实现,可能有不同的清空缓存的方法。以下是一种常见的通用方法:
- 如果使用的是 Spring Cache 框架,可以使用
CacheManager 的clearAllCaches() 方法来清空所有缓存。例如:cacheManager.clearAllCaches() - 如果使用的是其他缓存框架,可以查阅相应的文档,了解如何清空缓存的方法。
以下是一个示例代码,展示如何手动清空缓存:
@Autowired private CacheManager cacheManager; public void clearCache() { // 清空所有缓存 cacheManager.clearAllCaches(); }
在上述示例中,通过注入的方式获取到
需要注意的是,使用手动清空缓存的方式可能会导致缓存中的所有数据被清空,因此要慎重使用,确保在适当的时机清空缓存。另外,清空缓存可能会对系统性能产生一定的影响,需要根据实际情况进行评估和选择。
?? 06. 如何在使用 @Cacheable 注解时,同时设置多个缓存策略?
在使用
以下是一种实现方式:
1.自定义缓存管理器:创建一个实现了
2.配置缓存管理器:在 Spring 配置文件中配置自定义的缓存管理器。可以通过
3.在使用
下面是一个示例代码,展示如何同时设置多个缓存策略:
@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { // 创建自定义的缓存管理器,例如使用一个内存缓存和一个 Redis 缓存 SimpleCacheManager cacheManager = new SimpleCacheManager(); // 创建内存缓存 Cache cache1 = new ConcurrentMapCache("cache1"); // 创建 Redis 缓存 Cache cache2 = new RedisCache("cache2", /* Redis 配置 */); // 设置缓存策略 cacheManager.setCaches(Arrays.asList(cache1, cache2)); return cacheManager; } }
在上述示例中,使用
然后,可以在使用
@Service public class UserService { @Cacheable(cacheNames = "cache1") public User getUserById(Long userId) { // ... } @Cacheable(cacheNames = "cache2") public User getUserByName(String userName) { // ... } // ... }
在上述示例中,
通过自定义缓存管理器的方式,可以灵活配置和管理多个缓存策略,并在使用
?? 07. 如何实现一个基于 @Cacheable 注解的缓存管理器?
要实现一个基于
1.创建一个自定义缓存管理器类,实现
@Configuration public class CustomCacheManager implements CacheManager { @Override public Cache getCache(String name) { return new CustomCache(name); } @Override public Collection<String> getCacheNames() { // 返回所有缓存的名称 return Arrays.asList("cache1", "cache2"); } }
在上述示例中,自定义了一个
2.创建一个自定义缓存类,实现
public class CustomCache implements Cache { private final String name; // 使用一个 Map 作为缓存存储 private final Map<Object, Object> cache = new ConcurrentHashMap<>(); public CustomCache(String name) { this.name = name; } @Override public String getName() { return name; } @Override public Object getNativeCache() { return cache; } @Override public ValueWrapper get(Object key) { Object value = cache.get(key); return (value != null ? new SimpleValueWrapper(value) : null); } @Override public void put(Object key, Object value) { cache.put(key, value); } @Override public void evict(Object key) { cache.remove(key); } @Override public void clear() { cache.clear(); } }
在上述示例中,自定义了一个
3.在 Spring 配置文件中配置自定义缓存管理器:
@Configuration @EnableCaching public class CacheConfig { // 将自定义的缓存管理器注入到 Spring 容器中 @Bean public CustomCacheManager cacheManager() { return new CustomCacheManager(); } }
在上述示例中,使用了
最后,可以在需要使用缓存的方法上使用
@Service public class UserService { @Cacheable(cacheNames = "cache1") public User getUserById(Long userId) { // ... } @Cacheable(cacheNames = "cache2") public User getUserByName(String userName) { // ... } // ... }
在上述示例中,
通过实现自定义的缓存管理器和缓存对象,并使用
?? 08. @Cacheable 注解适用于哪些场景,哪些场景不适用?
@Cacheable 注解适用于需要缓存方法的返回值的场景。它可以在方法执行前先检查缓存中是否存在相同输入参数的结果,如果存在则直接返回缓存中的结果,而不执行方法的实际逻辑。这样可以提高系统的性能和响应速度。
适用场景:
- 频繁查询数据库或其他耗时操作,但是结果不经常变化的方法。可以使用缓存来避免重复查询。
- 需要对方法的结果进行缓存,并且希望能够根据方法的输入参数来作为缓存的 key。
不适用场景:
- 方法的返回结果经常变化,不适合缓存。例如,计算方法返回的结果会不断变化,每次调用都需要重新计算。
- 对于写操作(如新增、更新、删除),由于缓存的一致性问题,不适合使用缓存。因为写操作可能会修改缓存中的数据,导致缓存数据和数据库中的数据不一致。
需要注意的是,使用 @Cacheable 注解需要谨慎考虑缓存的一致性和过期策略,以及缓存的空间管理。不合理使用缓存可能导致缓存过期不及时,或者缓存空间占满等问题。
?? 09. 如何处理缓存穿透和缓存膨胀的问题?
缓存穿透问题指的是缓存中查不到数据,导致每次请求都需要去数据库查找,从而增加数据库的压力;缓存膨胀问题指的是缓存中数据过多,占用了太多的空间,从而导致系统的性能问题。下面是处理这两个问题的一些方法:
1.缓存穿透
- 前置校验:在查询数据前,先进行参数的校验,若参数不符合要求,则直接返回错误结果。
- 布隆过滤器:使用布隆过滤器对请求参数进行过滤,将在缓存中不存在的参数拦截掉,从而减轻了数据库的访问压力。
- 热点数据预加载:将缓存中的热点数据提前加载到缓存中,从而避免因为缓存失效而导致的缓存穿透问题。
2.缓存膨胀
- 设置过期时间:设置缓存数据的过期时间,避免缓存数据一直存在而导致缓存膨胀问题。
- 缓存清理策略:在缓存中设置合适的清理策略,如 LRU (Least Recently Used 最近最少使用), LFU (Least Frequently Used 最不经常使用) 等,来清理不常用的数据。
- 分布式缓存:使用分布式缓存来分散缓存的存储,从而减轻单个缓存节点的压力,即使缓存膨胀,也不会对整个系统造成太大的影响。
以上是几种较常见的处理缓存穿透和缓存膨胀问题的方法,但是不同的场景可能需要使用不同的方法。因此,在实际场景中需要根据具体情况采用合适的方法来进行处理。
?? 10. 在使用 @Cacheable 注解时,如何优化缓存的命中率?
要优化缓存的命中率,可以考虑以下几点:
1.精确设置缓存的 key:确保生成缓存 key 的算法准确无误,避免相同参数得到不同的缓存 key。可以使用参数的组合作为缓存 key,确保每个请求的唯一性。
2.缓存热点数据:根据业务需求,将最常被访问的数据预先加载到缓存中,提前设置好合适的过期时间,以充分利用缓存资源,提高缓存命中率。
3.考虑缓存的淘汰策略:根据业务需求和数据特点,选择合适的缓存淘汰策略,例如 Least Recently Used (LRU)、Least Frequently Used (LFU) 等,将不常访问的数据及时从缓存中清理掉,防止缓存的膨胀。
4.设置适当的缓存过期时间:根据数据的变化频率和业务特点,合理设置缓存的过期时间,确保缓存命中时数据仍然是有效的。过长的过期时间可能导致缓存中的数据已过时,过短的过期时间可能增加缓存失效导致的数据库负载。
5.缓存穿透预防:通过参数校验、布隆过滤器等方式,拦截可能产生缓存穿透的请求,避免无效请求落到数据库上。
6.合理处理异常情况:当从缓存获取数据失败时,可以考虑补偿措施,如从数据库中获取,并将数据添加到缓存中,以提高缓存的命中率。
综上所述,通过合理设置缓存的 key,缓存热点数据,选择合适的淘汰策略和过期时间,以及预防缓存穿透,能够有效提高缓存的命中率,减轻后端系统的负载。需根据具体业务场景和数据特点来选择合适的优化方法。