百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

springboot cache redis优雅实现缓存

mhr18 2024-11-19 06:58 19 浏览 0 评论

导读

你还在代码里面写查询数据,然后写入缓存代码吗?没错,你曾经应该都是这么做,做得很累,代码很臃肿,那么从本文开始,可以抛掉了,让我们缓存代码更加优雅起来,更加简单起来。

Spring Cache前身

Spring cache是使用org.springframework.cache.Cache和org.springframework.cache.CacheManager来管理接口,其中CacheManager是Spring提供的各种缓存技术的抽象接口。而Cache接口包含缓存的各种操作。

Cache接口是缓存组件规范,操作各种缓存数据,提供支持各种缓存框架实现,如:RedisCache,ConcurrentMapCache,EhcacheCache等,RedisCache是我们最常用的。

Spring cache 使用注解来简化实现缓存开发,是基于AOP技术实现。

Spring CacheManger相关技术说明

SimpleCacheManager	                使用简单的来存储缓存,单一存储,很少用
ConcurrentMapCacheManager	  使用ConcurrentMap作为缓存技术(默认),需要显式的删除缓存,无过期机制
NoOpCacheManager	                 不用存储缓存
EhCacheCacheManager	              使用EhCache作为缓存技术,以前在很老系统可能用到
GuavaCacheManager	                  使用google guava cache作为缓存技术
CaffeineCacheManager	              是使用Java8对Guava缓存的重写,spring5(springboot2)开始用Caffeine取代guava
HazelcastCacheManager	             使用Hazelcast作为缓存技术
JCacheCacheManager	                使用JCache标准的实现作为缓存技术,如Apache Commons JCS
RedisCacheManager	                    使用Redis作为缓存技术

最常用:RedisCacheManager,系统默认使用ConcurrentMapCacheManager是单机JVM内存,不适合分布式多台服务

缓存概念以及注解说明

Cache	                  缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager	   缓存管理器,管理各种缓存组件,如上说明

@Cacheable	        主要针对方法配置,能够根据方法的请求参数对其进行缓存(必用)
@CacheEvict	        清空缓存(必用)
@CachePut	          保证方法被调用,更新结果被缓存。常用于更新后操作(可用)

@EnableCaching	 开启基于注解的缓存(必须)

@CacheConfig	    统一配置本类的缓存注解的属性(可用)

keyGenerator	      缓存数据时key生成策略
serialize	                缓存数据时value序列化策略

@Cacheable/@CachePut/@CacheEvict 重要参数说明

参数:value         缓存名称,必填,如:@Cacheable(value = "user")
参数:key            缓存key,非必填,使用SpEL表达式,一般用在参数中,如:@Cacheable(value = "user",key = "#userId")
参数:condition  表达式条件满足缓存,非必填,使用SpEL表达式,满足条件true缓存,false不缓存,如:@Cacheable(value = "user:condition",condition="#userPO.userName.length()>2")
参数:unless       表达式条件满足不缓存,非必填,使用SpEL表达式,跟condition参数反向,如:@Cacheable(value = "user:unless",unless ="#userPO.id>3")
参数:sync           @Cacheable特有,多线程情况下可能出现重复载入缓存情况,设置为true可解决,默认是false
参数:allEntries   @CacheEvict特有,清除下面value下面所有的::缓存数据,设置不设置key参数都不影响删除结果,如:@CacheEvict(value = "user",key = "#userPO.id",allEntries = true)
参数:beforeInvocation 
                           @CacheEvict特有,是否执行方法前就清除了数据,默认是false执行方法后再清除缓存,设置true执行方法前就清除了数据。
                            如:@CacheEvict(value = "user",key = "#userPO.id",allEntries = true,beforeInvocation = true)

注解:@CachePut     更新缓存后,又重新把数据放入缓存,如:@CachePut(value = "user",key = "#userPO.id")

SpEL表达式说明

名称	                         位置	                    描述	                                                                          示例
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
Argument  Name	    执行上下文	        当前被调用的方法的参数
                                                            如:getUser(User user),可以通过#user.id获得参数	     #user.id
result	                       执行上下文	       方法执行后的返回值(仅当方法执行后的判断有效)      #result

SpEL运算符

类型	               运算符
关系	               <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算术	               +,- ,* ,/,%,^
逻辑	               &&,||,!,and,or,not,between,instanceof
条件	               ?: (ternary),?: (elvis)
正则表达式	      matches
其他类型	        ?.,?[…],![…],^[…],$[…]

开始项目

