spring boot Cache缓存数据原理详解

对于热点数据,一般存储在缓存中,当需要时直接从缓存中获取,在缓存找不到的时候才访问数据库。访问缓存和向缓存中插入数据,是一个统一标准的动作,代码都是一样的,如果在每个方法里面都写这么一段访问缓存的代码,那会造成大量代码的冗余,spring也想到了这一点,它为我们提供了一些注解,对缓存数据的存取只需在方法上添加这些注解即可,无需再代码里面编写繁琐重复的代码。
下面先介绍一个例子,然后介绍spring缓存数据背后的原理。

一、使用spring缓存

使用spring缓存需要在代码中完成以下几步:

1、在pom文件中添加cache依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2、在主程序中添加注解@EnableCaching以开启基于注解的缓存。

@EnableCaching
public class Main {
	public static void main(String[] args) throws Exception {
		// 创建spring容器
		ApplicationContext context =SpringApplication.run(Main.class, args);
	}
}

3、在需要缓存数据的方法上添加@Cacheable、@CachePut等注解。为了简单,例子中没有真实访问数据库。

@Component("cacheTest")
public class CacheTest {
    //先检查是否有缓存,如果有则不执行方法,如果没有则执行方法并将方法返回值放入缓存
    @Cacheable(cacheNames="test")
    public User testCaching(String name){
        System.out.println("没有找到数据访问数据库");
        User user=new User();
        user.setAge(18);
        user.setName(name);
        return user;
    }
    //将方法返回值放入到缓存中
    @CachePut(cacheNames="test")
    public User testPutCaching(String name){
        System.out.println("数据放入缓存");
        User user=new User();
        user.setAge(19);
        user.setName(name);
        return user;
    }
    //删除缓存
    @CacheEvict(cacheNames="test")
    public void testEvictCaching(String name){
        System.out.println("删除缓存");
    }
}

调用CacheTest方法的流程如下:

		CacheTest cache=(CacheTest)context.getBean("cacheTest");
		//将数据放入缓存
		System.out.println(cache.testPutCaching("张三"));
		//数据已经在缓存,不访问数据库
		System.out.println(cache.testCaching("张三"));
		//删除缓存
		cache.testEvictCaching("张三");
		//数据不在缓存中,访问数据库
		System.out.println(cache.testCaching("张三"));
		//数据已缓存中,不访问数据库
		System.out.println(cache.testCaching("张三"));
		//以“张三”为key的数据已缓存中,但是“李四”不在
		System.out.println(cache.testCaching("李四"));

上面代码的执行结果如下:

数据放入缓存
name=张三;age=19
name=张三;age=19
删除缓存
没有找到数据访问数据库
name=张三;age=18
name=张三;age=18
没有找到数据访问数据库
name=李四;age=18

二、关键注解介绍

1、@Cacheable

@Cacheable用于类或方法上,在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值,不再执行目标方法;没有则执行目标方法,并将方法的返回值作为value,并以键值对的形式存入缓存。
关键属性如下:

  • cacheNames/value:缓存名,相当于命名空间,String类型的数组,可以指定多个缓存名,不同命名空间下,可以有相同的key。执行方法前,按顺序遍历每个命名空间,如果其中一个缓存空间中有需要的key,那么返回该空间下的key对应的value,如果都没有,则执行方法,执行后,将键值对放入到每个缓存空间下。
  • key:缓存键,默认是方法的所有入参值,也可以使用SpEL表达式指定。
  • keyGenerator:键产生器,用于产生键,该属性与key是互斥的,如果在spring容器中有多个产生器,可以使用该属性指定;如果没有指定key,spring也会使用产生器产生key,前提是容器中有产生器。
  • cacheManager:指定缓存管理器,在一个容器中可以同时存在多个管理器,属性值是缓存管理器在容器中的名字;
  • cacheResolver:解析器,该属性与cacheManager是互斥的,只能指定一个;
  • condition:在激活注解功能前,进行condition验证,如果condition结果为true,则表明验证通过,缓存注解生效;否则缓存注解不生效。使用SpEL表达式。默认是true。
  • unless:是否令注解(在方法执行后的功能)不生效;若unless的结果为true,则(方法执行后的功能)不生效;若unless的结果为false,则(方法执行后的)功能生效。
  • sync:表示强制同步执行。如果多个线程试图查找同一个键的value,那么Cache会对key进行加锁,以同步的方式来进行目标方法的调用,同步的好处是:后一个线程会读取到前一个线程的缓存数据,不用再调用目标方法了。默认为false,表示不用同步。是否能够同步还依赖于底层Cache的实现,有些Cache是不支持同步的,也就是即使设置sync=true也是以非同步的方式执行。

2、@CachePut

@CachePut用于类或方法上,在执行完目标方法后,将方法的返回值作为value,以键值对的形式存入缓存。
该注解的属性与@Cacheable相同。

