Как блокируются таблицы innodb при обработке триггера ON INSERT? - PullRequest
5 голосов
/ 19 января 2011

У меня есть две таблицы 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?

Ответы [ 2 ]

5 голосов
/ 19 января 2011

Что касается проблем с параллелизмом, у вас есть 'простой' способ предотвратить любые проблемы с параллелизмом во втором методе, внутри вашей транзакции выполните выборку в строке статей (теперь For update неявно) , Любая одновременная вставка в ту же статью не сможет получить такую ​​же блокировку и будет ждать вас.

С новыми уровнями изоляции по умолчанию, даже не используя уровень сериализации в транзакции, вы не увидите одновременной вставки в таблицу голосования до конца транзакции. Таким образом, ваша сумма должна оставаться согласованной или выглядеть как согласованная . Но если при одновременной транзакции вставить голосование по той же статье и зафиксировать перед вами (а эта вторая транзакция не увидит вашу вставку), последняя транзакция, которая будет зафиксирована, перезапишет счетчик, и вы потеряете 1 голос. Так что выполните блокировку строки для статьи, используя select перед (и, конечно, выполняйте свою работу в транзакции). Это легко проверить, открыть 2 интерактивных сеанса на MySQL и начать транзакции с BEGIN.

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

  • Не забудьте удалить триггеры.
  • Не забывайте триггеры обновления.
  • Если вы не используете триггеры и остаетесь в коде будьте осторожны каждый вставить / удалить / обновить запрос при голосовании следует выполнить блокировку строки на соответствующая статья, прежде чем в сделка. Это не очень сложно забудь одну.

Последнее замечание: совершать более сложные транзакции перед началом использования транзакции:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Таким образом, вам не нужно блокировать строки в статьях, MySQL обнаружит, что происходит потенциальная запись в ту же строку, и заблокирует другие транзакции, пока вы не закончите. Но не используйте то, что вы вычислили из предыдущего запроса . Запрос на обновление будет ожидать снятия блокировки статей, когда блокировка снимается с помощью 1-й транзакции 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;

Но будьте готовы обработать разрывающую транзакцию, которую вы должны повторить.

1 голос
/ 19 января 2011

попробуйте это:

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;
...