How to implement transaction in Spring Data Redis in a clean way?

sinujohn picture sinujohn · Feb 9, 2014 · Viewed 11k times · Source

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.

Answer

Christoph Strobl picture Christoph Strobl · Apr 28, 2014

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).