Spring Boot 2系列(十二):Spring Data Redis 集成详解与使用

  Redis 是基于 key-value 键 / 值对的开源内存数据存储系统,现在非常流行用作缓存存储。

  Spring Boot 集成 Redis 非常简单,也容易使用。Spring Boot 自动注册了 RedisConnectionFactory ,并提供了RedisTemplateStringRedisTemplate 两个模板来操作数据。所以在 Spring Boot 环境,只需配置下 Redis 的连接参数就可以直接使用了。

  Spring Boot 对 Redis 自动配置的支持依赖于 Sping Data Redis。Spring Data Redis 将数据操作抽象出了统一的方法便于使用。更多参考 官方 Spring Data Redis 项目

Spring Data Redis

Spring Data Redis 提供了两种方式来连接 Redis,分别是 LettuceConnectionFactoryRedisConnectionFactoryLettuce 是基于 Netty 的开源连接器,性能更高,在多线程并发情况下,连接是线程安全的; 而 Jedis 在多线程并发下不是线程安全的,就需要使用线程池来给每个线程创建物理连接。

在Spring Boot 项目里不需要人为配置这两个工厂 Bean,默认集成的是 Lettuce 依赖,使用的是**LettuceConnectionFactory **创建连接。

LettuceConnectionFactory

1
2
3
4
5
6
7
8
9
@Configuration
class AppConfig {

@Bean
public LettuceConnectionFactory redisConnectionFactory() {

return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379));
}
}

RedisConnectionFactory

1
2
3
4
5
6
7
8
@Configuration
class AppConfig {

@Bean
public RedisConnectionFactory redisConnectionFactory(){
return new JedisConnectionFactory();
}
}

RedisTemplate

Spring Data Redis 对操作提供了高层的抽象,可以自定义序列化和连接管理,提供了丰富的操作接口,下面列有是常用的操作接口:

Interface Description
HashOperations Redis hash operations
ListOperations Redis list operations
SetOperations Redis set operations
ValueOperations Redis string (or value) operations
ZSetOperations Redis zset (or sorted set) operations

该模板是线程安全的,可在并发多线程重复使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Example {

// inject the actual template
@Autowired
private RedisTemplate<String, String> template;

// inject the template as ListOperations
@Resource(name="redisTemplate")
private ListOperations<String, String> listOps;

public void addLink(String userId, URL url) {
listOps.leftPush(userId, url.toExternalForm());
}
}

RedisTemplate 默认的的序列化方式是采用 JDK的二进制来序列化,键值用户不可读。

StringRedisTemplate

由于存储在 Redis 中的通常是 String 类型,Redis模块为 RedisConnectionRedisTemplate 提供了两个扩展,分别为 StringRedisConnection(及其DefaultStringRedisConnection实现)和 StringRedisTemplate。为大量的字符串操作提供了便 捷的操作。 除了绑定到 String 键之外,模板和连接使用 StringRedisSerializer来序列化,这样存储的键和值是用户是可读的(前提是存在 Redis 和代码中都使用相同的编码)。

1
2
3
4
5
6
7
8
9
public class Example {

@Autowired
private StringRedisTemplate redisTemplate;

public void addLink(String userId, URL url) {
redisTemplate.opsForList().leftPush(userId, url.toExternalForm());
}
}

CacheManager

若需要,也可自定义缓存管理器。

1
2
3
4
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate){
return new RedisCacheManager(redisTemplate);
}

KeyGenerator

默认使用的是 org.springframework.cache.interceptor.SimpleKeyGenerator 来自动生成 Key。也可自定义键生成策略。下面示例:类名 + 方法名 + 参数 来生成缓存的 Key,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(":" + method.getName());
for (Object obj : objects) {
sb.append(":" + obj.toString());
}
return sb;
}
};
}

使用自定义键生成策略

1
2
3
4
@Cacheable(value = "#id", keyGenerator = "keyGenerator")
public Actor getById(String id){
return actorRepository.findById(id);
}

Redis Resporites

