Как таблицы innodb блокируются при обработке триггера ON INSERT?
-
12-10-2019 - |
Вопрос
У меня есть две таблицы innodb:
статьи
id | title | sum_votes
------------------------------
1 | art 1 | 5
2 | art 2 | 8
3 | art 3 | 35
голоса
id | article_id | vote
------------------------------
1 | 1 | 1
2 | 1 | 2
3 | 1 | 2
4 | 2 | 10
5 | 2 | -2
6 | 3 | 10
7 | 3 | 15
8 | 3 | 12
9 | 3 | -2
При вставке новой записи в votes
таблицу, я хочу обновить sum_votes
поле в articles
таблицу, подсчитав сумму всех голосов.
Вопрос
Какой способ более эффективен, если сам расчет SUM() очень тяжелый (votes
таблица содержит 700 тыс. записей).
1.Создание триггера
CREATE TRIGGER `views_on_insert`
AFTER INSERT
ON `votes`
FOR EACH ROW
BEGIN
UPDATE `articles` SET
sum_votes = (
SELECT SUM(`vote`)
FROM `votes`
WHERE `id` = NEW.article_id
)
WHERE `id` = NEW.article_id;
END;
2.Использование двух запросов в моем приложении
SELECT SUM(`vote`) FROM `votes` WHERE `article_id` = 1;
UPDATE `articles`
SET sum_votes = <1st_query_result>
WHERE `id` = 1;
1-й способ кажется чище, но будет ли таблица заблокирована все время выполнения запроса SELECT?
Решение
Что касается проблем параллелизма, у вас есть 'легкий' Чтобы предотвратить любые проблемы параллелизма во втором методе, внутри вашей транзакции выполните выбор в строке статей ( For update
теперь является неявным).Любая одновременная вставка в одну и ту же статью не сможет получить эту же блокировку и будет ждать вас.
С новыми уровнями изоляции по умолчанию, даже не используя уровень сериализации в транзакции, вы не увидите никаких одновременных вставок в таблицу голосования до конца вашей транзакции.Таким образом, ваша СУММА должна оставаться последовательной. или выглядит связно.Но если параллельная транзакция вставит голосование по той же статье и зафиксирует ее раньше вас (и эта вторая транзакция не увидит вашу вставку), последняя фиксируемая транзакция перезапишет счетчик, и вы потеряете 1 голос. Поэтому выполните блокировку строки в статье, используя выбор перед (и, конечно, выполняйте свою работу в транзакции).Это легко протестировать, открыть 2 интерактивных сеанса в MySQL и начать транзакции с помощью BEGIN.
Если вы используете триггер, вы по умолчанию участвуете в транзакции.Но я думаю, вам также следует выполнить выбор в таблице статей, чтобы сделать неявную блокировку строк для одновременно работающих триггеров (сложнее проверить).
- Не забудьте удалить триггеры.
- Не забывайте триггеры обновления.
- Если вы не используете триггеры и остаетесь в коде, будьте осторожны с каждым запросом вставки/удаления/обновления в голосовании должны выполнять блокировку строки в соответствующей статье ранее в транзакции.Это не очень сложно забыть.
Последний пункт:делайте более сложные транзакции, перед началом транзакции используйте:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Таким образом, вам не нужны блокировки строк в статьях, MySQL обнаружит, что происходит потенциальная запись в той же строке, и заблокирует остальные транзакции, пока вы не закончите. Но не используйте то, что вы вычислили по предыдущему запросу..Запрос на обновление будет ожидать снятия блокировки со статей, когда блокировка будет снята первой транзакцией. COMMIT
вычисление SUM
необходимо сделать еще раз для подсчета.Таким образом, запрос на обновление должен содержать SUM
или сделайте дополнение.
update articles set nb_votes=(SELECT count(*) from vote) where id=2;
И здесь вы увидите, что MySQL умен: взаимоблокировка обнаруживается, если две транзакции пытаются сделать это, в то время как вставка была выполнена одновременно.На уровнях сериализации я не нашел способа получить неправильное значение с помощью:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
insert into vote (...
update articles set nb_votes=(
SELECT count(*) from vote where article_id=xx
) where id=XX;
COMMIT;
Но будьте готовы к нарушению транзакции, которую вам придется повторить.
Другие советы
попробуй это:
PHP:Концепция системы звездного рейтинга?
РЕДАКТИРОВАТЬ: изменена схема, чтобы пользователь мог голосовать за одно и то же изображение много раз:
drop table if exists image;
create table image
(
image_id int unsigned not null auto_increment primary key,
caption varchar(255) not null,
num_votes int unsigned not null default 0,
total_score int unsigned not null default 0,
rating decimal(8,2) not null default 0
)
engine = innodb;
drop table if exists image_vote;
create table image_vote
(
vote_id int unsigned not null auto_increment primary key,
image_id int unsigned not null,
user_id int unsigned not null,
score tinyint unsigned not null default 0,
key (image_id, user_id)
)
engine=innodb;
delimiter #
create trigger image_vote_after_ins_trig after insert on image_vote
for each row
begin
update image set
num_votes = num_votes + 1,
total_score = total_score + new.score,
rating = total_score / num_votes
where
image_id = new.image_id;
end#
delimiter ;
insert into image (caption) values ('image 1'),('image 2'), ('image 3');
insert into image_vote (image_id, user_id, score) values
(1,1,5),(1,2,4),(1,3,3),(1,4,2),(1,5,1),(1,5,2),(1,5,3),
(2,1,2),(2,2,1),(2,3,4),(2,3,2),
(3,1,4),(3,5,2);
select * from image;
select * from image_vote;