新建一个springboot项目,导入springboot依赖包版本,同时项目新增改查依赖mybatis-plus具体整合,日志用lombok,不懂整合这两个技术的可以咨询我

 <!-- 引入springboot版本依赖包 -->
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/>
   </parent>
 <!-- 引入springboot-web版本依赖包 -->
  <dependencies>
    
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
</dependencies>

引入SpringBoot Cache依赖包

<!-- springboot cache 依赖 -->
<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

整合Redis依赖包

<!-- 整合 Redis -->
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置application.properties

server.port=8010
server.servlet.context-path=/

################ Redis 基础配置 ##############
# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 链接超时时间 单位 ms(毫秒)
spring.redis.timeout=3000

编写SpringBoot启动类

@SpringBootApplication
public class UserProvideApp 
{
    public static void main( String[] args )
    {
        SpringApplication.run(UserProvideApp.class, args);
    }
}

编写开启@EnableCaching注解类

创建SpringCacheConfig类,在该类上新增@EnableCaching注解,该注解是必须要配置的,也可以直接配置在启动类UserProvideApp上

/**
 * 开启 @EnableCaching 注解,必须要开启才能用到缓存注解类<br>
 * 这个注解可以配置在启动类上
 */
@Configuration
@EnableCaching
public class SpringCacheConfig {

}

新建UserPO

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("user")
public class UserPO implements Serializable {

    private static final long serialVersionUID=1L;

    /**
     * 用户ID
     */
    private Long id;

    /**
     * 用户名
     */
    private String userName;
    
    /**
     * 密码
     */
    private String pwd;
    
    /**
     * 回答总数
     */
    private Integer qaReplyCount;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

新建UserCacheService缓存类

UserCacheServiceImpl实现类如下,UserCacheService自行实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hsb.user.po.UserPO;
import com.hsb.user.provide.dao.UserDAO;

import lombok.extern.slf4j.Slf4j;

/**
 * 用户缓存,业务接口
 */
@Service
@Slf4j
public class UserCacheServiceImpl implements UserCacheService{
	
	@Autowired
	UserDAO userDAO;
	
    /**
     * 参数缓存<br>
     * value 参数必填,缓存名称<br>
     * key   缓存key,参数表达式要和参数名称一样<br>
     * 最终缓存key user:(userId)组合,如:user::2<br>
     */
	@Cacheable(value = "user",key = "#userId")//user:1
	public UserPO getByUserId(Long userId) {
		log.info("查询用户ID={}",userId);
		return userDAO.selectById(userId);
	}
	
	/**
	 * 对象参数缓存<br>
     * value 参数必填,缓存名称<br>
     * key   缓存key,参数表达式要和对象参数名称一样<br>
     * 最终缓存key user:po:(userId)组合<br>
     */
	@Cacheable(value = "user:po",key = "#userPO.id")//user:1
	public UserPO getByUser(UserPO userPO) {
		log.info("查询用户userPO={}",userPO);
		QueryWrapper<UserPO> queryWrapper=new QueryWrapper<UserPO>(userPO);
		return userDAO.selectOne(queryWrapper);
	}

	/**
	 * 表达式缓存,满足条件缓存<br>
     * value 参数必填,缓存名称<br>
     * condition   表达式,满是缓存<br>
     * 最终缓存key user:condition:(userName)组合<br>
     */
	@Cacheable(value = "user:condition",key = "#userPO.userName",condition="#userPO.userName.length()>2")
	public UserPO getByUserCondition(UserPO userPO) {
		log.info("查询用户UserCondition={}",userPO);
		QueryWrapper<UserPO> queryWrapper=new QueryWrapper<UserPO>(userPO);
		return userDAO.selectOne(queryWrapper);
	}

	/**
	 * 表达式缓存,满足条件不缓存,反之缓存,跟condition参数刚刚相反<br>
	 * 
     * value 参数必填,缓存名称<br>
     * unless   表达式,满足不缓存<br>
     * 最终缓存key user:cunless:组合<br>
     */
	@Cacheable(value = "user:unless",unless ="#userPO.id>3")
	public UserPO getByUserUnless(UserPO userPO) {
		log.info("查询用户UserUnless={}",userPO);
		QueryWrapper<UserPO> queryWrapper=new QueryWrapper<UserPO>(userPO);
		return userDAO.selectOne(queryWrapper);
	}
	
