公司注册网站需要什么资料,网站所有权包括,网站关键词提交,开发投资集团Spring Boot 缓存 Cache 入门
1.概述
在系统访问量越来越大之后#xff0c;往往最先出现瓶颈的往往是数据库。而为了减少数据库的压力#xff0c;我们可以选择让产品砍掉消耗数据库性能的需求。 当然也可以引入缓存,在引入缓存之后#xff0c;我们的读操作的代码#xff…Spring Boot 缓存 Cache 入门
1.概述
在系统访问量越来越大之后往往最先出现瓶颈的往往是数据库。而为了减少数据库的压力我们可以选择让产品砍掉消耗数据库性能的需求。 当然也可以引入缓存,在引入缓存之后我们的读操作的代码往往代码如下
// UserService.javaAutowired
private UserMapper userMapper; // 读取 DBAutowired
private UserCacheDao userCacheDao; // 读取 Cachepublic UserDO getUser(Integer id) {// 从 Cache 中查询用户信息UserDO user userCacheDao.get(id);if (user ! null) {return user;}// 如果 Cache 查询不到从 DB 中读取user userMapper.selectById(id);if (user ! null) { // 非空则缓存到 Cache 中userCacheDao.put(user);}// 返回结果return user;
}这段代码是比较常用的缓存策略俗称**“被动写”**。整体步骤如下 1首先从 Cache 中读取用户缓存。如果存在则直接返回。2然后从 DB 中读取用户数据。如果存在写入 Cache 中。3最后返回 DB 的查询结果。
Spring Cache 缓存让我们可以像使用 Transactional 声明式事务使用 Spring Cache 提供的 Cacheable 等注解 声明式缓存。而在实现原理上也是基于 Spring AOP 拦截实现缓存相关的操作。
下面我们使用 Spring Cache 将 #getUser(Integer id) 方法进行简化。代码如下
// UserService.java
public UserDO getUser2(Integer id) {return userMapper.selectById(id);
}// UserMapper.java
Cacheable(value users, key #id)
UserDO selectById(Integer id);在 UserService 的 #getUser2(Integer id) 方法上我们直接调用 UserMapper 从 DB 中查询数据。在 UserMapper 的 #selectById(Integer id) 方法上有 Cacheable 注解。Spring Cache 会拦截有 Cacheable 注解的方法实现“被动写”的逻辑。
2. 注解
在入门 Spring Cache 之前我们先了解下其提供的所有注解
CacheableCachePutCacheEvictCacheConfigCachingEnableCaching
2.1 Cacheable
Cacheable 注解添加在方法上缓存方法的执行结果。执行过程如下
1首先判断方法执行结果的缓存。如果有则直接返回该缓存结果。2然后执行方法获得方法结果。3之后根据是否满足缓存的条件。如果满足则缓存方法结果到缓存。4最后返回方法结果。
Cacheable 注解的常用属性如下
cacheNames 属性缓存名。必填。[] 数组可以填写多个缓存名。values 属性和 cacheNames 属性相同是它的别名。key 属性缓存的 key 。允许空。 如果为空则默认方法的所有参数进行组合。如果非空则需要按照 SpEL(Spring Expression Language) 来配置。例如说Cacheable(value users, key #id) 使用方法参数 id 的值作为缓存的 key 。 condition 属性基于方法入参判断要缓存的条件。允许空。 如果为空则不进行入参的判断。如果非空则需要按照 SpEL(Spring Expression Language) 来配置。例如说Cacheable(condition#id 0) 需要传入的 id 大于零。 unless 属性基于方法返回判断不缓存的条件。允许空。 如果为空则不进行入参的判断。如果非空则需要按照 SpEL(Spring Expression Language) 来配置。例如说Cacheable(unless#result null) 如果返回结果为 null 则不进行缓存。要注意condition 和 unless 都是条件属性差别在于前者针对入参后者针对结果。
Cacheable 注解的不常用属性如下
keyGenerator 属性自定义 key 生成器 KeyGenerator Bean 的名字。允许空。如果设置则 key 失效。cacheManager 属性自定义缓存管理器 CacheManager Bean 的名字。允许空。一般不填写除非有多个 CacheManager Bean 的情况下。cacheResolver 属性自定义缓存解析器 CacheResolver Bean 的名字。允许空。sync 属性在获得不到缓存的情况下是否同步执行方法。 默认为 false 表示无需同步。如果设置为 true 则执行方法时会进行加锁保证同一时刻有且仅有一个方法在执行其它线程阻塞等待。通过这样的方式避免重复执行方法。注意该功能的实现需要参考第三方缓存的具体实现。
2.2 CachePut
CachePut 注解添加在方法上缓存方法的执行结果。不同于 Cacheable 注解它的执行过程如下
1首先执行方法获得方法结果。也就是说无论是否有缓存都会执行方法。2然后根据是否满足缓存的条件。如果满足则缓存方法结果到缓存。3最后返回方法结果。
一般来说使用方式如下
Cacheable搭配读操作实现缓存的被动写。CachePut配置写操作实现缓存的主动写。
Cacheable 注解的属性和 Cacheable 注解的属性基本一致只少一个 sync 属性。
2.3 CacheEvict
CacheEvict 注解添加在方法上删除缓存。
相比 CachePut 注解它额外多了两个属性
allEntries 属性是否删除缓存名( cacheNames )下所有 key 对应的缓存。默认为 false 只删除指定 key 的缓存。beforeInvocation 属性是否在方法执行前删除缓存。默认为 false 在方法执行后删除缓存。
2.4 Caching
Caching 注解添加在方法上可以组合使用多个 Cacheable、CachePut、CacheEvict 注解。不太常用可以暂时忽略。
2.5 CacheConfig
CacheConfig 注解添加在类上共享如下四个属性的配置
cacheNameskeyGeneratorcacheManagercacheResolver
2.6 EnableCaching
EnableCaching 注解标记开启 Spring Cache 功能所以一定要添加。代码如下
// EnableCaching.javaboolean proxyTargetClass() default false;AdviceMode mode() default AdviceMode.PROXY;int order() default Ordered.LOWEST_PRECEDENCE;3. Spring Boot 集成
在 Spring Boot 里提供了 spring-boot-starter-cache 库实现 Spring Cache 的自动化配置通过 CacheAutoConfiguration 配置类。
在 Java 后端开发中常见的缓存工具和框架列举如下 本地缓存Guava LocalCache、Ehcache、Caffeine 。 Ehcache 的功能更加丰富Caffeine 的性能要比 Guava LocalCache 好。 分布式缓存Redis、Memcached、Tair 。 Redis 最为主流和常用。
4.Redis示例
4.1引入依赖 dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- 实现对数据库连接池的自动化配置 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependency !-- 本示例我们使用 MySQL --groupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!-- 实现对 MyBatis Plus 的自动化配置 --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactId/dependency!-- 实现对 Caches 的自动化配置 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-cache/artifactId/dependency!-- 实现对 Spring Data Redis 的自动化配置 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactIdexclusions!-- 去掉对 Lettuce 的依赖因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 --exclusiongroupIdio.lettuce/groupIdartifactIdlettuce-core/artifactId/exclusion/exclusions/dependency!-- 引入 Jedis 的依赖这样 Spring Boot 实现对 Jedis 的自动化配置 --dependencygroupIdredis.clients/groupIdartifactIdjedis/artifactId/dependency!--引入lombok--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency!-- 方便等会写单元测试 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency/dependenciesSpring Data 使用 Redis 作为缓存的方案的时候底层使用的是 Spring Data 提供的 RedisTemplate 所以我们引入 spring-boot-starter-data-redis 依赖实现对 RedisTemplate 的自动化配置。
4.2应用配置文件
spring:# datasource 数据源配置内容datasource:url: jdbc:mysql://127.0.0.1:3306/llp?useSSLfalseuseUnicodetruecharacterEncodingUTF-8driver-class-name: com.mysql.jdbc.Driverusername: rootpassword: root# 对应 RedisProperties 类redis:host: 127.0.0.1port: 6379password: # Redis 服务器密码默认为空。生产中一定要设置 Redis 密码database: 0 # Redis 数据库号默认为 0 。timeout: 0 # Redis 连接超时时间单位毫秒。# 对应 RedisProperties.Jedis 内部类jedis:pool:max-active: 8 # 连接池最大连接数默认为 8 。使用负数表示没有限制。max-idle: 8 # 默认连接数最小空闲的连接数默认为 8 。使用负数表示没有限制。min-idle: 0 # 默认连接池最小空闲的连接数默认为 0 。允许设置 0 和 正数。max-wait: -1 # 连接池最大阻塞等待时间单位毫秒。默认为 -1 表示不限制。# cache 缓存配置内容cache:type: redis# mybatis-plus 配置内容
mybatis-plus:configuration:map-underscore-to-camel-case: true # 虽然默认为 true 但是还是显示去指定下。global-config:db-config:id-type: auto # ID 主键自增logic-delete-value: 1 # 逻辑已删除值(默认为 1)logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)mapper-locations: classpath*:mapper/*.xmltype-aliases-package: com.llp.cache.dataobject# logging
logging:level:# dao 开启 debug 模式 mybatis 输入 sqlcom:llp:cache:mapper: debug
spring.datasource 配置项下设置数据源相关的配置。spring.cache配置项下设置 Cache 相关的配置。 type 属性设置 Cache 使用方案为 Redis 。 spring.redis 配置项下设置 Spring Data Redis 相关的配置。如果没有使用过 Spring Data Redis 的胖友不用慌照着改就好。mybatis-plus 配置项下设置 MyBatis-Plus 相关的配置。logging 配置项设置打印 SQL 日志方便我们查看是否读取了 DB 。
4.3 Application启动类
EnableCaching //开启缓存支持
MapperScan(basePackages {com.llp.cache.mapper})
SpringBootApplication
public class CacheApplication {public static void main(String[] args) {SpringApplication.run(CacheApplication.class, args);}
}4.4UserDO
TableName(value users)
Data
public class UserDO {/*** 用户编号*/private Integer id;/*** 账号*/private String username;/*** 密码明文** ps生产环境下千万不要明文噢*/private String password;/*** 创建时间*/private Date createTime;/*** 是否删除*/TableLogicprivate Integer deleted;}4.5UserMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.llp.cache.dataobject.UserDO;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;Repository
//统一配置该 UserMapper 使用的缓存名为 users ,类的方法上使用cacheNames将不会生效
CacheConfig(cacheNames users)
public interface UserMapper extends BaseMapperUserDO {/*** Cacheable 注解添加在方法上缓存方法的执行结果。执行过程如下** 1首先判断方法执行结果的缓存。如果有则直接返回该缓存结果。* 2然后执行方法获得方法结果。* 3之后根据是否满足缓存的条件。如果满足则缓存方法结果到缓存。* 4最后返回方法结果。* 只有一个参数: #a0 或 #p0* param id* return*/Cacheable(key #a0)UserDO selectById(Integer id);/*** CachePut 注解添加在方法上缓存方法的执行结果。不同于 Cacheable 注解它的执行过程如下** 1首先执行方法获得方法结果。也就是说无论是否有缓存都会执行方法。* 2然后根据是否满足缓存的条件。如果满足则缓存方法结果到缓存。* 3最后返回方法结果。* param user* return*/CachePut(key #user.id)default UserDO insert0(UserDO user) {// 插入记录this.insert(user);// 返回用户return user;}//CacheEvict清理掉缓存,常用于修改和删除CacheEvict(key #p0)int deleteById(Integer id);}4.6UserMapperTest
RunWith(SpringRunner.class)
SpringBootTest(classes CacheApplication.class)
public class UserMapperTest {private static final String CACHE_NAME_USER users;Autowiredprivate UserMapper userMapper;Autowiredprivate CacheManager cacheManager;Testpublic void testCacheManager() {System.out.println(cacheManager);}Testpublic void testSelectById() {// 这里胖友事先插入一条 id 1 的记录。Integer id 1;// 1.1 查询 id 1 的记录UserDO user userMapper.selectById(id);System.out.println(user user);// 1.2 判断缓存中是不是存在Assert.assertNotNull(缓存为空, cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));// 2 查询 id 1 的记录user userMapper.selectById(id);System.out.println(user user);}Testpublic void testInsert() {// 1 插入记录UserDO user new UserDO();user.setUsername(UUID.randomUUID().toString()); // 随机账号因为唯一索引user.setPassword(llp);user.setCreateTime(new Date());user.setDeleted(0);userMapper.insert0(user);// 2 判断缓存中是不是存在Assert.assertNotNull(缓存为空, cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));}Testpublic void testDeleteById() {// 1 插入记录为了让缓存里有记录UserDO user new UserDO();user.setUsername(UUID.randomUUID().toString()); // 随机账号因为唯一索引user.setPassword(llp);user.setCreateTime(new Date());user.setDeleted(0);userMapper.insert0(user);// 2 判断缓存中是不是存在Assert.assertNotNull(缓存为空, cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));// 3.1 删除记录为了让缓存被删除userMapper.deleteById(user.getId());// 3.2 判断缓存中是不是存在Assert.assertNull(缓存不为空, cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));}}4.7过期时间
在 Spring Data 使用 Redis 作为缓存方案时默认情况下是永不过期的。
127.0.0.1:6379 ttl users::1
(integer) -1在 Redis 命令行中我们可以看到 users::1 的过期时间为 -1 永不过期。
虽然说我们可以通 spring.cache.redis.time-to-live 配置项设置过期时间。但是它是全局的统一的。这样在实际使用时是无法满足我们希望不同的缓存使用不同的过期时间。
spring:# datasource 数据源配置内容datasource:url: jdbc:mysql://127.0.0.1:3306/llp?useSSLfalseuseUnicodetruecharacterEncodingUTF-8driver-class-name: com.mysql.jdbc.Driverusername: rootpassword: root# 对应 RedisProperties 类redis:host: 127.0.0.1port: 6379password: # Redis 服务器密码默认为空。生产中一定要设置 Redis 密码database: 0 # Redis 数据库号默认为 0 。timeout: 0 # Redis 连接超时时间单位毫秒。cache:type: redisredis:time-to-live: 1h4.8 Cacheable(Redis)缓存失效时间解决方案
问题
Cacheable注解不支持配置过期时间所有需要通过配置CacheManneg来配置默认的过期时间和针对每个类或者是方法进行缓存失效时间配置。
解决
可以采用如下的配置信息来解决的设置失效时间问题
配置信息
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;Configuration
public class CacheConfig {Beanpublic CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),this.getRedisCacheConfigurationWithTtl(30 * 60), // 默认策略未配置的 key 会使用这个this.getRedisCacheConfigurationMap() // 指定 key 策略);}private MapString, RedisCacheConfiguration getRedisCacheConfigurationMap() {MapString, RedisCacheConfiguration redisCacheConfigurationMap new HashMap();//SsoCache和BasicDataCache进行过期时间配置redisCacheConfigurationMap.put(SsoCache, this.getRedisCacheConfigurationWithTtl(24 * 60 * 60));redisCacheConfigurationMap.put(BasicDataCache, this.getRedisCacheConfigurationWithTtl(30 * 60));return redisCacheConfigurationMap;}private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {Jackson2JsonRedisSerializerObject jackson2JsonRedisSerializer new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);RedisCacheConfiguration redisCacheConfiguration RedisCacheConfiguration.defaultCacheConfig();redisCacheConfiguration redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofSeconds(seconds));return redisCacheConfiguration;}//指定缓存key的生成方式Beanpublic KeyGenerator wiselyKeyGenerator() {KeyGenerator keyGenerator new KeyGenerator() {Overridepublic Object generate(Object target, Method method, Object... params) {StringBuilder sb new StringBuilder();sb.append(target.getClass().getName());sb.append(. method.getName());if (params null || params.length 0 || params[0] null) {return null;}String join String.join(, Arrays.stream(params).map(Object::toString).collect(Collectors.toList()));String format String.format(%s{%s}, sb.toString(), join);//log.info(缓存key format);return format;}};return keyGenerator;}}
使用方式
Repository
//统一配置该 UserMapper 使用的缓存名为 users ,方法中使用了cacheNames,则类的方法上使用cacheNames将不会生效
CacheConfig(cacheNames users)
public class SsoCache{Cacheable(value BasicDataCache,keyGenerator wiselyKeyGenerator)UserDO selectById(Integer id);//二者选其一,可以使用value上的信息来替换类上cacheNames的信息Cacheable(value SsoCache,keyGenerator wiselyKeyGenerator)UserDO selectById(Integer id);
}BasicDataCache SsoCache