前言
之前的文章spring-boot优雅的使用缓存介绍了使用spring3开始的cache功能, 并使用guava
实现完成一次示例。但是在分布式环境下,进程内的本地缓存是独立的,在一些场景并不使用。
在现在互联网企业中广泛使用了一些中间件比如memcache
,redis
等来实现分布式环境下的集中式缓存。 本文将介绍spring-boot下集成redis做缓存的实现细节。
redis
redis是开源免费的、高性能key-value数据库。 它的特点主要有:
- 高性能,基于内存内的数据结构,读写性能都很好
- 支持持久化,数据支持写进磁盘
- 数据结构有五种形式,string,list,set,zset,hash,各有特点,使用场景广泛。
- 高可用,支持集群
- 支持发布/订阅
现如今大多公司使用redis
,大有替代memcache
的意思(具体根据业务场景选型)。
集成redis
依旧三步完成redis缓存:
- maven依赖
- 配置redis和缓存
- 代码使用及测试
maven依赖
1.4.x
版本的spring-boot可依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
但是从1.5.x
开始已经被废弃,请依赖如下:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件application.yml
添加redis
相关配置,以及缓存开启为redis
.
spring:
# redis
redis:
database: 2
host: 127.0.0.1
port: 6379
password:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 0
# cache
cache:
type: redis
代码使用及单元测试
redis的配置类
在spring-boot-autoconfigure
包中的RedisAutoConfiguration
类中有自动装配了RedisConnectionFactory
,RedisTemplate
,无需多配置即可完成直接调用即可。
但是此处我们想要定制RedisTemplate
,因为默认的RedisTemplate
的keySerializer
和valueSerializer
都是默认的JdkSerializationRedisSerializer
。由于业务的需要(强迫症+颜控),我们希望key
是字符串类型,value
是json格式。如下修改RedisTemplate
的序列化格式即可:
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
cache配置使用redis
当application.yml
指定为spring.cache.type=redis
时,即已经会自动加载spring-boot-autoconfigure
包中的RedisCacheConfiguration
,从源码中可以看到它自动装配了RedisCacheManager
,原则上无需配置直接使用即可。
但是此处默认的RedisCacheManager
不没有设置过期时间,即不过期。这显然不合理。因此需要自定义RedisCacheManager
。
有两种方式自定义RedisCacheManager
:
- 直接装配
RedisCacheManager
- 装配
CacheManagerCustomizer
方式一. 直接手动装配RedisCacheManager
方式如下:
@Bean
public CacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
redisCacheManager.setDefaultExpiration(60L); // 默认缓存 1 分钟
redisCacheManager.setUsePrefix(true);
Map<String, Long> expires = new ConcurrentHashMap<String, Long>(2);
expires.put("userCache", 90L); // 指定 cacheName 缓存时间
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
方式二. spring boot添加了可扩展RedisCacheManager
的方式,即CacheManagerCustomizer
接口。可通过装配CacheManagerCustomizer
的实现,实现其customize(T cacheManager)
方法即可实现自定义:
@Bean
public CacheManagerCustomizer redisCacheManagerCustomizer() {
return (CacheManagerCustomizer<RedisCacheManager>) cacheManager -> {
cacheManager.setDefaultExpiration(60L);// 默认过期时间,单位秒
Map<String, Long> expires = new ConcurrentHashMap<>(2);
expires.put("userCache", 2000L); // 指定 cacheName 缓存时间
cacheManager.setExpires(expires);
};
}
测试redis操作
首先测试通过redisTemplate
来操作redis。测试用例:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = App.class)
public class RedisTemplateTest {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
public void test() {
redisTemplate.opsForValue().set("key1", "value1");
String value1 = redisTemplate.opsForValue().get("key1");
Assert.assertEquals("value1", value1);
}
}
调用正常结束,查看redis库中有此缓存。
测试redis缓存
比如我们有个User服务,代码如下:
@Service
@CacheConfig(cacheNames = "userCache")
public class SbpUserServiceImpl implements SbpUserService {
@Resource
private SbpUserMapper sbpUserMapper;
@Override
@CachePut(key = "'id:'+ #p0.id")
public SbpUser insert(SbpUser user) {
sbpUserMapper.insert(user);
return user;
}
@Override
@CachePut(key = "'id:'+ #p0.id")
public SbpUser update(SbpUser user) {
sbpUserMapper.updateByPrimaryKey(user);
return user;
}
@Override
@CacheEvict(allEntries = true, beforeInvocation = true)// 清空 userCache 缓存
public int insertList(List<SbpUser> list) {
return sbpUserMapper.insertList(list);
}
@Override
public Page<SbpUser> getListPageInfo(int pageNo, int pageSize) {
// 开启分页
Page<SbpUser> page = PageHelper.startPage(pageNo, pageSize);
sbpUserMapper.getAll();
return page;
}
@Override
@CacheEvict(key = "'id:'+#id", beforeInvocation = true)
public boolean deleteById(Long id) {
return sbpUserMapper.deleteByPrimaryKey(id) > 0;
}
@Override
@Cacheable(key = "'id:'+ #id")
public SbpUser getObjectById(Long id) {
return sbpUserMapper.selectByPrimaryKey(id);
}
}
说明:
@CacheConfig(cacheNames = "userCache")
注解表示SbpUserServiceImpl
服务下的所有缓存的前缀加上了”userCache”。注意前提是RedisCacheManager
配置的usePrefix
为true
时才生效。@CachePut
注解使得数据更新时放入缓存,此处在insert
和update
方法时都使用了此注解更新缓存。@CacheEvict
注解用在delete
方法上,并配置了删除时清空”userCache”里的缓存。@Cacheable
设置了查询缓存。
单元测试用例:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = App.class)
@Slf4j
public class SbpUserServiceTest {
@Autowired
private SbpUserService sbpUserService;
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisCache() {
long now = System.currentTimeMillis();
SbpUser user1 = SbpUser.builder().id(666L).mobile("11100006666")
.nickName("老A").password("111111")
.createAt(now).updateAt(now).build();
sbpUserService.insert(user1);
log.info("~~~~~~~~~~~~~insert data~~~~~~~~~~~~~~~~~~~~~~");
log.info("=============first select start===============");
SbpUser user2 = sbpUserService.getObjectById(666L);
log.info("=============first select end=================,user:{}", user2);
user2.setNickName("老B");
sbpUserService.update(user2);
log.info("~~~~~~~~~~~~~update data~~~~~~~~~~~~~~~~~~~~~~");
log.info("=============second select start===============");
SbpUser user3 = sbpUserService.getObjectById(666L);
log.info("=============second select end=================,user:{}", user3);
sbpUserService.deleteById(666L);
log.info("~~~~~~~~~~~~~delete data~~~~~~~~~~~~~~~~~~~~~~");
log.info("=============third select start===============");
SbpUser user4 = sbpUserService.getObjectById(666L);
log.info("=============third select end=================,user:{}", user4);
assert 666L == sbpUserService.insert(SbpUser.builder().id(666L).mobile("11100006666")
.nickName("老A").password("111111")
.createAt(now).updateAt(now).build()).getId();
redisTemplate.delete("userCache:id:666"); // 手动删除redis缓存
log.info("~~~~~~~~~~~~~insert data and delete cache~~~~~~~~~~~~~~~~~~~~~~");
log.info("=============fourth select start===============");
SbpUser user5 = sbpUserService.getObjectById(666L);
log.info("=============fourth select end=================,user:{}", user5);
}
}
控制台输出:
JDBC Connection [com.mysql.jdbc.JDBC4Connection@759f45f1] will not be managed by Spring
==> Preparing: insert into sbp_user (id, nick_name, password, mobile, create_at, update_at ) values (?, ?, ?, ?, ?, ? )
==> Parameters: 666(Long), 老A(String), 111111(String), 11100006666(String), 1532439901401(Long), 1532439901401(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@552ffa44]
2018-07-24 21:45:02.196 [main] INFO com.crw.service.SbpUserServiceTest - ~~~~~~~~~~~~~insert data~~~~~~~~~~~~~~~~~~~~~~
2018-07-24 21:45:02.199 [main] INFO com.crw.service.SbpUserServiceTest - =============first select start===============
2018-07-24 21:45:02.280 [main] INFO com.crw.service.SbpUserServiceTest - =============first select end=================,user:SbpUser(id=666, nickName=老A, password=111111, mobile=11100006666, createAt=1532439901401, updateAt=1532439901401)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@578198d9] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.jdbc.JDBC4Connection@759f45f1] will not be managed by Spring
==> Preparing: update sbp_user set nick_name = ?, password = ?, mobile = ?, create_at = ?, update_at = ? where id = ?
==> Parameters: 老B(String), 111111(String), 11100006666(String), 1532439901401(Long), 1532439901401(Long), 666(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@578198d9]
2018-07-24 21:45:02.286 [main] INFO com.crw.service.SbpUserServiceTest - ~~~~~~~~~~~~~update data~~~~~~~~~~~~~~~~~~~~~~
2018-07-24 21:45:02.287 [main] INFO com.crw.service.SbpUserServiceTest - =============second select start===============
2018-07-24 21:45:02.289 [main] INFO com.crw.service.SbpUserServiceTest - =============second select end=================,user:SbpUser(id=666, nickName=老B, password=111111, mobile=11100006666, createAt=1532439901401, updateAt=1532439901401)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3ff53704] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.jdbc.JDBC4Connection@759f45f1] will not be managed by Spring
==> Preparing: delete from sbp_user where id = ?
==> Parameters: 666(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3ff53704]
2018-07-24 21:45:02.298 [main] INFO com.crw.service.SbpUserServiceTest - ~~~~~~~~~~~~~delete data~~~~~~~~~~~~~~~~~~~~~~
2018-07-24 21:45:02.298 [main] INFO com.crw.service.SbpUserServiceTest - =============third select start===============
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@31b289da] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.jdbc.JDBC4Connection@759f45f1] will not be managed by Spring
==> Preparing: select id, nick_name, password, mobile, create_at, update_at from sbp_user where id = ?
==> Parameters: 666(Long)
<== Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@31b289da]
2018-07-24 21:45:02.326 [main] INFO com.crw.service.SbpUserServiceTest - =============third select end=================,user:null
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58a7ca42] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.jdbc.JDBC4Connection@759f45f1] will not be managed by Spring
==> Preparing: insert into sbp_user (id, nick_name, password, mobile, create_at, update_at ) values (?, ?, ?, ?, ?, ? )
==> Parameters: 666(Long), 老A(String), 111111(String), 11100006666(String), 1532439901401(Long), 1532439901401(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@58a7ca42]
2018-07-24 21:45:02.332 [main] INFO com.crw.service.SbpUserServiceTest - ~~~~~~~~~~~~~insert data and delete cache~~~~~~~~~~~~~~~~~~~~~~
2018-07-24 21:45:02.332 [main] INFO com.crw.service.SbpUserServiceTest - =============fourth select start===============
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16890f00] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.jdbc.JDBC4Connection@759f45f1] will not be managed by Spring
==> Preparing: select id, nick_name, password, mobile, create_at, update_at from sbp_user where id = ?
==> Parameters: 666(Long)
<== Columns: id, nick_name, password, mobile, create_at, update_at
<== Row: 666, 老A, 111111, 11100006666, 1532439901401, 1532439901401
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16890f00]
2018-07-24 21:45:02.338 [main] INFO com.crw.service.SbpUserServiceTest - =============fourth select end=================,user:SbpUser(id=666, nickName=老A, password=111111, mobile=11100006666, createAt=1532439901401, updateAt=1532439901401)
2018-07-24 21:45:02.343 [Thread-4] INFO org.springframework.web.context.support.GenericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@1556f2dd: startup date [Tue Jul 24 21:44:55 CST 2018]; root of context hierarchy
Disconnected from the target VM, address: '127.0.0.1:51531', transport: 'socket'
Process finished with exit code 0
输出结果分析:
- 第一次查询在插入数据之后,并没有打印查询sql表明是从缓存查询。即
insert
操作的@CachePut
注解其效果。 - 第二次查询在修改数据之后,也没有打印查询sql表明从缓存查询。继续观察输出的
user
对象,nickName已经从“老A”变为“老B”了。即update
操作的@CachePut
注解其效果。 - 第三次查询在删除数据之后,打印sql说明未从缓存查询。继续观察输出的
user
对象为null,说明delete
操作的@CacheEvict
注解其效果。 - 第四次查询在插入数据并删除缓存的情况下,打印sql了说明从数据库查询。再连接redis可以看到: 图上,redis已有缓存,说明
select
操作也会增加redis缓存。并且输出格式正如RedisTemplate
配置的那样,key
为字符串,value
为json格式。同时设置的缓存时间为2000秒。