spring-data-redis -- 一次执行链路的分析

前言

最近在项目中,使用到了 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 中的配置和配置类一一对应)

image

可以看到 在配置类中,主要分这几个部分

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

image

采取 stringRedisTemplate 的结果 30 bytes

image

并且我们可以看到,基于 默认序列化的处理之后,可读性也较差。我们在选取 RedisTemplate

之前,需要去指定序列化方式,并且也可以实现自己的序列化方式,如 protobuf 等

目前实序列化如下

image

如果项目中都是string 类型的存储结构,则直接选取 stringRedisTemplate 就可以。

spring-data-redis 的 封装实现

我们所有的入口,都是基于 redisTemplate 来进行交互操作的,我们看下 redisTemplate 的实现 放一张类图

image

可以看到,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 接口,来看下他的实现以及 做了哪些事情

image

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);
    }

可以很明显的看到,做的操作有这样几步

  1. key ,value 转换为 字节数组
  2. 通过 匿名内部类的ValueDeserializingRedisCallback 的实现 来进行具体的调用,doInRedis
  3. 通过 connection 来执行的redis 原生命令

RedisConnection 接口的类图

image

可以看到,RedisConnection 集成 RedisCommands 集成 一系列的 数据结构相关的 commands 接口

不同客户端的操作执行,都会按照数据结构的纬度,去实现对这一系列命令 commands 接口,实现对redis 的操作

具体可以看下面这张类图

image

这样的话,进过层层的抽象继承,最终实现了一个入口,多个不同客户端的实现逻辑

看下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);
        }
    }

最后附一张调用链路分析图

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容