由 RedisTemplate 事务 enableTransactionSupport 引发的血案

16 篇文章 0 订阅
9 篇文章 0 订阅

在最近的项目当中因为使用了类似字典表的数据所以我使用的 Spring Farmework 当中的 Cache 抽象,通过 Redis 来做为缓存。因为原有项目当中配置了 Redis,而且项目是基于 Spring Boot 构建的,并没有去除 Redis 的自动依赖(RedisAutoConfiguration)。导致有些 Redis 的有些类是基于项目中自己配置的 RedisTemplate,而有些又是引用的 Spring Boot 中 Redis 的自动配置生成的 RedisTemplate。所以我就把 Spring Boot 中的 Redis 自动配置去除了。

去除 Spring Boot Redis 自动配置

@SpringBootApplication(exclude = {RedisAutoConfiguration.class})

上午进行项目发布,在下午的时候运维就找到说今天上午发布的项目 Redis 连接数一直在添加。(平时这个项目的 Redis 连接数也小)。因为使用的是 AWS ,所以在 CloudWatch 上面可以看到 Redis 的连接数:

在这里插入图片描述

并且登陆跳板机到 Linux 服务器上也看到连接 Redis 的服务是早上我新发布的服务:
在这里插入图片描述

并且在 Redis 中使用 client list 也看到是上午启动的服务的 IP 地址。

在这里插入图片描述
并且可以看到客户端请求的命令。而且服务当中也在报无法获取到 Redis 连接。

redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool  

这个很明显就是 Redis 连接无法获取,并且设置的 Redis 的最大连接数是 1000 所以 Redis 的连接数一直在增加。当在网上搜索 Redis 的连接没有被释放的时候搜索到了 RedisTemplate 设置了支持事务(enableTransactionSupport) 为 true。那么这个 Redis 请求必须在 JDBC 的事务当中,要么就不会释放连接。果然手动配置的 RedisTemplate 设置了支持事务(默认为 false,不支持)。然后在测试环境把这个值设置成 false 并且压力测试如果没有导致 Redis 请求数飙升。

所以一直没有产生这种情况的原因是手动配置 Redis 配置文件并没有生效,使用的是 Spring Boot 的 Redis 的自动依赖。自动依赖并没有配置这个值为 true(默认为 false),所以连接就会释放。RedisTemplate 使用 Redis 连接池释放连接的 2 种情况是:

  • RedisTemplate 不支持事务,当使用完连接之后就会释放连接到 Pool 当中
  • RedisTemplate 支持事务,并且必须和 JDBC 在一个事务当中,标注了 @Transactional注解

下面我们就通过源码来分析,RedisTemplate通过 opsForXXX 支持 Redis 中的各种数据操作,比如: opsForValue 操作 String 类型opsForSet 操作 Set 类型 等待。其实它最终会调用到:RedisTemplate#execute

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 = 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);

			boolean pipelineStatus = connToUse.isPipelined();
			if (pipeline && !pipelineStatus) {
				connToUse.openPipeline();
			}

			RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
			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);
		}
	}

上面代码的执行逻辑为:

  • 获取 Redis 的连接工厂 RedisConnectionFactory
  • 通过 boolean 类型值 enableTransactionSupport判断是否支持事务,如果支持事务通过方法RedisConnectionUtils.bindConnection获取并绑定 RedisConnectionHolder 到本地线程,如果不支持事务就通过方法 RedisConnectionUtils#getConnection 获取RedisConnectionHolder
  • 调用 RedisTemplate 的前置方法 RedisTemplate#preProcessConnection
  • 判断 RedisTemplate 是否使用了 pipeline,如果使用了调用 RedisConnection#openPipeline
  • 如果 exposeConnection 直接使用创建的 RedisConnection,否则创建代理对象 CloseSuppressingInvocationHandler
  • 调用回调方法 RedisCallback#doInRedis 执行 Redis 命令
  • 如果是 pipeline 就调用 RedisConnection#closePipeline关闭 pipeline
  • 最后在 finally 方法中调用 RedisConnectionUtils.releaseConnection 释放连接

RedisConnectionUtils#releaseConnection

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

