← Назад к вопросам

Как сделать транзакцию на Redis

2.3 Middle🔥 121 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как сделать транзакцию на Redis

Redis — это in-memory data store, НО это не просто кеш. Redis поддерживает транзакции через команды MULTI/EXEC и WATCH для обеспечения атомарности операций.

1. MULTI/EXEC: Базовые транзакции Redis

Концепция

CLIENT → MULTI (начать транзакцию)
      → SET key1 value1 (queue)
      → INCR counter (queue)
      → GET key1 (queue)
      → EXEC (выполнить все атомарно)

Ключевые моменты:

  • MULTI открывает транзакцию
  • Команды НЕ выполняются сразу, попадают в очередь
  • EXEC выполняет все команды атомарно (все или ничего)
  • Результат: массив ответов

Redis CLI пример

# В Redis CLI
MULTI
OK

SET user:1:name "John"
QUEUED

SET user:1:age "30"
QUEUED

INCR user:total_count
QUEUED

EXEC
1) OK
2) OK
3) (integer) 1

# Все три команды выполнены атомарно

2. Реализация на Java

С использованием Jedis

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class RedisTransactionExample {
    
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        try {
            // Начать транзакцию
            Transaction transaction = jedis.multi();
            
            // Добавить команды (они не выполняются сразу)
            transaction.set("user:1:name", "John");
            transaction.set("user:1:age", "30");
            transaction.incr("user:total_count");
            transaction.get("user:1:name");
            
            // Выполнить все команды атомарно
            List<Object> results = transaction.exec();
            
            // Результаты
            results.forEach(result -> System.out.println(result));
            // Вывод:
            // OK
            // OK
            // 1
            // John
            
        } finally {
            jedis.close();
        }
    }
}

С использованием Spring Data Redis

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void createUserTransaction(User user) {
        // SessionCallback выполняет операции в одной сессии Redis
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // Начать транзакцию
                operations.multi();
                
                // Добавить команды
                operations.opsForValue().set("user:" + user.getId() + ":name", user.getName());
                operations.opsForValue().set("user:" + user.getId() + ":email", user.getEmail());
                operations.opsForHash().putAll(
                    "user:" + user.getId(),
                    Map.of(
                        "name", user.getName(),
                        "email", user.getEmail(),
                        "age", user.getAge().toString()
                    )
                );
                
                // Выполнить все команды атомарно
                return operations.exec();
            }
        });
    }
}

3. WATCH: Оптимистичное блокирование

Проблема: MULTI/EXEC не предотвращает race condition'ы:

Thread 1: GET balance → 1000
Thread 2: GET balance → 1000
Thread 1: SET balance 500 (EXEC)
Thread 2: SET balance 500 (EXEC) — ОШИБКА! Потеряли обновление от Thread 1

Решение: WATCH

# Redis CLI
WATCH user:balance

GET user:balance  # → 1000

# Если другой клиент изменит user:balance здесь...
# ...то EXEC вернет NULL!

MULTI
SET user:balance 500
EXEC  # → NULL (watch failed)

Java пример с WATCH

public class RedisWatchExample {
    
    public static void transferMoney(String fromUser, String toUser, long amount) {
        Jedis jedis = new Jedis();
        
        while (true) {
            try {
                // WATCH баланс отправителя
                jedis.watch("user:" + fromUser + ":balance");
                
                // Получить текущий баланс
                long balance = Long.parseLong(
                    jedis.get("user:" + fromUser + ":balance") ?? "0"
                );
                
                if (balance < amount) {
                    throw new InsufficientFundsException();
                }
                
                // Начать транзакцию
                Transaction tx = jedis.multi();
                
                // Команды
                tx.decrBy("user:" + fromUser + ":balance", amount);
                tx.incrBy("user:" + toUser + ":balance", amount);
                tx.lpush("transactions", 
                    fromUser + " → " + toUser + ": " + amount);
                
                List<Object> result = tx.exec();
                
                if (result != null) {
                    // Успех
                    System.out.println("Транзакция успешна");
                    break;
                } else {
                    // Watch failed, retry
                    System.out.println("Retry...");
                }
            } finally {
                jedis.unwatch(); // отменить watch
            }
        }
    }
}

Spring Data Redis с WATCH

@Service
public class AccountService {
    @Autowired
    private RedisTemplate<String, Long> redisTemplate;
    
