Комментарии (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
| Параметр | RDBMS | Redis |
|---|---|---|
| ACID | Полная | Только A и часть D |
| Rollback | ✓ Есть | ✗ Нет |
| WATCH/Lock | ✓ Есть | ✓ WATCH (оптимистичный) |
| Производительность | Средняя | Очень высокая |
| Масштабируемость | Хорошая | Отличная (in-memory) |
Вывод
Для транзакций в Redis:
- Простые случаи → MULTI/EXEC
- Конкурентный доступ → WATCH + MULTI/EXEC (с retry)
- Сложная логика → Lua скрипты
- RDBMS-стиль гарантии → используй RDBMS, не Redis
Redis транзакции предназначены для простых атомарных операций, а не для замены RDBMS.