前言
最近在项目中,使用到了 spring-data-redis ,基于他的一些实现原理与细节,做一次学习
spring-redis 基于配置项的自动装载
redis 基于 bean 初始化的装载
spring-data-redis 的基本用法,事务的事项方式,等
难免会有疏漏,不对之处,欢迎指出,一起探讨
简介
Spring Data Redis, part of the larger Spring Data family, provides easy configuration and access to Redis from Spring applications. It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns
--- 官网介绍
可以看出,提供统一的底层抽象封装,简化操作逻辑,提供高低版本之间存储库之间的交互方式
spring-data-redis 的初始化
spring-data-redis 的初始化
先看下配置文件
只展示部分,就不一一列举了
spring:
redis:
host: 127.0.0.1
password: xxxxx
jedis:
pool:
max-wait:
....
看下关于redis 配置类内都有哪些元素 (yml 中的配置和配置类一一对应)
可以看到 在配置类中,主要分这几个部分
1. 基本链接配置 database,url,host,passwrod 等
2. 客户端配置 jedis,Lettuce
3. 链接池配置 Pool
4. 集群配置 (本文暂不做过多,后期加上)
这一部分数据在spirng 初始化的时候,会自动进行bean的装载与注入 RedisAutoConfiguration,并根据当前配置的redis 客户端,进行客户端的统一封装。并会初始化2个bean RedisTemplate 和 StringRedisTemplate
后续我们在使用的时候,只需要通过注入的方式,来进行调用
大致流程如下
[图片上传失败...(image-d19ed0-1584797731464)]
具体内容,可查看 RedisAutoConfiguration 类
spring-data-redis序列化
在看了RedisAutoConfiguration
类之后,我们会发现初始化了2个bean RedisTemplate
StringRedisTemplate
为什么会初始化 2个bean ?这两者之间,有什么区别呢?
看代码可以知道,做了如下操作
public class StringRedisTemplate extends RedisTemplate<String, String> {
/**
* Constructs a new <code>StringRedisTemplate</code> instance. {@link #setConnectionFactory(RedisConnectionFactory)}
* and {@link #afterPropertiesSet()} still need to be called.
*/
public StringRedisTemplate() {
setKeySerializer(RedisSerializer.string());
setValueSerializer(RedisSerializer.string());
setHashKeySerializer(RedisSerializer.string());
setHashValueSerializer(RedisSerializer.string());
}
.... 其余内容省略
}
==========================================================
RedisTemplate 内的 初始化操作
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
}
其实可以看到,这两之间的唯一区别,是在于 序列化的设置,StringRedisTemplate 采取的是 string 序列化 Charset UTF_8 = Charset.forName("UTF-8");
而 RedisTemplate 采取的是 默认的 jdk 序列化 默认的jdk 序列化。我们可以看下对比
循环1000 次 ,写入一个list
for (long i = 0; i < 1000; i++) {
GoodsCache goodsCache = GoodsCache.builder().goodsId(i)
.storeId(1001L)
.build();
stringRedisTemplate.opsForList().leftPush("goods:jdk", JSON.toJSONString(goodsCache));
}
采取 redisTemplate 的结果 37
bytes
采取 stringRedisTemplate 的结果 30
bytes
并且我们可以看到,基于 默认序列化的处理之后,可读性也较差。我们在选取 RedisTemplate
之前,需要去指定序列化方式,并且也可以实现自己的序列化方式,如 protobuf 等
目前实序列化如下
如果项目中都是string 类型的存储结构,则直接选取 stringRedisTemplate 就可以。
spring-data-redis 的 封装实现
我们所有的入口,都是基于 redisTemplate 来进行交互操作的,我们看下 redisTemplate 的实现 放一张类图
可以看到,RedisTemplate 主要是有3部分
- 继承自RedisAccessor 实现了 InitializingBean 来去做一些 bean的初始化时候操作,如 序列化方式的设置等
- 实现了BeanClassLoaderAware 接口 来去设置 类加载器 setBeanClassLoader
- 实现了 RedisOperations 接口,该接口定义了一些列的redis 交互操作,所有的redis 交互,都由该接口的实现来完成
RedisOperations
在该接口中,主要包含以下
execute 系列操作 ,pipline,批量,事务等
公有redis 抽象命令,如 deletd,keys 等
opsForXXX 基于 redis 数据结构的封装 一些数据结构独特的命令用法,需要 先 转换为该对象之后,才可以进行执行
eg: (java doc) Returns the operations performed on list values.
抽其中的 ListOperations 接口,来看下他的实现以及 做了哪些事情
AbstractOperations
在 AbstractOperations 抽象类中,主要用来做一些序列化,和反序列化 以及 一个内部抽象类 ValueDeserializingRedisCallback
abstract class ValueDeserializingRedisCallback implements RedisCallback<V> {
private Object key;
public ValueDeserializingRedisCallback(Object key) {
this.key = key;
}
public final V doInRedis(RedisConnection connection) {
byte[] result = inRedis(rawKey(key), connection);
return deserializeValue(result);
}
@Nullable
protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection);
}
DefaultListOperations ListOperations 的实现类中,主要就是进行一系列的原生命令的封装和调用,具体看个栗子
@Override
public Long leftPush(K key, V value) {
byte[] rawKey = rawKey(key);
byte[] rawValue = rawValue(value);
return execute(connection -> connection.lPush(rawKey, rawValue), true);
}
*/
@Override
public V rightPop(K key) {
return execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.rPop(rawKey);
}
}, true);
}
可以很明显的看到,做的操作有这样几步
- key ,value 转换为 字节数组
- 通过 匿名内部类的ValueDeserializingRedisCallback 的实现 来进行具体的调用,doInRedis
- 通过 connection 来执行的redis 原生命令
RedisConnection 接口的类图
可以看到,RedisConnection 集成 RedisCommands 集成 一系列的 数据结构相关的 commands 接口
不同客户端的操作执行,都会按照数据结构的纬度,去实现对这一系列命令 commands 接口,实现对redis 的操作
具体可以看下面这张类图
这样的话,进过层层的抽象继承,最终实现了一个入口,多个不同客户端的实现逻辑
看下Jedis 对 rpush 的实现逻辑
@Override
public Long rPush(byte[] key, byte[]... values) {
Assert.notNull(key, "Key must not be null!");
try {
if (isPipelined()) {
pipeline(connection.newJedisResult(connection.getRequiredPipeline().rpush(key, values)));
return null;
}
if (isQueueing()) {
transaction(connection.newJedisResult(connection.getRequiredTransaction().rpush(key, values)));
return null;
}
return connection.getJedis().rpush(key, values);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
在Jedis 以及 Lettuce 客户端的封装实现中,都提供了两种模式的操作 **pipline & transaction **
支持管道操作 和 事物处理
经过上边的一路跟踪,我们知道了,相关的底层操作,是如何进行封装和处理,那么在来看下,关于 前文提到的
最终的execute 是如何进行实现的
最终的execute 方法的实现,是在 RedisTemplate#execute
方法中
@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
Assert.notNull(action, "Callback object must not be null");
// 获取当前 的工厂实例,jedis or Lettuce等
RedisConnectionFactory factory = getRequiredConnectionFactory();
RedisConnection conn = null;
try {
// 进行是否开启事务的传播
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
// 事务相关处理
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
// 获取链接
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
// pipeline 相关处理
boolean pipelineStatus = connToUse.isPipelined();
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
// 具体的进行redis 操作执行,交由 最底层的 客户端封装层去执行
T result = action.doInRedis(connToExpose);
// close pipeline
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
// 释放当前的链接资源
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
最后附一张调用链路分析图