	/**
	 * 1、删除数据<br>
	 * 
	 * 2、清除缓存<br>
	 * 
	 * 3、参数:allEntries 清除下面value下面所有的::缓存数据,设置不设置key参数不影响删除结果<br>
	 * 如:清除:user::xx 所有数据<br>
	 * 4、如果想删除指定value+key,去掉allEntries参数即可<br>
	 * 
	 * @param userPO
	 * @return
	 */
	@CacheEvict(value = "user",key = "#userPO.id",allEntries = true)
	public int delUserPo(UserPO userPO) {
		log.info("删除用户userPO={}",userPO);
		QueryWrapper<UserPO> queryWrapper=new QueryWrapper<UserPO>(userPO);
		return userDAO.delete(queryWrapper);
	}
	
	/**
	 * 1、删除数据<br>
	 * 2、清除指定key缓存,不是删除value下面所有所有<br>
	 * 3、beforeInvocation 是否执行方法前就清除了数据,默认是false执行方法后再清除缓存,设置true执行方法前就清除了数据<br>
	 * 4、异常测试,指定方法前指定了删除操作<br>
	 * 
	 * @param userPO
	 * @return
	 */
	@CacheEvict(value = "user",key = "#userPO.id",beforeInvocation = true)
	public int delUserPoBybeforeInvocation(UserPO userPO) {
		log.info("删除用户beforeInvocation-userPO={}",userPO);
		int i=1/0;//异常测试,指定方法前指定了删除操作
		QueryWrapper<UserPO> queryWrapper=new QueryWrapper<UserPO>(userPO);
		return userDAO.delete(queryWrapper);
	}
	
	/**
	 * @CachePut 注解使用<br>
	 * 
	 * 更新用户后,从新把更新信息放入缓存<br>
	 * 
	 * @param userPO
	 * @return
	 */
	@CachePut(value = "user",key = "#userPO.id")
	public UserPO updateUser(UserPO userPO) {
		log.info("更新用户userPO={}",userPO);
		int rt=userDAO.updateById(userPO);
		if(rt<=0) {
			throw new RuntimeException("更新用户失败userPo="+userPO);
		}
		return getByUser(userPO);
	}
}

编写UserCacheController

/**
 * 用户缓存测试
 */
@RestController
@RequestMapping("/user/cache")
public class UserCacheController {
	
	@Autowired
	UserCacheService userCacheService;
	
	@GetMapping("/getByUserId")
	public UserPO getByUserId(Long userId) {
		return userCacheService.getByUserId(userId);
	}
	
	/**
	 * 对象参数缓存
     * value 参数必填,缓存名称
     * key   缓存key,参数表达式要和对象参数名称一样
     * 最终缓存key user:po:(userId)组合
     */
	@GetMapping("/getByUser")
	public UserPO getByUser(UserPO userPO) {
		return userCacheService.getByUser(userPO);
	}

	/**
	 * 表达式缓存,满足条件缓存
     * value 参数必填,缓存名称
     * condition   表达式,满是缓存
     * 最终缓存key user:condition:(userName)组合
     */
	@GetMapping("/getByUserCondition")
	public UserPO getByUserCondition(UserPO userPO){
		return userCacheService.getByUserCondition(userPO);
	}

	/**
	 * 表达式缓存,满足条件不缓存,跟condition参数刚刚相反
	 * 
     * value 参数必填,缓存名称
     * unless   表达式,满足不缓存
     * 最终缓存key user:cunless:组合
     */
	@GetMapping("/getByUserUnless")
	public UserPO getByUserUnless(UserPO userPO) {
		return userCacheService.getByUserUnless(userPO);
	}
	
	/**
	 * 删除数据
	 * 
	 * 清除缓存
	 * 
	 * 参数:allEntries 清除下面所有的缓存数据
	 * 
	 * @param userPO
	 * @return
	 */
	@GetMapping("/delUserPo")
	public int delUserPo(UserPO userPO){
		return userCacheService.delUserPo(userPO);
	}
	
	/**
	 * 删除数据
	 * 
	 * beforeInvocation 是否执行方法前就清除了数据,默认是false执行方法后再清除缓存,设置true执行方法前就清除了数据
	 * 
	 * @param userPO
	 * @return
	 */
	@GetMapping("/delUserPoBybeforeInvocation")
	public int delUserPoBybeforeInvocation(UserPO userPO){
		return userCacheService.delUserPoBybeforeInvocation(userPO);
	}
	
