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

Как выглядит транзакционность в NoSQL

2.3 Middle🔥 71 комментариев
#Базы данных и SQL#Кэширование и NoSQL

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

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

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

# Транзакции в NoSQL базах данных

НоSQL СУБД радикально отличаются от SQL по подходу к транзакциям. Вместо ACID в классическом смысле, они часто используют BASE модель и предлагают различные уровни консистентности.

ACID vs BASE

ACID (SQL базы):

  • Atomicity — все или ничего
  • Consistency — данные всегда в корректном состоянии
  • Isolation — транзакции не влияют друг на друга
  • Durability — данные сохранены навсегда

BASE (NoSQL базы):

  • Basically Available — система доступна большую часть времени
  • Soft state — состояние может измениться даже без новых данных
  • Eventually Consistent — консистентность достигается со временем

Типы NoSQL баз и их подход к транзакциям

1. Document Store (MongoDB)

Одиночные документы — ACID транзакции:

// MongoDB обеспечивает ACID для одного документа
MongoDatabase db = mongoClient.getDatabase("store");
MongoCollection<Document> orders = db.getCollection("orders");

// Это атомарно — либо обновляется весь документ, либо ничего
order.put("status", "paid");
order.put("amount", 100);
order.put("timestamp", new Date());
orders.updateOne(new Document("_id", orderId), 
                 new Document("$set", order));

Преимущество: каждый документ обновляется атомарно. Недостаток: нет гарантий между несколькими документами.

Multi-document транзакции (MongoDB 4.0+):

ClientSession session = mongoClient.startSession();
try {
    session.startTransaction();
    
    // Несколько операций в одной транзакции
    orders.insertOne(session, new Document("_id", 1));
    accounts.updateOne(session, 
                       new Document("_id", customerId),
                       new Document("$inc", new Document("balance", -100)));
    
    session.commitTransaction();  // ACID гарантия для всех операций
} catch (Exception e) {
    session.abortTransaction();  // Откатываются все операции
} finally {
    session.close();
}

Ограничения:

  • Работает только на replica sets
  • Максимум 16 операций в транзакции (MongoDB 4.2)
  • Медленнее, чем обновление одного документа

2. Key-Value Store (Redis)

Redis не имеет транзакций в классическом смысле, но есть MULTI/EXEC:

RedisClient client = new RedisClient();
Jedis jedis = client.getJedis();

// Использование транзакции
Transaction transaction = jedis.multi();
transaction.set("account:1:balance", "1000");
transaction.decrBy("account:2:balance", 100);
List<Object> results = transaction.exec();  // выполняет все команды

Особенности:

  • MULTI начинает транзакцию
  • Все команды ставятся в очередь
  • EXEC выполняет их атомарно
  • Нет rollback — если ошибка, часть команд может выполниться

Проблема изоляции:

// Thread 1
jedis.multi();
jedis.get("counter");      // читает 5
jedis.incr("counter");     // будет 6
jedis.exec();

// Thread 2 может изменить counter между get и incr!
// Redis read-committed isolation level

3. Column Family Store (Cassandra)

Cassandra не поддерживает транзакции между строками, но обеспечивает линеаризуемость для одной строки:

Session session = cluster.connect("store");

// Каждая строка может быть обновлена атомарно
BoundStatement updateOrder = session.prepare(
    "UPDATE orders SET status = ?, amount = ? WHERE id = ?"
).bind("paid", 100, orderId);

ResultSet results = session.execute(updateOrder);
// Гарантия: либо обновится вся строка, либо ничего

// НО: две разные строки НЕ имеют гарантий
BoundStatement stmt1 = session.prepare(
    "UPDATE orders SET amount = ? WHERE id = ?"
).bind(150, orderId);
BoundStatement stmt2 = session.prepare(
    "UPDATE accounts SET balance = ? WHERE id = ?"
).bind(850, accountId);

session.execute(stmt1);  // Может выполниться
session.execute(stmt2);  // А это может не выполниться (сбой сети)

4. Graph Database (Neo4j)

Neo4j наиболее близка к SQL транзакциям:

Driver driver = GraphDatabase.driver("bolt://localhost:7687");
Session session = driver.session();

// ACID транзакция
Result result = session.writeTransaction(tx -> {
    // Все операции внутри выполняются атомарно
    Result r1 = tx.run(
        "CREATE (p:Person {name: $name}) RETURN p",
        parameters("name", "Alice")
    );
    Result r2 = tx.run(
        "MATCH (a:Person {name: Alice}), (b:Person {name: Bob}) " +
        "CREATE (a)-[:KNOWS]->(b)"
    );
    return r2;
});

