在 Spring Boot 中使用 Redis

Redis 本身的一些概念

Redis 支持的数据结构

  • String 字符串
  • Hash 字典
  • List 列表
  • Set 集合
  • Sorted Set 有序集合

String 和 Hash 的对比

String 实际是就是一个 Key - Value 的映射;

Hash 就是一个 Key - (Key - Value) 的两层映射。

# redis-cli
# Redis 中命令不区分大小写。这里命令使用小写,仅在特别的地方用大写。
# 参数使用“大写+下划线”的方式。

# String
set KEY VALUE
get KEY

# Hash
hset HASH_NAME KEY VALUE
hget HASH_NAME KEY
hMset HASH_NAME KEY0 VALUE0 KEY1 VALUE1 ...
hMget HASH_NAME KEY0 KEY1 ...

STACK OVERFLOW 上一个对 String 和 Hash 的讨论

对于一个对象是把本身的数据序列化后用 String 存储,还是使用 Hash 来分别存储对象的各个属性:

  • 如果在大多数时候要访问对象的大部分数据:使用 String
  • 如果在大多数时候只要访问对象的小部分数据:使用 Hash
  • 如果对象里面还有对象这种结构复杂的,最好用 String。否则最外层用 Hash,里面又将对象序列化,两者混用可能导致混乱。

Spring Boot 添加 Redis 的配置

以 gradle 为例。

  • 修改 build.gradle
compile("org.springframework.boot:spring-boot-starter-data-redis")
  • 修改 application.yml
spring:
    # redis
    redis:
        host: 127.0.0.1
        # 数据库索引(默认为0)
        database: 0
        port: 6379
        password: PASSWORD
        # 连接池中的最大空闲连接
        pool.max-idle: 8
        # 连接池中的最小空闲连接
        pool.min-idle: 0
        # 连接池最大连接数(使用负值表示没有限制)
        pool.max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        pool.max-wait: -1
        # 连接超时时间(毫秒)
        timeout: 0
  • 添加 RedisConfig
package zz.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * RedisConfig
 *
 * @author zz
 * @date 2018/5/7
 */
@Configuration
@EnableCaching
@Slf4j
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public KeyGenerator wiselyKeyGenerator() {
        return new KeyGenerator() {
            private static final String SEPARATE = ":";

            @Override
            public Object generate(Object target, Method method, Object... params) {
                log.debug("+++++generate");
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(SEPARATE).append(method);
                for (Object obj : params) {
                    sb.append(SEPARATE).append(obj);
                }
                return sb.toString();
            }
        };
    }

    /**
     * https://www.jianshu.com/p/9255b2484818
     *
     * TODO: 对 Spring @CacheXXX 注解进行扩展:注解失效时间 + 主动刷新缓存
     */
    @Bean
    public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
        log.debug("++++cacheManager");
        RedisCacheManager redisCacheManager =new RedisCacheManager(redisTemplate);
        redisCacheManager.setTransactionAware(true);
        redisCacheManager.setLoadRemoteCachesOnStartup(true);

        // 最终在 Redis 中的 key = @Cacheable 注解中 'cacheNames' + 'key'
        redisCacheManager.setUsePrefix(true);

        // 所有 key 的默认过期时间,不设置则永不过期
        // redisCacheManager.setDefaultExpiration(6000L);

        // 对某些 key 单独设置过期时间
        // 这里的 key 是 @Cacheable 注解中的 'cacheNames'
        Map<String, Long> expires = new HashMap<>(10);
        // expires.put("feedCategoryDto", 5000L);
        // expires.put("feedDto", 5000L);
        redisCacheManager.setExpires(expires);

        return redisCacheManager;
    }


    // value serializer

    private Jackson2JsonRedisSerializer getJackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer 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);

        return jackson2JsonRedisSerializer;
    }

    private GenericJackson2JsonRedisSerializer getGenericJackson2JsonRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    /**
     *
     * Once configured, the template is thread-safe and can be reused across multiple instances.
     * -- https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/
     */
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        log.debug("++++redisTemplate");
        StringRedisTemplate template = new StringRedisTemplate(factory);

        // key serializer
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();


        RedisSerializer valueRedisSerializer;
        // -- 1 Jackson2JsonRedisSerializer
        // valueRedisSerializer = getJackson2JsonRedisSerializer();

        // -- 2 GenericJackson2JsonRedisSerializer
        valueRedisSerializer = getGenericJackson2JsonRedisSerializer();

        // set serializer
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(valueRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(valueRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}


RedisConfig 中定义了三个函数,主要作用如下:

  • wiselyKeyGenerator:定义了一个生成 Redis 的 key 的方法。如下文使用了 @Cacheable 注解的地方,可以指定 key 的生成方法使用我们这个函数。
  • cacheManager:定义了对 Redis 的一些基本设置。
  • redisTemplate:对我们要使用的 RedisTemplate 做一些设置。主要是确定序列化方法。

RedisTemplate 设置序列化器

Spring Redis 虽然提供了对 list、set、hash 等数据类型的支持,但是没有提供对 POJO 对象的支持,底层都是把对象序列化后再以字节的方式存储的。

因此,Spring Data Redis 提供了若干个 Serializer,主要包括:
  • JdkSerializationRedisSerializer: 默认的序列化器。序列化速度快,生成的字节长度较大。
  • OxmSerializer: 生成 XML 格式的字节。
  • StringSerializer: 只能对 String 类型进行序列化。
  • JacksonJsonRedisSerializer:以 JSON 格式进行序列化。
  • Jackson2JsonRedisSerializer:JacksonJsonRedisSerializer 的升级版。
  • GenericJackson2JsonRedisSerializer:Jackson2JsonRedisSerializer 的泛型版。
RedisTemplate 中需要声明 4 种 serializer(默认使用的是 JdkSerializationRedisSerializer):
  • keySerializer :对于普通 K-V 操作时,key 采取的序列化策略
  • valueSerializer:value 采取的序列化策略
  • hashKeySerializer: 在 hash 数据结构中,hash-key 的序列化策略
  • hashValueSerializer:hash-value 的序列化策略

无论如何,建议 key/hashKey 采用 StringRedisSerializer。

by Spring-data-redis: serializer实例

我们设置了 serializer 后,读写 Redis 要使用同一种 serizlizer,否则会读不出之前用不同 serializer 写入的数据。

也就是设置 valueSerializer 为GenericJackson2JsonRedisSerializer,然后写入了数据。
后面要读数据的时候,如果将 valueSerializer 又设置成了 Jackson2JsonRedisSerializer,那么读取数据时就会报错。

通常情况下,我们只需要在 RedisConfig 中统一设置好 4 个 serializer 即可。

Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 的对比

  • 两者都是将对象的数据序列化成 JSON 格式的字符串。
  • Jackson2JsonRedisSerializer 需要自己指定 ObjectMaper 或某个特定的类型。
  • GenericJackson2JsonRedisSerializer 是 Jackson2JsonRedisSerializer 的一个特例,默认支持所有类型。
  • 两者序列化时,都会将原始对象的类名和包名写入 JSON 字符串中。以便反序列化时,确认要将 JSON 转成何种格式。
可用如下方式来获得通用的 Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer 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);

Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 生成 JSON 的对比

