解决 No ongoing transaction. Did you forget to call multi?

背景

需要使用 setIfAbsent(Object key, Object value, long timeout, TimeUnit unit)方法,然后当前版本不支持,所以自己定义一个这样的方法

思路: 使用redis的事务操作,把保存key和设置过期时间放到一个事务执行

  • 切换事务模式
  • 事务队列
  • 执行事务

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

试一下
                redisTemplate.multi();
                redisTemplate.opsForValue().setIfAbsent(key,JSON.toJSONString(value));
                redisTemplate.expire(key,timeout, unit);
                redisTemplate.exec();

报错了

org.springframework.dao.InvalidDataAccessApiUsageException: No ongoing transaction. Did you forget to call multi?
    at org.springframework.data.redis.connection.jedis.JedisConnection.exec(JedisConnection.java:785) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.connection.DefaultStringRedisConnection.exec(DefaultStringRedisConnection.java:252) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_131]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_131]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_131]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_131]
    at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:57) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at com.sun.proxy.$Proxy197.exec(Unknown Source) ~[?:?]
    at org.springframework.data.redis.core.RedisTemplate$3.doInRedis(RedisTemplate.java:622) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.core.RedisTemplate$3.doInRedis(RedisTemplate.java:620) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:207) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:157) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.core.RedisTemplate.execRaw(RedisTemplate.java:620) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
    at org.springframework.data.redis.core.RedisTemplate.exec(RedisTemplate.java:607) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]

异常的意思很明显

没有开启的事务,你是不是忘了执行multi()?

不是明明开启了吗?

如果是jedis的话这样确实就可以了,但是springboot-data-redis对jedis封装后就不一样了,出现上述异常是因为在执行jedis在执行exec的时候transaction属性为空

@Override
    public List<Object> exec() {
        try {
            if (isPipelined()) {
                pipeline(newJedisResult(getRequiredPipeline().exec(),
                        new TransactionResultConverter<>(new LinkedList<>(txResults), JedisConverters.exceptionConverter())));
                return null;
            }
            if (transaction == null) {
                throw new InvalidDataAccessApiUsageException("No ongoing transaction. Did you forget to call multi?");
            }
            List<Object> results = transaction.exec();

            return !CollectionUtils.isEmpty(results)
                    ? new TransactionResultConverter<>(txResults, JedisConverters.exceptionConverter()).convert(results)
                    : results;
        } catch (Exception ex) {
            throw convertJedisAccessException(ex);
        } finally {
            txResults.clear();
            transaction = null;
        }
    }

既然这里的transactionnull,那么在那里赋值呢 接着看

private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionHolder connHolder,
            final RedisConnectionFactory factory) {

        if (isActualNonReadonlyTransactionActive()) {
            if (!connHolder.isTransactionSyncronisationActive()) {
                connHolder.setTransactionSyncronisationActive(true);
                RedisConnection conn = connHolder.getConnection();
                conn.multi();//jedis开启事务
                TransactionSynchronizationManager
                        .registerSynchronization(new RedisTransactionSynchronizer(connHolder, conn, factory));
            }
        }
    }

@Override
    public void multi() {
            //定义transaction
            this.transaction = jedis.multi();
        } catch (Exception ex) {
            throw convertJedisAccessException(ex);
        }
    }

可以看到

potentiallyRegisterTransactionSynchronisation()时会调用jedis的multi从而开启redis的事务,jedis的multi()里完成了transaction的定义

继续看

 @Nullable
 public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
     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);

         RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
         T result = action.doInRedis(connToExpose);

         // close pipeline
         // TODO: any other connection processing?
         return postProcessResult(result, connToUse, existingConnection);
     } finally {
         RedisConnectionUtils.releaseConnection(conn, factory);
     }
 }

在执行redis excute()时都会去判断enableTransactionSupport这个属性.决定开启一个链接还是获取一个链接(从池里)

