Проектирование системы: стратегии борьбы с тяжелыми записями в БД
С точки зрения проектирования / масштабируемости систем, каковы некоторые стандартные отраслевые стратегии работы с системой, требующей интенсивной записи в конкретную таблицу в БД.
Для простоты, скажем, таблица представляет собой таблицу инвентаризации продуктов и имеет столбец "Имя продукта" и столбец "Счет", и он просто увеличивается на +1 каждый раз, когда в систему покупается новый продукт. И миллионы пользователей покупают разные продукты каждый второй, и мы должны следить за последним подсчетом каждого продукта, но это не обязательно должно быть строго в реальном времени, возможно, допустимо отставание в 5 минут.
Мои варианты:
1) Репликация главного подчиненного, где главная БД обрабатывает все записи, а подчиненные - чтения. Но это не решает проблему записи
2) Разделение базы данных на основе диапазона названий продуктов или его хешированного значения. Но что, если есть определенный продукт (например, Apple), который получает большое количество обновлений за короткое время, он все равно попадет в ту же БД.
3) Пакетные обновления? Использовать какое-то кэширование и записывать в таблицу каждые X секунд с накопленным подсчетом того, что мы получили за эти Х секунд? Это допустимая опция, и какой механизм кэширования я использую? А что, если между последним чтением и следующей записью произойдет сбой? Как мне восстановить потерянный счет?
4) Есть ли другие очевидные варианты, о которых я забыл?
Любое понимание приветствуется!
2 ответа
Я бы сказал, что решение будет сильно зависеть от того, что именно вам нужно сделать. Решение для записи тысяч записей в секунду может сильно отличаться от увеличения счетчика в приведенном вами примере. Более того, не может быть tables
вообще справиться с такой нагрузкой. Consistency
/availability
требования также отсутствуют в вашем вопросе, и в зависимости от них вся архитектура может сильно отличаться.
В любом случае, вернемся к вашему конкретному упрощенному случаю и вашим возможностям.
Вариант 1 (репликация Master-Slave)
Здесь вы столкнетесь с проблемой базы данных locking
- для каждого приращения потребуется блокировка записи, чтобы избежать условий гонки, и вы быстро отправите свои процессы в вашу базу данных, ожидающую в очереди, а ваша система не работает. Даже при умеренной нагрузке)
Вариант 2 (Sharding the DB)
Ваше предположение верно, мало чем отличается от ч.1
Вариант 3 (Пакетные обновления)
Очень близко. Кэширующий слой, обеспеченный легким хранилищем, обеспечивающий одновременные атомные приращения / уменьшения с постоянством, чтобы не потерять ваши данные. Мы использовали redis
для аналогичной цели, хотя любая другая база данных ключ-значение подойдет - буквально существуют десятки таких баз данных.
База данных значения ключа или хранилище значения ключа - это парадигма хранения данных, предназначенная для хранения, извлечения и управления ассоциативными массивами, структура данных, более известная сегодня как словарь или хеш-таблица
Решение будет выглядеть следующим образом:
incoming requests → your backend server -> kv_storage (atomic increment(product_id))
И у вас будет запущен "очищающий" скрипт, т.е. */5
что делает следующее (упрощенно):
- для каждого
product_id
в kv_storage читайте его текущийvalue
- обновите свой счетчик БД (
+= value
) - уменьшать
value
в кв_стораге
Дальнейшее масштабирование
- в случае сбоя сценария ничего плохого не произойдет - обновления появятся при следующем запуске
- если ваши бэкэнд-боксы не справляются с нагрузкой - вы можете легко добавить больше ящиков
- если один db со значением ключа не может справиться с нагрузкой - большинство из них поддерживает масштабирование по нескольким блокам, или простая стратегия шардинга в ваших скриптах бэкэнда будет работать нормально
- если один "очищающий" скрипт не поспевает за приращениями - вы можете масштабировать их до нескольких блоков и решить, какие диапазоны ключей обрабатываются каждым из них
Вы задали типичный вопрос CQRS. "CQRS" означает разделение ответственности по командным запросам. Вот как это звучит - вы отделяете ваши записи (команды) от ваших операций чтения (запросов). Этот подход решает проблемы, когда у вас есть разные потребности между записью и чтением - именно ваша ситуация.
Чтобы добиться этого масштабируемым образом, вам необходимо подтвердить (т.е. принять) запрос на приращение и поставить его в очередь для обработки. И пусть чтения работают в режиме реального времени за запрос. Обработайте запросы в очереди с помощью фонового обработчика команд, который знает, как согласовать. то есть, если это терпит неудачу, он должен знать, как разрешить конфликт (например, если кто-то еще обновил строку, получить более новую версию и повторить попытку).
Я полностью не согласен с другим ответом, в котором кто-то предположил, что организация очередей разрушит всю вашу систему. Очереди ничего не сбивает, потому что это очередь, а не обработка в реальном времени. Это точка масштабирования. Наоборот, внесение изменений в реальном времени, даже если это означает просто изменение логического флага в кэш-памяти в памяти, намного хуже, чем в очереди. Подумайте, что произойдет, если кеш в памяти в этот момент не работает. Асинхронная автономная (фоновая) обработка гарантирует, что такие проблемы не мешают выполнению команды в конечном итоге. Однако вам может понадобиться либо медленно обрабатывать команды из очереди (независимо от скорости, с которой он может справиться, не влияя на чтение), либо в отдельной копии данных.
Вы можете использовать определенную технологию, такую как кэш в памяти, как предлагали другие, но это опять-таки еще одна реализация парадигмы CQRS. Это может быть кеш или просто другая копия записи или базы данных. То же самое и тот же эффект.