Fork me on GitHub

spring-boot优雅使用redis集中式缓存

前言

之前的文章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,因为默认的RedisTemplatekeySerializervalueSerializer都是默认的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配置的usePrefixtrue时才生效。
  • @CachePut注解使得数据更新时放入缓存,此处在insertupdate方法时都使用了此注解更新缓存。
  • @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

输出结果分析:

  1. 第一次查询在插入数据之后,并没有打印查询sql表明是从缓存查询。即insert操作的@CachePut注解其效果。
  2. 第二次查询在修改数据之后,也没有打印查询sql表明从缓存查询。继续观察输出的user对象,nickName已经从“老A”变为“老B”了。即update操作的@CachePut注解其效果。
  3. 第三次查询在删除数据之后,打印sql说明未从缓存查询。继续观察输出的user对象为null,说明delete操作的@CacheEvict注解其效果。
  4. 第四次查询在插入数据并删除缓存的情况下,打印sql了说明从数据库查询。再连接redis可以看到: 图上,redis已有缓存,说明select操作也会增加redis缓存。并且输出格式正如RedisTemplate配置的那样,key为字符串,value为json格式。同时设置的缓存时间为2000秒。
-------------本文结束,感谢您的阅读-------------
贵在坚持,如果您觉得本文还不错,不妨打赏一下~
0%