3、@CacheEvict

@CacheEvict用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话)。
下面只介绍与@Cacheable不相同的属性:

  • allEntries:表示是否清除指定命名空间中的所有数据,默认为false。
  • beforeInvocation:是否在目标方法执行前删除缓存数据。 默认为false,即目标方法执行完毕后删除数据。

注解的属性中使用了SpEL表达式,下面介绍一下SpEL表达式的规则:

名字位置描述例子
methodNameroot object当前被调用的方法名#root.methodName
methodroot object当前被调用的方法#root.method.name
targetroot object当前被调用的目标对象#root.target
targetClassroot object当前被调用的目标对象类#root.targetClass
argsroot object当前被调用的方法的参数列表#root.args[0]
cachesroot object当前方法调用使用的缓存列表#root.caches[0].name
argument nameevaluation context方法参数的名字. 可以直接#参数名,也可以使用#p0或#a0 的形式,0代表参数的索引#iban、#a0 、#p0
resultevaluation context方法执行后的返回值#result

三、实现原理

spring缓存里面涉及到几个关键类:

  • CacheManager:缓存管理器,一般的spring容器中只能有一个管理器存在,如果定义多个,启动时会抛出NoUniqueBeanDefinitionException异常,如果定义了多个管理器,需要使用@Primary指定一个主管理器,如果不设置注解的cacheManager属性值,默认都是使用该主管理器,缓存管理器中持有所有由该管理器管理的Cache对象;
  • CacheResolver:缓存解析器,与CacheManager一一对应,作用是查找Cache对象;
  • Cache:存储缓存数据,类似于一个缓存的存储器,第二节介绍的三个注解的属性cacheNames指的便是Cache的名字,key和value存储在Cache中;
  • CacheOperation:方法上的每个注解都被解析为一个CacheOperation对象,并将方法与CacheOperation对象集合的对应关系保存到Map对象中,CacheOperation有三个子类:CacheableOperation、CacheEvictOperation、CachePutOperation,分别对应三个注解,注解的属性值保存在三个子类对象中,子类对象里面没有具体的处理逻辑,只是用来存储属性值,spring执行时,通过Map对象找到CacheOperation对象集合,然后判断某个CacheOperation对象是否有,如果有则执行对应的逻辑,如果没有则跳过;

先介绍一下注解@EnableCaching的作用。
注解@EnableCaching加载了一个配置类ProxyCachingConfiguration,该配置类又引入了一个非常关键的类:CacheInterceptor缓存拦截器。该拦截器负责拦截添加了注解的方法(拦截通过AOP实现),如果没被该拦截器拦截,那么无法使用spring缓存。
CacheInterceptor拦截了方法之后,首先获得该方法的CacheOperation对象集合,然后遍历该集合,根据CacheOperation的属性值,从容器中找到如下对象:KeyGenerator、CacheResolver、CacheManager,解决了这三个对象之后,就可以根据cacheNames找到对应的Cache对象,这样CacheOperation、KeyGenerator、CacheResolver、CacheManager、Cache之间形成了一一对应关系。
接下来,CacheInterceptor检查CacheOperation对象集合是否有CacheEvictOperation对象,其实也就是检查是否有@CacheEvict注解,如果有,则根据condition和beforeInvocation属性的配置来决定是否删除缓存;然后判断是否有CacheableOperation对象,如果有则先检查Cache对象中是否有对应缓存数据,如果有多个Cache对象,则进行遍历,如果从缓存中找到了缓存数据,则不再调用方法,如果没有找到则调用方法;之后判断是否有CachePutOperation或者CacheableOperation对象,如果有将缓存存储到Cache中,如果有多个Cache,则每个都存储;最后是再次判断是否有CacheEvictOperation对象,如果有则根据condition属性的配置来决定是否删除缓存。执行完上述逻辑之后,将方法返回值返回给调用方,处理结束。

CacheInterceptor处理过程中查找KeyGenerator、CacheResolver、CacheManager三个对象,如果配置了cacheResolver或者cacheManager属性,那么根据属性值查找CacheResolver和CacheManager对象,如果没有配置,那么使用容器中默认的CacheResolver和CacheManager对象。KeyGenerator也是一样,如果没有配置属性keyGenerator,spring会创建一个SimpleKeyGenerator对象作为key产生器。SimpleKeyGenerator会将方法所有入参作为key,处理规则比较简单:

  • 如果方法没有入参,则创建一个空的SimpleyKey对象作为key;
  • 如果方法有一个入参,那么该入参就是key;
  • 如果方法有多个入参,那么首先创建一个SimpleyKey对象,然后将方法入参组成数组作为SimpleyKey的属性,SimpleyKey对象作为key返回。

参考文章

https://blog.csdn.net/Forlogen/article/details/106882202
https://blog.csdn.net/justry_deng/article/details/89283664

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值