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

Почему выбрал бы Repeatable Read для таблицы пользователей, где каждый пользователь может изменять только свою строку?

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

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

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

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

Repeatable Read изоляция для таблицы пользователей

Краткий ответ

Repeatable Read — это уровень изоляции транзакций, который гарантирует, что если транзакция дважды прочитает одну и ту же строку, получит одинаковый результат. Для таблицы пользователей, где каждый может менять только свои данные, это оптимальный выбор, так как:

  1. Предотвращает non-repeatable read (грязные чтения)
  2. Снижает количество конфликтов по сравнению с SERIALIZABLE
  3. Не требует дорогостоящих блокировок для чтения
  4. Достаточно для требования изоляции между пользователями

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

1. READ UNCOMMITTED (самый низкий)

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- Проблема: Dirty Read
-- Транзакция A видит незафиксированные изменения от транзакции B

Транзакция A:              Транзакция B:
BEGIN;                     BEGIN;
                           UPDATE users SET balance = 100
                           WHERE id = 1;
-- A видит balance = 100, хотя B не коммитнула!
SELECT balance FROM users
WHERE id = 1;              (откат изменений)
COMMIT;

Используется: Практически никогда (очень рискованно)

2. READ COMMITTED (по умолчанию в PostgreSQL)

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Проблема: Non-repeatable Read
-- Одна транзакция видит разные значения при повторном чтении

Транзакция A:              Транзакция B:
BEGIN;                     BEGIN;
SELECT balance FROM users  
WHERE id = 1;              -- balance = 100
                           UPDATE users SET balance = 150
                           WHERE id = 1;
                           COMMIT;
-- A при повторном чтении видит новое значение!
SELECT balance FROM users
WHERE id = 1;              -- balance = 150 (изменилось!)
COMMIT;

Используется: Большинство приложений, когда изолированность не критична

3. REPEATABLE READ (рекомендуемый для вашего случая)

SET SESSION ISOLATION LEVEL REPEATABLE READ;

-- Гарантия: все чтения в одной транзакции видят одно и то же
-- Проблема: Phantom Read может быть

Транзакция A:              Транзакция B:
BEGIN;                     BEGIN;
SELECT balance FROM users  
WHERE id = 1;              -- balance = 100
                           UPDATE users SET balance = 150
                           WHERE id = 1;
                           COMMIT;
-- A видит ПЕРВОНАЧАЛЬНОЕ значение (100)
SELECT balance FROM users
WHERE id = 1;              -- balance = 100 (не изменилось!)
COMMIT;

Используется: Когда нужна консистентность в одной транзакции

4. SERIALIZABLE (самый строгий)

SET SESSION ISOLATION LEVEL SERIALIZABLE;

-- Полная изоляция - как если бы транзакции выполнялись по очереди
-- Безопасно, но МЕДЛЕННО и много конфликтов

Транзакция A:              Транзакция B:
BEGIN;                     BEGIN;
SELECT balance FROM users  
WHERE id = 1;              -- Ждёт блокировки от A!
                           ...
                           (конфликт сериализации)
UPDATE users SET balance = 200
WHERE id = 1;
COMMIT;                    -- Теперь B может выполниться

Используется: Критичные финансовые операции, историчные данные

Почему REPEATABLE READ оптимален для вашего случая

Условие: Каждый пользователь может менять ТОЛЬКО СВОЮ строку

Транзакция пользователя A:    Транзакция пользователя B:
BEGIN;                        BEGIN;
REPEATABLE READ;              REPEATABLE READ;

-- A читает свои данные
SELECT * FROM users
WHERE id = 1;                 -- B читает свои данные
                              SELECT * FROM users
                              WHERE id = 2;

-- A обновляет свою строку
UPDATE users SET balance = 100
WHERE id = 1;                 -- B обновляет свою строку
                              UPDATE users SET email = 'new@example.com'
                              WHERE id = 2;

-- A видит свою версию данных
SELECT * FROM users
WHERE id = 1;                 -- B видит свою версию данных
                              SELECT * FROM users
                              WHERE id = 2;

COMMIT;                       COMMIT;

✓ НЕТ КОНФЛИКТОВ - разные строки!
✓ REPEATABLE READ - каждый видит свои данные
✓ ИЗОЛИРОВАНО - не видят изменения друг друга

Реализация в Spring Data JPA

@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    private String name;
    private BigDecimal balance;
    private String email;
}

@Service
public class UserService {
    @Autowired
    private UserRepository repo;
    
    @Autowired
    private EntityManager em;
    
