zoukankan      html  css  js  c++  java
  • 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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/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>
            <project.reporting.outputEncoding>UTF-8</project.reporting.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) {
            this.template = 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;
        }
    
    }
    

    启动后,我们使用RestClient发送请求http://localhost:8080/put,发送8次之后就会发现没有返回了。这个时候我们查看redis的链接数,发现已经超过8个,springboot对于jedis连接池默认的最大活跃连接数是8,所以看出来是连接池被耗光了。

    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.s.data.redis.core.RedisConnectionUtils [doGetConnection:126] Opening RedisConnection
    2018-08-11 11:00:50.169 [DEBUG][http-nio-8080-exec-8]:o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor [writeWithMessageConverters:249] Written [success key39] as "text/plain" using [org.springframework.http.converter.StringHttpMessageConverter@766a49c7]
    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 HandlerAdapter completed request handling
    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.OrderedRequestContextFilter [doFilterInternal:104] Cleared thread-bound request context: org.apache.catalina.connector.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.apache.catalina.connector.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.w.s.m.m.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.w.s.m.m.a.RequestMappingHandlerMapping [getHandlerInternal:317] Returning handler method [public java.lang.String com.github.springboot.demo.DemoController.redisSet()]
    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.s.data.redis.core.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.s.data.redis.core.RedisConnectionUtils [doGetConnection:126] Opening RedisConnection
    2018-08-11 15:55:20.029 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.RedisConnectionUtils [releaseConnection:210] Closing Redis Connection
    2018-08-11 15:55:20.056 [DEBUG][http-nio-8080-exec-1]:o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor [writeWithMessageConverters:249] Written [success key72] as "text/plain" using [org.springframework.http.converter.StringHttpMessageConverter@51ab1ee3]
    

    也就是说,如果我们把事务的支持打开,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");
            Assert.notNull(action, "Callback object must not be null");
    
            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 = 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);
            }
        }
    

    可以看到获取连接的操作也针对打开事务支持的template有特殊的处理逻辑。这里我们先跳过,先看看最终肯定会走到的RedisConnectionUtils.releaseConnection(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) 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();
            }
        }
    

    可以看到针对打开事务支持的template,只是解绑了连接,根本没有做close的操作。关于什么是解绑,其实这个方法的注释中已经说的比较清楚了,对于开启了事务的Template,由于已经绑定了线程中连接,所以这里是不会关闭的,只是做了解绑的操作。
    到这里原因就很清楚了,就是只要template开启了事务支持,spring就认为只要使用这个template就会包含在事务当中,因为一个事务中的操作必须在同一个连接中完成,所以在每次get/set之后,template是不会关闭链接的,因为它不知道事务有没有结束。

    使用@Transanctional注解支持Redis事务

    既然RedisTemlatesetEnableTransactionSupport会造成连接不关闭,那怎么样才能正常关闭呢?我们将事务支持开关和@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) {
            this.template = 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();
    }
    

    完成这些工作之后,再往http://localhost:8080/puttx发送请求,无论点多少次,Redis的连接数始终维持在1个不变。在看程序的输出日志里面我们也发现了,事务结束后连接被正常释放。因为使用了JDBC的事务管理器,所以还顺便做了一次数据库事务的开启和提交。还有一点值得注意的是,跟数据库一样,使用注解来做事务管理,spring也会主动管理redis事务的提交和回滚,也就是在之前发送一条MULTI命令,成功后发送EXEC,失败后发送DISCARD。

    2018-08-11 20:57:04.990 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.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.data.redis.connection.jedis.JedisConnection@20f2be3c]
    2018-08-11 20:57:04.990 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.RedisConnectionUtils [intercept:337] Invoke 'multi' on bound conneciton
    2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.RedisConnectionUtils [intercept:337] Invoke 'isPipelined' on bound conneciton
    2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.RedisConnectionUtils [intercept:337] Invoke 'setEx' on bound conneciton
    2018-08-11 20:57:04.991 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.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:testdb user=SA]]]
    2018-08-11 20:57:04.992 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.RedisConnectionUtils [intercept:337] Invoke 'exec' on bound conneciton
    2018-08-11 20:57:04.992 [DEBUG][http-nio-8080-exec-1]:o.s.data.redis.core.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.s.data.redis.core.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:testdb user=SA]]] after transaction
    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注解式事务,首先要设置RedisTemplateenableTransactionSupport属性为true,然后配置一个jdbc的事务管理器。
    这里有一点非常重要,一旦这样配置,所有使用这个template的redis操作都必须走注解式事务,要不然会导致连接一直占用,不关闭。

    建议

    • 升级到springboot 2.0以上版本,如果因为项目原因无法升级看下面的建议
    • 如果使用Redis事务的场景不多,完全可以自己管理,不需要使用spring的注解式事务。如下面这样使用:
    List<Object> txResults = redisTemplate.execute(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
        return operations.exec();
      }
    });
    
    • 如果一定要使用spring提供的注解式事务,建议初始化两个RedisTemplate Bean,分别设置enableTransactionSupport属性为true和false。针对需要事务和不需要事务的操作使用不同的template。
    • 从个人角度,我不建议使用redis事务,因为redis对于事务的支持并不是关系型数据库那样满足ACID。Redis事务只能保证ACID中的隔离性和一致性,无法保证原子性和持久性。而我们使用事务最重要的一个理由就是原子性,这一点无法保证,事务的意义就去掉一大半了。所以事务的场景可以尝试通过业务代码来实现。

    本篇博客使用的代码:https://github.com/chilexun/springboot-demo.git

  • 相关阅读:
    开博
    jmeter插件安装
    eclipse清理项目缓存
    java.lang.UnsupportedClassVersionError: JVMCFRE003解决方法--jdk 1.6 中switch的参数无法使用String类型
    转:Eclipse Memory Analyzer入门学习笔记
    转发: 探秘Java中的String、StringBuilder以及StringBuffer
    Java.net.SocketException: Unrecognized Windows Sockets error: 0: JVM_Bind异常
    windows 查看端口是否被占用
    iostat命令
    计算机原码、补码、反码与java移位运算符(<</>>/>>>)
  • 原文地址:https://www.cnblogs.com/fantjesse/p/12206688.html
Copyright © 2011-2022 走看看