Spring Data Redis中的事务陷阱

本文深入探讨了在Spring Data Redis中使用事务时遇到的问题及解决办法。主要关注于RedisTemplate开启事务后,若方法未标注@Transactional注解,则可能出现连接未正常释放的情况。文章通过源码分析,解释了问题产生的原因,并提供了具体的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址,转载请注明出处: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)相结合的不利之处。混合型事务的表现和预想的不太一样。
<think>嗯,用户明确询问Redis实现排行榜功能应该选择哪种数据类型,这其实是个非常经典的应用场景。从用户引用的资料来看,ta已经做了一些基础调研,看到了引用[1][2][3]都指向同一种解决方案——有序集合(Sorted Set)。不过用户似乎想再确认下这个选择的可靠性。 用户可能正在开发游戏排名或电商热销榜这类功能,需要实时更新和高效查询。ta特意强调“不是用户真正的需求”,说明ta更关注技术实现的准确性而非业务背景。有趣的是,三个引用都来自不同资料但结论完全一致,这种一致性在技术选型中很难得。 让我整理下关键证据链:引用[1]直接定义“有序集合为实现排行榜功能的解决方案”,引用[2]将其列为“核心数据结构”,引用[3]则用实际代码展示zset操作。特别是redisTemplate.opsForZSet()这个API调用,属于Spring Data Redis的典型用法,验证了方案的普适性。 考虑到用户可能需要的技术细节,我决定在回答中强调三个核心优势:自动排序特性(避免手动排序开销)、分数更新原子性(防止并发错误)、O(logN)的查询效率(支撑高并发)。这些在分布式系统中都是致命痛点,比如游戏排行榜每秒要处理数万次分数更新,传统数据库根本扛不住。 用户没问但可能关心的潜在问题是:当数据量极大时(比如10亿用户),单个zset能否撑住?其实Redis Cluster可以分片存储,每个节点只存部分用户数据,但全局排名计算会有精度问题。这个深度内容可以放在“相关问题”里引导扩展。 最后注意避免技术陷阱:引用[3]示例代码里用日期作为key的一部分,这暗示了重要实践——必须设计合理的key结构(如“rank:业务:维度:时间”),否则后期扩展开销巨大。这个细节要写在回答里。</think>要实现高效的排行榜功能,**Redis 的有序集合(Sorted Set / ZSET)** 是唯一且最优的数据类型选择。以下是详细说明和操作示例: --- ### 一、为什么选择有序集合(ZSET)? 有序集合的核心特性完美契合排行榜需求: 1. **自动按分数排序** 每个元素关联一个分数(`score`),Redis 自动按分数从低到高排序(默认升序,可通过命令反转)。 2. **高效操作** - 更新分数:`ZINCRBY key increment member`(原子操作,避免并发问题) - 查询排名:`ZRANK key member`(升序排名)/ `ZREVRANK key member`(降序排名) - 范围查询:`ZRANGE key start stop [WITHSCORES]`(获取指定区间成员) 3. **时间复杂度低** 增删改查操作均在 **$O(\log N)$** 内完成($N$ 为元素数量),支持百万级数据实时响应[^1][^2]。 --- ### 二、实现步骤与代码示例 #### 场景:游戏玩家得分排行榜 ```java // 1. 初始化排行榜Key(按业务维度设计,如:game:rank:2024) String rankKey = "game:rank:season5"; // 2. 更新玩家分数(原子操作,无需事务redisTemplate.opsForZSet().incrementScore(rankKey, "playerA", 50); // 玩家A增加50分 redisTemplate.opsForZSet().incrementScore(rankKey, "playerB", 30); // 3. 查询TOP10玩家(降序) Set<ZSetOperations.TypedTuple<String>> topPlayers = redisTemplate.opsForZSet() .reverseRangeWithScores(rankKey, 0, 9); // 返回玩家ID和分数 // 4. 查询单个玩家排名(降序排名从0开始) Long rank = redisTemplate.opsForZSet().reverseRank(rankKey, "playerA"); System.out.println("玩家A排名:" + (rank + 1)); // 输出第1名 ``` #### 关键命令说明: | 命令 | 作用 | 示例 | |---------------------|-----------------------------|-------------------------------| | `ZADD` | 添加/更新成员分数 | `ZADD rankKey 100 playerC` | | `ZINCRBY` | 原子增加分数 | `ZINCRBY rankKey 20 playerA` | | `ZREVRANGE` | 按分数降序获取成员 | `ZREVRANGE rankKey 0 9 WITHSCORES` | | `ZREVRANK` | 查询成员降序排名(0为第1名) | `ZREVRANK rankKey playerB` | --- ### 三、高级优化技巧 1. **分片存储** 当数据量极大时(如 > 100万成员),按业务维度拆分多个ZSET(如按地区/日期),减轻单Key压力[^3]。 ```java // 示例:按日期分片 String dailyKey = "rank:employee:广东省:" + LocalDate.now(); ``` 2. **混合存储** 将用户基础信息(如头像、昵称)存入Hash,ZSET仅存储ID和分数,减少内存占用: ```java // 用户详情存储 redisTemplate.opsForHash().put("user:playerA", "name", "张三"); redisTemplate.opsForHash().put("user:playerA", "avatar", "url.png"); ``` 3. **过期策略** 为临时榜单(如周榜)设置TTL,自动清理历史数据: ```java redisTemplate.expire(rankKey, 7, TimeUnit.DAYS); // 7天后自动删除 ``` --- ### 四、对比其他数据类型的劣势 - **普通集合(Set)**:无法排序,仅支持去重存储。 - **列表(List)**:插入有序但查询排名效率低($O(N)$)。 - **字符串(String)**:需手动维护排序,并发场景易出错。 > ✅ **结论:有序集合(ZSET)是实现排行榜功能的标准答案**,其设计直接解决了排序、更新、查询的核心需求[^1][^2][^3]。 --- ### 相关问题 1. 如何用Redis实现实时更新的全球排行榜? 2. 有序集合(ZSET)在超大规模数据下如何优化性能? 3. Redis实现排行榜时如何避免分数重复导致的排名错误? 4. ZSET的分数存储精度对排行榜有什么影响? 5. 如何用Redis实现多维度综合排行榜(如销量+评分)? 如果需要具体场景的代码实现或架构设计,可进一步交流!
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值