Ведение данных из базы данных в горизонтальном масштабе

Предположим, у нас есть микросервис "А". Теперь мы масштабируем его по горизонтали, что означает, что у нас есть 3 экземпляра "А", работающих с одним экземпляром БД (и схема, как правило, предполагает, что 3 экземпляра "А" могут выполнять чтение и запись для одних и тех же данных).

Теперь я продемонстрирую вопрос с помощью некоторого псевдокода. У нас есть следующая функция обновления в "A":

Product p = getProdFromDb(); // for example selecting 
// from Postgresql db

p.updateInnerData(); // synch method that updates 
// something inside the p model that takes significant 
// amount of time
p.updateInDb(); //  for example update back in postgresql

Проблема здесь в том, что другие экземпляры "А" могут изменять продукт p, пока мы обновляем его здесь (не в этой функции, а в связи с такими другими функциями, которые изменяют продукты в "А"). Одно из известных мне решений - это использование блокировки на БД (например, с помощью "Выбрать... для обновления"), но это создает узкое место в производительности в этой функции. Я хотел бы видеть лучшие решения, которые решают эту проблему без этого узкого места, реальные примеры в Java (или JS) были бы очень полезны.

Изменить: предположить, что разделение не вариант

2 ответа

Существует два вида блокировок: пессимистическая (та, которую вы пытаетесь избежать) и оптимистическая блокировка.

При оптимистической блокировке вы не держите блокировку, а пытаетесь сохранить документ; если документ уже был изменен одновременно (то есть он был изменен с тех пор, как мы его загрузили), тогда вы повторяете весь процесс (загрузка + изменение + сохранение).

Один из способов сделать это - иметь version столбец, который увеличивается каждый раз, когда вы изменяете сущность. Когда вы пытаетесь упорствовать, вы ожидаете, что сущность с version = version + 1 не существует Если он уже существует, это означает, что произошло параллельное обновление, и вы повторите попытку (загрузка + изменение + сохранение).

В псевдокоде алгоритм выглядит так:

function updateEntity(ID, load, mutate, create)

    do
    {
        entity, version = load(ID) or create entity
        entity = mutate entity
        updateRow(that matches the ID and version) and increment version
    }
    while (row has not changed and was not inserted)

Я дам вам также пример кода на PHP (надеюсь, это легко понять) для MongoDB:

class OptimisticMongoDocumentUpdater
{

    public function addOrUpdate(Collection $collection, $id, callable $hidrator, callable $factory = null, callable $updater, callable $serializer)
    {
        /**
         * We try to add/update the entity in a concurrent safe manner
         * using optimistic locking: we always try to update the existing version;
         * if another concurrent write has finished before us in the mean time
         * then retry the *whole* updating process
         */

        do {
            $document = $collection->findOne([
                '_id' => new ObjectID($id),
            ]);

            if ($document) {
                $version = $document['version'];
                $entity = \call_user_func($hidrator, $document);
            } else {
                if (!$factory) {
                    return;//do not create if factory does not exist
                }
                $entity = $factory();
                $version = 0;
            }

            $entity = $updater($entity);

            $serialized = $serializer($entity);

            unset($serialized['version']);

            try {
                $result = $collection->updateOne(
                    [
                        '_id'     => new ObjectID($id),
                        'version' => $version,
                    ],
                    [
                        '$set' => $serialized,
                        '$inc' => ['version' => 1],
                    ],
                    [
                        'upsert' => true,
                    ]
                );
            } catch (\MongoDB\Driver\Exception\WriteException $writeException) {
                $result = $writeException->getWriteResult();
            }

        } while (0 == $result->getMatchedCount() && 0 == $result->getUpsertedCount());//no side effect? then concurrent update -> retry
    }
}

В моем ответе я предполагаю, что вы хотите 100% надежности.

Если это так, вы можете разделить ваши таблицы на множество страниц, где каждая страница будет содержать X строк. когда вы попытаетесь обновить таблицу, вы заблокируете только эту страницу, но тогда будет больше операций ввода-вывода.

Кроме того, в вашей базе данных вы можете настроить ее так, чтобы команда select читала даже незафиксированные строки, что повысит скорость - для сервера SQL это SELECT WITH (NOLOCK)

Если p.updateInnerData();требуется время X, и у вас высокая скорость ввода-вывода, которая может не отправлять Y новых запросов к микросервису, тогда сама функция создает узкое место для производительности. В идеале операции, связанные с базой данных микросервисов, должны быть разработаны с учетом четких тестов производительности; это часто приводит нас к выбору самой базы данных, которая позволяет достичь высоких / ожидаемых IOPS, которые вы хотите достичь.

  1. Когда РСУБД является целевой базой данных, одним из возможных вариантов может быть отделение дорогостоящей операции с базой данных, связанной с p.updateInnerData();и сделать его асинхронным через надежную платформу обмена сообщениями, которая обеспечивает строгое упорядочивание без снижения скорости; например, Кафка; мы можем даже подумать о резервном копировании сообщений путем храненияp объект / изменяется сам по себе, как BLOB/JSON в таблице Db, и немедленно возвращает элемент управления пользователю, а затем запускает сообщение асинхронно.

  2. Поскольку NoSQL является целевой базой данных, мы хотели бы выбирать на основе наших потребностей ЧТЕНИЯ / ЗАПИСИ и, таким образом, в значительной степени уменьшая задержку, связанную с записью и чтением.

Другие вопросы по тегам