缓存注解使用案例
关于缓存注解的常规使用案例,我觉得本文没有必要介绍。
接下来主要讲解一些特殊的使用:
若方法返回值为null,还会缓存吗?
比如上例返回值改为null:
@Service public class CacheDemoServiceImpl implements CacheDemoService { @Cacheable(cacheNames = "demoCache", key = "#id") @Override public Object getFromDB(Integer id) { System.out.println("模拟去db查询~~~" + id); //return "hello cache..."; return null; } }
执行单测:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class, CacheConfig.class}) public class TestSpringBean { @Autowired private CacheDemoService cacheDemoService; @Autowired private CacheManager cacheManager; @Test public void test1() { cacheDemoService.getFromDB(1); cacheDemoService.getFromDB(1); System.out.println("----------验证缓存是否生效----------"); Cache cache = cacheManager.getCache("demoCache"); System.out.println(cache); System.out.println(cache.get(1, String.class)); } }
结果打印:
模拟去db查询~~~1 ----------验证缓存是否生效---------- org.springframework.cache.concurrent.ConcurrentMapCache@15f2eda3 null
结论是:默认情况下,返回值是null也是会缓存的(第二次过来就不会再查询了)。通过一个断点会更清晰:
但假如修改缓存注解如下:
// 注意:unless 是非的意思 @Cacheable(cacheNames = "demoCache", key = "#id",unless = "#result == null")
运行打印如下:
模拟去db查询~~~1 模拟去db查询~~~1 ----------验证缓存是否生效---------- org.springframework.cache.concurrent.ConcurrentMapCache@6a282fdd null
查了两次DB,证明此时返回null就不会再缓存了,unless生效。
倘若修改配置如下:
@Bean public CacheManager cacheManager() { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); cacheManager.setAllowNullValues(false); return cacheManager; }
运行则报错:
java.lang.IllegalArgumentException: Cache 'demoCache' is configured to not allow null values but null was provided
一般情况下,不建议这么设置,因为一般都是允许缓存null值的。
@Cacheable注解sync=true的效果
在多线程环境下,某些操作可能使用相同参数同步调用(相同的key)。默认情况下,缓存不锁定任何资源,可能导致多次计算,而违反了缓存的目的。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中(Spring4.3提供的)。
下面给个例子直接看看效果就成:
@Cacheable(cacheNames = "demoCache", key = "#id")
测试Demo(多线程):
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class, CacheConfig.class}) public class TestSpringBean { @Autowired private CacheDemoService cacheDemoService; @Test public void test1() throws InterruptedException { for (int i = 0; i < 10; i++) { new Thread(() -> { cacheDemoService.getFromDB(1); }).start(); } // 保证main线程不要这么快结束(否则没有日志结果的~~~) TimeUnit.SECONDS.sleep(10); } }
打印结果:
模拟去db查询~~~1 模拟去db查询~~~1 模拟去db查询~~~1 模拟去db查询~~~1 模拟去db查询~~~1
打印可能5次、可能6次不确定。但是若我们sync=true了呢?
@Cacheable(cacheNames = "demoCache", key = "#id", sync = true)
再次运行测试打印结果如下:
模拟去db查询~~~1
永远只会打印一次。 所以在高并发环境下,我个人是十分建议开启此同步开关的,至于非高并发,无所谓啦~
注解可重复标注吗?
因为源码都分析了,所以此处看结论即可。
@CachePut(cacheNames = "demoCache", key = "#id") // 不同的注解可以标注多个 //@Cacheable(cacheNames = "demoCache", key = "#id") // 相同注解标注两个是不行的 因为它并不是@Repeatable的 @Cacheable(cacheNames = "demoCache", key = "#id") @Override public Object getFromDB(Integer id) { System.out.println("模拟去db查询~~~" + id); return "hello cache..."; }
不同的注解可以标注多个,且都能生效。若需要相同注解标注多个等更复杂的场景,推荐使用@Caching
注解
如何
给缓存注解设置专用的key生成器、缓存管理器等等
标准写法是这样的:
@EnableCaching @Configuration public class CacheConfig implements CachingConfigurer { @Override public CacheResolver cacheResolver() { return null; } @Override public KeyGenerator keyGenerator() { return null; } @Override public CacheErrorHandler errorHandler() { return null; } }
实现对应的方法,可以new一个对象返回,也可以用容器内的。
大多数情况下我们都不需要特别的指定缓存注解使用的管理器,因为它自己会去容器里找。 但是,但是,但是当你使用了多套缓存时,我还是建议显示的指定的。
总结
本篇文章相对较长,因为从原理处深度分析了Spring Cache的执行过程,希望能帮助到大家做到心里有数,从而更加健康的书写代码和扩展功能。
关于缓存注解这块,我相信很多人都有一个痛点:注解并不支持设置过期时间。其实我想说,如果你看了上篇文章就知道,这个Spring它做不了,因为它并没有对Expire进行抽象。
但是Spring做不了不代表我们自己做不了,因此有兴趣的同学可以在此基础上,扩展出可以自定义超时时间的能力~~~