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

Чем консистентность поддерживается в БД?

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

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

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

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

Консистентность данных в базе данных

Консистентность — это один из ключевых столпов ACID свойств транзакций. Я работаю с несколькими механизмами её обеспечения в своих проектах.

ACID свойства транзакций

Консистентность (Consistency) — база данных переходит из одного согласованного состояния в другое:

// Пример: перевод денег между счётами
public class BankTransferService {
    
    @Transactional
    public void transferMoney(String fromAccountId, String toAccountId, BigDecimal amount) {
        // До транзакции: оба счёта в согласованном состоянии
        
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow();
        
        // Проверка бизнес-правил (консистентность)
        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException("Недостаточно средств");
        }
        
        // Изменяем состояние
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
        
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
        
        // После транзакции: оба счёта снова в согласованном состоянии
        // (сумма денег не создалась и не исчезла)
    }
}

Механизмы обеспечения консистентности

1. Constraints (Ограничения)

-- PRIMARY KEY — уникальность
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    username VARCHAR(100) NOT NULL
);

-- FOREIGN KEY — реферанциальная целостность
CREATE TABLE orders (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- CHECK — проверка значений
CREATE TABLE products (
    id UUID PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10, 2),
    CHECK (price > 0)
);

-- NOT NULL — обязательные поля
CREATE TABLE accounts (
    id UUID PRIMARY KEY,
    balance DECIMAL(18, 2) NOT NULL DEFAULT 0,
    created_at TIMESTAMP NOT NULL
);

2. Транзакции (ACID)

@Service
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;
    private final PaymentService paymentService;
    
    @Transactional
    public Order placeOrder(OrderRequest request) {
        // 1. Резервируем товар
        for (OrderItem item : request.getItems()) {
            Inventory inventory = inventoryRepository.findById(item.getProductId())
                .orElseThrow(() -> new ProductNotFoundException());
            
            if (inventory.getQuantity() < item.getQuantity()) {
                throw new InsufficientInventoryException("No stock");
            }
            
            inventory.setQuantity(inventory.getQuantity() - item.getQuantity());
            inventoryRepository.save(inventory);
        }
        
        // 2. Обрабатываем платёж
        Payment payment = paymentService.processPayment(request.getPaymentDetails());
        
        if (!payment.isSuccessful()) {
            // Откат всех изменений автоматически
            throw new PaymentFailedException("Payment declined");
        }
        
        // 3. Создаём заказ
        Order order = new Order();
        order.setUser(request.getUser());
        order.setItems(request.getItems());
        order.setPaymentId(payment.getId());
        order.setStatus(OrderStatus.CONFIRMED);
        order.setCreatedAt(LocalDateTime.now(UTC));
        
        return orderRepository.save(order);
        
        // Если всё успешно — коммитим. Если ошибка — откатываем всё
    }
}

3. Уровни изоляции транзакций

// READ UNCOMMITTED — грязные чтения
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample() {
    // Опасно! Может прочитать незафиксированные данные
}

// READ COMMITTED — только зафиксированные данные (по умолчанию)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() {
    // Стандартный уровень — хороший баланс
}

// REPEATABLE READ — повторяемое чтение
@Transactional(isolation = Isolation.REPEATABLE_READ)
public BigDecimal getAccountBalance(String accountId) {
    Account account = accountRepository.findById(accountId).orElseThrow();
    // Блокировка на весь период транзакции — может быть медленно
    return account.getBalance();
}

// SERIALIZABLE — полная изоляция
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample() {
    // Как будто транзакции выполняются последовательно
    // Но очень медленно!
}

4. Locks (Блокировки)

@Service
public class TicketBookingService {
    
    private final SeatRepository seatRepository;
    
    @Transactional
    public Ticket bookSeat(String seatId, String userId) {
        // SELECT FOR UPDATE — блокирует строку до конца транзакции
        Seat seat = seatRepository.findByIdWithLock(seatId)
            .orElseThrow(() -> new SeatNotFoundException());
        
        if (seat.isBooked()) {
            throw new SeatAlreadyBookedException();
        }
        
        // Другие потоки будут ждать этой блокировки
        seat.setBooked(true);
        seat.setUserId(userId);
        seat.setBookedAt(LocalDateTime.now(UTC));
        
        seatRepository.save(seat);
        
        return new Ticket(seatId, userId);
    }
}

