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

Что такое надежность в ACID?

1.0 Junior🔥 241 комментариев
#Другое

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

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

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

ACID: буква D — Durability (Долговечность/Надёжность)

Durability (Долговечность) — это третья буква в аббревиатуре ACID, которая гарантирует, что успешно завершённая транзакция сохраняется навсегда, даже в случае сбоя системы (потеря питания, крах диска, отключение сервера).

Что такое ACID?

ACID — набор свойств транзакций БД:

A - Atomicity    (Атомарность)
    Либо всё, либо ничего

C - Consistency  (Консистентность)
    Данные в допустимом состоянии

I - Isolation    (Изоляция)
    Транзакции не мешают друг другу

D - Durability   (Долговечность)
    Сохранённые данные не потеряются

Понимание Durability

# Пример: перевод денег между счётами

BEGIN TRANSACTION
    SELECT balance FROM accounts WHERE id = 1
    # Balance = 1000
    
    UPDATE accounts SET balance = balance - 100 WHERE id = 1
    # Account 1: 900
    
    UPDATE accounts SET balance = balance + 100 WHERE id = 2
    # Account 2: 1100
    
COMMIT  # Транзакция завершена

После COMMIT:

❌ БЕЗ Durability:
- COMMIT выполнен
- Данные в памяти
- Сервер падает
- Данные потеряны

✓ С Durability:
- COMMIT выполнен
- Данные записаны на диск
- Данные в WAL (Write-Ahead Log)
- Сервер падает
- При перезагрузке данные восстановлены
- Деньги на месте: счёт 1 = 900, счёт 2 = 1100

Как реализуется Durability?

1. Write-Ahead Log (WAL)

Процесс:

1. Транзакция COMMIT
2. БД записывает лог на диск
   "Transfer: 100 from account 1 to account 2"
3. Затем обновляет данные в памяти
4. В фоне сбрасывает на диск

Если крах на шаге 3:
- Лог на диске → можно восстановить
- Данные в памяти потеряны → восстановим из лога

Если крах на шаге 4:
- И лог и данные на диске → ничего не потеряется

2. fsync() — запись на диск

# На уровне ОС
import os

# Обычное открытие файла
f = open('data.txt', 'w')
f.write('important data')  # ← В буфере, не на диске!

# Может потеряться при крахе!

# Правильно: flush + fsync
f.write('important data')
f.flush()           # Выводим из буфера приложения
os.fsync(f.fileno())  # Выводим из буфера ОС на диск

# Теперь данные ДЕЙСТВИТЕЛЬНО на диске

3. Уровни Durability

# PostgreSQL synchronous_commit

# synchronous_commit = off (НЕБЕЗОПАСНО)
COMMIT выполняется сразу
Данные только в памяти
У меньше задержек
Риск потери данных высокий

# synchronous_commit = local (БЕЗОПАСНЕЕ)
Данные записаны в WAL на диск
Копии на реплику НЕ требуются
Сбалансированный вариант

# synchronous_commit = on (БЕЗОПАСНО)
Данные записаны на диск
Данные отправлены на реплику
Медленнее, но очень надёжно

# synchronous_commit = remote_apply (МАКСИМАЛЬНО БЕЗОПАСНО)
Данные применены на реплике
Евро-стандарт для финтеха
Очень медленно, но абсолютная надёжность

Практический пример: Обработка платежей

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Account(Base):
    __tablename__ = "accounts"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    balance = Column(Integer)

class PaymentLog(Base):
    __tablename__ = "payment_log"
    id = Column(Integer, primary_key=True)
    from_account = Column(Integer)
    to_account = Column(Integer)
    amount = Column(Integer)
    status = Column(String)  # 'pending', 'completed', 'failed'

engine = create_engine(
    'postgresql://user:pass@localhost/bank_db',
    # Максимальная безопасность для банка
    connect_args={'connect_timeout': 10}
)

Session = sessionmaker(bind=engine)

def transfer_money(from_id: int, to_id: int, amount: int) -> bool:
    """
    Безопасный перевод денег с ACID гарантиями.
    Durability обеспечивает, что если COMMIT прошёл —
    данные не потеряются даже при крахе.
    """
    session = Session()
    
    try:
        # Логируем начало платежа
        log_entry = PaymentLog(
            from_account=from_id,
            to_account=to_id,
            amount=amount,
            status='pending'
        )
        session.add(log_entry)
        session.flush()  # Убедиться что лог в БД
        
        # Проверяем баланс
        from_account = session.query(Account).filter(
            Account.id == from_id
        ).with_for_update().first()  # Lock
        
        if not from_account or from_account.balance < amount:
            session.rollback()
            return False
        
        # Выполняем транзакцию
        from_account.balance -= amount
        
        to_account = session.query(Account).filter(
            Account.id == to_id
        ).with_for_update().first()
        to_account.balance += amount
        
        # Обновляем статус лога
        log_entry.status = 'completed'
        
        # ✓ COMMIT — после этого Durability гарантирует сохранение
        session.commit()
        
        # Если здесь крах:
        # - БД восстановится из WAL
        # - Деньги будут переведены
        # - Лог будет обновлён
        
        return True
        
    except Exception as e:
        session.rollback()  # Откатываем всё
        log_entry.status = 'failed'
        session.commit()
        return False
    finally:
        session.close()

# Использование
if transfer_money(from_id=1, to_id=2, amount=100):
    print("✓ Платёж выполнен и сохранён")
else:
    print("✗ Платёж отклонён")

