@CacheEvict 使用详解

  • A+
所属分类:Java redis springboot 缓存

1. 功能说明

  除了填充缓存,spring cache 也支持使用 @CacheEvict 来删除缓存。@CacheEvict 就是一个触发器,在每次调用被它注解的方法时,就会触发删除它指定的缓存的动作。跟 @Cacheable 和 @CachePut 一样,@CacheEvict 也要求指定一个或多个缓存,也指定自定义一的缓存解析器和 key 生成器,也支持指定条件(condition 参数)。

  关于这些(value、cacheNames、key、keyGenerator、cacheManager、cacheResolver、condition)属性参数的用法和说明,请参考:

    spring cache 学习 —— @Cacheable 使用详解

 

2. 例子

  我们来举个简单的例子,首先看一下 mysql 数据,我们想要删除 name=“微服务测试0修改一下试试” 这条数据(逻辑删除)

    @CacheEvict 使用详解

 

 

   这条数据在 redis 中已经存在了

    @CacheEvict 使用详解

 

 

   接下来我们编写一个删除方法,并使用 @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);
    }

  接下来,我们请求一下接口,看看结果是否删除成功的:

    @CacheEvict 使用详解

 

 

   再看看 mysq 数据的 del 字段和 redis 是否还有数据:

    @CacheEvict 使用详解

 

 

     @CacheEvict 使用详解

 

 

   可以看到,mysql 和 缓存都删除成功了。

 

3. allEntries 参数

  allEntries 是 @CacheEvict 特有的一个属性,意为是否删除整个缓存(value 或 cacheNames 指定的),默认为 false。从上述的例子中,我们可以看到,结果只删除了指定 key 的缓存数据条目,另一个没有被删除。现在我们来验证这个参数。

  首先,我们使用 @Cacheable 或者 @CachePut 让缓存产生两条数据:

    @CacheEvict 使用详解

 

 

   接下来,我们将上述删除方法的 allEntries=true,再请求一遍删除接口,结果:

    @CacheEvict 使用详解

    @CacheEvict 使用详解

   可以看到,尽管我的接口只指定一条数据删除,而且 mysql 中也确实只删除了一条数据,但是缓存中整个都被删除了,说明 allEntries 起作用了。

 

4. beforeInvocation 参数

  beforeInvocation 是 @CacheEvict 中特有的一个属性,意为是否在执行对应方法之前删除缓存,默认 false(即执行方法之后再删除缓存)。首先思考一个问题,在什么情况下我们需要主动去删除缓存呢?一般来讲都是在删除数据的时候,需要主动去删除缓存。那么就存在一个问题,程序执行时顺序的,那我们到底是应该先删除缓存,再调用方法去数据库中删除;还是先从数据库中删除,完了之后再去删除对应的缓存呢?在正常情况下,这两种方式差别并不大,毕竟程序执行都是毫秒级的,顺序执行没有什么时间跨度。但是,现实环境复杂,缓存访问和 db 访问都可能会出现异常,这种情况下就有区别了:

    • 如果先删除缓存成功,然后 db 删除失败,那么接下来的查询就会直达数据库,造成压力;

    • 如果先 db 删除成功,然后删除缓存失败,那么就会造成脏缓存;

  至于该如何取舍,spring cache 通过 beforeInvocation 给开发者提供选择。

  接下来,我们来模拟这两种情况,直观地感受一下 beforeInvocation=true 或 false 的区别。虽然我的应用是在本地执行,但是 mysql 和 redis 都是远程服务器上的,所以可以通过断开网络连接的方式来模拟访问异常的情况。

  首先让缓存中产生两条数据

    @CacheEvict 使用详解

    对应的 mysql 数据:

    @CacheEvict 使用详解

 

 

 

  我们利用线程的 sleep 方法来延长执行时间,以便来得及进行断开网络的操作。。。。。。。。。。

  4.1. beforeInvocation=false 时,预期的结果应该是:mysql 中 del 变为了 true,但是缓存中仍然有数据。

    首先看下代码:

@CacheEvict 使用详解

@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;
    }

@CacheEvict 使用详解

    接下来,我们请求接口删除 name=“微服务测试0修改一下试试”这条数据。

    因为中途关闭了网络,所以返回是这样的:

      @CacheEvict 使用详解

 

 

     打印是这样的:

      @CacheEvict 使用详解

 

 

     数据库结果,已经删除:

      @CacheEvict 使用详解

 

 

     缓存结果,没有删除:

      @CacheEvict 使用详解

 

 

     结果符合预期,说明当 beforeInvocation = false 时,是先执行方法,再操作缓存。

  4.2. beforeInvocation = true 时,预期的结果应该是:mysql 中 del 仍然为 false,但是缓存中已经被删除。

    首先看下代码:

@CacheEvict 使用详解

@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;
}

@CacheEvict 使用详解

    接下来,我们请求接口删除 name=“微服务测试2” 这条数据。

    返回是这样的:

      @CacheEvict 使用详解

 

 

     打印是这样的:

      @CacheEvict 使用详解

 

 

     数据库结果,没有删除:

      @CacheEvict 使用详解

 

 

    缓存结果,已经删除:

       @CacheEvict 使用详解

 

 

     结果符合预期,说明当 beforeInvocation = true 时,是先操作缓存,再执行方法。

  验证成功。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: