← Назад к вопросам
Какая интересная история с благополучным концом была на рабочем проекте?
1.3 Junior🔥 201 комментариев
#Soft Skills и карьера
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
История проблемы с базой данных и благополучный конец
Предистория
Работал я на достаточно крупном проекте — платформа для управления подписками. Пиковая нагрузка: 100K пользователей, 10K одновременных запросов к БД. Всё было хорошо, пока не наступил день, который я никогда не забуду.
Проблема: The Slowdown
Вторник, 11:30 утра. На Slack получаю сообщение от support'а: "Платформа работает как черепаха, пользователи жалуются".
Я запускаю мониторинг (Prometheus + Grafana):
- CPU: нормально
- Память: нормально
- Дисковое пространство: нормально
- Время ответа API: 8-10 секунд вместо обычных 100-200ms
// Мои первые шаги — логирование SQL запросов
@Configuration
public class LoggingConfiguration {
@Bean
public DataSource loggingDataSource(DataSource dataSource) {
// Включил логирование всех SQL запросов
// (На production это плохо для performance, но для диагностики критично)
return LoggingDataSourceProxy.wrap(dataSource);
}
}
// Результат: обнаружил запросы на 30+ секунд!
SELECT * FROM orders
WHERE user_id = ? AND status IN ('active', 'pending')
JOIN subscriptions ON ...
JOIN users ON ...
JOIN payments ON ...
JOIN invoices ON ...
// Без индексов на WHERE условиях!
Диагностика: N+1 Query Problem
// Вот что происходило в коде:
@Entity
public class User {
@OneToMany
private List<Order> orders; // Без @EntityGraph!
}
@Service
public class OrderService {
@Autowired
private UserRepository userRepository;
public List<OrderResponse> getUserOrders(Long userId) {
// 1 запрос: SELECT * FROM users WHERE id = ?
User user = userRepository.findById(userId).orElseThrow();
// На каждый order — ещё запрос!
for (Order order : user.getOrders()) { // N запросов!
// SELECT * FROM subscriptions WHERE order_id = ?
// SELECT * FROM payments WHERE order_id = ?
processOrder(order);
}
}
}
Первое решение (неправильное)
В панике я думал: "Может быть, это deadlock'и в БД?"
// Попробовал просто кэшировать результаты
@Cacheable(value = "orders", key = "#userId")
public List<OrderResponse> getUserOrders(Long userId) {
return fetchOrders(userId);
}
// Результат: работало быстрее, но маскировало реальную проблему!
// Через час кэш переполнился и приложение упало с OutOfMemoryError
Анализ: Использование JPA Query плана
// Включил логирование планов выполнения
@Configuration
public class JpaConfiguration {
@Bean
public PersistenceUnitPostProcessor persistenceUnitPostProcessor() {
return persistenceUnitInfo -> {
Map<String, Object> properties = persistenceUnitInfo.getProperties();
// Логируем SQL explain
properties.put("hibernate.generate_statistics", true);
};
}
}
// Статистика показала:
// - 1 базовый запрос: 10ms
// - 50 запросов на загрузку related данных: 8s
// Итого: 50-200 запросов на один HTTP запрос!
Решение: Entity Graph + JOIN FETCH
// Это было моментом эврики!
// Определили Entity Graph
@NamedEntityGraph(
name = "User.withOrders",
attributeNodes = {
@NamedAttributeNode(value = "orders", subgraph = "orderSubgraph")
},
subgraphs = @NamedSubgraph(
name = "orderSubgraph",
attributeNodes = {
@NamedAttributeNode("subscriptions"),
@NamedAttributeNode("payments")
}
)
)
@Entity
public class User {
@OneToMany
private List<Order> orders;
}
// Использовали Entity Graph в repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(value = "User.withOrders", type = EntityGraphType.LOAD)
Optional<User> findById(Long id);
}
// ИЛИ через JPQL с JOIN FETCH (мой финальный выбор)
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT DISTINCT o
FROM Order o
LEFT JOIN FETCH o.subscriptions
LEFT JOIN FETCH o.payments
WHERE o.userId = :userId
AND o.status IN ('active', 'pending')
""")
List<Order> findActiveOrdersForUser(@Param("userId") Long userId);
}
Добавили индексы в БД
-- Миграция (Goose)
CREATE INDEX idx_orders_user_id_status
ON orders(user_id, status)
WHERE status IN ('active', 'pending');
CREATE INDEX idx_subscriptions_order_id
ON subscriptions(order_id);
CREATE INDEX idx_payments_order_id
ON payments(order_id);
// Добавили аннотации в модели для документирования
@Entity
@Table(name = "orders", indexes = {
@Index(name = "idx_orders_user_id_status",
columnList = "user_id,status")
})
public class Order {
@Id
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "status")
private String status;
}
Результат: Волшебство!
ДО исправления:
- Количество запросов: 150-200 на один HTTP запрос
- Время ответа: 8-10 секунд
- CPU: 95%+
- БД подключений: 150/200 (почти все заняты)
ПОСЛЕ исправления:
- Количество запросов: 2-3 на один HTTP запрос
- Время ответа: 80-120ms
- CPU: 15%
- БД подключений: 5-10 (спокойная жизнь)
Визуальное представление улучшения
Время ответа API (Grafana):
ДО: | ████████████████████ (8000ms)
ПОСЛЕ: | ██ (100ms)
|
0ms ────────────────→ 10000ms
Что я выучил из этой истории
// 1. ВСЕГДА профилируй прежде, чем оптимизировать
public void diagnosis() {
// Используй:
// - Spring Boot Actuator metrics
// - Prometheus + Grafana
// - New Relic
// - Database query logging
}
// 2. Проблемы N+1 обнаруживаются на testing с реальными данными
@Test
public void testWithManyOrders() {
// Создать 1000 заказов
// Загрузить всех пользователей
// Проверить количество SQL запросов
int queryCount = queryCountListener.getQueryCount();
assertTrue(queryCount < 5, "N+1 problem detected!");
}
// 3. Индексы критичны
// @Index на @Entity
// + мониторинг unused индексов
// + EXPLAIN ANALYZE для slow queries
// 4. Entity Graph vs JOIN FETCH vs Query Hints
// - Entity Graph: декларативный, переиспользуемый
// - JOIN FETCH: гибкий, но нужен в каждом запросе
// - Lazy loading + Hibernate.initialize(): худший вариант
// 5. Кэширование — последняя оптимизация
// Не кэшируй плохо спроектированный запрос!
Как это предотвратить в новых проектах
public class BestPractices {
// 1. Используй @EntityGraph из коробки
public Optional<User> findUserWithEverything(Long userId) {
// ...
}
// 2. Тестируй N+1 на Unit тестах
@Test
void testNoNPlusOneQueries() {
// Использовать QueryCountListener
}
// 3. Мониторь query count в production
@Transactional
public void monitorQueries() {
// Логируй количество запросов
//Alert если > N (зависит от endpoint)
}
// 4. Code review для всех запросов к БД
// - Есть ли JOIN FETCH или @EntityGraph?
// - Есть ли индексы?
// - Есть ли WHERE условия?
}
Благополучный конец
После исправления:
- SLA улучшился с 95% до 99.9%
- Стоимость облака упала на 60% (меньше серверов)
- Дерегулировал alert'ы, которые звонили каждый час
- Команда была счастлива (включая меня после 36 часов без сна)
- На постмортеме решили:
- Всегда писать тесты на количество SQL запросов
- Требовать Entity Graph для всех fetch операций
- Проводить регулярные аудиты медленных запросов
Мораль истории
public class Lesson {
// 1. Измеряй перед тем, как оптимизировать
// 2. N+1 проблема — частая ошибка Hibernate/JPA
// 3. JOIN FETCH и Entity Graph — твои друзья
// 4. Индексы спасают жизни
// 5. Профилирование и мониторинг критичны
// "От героической работы в последний момент
// переходи к систематическому тестированию перформанса"
}
Эта история преподала мне одноиз самых ценных уроков в карьере: хорошее проектирование и архитектура намного дешевле, чем ночные вызовы из-за production падений!