Mysql Переход на транзакции в InnoDB с LOCK TABLES MyISAM

Royal Flash

-=MaestrO=-
Здравствуйте.

Помогите перейти на транзакции и блокировки строк в InnoDB с LOCK TABLES MyISAM

Я решил перейти на InnoDB с MyISAM.
Есть 3 таблицы в БД: balance_user, balance_log, balance_comment (комментарии к транзакциям по балансу, они могут быть, могут не быть, по этому заведена отдельная таблица).

Таблица balance_user:
| user_id | .... | balance_sum |
Т.е. в этой таблице хранятся суммы баланса пользователей. Каждому пользователю - одна строка.

Таблица balance_log
| id | user_id | sum | .... |

При приходе/расходе средств в balance_user изменяется значение колонки balance_sum для каждого пользователя. Причем, если с пользовательским балансом операция совершается впервые, в balance_user нужно добавить строку, если информация о балансе уже есть - обновить. В balance_log строки всегда только добавляются, т.е. эта таблица - журнал операций с балансом. В balance_comment строки могут быть добавлены или нет, в зависимости от наличия комментариев.
За один раз могут быть изменены балансы более одного пользователя.

На MyISAM все просто: в скрипте блокируются все 3 таблицы (LOCK TABLES ... WRITE), далее узнать MAX(id) balance_log, после сгененрировать sql-запросы insert/update для balance_user, создать insert для balance_log и, если требуется, insert для balance_comment.
И если с UPDATE все просто - сколько изменений в пользовательских балансах balance_user, столько и запросов UPDATE, то с INSERT немного сложнее: чтобы не заваливать БД пачкой запросов INSERT, все INSERT к одной таблице формируются в один запрос (INSERT INTO balance_log (id, user_id, sum) VALUES (1, 1, 30), (2, 195, 5598) ... ).
Поскольку после INSERT, в котором добавляется более одной строки, проблематично узнать ID созданный autoincremet для каждой строки, ID для balance_log формируется в скрипте, создающем запросы (для этого нужен первый запрос SELECT MAX(id) FROM balance_log).
По этому же ID balance_log привязывается и balance_comment.

Схема запросов MyISAM:

LOCK TABLES ... WRITE

SELECT MAX(id) FROM balance_log

// тут формируются запросы UPDATE/INSERT для balance_user и INSERT для balance_log, balance_comment

UPDATE balance_user SET balance_sum = 234 WHERE user_id = 1
UPDATE ...

INSERT INTO balance_user (user_id, balance_sum) VALUES (2, 304), (3, 58) , ...

INSERT INTO balance_log (id, user_id, sum) VALUES (1, 1, 30), (2, 195, 5598), ....

INSERT INTO balance_comment (balance_log_id, comment) VALUES (1, "Коментарий"), ....

UNLOCK TABLES

Подскажите, как переписать эту логику с учетом транзакций в InnoDB, чтобы блокировать не все таблицы, а только строки, с которыми будет проходить UPDATE и интервал ID, с которыми будет вставляться INSERT?
 
Да, об именно этом. По скольку с InnoDB сталкиваюсь впервые, просьба расставить блокировки по схеме.
 
Поглядел, перечитал мануал, книжки, статьи в нете, но до конца так и не разобрался. Вопросов появилось больше, чем было до чтения всего вышеперечисленного. Тяжело дается понятие ISOLATION LEVEL.

Предполагаю делать так:

// Чтобы не зависеть от настроек сервера
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ

START TRANSACTION

SELECT MAX(id) AS max_id FROM balance_log
// $max_id = MAX(id);

// Блокировка на запись интервала новых id таблицы balance_log, чтобы безопасно сгенерировать новые id в скрипте
SELECT id FROM balance_log WHERE id > $max_id FOR UPDATE

// Блокировка на запись интервала новых id таблицы balance_comment
SELECT id FROM balance_comment WHERE id > $max_id FOR UPDATE