// SQL запрос с блокировкой
@Repository
public interface SeatRepository extends JpaRepository<Seat, String> {
    
    @Query("SELECT s FROM Seat s WHERE s.id = :id FOR UPDATE")
    Optional<Seat> findByIdWithLock(@Param("id") String id);
}

5. Уникальные индексы

-- Уникальный индекс на email
CREATE UNIQUE INDEX idx_users_email ON users(email);

-- Комбинированный уникальный индекс
CREATE UNIQUE INDEX idx_order_items ON order_items(order_id, product_id);

6. Triggers и хранимые процедуры

-- Trigger для обновления updated_at
CREATE TRIGGER update_users_timestamp
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
    NEW.updated_at = NOW();
END;

-- Trigger для каскадного обновления
CREATE TRIGGER update_order_total
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
    UPDATE orders 
    SET total = (SELECT SUM(price * quantity) FROM order_items WHERE order_id = NEW.order_id)
    WHERE id = NEW.order_id;
END;

Проблемы консистентности и их решения

Race Condition при конкурентном доступе

// ❌ Проблема: race condition
public void incrementCounter(String counterId) {
    Counter counter = counterRepository.findById(counterId).orElseThrow();
    counter.setValue(counter.getValue() + 1);
    counterRepository.save(counter);
    // Если два потока одновременно читают значение 5,
    // оба запишут 6, хотя должно быть 7
}

// ✅ Решение 1: SELECT FOR UPDATE
@Transactional
public void incrementCounterWithLock(String counterId) {
    Counter counter = counterRepository.findByIdWithLock(counterId).orElseThrow();
    counter.setValue(counter.getValue() + 1);
    counterRepository.save(counter);
}

// ✅ Решение 2: Atomic операция в БД
@Transactional
public void incrementCounterWithSQL(String counterId) {
    counterRepository.incrementByOne(counterId);
}

@Repository
public interface CounterRepository extends JpaRepository<Counter, String> {
    @Modifying
    @Query("UPDATE Counter c SET c.value = c.value + 1 WHERE c.id = :id")
    void incrementByOne(@Param("id") String id);
}

Фантомное чтение в распределённых системах

// Проблема: между двумя SELECT запросами кто-то вставил данные
@Transactional(isolation = Isolation.REPEATABLE_READ)
public List<Order> findRecentOrders(String userId) {
    List<Order> orders = orderRepository.findByUserId(userId);
    // Между первым и вторым запросом вставили новый заказ
    
    // Если повторим запрос, получим другой результат
    List<Order> ordersAgain = orderRepository.findByUserId(userId);
    // orders != ordersAgain
}

// ✅ Решение: SERIALIZABLE изоляция
@Transactional(isolation = Isolation.SERIALIZABLE)
public List<Order> findRecentOrdersSerial(String userId) {
    List<Order> orders = orderRepository.findByUserId(userId);
    // Гарантировано — никто не сможет вставить данные
    List<Order> ordersAgain = orderRepository.findByUserId(userId);
    // orders == ordersAgain
}

Eventual Consistency в микросервисах

// В распределённых системах иногда нужно жертвовать немедленной консистентностью
@Service
public class EventDrivenConsistency {
    
    private final OrderRepository orderRepository;
    private final EventBus eventBus;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setStatus(OrderStatus.PENDING);
        Order savedOrder = orderRepository.save(order);
        
        // Публикуем событие
        eventBus.publish(new OrderCreatedEvent(savedOrder.getId()));
        
        return savedOrder;
    }
}

@Service
public class InventoryService {
    
    private final InventoryRepository inventoryRepository;
    
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Обновляем инвентарь асинхронно
        // Временно будет inconsistency, но со временем система придёт в консистентное состояние
        inventoryRepository.decrementStock(event.getOrderId());
    }
}

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

  1. Используйте @Transactional для критичных операций
  2. Выбирайте правильный уровень изоляции — не все требуют SERIALIZABLE
  3. Добавляйте constraints в БД — это первая линия защиты
  4. Используйте SELECT FOR UPDATE для конкурентных операций
  5. Логируйте и мониторьте нарушения консистентности
  6. Тестируйте race conditions с помощью concurrent тестов
  7. В микросервисах — используйте Saga pattern для распределённых транзакций

Заключение

Консистентность данных — это результат комплексного подхода: constraints в БД, ACID транзакции, правильные уровни изоляции и locks. Игнорирование этих механизмов приводит к коррупции данных и потере денег.