Spring Data Redis中的事务陷阱 - Go语言中文社区

Spring Data Redis中的事务陷阱


原文地址,转载请注明出处:https://blog.csdn.net/qq_34021712/article/details/79606551   ©王赛超

之前spring整合redis开启事务,在功能测试环境下跑了N天之后,突然发现服务异常,查看日志报异常的具体内容如下:

org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
        at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204)
        at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348)
        at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129)
        at org.springframework.data.redis.core.RedisConnectionUtils.bindConnection(RedisConnectionUtils.java:67)
        at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:192)
        at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169)
        at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:91)
        at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:43)
        at com.ruubypay.miss.global.utils.RedisUtil.get(RedisUtil.java:80)
        at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl.verifyAuthFalse(LoginInfoServiceImpl.java:268)
        at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl.getLoginStatus(LoginInfoServiceImpl.java:81)
        at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl$$FastClassBySpringCGLIB$$8a83fccf.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
        at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:52)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:97)
        at com.ruubypay.miss.riskcontrol.config.SpringAOP.timeAround(SpringAOP.java:90)
        at sun.reflect.GeneratedMethodAccessor82.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629)
        at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618)
        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:52)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673)
        at com.ruubypay.miss.riskcontrol.interfaces.service.impl.LoginInfoServiceImpl$$EnhancerBySpringCGLIB$$57c35955.getLoginStatus(<generated>)
        at com.alibaba.dubbo.common.bytecode.Wrapper2.invokeMethod(Wrapper2.java)
        at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:46)
        at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:72)
        at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53)
        at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:64)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:70)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:132)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38)
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
        at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:113)
        at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:84)
        at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:170)
        at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:52)
        at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:82)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
Caused by: redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
        at redis.clients.util.Pool.getResource(Pool.java:51)
        at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
        at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16)
        at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194)
        ... 59 common frames omitted
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
        at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
        at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
        at redis.clients.util.Pool.getResource(Pool.java:49)
        ... 62 common frames omitted

无法从连接池中获取连接,环境配置连接数是1000,查了一下配置,发现跟别的项目唯一不同的地方就是该redis配置中对redis开启了事务。

初步预想

难道是redis连接用完没有释放吗?还是自己redis用的方式不对?

查看官方redis的文档:https://docs.spring.io/spring-data/redis/docs/1.8.8.RELEASE/reference/html/#tx.spring
5.10.1. @Transactional Support
Transaction Support is disabled by default and has to be explicitly enabled for each RedisTemplate in use by setting setEnableTransactionSupport(true). This will force binding the RedisConnection in use to the current Thread triggering MULTI. If the transaction finishes without errors, EXEC is called, otherwise DISCARD. Once in MULTI, RedisConnection would queue write operations, all readonly operations, such as KEYS are piped to a fresh (non thread bound) RedisConnection.

文档中也没多少东西,redis开启事务设置setEnableTransactionSupport(true).要在 添加了@Transactional注解的方法上,由于方法没必要开启事务,所以方法上没有加事务注解。难道是因为这个原因吗?

写了一段代码进行测试

在service的一个方法上没有加@Transactional注解,然后在使用redisTemplate之后,搞一个异常,代码如下:

Object o = redisTemplate.opsForValue().get("6666");
int a = 10/0;

结果很快就报异常了,异常信息如下:


开启debug过程

①首先我们定位到代码RedisTemplate.execute方法,源代码如下:

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 = getConnectionFactory();
	RedisConnection conn = null;
	try {


		//这里判断redis的EnableTransactionSupport是否为true,如果为true将连接绑定到当前线程
		if (enableTransactionSupport) {
			// 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);


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


		// TODO: any other connection processing?
		return postProcessResult(result, connToUse, existingConnection);
	} finally {
		//最后释放连接
		RedisConnectionUtils.releaseConnection(conn, factory);
	}
}
②因为将EnableTransactionSupport设置为true,所以我们定位到conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);这行代码,源代码如下:
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
												  boolean enableTransactionSupport) {


		Assert.notNull(factory, "No RedisConnectionFactory specified");


		//这里拿到的是一个null值
		RedisConnectionUtils.RedisConnectionHolder connHolder = (RedisConnectionUtils.RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);


		if (connHolder != null) {
			if (enableTransactionSupport) {
				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;
			if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
				connectionToBind = createConnectionProxy(conn, factory);
			}


			connHolder = new RedisConnectionUtils.RedisConnectionHolder(connectionToBind);


			//这里将代码绑定到当前线程,内部使用ThreadLocal实现
			TransactionSynchronizationManager.bindResource(factory, connHolder);
			if (enableTransactionSupport) {
				//这个方法是如果加了@Transactional注解,帮你开启 multi
				potentiallyRegisterTransactionSynchronisation(connHolder, factory);
			}


			//这里将当前线程绑定的连接返回
			return connHolder.getConnection();
		}


		return conn;
	}