上面的代码逻辑如下:

  • TransactionSynchronizationManager#getResource 获取 RedisConnectionHolder
  • 如果 RedisConnectionHolder 不为空并在事务处理当中直接返回
  • 如果传入的 RedisConnection 在事务当中,并且事务为只读就释放连接
  • 如果传入的 RedisConnection不在事务当中,就调用 RedisConnection#close 释放连接

可以看到如果 Redis 里面没有事务的时候就会直接调用 RedisConnection#close 把连接释放掉。当配置 RedisTemplate 的时候我看网上很多就是说在上面的第三步释放连接,这个说法是错误的。我把事务的条件都满足了都不会进行到第三步。这个问题困扰了我很久。ts

后面我自己 debug 解决了这个问题,答案就是在绑定并获取连接的时候会添加一个事务完成的回调方法,在这个回调方法当中释放 Redis 连接。

RedisConnectionUtils#bindConnection

RedisTemplate#execute 中,如果设置了 enableTransactionSupporttrue,就会通过 RedisConnectionUtils#bindConnection 方法获取并绑定 Redis 连接。

RedisConnectionUtils#bindConnection

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

		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 RedisConnectionHolder(connectionToBind);

			TransactionSynchronizationManager.bindResource(factory, connHolder);
			if (enableTransactionSupport) {
				potentiallyRegisterTransactionSynchronisation(connHolder, factory);
			}

			return connHolder.getConnection();
		}

		return conn;
	}

最终会调用到方法 RedisConnectionUtils#potentiallyRegisterTransactionSynchronisation

RedisConnectionUtils#potentiallyRegisterTransactionSynchronisation

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

		if (isActualNonReadonlyTransactionActive()) {

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

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

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

上面的代码逻辑如下:

  • 判断当前方法是否在一个数据库 JDBC 事务当中
  • 判断当前 Redis 连接是否新开启一个事务
  • 调用 RedisConnection#multi标记一个 Redis 事务块的开始
  • 注册添加一个事务回调类 RedisTransactionSynchronizer,它实现了TransactionSynchronization#afterCompletion 当数据库事务完成的时候会调用这个回调方法

TransactionSynchronization#afterCompletion

	@Override
	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);
		}
	}
  • 当事务完成之后会调用 RedisConnection#exec(),用于执行所有事务块内的命令。
  • 调用 RedisConnection#close 释放 Redis 连接
  • 调用 TransactionSynchronizationManager.unbindResource 解绑 ThreadLocal中的 Redis 连接

下面就是我写的一个支持事务的 RedisTemplate,它必须和数据 JDBC 事务一起使用。

Redis 相关的配置:

@Configuration
public class RedisConfig {

    @Bean(name = "redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory(generatePoolConfig());
        factory.setHostName("localhost");
        factory.setPort(6379);
        factory.setUsePool(true);
        factory.setConvertPipelineAndTxResults(true);
        factory.afterPropertiesSet();
        return factory;
    }

    @Bean(name = "redisTemplate")
    public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setEnableTransactionSupport(true);
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setValueSerializer(stringRedisSerializer);
        template.setDefaultSerializer(stringRedisSerializer);
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    private JedisPoolConfig generatePoolConfig() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(1000);
        poolConfig.setTestOnBorrow(true);
        return poolConfig;
    }

}

JDBC 与 Redis 事务使用:

@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource(name = "redisTemplate")
    private RedisTemplate<String, String> redisTemplate;

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional
    public void hello() {
        jdbcTemplate.execute("select 1");
        redisTemplate.opsForValue().get("test");
    }
}

必须满足以下几个条件:

  • RedisTemplate设置 enableTransactionSupporttrue,使用 Spring Redis 操作支付事务
  • 在方法上面添加 @Transactional ,并且配置 @EnableTransactionManagement 支持数据库事务
  • 使用数据连接操作,在这里面我使用的是最简单的 jdbcTemplate.execute("select 1") 执行数据库 ping 操作
  • 使用支持事务的 RedisTemplate 进行 Redis 命令操作

满足上面的 4 点条件才会释放 Spring RedisTemplate支持事务操作并且释放 Redis 连接,不然就不会释放 Redis 操作。

平常情况下不需要使用 RedisTemplate 的事务,直接使用默认值(不支持事务即可)

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值