缓存是分布式系统的重要组件,主要解决数据库数据的高并发访问问题,对于用户访问量大的网站,缓存对于提高服务器访问性能,减少数据库压力和提高用户体验十分重要。spring boot对缓存提供了很好的支持,下面我们将对spring boot的缓存进行介绍和对spring boot与redis缓存中间件进心整合
spring框架支持透明的向应用程序添加缓存并对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据据的方法中,从而减少操作数据的次数,同时不对程序本身造成干扰。spring boot继承了spring的缓存管理功能,通过
@EnableCaching
注解开启基于注解的缓存支持,springboot可以启动缓存管理的自动化配置。下面我们对springboot支持的默认缓存管理进行讨论
@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;
//省略get-set和toString方法
}
编写数据库操作的repository.Repository接口文件,用于操作Comment实体
public interface Repository extends JpaRepository<Comment,Integer> {
@Transactional
@Modifying
@Query("UPDATE t_comment c SET c.author= ?1 WHERE c.id = ?2")
public int updateComment(String author,Integer id);//根据评论id修改评论作者author
}
编写业务操作类service.CommentService,包括查询,更新和删除功能
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
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);
}
}
编写web访问层controller.CommentController
@RestController
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);
}
}
# mysql数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=
spring.datasource.password=
# 显示使用JPA进行数据库查询的sql语句
spring.jpa.show-sql=true
之所以会出现这种情况,是因为没有开启缓存,这样数据表中的数据虽没有变化,但每执行一次查询操作(本质是执行sql语句),都会访问一次数据库,随着时间和访问量的增加,数据规模会越来越大,这样会影响用户体验,为此我们要使用缓存来解决问题
@EnableCaching
注解开启缓存支持@EnableCaching //开启缓存支持
@SpringBootApplication
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
2.在Service类的查询方法上使用@Cacheable
注解对数据操作方法进行缓存管理
@Cacheable(cacheNames = "comment")
public Comment findById(int comment_id){
Optional<Comment> optional = commentRepository.findById(comment_id);
if(optional.isPresent()){
return optional.get();
}
return null;
}
该@Cacheable
注解的作用是将查询结果Comment存放在spring boot默认缓存中名称为comment的名称空间(namespace)中,对应缓存的唯一标识(即缓存数据对应的主键key)默认为方法参数comment_id的值
3. 效果测试(略)
测试中我们可以发现,多次刷新页面,控制台也只是显示同一条sql语句,说明执行查询操作时,只执行了一次sql语句,缓存开启成功
上一小节介绍了@EnableCaching
和@Cacheable
实现了spring boot基于注解的缓存管理,下面我们进一步讨论关于缓存管理的其他注解
@EnableCaching
@Cacheable
@Cacheable
的执行顺序时,先进行缓存查询,如果为空则进行方法查询,并将结果缓存;如果缓存中有数据,则不进行方法查询,直接使用缓存数据@Cacheabl
提供了多个属性,用于对缓存存储进行相关配置,具体说明如下属性名 | 说明 |
---|---|
value/cacheNames | 指定缓存空间的名称,必配属性。这两个属性二选一使用 |
key | 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式 |
keyGenerator | 指定缓存数据的key生成器,于key属性二选一 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定缓存解析器,与cacheManager属性二选一 |
condition | 指定在符合某条件下,进行数据缓存 |
unless | 指定在符合某条件下,不进行数据缓存 |
sync | 指定是否使用异步缓存,默认false |
@Cacheable(cacheNames = {"c1","c2"})
)。如果@Cacheable
只配置value(或cacheNames)的一个属性,那这两个属性名可以省略,如@Cacheable("c1")
指定了缓存的名称空间为c1generateKey(Object...params)
方法参数生成key值。默认下,generateKey()
只有一个参数,参数值就是key属性的值,如果generateKey()
没有参数,那key属性是一个空参的SimpleKey[]对象,如果有多个参数,那key属性是一个带参的SimpleKey[p1,[p2…]]对象除了使用默认key属性值外,还可以手动指定key属性值,或使用spring提供的SqEL表达式
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象示例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args [0] |
caches | root对象 | 当前被调用的方法的缓存列表 | #root.caches [0].name |
ArgumentName | 执行上下文 | 当前被调用的方法参数,可以用#参数名或者#a0、#p0的形式标识(0标识参数索引,从0开始) | #comment_id、#a0、#p0 |
result | 执行上下文 | 当前方法执行后的返回结果 | #result |
@Cacheable(cacheNames="comment",condition="#comment_id>10")
表示方法参数comment_id的值大于10才会对结果进行缓存@Cacheable(cacheNames="comment",unless="#result==null")
表示只有查询结果不为空才会对结果进行缓存@CachePut
注解:可以作用于类或方法(一般为数据更新方法上),作用是更新缓存数据。执行顺序:先进行方法调用,然后将方法结果更新到缓存。@CachePut
也有多个属性,这些属性和Cacheable
注解的属性完全相同@CacheEvict
注解:可以作用于类或方法(一般为数据删除方法上),作用是删除缓存数据。执行顺序:先进行方法调用,然后清除缓存。@CacheEvict
也有多个属性,这些属性和Cacheable
注解的属性基本相同,需额外注意另外两个特殊属性allEntries和beforeInvocation,说明如下@CacheEvict(cacheNames = "comment",allEntries = true)
表示删除缓存空间comment中所有数据@CacheEvict(cacheNames = "comment",beforeInvocation= true)
表示在方法执行前进行缓存清除;需要注意的是,该设置为true时有弊端,如再进行数据删除的方法中发生了异常,会导致实际数据没有被删除,但是缓存数据被提前删除了@Caching
注解:如果处理复杂规则的数据缓存可以使用@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();
}
@Caching
作用于根据id执行查询操作并将查询到的Comment对象进行缓存管理的getComment()
上,在@Caching
中使用了cacheable和put两个属性,并且这两个属性嵌套引入@Cacheable
和@CachePut
两个注解,在两个注解中分别使用#id和#result.author缓存key的值
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;
}
...
}
上述代码中,使用
@CacheConfig
在CommentService上标注并用cacheNames属性将缓存空间统一设置为comment,这样该类的所有方法上使用缓存注解时可以省略相应的cacheNames属性;需要注意的时如果在类上标注了@CacheConfig
注解定义了某个属性(如cacheNames),同时又在该类方法中使用缓存注解定义了相同属性,那么该属性值就会使用“就近原则”,以方法上注解中的属性值为准
spring boot中,数据的管理存储依赖于spring框架中cache相关的org.springframework.cache.Cache和org.springframework.cache.CacheManager缓存管理器接口。如果程序中没有定义类型为cacheManager的Bean组件或是名为cacheResolver的cacheResolver缓存解析器,spring boot将尝试选择并启用一下缓存组件(按照指定的顺序加载)
Generic>JCache>EhCache>Hazelcast>Infinispan>Couchbase>Redis>Caffeine>Simple
添加某个缓存组件后,spring boot会选择并启动对应的缓存管理器。如果同时添加了多个缓存组件且没有指定缓存管理器或缓存解析器(cacheManager&cacheResolver),则优先启动指定的缓存组件并进行缓存管理。
在前面的默认缓存笔记中,我们没有添加任何缓存管理组件却能进行缓存管理,这是因为开启缓存管理后,spring boot将按照上面的顺序查找有效的缓存组件进行缓存管理,如果没有任何缓存组件,默认使用最后一个Simple;Simple时spring boot默认的缓存管理组件,默认使用内存中的ConcurrentHashMap进行缓存存储。
我们在前面笔记的默认缓存管理基础上引入redis缓存组件
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
# redis服务地址
spring.redis.host=127.0.0.1
# redis服务连接端口
spring.redis.port=6379
# redis服务器连接密码(默认为空)
spring.redis.password=
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@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);
}
}
可以看到执行了sql语句,但是出现非法参数异常,提示信息要求对应Comment实体类必须实现序列化
我们 剋在配置文件中添加属性统一配置redis数据的有效期(单位为毫秒),但这种方法不够灵活,并且对下面要讨论的基于API的redis缓存实现没有效果
# 对基于注解的redis缓存数据统一设置有效期为1min,单位毫秒
spring.cache.redis.time-to-live=60000
spring boot除了基于注解形式实现redis缓存外,还有基于API的redis缓存实现,下面就来实现一下吧
@Service
public class ApiCommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private RedisTemplate redisTemplate;
public Comment findBuId(int comment_id){
//先从redis缓存中查询数据,注意本部分特意把前缀设置为comment_,加以区分
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);
}
}
RedisTemplate有如下特点
对象类型数据,其子类StringReidsTemplate是专门用于操作
字符串类型的数据的redisTemplate.opsValue().set("comment_"+comment_id,comment);
redisTemplate.expire("comment_)"+comment_id,90,TimeUnit,SECONDS);
@RestController
@RequestMapping("/api")//窄化请求路径
public class ApiCpmmentController {
@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 comment;
}
@GetMapping("/delete/{id}")
public void deleteComment(@PathVariable("id") int comment_id){
apiCommentService.deleteComment(comment_id);
}
}
设置相关配置
基于PAI的redis缓存不需要@EnableCaching
来开启缓存支持,所以把之前加的@EnableCaching
去掉(当然,不删也没啥关系);然后我们还需要引入redis依赖,并在配置文件中配置redis服务连接,勇士为进行数据存储的Comment实体类实现序列化接口,这些我们在基于注解的redis实现已经做了,那就当我说废话吧
效果测试(略)
这里的效果和基于注解的redis缓存实现一毛一样,大家自己康康
相对于基于注解的方式,基于PAI的方式会更灵活,如手机验证码进行验证时,可以在缓存中设置验证的等待时间,但是代码量可能会更多
在上面的缓存管理中的实体类数据使用的时JDK序列化机制,这样很不便于可视化管理工具的查看和管理,接下来我们就来看看分别基于注解和PAI的数据序列化机制进行讨论,并自定义JSON格式的数据序列化机制进行数据缓存管理
下面我们针对基于注解的Redis缓存机制和自定义序列化方式的实现进行讨论
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
...
//声明了key、value的各种序列化方式,初始值=null
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
...
public RedisTemplate() {
}
@Override
public void afterPropertiesSet() {
//进行默认序列化方式设置,设置为JDK序列化方式
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
//如果defaultSerializer为null,则数据序列化方式为JDK
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
...
由此我们可以知道
JdkSerializationRedisSerializer
系列化方式要求被序列化的实体类继承Serializer
接口defaultSerializer = new JdkSerializationRedisSerializer()
进行序列化的JdkSerializtionRedisSerializer
(JDK自带),也是ReidsTemplate内部默认的序列化方式,我们可以根据需求选择其他支持的序列化方式,如JSONRedisAutoConfiguration
自动配置生效,我们进入RedisAutoConfiguration
类查看有关ReidsTemplate的定义部分...
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")//当某个类不存在时生效
//(接上)如果我们自己开发一个名为redisTemplate的Bean则RedisTemplate会使用这个自定义的Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
...
接下来我们创建一个redis自定义配置类config.RedisConfig
@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模板PAI的序列化方式为JSON
template.setDefaultSerializer(jacksonSeial);
return template;
}
}
RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache)
源码信息(部分)class RedisCacheConfiguration {
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
//通过redis连接工厂redisConnectionFactory定义了缓存管理器
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
...
private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
//使用默认的JdkSerializationRedisSerializer
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
...
return config;
}
}
我们要自定义的话,可以参考上面源码,创建一个名为cacheManager的Bean组件并设置相关序列化即可;在spring boot2.x中RedisCacheManager是单独构建的(1.x则是在RedisTemplate基础上构建),即对RedisTemplate进行自定义序列化机制构建后,既然无法对RedisTemplate内部默认序列化机制进行覆盖(这也是为什么基于注解的redis缓存依旧使用jdk默认序列化机制),因此我们需要自定义RedisCacheManager
@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);
//定制缓存数据序列化方式及时效,将缓存数据有效期设置为1天
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;
}
该SpringBoot学习笔记学习自黑马程序员出版的《Spring Boot企业级开发教程》,是对知识点的整理和自我认识的梳理,如有不当之处,欢迎指出