Spring缓存注解浅析及实践

一 背景

缓存是我们日常开发常被使用的技术栈,一般用来降低数据库读取压力,提升系统查询性能。使用缓存的数据一般为不常变化且使用率很高的数据,比如:配置信息、商品信息、用户信息等。我们一般的做法:第一次从数据库中读取数据,然后放到缓存中并设置缓存超期时间,缓存超期之后再从数据库从新读取,如果涉及到更新和删除数据也要同步缓存,这样才能解决缓存数据一致性问题,但我们常规的做法一般是使用缓存的put、get等命令把写入和读取缓存的代码写在方法体内部,这样缓存相关的操作代码就会耦合在业务代码里。

能不能加个缓存注解就能把缓存的的问题给解决了呢?常规的做法是自己定义一个缓存注解,使用AOP的机制来实现缓存读写和同步,但实际上我们做这一步是多余的,因为Spring本身就提供了强大的缓存注解功能,我们何必再重复造轮子呢。下面将简单介绍下Spring的几个关键缓存注解及如何使用它们来实现缓存读写、更新和删除。

二 Spring几个关键缓存注解介绍

下面简单介绍几个Spring提供的核心缓存注解:@EnableCaching,@Cacheable,@CachePut,@CacheEvict ,如下:

注解名称简介
@EnableCaching该注解用来开启缓存功能,配置类中需要加上这个注解,Spring才知道你需要缓存功能,另外其他和缓存相关的注解才会生效,Spring缓存注解也是通过AOP实现的,通过AOP来拦截需要使用缓存的方法,实现缓存功能。
@Cacheable该注解用来赋予缓存功能,它可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数访问该方法可以直接从缓存中获取结果,而不需要再次执行该方法。@Cacheable可以指定三个属性:valuekeycondition。 value: value和cacheNames属性作用一样,必须指定其中一个,表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。 key: 缓存以key->value的形式存储,key属性指定缓存内容对应的key,key属性支持SpEL表达式;当我们没有指定该属性时,Spring将使用默认策略生成key。 condition: 用来控制缓存的使用条件,condition属性默认为true,其值是通过SpEL表达式来指定的,当为true时表示先尝试从缓存中获取;若缓存中不存在则执行方法并将方法返回值存入缓存;当为false时不走缓存直接执行方法,并且返回结果也不会存入缓存。
@CachePut该注解用来将结果放入缓存,该注解的用法跟@Cacheable类似,区别如下: @CachePut:这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。 @Cacheable:当重复使用相同参数调用方法的时候,方法本身不会被调用执行,即方法本身被略过了,取而代之的是方法的结果直接从缓存中找到并返回了。 所以,@CachePut一般被用于缓存的更新同步,确保缓存数据一致性。
@CacheEvict该注解用来清除缓存,如果标注在方法上则目标方法被调用时会清除指定的缓存,@CacheEvict一般用于数据删除时同时删除缓存,确保缓存数据一致性。

三 工程实践

3.1 引入依赖

要在springboot中使用缓存,重点要引入依赖:spring-boot-starter-data-redis


xml

代码解读

复制代码

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>spring-cache</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-cache</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.boot.version>2.3.8.RELEASE</spring.boot.version> <slf4j-api.version>1.7.29</slf4j-api.version> <log4j-api.version>2.3</log4j-api.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring.boot.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j-api.version}</version> <scope>provided</scope> </dependency> </dependencies> </project>

3.2 核心代码

首先,要创建缓存配置类,配置类中需要定义一个bean:缓存管理器,类型为CacheManager;另外两个配置:cacheEnable为true开启缓存,false为关闭缓存,cacheTtl为统一的缓存超时时间。


kotlin

代码解读

复制代码

package com.java.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.support.NoOpCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import java.time.Duration; /** * redis缓存配置类 * * @author jiangzhaojing * @date 2024-11-29 15:01:12 */ @Configuration @EnableCaching public class RedisCacheConfig { @Value("${cache.enable:false}") private Boolean cacheEnable; @Value("${cache.ttl:120}") private Long cacheTtl; @Autowired private StringRedisTemplate redisTemplate; /** * 缓存管理bean注入 * * @param redisConnectionFactory * @return */ @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { if (cacheEnable) { RedisCacheConfiguration config = instanceConfig(); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .transactionAware() .build(); } return new NoOpCacheManager(); } /** * 实例配置 * * @return */ private RedisCacheConfiguration instanceConfig() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(cacheTtl)) .disableCachingNullValues(); } }

其次,创建测试需要实体类,需要注意是,该实体类必须实现Serializable,否则会出现序列化异常。


typescript

代码解读

复制代码

package com.java.demo.model; import java.io.Serializable; /** * 用户实体类 * * @author jiangzhaojing * @date 2024-11-29 15:01:12 * 用户相关属性 */ public class User implements Serializable { private String userId; private String userName; public User() { } public User(String userId, String userName) { this.userId = userId; this.userName = userName; } @Override public String toString() { return String.format("[userId:%s,userName:%s]", userId, userName); } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } }

然后,创建接口服务及实现类,并在实现类方法上增加缓存注解,如下:


java

代码解读

复制代码

package com.java.demo.service; import com.java.demo.model.User; /** * 用户相关服务 * * @author jiangzhaojing * @date 2024-11-29 15:01:12 */ public interface UserService { /** * 根据用户ID获取用户 * * @param userId * @return */ User getUserById(String userId); /** * 更新用户 * * @param user * @return */ User updateUser(User user); /** * 删除用户 * * @param userId */ void deleteUser(String userId); }


kotlin

代码解读

复制代码

package com.java.demo.service.impl; import com.java.demo.model.User; import com.java.demo.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; /** * 用户相关服务实现 * * @author jiangzhaojing * @date 2024-11-29 15:01:12 */ @Component public class UserServiceImpl implements UserService { private final static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); /** * 根据用户ID获取用户 * * @param userId * @return */ @Override @Cacheable(cacheNames = "users", key = "#userId") public User getUserById(String userId) { logger.info("调用了方法[getUserById],入参:{}", userId); //正常下面应该从数据库中读取 return new User("123", "li lei"); } /** * 更新用户 * * @param user * @return */ @Override @CachePut(cacheNames = "users", key = "#user.userId") public User updateUser(User user) { logger.info("调用了方法[updateUser],入参:{}", user); return user; } /** * 更新用户 * * @param userId * @return */ @Override @CacheEvict(cacheNames = "users", key = "#userId") public void deleteUser(String userId) { logger.info("调用了方法[deleteUser],入参:{}", userId); } }

然后,写一个应用的启动类


kotlin

代码解读

复制代码

package com.java.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @ComponentScan("com.java.demo") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }

最后,配置文件配置缓存相关配置项,其中spring.redis.host,spring.redis.password,spring.redis.port三项根据实际配置填写


ini

代码解读

复制代码

spring.redis.host=//redis地址 spring.redis.database=0 spring.redis.port=//redis端口 spring.redis.password=//redis密码 spring.redis.timeout=5000 spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle=1 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait=3000 cache.enable=true cache.ttl=300

3.3 测试用例


java

代码解读

复制代码

package com.java.demo; import com.java.demo.model.User; import com.java.demo.service.UserService; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; /** * 用户相关服务 * * @author jiangzhaojing * @date 2024-11-29 15:01:12 */ @RunWith(SpringRunner.class) @SpringBootTest public class CacheTest { private final static Logger logger = LoggerFactory.getLogger(CacheTest.class); @Autowired private UserService userService; @Test public void testCache() { //第一次读取缓存为空 logger.info("1.user:{}", userService.getUserById("123")); //第二次直接从缓存读取 logger.info("2.user:{}", userService.getUserById("123")); //更新缓存 userService.updateUser(new User("123", "zhang hua")); //第三次直接从缓存读取 logger.info("3.user:{}", userService.getUserById("123")); //删除缓存 userService.deleteUser("123"); logger.info("test finish!"); } }

第一次读取,缓存还没有则直接进入方法体并写入缓存,如下图:





第二次读取,因缓存存在则跳过方法直接从缓存中读取,从第三行日志可以看出来,如下:





更新数据时,使用@CachePut更新缓存,同步缓存数据:





删除数据时,及时使用 @CacheEvict清理缓存,确保缓存数据与数据库数据一致。

四 总结

从上面的解析和实践中可以看到使用Spring提供的@EnableCaching注解可以方便进行缓存的处理,避免缓存处理逻辑与业务代码耦合,让代码更优雅,从一定程度上提升了开发效率。但细心的同学会发现一个问题:@EnableCaching注解并未提供缓存超期的属性,所以我们无法通过@EnableCaching设置缓存超时时间,只能通过CacheManager设置一个统一的缓存超期时间。通过@EnableCaching源码我们也能发现并无缓存超期属性,如下:


java

代码解读

复制代码

package org.springframework.cache.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Cacheable { @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; String key() default ""; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; String condition() default ""; String unless() default ""; boolean sync() default false; }

至于Spring不提供这个属性原因,可能是基于框架的扩展性和通用性方面的考虑,不过Spring的强大之处就在于它是可以扩展的,预留了很多扩展点等待我们去实现,本文因篇幅有限不在本篇讨论如何扩展实现缓存超时时间的问题,留在后面的文章继续探讨。以上的分析讨论及代码难免有错误之处,敬请同学们指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值