	/**
	 * @CachePut 注解使用
	 * 
	 * 更新用户后,从新把更新信息放入缓存
	 * 
	 * @param userPO
	 * @return
	 */
	@GetMapping("/updateUser")
	public UserPO updateUser(UserPO userPO){
		return userCacheService.updateUser(userPO);
	}
}

启动服务

启动服务,就可以通过浏览器访问测试每个使用的效果了。

测试地址    如:http://127.0.0.1:8010/user/cache/getByUserId?userId=8

看到缓存结果



其他测试接口,如有不懂的,可以关注我,跟我私聊。

从上面可以看到缓存结果是一推密密麻麻的乱码,看不懂,这是因为默认采用的是JdkSerializationRedisSerializer序列化,如果你想把这个结果看得懂,如结果使用json格式,那么也是转换成指定的序列即。

引入Fastjson依赖

 <!--alibaba fastjson -->
         <dependency>
		  	<groupId>com.alibaba</groupId>
		  	<artifactId>fastjson</artifactId>
		  	<version>1.2.73</version>
		  </dependency>

新增序列化Bean

在SpringCacheConfig类上,编写配置Fastjson序列化对象,如下

/**
	  * 配置Fastjson序列化对象种作为缓存结果
	  * @return
	  */
	 @Bean  
	 public RedisCacheConfiguration redisCacheConfiguration() {
	    //使用fastjson序列化对象
		RedisSerializer<?> valSeri=new GenericFastJsonRedisSerializer();
	    return RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valSeri));
	 }

重启服务

重新访问测试上面路径,就可以看到缓存结果是json格式了,如下图所示:


缓存过期时间

经过以上发现,没有哪个参数可以设置一下过期时间,现在需求就是设置一下缓存过期时间,那么如何做呢?我们需要重新编辑SpringCacheConfig类上面序列化Bean代码,如下重写代码

/**
	  * 配置Fastjson序列化对象种作为缓存结果
	  * @return
	  */
	 @Bean  
	 public RedisCacheConfiguration redisCacheConfiguration() {
	    //使用fastjson序列化对象
		RedisSerializer<?> valSeri=new GenericFastJsonRedisSerializer();
		
		RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
			.serializeValuesWith(
	            RedisSerializationContext
	                .SerializationPair
	                .fromSerializer(valSeri)
	        ).entryTtl(Duration.ofSeconds(30));//所有的缓存都是默认30秒后过期

	    return redisCacheConfiguration;
	 }

这样就可以设置所有缓存key都是30秒以上都过期了,很简单吧。

现在问题又来了,如果需要对特定的缓存名称,设置不同时间呢?那又如何处理?我们得继续改进配置SpringCacheConfig缓存序列相关配置对象,代码如下

   //================配置特定key缓存过期时间================
	 /**
	  * 新增配置缓存管理Bean
	  * @param redisConnectionFactory
	  * @return
	  */
	 @Bean
	 public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
		 CacheManager cacheManager=new RedisCacheManager(
				  RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)
				 , defaulRedisCacheConfiguration() //默认缓存配置30秒过期
				 , redisCacheConfigurationMap());//指定缓存key过期
		 return cacheManager;
	 }
	 
	 /**
	  * 配置过期时间
	  * @param seconds
	  * @return
	  */
	 private RedisCacheConfiguration redisCacheConfiguration(Long seconds) {
		    //使用fastjson序列化对象
			RedisSerializer<?> valSeri=new GenericFastJsonRedisSerializer();
			
			RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
				.serializeValuesWith(
		            RedisSerializationContext
		                .SerializationPair
		                .fromSerializer(valSeri)
		        ).entryTtl(Duration.ofSeconds(seconds));//所有的缓存都是默认30秒后过期

		    return redisCacheConfiguration;
		 }
	 
	 /**
	  * 配置多个value配置使用不同配置
	  * @return
	  */
	 private Map<String, RedisCacheConfiguration> redisCacheConfigurationMap(){
		 Map<String, RedisCacheConfiguration> map=new HashMap<String, RedisCacheConfiguration>();
		 map.put("user", redisCacheConfiguration(20l));//指定value=user 缓存过期为20秒
		 map.put("user:po", redisCacheConfiguration(15l));//指定value=user:po 缓存过期为15秒
		 return map;
	 }

代码里面就指定了特定value缓存名称对应的过期时间,如果有新的需要单独处理,新增代码Map对象即可,重启服务,测试通过

总结

利用springboot整合spring cache,redis,fastjson优雅实现了缓存存在,大大地提高开发效率,满足了部分缓存业务实现功能,唯一不足地方就是该框架缓存过期时间设计的不是非常灵活,有点繁琐,但是整体还是不错的,如大家使用或者觉得不理解的地方,欢迎大家讨论。

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: