• spring在加Transactional的方法中使用redis取值为空的问题


    https://blog.csdn.net/qq_34021712/article/details/79606551

    http://www.kaysonlv.com/%E8%AF%B4%E8%AF%B4redis-data-redis%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%BD%BF%E7%94%A8.html

    https://my.oschina.net/u/1168037/blog/967685

    事务的过程分为三个步骤:

    一、切换事务模式

    二、事务入队列

    三、执行事务。

    redis客户端执行multi后返回ok,表明redis进入事务状态。进入事务状态以后redis并不会立即执行命令,会将redis客户端发送的命令存入队列,暂不执行,此时返回queued。最后调用exec,将命令从队列中取出来,然后一次性执行,这些,命令同时成功同时失败,最后将命令执行结果一次性返回,并且将事务状态标志复位。在执行这些命令的过程中,使用同一客户端,并且不会被其它客户端中断。

     在Spring Data Redis提供了RedisTemplate对redis进行读写操作并且支持事务。先配置开启事务

    redisTemplate.setEnableTransactionSupport(true);

    其它相关配置,参考文档:https://docs.spring.io/spring-data/redis/docs/2.1.8.RELEASE/reference/html/#tx.spring

    然后在要使用事务的方法上面添加@Transactional,这样就使用了redis的事务了

    如果在同一线程(比如Web环境的一次请求中)中存在下面操作将会造成读操作无法直接读取出数据

    1. 先在非事务环境下执行reids操作(调用没有加@Transactional注解)
    2. 然后在事务环境下执行redis操作(调用添加了@Transactional注解的方法)

    可以从RedisTemplate源码中找到原因。

            RedisTemplate中对Redis的各种数据类型的操作都抽象出了相对于的操作类 如 ValueOperations,ListOperations,SetOperations等,而这些类在执行操作时最终还是会调用RedisTemplate#execute

        public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline)

    这个方法是RedisTemplate的操作Reids的核心方法

        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 {
                //这里判断redis的EnableTransactionSupport是否为true,如果为true将连接绑定到当前线程    
                if (enableTransactionSupport) {
                    //如果设置了启用事务,则调用bindConnection
                    // only bind resources in case of potential transaction synchronization
                    conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
                } else {
                    //如果没有开启事务,直接获取一个连接
                    conn = RedisConnectionUtils.getConnection(factory);
                }
                //获取当前线程绑定的连接,如果开启事务,也就是上面的bindConnection(factory, enableTransactionSupport)代码执行时那个连接
                boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
                //预留钩子函数可在执行具体操作前对connection做一些处理
                RedisConnection connToUse = preProcessConnection(conn, existingConnection);
    
                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();
                }
    
                //预留钩子函数可在执行具体操作后对connection做一些处理
                // TODO: any other connection processing?
                return postProcessResult(result, connToUse, existingConnection);
            } finally {
                RedisConnectionUtils.releaseConnection(conn, factory);
            }
        }

    RedisConnectionUtils是获取连接的工具类,在配置RedisTemplate是如果设置了enableTransactionSupport=true时,则会通过bindConnection方法获取连接

        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) {
                    //开启reids事务
                    potentiallyRegisterTransactionSynchronisation(connHolder, factory);
                }
                return connHolder.getConnection();
            }
    
            if (!allowCreate) {
                throw new IllegalArgumentException("No connection found and allowCreate = false");
            }
    
            if (log.isDebugEnabled()) {
                log.debug("Opening RedisConnection");
            }
            //如果当前线程中不存在连接则创建连接
            RedisConnection conn = factory.getConnection();
    
            if (bind) {
    
                RedisConnection connectionToBind = conn;
                //如果开启的事务且调用添加了@Transactional的方法,这里会创建一个连接的代理对象
                if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
                    connectionToBind = createConnectionProxy(conn, factory);
                }
    
                connHolder = new RedisConnectionHolder(connectionToBind);
    
                //绑定连接到当前线程中
                TransactionSynchronizationManager.bindResource(factory, connHolder);
                if (enableTransactionSupport) {
                    //开启reids事务
                    potentiallyRegisterTransactionSynchronisation(connHolder, factory);
                }
    
                return connHolder.getConnection();
            }
    
            return conn;
        }

    将代码定位到TransactionSynchronizationManager.bindResource(factory, connHolder);这一行,内部使用ThreadLocal实现,

    查看potentiallyRegisterTransactionSynchronisation函数,如果加了@Transactional就会开启事务

        private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionHolder connHolder,
                final RedisConnectionFactory factory) {
    
            if (isActualNonReadonlyTransactionActive()) {
    
                if (!connHolder.isTransactionSyncronisationActive()) {
                    connHolder.setTransactionSyncronisationActive(true);
    
                    RedisConnection conn = connHolder.getConnection();
                    conn.multi();
                    //注册一个事务完成时的回调,用于提交或回滚redis事务
                    TransactionSynchronizationManager
                            .registerSynchronization(new RedisTransactionSynchronizer(connHolder, conn, factory));
                }
            }
        }

    上面代码可以看出获取连接的整个流程

    1. TransactionSynchronizationManager.getResource(factory)(从当前线程中获取连接,TransactionSynchronizationManager使用ThreadLocal把连接绑定到当前线程上。
    2. 如果获取到连接则开启事务,返回连接,如果没有获取到则创建连接
    3. 创建完连接后会判断当前操作是否在事务中isActualNonReadonlyTransactionActive (是否添加了@Transactional注解,并且事务不是ReadOnly的)
    4. 如果操作实在事务中,则会创建一个连接的代理对象
    5. TransactionSynchronizationManager.bindResource(factory, connHolder); 绑定事务到当前线程中
    6. potentiallyRegisterTransactionSynchronisation(connHolder, factory); 开启redis事务
    7. 返回连接

    从上面流程可以看出在事务中执行和不在事务中执行的关键区别在于,是否创建了一个连接的代理对象,下面看一下createConnectionProxy的代码

        private static RedisConnection createConnectionProxy(RedisConnection connection, RedisConnectionFactory factory) {
    
            ProxyFactory proxyFactory = new ProxyFactory(connection);
            //创建了一个ConnectionSplittingInterceptor类用于拦截RedisConnection所有方法
            proxyFactory.addAdvice(new ConnectionSplittingInterceptor(factory));
    
            return RedisConnection.class.cast(proxyFactory.getProxy());
        }

    上面代码中创建了一个ConnectionSplittingInterceptor类用于拦截RedisConnection中的所有方法,ConnectionSplittingInterceptor中的核心代码是intecepter方法

            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    
                RedisCommand commandToExecute = RedisCommand.failsafeCommandLookup(method.getName());
                //判断命令是否为只读命令,如果是则新开一个连接执行度操作,如果是写命令则放在事务中执行
                if (isPotentiallyThreadBoundCommand(commandToExecute)) {
    
                    if (log.isDebugEnabled()) {
                        log.debug(String.format("Invoke '%s' on bound conneciton", method.getName()));
                    }
    
                    return invoke(method, obj, args);
                }
    
                if (log.isDebugEnabled()) {
                    log.debug(String.format("Invoke '%s' on unbound conneciton", method.getName()));
                }
    
                RedisConnection connection = factory.getConnection();
    
                try {
                    return invoke(method, connection, args);
                } finally {
                    // properly close the unbound connection after executing command
                    if (!connection.isClosed()) {
                        connection.close();
                    }
                }
            }

    intecepter方法中会判断这次执行的命令是否是读命令。如果不是,会用当前线程中的连接执行也就是放在事务中执行,如果是读操作,会创建一个新的连接执行,这样就能立即获得读取的数据。

    通过代码可以看出出错的大致流程:

    1. 调用没有使用事务的reids操作
    2. 创建一个连接并绑定到当前线程中(由于没有使用事务,不会创建连接的代理对象)
    3. 执行reids操作 (操作完成后并没有把当前线程中的连接清除)
    4. 调用使用事务的redis操作(方法上添加了@Transactional注解)
    5. 获取连接方向当前线程中已经存在了连接不再重新创建(获取到的是没有使用事务时创建的连接,此连接对象不是代理对象)
    6. 开启事务
    7. 执行操作(如果执行的是读操作,由于连接对象不是代理对象,读操作并不会重新创建一个连接,而是使用当前连接,并且放在事务中运行,因此读操作并不会立即执行而是等到事务提交时才能执行,导致读操作读取的结果为null)

    解决方案:

         此问题关键在于如果执行了为使用事务的reids操作,在操作完成后要将当前线程中绑定的连接对象给清除掉,或者在使用的事务的reids操作之前,判断获取到的连接是否是代理对象,如果不是则清除掉,重新获取连接。

    方法一:

        既然它没有执行释放的动作,那我们帮他执行就好了。继续阅读TransactionSynchronizationManager的源码,发现有TransactionSynchronizationManager.unbindResource(factory);这个方法,这个方法的内部就是将资源释放,如果你的redisTemplate开启了事务,在未标明@Transactional的方法内使用时,可以在redisTemplate操作redis之后立马调用该方法,具体代码如下:

    public void getRedis() {
            Object testtredis = redisTemplate.opsForValue().get("testtredis");
            TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
            System.out.println(testtredis);
        }

    方法二:

    在RedisTemplate的execute方法中我们看到了 reids为我们预留了两个钩子函数,

    preProcessConnection(conn, existingConnection) 

    postProcessResult(result, connToUse, existingConnection)

    因此我们可以继承RedisTemplate来对连接进行处理

    public class CustomRedisTemplate<K, V> extends RedisTemplate<K, V> {
        private boolean enableTransactionSupport = false;
    
        private static boolean isActualNonReadonlyTransactionActive() {
            return TransactionSynchronizationManager.isActualTransactionActive()
                    && !TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        }
    
        /**
         * 解决 redis先非事务中运行,然后又在事务中运行,出现取到的连接还是非事务连接的问题
         * 在事务环境中用非事务连接,读取操作无法马上读出数据
         *
         * @param connection
         * @param existingConnection
         * @return
         */
        @Override
        protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
            if (existingConnection && !Proxy.isProxyClass(connection.getClass()) && isActualNonReadonlyTransactionActive()) {
                RedisConnectionUtils.unbindConnection(getConnectionFactory());
                List<TransactionSynchronization> list = new ArrayList<>(TransactionSynchronizationManager.getSynchronizations());
                TransactionSynchronizationManager.clearSynchronization();
                TransactionSynchronizationManager.initSynchronization();
                //移除最后一个回调(由于之前回去连接是会注册一个事务回调,下面如果再获取连接会导致注册两个事务回调。事务完成后会执行两次回调,
                //回调中会清除资源,第一次已经清除,第二次再清的时候回抛出异常)
                list.remove(list.size() - 1);
                list.forEach(TransactionSynchronizationManager::registerSynchronization);
                connection = RedisConnectionUtils.bindConnection(getConnectionFactory(), enableTransactionSupport);
            }
            return connection;
        }
    
        @Override
        public void setEnableTransactionSupport(boolean enableTransactionSupport) {
            super.setEnableTransactionSupport(enableTransactionSupport);
            this.enableTransactionSupport = enableTransactionSupport;
        }
    }
  • 相关阅读:
    WinForm DevExpress使用之ChartControl控件绘制图表二——进阶
    正则表达式(JavaScript)
    WinForm DevExpress使用之ChartControl控件绘制图表一——基础
    Git常用命令总结
    CSS样式设置小技巧
    java Socket编程
    java synchronized详解
    SpringBoot应用整合ELK实现日志收集
    mongoDB中的的常用语法
    使用Nginx做图片服务器时候,配置之后图片访问一直是 404问题解决
  • 原文地址:https://www.cnblogs.com/grasp/p/12510225.html
Copyright © 2020-2023  润新知