RedisTemplate使⽤事务处理
⾸先从使⽤springboot+redis碰到的⼀个问题说起。在前⼏篇⽂章中介绍了⽤SpringBoot+redis构建了⼀个个⼈博客。在刚开始远⾏的时候发现发了⼏个请求操作了⼏次redis之后,后⾯的就被阻塞了,请求⼀直在等待返回,我们重现⼀下问题。
[注意] 该问题只会出现在springboot 2.0之前的版本;2.0之后springboot连接Redis改成了lettuce,并重新实现,问题已经不存在
打开Template的事务⽀持
POM配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/POM/4.0.0" xmlns:xsi="/2001/XMLSchema-instance"
xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.springboot</groupId>
<artifactId>redis-tx-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBoot redis TX demo</name>
<description>Demo project for Spring Boot with Redis transaction</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
&porting.outputEncoding>UTF-8</porting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Redis configuration (EnbaleTransactionSupport设为true):
@Configuration
public class RedisConfiguration {
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
template.setEnableTransactionSupport(true); //打开事务⽀持
return template;
}
}
Controller就是简单的set⼀个key到redis:
@RestController
public class DemoController {
private StringRedisTemplate template;
public DemoController(StringRedisTemplate template) {
}
@GetMapping("/put")
public String redisSet() {
int i = (int)(Math.random() * 100);
template.opsForValue().set("key"+i, "value"+i, 300, TimeUnit.SECONDS);
return "success "+"key"+i;
}
}
127.0.0.1:6379> info clients
# Clients
connected_clients:9
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:0
127.0.0.1:6379>
还有查看程序的⽇志可以发现,RedisConnectionUtils只有Opening RedisConnection⽽没有close。
2018-08-11 11:00:48.889 [DEBUG][http-nio-8080-exec-8]:o.RedisConnectionUtils [doGetConnection:126] Opening RedisConnection
2018-08-11 11:00:50.169 [DEBUG][http-nio-8080-exec-8]:o.s.a.RequestResponseBodyMethodProcessor [writeWithMessageConverters:249] Written [success key39] as "text/plain" using [org.verter.StringHttpMe 2018-08-11 11:00:50.170 [DEBUG][http-nio-8080-exec-8]:org.springframework.web.servlet.DispatcherServlet [processDispatchResult:1044] Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapte 2018-08-11 11:00:50.170 [DEBUG][http-nio-8080-exec-8]:org.springframework.web.servlet.DispatcherServlet [processRequest:1000] Successfully completed request
2018-08-11 11:00:50.170 [DEBUG][http-nio-8080-exec-8]:o.s.boot.web.filter.OrderedRequestContextFi
lter [doFilterInternal:104] Cleared thread-bound request context: org.tor.RequestFacade@c03b2d8
2018-08-11 11:00:53.854 [DEBUG][http-nio-8080-exec-9]:o.s.boot.web.filter.OrderedRequestContextFilter [initContextHolders:114] Bound request context to thread: org.tor.RequestFacade@c03b2d8
2018-08-11 11:00:53.856 [DEBUG][http-nio-8080-exec-9]:org.springframework.web.servlet.DispatcherServlet [doService:865] DispatcherServlet with name 'dispatcherServlet' processing GET request for [/put]
2018-08-11 11:00:53.857 [DEBUG][http-nio-8080-exec-9]:o.s.a.RequestMappingHandlerMapping [getHandlerInternal:310] Looking up handler method for path /put
2018-08-11 11:00:53.857 [DEBUG][http-nio-8080-exec-9]:o.s.a.RequestMappingHandlerMapping [getHandlerInternal:317] Returning handler method [public java.lang.String com.github.springboot.disSet
2018-08-11 11:00:53.858 [DEBUG][http-nio-8080-exec-9]:o.s.b.factory.support.DefaultListableBeanFactory [doGetBean:251] Returning cached instance of singleton bean 'demoController'
2018-08-11 11:00:53.858 [DEBUG][http-nio-8080-exec-9]:org.springframework.web.servlet.DispatcherServlet [doDispatch:951] Last-Modified value for [/put] is: -1
2018-08-11 11:00:53.861 [DEBUG][http-nio-8080-exec-9]:o.RedisConnectionUtils [doGetConnection:126] Opening RedisConnection
关闭template的事务⽀持
接下来我们修改⼀下RedisConfiguration的配置,不启⽤事务管理,
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
//      template.setEnableTransactionSupport(true);  //禁⽤事务⽀持
return template;
}
重新测试⼀下,发现是正常的,redis的client链接数⼀直保持在2。程序⽇志⾥的也可以看到Redis Connection关闭的⽇志。
2018-08-11 15:55:19.975 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [doGetConnection:126] Opening RedisConnection
2018-08-11 15:55:20.029 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [releaseConnection:210] Closing Redis Connection
2018-08-11 15:55:20.056 [DEBUG][http-nio-8080-exec-1]:o.s.a.RequestResponseBodyMethodProcessor [writeWithMessageConverters:249] Written [success key72] as "text/plain" using [org.verter.
也就是说,如果我们把事务的⽀持打开,spring在每次操作之后是不会主动关闭连接的。我们去RedisTemplate的源码中下原因。
public ValueOperations<K, V> opsForValue() {
if (valueOps == null) {
valueOps = new DefaultValueOperations<K, V>(this);
}
return valueOps;
}
可以发现template.opsForValue().set()操作最终是调⽤的DefaultValueOperations中的set()⽅法,继续跟进去最终调⽤的RedisTemplate中的execute(RedisCallback<T> action, boolean
exposeConnection, boolean pipeline)⽅法。
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
RedisConnectionFactory factory = getConnectionFactory();
RedisConnection conn = null;
try {
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = Connection(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);springframework事务
} finally {
}
}
可以看到获取连接的操作也针对打开事务⽀持的template有特殊的处理逻辑。这⾥我们先跳过,先看看最终肯定会⾛到的leaseConnection(conn, factory)这⼀步。
/**
* Closes the given connection, created via the given factory if not managed externally (i.e. not bound to the
* thread).
*
* @param conn the Redis connection to close
* @param factory the Redis factory that the connection was created with
*/
public static void releaseConnection(RedisConnection conn, RedisConnectionFactory factory) {
if (conn == null) {
return;
}
RedisConnectionHolder connHolder = (RedisConnectionHolder) Resource(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();
}
}
可以看到针对打开事务⽀持的template,只是解绑了连接,根本没有做close的操作。关于什么是解绑,其实这个⽅法的注释中已经说的⽐较清楚了,对于开启了事务的Template,
由于已经绑定了线程中连接,所以这⾥是不会关闭的,只是做了解绑的操作。
到这⾥原因就很清楚了,就是只要template开启了事务⽀持,spring就认为只要使⽤这个template就会包含在事务当中,因为⼀个事务中的操作必须在同⼀个连接中完成,所以在
每次get/set之后,template是不会关闭链接的,因为它不知道事务有没有结束。
使⽤@Transanctional注解⽀持Redis事务
既然RedisTemlate在setEnableTransactionSupport会造成连接不关闭,那怎么样才能正常关闭呢?我们将事务⽀持开关和@Transanctional结合起来⽤看看会怎么样。
spring中要使⽤@Transanctional⾸先要配transactionManager,但是spring没有专门针对Redis的事务管理器实现,⽽是所有调⽤RedisTemplate的⽅法最终都会调⽤到RedisConnctionUtils这
个类的⽅法上⾯,在这个类⾥⾯会判断是不是进⼊到事务⾥⾯,也就是说Redis的事务管理的功能是由RedisConnctionUtils内部实现的。
根据官⽅⽂档,我只想⽤Redis事务,也必须把JDBC捎上。当然反过来讲,不依赖数据的项⽬确实不多,貌似这么实现影响也不⼤。下⾯我们先根据官⽅⽂档配置⼀下看看效
果。
⾸先修改POM配置,添加两个依赖。如果项⽬⾥本来已经使⽤了数据库,那这⼀步就不需要了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
然后修改RedisConfiguration
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
template.setEnableTransactionSupport(true);//打开事务⽀持
return template;
}
/
/配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
我们新建⼀个RedisService,将原来的数据操作移到service⾥⾯。同时将Service⽅法加上@Transactional注解。
@Service
public class RedisService {
private StringRedisTemplate template;
public RedisService(StringRedisTemplate template) {
}
@Transactional
public String put() {
int i = (int)(Math.random() * 100);
template.opsForValue().set("key"+i, "value"+i, 300, TimeUnit.SECONDS);
return "success "+"key"+i;
}
}
//-----------------------------------------------------------
/
/controller⾥⾯加⼀个新的⽅法,调⽤Service
@GetMapping("/puttx")
public String redisTxSet() {
return redisService.put();
}
2018-08-11 20:57:04.990 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [doGetConnection:126] Opening RedisConnection
2018-08-11 20:57:04.990 [DEBUG][http-nio-8080-exec-1]:o.springframework.aop.framework.JdkDynamicAopProxy [getProxy:118] Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework 2018-08-11 20:57:04.990 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [intercept:337] Invoke 'multi' on bound conneciton
2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [intercept:337] Invoke 'isPipelined' on bound conneciton
2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [intercept:337] Invoke 'setEx' on bound conneciton
2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [releaseConnection:198] Redis Connection will be closed when transaction finished.
2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.s.jdbc.datasource.DataSourceTransactionManager [processCommit:759] Initiating transaction commit
2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.s.jdbc.datasource.DataSourceTransactionManager [doCommit:310] Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[conn9: url=jdbc:h2:mem 2018-08-11 20:57:04.992 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [intercept:337] Invoke 'exec' on bound conneciton
2018-08-11 20:57:04.992 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [afterCompletion:306] Closing bound connection after transaction completed with 0
2018-08-11 20:57:04.992 [DEBUG][http-nio-8080-exec-1]:o.RedisConnectionUtils [intercept:337] Invoke 'close' on bound conneciton
2018-08-11 20:57:04.993 [DEBUG][http-nio-8080-exec-1]:o.s.jdbc.datasource.DataSourceTransactionManager [doCleanupAfterCompletion:368] Releasing JDBC Connection [ProxyConnection[PooledConnection[conn9: url=jdbc:h2:mem 2018-08-11 20:57:04.993 [DEBUG][http-nio-8080-exec-1]:o.springframework.jdbc.datasource.DataSourceUtils [doReleaseConnection:327] Returning JDBC Connection to DataSource
总结
在spring中要使⽤Redis注解式事务,⾸先要设置RedisTemplate的enableTransactionSupport属性为true,然后配置⼀个jdbc的事务管理器。
这⾥有⼀点⾮常重要,⼀旦这样配置,所有使⽤这个template的redis操作都必须⾛注解式事务,要不然会导致连接⼀直占⽤,不关闭。
建议
升级到springboot 2.0以上版本,如果因为项⽬原因⽆法升级看下⾯的建议
如果使⽤Redis事务的场景不多,完全可以⾃⼰管理,不需要使⽤spring的注解式事务。如下⾯这样使⽤:
List<Object> txResults = ute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForSet().add("key", "value1");
// This will contain the results of all ops in the transaction
();
}
});
如果⼀定要使⽤spring提供的注解式事务,建议初始化两个RedisTemplate Bean,分别设置enableTransactionSupport属性为true和false。针对需要事务和不需要事务的操作使⽤不同的template。
从个⼈⾓度,我不建议使⽤redis事务,因为redis对于事务的⽀持并不是关系型数据库那样满⾜ACID。Redis事务只能保证ACID中的隔离性和⼀致性,⽆法保证原⼦性和持久性。⽽我们使⽤事务最重要的⼀个理由就是原⼦性,这⼀点⽆法保证,事务的意义就去掉⼀⼤半了。所以事务的场景可以尝试通过业务代码来实现。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。