juniordev

Заметки о бэкенде: Java, PostgreSQL, очереди и интеграции. Записываю то, что пригодилось в работе, чтобы не искать решение второй раз.

Частичный индекс вместо обычного

Таблица заказов на десятки миллионов строк, а в работе — несколько тысяч со статусом NEW. Обычный индекс по (status, created_at) хранит всё подряд и растёт вместе с архивом. Частичный индекс хранит только нужные строки и помещается в память целиком.

CREATE INDEX CONCURRENTLY orders_new_created_idx
    ON orders (created_at)
    WHERE status = 'NEW';

Подвох один: условие запроса должно совпадать с условием индекса. Если статус приходит параметром, планировщик в generic-плане может не доказать, что индекс подходит, — смотрите EXPLAIN на реальном запросе из приложения.

postgresql

Повторы в RabbitMQ без бесконечного цикла

Сообщение не обработалось — отправляем его в очередь ожидания с TTL. Когда TTL истекает, dead-letter exchange возвращает его в рабочую очередь. Число попыток видно в заголовке x-death, после пятой сообщение уходит в отдельную очередь на разбор.

Map<String, Object> args = Map.of(
    "x-message-ttl", 30_000,
    "x-dead-letter-exchange", "orders",
    "x-dead-letter-routing-key", "orders.process");
channel.queueDeclare("orders.retry", true, false, false, args);

Так повторы не забивают рабочую очередь и не крутятся вечно, а сломанные сообщения не теряются.

rabbitmqинтеграции

Graceful shutdown в Spring Boot

При выкатке под Kubernetes часть запросов падала с 502. Spring Boot умеет дожидаться активных запросов, но Kubernetes убирает под из балансировки не мгновенно — сначала нужно немного подождать, и только потом останавливаться.

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Плюс preStop-хук со sleep на 5–10 секунд в манифесте: за это время под исчезает из эндпоинтов, и новые запросы на него уже не приходят.

springjava

Идемпотентность входящих сообщений

Партнёр может прислать одно и то же сообщение дважды — после таймаута или своего перезапуска. Храним идентификаторы обработанных сообщений и вставляем новый в той же транзакции, что и бизнес-изменение.

INSERT INTO processed_message (message_id, received_at)
VALUES (:messageId, now())
ON CONFLICT (message_id) DO NOTHING;

Вставилась строка — обрабатываем. Не вставилась — это повтор, отвечаем тем же результатом и ничего не делаем.

интеграцииpostgresql

Виртуальные потоки: где помогли, а где нет

Сервис ходит к десятку внешних систем и в основном ждёт ответа. Перевод этих вызовов на виртуальные потоки убрал возню с размером пула: каждому запросу свой поток, и памяти это почти не стоит.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var answers = partners.stream()
        .map(p -> executor.submit(() -> client.fetch(p)))
        .toList();
    // ...
}

На расчётных задачах выигрыша нет: процессор от этого не прибавляется. И в Java 21 блок synchronized пришпиливает виртуальный поток к несущему — в JDK 24 это исправили (JEP 491).

java