SpringBoot中使用redis事務

原文鏈接:https://www.jianshu.com/p/c9f5718e58f0

首先從使用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事務

既然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) {
        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註解式事務,首先要設置RedisTemplate的enableTransactionSupport屬性爲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://wolzq.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章