#mysql #sql #database-design #database-locking
#mysql #sql #проектирование базы данных #блокировка базы данных
Вопрос:
Я действительно новичок в реляционных базах данных. Я работаю над проектом, который связан с финансами, и поэтому я хочу, чтобы любые действия, влияющие на баланс, не происходили одновременно, и я хочу добиться этого с помощью блокировок, однако я не уверен, как их использовать. Теперь у меня есть видение: я хочу иметь отдельную таблицу для каждого действия и поле баланса в таблице users, значение которого будет получено из всех соответствующих таблиц. Это печально, я никогда не собираюсь обновлять существующие записи — только добавлять их. Я хочу убедиться, что одновременно в эти таблицы вставляется только одна запись для каждого пользователя. Например: 3 транзакции происходят одновременно, и поэтому 3 записи должны быть добавлены в любые соответствующие таблицы. Две записи имеют один и тот же идентификатор пользователя, внешний ключ к моей таблице пользователей, а другая имеет другой. Я хочу, чтобы мои записи с одинаковыми внешними ключами передавались по конвейеру, а другой можно было выполнять в любое время. Как мне этого добиться? Есть ли какие-либо лучшие способы подойти к этому?
Ответ №1:
Я хочу, чтобы любые действия, влияющие на баланс, не выполнялись одновременно
Почему?
Я хочу добиться этого с помощью блокировок
Почему?
Чтобы дать вам встречный пример. Допустим, вы хотите избежать отрицательных остатков на счетах. Когда пользователь выводит 500 $, как вы можете смоделировать это без блокировок.
UPDATE accounts
SET balance = balance - 500
WHERE accountholderid = 42
AND balance >= 500
Это работает без каких-либо явных блокировок и безопасно для одновременного доступа. Вам нужно будет проверить количество обновлений, если оно равно 0, вы бы переполнили счет.
(Я знаю, что MySQL все равно получит блокировку строк)
По-прежнему имеет смысл иметь бухгалтерскую книгу, но даже там необходимость в блокировках для меня не очевидна.
Комментарии:
1. Мне сказали, что хранение баланса в виде обычного числа является плохой практикой. Зачем хранить его, когда он может быть получен из данных, которые я записываю в любом случае? Мне нужно убедиться, что одновременно не происходит транзакций, связанных с 51% баланса. В противном случае полученный баланс для этого пользователя окажется отрицательным.
2. Вам была указана причина, почему это так? Каково максимальное количество записей, которые вам нужно будет объединить, чтобы определить баланс счета? Каково ваше определение «одновременно»? Как баланс станет отрицательным в приведенном мной примере кода?
3. Нет, мне не указали причину, но для меня это имеет смысл. Максимальное количество записей не ограничено. В то же время для меня, если мое приложение узла запускает 2 разные задачи примерно в одно и то же время, когда одна из них не повлияла на базу данных перед запуском другой, поэтому оба используют одно и то же производное значение для баланса пользователя, и каждая потенциально может создать новую запись, которая использует 51% от общего объема данных.баланс.
4. Если максимальное количество записей не ограничено, вам нужно суммировать неограниченное количество записей, чтобы получить баланс счета. Вы видите, в чем проблема? Опять же, показанный запрос позволяет избежать этой проблемы, а также избежать проблемы с тремя «одновременными» транзакциями.
5. Я знаю о параллелизме и снижении производительности. Однако, разве безопасность не важнее? Я бы предпочел исключить возможность ошибки в запросе, которая навсегда увеличила бы чей-то баланс на какую-то огромную сумму и позволила бы им обанкротить ваш бизнес.
Ответ №2:
- Используйте
ENGINE=InnoDB
для всех ваших таблиц. -
Используйте транзакции:
BEGIN; do all the work for a single action COMMIT;
Классическим примером одного действия является снятие денег с одного счета и добавление их на другой счет. Удаление будет включать проверку на овердрафт, и в этом случае у вас будет код ROLLBACK
вместо COMMIT
.
Блокировки, которые вы получаете, гарантируют, что все для одного действия либо полностью выполнено, либо вообще ничего не сделано. Это применимо даже в случае сбоя системы между BEGIN
и COMMIT
.
Без begin и commit, но с autocommit=ON, каждый оператор неявно окружен begin и commit . То есть UPDATE
пример в предыдущем ответе является «атомарным». Однако, если деньги, снятые с одной учетной записи, необходимо добавить на другую учетную запись, что произойдет, если сбой произойдет сразу после UPDATE
? Деньги исчезают. Итак, вам действительно нужно
BEGIN;
if not enough funds, ROLLBACK and exit
UPDATE to take money from one account
UPDATE to add that money to another account
INSERT into some log or audit trail to track all transactions
COMMIT;
Проверяйте после каждого шага — ОТКАТ и принятие уклончивых действий при любой неожиданной ошибке.
Что произойдет, если 2 (или более) действия будут выполняться «одновременно»?
- Один ожидает другого.
- Возникает взаимоблокировка, и для нее выполняется принудительный ОТКАТ.
Но ни в коем случае данные не будут испорчены.
Еще одно замечание… В некоторых случаях вам нужно FOR UPDATE
:
BEGIN;
SELECT some stuff from a row FOR UPDATE;
test the stuff, such as account balance
UPDATE that same row;
COMMIT;
FOR UPDATE
Другим потокам говорится: «Уберите руки от этой строки, я, вероятно, изменю ее; пожалуйста, подождите, пока я не закончу». Без FOR UPDATE
этого другой поток может проникнуть и истощить счет денег, которые, как вы думали, были там.
Комментарии к некоторым вашим мыслям:
- Одной таблицы обычно достаточно для многих пользователей и их учетной записи. Он будет содержать «текущий» баланс для каждой учетной записи. Я упомянул «журнал»; это будет отдельная таблица; она будет содержать «историю» (в отличие от просто «текущей» информации).
FOREIGN KEYs
в основном не имеют отношения к этому обсуждению. Они служат 2 целям: убедитесь, что в другой таблице есть строка, которая должна быть там; и неявно создайтеINDEX
, чтобы ускорить эту проверку.- Конвейерная обработка? Если вы выполняете не более сотни «транзакций» в секунду,
BEGIN..COMMIT
логика — это все, о чем вам нужно беспокоиться. - «В то же время» и «одновременно» используются неправильно. Очень маловероятно, что два пользователя попадут в базу данных «одновременно» — учитывайте задержки браузера, сетевые задержки, задержки ОС и т. Д. Плюс тот факт, что большинство этих шагов заставляют activity работать в одном файле. Сеть заставляет одно сообщение попадать туда раньше другого. Между тем, если одна из ваших «транзакций» занимает 0,01 секунды, кого волнует, должен ли «одновременный» запрос ждать его завершения. Дело в том, что то, что я описал, заставит «ждать», если это необходимо, чтобы избежать искажения данных.
Все это говорит о том, что все еще могут быть некоторые «одновременно» — если транзакции не касаются одних и тех же строк, то несколько миллисекунд, которые требуются от BEGIN
to COMMIT
, могут перекрываться. Рассмотрим эту временную шкалу двух транзакций, которые произошли почти одновременно:
BEGIN; -- A
pull money from Alice -- A
BEGIN; -- B
pull money from Bobby -- B
give Alice's money to Alan -- A
give Bobby's money to Betty --B
COMMIT; --A
COMMIT; --B