③将代码定位到TransactionSynchronizationManager.bindResource(factory, connHolder);这一行,内部使用ThreadLocal实现,源代码如下:
public static void bindResource(Object key, Object value) throws IllegalStateException {
	Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
	Assert.notNull(value, "Value must not be null");
	Map<Object, Object> map = resources.get();
	// set ThreadLocal Map if none found
	if (map == null) {
		map = new HashMap<Object, Object>();
		resources.set(map);
	}
	Object oldValue = map.put(actualKey, value);
	// Transparently suppress a ResourceHolder that was marked as void...
	if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
		oldValue = null;
	}
	if (oldValue != null) {
		throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
				actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
				Thread.currentThread().getName() + "]");
	}
}
这里的resources就是ThreadLocal

④再将代码定位到第二步中的potentiallyRegisterTransactionSynchronisation(connHolder, factory);内部会判断是不是在一个事务中,如果在一个事务中,它帮你开启multi,但是这个方法里面的代码并没有执行,因为没有加@Transactional注解,根本就没在一个事务中,源代码如下:

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


	//这里的isActualNonReadonlyTransactionActive()方法判断是否在一个事务中,debug并没有进这段代码,因为没有加@Transactional注解
	if (isActualNonReadonlyTransactionActive()) {


		if (!connHolder.isTransactionSyncronisationActive()) {
			connHolder.setTransactionSyncronisationActive(true);


			RedisConnection conn = connHolder.getConnection();
			conn.multi();


			TransactionSynchronizationManager.registerSynchronization(new RedisConnectionUtils.RedisTransactionSynchronizer(connHolder, conn,
					factory));
		}
	}
}

到这里,以上就是开启连接,并从redis中获取属性的过程,因为没有加@Transactional注解,所以并没有开启multi,命令也没有进入Queue中,可以直接拿到redis中的值。但是, 还是进行了 将连接绑定到当前线程的操作。下面我们看一下关闭连接的过程。

⑤ 将代码定位到第一步RedisConnectionUtils.releaseConnection(conn, factory);源代码如下:
public static void releaseConnection(RedisConnection conn, RedisConnectionFactory factory) {


	if (conn == null) {
		return;
	}


	RedisConnectionUtils.RedisConnectionHolder connHolder = (RedisConnectionUtils.RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);


	//可以获取到connHolder 但是 connHolder.isTransactionSyncronisationActive() 却是false,因为之前绑定连接的时候,并没有在一个事务中,连接绑定了,但是isTransactionSyncronisationActive属性并没有给值
	//忘记的朋友可以看一下 第四步 potentiallyRegisterTransactionSynchronisation 中的代码,其实 是没有执行的,所以 isTransactionSyncronisationActive 的默认值是false
	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
	//第一个条件判断为true 但是第二个条件判断为false 不是一个只读事务,所以unbindConnection(factory) 代码没有执行
	if (isConnectionTransactional(conn, factory)
			&& TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
		unbindConnection(factory);
	//然后 else if 也是返回false  因为	isConnectionTransactional(conn, factory)返回的是true 内部代码判断连接这个连接和 线程中绑定的连接是不是同一个,是同一个 由于前面加了一个 ! 号 所以结果为false
	} else if (!isConnectionTransactional(conn, factory)) {
		if (log.isDebugEnabled()) {
			log.debug("Closing Redis Connection");
		}
		conn.close();
	}
}

第一个if中,条件(isConnectionTransactional(conn, factory)判断为true 但是第二个条件

TransactionSynchronizationManager.isCurrentTransactionReadOnly()判断为false 不是一个只读事务,所以unbindConnection(factory) 代码没有执行

第二个else if中 isConnectionTransactional(conn, factory) 判断 传过来的这个连接是不是 线程绑定的连接,如果是返回true 结果是同一个连接,返回了 true  但是由于前面加了一个 ! 号 true变 false  代码conn.close(); 并没有执行,然后你会很操.蛋的发现,方法就这样过去了,结束了,连接没有关闭,也没有从当前线程上释放。

解决方案

以上是debug的过程,那既然发现了问题,就要解决问题既然它没有执行释放的动作,那我们帮他执行就好了。继续阅读TransactionSynchronizationManager的源码,发现有TransactionSynchronizationManager.unbindResource(factory);这个方法,这个方法的内部就是将资源释放,最后执行方法的源代码如下:

private static Object doUnbindResource(Object actualKey) {
	Map<Object, Object> map = resources.get();
	if (map == null) {
		return null;
	}
	Object value = map.remove(actualKey);
	// Remove entire ThreadLocal if empty...
	if (map.isEmpty()) {
		resources.remove();
	}
	// Transparently suppress a ResourceHolder that was marked as void...
	if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
		value = null;
	}
	if (value != null && logger.isTraceEnabled()) {
		logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" +
				Thread.currentThread().getName() + "]");
	}
	return value;
}
如果你的redisTemplate开启了事务,在未标明@Transactional的方法内使用时,可以在redisTemplate操作redis之后立马调用该方法,具体代码如下:
/**
 * 普通缓存获取
 * @param key 键
 * @return 值
 */