// Либо все создается, либо ничего (откат при ошибке)

Distributed Transactions в NoSQL

Для транзакций между разными сервисами/узлами используют паттерны:

1. Saga Pattern

Разбивает транзакцию на локальные операции с компенсацией:

public class OrderSaga {
    // Прямой путь
    public void executeOrder(Order order) {
        orderService.createOrder(order);      // шаг 1
        inventoryService.reserveItems(order); // шаг 2
        paymentService.processPayment(order); // шаг 3
    }
    
    // Если что-то не так, откатываемся
    public void compensateOrder(Order order) {
        paymentService.refund(order);         // откат 3
        inventoryService.releaseItems(order); // откат 2
        orderService.cancelOrder(order);      // откат 1
    }
}

Преимущества: работает в распределённых системах. Недостатки: нет истинной ACID, нужно обрабатывать ошибки откомпенсации.

2. Event Sourcing

Вместо сохранения состояния сохраняют события:

public class Order {
    private String orderId;
    private List<Event> events = new ArrayList<>();
    
    public void createOrder(String orderId) {
        events.add(new OrderCreated(orderId));  // событие
    }
    
    public void addItem(String itemId) {
        events.add(new ItemAdded(orderId, itemId));
    }
    
    public void paymentProcessed() {
        events.add(new PaymentProcessed(orderId));
    }
    
    // Состояние восстанавливается из событий
    public String getStatus() {
        return events.stream()
            .filter(e -> e instanceof PaymentProcessed)
            .findAny()
            .map(e -> "paid")
            .orElse("pending");
    }
}

3. Two-Phase Commit (редко в NoSQL)

Некоторые NoSQL (например, Google Spanner) реализуют 2PC:

Фаза 1: Prepare (подготовка)
- Координатор спрашивает все узлы: готовы ли вы?
- Узлы запираают ресурсы, но не коммитят

Фаза 2: Commit (применение)
- Если все согласились: COMMIT
- Если хоть один отказал: ROLLBACK

Но это редко, так как замораживает систему при сбое координатора.

Практические примеры: обработка отсутствия транзакций

Проблема: двойная траты

// Без транзакций
long balance = getBalance(accountId);  // 100
if (balance >= 50) {
    setBalance(accountId, balance - 50);  // записывает 50
}
// Если две операции одновременно, может пройти дважды!

Решение: использовать atomic операции:

// Cassandra CAS (Compare-And-Set)
BoundStatement stmt = session.prepare(
    "UPDATE accounts SET balance = balance - ? " +
    "WHERE id = ? IF balance >= ?"
).bind(50, accountId, 50);

ResultSet rs = session.execute(stmt);
if (rs.wasApplied()) {
    // Успешно списано
} else {
    // Недостаточно средств
}

Проблема: стейл дата

// MongoDB с eventual consistency
order = orders.findOne({_id: orderId});
if (order.status == "pending") {
    orders.updateOne({_id: orderId}, {$set: {status: "shipped"}});
}
// Но order может быть устаревшим!

Решение: использовать versioning:

BasicDBObject query = new BasicDBObject()
    .append("_id", orderId)
    .append("version", order.getVersion());

BasicDBObject update = new BasicDBObject("$set",
    new BasicDBObject()
        .append("status", "shipped")
        .append("version", order.getVersion() + 1)
);

UpdateResult result = orders.updateOne(query, update);
if (result.getModifiedCount() == 0) {
    // Версия не совпадает, order был обновлен
    throw new OptimisticLockException();
}

Сравнение подходов

БазаУровень ACIDМасштабируемостьСложность
MongoDBMulti-doc (новая)ХорошаяСредняя
RedisSingle-keyОтличнаяНизкая
CassandraSingle-rowОтличнаяВысокая
Neo4jFull ACIDХорошаяСредняя
DynamoDBSingle-itemОтличнаяСредняя

Лучшие практики

  1. Дизайн данных: размещайте связанные данные в одном документе/строке
  2. Idempotency: все операции должны быть идемпотентны (повторяемы)
  3. Версионирование: добавляйте версии для optimistic locking
  4. Event log: логируйте все события для аудита
  5. Тестирование: тестируйте отказы сети и race conditions

Заключение: NoSQL требует другого подхода к транзакциям. Вместо полагания на ACID, нужно проектировать систему так, чтобы она была устойчива к сбоям и некорректностям. Это требует более глубокого понимания распределенных систем, но дает взамен масштабируемость.

Как выглядит транзакционность в NoSQL | PrepBro