第6章 Spring Boot缓存管理
入门级缓存管理
缓存是分布式系统中的重要组件,主要解决数据库数据的高并发访问。
在实际开发中,尤其是用户访问量较大的网站,用户对高频热点数据的访问非常频繁,为了提高服务器访问性能、减少数据库的压力、提高用户体验,使用缓存显得尤为重要。
Spring Boot 默认缓存管理
Spring框架支持透明地向应用程序添加缓存并对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法中,从而减少操作数据的次数,同时不会对程序本身造成任何干扰。
Spring Boot继承了Spring框架的缓存功能,使用@EnableCaching
注解开启基于注解的缓存支持。
1.基础环境搭建
(1)准备数据,创建项目
1)创建Spring Boot项目,引入相关依赖
- 这里使用之前创建的springbootdata的数据库,该数据库有两个表 t_article和t_comment,这两个表预先插入几条测试数据。
- 使用Spring Initializr方式创建一个Spring Boot项目,在Dependencies依赖选择项中JPA依赖、MySQL依赖和Web依赖。
注:mysql依赖要与自己的数据库版本对应
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency>
2)编写数据库表对应的实体类
创建domain包,在包下新建类Comment、并使用JPA相关注解配置映射关系
@Entity(name="t_comment") // 设置ORM实体类,并指定映射的表名 public class Comment { @Id // 表明映射对应的主键id @GeneratedValue(strategy = GenerationType.IDENTITY) // 设置主键自增策略 private Integer id; private String content; private String author; @Column(name = "a_id") //指定映射的表字段名 private Integer aId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getaId() { return aId; } public void setaId(Integer aId) { this.aId = aId; } @Override public String toString() { return "Comment{" + "id=" + id + ", content='" + content + '\'' + ", author='" + author + '\'' + ", aId=" + aId + '}'; } }
3)编写数据库操作的Repository接口文件CommentRepository
创建repository包,在包下创建用于操作实体类Comment的Repository接口,该接口继承JpaRepository
public interface CommentRepository extends JpaRepository<Comment,Integer> { // 根据评论id修改评论作者评论作者author @Transactional @Modifying @Query("UPDATE t_comment c SET c.author= ?1 WHERE c.id = ?2") public int updateComment(String author,Integer id); }
4)编写业务操作类Service文件CommentService,在该类中编写数据的查询、修改和删除操作
创建service包,在包下创建一个用于Comment相关业务操作的实体类
@Service public class CommentService { @Autowired private CommentRepository commentRepository; // 根据评论id查询评论信息 public Comment findById(int comment_id){ Optional<Comment> optional = commentRepository.findById(comment_id); if(optional.isPresent()){ return optional.get(); } return null; } // 更新评论信息 public Comment updateComment(Comment comment){ commentRepository.updateComment(comment.getAuthor(), comment.getaId()); return comment; } // 删除评论信息 public void deleteComment(int comment_id){ commentRepository.deleteById(comment_id); } }
5)编写Web访问层Controller文件CommentController,使用注入的CommentService实例对象编写对Comment评论数据的查询、修改和删除方法。
创建controller包,在包下创建一个访问层Controller文件CommentController
@Controller public class CommentController { @Autowired private CommentService commentService; @GetMapping("/get/{id}") public Comment findById(@PathVariable("id") int comment_id){ Comment comment = commentService.findById(comment_id); return comment; } @GetMapping("/update/{id}/{author}") public Comment updateComment(@PathVariable("id") int comment_id, @PathVariable("author") String author){ Comment comment = commentService.findById(comment_id); comment.setAuthor(author); Comment updateComment = commentService.updateComment(comment); return updateComment; } @GetMapping("/delete/{id}") public void deleteComment(@PathVariable("id") int comment_id){ commentService.deleteComment(comment_id); } }
(2)编写配置文件
#mysql数据库连接配置 spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC&useSSL=false spring.datasource.username=root spring.datasource.password=123456 # 显示使用JPA进行数据库查询的SQL语句 spring.jpa.show-sql=true
(3)项目测试
如果不写spring.jpa.show-sql=true配置,控制台不会输出相关信息
项目启动成功后,在浏览器上访问http://localhost:8080/get/1
浏览器每刷新一次,控制台会新输出一条SQL语句
2.Spring Boot 默认缓存体验
(1)使用@EnableCaching注解开启基于注解的缓存支持
@EnableCaching // 开启Spring Boot基于注解的缓存管理支持 @SpringBootApplication public class Demo06Application { public static void main(String[] args) { SpringApplication.run(Demo06Application.class, args); } }
(2)使用@Cacheable注解对数据操作方法进行缓存管理
修改CommentService类中的方法
@Cacheable(cacheNames = "comment") public Comment findById(int comment_id){ Optional<Comment> optional = commentRepository.findById(comment_id); if(optional.isPresent()){ return optional.get(); } return null; }
(3)项目测试
项目启动成功后,在浏览器上访问http://localhost:8080/get/1,
不论浏览器刷新多少次,页面的查询结果都会显示同一条数据
Spring Boot 缓存注解介绍
1.@EnableCaching注解
@EnableCaching是由Spring框架提供的,Spring Boot框架对该注解进行了继承,
配置位置: 该注解需要配置在类上(在Spring Boot中,通常配置在项目启动类上),
作用: 用于开启基于注解的缓存支持。
2.@Cacheable注解
@Cacheable注解也是由Spring框架提供的.
配置位置: 可以作用于类或方法(通常用在数据查询方法上),
作用: 用于对方法结果进行缓存存储。
@Cacheable注解的执行顺序
先进行缓存查询,
- 如果为空则进行方法查询,并将结果进行缓存;
- 如果缓存中有数据,不进行方法查询,而是直接使用缓存数据。
@Cacheable注解的属性
属性名 | 说明 |
---|---|
value/cacheNames | 指定缓存空间的名称,必配属性。这两个属性二选一使用 |
key | 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式 |
keyGenerator | 指定缓存数据的key的生成器,与key属性二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定缓存解析器,与cacheManager属性二选一使用 |
condition | 指定在符合某条件下,进行数据缓存 |
unless | 指定在符合某条件下,不进行数据缓存 |
sync | 指定是否使用异步缓存。默认false |
Cache SpEL 表达式
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; | #iban 、 #a0 、 #p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false) | #result |
3.@CachePut注解
@CachePut注解是由Spring框架提供的,
配置位置: 可以作用于类或方法(通常用在数据更新方法上),
作用:更新缓存数据。
执行顺序:
先进行方法调用,然后将方法结果更新到缓存中。
属性:
与@Cacheable注解的属性完全相同。
4.@CacheEvict注解
@CacheEvict注解是由Spring框架提供的,
配置位置: 可以作用于类或方法(通常用在数据删除方法上),
作用:删除缓存数据
默认执行顺序
先进行方法调用,然后将缓存进行清除。
属性
除此@Cacheable注解的属性之外
还额外提供了两个特殊属性
- allEntries
- beforeInvocation
allEntries属性
allEntries属性表示是否清除指定缓存空间中的所有缓存数据,默认值为false(即默认只删除指定key对应的缓存数据)。
beforeInvocation属性
beforeInvocation属性表示是否在方法执行之前进行缓存清除,默认值为false(即默认在执行方法后再进行缓存清除)。
5.@Caching注解
配置位置: 可以作用于类或方法
作用: 用于针对复杂规则的数据缓存管理,。
实现:
在@Caching注解内部包含有Cacheable、put和evict三个属性,分别对应于@Cacheable、@CachePut和@CacheEvict三个注解。
例:
@Caching(cacheable={@Cacheable(cacheNames ="comment",key = "#id")}, put = {@CachePut(cacheNames = "comment",key = "#result.author")}) public Comment getComment(int comment_id){ return commentRepository.findById(comment_id).get(); }
6.@CacheConfig注解
配置位置: 使用在类上
作用: 主要用于统筹管理类中所有使用@Cacheable、@CachePut和@CacheEvict注解标注方法中的公共属性。
公共属性
cacheNames、keyGenerator、cacheManager和cacheResolver。
例:
@CacheConfig(cacheNames = "comment") @Service public class CommentService { @Autowired private CommentRepository commentRepository; @Cacheable public Comment findById(int comment_id){ Comment comment = commentRepository.findById(comment_id).get(); return comment; } ...}
整合Redis缓存实现 - 写入硬盘
Spring Boot支持的缓存组件
注意:
-
相对使用注解的方式,使用Redis API进行数据缓存管理更加灵活,例如,
手机验证码进行验证时,可以在缓存中设置验证等待时间。 -
相比使用注解的方式进行缓存管理,使用Redis API的方式编写的代码量可能会更多。
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,若程序中没有定义类型为cacheManager的Bean组件或者名为cacheResolver的cacheResolver缓存解析器,Spring Boot将尝试启用以下缓存组件(按照指定的顺序)
(1)Generic
(2)JCache (JSR-107) (EhCache 3、Hazelcast、Infinispan等)
(3)EhCache 2.x
(4)Hazelcast
(5)Infinispan
(6)Couchbase
(7)Redis
(8)Caffeine
(9)Simple(默认)
基于注解实现
注意:
-
相对使用注解的方式,使用Redis API进行数据缓存管理更加灵活,例如,
手机验证码进行验证时,可以在缓存中设置验证等待时间。 -
相比使用注解的方式进行缓存管理,使用Redis API的方式编写的代码量可能会更多。
(1)添加Spring Data Redis 依赖启动器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
springboot集成redis时,引入spring-boot-starter-redis包报错,maven找不到这个资源.如下图:
我的项目中,spring boot是 用的2.0.4版本.spring-boot-starter-redis在springboot 1.4.7版本后,改为了spring-boot-starter-data-redis,
所以如果想集成redis
应该引用spring-boot-starter-data-redis
如果就是想用前者,
那么就应该加上版本号
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> <version>1.4.7.RELEASE</version> </dependency>
(2)Redis服务连接配置
# Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password=
(3)使用@Cacheable、@CachePut、@CacheEvict注解定制缓存管理
对CommentService类中的方法进行修改使用@Cacheable、@CachePut、@CacheEvict三个注解定制缓存管理
@Service public class CommentService { @Autowired private CommentRepository commentRepository; // 根据评论id查询评论信息 //@Cacheable(cacheNames = "comment") @Cacheable(cacheNames = "comment",unless = "#result==null") public Comment findById(int comment_id){ Optional<Comment> optional = commentRepository.findById(comment_id); if(optional.isPresent()){ return optional.get(); } return null; } // 更新评论信息 @CachePut(cacheNames = "comment",key = "#result.id") public Comment updateComment(Comment comment){ commentRepository.updateComment(comment.getAuthor(), comment.getaId()); return comment; } // 删除评论信息 @CacheEvict(cacheNames = "comment") public void deleteComment(int comment_id){ commentRepository.deleteById(comment_id); } }
(4)基于注解的Redis查询缓存测试
项目启动成功后,通过浏览器访问http://localhost:8080/get/1
会出现如下错误提示
提示信息要求对应实体类必须实现序列化。
(5)将缓存对象实现序列化
@Entity(name="t_comment") // 设置ORM实体类,并指定映射的表名 public class Comment implements Serializable { @Id // 表明映射对应的主键id @GeneratedValue(strategy = GenerationType.IDENTITY) // 设置主键自增策略 private Integer id; private String content; private String author; @Column(name = "a_id") //指定映射的表字段名 private Integer aId; ... }
(6)基于注解的Redis缓存查询测试
项目启动成功后,通过浏览器访问http://localhost:8080/get/1,并重复刷新浏览器查询同一条数据信息
(7)基于注解的Redis缓存更新测试
项目启动成功后,通过浏览器访问http://localhost:8080/update/1/shitou,接着,继续访问http://localhost:8080/get/1
执行updateComment()方法更新id为1的数据时执行了一条更新SQL语句,后续调用findById()方法查询id为1的用户评论信息时没有执行查询SQL语句。
(8)基于注解的Redis缓存删除测试
项目启动成功后,通过浏览器访问http://localhost:8080/delete/1,接着,继续访http://localhost:8080/get/1
#对于基于注解的Redis缓存数据统一设置有效期为1分钟,单位毫秒 spring.cache.redis.time-to-live=60000 #不灵活,对基于API的Redis缓存实现没效果
基于API实现
注意:
-
相对使用注解的方式,使用Redis API进行数据缓存管理更加灵活,例如,
手机验证码进行验证时,可以在缓存中设置验证等待时间。 -
相比使用注解的方式进行缓存管理,使用Redis API的方式编写的代码量可能会更多。
(1)使用Redis API 进行业务数据缓存管理
编写一个进行业务处理的类ApiCommentService,使用@Autowired注解注入Redis API中常用的RedisTemplate(类似于Java基础API中的JdbcTemplate);然后在数据查询、修改和删除三个方法中,根据业务需求分别进行数据缓存查询、缓存存储、缓存更新和缓存删除。同时,Comment数据对应缓存管理的key值都手动设置了一个前缀“comment_”,这是针对不同业务数据进行缓存管理设置的唯一key,避免与其他业务缓存数据的key重复。
@Service public class ApiCommentService { @Autowired private CommentRepository commentRepository; @Autowired private RedisTemplate redisTemplate; public Comment findById(int comment_id){ // 先从Redis缓存中查询数据 Object object = redisTemplate.opsForValue().get("comment_"+comment_id); if (object!=null){ return (Comment)object; }else { // 缓存中没有,就进入数据库查询 Optional<Comment> optional = commentRepository.findById(comment_id); if(optional.isPresent()){ Comment comment= optional.get(); // 将查询结果进行缓存,并设置有效期为1天 redisTemplate.opsForValue().set("comment_"+comment_id, comment,1, TimeUnit.DAYS); return comment; }else { return null; } } } public Comment updateComment(Comment comment){ commentRepository.updateComment(comment.getAuthor(), comment.getaId()); // 更新数据后进行缓存更新 redisTemplate.opsForValue().set("comment_"+comment.getId(),comment); return comment; } public void deleteComment(int comment_id){ commentRepository.deleteById(comment_id); // 删除数据后进行缓存删除 redisTemplate.delete("comment_"+comment_id); } }
(2)编写Web访问层Controller文件ApiCommentController
在类上加入了@RequestMapping(“/api”)注解用于窄化请求,并通过@Autowired注解注入了新编写的ApiCommentService实例对象,然后调用ApiCommentService中的相关方法进行数据查询、修改和删除。
@RestController @RequestMapping("/api") // 窄化请求路径 public class ApiCommentController { @Autowired private ApiCommentService apiCommentService; @GetMapping("/get/{id}") public Comment findById(@PathVariable("id") int comment_id){ Comment comment = apiCommentService.findById(comment_id); return comment; } @GetMapping("/update/{id}/{author}") public Comment updateComment(@PathVariable("id") int comment_id, @PathVariable("author") String author){ Comment comment = apiCommentService.findById(comment_id); comment.setAuthor(author); Comment updateComment = apiCommentService.updateComment(comment); return updateComment; } @GetMapping("/delete/{id}") public void deleteComment(@PathVariable("id") int comment_id){ apiCommentService.deleteComment(comment_id); } }
(3)基于API的Redis缓存实现的相关配置
- 基于API的Redis缓存实现不需要@EnableCaching注解开启基于注解的缓存支持。
- 基于API的Redis缓存实现需要在Spring Boot项目的pom.xml文件中引入Redis依赖启动器,并在配置文件中进行Redis服务连接配置,同时将进行数据存储的Comment实体类实现序列化接口。
- 缓存测试与基于注解的Redis缓存实现的测试完全一样。
注意:整合其他缓存管理,需要添加依赖spring-boot-starter-cache
自定义Redis缓存序列化机制
1.自定义RedisTemplate
(1)Redis API 默认序列化机制
基于Redis API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的.
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware { private boolean enableTransactionSupport = false; private boolean exposeConnection = false; private boolean initialized = false; private boolean enableDefaultSerializer = true; @Nullable private RedisSerializer<?> defaultSerializer; @Nullable private ClassLoader classLoader; @Nullable private RedisSerializer keySerializer = null; @Nullable private RedisSerializer valueSerializer = null; @Nullable private RedisSerializer hashKeySerializer = null; @Nullable private RedisSerializer hashValueSerializer = null; ... public void afterPropertiesSet() { super.afterPropertiesSet(); boolean defaultUsed = false; if (this.defaultSerializer == null) { this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader()); } ... }
-
使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是JdkSerializationRedisSerializer序列化方式.
所以进行数据缓存的实体类必须实现JDK自带的序列化接口(例如Serializable); -
使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式defaultSerializer,那么将使用自定义的序列化方式。
(2)自定义RedisTemplate序列化机制
在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式可知:
在Redis自动配置类中,通过Redis连接工厂RedisConnectionFactory初始化了一个RedisTemplate;该类上方添加了@ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发者自定义了一个名为redisTemplate的Bean,则该默认初始化的RedisTemplate会被覆盖。
因此
如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可。
- 在项目中创建创建一个Redis自定义配置类RedisConfig,
- 通过@Configuration注解定义了一个RedisConfig配置类,
- 并使用@Bean注解注入了一个默认名称为方法名的redisTemplate组件(注意,该Bean组件名称必须是redisTemplate)。
- 在定义的Bean组件中,自定义了一个RedisTemplate,使用自定义的Jackson2JsonRedisSerializer数据序列化方式;
在定制序列化方式中,定义了一个ObjectMapper用于进行数据转换设置。
@Configuration // 定义一个配置类 public class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); // 使用JSON格式序列化对象,对缓存数据key和value进行转换 Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); // 解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 设置RedisTemplate模板API的序列化方式为JSON template.setDefaultSerializer(jacksonSeial); return template; } }
(3)效果测试
项目启动成功后,通过浏览器访问http://localhost:8080/api/get/3,并重复刷新浏览器查看同一条数据信息
2.自定义RedisCacheManager
(1)Redis 注解默认序列化机制
Spring Boot整合Redis组件提供的缓存自动配置类RedisCacheConfiguration,其内部是通过Redis连接工厂RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。
因此:
如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可。
(2)自定义RedisCacheManager
在RedisConfig类中添加方法cacheManager
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换 RedisSerializer<String> strSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); // 解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 定制缓存数据序列化方式及时效 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build(); return cacheManager; }
(3)效果测试
项目启动成功后,通过浏览器访问http://localhost:8080/api/get/3,并重复刷新浏览器查看同一条数据信息
执行findById()方法正确查询出用户评论信息Comment,重复进行同样的查询操作,数据库只执行了一次SQL语句
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/16733210.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步