Мы спроектировали трёхуровневую архитектуру кэширования, учитывающую требования strong consistency для финансовых данных:
┌──────────────────────────────────────────────────┐
│ Приложение │
│ ┌────────────────┐ ┌─────────────────────────┐ │
│ │ Финансовые ops │ │ Справочные/read-heavy │ │
│ │ (балансы, транз)│ │ (тарифы, каталоги) │ │
│ └───────┬────────┘ └──────────┬──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌─────────────────────┐ │
│ │ PostgreSQL │ │ Cache-aside (Redis) │ │
│ │ (напрямую) │ │ TTL: 60-300 сек │ │
│ └──────────────┘ └──────────┬──────────┘ │
│ │ cache miss │
│ ▼ │
│ ┌──────────────────┐ │
│ │ PostgreSQL │ │
│ │ (read replica) │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────┘
Для операций с балансами, транзакциями и расчётами мы сознательно не использовали кэш. Причина — strong consistency requirement. Любое расхождение баланса в кэше и БД для биллинговой системы недопустимо.
Вместо кэша мы оптимизировали PostgreSQL:
# postgresql.conf — оптимизация для point-select
shared_buffers = 32GB # 25% RAM
effective_cache_size = 96GB # 75% RAM
work_mem = 256MB
maintenance_work_mem = 2GB
# Connection pooling (pgbouncer)
[pgbouncer]
pool_mode = transaction
default_pool_size = 100
max_client_conn = 5000
server_check_query = SELECT 1
# Партиционирование таблицы транзакций по месяцам
CREATE TABLE transactions (
id BIGSERIAL,
account_id BIGINT NOT NULL,
amount DECIMAL(15,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);
CREATE TABLE transactions_2026_01
PARTITION OF transactions
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- и т.д. для каждого месяца
Для тарифных планов, каталогов услуг и конфигураций мы внедрили паттерн cache-aside:
# Конфигурация Redis для кэш-слоя
# redis.conf
bind 0.0.0.0
port 6379
protected-mode yes
requirepass ${REDIS_PASSWORD}
# Отключение персистентности (чистый кэш)
appendonly no
save ""
# Многопоточный I/O
io-threads 8
io-threads-do-reads yes
# Политика вытеснения
maxmemory 16gb
maxmemory-policy allkeys-lfu
# Оптимизация для кэш-сценария
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
Пример реализации cache-aside на стороне приложения (Python):
import redis
import json
import psycopg2
from functools import wraps
redis_client = redis.Redis(
host='redis-cache.internal',
port=6379,
password=os.environ['REDIS_PASSWORD'],
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=1,
retry_on_timeout=True
)
def cached(ttl=120, key_prefix=''):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = f"{key_prefix}:{func.__name__}:{hash(str(args)+str(kwargs))}"
# 1. Пробуем получить из кэша
try:
cached_value = redis_client.get(cache_key)
if cached_value:
return json.loads(cached_value)
except redis.RedisError:
# Redis недоступен — идём напрямую в БД
pass
# 2. Cache miss — запрос к PostgreSQL
result = func(*args, **kwargs)
# 3. Записываем в кэш
try:
redis_client.setex(
cache_key,
ttl,
json.dumps(result, default=str)
)
except redis.RedisError:
pass # Не ломаем основной flow при проблемах с кэшом
return result
return wrapper
return decorator
@cached(ttl=300, key_prefix='tariff')
def get_tariff_plan(tariff_id: int) -> dict:
with get_db_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM tariff_plans WHERE id = %s",
(tariff_id,)
)
return dict(cur.fetchone())
Оставить комментарий