7

I am following RetwisJ tutorial available here. In this I don't think Redis transactions are implemented. For example, in the following function, if some exception occurs in between, the data will be left in an inconsistent state. I want to know how a function like the following can be implemented in Spring Data Redis as a single transaction:

public String addUser(String name, String password) {
        String uid = String.valueOf(userIdCounter.incrementAndGet());

        // save user as hash
        // uid -> user
        BoundHashOperations<String, String, String> userOps = template.boundHashOps(KeyUtils.uid(uid));
        userOps.put("name", name);
        userOps.put("pass", password);
        valueOps.set(KeyUtils.user(name), uid);

        users.addFirst(name);
        return addAuth(name);
    }

Here userIdCounter, valueOps and users are initialized in the constructor. I have come across this in the documentation(section 4.8), but I can't figure out how to fit that into this function where some variables are initialized outside the function(please don't tell I have to initialize these variables in each and every function where I need transactions!).

PS: Also is there any @Transaction annotation or transaction manager available for Spring Data Redis?

UPDATE: I have tried using MULTI, EXEC. The code which I have written is for another project, but when its applied to this problem it'll be as follows:

public String addMyUser(String name, String password) {
        String uid = String.valueOf(userIdCounter.incrementAndGet());
        template.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations)
                    throws DataAccessException {
                operations.multi();
                getUserOps(operations, KeyUtils.uid(uid)).put("name", name);
                getUserOps(operations, KeyUtils.uid(uid)).put("pass", password);
                getValueOps(operations).set(KeyUtils.user(name), uid);
                getUserList(operations, KeyUtils.users()).leftPush(name);
                operations.exec();
                return null;
            }
        });
        return addAuth(name);
    }
    private ValueOperations<String, String> getValueOps(RedisOperations operations) {
        return operations.opsForValue();
    }
    private BoundHashOperations<String, String, String> getUserOps(RedisOperations operations, String key) {
        return operations.boundHashOps(key);
    }
    private BoundListOperations<String, String> getUserList(RedisOperations operations, String key) {
        return operations.boundListOps(key);
    }

Please tell whether this way of using MULTI, EXEC is recommended or not.

sinujohn
  • 2,506
  • 3
  • 21
  • 26

2 Answers2

3

By default, RedisTemplate does not participate in managed Spring transactions. If you want RedisTemplate to make use of Redis transaction when using @Transactional or TransactionTemplate, you need to be explicitly enable transaction support for each RedisTemplate by setting setEnableTransactionSupport(true). Enabling transaction support binds RedisConnection to the current transaction backed by a ThreadLocal. If the transaction finishes without errors, the Redis transaction gets commited with EXEC, otherwise rolled back with DISCARD. Redis transactions are batch-oriented. Commands issued during an ongoing transaction are queued and only applied when committing the transaction.

Spring Data Redis distinguishes between read-only and write commands in an ongoing transaction. Read-only commands, such as KEYS, are piped to a fresh (non-thread-bound) RedisConnection to allow reads. Write commands are queued by RedisTemplate and applied upon commit.

https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#tx.spring

1

Up to SD Redis 1.2 you will have to take care of tansaction handling yourself using TransactionSynchronisationManager

The snipplet above could then look something like this:

public String addUser(String name, String password) {

    String uid = String.valueOf(userIdCounter.incrementAndGet());

    // start the transaction
    template.multi(); 

    // register synchronisation
    if(TransactionSynchronisationManager.isActualTransactionActive()) {
        TransactionSynchronisationManager.registerSynchronisation(new TransactionSynchronizationAdapter()) {

            @Override
            public void afterCompletion(int status) {
                switch(status) {
                    case STATUS_COMMITTED : template.exec(); break;
                    case STATUS_ROLLED_BACK : template.discard(); break;
                    default : template.discard(); 
                }
            }
        }
    }

    BoundHashOperations<String, String, String> userOps = template.boundHashOps(KeyUtils.uid(uid));
    userOps.put("name", name);
    userOps.put("pass", password);
    valueOps.set(KeyUtils.user(name), uid);

    users.addFirst(name);

    return addAuth(name);
}

Please note that once in multi, read operations will also be part of the transaction which means you'll likely not be able to read data from redis server. The setup might differ form the above one as you could want to additionally call WATCH. Further on you'll also have to take care of multiple callbacks do not sending MULTI and/or EXEC more than once.

The upcoming 1.3 RELEASE of Spring Data Redis will ship with support for spring managed transactions in a way of taking care of MULTi|EXEC|DISCARD plus allowing read operations (on already existing keys) while transaction synchronization is active. You could already give the BUILD-SNAPSHOT a spin and turn this on by setting template.setEnableTransactionSupport(true).

Christoph Strobl
  • 6,491
  • 25
  • 33
  • Hi, it has been a long time since this was asked. And since no one answered, I figured out how to do it(updated in the question). But I don't know whether this is the recommended way of doing it. Is TransactionSynchronisationManager the way to go? Does it require any additional configuration other than this? The advantage I have found in the code I have written is, if I have to read data from Redis, I can use 'template', and thus it won't be part of transaction – sinujohn May 10 '14 at 21:18
  • 1
    Using the `SessionCallback` is perfectly fine (see [reference](http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/redis.html#tx)) as this binds the used connection for the time the commands take to execute and releases it afterwards. The `TransactionSynchronisationManager` or in next version 1.3 `template.setEnableTransactionSupport(true)` is rather for dealing with multiple calls to redis that potentially are not closely located while there's an ongoing outer transaction active. Normally one should not have to directly interact with the TSM. – Christoph Strobl May 19 '14 at 06:11
  • @ChristophStrobl: I have similar case. I have searched through documentation but I don't see any reference how we guarantee transaction. Does properly configured RestTemplate should rollback once any exception is thrown? Let's say we have method in Spring Service: `@Transactional public void doSomth() { redisTemplate.delete(...); redisTemplate.delete(...);} ` Should it normally roll back first delete once second will fail? – Gazeciarz Mar 13 '18 at 12:26
  • @christoph-strobl having similar problem you described where I am successfully writing in the `SessionCallback` but in my test case reading in null. I am using spring boot 3.0.6. Is there anyway to force the operations to execute on the Redis server? – user3465651 May 11 '23 at 07:34