public Object get(String key){
	Object o = redisTemplate.opsForValue().get(key);
	TransactionSynchronizationManager.unbindResource(redisTemplate.getConnectionFactory());
	return o;
}

到这里可能有个疑问,那@Transactional注解是怎么释放连接的呢?

不妨在方法上添加该注解之后,再次debug查看代码,这里就不发各个方法的源代码了,有兴趣的朋友自己debug一下立马明白,我们发现最终的代码执行流程如下:

org.springframework.data.redis.core.RedisConnectionUtils$RedisTransactionSynchronizer.afterCompletion(RedisConnectionUtils.java:292)
org.springframework.transaction.support.TransactionSynchronizationUtils.invokeAfterCompletion(TransactionSynchronizationUtils.java:165)
org.springframework.transaction.support.AbstractPlatformTransactionManager.invokeAfterCompletion(AbstractPlatformTransactionManager.java:1002)
org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerAfterCompletion(AbstractPlatformTransactionManager.java:968)
org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:741)
org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:703)
org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:500)
org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
org.springframework.aop.framework.CglibAopProxy.intercept(CglibAopProxy.java:673)
可以直接按照我说的方法处打断点验证一下,最后我们发现TransactionSynchronizationUtils.invokeAfterCompletion 这个方法,这个方法是关键啊,我们可以看一下它的源码,源码如下:
public static void invokeAfterCompletion(List<TransactionSynchronization> synchronizations, int completionStatus) {
	if (synchronizations != null) {
		for (TransactionSynchronization synchronization : synchronizations) {
			try {
				synchronization.afterCompletion(completionStatus);
			}
			catch (Throwable tsex) {
				logger.error("TransactionSynchronization.afterCompletion threw exception", tsex);
			}
		}
	}
}
遍历得到每一个TransactionSynchronization,然后调用它的afterCompletion方法,TransactionSynchronization是一个接口,实现它的有很多,像jdbc数据源释放 什么的都是通过该方法一次性释放

我们可以看一下RedisTransactionSynchronizer 的 afterCompletion方法,在此方法内提交或回滚事务,并最终释放连接,源代码如下:

public void afterCompletion(int status) {

	try {
		switch (status) {
			//如果事务正常,最终提交事务
			case TransactionSynchronization.STATUS_COMMITTED:
				connection.exec();
				break;
			//如果有异常,事务回滚
			case TransactionSynchronization.STATUS_ROLLED_BACK:
			case TransactionSynchronization.STATUS_UNKNOWN:
			default:
				connection.discard();
		}
	} finally {

		if (log.isDebugEnabled()) {
			log.debug("Closing bound connection after transaction completed with " + status);
		}

		connHolder.setTransactionSyncronisationActive(false);
		//关闭连接
		connection.close();
		//从当前线程释放连接
		TransactionSynchronizationManager.unbindResource(factory);
	}
}

redisTemplate开启事务的另一个坑

也就是上面提到过得,将命令放到Queue中,代码片段如下:
Long increment = redisTemplate.opsForValue().increment("6666", 1);
System.out.println(increment);

是无法拿到返回值的,是因为redis在MULTI/EXEC代码块中,命令都会被delay,放入Queue中,而不会直接返回对应的值。即例子中的increment自增,自增命令执行完成,默认是会返回自增之后的值,但是却是返回了null.

redisTemplate正确使用
其实,别人之前也有遇到过这个问题,A transaction pitfall in Spring Data Redis
我在 Spring 的 RedisTemplate 类 redisTemplate.setEnableTransactionSupport(true); 中启用 Redis 事务时得到一个惨痛的教训:Redis 会在运行几天后开始返回垃圾数据,导致数据严重损坏。StackOverflow上也报道了类似情况。
在运行一个 monitor 命令后,我的团队发现,在进行 Redis 操作或 RedisCallback 后,Spring 并没有自动关闭 Redis 连接,而事实上它是应该关闭的。如果再次使用未关闭的连接,可能会从意想不到的 Redis 密钥返回垃圾数据。有意思的是,如果在 RedisTemplate 中把事务支持设为 false,这一问题就不会出现了。
我们发现,我们可以先在 Spring 语境里配置一个 PlatformTransactionManager(例如 DataSourceTransactionManager),然后再用 @Transactional 注释来声明 Redis 事务的范围,让 Spring 自动关闭 Redis 连接。
根据这一经验,我们相信,在 Spring 里配置两个单独的 RedisTemplate 是很好的做法:其中一个 RedisTemplates 的事务设为 false,用于大多数 Redis 操作,另一个 RedisTemplates 的事务已激活,仅用于 Redis 事务。当然必须要声明 PlatformTransactionManager 和 @Transactional,以防返回垃圾数值。
另外,我们还发现了 Redis 事务和关系数据库事务(在本例中,即 JDBC)相结合的不利之处。混合型事务的表现和预想的不太一样。
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_34021712/article/details/79606551
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 20:15:15
  • 阅读 ( 1127 )
  • 分类:Redis

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