    public void transferMoney(String fromAccount, String toAccount, long amount) 
            throws InsufficientFundsException {
        
        while (true) {
            try {
                // Использовать SessionCallback для контроля транзакции
                Boolean success = redisTemplate.execute(
                    new SessionCallback<Boolean>() {
                        @Override
                        public Boolean execute(RedisOperations ops) 
                                throws DataAccessException {
                            
                            // WATCH
                            ops.watch(Collections.singletonList(
                                "account:" + fromAccount + ":balance"
                            ));
                            
                            // Получить баланс
                            Long balance = (Long) ops.opsForValue().get(
                                "account:" + fromAccount + ":balance"
                            );
                            
                            if (balance == null || balance < amount) {
                                throw new InsufficientFundsException();
                            }
                            
                            // MULTI
                            ops.multi();
                            
                            // Операции
                            ops.opsForValue().decrement(
                                "account:" + fromAccount + ":balance", amount
                            );
                            ops.opsForValue().increment(
                                "account:" + toAccount + ":balance", amount
                            );
                            
                            // EXEC
                            List<Object> result = ops.exec();
                            
                            // null означает watch failed
                            return result != null;
                        }
                    }
                );
                
                if (success) {
                    System.out.println("Транзакция успешна");
                    break;
                }
                // else: retry
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

4. Ограничения Redis транзакций

// ПРОБЛЕМА 1: Нет rollback!
public void problematicTransaction() {
    Transaction tx = jedis.multi();
    
    tx.set("key1", "value1");
    tx.incr("key2"); // Если key2 строка, EXEC вернет ошибку
    // Но key1 все равно установится!
    // Redis транзакция атомична только на уровне EXEC,
    // ошибки внутри не откатывают предыдущие команды
    
    List<Object> results = tx.exec();
    // results[0] = OK
    // results[1] = error
}

// РЕШЕНИЕ: Проверить результаты
List<Object> results = tx.exec();
for (int i = 0; i < results.size(); i++) {
    Object result = results.get(i);
    if (result instanceof Exception) {
        // Ошибка в команде i
        rollbackManually(i);
    }
}

5. Транзакции с Lua скриптами (лучший подход)

Для большей гибкости используйте Lua:

public class LuaTransactionExample {
    private static final String TRANSFER_SCRIPT = 
        "if redis.call('get', KEYS[1]) < ARGV[1] then " +
        "  return 0 " +
        "end " +
        "redis.call('decrby', KEYS[1], ARGV[1]) " +
        "redis.call('incrby', KEYS[2], ARGV[1]) " +
        "return 1";
    
    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        
        // Выполнить скрипт атомарно
        Object result = jedis.eval(
            TRANSFER_SCRIPT,
            2,  // количество ключей
            "from_account", "to_account",  // KEYS
            "100"  // ARGV
        );
        
        if (result.equals(1L)) {
            System.out.println("Перевод успешен");
        } else {
            System.out.println("Недостаточно средств");
        }
    }
}

6. Best Practices

@Service
public class RedisTransactionService {
    
    // ✓ Хорошо: используй SessionCallback
    public void goodWay() {
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations ops) {
                ops.watch(keys);
                ops.multi();
                // операции
                return ops.exec();
            }
        });
    }
    
    // ✗ Плохо: пытаться синхронизировать вручную
    public void badWay() {
        synchronized(this) {
            // это НЕ поможет, если других клиентов Redis!
        }
    }
    
    // ✓ Лучше: использовать Lua скрипты
    public void bestWay() {
        String script = "...";
        redisTemplate.execute(new RedisScript<Boolean>() {
            // ...
        });
    }
}

Таблица: транзакции RDBMS vs Redis

ПараметрRDBMSRedis
ACIDПолнаяТолько A и часть D
Rollback✓ Есть✗ Нет
WATCH/Lock✓ Есть✓ WATCH (оптимистичный)
ПроизводительностьСредняяОчень высокая
МасштабируемостьХорошаяОтличная (in-memory)

Вывод

Для транзакций в Redis:

  1. Простые случаи → MULTI/EXEC
  2. Конкурентный доступ → WATCH + MULTI/EXEC (с retry)
  3. Сложная логика → Lua скрипты
  4. RDBMS-стиль гарантии → используй RDBMS, не Redis

Redis транзакции предназначены для простых атомарных операций, а не для замены RDBMS.

Как сделать транзакцию на Redis | PrepBro