天天看点

SpringBoot——RedisTemplate浅析(一)

RedisTemplate 理解分析

前言

最近在使用springboot整合redis的时候遇到了一些问题,问题大致可以分为

  1. redisTemplate如何去得到连接然后操作redis,每次调用是否使用了不同的连接
  2. redisTemplate支持开启事务,但是开启了事务之后它如何保证都是使用一个连接,因为我们在redisTemplate中的操作是每次都使用

    redisTemplate.opsForXxx.xxx()

由于是第一次使用,对里面的实现比较有兴趣所以就翻了翻源码。而且下面就先来看rediTemplate中的操作:

redisTemplate.opsForXxx.xxx()

  • 例如通过redisTemplate对redis中的hash进行put操作的过程中我们会调用redisTemplate.opsForHash.put()方法,这个opsForHash()方法实际上为我们返回了一个DefaultHashOperations对象(工厂方法),然后通过这个Operations对象再去进行各种redis操作。

Operations对象

  • 工厂方法会默认为我们生成一个对应redis数据结构的例如DefaultHashOperations的一个实例,DefaultHashOperations类继承自AbstractOperations,这个抽象类中有一个excute(),他会在具体的Operations对象中被调用来执行各种不同的操作。例如DefaultHashOperations中的put方法:
    @Override
    public void put(K key, HK hashKey, HV value) {
    
        byte[] rawKey = rawKey(key);
        byte[] rawHashKey = rawHashKey(hashKey);
        byte[] rawHashValue = rawHashValue(value);
    
        execute(connection -> {
            connection.hSet(rawKey, rawHashKey, rawHashValue);
            return null;
        }, true);
    }
               
    这里调用的是Operations中的excute()方法。方法的参数是一个RedisCallback对象还有一个布尔值,但是这里使用了一个lambda表达式,而且返回值是null,有点看不懂所以选择debug一下看看这个参数运行后的值是什么。
    @Nullable
    	<T> T execute(RedisCallback<T> callback, boolean exposeConnection) {
    		return template.execute(callback, exposeConnection);
    	}
               
    发现运行的时候这个callback其实就是[email protected]对象,excute将lambda表达式整个作为参数当作一个RedisCallback对象传进execute中。在lambda表达式中就书写了我们对应需要的redis操作,这里就是hSet,后面拿到连接connection之后会由connection对象再去调用redisHashCommond的hSet方法。而这个lambda表达式在execute方法中形参名称也被改成action。下面具体看一下RedisTemplate中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");

    RedisConnectionFactory factory = getRequiredConnectionFactory();
    RedisConnection conn = null;
    try {
		// 判断是否处于事务状态,这个boolean值可以在配置RedisTemplate时设置开启事务
        if (enableTransactionSupport) {
            // 这个方法会将连接绑定到当前这个事务中,后面再分析
            conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
        } else {
            conn = RedisConnectionUtils.getConnection(factory);
        }

        //这边又是一些判断,没有仔细去看,推测就是判断这个事务的连接是否正确有效之类的
        boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);

        RedisConnection connToUse = preProcessConnection(conn, existingConnection);

        //判断是否是管道操作,这个变量再connection中可以拿到
        boolean pipelineStatus = connToUse.isPipelined();
        if (pipeline && !pipelineStatus) {
            connToUse.openPipeline();
        }
		
        // 这里判断是否是暴露连接,如果不是就创建代理什么的,这个布尔值默认是穿了true,也没有再仔细去看,等有机会再看看
        RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
        
        // 这里就是调用到lambda表达式了,在这一步会通过connection去操作redis然后返回结果
        T result = action.doInRedis(connToExpose);

        // close pipeline
        if (pipeline && !pipelineStatus) {
            connToUse.closePipeline();
        }

        // TODO: any other connection processing?
        return postProcessResult(result, connToUse, existingConnection);
    } finally {
        // 释放连接 releaseConnection 后面会再看这个操作
        RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
    }
}
           
  • 这段代码中重点就是这一句

    T result = action.doInRedis(connToExpose);

    这里对传进来的lambda表达式做了调用。(lambda函数不会再传进去的时候马上做调用)。而lambda是非常灵活的,对应任何操作都可以在Operations里面编写不同的lambda表达式,然后都来调用相同的接口,实现不同的操作。这里可以来复习(预习)一下函数式接口。

函数式接口(参考 java 核心技术 卷I)

  • 对于只有一个抽象方法的接口,需要使用这种接口的对象时,就可以提供一个lambda表达式。这种皆空称为函数式接口。为了展示如何转化为函数式接口,下面考虑Array.sort 方法。它的第二个参数需要一个comparator 实例, Comparator就是只有一个抽象方法的函数式接口,所以可以提供一个lambda表达式
    Arrays.sort(words,
               (first,second)->first.length()-second.length());
               
    在底层,Arrays.sort方法会接收实现了Compatator的某个对象。在这个对象上调用compare方法会执行这个lambda表达式的体。
  • 看到这里大概解答了自己的疑惑,对应RedisTemplate中就成了:redisTemplate.execute()方法会接收一个实现了RedisCallback的某个对象。在这个对象上调用doInRedis方法会执行lambda表达式的体。
    public interface RedisCallback<T> {
    	/**
    	 * Gets called by {@link RedisTemplate} with an active Redis connection. Does not need to care about activating or
    	 * closing the connection or handling exceptions.
    	 *
    	 * @param connection active Redis connection
    	 * @return a result object or {@code null} if none
    	 * @throws DataAccessException
    	 */
    	@Nullable
    	T doInRedis(RedisConnection connection) throws DataAccessException;
    }
               
  • 所以,

    T result = action.doInRedis(connToExpose);

    就会得到表达式运行之后的值,我们调用的式set方法,它就返回一个null给了result。

redisTemplate事务

  • 最后来看下redisTemplate是怎么支持事务的,要支持事务,在我们进入multi()到exec()提交事务之间,肯定要使用一个连接操作。在execute()中刚才看过这段代码块
    // 判断是否处于事务状态,这个boolean值可以在配置RedisTemplate时设置开启事务
    if (enableTransactionSupport) {
        conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
    } else {
        conn = RedisConnectionUtils.getConnection(factory);
    }
               

    显然,bindConnection()就是他做的一个绑定操作了。再看下它内部是怎么实现的。

    RedisConnectionUtils.bindConnection()会调用一个方法是doGetConnection(), 部分代码如下:

    public static RedisConnection bindConnection(RedisConnectionFactory factory, boolean enableTranactionSupport) {
    		return doGetConnection(factory, true, true, enableTranactionSupport);
    	}
    
    public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind, boolean enableTransactionSupport) {
    
    		Assert.notNull(factory, "No RedisConnectionFactory specified");
    
    		RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
    
    		if (connHolder != null) {
    			if (enableTransactionSupport) {
    				potentiallyRegisterTransactionSynchronisation(connHolder, factory);
    			}
    			return connHolder.getConnection();
    		}
               
    可以看到这里就是使用了

    TransactionSynchronizationManager.getResource(factory)

    ,这个TransactionSynchronizationManager是spring中对事务的支持。再点进去,会看到调用了这个方法:
    @Nullable
    private static Object doGetResource(Object actualKey) {
        Map<Object, Object> map = (Map)resources.get();
        if (map == null) {
            return null;
        } else {
            Object value = map.get(actualKey);
            if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {
                map.remove(actualKey);
                if (map.isEmpty()) {
                    resources.remove();
                }
                value = null;
            }
            return value;
        }
    }
               
    这里由一个map,这个map肯定就是能将我们的事务跟这个连接绑定映射,再点进去看其实就是用了ThreadLocal将连接绑定到线程中:
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
               

后言

在使用过程中也查阅了一下博客,看到有个博主中说当redisTemplate开启了事务支持后,会导致使用这个redisTemplate中调用非事务操作无法解绑和关闭连接。看了下源码好像确实是如此,而查阅的博客中也建议将事务跟非事务的redisTemplate分开。而且在exec()之后也还是不会解绑连接,由于我是开启了连接池的,所以只要在事务操作后是用

RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());

接触绑定就好。

刚才说的博客:https://blog.csdn.net/qq_34021712/article/details/79606551

看这部分源码的时候也是感觉很复杂,这里面它每个方法的调用栈都比较深,debug也不太能整,而且一开始对lambda并不了解,一开始也看不懂。后续有时间可以再了解下函数编程以及redis的设计与实现。

如果有大佬发现错误,希望可以帮忙指出~

继续阅读