Ведение данных из базы данных в горизонтальном масштабе
Предположим, у нас есть микросервис "А". Теперь мы масштабируем его по горизонтали, что означает, что у нас есть 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, которые вы хотите достичь.
Когда РСУБД является целевой базой данных, одним из возможных вариантов может быть отделение дорогостоящей операции с базой данных, связанной с
p.updateInnerData();
и сделать его асинхронным через надежную платформу обмена сообщениями, которая обеспечивает строгое упорядочивание без снижения скорости; например, Кафка; мы можем даже подумать о резервном копировании сообщений путем храненияp
объект / изменяется сам по себе, как BLOB/JSON в таблице Db, и немедленно возвращает элемент управления пользователю, а затем запускает сообщение асинхронно.Поскольку NoSQL является целевой базой данных, мы хотели бы выбирать на основе наших потребностей ЧТЕНИЯ / ЗАПИСИ и, таким образом, в значительной степени уменьшая задержку, связанную с записью и чтением.