但是两个方法最后都会追踪到这个方法

 public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
            boolean enableTransactionSupport) {
        RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
        if (connHolder != null) {
            if (enableTransactionSupport) {
                potentiallyRegisterTransactionSynchronisation(connHolder, factory);
            }
            return connHolder.getConnection();
        }

        RedisConnection conn = factory.getConnection();
        if (bind) {
            RedisConnection connectionToBind = conn;
            if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
                connectionToBind = createConnectionProxy(conn, factory);
            }
            connHolder = new RedisConnectionHolder(connectionToBind);
            TransactionSynchronizationManager.bindResource(factory, connHolder);
            if (enableTransactionSupport) {
                potentiallyRegisterTransactionSynchronisation(connHolder, factory);
            }
            return connHolder.getConnection();
        }
        return conn;
    }

会根据enableTransactionSupport这个属性来决定是否调用potentiallyRegisterTransactionSynchronisation()这个方法,而这个就是我们前面找到的定义transaction的地方

解决

那我们这样不就可以了

				redisTemplate.setEnableTransactionSupport(true);
                redisTemplate.multi();
                redisTemplate.opsForValue().setIfAbsent(key,JSON.toJSONString(value));
                redisTemplate.expire(key,timeout, unit);
                redisTemplate.exec();

但是这样写有个很严重的问题,redisTemplate.exec()这个方法并不会去关闭连接,不信你自己去看源码

public List<Object> exec() {

		//看源码的顺序1
        List<Object> results = this.execRaw();
        return this.getConnectionFactory().getConvertPipelineAndTxResults() ? this.deserializeMixedResults(results, this.valueSerializer, this.hashKeySerializer, this.hashValueSerializer) : results;
    }

protected List<Object> execRaw() {
        return (List)this.execute(new RedisCallback<List<Object>>() {
            public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
            
            	//看源码的顺序2
                return connection.exec();
            }
        });
    }


//看源码的顺序3,去掉不关心的,
@Nullable
    public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
        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);
            }
            T result = action.doInRedis(connToExpose);
            // TODO: any other connection processing?
            return postProcessResult(result, connToUse, existingConnection);
        } finally {
        //这里关闭
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
    }

虽然最终会调用releaseConnection();

public static void releaseConnection(@Nullable RedisConnection conn, RedisConnectionFactory factory) {
        if (conn == null) {
            return;
        }
        RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);

        if (connHolder != null && connHolder.isTransactionSyncronisationActive()) {
            if (log.isDebugEnabled()) {
                log.debug("Redis Connection will be closed when transaction finished.");
            }
            return;
        }

        // release transactional/read-only and non-transactional/non-bound connections.
        // transactional connections for read-only transactions get no synchronizer registered
        if (isConnectionTransactional(conn, factory) && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            unbindConnection(factory);
        } else if (!isConnectionTransactional(conn, factory)) {
            if (log.isDebugEnabled()) {
                log.debug("Closing Redis Connection");
            }
            conn.close();
        }
    }

但是你并没有设置isTransactionSyncronisationActivefalse,所以并不能关闭连接。这样的话将会导致连接池泄露。

那么正确的姿势是什么呢?

sessionCallback
    public Boolean setIfAbsent(String key, Object value, long timeout, TimeUnit unit) {
        // 使用sessionCallBack处理
        SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
            List<Object> exec = null;
            @Override
            @SuppressWarnings("unchecked")
            public Boolean execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                redisTemplate.opsForValue().setIfAbsent(key,JSON.toJSONString(value));
                redisTemplate.expire(key,timeout, unit);
                exec = operations.exec();
                if(exec.size() > 0) {
                    return (Boolean) exec.get(0);
                }
                return false;
            }
        };
        return redisTemplate.execute(sessionCallback);
    }

而且sessionCallback的源码可以看到,

  1. operations.multi()直接使用jedis的multi会给transaction复制
  2. 最后会将连接释放
 public <T> T execute(SessionCallback<T> session) {
        RedisConnectionFactory factory = this.getConnectionFactory();
        RedisConnectionUtils.bindConnection(factory, this.enableTransactionSupport);
        Object var3;
        try {
            var3 = session.execute(this);
        } finally {
            RedisConnectionUtils.unbindConnection(factory);
        }
        return var3;
    }

先就这样吧,吃饭去了

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值