在很多应用场景中通常是获取前后相同或更新不频繁的数据,比如访问产品信息数据、网页数据。如果没有使用缓存,则访问每次需要重复请求数据库,这会导致大部分时间都耗费在数据库查询和方法调用上,因为数据库进行I/O操作非常耗费时间,这时就可以利用Spring Cache来解决。
Spring Cache 是Spring提供的一整套缓存解决方案。它本身并不提供缓存实现,而是提供统一的接口和代码规范、配置、注解等,以便整合各种Cache方案,使用户不用关心Cache的细节。
Spring Cache作用在方法上。当调用一个缓存方法时,会把方法参数和返回结果作为一个“键值对”(key/value)存放在缓存中,下次用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。所以在使用Spring Cache时,要保证在缓存的方法和方法参数相同时返回相同的结果。
1、声明式注解
注解 | 说明 |
---|---|
@EnableCaching | 启动类声明,用来开启缓存 |
@Cacheable | 可以作用在类和方法上,以键值对的方式缓存类或方法的返回值 |
@CachePut | 方法被调用,然后结果被缓存 |
@CacheEvict | 清空缓存 |
@Caching | 用来组合多个注解标签 |
-
@Cacheable
会先查询是否已有缓存,没有则再执行方法,将返回值缓存起来,key可以有默认策略和自定义策略。用于查询热点数据。 -
@CachePut
每次都会执行方法,并将方法的返回值缓存。用于更新数据。
2、Maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
3、应用缓存
3.1、Redis配置类
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 缓存管理器,适合2.x版本
*
* @param redisConnectionFactory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 对于指定的cacheNames设置缓存的过期时长
Map<String, RedisCacheConfiguration> cacheConfigMap = new HashMap<>();
cacheConfigMap.put("users", this.getCacheConfigurationWithTtl(2 * 60 * 60));
// 构建CacheManager对象,指定的cacheNames过期时长为2小时,其它默认为1小时
return RedisCacheManager.builder(redisConnectionFactory)
.withInitialCacheConfigurations(cacheConfigMap)
.cacheDefaults(this.getCacheConfigurationWithTtl(1 * 60 * 60))
.build();
}
/**
* 设置缓存过期时长
*
* @param seconds 过期时长
* @return
*/
private RedisCacheConfiguration getCacheConfigurationWithTtl(long seconds) {
// 序列化设置
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会抛出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 2.1.x
// om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); // 2.2.x
jacksonSeial.setObjectMapper(om);
// 基本配置
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial))
// 不缓存null
.disableCachingNullValues()
// 缓存数据保存时长
.entryTtl(Duration.ofSeconds(seconds));
return redisCacheConfiguration;
}
}
这里主要配置了序列化方式和对于不同缓存的过期时长设置。
3.2、数据层
建表语句
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(64) COMMENT '姓名',
age int(4) COMMENT '年龄',
PRIMARY KEY (id)
)COMMENT = '用户表';
insert into user values (1, '赵晓斌', 28);
insert into user values (2, '李白', 22);
insert into user values (3, '宋老三', 30);
实体类,一定要实现Serializable
接口。
@Data
public class User implements Serializable {
private static final long serialVersionUID = 5485617646232613710L;
private Long id;
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
}
缓存层接口
public interface UserCacheService {
User findById(Long id);
User updateUserById(User user);
void deleteById(Long id);
}
这里采用Mybatis来操作数据库,具体操作和配置不在这里展开。
3.3、缓存查询数据
@Service
@CacheConfig(cacheNames = "users") // 缓存名称,也是key前缀
public class UserCacheServiceImpl implements UserCacheService {
@Autowired
private UserDao userDao;
@Override
@Cacheable(key = "#p0") // 取第一个参数作为key的一部分,比如users::1
public User findById(Long id) {
return userDao.findById(id);
}
}
根据key值查询缓存,如果没有则执行方法查询数据库,将结果缓存起来,下次同样的key查询时直接从缓存取值返回,不再执行方法。
-
@CacheConfig
注解指定缓存key的前缀为users
。 -
@Cacheable
等注解指定key时,使用SpEL表达式:-
#p0
:表示取第一个参数。 -
#p0.id
:表示取第一个参数的id属性。 -
#id
:表示取参数id。
-
3.4、更新缓存数据
@Override
@CachePut(key = "#p0.id") // 访问bean属性
@Transactional
public User updateUserById(User user) {
userDao.updateUserById(user);
return user;
}
每次更新完数据库后,同步更新缓存中的数据。需要保持和缓存时的key相同。
3.5、删除缓存数据
@Override
@CacheEvict(key = "#id") // 可以用形参名表示key,allEntries = true 则代表 users:: 开头的键全部删除
@Transactional
public void deleteById(Long id) {
userDao.deleteById(id);
}
在删除完数据库的数据后,同步删除缓存中的数据。也是需要保持和缓存时的key相同。
如果需要批量删除某一类key,只需要把@CacheEvict
注解中的allEntries
属性设为true
,那就会清空所有缓存数据(仅限于同一个cacheNames
也就是@CacheConfig
注解指定的key前缀)。
4、测试验证
@RestController
public class CacheController {
@Autowired
private UserCacheService userCacheService;
@RequestMapping("/findById")
public User findById(Long id) {
return userCacheService.findById(id);
}
@RequestMapping("/updateUserById")
public void updateUserById(User user) {
userCacheService.updateUserById(user);
}
@RequestMapping("/deleteById")
public void deleteById(Long id) {
userCacheService.deleteById(id);
}
}
- 在第一次查询时,可以看到有查询数据库的日志输出。
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@608e7bd3] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4945f51a] will not be managed by Spring
==> Preparing: select id, name, age from user where id = ?
==> Parameters: 3(Long)
<== Columns: id, name, age
<== Row: 3, ares, 33
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@608e7bd3]
- 查询Redis,可以看到已经有数据缓存了,过期时间就是前面配置中设置的2小时。
127.0.0.1:9427> keys *
users::3
127.0.0.1:9427> get users::3
["net.zhaoxiaobin.cache.domain.User",{"id":3,"name":"ares","age":22}]
127.0.0.1:9427> ttl users::3
7185
-
再次查询,发现依旧可以得到相同结果,但没有查询数据库的日志输出,说明缓存生效了。
-
修改字段更新数据库,然后再查询,可以看到缓存数据也被更新。
-
删除一条数据后,再根据id查询就得到空,说明缓存中的这条数据也已被删除了。
网友评论