    // REPEATABLE READ для критичных операций
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void updateUserProfile(Long userId, UserUpdateRequest req) {
        // Все чтения в этой транзакции видят консистентные данные
        User user = repo.findById(userId)
            .orElseThrow(() -> new NotFoundException("User not found"));
        
        // Проверка: баланс не изменился с момента первого чтения
        BigDecimal initialBalance = user.getBalance();
        
        // Обновления
        user.setName(req.getName());
        user.setEmail(req.getEmail());
        // Баланс не трогаем - его может менять только сам пользователь
        
        // При втором чтении баланс будет тот же (благодаря REPEATABLE READ)
        BigDecimal secondBalance = user.getBalance();
        
        if (!initialBalance.equals(secondBalance)) {
            throw new ConcurrentModificationException("Balance was modified!");
        }
        
        repo.save(user);
    }
}

Настройка в PostgreSQL

// application.yml
spring:
  jpa:
    properties:
      hibernate:
        # Уровень изоляции транзакций
        jdbc:
          transaction_isolation: 4  # REPEATABLE_READ = 4

Или явно:

@Configuration
public class TransactionConfig {
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        JpaTransactionManager tm = new JpaTransactionManager(emf);
        tm.setDefaultIsolationLevel(
            Connection.TRANSACTION_REPEATABLE_READ // = 4
        );
        return tm;
    }
}

Сравнение уровней изоляции для вашего случая

УровеньDirty ReadNon-repeatable ReadPhantom ReadПроизводительностьДля вашего случая
READ UNCOMMITTED⚡⚡⚡❌ Опасно
READ COMMITTED⚡⚡⚠️ Может быть проблема
REPEATABLE READ✅ ИДЕАЛЬНО
SERIALIZABLE⚠️❌ Слишком строго

Проблемы с более низкими уровнями

// Пример проблемы с READ COMMITTED

// Пользователь 1 хочет перевести свой баланс
@Transactional(isolation = Isolation.READ_COMMITTED) // ❌
public void transferMoney(Long userId, BigDecimal amount) {
    User user = userRepository.findById(userId).get();
    BigDecimal balance = user.getBalance();  // balance = 100
    
    // В это время администратор пополняет баланс
    // UPDATE users SET balance = 200 WHERE id = 1;
    
    // Мы уменьшаем на 50
    user.setBalance(balance - amount);  // 100 - 50 = 50
    
    // Результат: финальный баланс 50 (должен быть 150!)
    // Администратор пополнил на +100, но наша операция переписала на 50
    userRepository.save(user);
}

// Решение: REPEATABLE READ
@Transactional(isolation = Isolation.REPEATABLE_READ) // ✅
public void transferMoney(Long userId, BigDecimal amount) {
    // Видим balance = 100
    User user = userRepository.findById(userId).get();
    BigDecimal initialBalance = user.getBalance();
    
    // Если администратор попытается обновить - конфликт сериализации
    // Или мы получим ошибку оптимистичной блокировки
    
    user.setBalance(initialBalance - amount);
    userRepository.save(user);
}

Практический пример: безопасное обновление профиля

@Service
public class UserProfileService {
    @Autowired
    private UserRepository repo;
    
    // REPEATABLE READ гарантирует, что изменения остаются в пределах транзакции
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void updateProfile(Long userId, ProfileUpdateRequest req) {
        User user = repo.findById(userId)
            .orElseThrow(() -> new AccessDeniedException("Not your user"));
        
        // Проверка: пользователь может менять только свои данные
        if (!user.getId().equals(userId)) {
            throw new AccessDeniedException("Cannot modify other user's profile");
        }
        
        // Снимок данных в начале транзакции
        String originalEmail = user.getEmail();
        BigDecimal originalBalance = user.getBalance();
        
        // Обновления
        user.setName(req.getName());
        user.setEmail(req.getEmail());
        user.setPhoneNumber(req.getPhoneNumber());
        
        // REPEATABLE READ гарантирует, что баланс не изменился
        if (!originalBalance.equals(user.getBalance())) {
            throw new ConcurrentModificationException(
                "Your balance was modified by another transaction"
            );
        }
        
        repo.save(user);
    }
}

Выводы

  1. REPEATABLE READ идеален для вашего случая, потому что:

    • Гарантирует консистентность данных в одной транзакции
    • Предотвращает non-repeatable reads
    • Не требует полной изоляции (как SERIALIZABLE)
    • Оптимальный баланс между безопасностью и производительностью
  2. READ COMMITTED недостаточно, потому что:

    • Может позволить одной транзакции видеть разные значения
    • Опасно для операций, требующих консистентности
  3. SERIALIZABLE слишком строгий, потому что:

    • Значительно медленнее
    • Много конфликтов и откатов
    • Не нужна полная сериализация для изолированных пользователей
  4. В PostgreSQL уровень изоляции можно установить:

    • На уровне приложения: @Transactional(isolation = Isolation.REPEATABLE_READ)
    • На уровне сессии: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
    • На уровне сервера: конфиг PostgreSQL
Почему выбрал бы Repeatable Read для таблицы пользователей, где каждый пользователь может изменять только свою строку? | PrepBro