// Блокировка id добавляемых пользователей
SELECT id FROM balance_user WHERE id IN (идентификаторы добавляемых пользователей) FOR UPDATE

// тут формируются запросы UPDATE/INSERT для balance_user и INSERT для balance_log, balance_comment

// Блокировка id обновляемых пользователей
SELECT id FROM balance_user WHERE id IN (идентификаторы обновляемых пользователей) FOR UPDATE

UPDATE balance_user SET balance_sum = 234 WHERE user_id = 1
UPDATE ...

INSERT INTO balance_user (user_id, balance_sum) VALUES (2, 304), (3, 58) , ...

INSERT INTO balance_log (id, user_id, sum) VALUES (1, 1, 30), (2, 195, 5598), ....

INSERT INTO balance_comment (balance_log_id, comment) VALUES (1, "Коментарий"), ....

COMMIT

Не уверен, что схема запросов верна. Больше всего беспокоит вопрос, что если 2 таких транзакции будут выполняться параллельно, $max_id у них окажется идентичным, и одна из транзакций откатится. Как за один запрос выбрать MAX(id) и заблокировать интервал строк id >= MAX(id)?

Также, подскажите, плз, как лучше хранить базы InnoDB на сервере: в одном файле, как установлено по умолчанию, или все таки имеет смысл разбивать все таблицы по файлам?
 
странная процедура, как и структура бд

если 2 таких транзакции будут выполняться параллельно, $max_id у них окажется идентичным.. да! предложу
Serializable, но ты явно намудрил. напрашивается sequence,

Также, подскажите, плз, как лучше хранить базы InnoDB на сервере: в одном файле, как установлено по умолчанию, или все таки имеет смысл разбивать все таблицы по файлам?
тебе не всеравно?
 
странная процедура, как и структура бд
если 2 таких транзакции будут выполняться параллельно, $max_id у них окажется идентичным.. да! предложу
Serializable, но ты явно намудрил. напрашивается sequence,
тебе не всеравно?
Чем поможет Serializable для блокировки интервала => max(id)? Ведь это блокирующее чтение, параллельная транзакция все равно сможет прочитать тот же max(id)...
Sequence - это такой "велосипед" для MySQL? Нет, спасибо, и так "велосипедов" хватает :)Ничего лучше, чем блокировать всю таблицу запросом SELECT id FROM balance_user WHERE id > 0 FOR UPDATE, перед SELECT MAX(id) не придумал.

тебе не всеравно?
Было бы все равно - не спрашивал бы. Если такая возможность есть - значит это зачем-то нужно?
 
Если ты не знаешь, зачем тебе это нужно, значит, тебе это не нужно.
Оставь головную боль настройки серверов тем, кто за это деньги получает.
 
это значит транзакции будут работать поочереди
Даже в Wikipedia написано: Транза́кция - группа последовательных операций (т.е. операций "поочереди") с базой данных, так что уровень изоляции на это никак не влияет. Serializable - это добавление ко всем SELECT в интервале транзакции LOCK IN SHARE MODE, что ни как не спасет от параллельного чтения 2-мя параллельными транзакциями max(id) с идентичным результатом (проверил тестами). Спасибо, конечно, что пытаетесь мне помочь, но давать неправильные советы - не есть хорошо...

это поле auto_increment
- да, согласен, на счет "велосипеда" был не прав.

Методом тестирования, обнаружил, что достаточно запустить SELECT MAX(id) AS max_id FROM balance_log FOR UPDATE при уровне изоляции REPEATABLE READ и этот же запрос из параллельной транзакции будет ждать, пока не отработает первая транзакция. Также, что очень важно, пока стоит блокировка FOR UPDATE в balance_log, невозможно в другом запросе вставить в таблицу строку с id > MAX(id), ни автоинкрементом, ни явным указанием id, хотя в промежутки id < MAX(id) записи вставляются без проблем, но это уже никак не влияет на мою транзакцию.
 
