个人博客


在很多应用场景中通常是获取前后相同或更新不频繁的数据,比如访问产品信息数据、网页数据。如果没有使用缓存,则访问每次需要重复请求数据库,这会导致大部分时间都耗费在数据库查询和方法调用上,因为数据库进行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依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@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、数据层

建表语句

1
2
3
4
5
6
7
8
9
10
11
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接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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);
}
}

缓存层接口

1
2
3
4
5
public interface UserCacheService {
User findById(Long id);
User updateUserById(User user);
void deleteById(Long id);
}

这里采用Mybatis来操作数据库,具体操作和配置不在这里展开。

3.3、缓存查询数据

1
2
3
4
5
6
7
8
9
10
11
12
@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、更新缓存数据

1
2
3
4
5
6
7
@Override
@CachePut(key = "#p0.id") // 访问bean属性
@Transactional
public User updateUserById(User user) {
userDao.updateUserById(user);
return user;
}

每次更新完数据库后,同步更新缓存中的数据。需要保持和缓存时的key相同。

3.5、删除缓存数据

1
2
3
4
5
6
@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、测试验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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);
}
}
  1. 在第一次查询时,可以看到有查询数据库的日志输出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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]
  2. 查询Redis,可以看到已经有数据缓存了,过期时间就是前面配置中设置的2小时。

    1
    2
    3
    4
    5
    6
    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
  3. 再次查询,发现依旧可以得到相同结果,但没有查询数据库的日志输出,说明缓存生效了。

  4. 修改字段更新数据库,然后再查询,可以看到缓存数据也被更新。

  5. 删除一条数据后,再根据id查询就得到空,说明缓存中的这条数据也已被删除了。

参考链接

代码地址