# Jackson2JsonRedisSerializer 序列化的效果
127.0.0.1:6379> get 123
"[\"zz.domain.User\",{\"id\":123,\"name\":\"name\"}]"
127.0.0.1:6379> get userList
"[\"java.util.LinkedList\",[[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}],[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}]]]"
# GenericJackson2JsonRedisSerializer 序列化的效果
127.0.0.1:6379> get 123
"{\"@class\":\"zz.domain.User\",\"id\":123,\"name\":\"name\"}"
127.0.0.1:6379> get userList
"[\"java.util.LinkedList\",[{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"},{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"}]]"

如何使用

使用注解来缓存函数的结果

在要缓存的方法上使用注解 @Cacheable@CachePut@CacheEvict 分别用于缓存返回数据、更新缓存数据、删除缓存数据。

package zz.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import zz.domain.User;

/**
 * UserService
 *
 * @author zz
 * @date 2018/5/7
 */
@Service
@Slf4j
public class UserService {
    public final String DEFAULT_NAME = "def";

    @Cacheable(cacheNames = "user", key = "'id_'+#userId")
    public User get(int userId) {
        // get from db
        log.debug("[++] get userId=" + userId);

        User user = new User();
        user.setId(userId);
        user.setName(DEFAULT_NAME);
        log.debug("[++] create default user=" + user);
        return user;
    }

    @CachePut(cacheNames = "user", key = "'id_'+#user.getId()")
    public User update(User user) {
        // save to db
        log.debug("[++] update user=" + user);
        return user;
    }

    @CacheEvict(cacheNames = "user", key = "'id_'+#userId")
    public void delete(int userId) {
        // delete from db
        log.debug("[++] delete userId=" + userId);
    }

    @CachePut(cacheNames = "user", key = "'id_'+#userId")
    public User updateName(int userId, String name) {
        // update to db
        log.debug("[++] updateName userId=" + userId + ", name=" + name);

        User user = get(userId);
        user.setName(name);
        return user;
    }
    
    public void innerCall(int userId) {
        log.debug("[++] innerCall");
        get(userId);
    }
}

  • 对函数的缓存是通过代理来实现的 :
    类内部的某个函数对其他函数(即便被调用函数有 @CacheXXX 注解)的调用是不会走代理的,也就没有缓存。(比如 innerCall 调用 get 时不会使用缓存) 。
  • 注解可以放到 Service、Dao 或 Controller 层。
  • @CacheXXX 会缓存函数的返回值。比如 increaseComment 会缓存更新后的 FeedCount
  • 当缓存中有数据时,@Cacheable 注解的函数不会执行,直接返回缓存中的数据。
  • @CachePut@CacheEvit 注解的函数,无论如何都会执行。

自定义缓存

如果要更细粒度地控制 Redis,可以使用 RedisTemplateStringRedisTemplate

StringRedisTemplate 是 RedisTemplate 的一个特例:key 和 value 都是 String 类型。

  • RedisTemplate 默认使用 JDK 对 key 和 value 进行序列化,转成字节存入 Redis。
  • StringRedisTemplate 的 key、value 本身就是 String,使用 StringRedisSerializer 将 String 转成字节存入 Redis。

当我们将 RedisTemplate 的 keySerializer 和 valueSerializer 都设置成了 StringRedisSerializer,则 RedisTemplate 和 StringRedisTemplate 的效果是相同的,就像下面的样例所示。

RedisTemplate 对 Redis 中各个数据结构的操作

  • redisTemplate.opsForValue();//操作字符串
  • redisTemplate.opsForHash();//操作hash
  • redisTemplate.opsForList();//操作list
  • redisTemplate.opsForSet();//操作set
  • redisTemplate.opsForZSet();//操作有序set
package zz;

import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.test.context.junit4.SpringRunner;
import zz.domain.User;
import zz.service.UserService;

import java.util.LinkedList;
import java.util.List;

/**
 * zz.TestRedis
 *
 * @author zz
 * @date 2018/5/7
 */
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class TestRedis {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    UserService userService;

    @Test
    public void testSerializer() {
        // 1.
        // 这里的 opsForValue().get() 的参数必须转成 String 类型。
        // 除非在 RedisConfig 中 将 keySerializer 设置成 GenericJackson2JsonRedisSerializer 等能将其他类型转换成 String 的。

        // 2.
        // 如果切换了 RedisConfig 中的 ValueSerializer,要先用 redis-cli 将其中的旧数据删除。
        // 不同 Serializer 格式之间的转换可能存在问题。

        final int ID = 123;
        User oldUser;
        oldUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID));
        log.debug("oldUser=" + oldUser);

        User user = new User();
        user.setId(ID);
        user.setName("name");
        log.debug("user=" + user);

        redisTemplate.opsForValue().set(String.valueOf(user.getId()), user);

        User newUser;
        newUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID));
        log.debug("newUser=" + newUser);

        Assert.assertEquals(user.getId(), newUser.getId());
        Assert.assertEquals(user.getName(), newUser.getName());


        List<User> userList = new LinkedList<>();
        userList.add(user);
        user.setId(233);
        user.setName("new");
        userList.add(user);

        redisTemplate.opsForValue().set("userList", userList);
        List<User> newUserList;
        newUserList = (List<User>) redisTemplate.opsForValue().get("userList");

        Assert.assertEquals(userList, newUserList);
    }

    @Test
    public void testSerizlizer2() {
        // 保存用于恢复,以免影响其他部分
        RedisSerializer oldKeySerializer = redisTemplate.getKeySerializer();
        RedisSerializer oldValueSerializer = redisTemplate.getValueSerializer();

        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(redisSerializer);

        final String KEY = "key";
        String VALUE = "value";

        redisTemplate.opsForValue().set(KEY, VALUE);
        Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY));
        Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY));


        VALUE = "Val2";
        stringRedisTemplate.opsForValue().set(KEY, VALUE);
        Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY));
        Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY));


        // 恢复原本设置
        redisTemplate.setKeySerializer(oldKeySerializer);
        redisTemplate.setValueSerializer(oldValueSerializer);
    }


    @Test
    public void testCache() {
        final int USER_ID = 1;

        User user = userService.get(USER_ID);
        log.debug("user=" + user);
        Assert.assertEquals(userService.DEFAULT_NAME, user.getName());

        // 这次会直接返回 cache
        user = userService.get(USER_ID);
        log.debug("user=" + user);

        // 获得修改过的 cache
        final String ANOTHER_NAME = "another user";
        user.setName(ANOTHER_NAME);
        userService.update(user);
        user = userService.get(USER_ID);
        log.debug("user=" + user);
        Assert.assertEquals(ANOTHER_NAME, user.getName());

        // 直接调用 get 会走缓存,通过 innerCall 来调用 get 不会走缓存
        log.debug("------ before");
        userService.get(USER_ID);
        log.debug("------ middle");
        userService.innerCall(USER_ID);
        log.debug("------ after");

        // 另一种修改的方式
        final String NEW_NAME = "updated";
        userService.updateName(USER_ID, NEW_NAME);
        user = userService.get(USER_ID);
        log.debug("user=" + user);
        Assert.assertEquals(NEW_NAME, user.getName());


        // 删除后,cache 中的数据会被删除,name 会变成初始值
        userService.delete(USER_ID);
        user = userService.get(USER_ID);
        log.debug("user=" + user);
        Assert.assertEquals(userService.DEFAULT_NAME, user.getName());

        // 即使 cache 中没有该数据,也会执行 delete 中的逻辑
        userService.delete(USER_ID);
        userService.delete(USER_ID);

    }

}


附录

本文中的完整代码见 spring-boot-redis

    原文作者:yzbyzz
    原文地址: https://www.jianshu.com/p/a899b81f7b65
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