- PVSM.RU - https://www.pvsm.ru -
Привет! 👋
В современной разработке мы привыкли решать проблемы производительности стандартным набором инструментов. "База не тянет? Поставь Redis!" — это стало почти рефлексом. Но всегда ли оправдано тащить в инфраструктуру лишний сервис, настраивать сетевые хопы и следить за инвалидацией, если ваша задача — это всего лишь быстрый доступ к небольшому справочнику?
В нашем Open Source проекте BMSTU-ITSTECH/SSO [1] мы столкнулись именно с таким кейсом. И решение оказалось элегантнее, чем просто "поднять Redis". Рассказываю, как мы сэкономили на инфраструктуре и получили мгновенный отклик, используя скрытую мощь PostgreSQL LISTEN/NOTIFY.
Мы пишем SSO (систему единого входа). В сердце системы есть таблица apps — реестр подключенных приложений.
Вводные данные такие:
Таблица маленькая: Вряд ли там будет больше 100 строк (сервисов в экосистеме не тысячи).
Читается постоянно: Почти каждый запрос на аутентификацию требует проверки: "А есть ли такое приложение? А какие у него права?".
Меняется редко: Новые сервисы добавляются не каждый день.
Лезть в БД диском на каждый чих — дорого.
Ставить Redis ради 100 строчек — это классическая стрельба из пушки по воробьям (плюс сетевые задержки, плюс сериализация).
Решение: Хранить данные прямо в памяти Go-приложения (map), а о любых изменениях узнавать мгновенно через нативный механизм подписок Postgres.
Postgres умеет работать как брокер сообщений. Механизм LISTEN/NOTIFY позволяет базе данных "крикнуть" подписчикам, что что-то произошло.
Вот SQL, который мы накатили в миграции. Он делает две вещи:
Определяет функцию, которая отправляет уведомление в канал update_cache.
Вешает триггер на любые изменения (INSERT, UPDATE, DELETE) в таблице apps.
-- 1. Функция-глашатай
CREATE OR REPLACE FUNCTION notify_table_update() RETURNS TRIGGER AS $$
BEGIN
-- Отправляем сигнал в канал 'update_cache'.
-- В качестве payload можно передать имя таблицы или ID записи.
PERFORM pg_notify('update_cache', TG_TABLE_NAME);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- 2. Триггер, который дергает функцию
CREATE TRIGGER secrets_changed
AFTER INSERT OR UPDATE OR DELETE ON apps
FOR EACH ROW EXECUTE FUNCTION notify_table_update();
Теперь, как только админ поменяет конфиг приложения в админке, Postgres сам пнёт наше Go-приложение.
На стороне Go мы реализуем простую структуру: RWMutex для потокобезопасного чтения и мапу с данными. Плюс горутина-слушатель.
Весь код можно найти в ветке dev_v2 нашего репозитория [2], но вот сама суть:
Обычная мапа, защищенная мьютексом. Чтение из неё — это наносекунды, быстрее любого Redis.
type AppCache struct {
mu *sync.RWMutex
data map[int32]models.AppRepos
db *sqlx.DB
}
// Потокобезопасное получение данных
func (c *AppCache) App(ctx context.Context, appId int32) (models.AppRepos, error) {
c.mu.RLock()
defer c.mu.RUnlock()
app, ok := c.data[appId]
if !ok {
return models.AppRepos{}, storage.ErrAppNotFound
}
return app, nil
}
Используем библиотеку lib/pq (можно и pgx, суть та же). Мы подписываемся на канал и ждем событий.
func UpdateListener(connString string, cash *AppCache) {
reportProblem := func(ev pq.ListenerEventType, err error) {
if err != nil { fmt.Println("Listener error:", err) }
}
// Настраиваем подключение и реконнекты
listener := pq.NewListener(connString, 10*time.Second, time.Minute, reportProblem)
// ПОДПИСЫВАЕМСЯ НА КАНАЛ
if err := listener.Listen("update_cache"); err != nil {
panic(err)
}
fmt.Println("Start monitoring PostgreSQL...")
go func() {
for {
select {
// Как только прилетел сигнал от триггера...
case <-listener.Notify:
fmt.Println("PostgreSQL updated! Reloading cache...")
// ...мы просто перечитываем всю таблицу
_ = cash.Reload()
// Пинг, чтобы соединение не отвалилось по таймауту
case <-time.After(1 * time.Minute):
go listener.Ping()
}
}
}()
}
Полная реализация инициализации и метода Reload() — тут [3].
Давайте честно сравним подходы для задачи "Справочник на 100 строк".
|
Характеристика |
Redis / Memcached |
In-Memory + PG Notify |
|---|---|---|
|
Скорость чтения |
Быстро (~0.5 - 1 ms). Требует сериализации/десериализации (JSON/Protobuf) и похода в сеть. |
Мгновенно (ns). Данные уже в куче Go в нужном формате. |
|
Инфраструктура |
Нужен отдельный контейнер/инстанс. Нужно мониторить память, настраивать персистентность. |
0 затрат. Используется уже существующее подключение к БД. |
|
Актуальность данных |
Нужно вручную инвалидировать кэш при записи в БД (pattern "Cache-Aside"). Есть риск рассинхрона. |
Реактивно. База сама сообщает об изменении. Рассинхрон минимален (время доставки сигнала). |
|
Сложность кода |
Средняя. Нужен клиент Redis. |
Низкая. Стандартные средства SQL и драйвера. |
У этого подхода есть границы применимости. Не делайте так, если:
Данных много. Держать гигабайты в RAM приложения — плохая идея (OOM Killer не дремлет).
Частые обновления. Если таблица обновляется 100 раз в секунду, вы замучаете приложение постоянными Reload() и локами на запись.
Распределенная система без Sticky Sessions. Каждая реплика вашего сервиса будет хранить свою копию кэша. Для 100 строк это ОК, для больших данных — дублирование памяти.
Для задачи кэширования небольших, редко изменяемых справочников (конфиги, списки приложений, права ролей, feature-flags) связка PostgreSQL LISTEN/NOTIFY + Go RWMutex работает идеально.
Мы получили:
Нулевой Latency при чтении.
Отсутствие лишней зависимости (Redis).
Гарантированную консистентность (кэш обновляется сразу после коммита транзакции в БД).
Иногда лучшие инструменты — это те, которые у вас уже есть, просто нужно уметь их готовить.
Посмотреть, как это работает в живом проекте, можно в нашем репозитории: github.com/bmstu-itstech/sso [1]. Заходите, ставьте звёздочки, предлагайте PR! ⭐
Автор: BOBAvov
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/postgresql/443953
Ссылки в тексте:
[1] BMSTU-ITSTECH/SSO: https://github.com/bmstu-itstech/sso
[2] dev_v2 нашего репозитория: https://github.com/bmstu-itstech/sso/tree/dev_v2
[3] тут: https://github.com/bmstu-itstech/sso/blob/dev_v2/internal/pkg/cache/app_cache.go
[4] Документация Postgres: CREATE TRIGGER: https://www.postgresql.org/docs/current/sql-createtrigger.html
[5] Документация Postgres: NOTIFY: https://www.postgresql.org/docs/current/sql-notify.html
[6] Источник: https://habr.com/ru/articles/992990/?utm_source=habrahabr&utm_medium=rss&utm_campaign=992990
Нажмите здесь для печати.