Spring-data-redis 提供了 Redis Repositories 来支持 Redis 缓存操作,可以通过继承 CrudRepository 或 JpaRepository 接口调用已提供的方法来操作传统数据库,再结合缓存注解来对数据进行缓存到 redis 的操作。

  1. 在应用启动类上或 java 配置类上添加注解 @EnableRedisRepositories 开启Redis Repositories 的支持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Configuration
    @EnableRedisRepositories
    public class ApplicationConfig {

    @Bean
    public RedisConnectionFactory connectionFactory() {
    return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {

    RedisTemplate<?, ?> template = new RedisTemplate<>();
    return template;
    }
    }
  2. 定义实体类

@Id 类似于数据库中的主键,可自动生成;**@RedisHash** value 属性定义 Hash 名称,timeToLive 属性设置有过期时长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//@Entity(name = "actor")
//@RedisHash(value = "User")
@RedisHash(value = "User", timeToLive = 3600)
public class User implements Serializable {

private static final long serialVersionUID = -336743741742403986L;

//自动生成键
//@Id
//private String id;

@Id
private Long userId;
private String firstName;
private String lastName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastUpdate;
//----set/get 方法-----
}
  1. 定义 Repository 接口继承 CrudRepository 或 JpaRepository
1
2
3
4
@Repository
public interface UserRepository extends CrudRepository<User, Long> {

}
  1. 业务层注入实体类的 Repository,调用 Repository 的方法来对数据库进行操作,在方法上添加缓存注解来对数据进行缓存操作

    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
    @Service
    public class UserServiceImpl implements UserService {

    //注入实体类的Repository
    @Autowired
    private UserRepository userRepository;

    /**
    * 使用Cache注解
    * 先查缓存,没有再查库,再写入缓存
    * 可使用 @Cacheable(key = "#id", value="user")来自动生成键
    */
    @Override
    @Cacheable(key = "#userId", value = "user")
    public User queryByUserId(Long userId) {
    User user = userRepository.findById(userId).get();
    return user;
    }

    /**
    * 调用 CrudRepository 方法
    */
    @Override
    public User queryByUserId(Long userId) {
    User user = userRepository.findById(userId).get();
    userRepository.save(user);
    return user;
    }
    }
  2. Spring Boot 的自动配置里默认就开启了对 Redis Repositories 的支持
    在配置文件里可通过以下属性来设置是否开启

    spring.data.redis.repositories.enabled=true

注(个人理解):
RedisTemplate 相当于 Redis 的客户端,是把 Redis 作为纯数据库来操作,这个数据库是在计算机物理内存中,自身具有缓存的特性;
Redis Repositories 结合缓存注解来使用,是把 Redis 作为 传统数据库的缓存来操作,相当于传统数据库上加了缓存层;这两者存储的数据在 Redis 中的表现也不同,Redis Repositories 存入 Redis 的数据是有与实体类映射关系的,有对象的概念,每条数据相当于一个实例。而 RedisTemplate 存入 Redis 的数据就单纯是条数据。

CacheErrorHandler

默认的异常处理是抛出异常,会导致后续业务中断,处理类是 org.springframework.cache.interceptor.SimpleCacheErrorHandler

可自定义缓存异常处理,实现 org.springframework.cache.interceptor.CacheErrorHandler 接口,重定里面的方法,例如捕获异常记录日志,而不是抛出异常,这样即使缓存操作异常也不会影响业务功能。

Spring Boot Redis

添加依赖

pom.xml

1
2
3
4
5
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Jedis 替换 Lettuce

若需要使用 Jedis 替换 Lettuce,则使用下面依赖,但不建议替换, lettuce 是基于 netty 实现的客户端连接,性能更优且是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

若使用默认的 Lettuce ,配置连接池需要依赖 apache commons-pool2

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

redis参数配置

1
2
3
4
5
6
7
8
spring.redis.database=0
spring.redis.host=192.168.220.128
spring.redis.password=123456
spring.redis.port=6379
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1ms

自定义序列化

  1. 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- fastjson -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
    </dependency>
    <!-- jackson serializer msgpack -->
    <dependency>
    <groupId>org.msgpack</groupId>
    <artifactId>jackson-dataformat-msgpack</artifactId>
    <version>0.8.16</version>
    </dependency>
    <!--kryo-->
    <dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.2</version>
    </dependency>
  2. 自定义Serializer:StringRedisSerializer

    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
    /**
    * @name: StringRedisSerializer
    * @desc: 重写StringRedisSerializer,支持对象数据序列化,默认只支持String数据
    **/
    public class StringRedisSerializer implements RedisSerializer<Object> {

    private final Charset charset;

    private final String target = "\"";

    private final String replacement = "";

    public StringRedisSerializer() {
    this(Charset.forName("UTF8"));
    }

    public StringRedisSerializer(Charset charset) {
    Assert.notNull(charset, "Charset must not be null!");
    this.charset = charset;
    }

    @Override
    public String deserialize(byte[] bytes) {
    return (bytes == null ? null : new String(bytes, charset));
    }

    @Override
    public byte[] serialize(Object object) {
    String string = JSON.toJSONString(object);
    if (string == null) {
    return null;
    }
    string = string.replace(target, replacement);
    return string.getBytes(charset);
    }
    }
  3. 自定义Serializer:MsgpackRedisSerializer

    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
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    /**
    * @name: MsgpackRedisSerializer
    * @desc: Msgpack 序列化
    **/
    public class MsgpackRedisSerializer<T> implements RedisSerializer<Object> {

    static final byte[] EMPTY_ARRAY = new byte[0];
    private final ObjectMapper mapper;


    public MsgpackRedisSerializer() {
    this.mapper = new ObjectMapper(new MessagePackFactory());
    this.mapper.registerModule((new SimpleModule()).addSerializer(new NullValueSerializer(null)));
    this.mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL.NON_FINAL, JsonTypeInfo.As.PROPERTY.PROPERTY);
    }

    @Override
    public byte[] serialize(@Nullable Object source) throws SerializationException {
    if (source == null) {
    return EMPTY_ARRAY;
    } else {
    try {
    return this.mapper.writeValueAsBytes(source);
    } catch (JsonProcessingException var3) {
    throw new SerializationException("Could not write JSON: " + var3.getMessage(), var3);
    }
    }
    }

    @Override
    public Object deserialize(@Nullable byte[] source) throws SerializationException {
    return this.deserialize(source, Object.class);
    }

    @Nullable
    public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {
    Assert.notNull(type, "Deserialization type must not be null! Pleaes provide Object.class to make use of Jackson2 default typing.");
    if (source == null || source.length == 0) {
    return null;
    } else {
    try {
    return this.mapper.readValue(source, type);
    } catch (Exception var4) {
    throw new SerializationException("Could not read JSON: " + var4.getMessage(), var4);
    }
    }
    }

    private class NullValueSerializer extends StdSerializer<NullValue> {
    private static final long serialVersionUID = 2199052150128658111L;
    private final String classIdentifier;

    NullValueSerializer(@Nullable String classIdentifier) {
    super(NullValue.class);
    this.classIdentifier = StringUtils.hasText(classIdentifier) ? classIdentifier : "@class";
    }

    @Override
    public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
    jgen.writeStartObject();
    jgen.writeStringField(this.classIdentifier, NullValue.class.getName());
    jgen.writeEndObject();
    }
    }
    }
  4. 自定义Serializer:KryoRedisSerializer

    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
    52
    53
    54
    55
    56
    57
    58
    59
    /**
    * @name: KryoRedisSerializer
    * @desc: Kryo序列化和反序列化
    **/
    public class KryoRedisSerializer<T> implements RedisSerializer<T> {
    Logger logger = LoggerFactory.getLogger(KryoRedisSerializer.class);

    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    private static final ThreadLocal<Kryo> kryos = ThreadLocal.withInitial(Kryo::new);

    private Class<T> clazz;

    public KryoRedisSerializer(Class<T> clazz) {
    super();
    this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
    if (t == null) {
    return EMPTY_BYTE_ARRAY;
    }

    Kryo kryo = kryos.get();
    kryo.setReferences(false);
    kryo.register(clazz);

    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Output output = new Output(baos)) {
    kryo.writeClassAndObject(output, t);
    output.flush();
    return baos.toByteArray();
    } catch (Exception e) {
    logger.error(e.getMessage(), e);
    }

    return EMPTY_BYTE_ARRAY;
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
    if (bytes == null || bytes.length <= 0) {
    return null;
    }

    Kryo kryo = kryos.get();
    kryo.setReferences(false);
    kryo.register(clazz);

    try (Input input = new Input(bytes)) {
    return (T) kryo.readClassAndObject(input);
    } catch (Exception e) {
    logger.error(e.getMessage(), e);
    }

    return null;
    }
    }

