Spring Boot 2系列(十二):Spring Data Redis 集成详解与使用
Redis 是基于 key-value 键 / 值对的开源内存数据存储系统,现在非常流行用作缓存存储。
Spring Boot 集成 Redis 非常简单,也容易使用。Spring Boot 自动注册了 RedisConnectionFactory ,并提供了RedisTemplate 和 StringRedisTemplate 两个模板来操作数据。所以在 Spring Boot 环境,只需配置下 Redis 的连接参数就可以直接使用了。
Spring Boot 对 Redis 自动配置的支持依赖于 Sping Data Redis。Spring Data Redis 将数据操作抽象出了统一的方法便于使用。更多参考 官方 Spring Data Redis 项目。
Spring Data Redis
Spring Data Redis 提供了两种方式来连接 Redis,分别是 LettuceConnectionFactory 和 RedisConnectionFactory。Lettuce 是基于 Netty 的开源连接器,性能更高,在多线程并发情况下,连接是线程安全的; 而 Jedis 在多线程并发下不是线程安全的,就需要使用线程池来给每个线程创建物理连接。
在Spring Boot 项目里不需要人为配置这两个工厂 Bean,默认集成的是 Lettuce 依赖,使用的是**LettuceConnectionFactory **创建连接。
LettuceConnectionFactory
| 1 | 
 | 
RedisConnectionFactory
| 1 | 
 | 
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 | public class Example { | 
RedisTemplate 默认的的序列化方式是采用 JDK的二进制来序列化,键值用户不可读。
StringRedisTemplate
由于存储在 Redis 中的键和值通常是 String 类型,Redis模块为 RedisConnection 和 RedisTemplate 提供了两个扩展,分别为 StringRedisConnection(及其DefaultStringRedisConnection实现)和 StringRedisTemplate。为大量的字符串操作提供了便 捷的操作。 除了绑定到 String 键之外,模板和连接使用 StringRedisSerializer来序列化,这样存储的键和值是用户是可读的(前提是存在 Redis 和代码中都使用相同的编码)。
| 1 | public class Example { | 
CacheManager
若需要,也可自定义缓存管理器。
| 1 | 
 | 
KeyGenerator
默认使用的是 org.springframework.cache.interceptor.SimpleKeyGenerator 来自动生成 Key。也可自定义键生成策略。下面示例:类名 + 方法名 + 参数 来生成缓存的 Key,
| 1 | 
 | 
使用自定义键生成策略
| 1 | 
 | 
Redis Resporites
Spring-data-redis 提供了 Redis Repositories 来支持 Redis 缓存操作,可以通过继承 CrudRepository 或 JpaRepository 接口调用已提供的方法来操作传统数据库,再结合缓存注解来对数据进行缓存到 redis 的操作。
- 在应用启动类上或 java 配置类上添加注解 @EnableRedisRepositories 开启Redis Repositories 的支持
| 1 | 
 | 
- 定义实体类
@Id 类似于数据库中的主键,可自动生成;@RedisHash value 属性定义 Hash 名称,timeToLive 属性设置有过期时长。
| 1 | //@Entity(name = "actor") | 
- 定义 Repository 接口继承 CrudRepository 或 JpaRepository
| 1 | 
 | 
- 业务层注入实体类的 Repository,调用 Repository 的方法来对数据库进行操作,在方法上添加缓存注解来对数据进行缓存操作
| 1 | 
 | 
- 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 | <!-- redis --> | 
Jedis 替换 Lettuce
若需要使用 Jedis 替换 Lettuce,则使用下面依赖,但不建议替换, lettuce 是基于 netty 实现的客户端连接,性能更优且是线程安全的。
| 1 | <dependency> | 
若使用默认的 Lettuce ,配置连接池需要依赖 apache commons-pool2
| 1 | <dependency> | 
redis参数配置
| 1 | spring.redis.database=0 | 
自定义序列化
- 添加依赖 - 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>
- 自定义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;
 }
 
 public String deserialize(byte[] bytes) {
 return (bytes == null ? null : new String(bytes, charset));
 }
 
 public byte[] serialize(Object object) {
 String string = JSON.toJSONString(object);
 if (string == null) {
 return null;
 }
 string = string.replace(target, replacement);
 return string.getBytes(charset);
 }
 }
- 自定义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);
 }
 
 public byte[] serialize( 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);
 }
 }
 }
 
 public Object deserialize( byte[] source) throws SerializationException {
 return this.deserialize(source, Object.class);
 }
 
 public <T> T deserialize( 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( String classIdentifier) {
 super(NullValue.class);
 this.classIdentifier = StringUtils.hasText(classIdentifier) ? classIdentifier : "@class";
 }
 
 public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
 jgen.writeStartObject();
 jgen.writeStringField(this.classIdentifier, NullValue.class.getName());
 jgen.writeEndObject();
 }
 }
 }
- 自定义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;
 }
 
 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;
 }
 
 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 | 
 | 
创建 Redis Repository
| 1 | import org.springframework.beans.factory.annotation.Autowired; | 
使用 Redis Repository
在业务层注入 RedisDao 就可使用
| 1 | import com.springboot.cache.dao.ActorRedisDao; | 
**注意:**使用 redis 接口方法操作数据, 自定义的序列化方式可以起效;使用注解方式将数据写入缓存,自定义序列化的数据格式不会起效,写入到缓存的数据是二进制格式。
文中原码 -> Github
Spring Boot 2系列(十二):Spring Data Redis 集成详解与使用

