- A+
1. 功能说明
除了填充缓存,spring cache 也支持使用 @CacheEvict 来删除缓存。@CacheEvict 就是一个触发器,在每次调用被它注解的方法时,就会触发删除它指定的缓存的动作。跟 @Cacheable 和 @CachePut 一样,@CacheEvict 也要求指定一个或多个缓存,也指定自定义一的缓存解析器和 key 生成器,也支持指定条件(condition 参数)。
关于这些(value、cacheNames、key、keyGenerator、cacheManager、cacheResolver、condition)属性参数的用法和说明,请参考:
spring cache 学习 —— @Cacheable 使用详解
2. 例子
我们来举个简单的例子,首先看一下 mysql 数据,我们想要删除 name=“微服务测试0修改一下试试” 这条数据(逻辑删除)
这条数据在 redis 中已经存在了
接下来我们编写一个删除方法,并使用 @CacheEvict 注解:
@Override @CacheEvict(value = "menuById", key = "#id") public Boolean deleteById(String id) { return this.removeById(id); }
然后在 controller 调用它:
@DeleteMapping("/deleteById/{id}") public Boolean deleteById(@PathVariable("id")String id){ return menuService.deleteById(id); }
接下来,我们请求一下接口,看看结果是否删除成功的:
再看看 mysq 数据的 del 字段和 redis 是否还有数据:
可以看到,mysql 和 缓存都删除成功了。
3. allEntries 参数
allEntries 是 @CacheEvict 特有的一个属性,意为是否删除整个缓存(value 或 cacheNames 指定的),默认为 false。从上述的例子中,我们可以看到,结果只删除了指定 key 的缓存数据条目,另一个没有被删除。现在我们来验证这个参数。
首先,我们使用 @Cacheable 或者 @CachePut 让缓存产生两条数据:
接下来,我们将上述删除方法的 allEntries=true,再请求一遍删除接口,结果:
可以看到,尽管我的接口只指定一条数据删除,而且 mysql 中也确实只删除了一条数据,但是缓存中整个都被删除了,说明 allEntries 起作用了。
4. beforeInvocation 参数
beforeInvocation 是 @CacheEvict 中特有的一个属性,意为是否在执行对应方法之前删除缓存,默认 false(即执行方法之后再删除缓存)。首先思考一个问题,在什么情况下我们需要主动去删除缓存呢?一般来讲都是在删除数据的时候,需要主动去删除缓存。那么就存在一个问题,程序执行时顺序的,那我们到底是应该先删除缓存,再调用方法去数据库中删除;还是先从数据库中删除,完了之后再去删除对应的缓存呢?在正常情况下,这两种方式差别并不大,毕竟程序执行都是毫秒级的,顺序执行没有什么时间跨度。但是,现实环境复杂,缓存访问和 db 访问都可能会出现异常,这种情况下就有区别了:
-
如果先删除缓存成功,然后 db 删除失败,那么接下来的查询就会直达数据库,造成压力;
-
如果先 db 删除成功,然后删除缓存失败,那么就会造成脏缓存;
至于该如何取舍,spring cache 通过 beforeInvocation 给开发者提供选择。
接下来,我们来模拟这两种情况,直观地感受一下 beforeInvocation=true 或 false 的区别。虽然我的应用是在本地执行,但是 mysql 和 redis 都是远程服务器上的,所以可以通过断开网络连接的方式来模拟访问异常的情况。
首先让缓存中产生两条数据
对应的 mysql 数据:
我们利用线程的 sleep 方法来延长执行时间,以便来得及进行断开网络的操作。。。。。。。。。。
4.1. beforeInvocation=false 时,预期的结果应该是:mysql 中 del 变为了 true,但是缓存中仍然有数据。
首先看下代码:
@Override @CacheEvict(value = "menuById", key = "#id") public Boolean deleteById(String id) { System.out.println("开始操作 db"); Boolean result = this.removeById(id); System.out.println("db 操作结束"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } return result; }
接下来,我们请求接口删除 name=“微服务测试0修改一下试试”这条数据。
因为中途关闭了网络,所以返回是这样的:
打印是这样的:
数据库结果,已经删除:
缓存结果,没有删除:
结果符合预期,说明当 beforeInvocation = false 时,是先执行方法,再操作缓存。
4.2. beforeInvocation = true 时,预期的结果应该是:mysql 中 del 仍然为 false,但是缓存中已经被删除。
首先看下代码:
@Override @CacheEvict(value = "menuById", key = "#id", beforeInvocation = true) public Boolean deleteById(String id) { System.out.println("开始操作 db"); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } Boolean result = this.removeById(id); System.out.println("db 操作结束"); return result; }
接下来,我们请求接口删除 name=“微服务测试2” 这条数据。
返回是这样的:
打印是这样的:
数据库结果,没有删除:
缓存结果,已经删除:
结果符合预期,说明当 beforeInvocation = true 时,是先操作缓存,再执行方法。
验证成功。