序列化配置

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
52
53
54
55
56
57
58
59
@Configuration
@EnableRedisRepositories//Spring Boot 默认已开启对Redis Repository的支持,此注解可省略
public class RedisConfig {

/**
* SpringBoot已自动注册了这两个Bean
*/
/*@Bean
public RedisConnectionFactory redisConnectionFactory(){
return new JedisConnectionFactory();
}

@Bean
public RedisTemplate<Object, Object> redisTemplate(){
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
return template;
}*/

/**
* redis默认使用jdk的二进制数据来序列化
* 以下自定义使用jackson来序列化
*
* @param redisConnectionFactory
* @return
* @throws UnknownHostException
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);

/* 序列化10000个对象数据,在Redis 所占用空间
* 根据最终测试, String 和 FastJson 占用较少
* */
// Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);//2.5M,若开启类型检测是2.96M
// StringRedisSerializer serializer = new StringRedisSerializer();//2.33M
// FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);//2.35M
// KryoRedisSerializer serializer = new KryoRedisSerializer(Object.class);//2.35M
MsgpackRedisSerializer serializer = new MsgpackRedisSerializer();//2.96M

// ObjectMapper om = new ObjectMapper();
// om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// serializer.setObjectMapper(om);

template.setKeySerializer(new StringRedisSerializer()); //1
template.setValueSerializer(serializer); //2

template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);

//开启事务支持
template.setEnableTransactionSupport(true);

template.afterPropertiesSet();
return template;
}
}

创建 Redis Repository

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;

@Repository
public class ActorRedisDao{

@Autowired
private RedisTemplate<Object,Object> redisTemplate;

@Resource(name = "redisTemplate")
ValueOperations<Object, Object> objValueOperations;

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Resource(name = "stringRedisTemplate")
ValueOperations<String, String> stringValueOperations;

public void stringRedisTemplateDmoe(String key, String value){
stringValueOperations.set(key, value);
}

public void save(String key, Object obj){
objValueOperations.set(key, obj);
}

public String getStr(String key){
return stringValueOperations.get(key);
}

public Object getActor(String key){
return objValueOperations.get(key);
}
}

使用 Redis Repository

在业务层注入 RedisDao 就可使用

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
import com.springboot.cache.dao.ActorRedisDao;
import com.springboot.cache.entity.Actor;
import com.springboot.cache.repository.ActorRepository;
import com.springboot.cache.service.ActorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ActorServiceImpl implements ActorService {

@Autowired
private ActorRepository actorRepository;

@Autowired
private ActorRedisDao actorRedisDao;

/**
* 先查Redis缓存,没有从库里查再写入缓存,手动写入Redis库
* @param actorId
* @return
*/
public Actor queryById(Long actorId) {
Actor actor = (Actor) actorRedisDao.getActor(String.valueOf(actorId));
// return actor != null ? actor : actorRepository.findById(actorId).get();
if(actor == null){
actor = actorRepository.findById(actorId).get();
actorRedisDao.save(String.valueOf(actor.getActorId()), actor);
}
return actor;
}

/**
* 使用@Cache注解
* 先查缓存,没有再查库,再写入缓存
* 缓存注解使用:http://112.74.59.39/2018/06/01/spring-cache-annotation/
* @param actorId
* @return
*/
@Override
@Cacheable(key = "#actorId",value = "actor")
public Actor queryByActorId(Long actorId) {
Actor actor = actorRepository.findById(actorId).get();;
return actor;
}
}

注意:使用 redis 接口方法操作数据, 自定义的序列化方式可以起效;使用注解方式将数据写入缓存,自定义序列化的数据格式不会起效,写入到缓存的数据是二进制格式。
文中原码 -> Github

Spring Boot 2系列(十二):Spring Data Redis 集成详解与使用

http://blog.gxitsky.com/2018/06/05/SpringBoot-12-redis/

作者

光星

发布于

2018-06-05

更新于

2022-06-17

许可协议

评论