Необходимо реализовать микросервис для работы с балансом пользователей (зачисление средств, списание средств, перевод средств от пользователя к пользователю, а также метод получения баланса пользователя). Сервис должен предоставлять HTTP API и принимать/отдавать запросы/ответы в формате JSON.
Для развертывания dev-среды в Docker перейдите в папку с проектом и выполните:
docker-compose up --build
В результате будет создано Docker приложение с 2-мя контейнерами:
- Postgres с созданными БД (Основная и тестовая)
- Контейнер с приложением
При запуске приложения будут выполнены тесты.
Приложение принимает и отдает запросы и ответы в формате JSON. В репозитории содержится файл balance-server.postman_collection.json
- коллекция с запросами для Postman.
Ответ сервиса всегда содержит следующие поля:
- status (Статус запроса, целое положительное число)
- 0 = Запрос успешно выполнен
- Не 0 = Произошла ошибка
- data (Ответ сервиса)
- Может быть любого типа в зависимости от запроса
Сервис принимает запросы на endpoint'ы:
- GET /balance (Баланс пользователя)
-
Обязательные
- id (ID пользователя, целое число > 0)
-
Необязательные
- currency (Валюта для конвертации, код из 3 символов)
-
Пример запроса
{ "id": 1, "currency": "USD" }
-
Пример ответа
{ "status": 0, "data": 1060 }
-
- POST /transaction (Списание/Зачисление средств)
-
Обязательные
- id (ID пользователя для списания/зачисления)
- sum (Сумма платежа, вещественное число. Если отрицательное -> списание, положительное -> зачисление)
-
Необязательные
- desc (Описание платежа, текст)
-
Пример запроса
{ "id": 1, "sum": 51.7, "desc": "Random income" }
-
Пример ответа
{ "status": 0, "data": "Transaction completed" }
-
- POST /transfer (Перевод средств между пользователями)
-
Обязательные
- id (ID пользователя для списания)
- to (ID пользователя для зачисления)
- sum (Сумма платежа, вещественное положительное число)
-
Пример запроса
{ "id": 1, "sum": 27.43, "to": 2 }
-
Пример ответа
{ "status": 0, "data": "Transfer completed" }
-
- GET /transactions (История транзакций)
-
Обязательные
- id (ID пользователя)
-
Необязательные
- sort (Сортировка, текст)
- По умолчанию по дате
- sum (По сумме транзакции)
- date (По дате транзакции)
- from (Указатель на транзакцию для вывода)
- Без указания или с 0 выводит с первой транзакции
- Указатель на следующую страницу передается в теле ответа в параметре next, который необходимо использовать в качестве from для загрузки следующих транзакций
- Если параметр next в ответе = -1, то получена последняя страница
- start (Время в формате Unix timestamp)
- Выводит транзакции, произведенные после указанной даты
- По умолчанию - 0
- end (Время в формате Unix timestamp)
- Выводит транзакции, произведенные до указанной даты
- По умолчанию - текущая дата
- sort (Сортировка, текст)
-
Пример запроса
{ "id": 1, "sort": "sum", "from": 0, "start": 1637602965, "end": 1669138965 }
-
Пример овтета
{ "status": 0, "data": { "next": 6, "transactions": [ { "date": "2021-12-24T19:17:30Z", "desc":"Random income", "operation": 0, "sum": 14.53 }, { "date": "2021-12-24T19:21:20Z", "desc": "Random income", "operation": 1, "sum": -0.83 } ] } }
-
База данных
Работа с балансом: Баланс пользователя не хранится в явном виде. Вместо этого хранятся только произведенные операции. Это позволяет избежать проблем с одновременным доступом. Все операции с балансом выполняются в транзакциях с использованием рекомендательных блокировок (advisory lock). В каждой новой транзакции выполняется блокировка на идентификатор пользователя и код операции и автоматически снимается по завершению транзакции. Во время активной блокировки другие транзакции не смогут получить блокировку на тот же идентификатор и операцию. Таким образом баланс всегда будет поддерживаться в валидном состоянии, а транзакции для разных пользователей и операций не будут блокировать друг друга. На ожидание блокировки выделено 10 сек, если по истечении этого времени транзакция не сможет получить блокировку, то клиенту вернется HTTP 408 с сообщением о таймауте.
Сортировка и пагинация: Для быстрого отображения транзакций отсортированных по сумме с пагинацией, создается materialized view transactions_sum_order хранящее сортировку транзакций по сумме. Представление хранит только id транзакции и позицию транзакции в сортировке. Для вывода транзакций с пагинацией выполняется запрос к представлению для получения id транзакций. Все данные о выбранных транзакциях получаются с помощью INNER JOIN с оригинальной таблицей. Это позволяет избежать использования OFFSET, производительность которого падает с количеством данных в таблице. Представление обновляется каждые 3 минуты с помощью планировщика задач в main.go Обновление представления на 5 млн строк занимает ~30 сек. Любой запрос транзакций с пагинацией на любой странице выполняется за < 100 ms. В тоже время OFFSET на оригинальной таблице с сортировкой по сумме на 4999990 записей выполняется за > 4 секунд.
Для таблицы с транзакциями и представления созданы индексы для ускорения запросов.
Структура
- server
- содержит основной код контроллера, сервиса и репозитория
- test
- содержит код для тестов
- sql/init.sql
- содержит код для создания БД
Библиотеки и фреймворки
- Web фреймворк - gin-gonic
- Работа с Postgres через драйвер pgx
- Assertions для тестов из testify
- Для быстрого развертывания dev среды используется Docker с Compile Daemon