写点什么

Spring 缓存注解浅析及实践

  • 2025-01-13
    北京
  • 本文字数:5413 字

    阅读完需:约 18 分钟

作者:京东物流 江兆晶

一 背景

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


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

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

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


三 工程实践

3.1 引入依赖

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


<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 为统一的缓存超时时间。


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@EnableCachingpublic 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,否则会出现序列化异常。


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; }}
复制代码


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


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);}
复制代码


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 */@Componentpublic 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); }

}
复制代码


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


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 三项根据实际配置填写


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

3.3 测试用例

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)@SpringBootTestpublic 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 源码我们也能发现并无缓存超期属性,如下:


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@Documentedpublic @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 的强大之处就在于它是可以扩展的,预留了很多扩展点等待我们去实现,本文因篇幅有限不在本篇讨论如何扩展实现缓存超时时间的问题,留在后面的文章继续探讨。以上的分析讨论及代码难免有错误之处,敬请同学们指正!

五 源码

Spring 缓存注解工程实践相关源码: https://3.cn/10h-Vk1KT

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Spring缓存注解浅析及实践_京东科技开发者_InfoQ写作社区