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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ApplicationConfig {
public RedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> template = new RedisTemplate<>();
return template;
}
}定义实体类
@Id 类似于数据库中的主键,可自动生成;**@RedisHash** value 属性定义 Hash 名称,timeToLive 属性设置有过期时长。
1 | //@Entity(name = "actor") |
- 定义 Repository 接口继承 CrudRepository 或 JpaRepository
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
public class UserServiceImpl implements UserService {
//注入实体类的Repository
private UserRepository userRepository;
/**
* 使用Cache注解
* 先查缓存,没有再查库,再写入缓存
* 可使用 @Cacheable(key = "#id", value="user")来自动生成键
*/
public User queryByUserId(Long userId) {
User user = userRepository.findById(userId).get();
return user;
}
/**
* 调用 CrudRepository 方法
*/
public User queryByUserId(Long userId) {
User user = userRepository.findById(userId).get();
userRepository.save(user);
return user;
}
}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 | 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 集成详解与使用