Сравнение БД по Durability

# PostgreSQL (ОЧЕНЬ НАДЁЖНАЯ)
# - WAL (Write-Ahead Log) по умолчанию
# - fsync обеспечивает запись на диск
# - Поддерживает асинхронную репликацию
engine = create_engine('postgresql://...')

# MySQL/InnoDB (НАДЁЖНАЯ)
# - InnoDB имеет свой log
# - Можно настроить innodb_flush_log_at_trx_commit
engine = create_engine('mysql+pymysql://...')

# SQLite (МЕНЬШЕ НАДЁЖНА)
# - По умолчанию не гарантирует запись на диск
# - Можно включить synchronous mode
engine = create_engine('sqlite:///data.db')

# Redis (СЛАБАЯ DURABILITY)
# - В памяти, может потерять данные
# - AOF (Append-Only File) помогает
# - Но не гарантирует полную Durability
r = redis.Redis()

# MongoDB (ЗАВИСИТ ОТ КОНФИГУРАЦИИ)
# - journaling обеспечивает Durability
# - write_concern влияет на уровень
from pymongo import MongoClient
client = MongoClient()
db = client.mydb

Практический пример: Критическая операция

import logging
from contextlib import contextmanager

logger = logging.getLogger(__name__)

@contextmanager
def safe_transaction(session):
    """
    Context manager с гарантией Durability.
    """
    try:
        yield session
        session.commit()
        logger.info("✓ Транзакция успешно завершена (Durability гарантирована)")
    except Exception as e:
        session.rollback()
        logger.error(f"✗ Транзакция отменена: {e}")
        raise
    finally:
        session.close()

# Использование
with safe_transaction(session) as s:
    # Все операции здесь атомарны
    user = s.query(User).filter(User.id == 1).first()
    user.email = "new@example.com"
    # При commit() данные гарантированно сохранятся

Проблемы без Durability

# ❌ Сценарий 1: Крах после COMMIT

BEGIN TRANSACTION
UPDATE products SET stock = stock - 1 WHERE id = 1
COMMIT  # ← Данные в памяти
# КРАХ СЕРВЕРА

# Последствия:
# - Товар не куплен (откатили? нет, тут нет откатывания)
# - Клиент заплатил, товара нет
# - Деньги потеряны

# ✓ С Durability:
# - Данные в WAL на диске
# - После перезагрузки система восстановится
# - Товар остаётся куплен
# - Все согласовано

# ❌ Сценарий 2: Потеря БД
sql_dump = "backup.sql"
# Создаём снимок БД
dump_db_to_file(sql_dump)
# Потом не восстанавливаем, а продолжаем работу
# БД сбивается, снимок устарел
# Потеряны все новые данные

Как проверить Durability в приложении

import subprocess
import time
from sqlalchemy.orm import Session

def test_durability():
    """
    Тест: убедиться что данные сохранились при крахе.
    """
    session = Session()
    
    # Шаг 1: Запишем данные и COMMIT
    new_item = Item(name="test_durability")
    session.add(new_item)
    session.commit()  # ← Durability гарантирует запись
    item_id = new_item.id
    
    # Шаг 2: Симулируем крах (в реальности это может быть killall -9 postgres)
    # В тесте мы просто закроем соединение
    session.close()
    
    # Шаг 3: Переподключаемся
    new_session = Session()
    
    # Шаг 4: Проверяем что данные остались
    item = new_session.query(Item).filter(Item.id == item_id).first()
    assert item is not None, "Данные потеряны! Durability не гарантирована"
    assert item.name == "test_durability"
    print("✓ Durability тест пройден")

Тонкости Durability

# Проблема 1: Асинхронная запись
# Даже если на диск записалось, может быть в cache диска

f.write(data)
f.flush()  # Вывести из буфера приложения
os.fsync(f.fileno())  # Вывести из буфера ОС (ВАЖНО!)

# Проблема 2: Репликация
# Если используешь репликацию, нужно ждать репики
session.commit()  # На primary OK
# Но кто гарантирует что реплика получила?
# Ответ: synchronous_commit = remote_write

# Проблема 3: Сетевые сбои
# Между COMMIT на клиенте и БД
try:
    session.commit()  # Может упасть
except ConnectionError:
    # Но транзакция может быть уже выполнена на сервере!
    # Durability не гарантирует что клиент узнает
    pass

Рекомендации

# 1. Для финтеха: максимальная Durability
engine = create_engine(
    'postgresql://...',
    execution_options={
        'synchronous_commit': 'remote_apply'
    }
)

# 2. Для веб-приложения: баланс скорости и надёжности
engine = create_engine(
    'postgresql://...',
    execution_options={
        'synchronous_commit': 'local'  # Хороший компромисс
    }
)

# 3. Для кэша: меньше Durability
redis_client = redis.Redis(
    decode_responses=True,
    write_concern={'w': 0}  # Быстро, но может потеряться
)

# 4. Всегда делай резервные копии
# Durability гарантирует не потерять при сбое
# Но не гарантирует от ошибок приложения

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

  • Durability = успешная транзакция сохраняется навсегда
  • WAL (Write-Ahead Log) основной механизм реализации
  • fsync() обеспечивает запись на физический диск
  • Разные БД имеют разные уровни Durability
  • Асинхронная репликация может потерять последние данные
  • Синхронная репликация добавляет задержку
  • Без Durability потеря денег в финтехе или потеря данных
  • Проверяй конфигурацию своей БД для критичных приложений