так что уровень изоляции на это никак не влияет.
влияет,
что ни как не спасет от параллельного чтения 2-мя параллельными транзакциями max(id) с идентичным результатом
не спасет
Методом тестирования, обнаружил, что достаточно запустить SELECT MAX(id) AS max_id FROM balance_log FOR UPDATE при уровне изоляции REPEATABLE READ и этот же запрос из параллельной транзакции будет ждать, пока не отработает первая транзакция.
а вот это странно, у меня не получилось, все хуже, serializable строже, и если при REPEATABLE READ работает, то при serializable будет тем более
 
Последнее редактирование:
Royal Flash, разумеется, строка же заблокирована. Плюс в случае вставки еще и tail lock. Вот если в одном запросе id=1 а в другом id=2 - тогда без проблем.

Но с балансом лучше делать иначе. Есть лог операций, а баланс высчитывается как сумма всех операций. Если хочется иметь денормализацию в виде текущего баланса - обновление денормализованного поля выполнять как подсчет той же суммы последней операцией в транзакции.
 
а вот это странно, у меня не получилось, все хуже, serializable строже, и если при REPEATABLE READ работает, то при serializable будет тем более
С serializable я пробовал без FOR UPDATE - так не работало как нужно, с FOR UPDATE, конечно, работало так же, как и при REPEATABLE READ.

Но с балансом лучше делать иначе. Есть лог операций, а баланс высчитывается как сумма всех операций. Если хочется иметь денормализацию в виде текущего баланса - обновление денормализованного поля выполнять как подсчет той же суммы последней операцией в транзакции.
У меня, это так и организовано. У пользователя есть текущий баланс (balance_user), чтобы его можно было быстро прочитать и лог операций (balance_log), в котором также хранится текущий баланс (в последней записи) и все операции по балансу. Когда происходит изменение баланса высчитывается сумма между балансом в balance_user и суммой операции, и уже новые данные вносятся в balance_log, а в balance_user изменяется баланс. Таким образом, размер БД немного вырастает, за счет избыточного хранения данных, но повышается надежность - можно отследить все операции по балансу. Хотя, возможно, я не совсем понял суть Вашего совета?

Если бы балансы пользователей менялись по одному за раз, тогда еще можно было бы брать сумму баланса путем подсчета всех операций в balance_log, но поскольку иногда требуется вносить за один раз изменения по более чем одному балансу пользователя, просчитывать баланс для каждого пользователя видеться мне задачей не очень быстрой...
 
Последнее редактирование:
Royal Flash, я имел ввиду так
PHP:
begin;
insert into user_billing_operations (user_id, operation_type_id, amount) values (1, 1, 100);
insert into user_billing_operations (user_id, operation_type_id, amount) values (1, 3, 200);
insert into user_billing_operations (user_id, operation_type_id, amount) values (1, 9, -100);
update user_billing_state set total_balance = (select sum(amount) from user_billing_operations where user_id = 1) where user_id = 1;
commit;
 
Спасибо, смысл понятен. Но чем черевато хранение баланса, как предлагаю я, в отдельном поле таблицы, чтобы каждый раз его не пересчитывать? В Вашем варианте меня беспокоит то, что чем больше операций будет у пользователя, тем дольше будет пересчитываться его баланс.

Код:
begin;
insert into user_billing_operations (user_id, operation_type_id, amount) values (1, 1, 100), (2, 1, 200), (3, 1, 50);
update user_billing_state set total_balance =  total_balance + 100 where user_id = 1;
update user_billing_state set total_balance =  total_balance + 200 where user_id = 2;
update user_billing_state set total_balance =  total_balance + 50 where user_id = 3;
commit;

P.S. За один раз нужно пересчитывать баланс у разных пользователей, а не у одного, несколько операций по балансу за раз.
 
Но чем черевато хранение баланса, как предлагаю я, в отдельном поле таблицы, чтобы каждый раз его не пересчитывать?
Мой подход автоматически исправляет ошибки рассинхронизации, хоть и немного дороже.
 
Сверху