|
|
|
| Ситуация следующая. Есть чат, в нём есть так называемые боты, т.е. в чат добавляются при каких-то событиях сообщения от системы. Некоторые боты ожидают, пока не истечёт время, т.е. пока разность стартовой метки времени и текущей не превысит, скажем, 5 минут. Но поскольку в чате посетителей довольно много, то относительно часто возникает ситуация, когда бот повторяет одно и то же сообщение 2 раза, то есть до того, пока метка времени не обновилась "срабатывает" сразу несколько раз условие в параллельно вызванных скриптах.
Очень кратко:
1. Происходит SELECT last_posting_date, ... FROM bots ...
В зависимости от last_posting_date бот либо выдаёт новое сообщение, либо молчит.
2. Если с момента последнего постинга прошло более пяти минут (time() - last_posting_date > 300), то обновляется last_posting_date в соответствующей боту запись в таблице bots. И после этого происходит добавление сообщения.
Очевидно, что иногда происходит почти одновременная выборка last_posting_date (первый пункт) и в двух выполняющихся скриптах выполняется условие time() - last_posting_date > 300 со всеми вытекающими!
Как лучше поступить в данной ситуации? Может надо включить режим транзакции до пункта 1 и выключить его после пункта 2 (тогда нужен InnoDB?)?
Завтра так и попробую, но может быть кто поправит меня. Я примерно представляю режим транзакции таким образом (грубо), что все InnoDB-таблицы внтури блокируются (или отдельные записи в них - не знаю), выполняются все запросы, записываются и только после даётся возможность другим достучаться до табличек. | |
|
|
|
|
|
|
|
для: Fractured
(17.08.2009 в 02:32)
| | По-моему, достаточно механизма mysql_insert_id .
$lpd1 = SELECT MAX(last_posting_date), ... FROM bots ...
если (time() - $lpd1 > 300)
{
$id = INSERT INTO bots
$lpd2 = SELECT MAX(last_posting_date) FROM bots WHERE id < $id
[ или даже $lpd2 = SELECT last_posting_date FROM bots WHERE id =
(SELECT MAX(id) FROM bots WHERE id < $id) ]
если (time() - $lpd2 > 300)
{
выпустить сообщение
}
иначе DELETE FROM bots where id = $id
}
|
| |
|
|
|
|
|
|
|
для: Trianon
(17.08.2009 в 10:51)
| | А это нормальная практика? В моём случае многовато кода выходит, хотя вариант. | |
|
|
|
|
|
|
|
для: Fractured
(21.08.2009 в 18:14)
| | а-я предложил красивый ход - уникальным ключ, формируемый путем округления таймштампа. | |
|
|
|
|
|
|
|
для: Trianon
(22.08.2009 в 00:22)
| | Вновь возвращаюсь к этой теме :confused:
Такой ход не подходит. Я не совсем раскрыл суть проблемы.
Бот задаёт какой-то вопрос. Пользователи отвечают. Иногда он задаёт подряд 2 вопроса. И всё это очень походит на борьбу со следствием, а не причиной. Причём Ваш вариант тоже, если честно...
Т.е. причина: начинается смена стадии этой викторины (стадий несколько: инициализация вопроса, подсказка, завершение в случае истечения времени) параллельно в 2-х скриптах. race condition...
По совету а-я получится так: бот задаст какой-то вопрос (т.е. идентификатор вопроса сохранится в определенном месте), он появится в чате. Но другой скрипт перезатрёт идентификатор вопроса, но в чате новый вопрос не появится. | |
|
|
|
|
|
|
|
для: Fractured#
(27.09.2009 в 23:14)
| | в моем варианте я не вижу никакого перезатирания .
в вапранте а-я, очевидно, слегка придется доточить логику. | |
|
|
|
|
|
|
|
для: Trianon
(27.09.2009 в 23:27)
| | В Вашем - да. Просто он не очень нравится... OK, я ещё поищу другие способы. | |
|
|
|
|
|
|
|
для: Fractured#
(27.09.2009 в 23:28)
| | Всё-таки решается по-нормальному средствами MySQL (тип таблицы - InnoDB):
<?php
mysql_query('SET `autocommit` = 0;');
mysql_query('START TRANSACTION;');
mysql_query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;');
mysql_query('SELECT `last_posting_date` FROM ... FOR UPDATE');
if( time() - $last_posting_date > 300 )
{
mysql_query('UPDATE ... SET `last_posting_date` = ' . time() . ';');
# Добавление поста и т.д.
}
mysql_query('COMMIT;');
?>
|
| |
|
|
|
|
|
|
|
для: Fractured#
(29.09.2009 в 17:36)
| | InnoDB медленнее MyISAM
Транзакции нужны грубо говоря из-за возможности отката, что вы собираетесь откатывать в чате?
Того же можно добиться с помощью блокировки таблиц, хотя нужна она тут или нет я сказать не возьмусь, уж очень непонятно вы всё обьяснили.
Вы случайно не WoW ковыряете? | |
|
|
|
|
|
|
|
для: Valick
(29.09.2009 в 18:04)
| | Выражусь культурнее: спасибо, мне не нужна блокировка всей таблицы сразу. | |
|
|
|
|
|
|
|
для: Fractured#
(29.09.2009 в 23:15)
| | модератор удалил вместе с Вашим постом мой последний дебильный совет, о том что блокировка (если она нужна, в чём лично я сильно сомневаюсь) раз в 5 минут - это лучше, чем ежесекундный проигрышь в скорости при использовании InnoDB (тем более что у вас такой нагруженный чат) | |
|
|
|
|
|
|
|
для: Valick
(30.09.2009 в 00:12)
| | Ваш способ предполагает блокировку каждый раз, в противном случае это никак не поможет. И вообще, скажу Вам по секрету, InnoDB вообще-то при интенсивных запросах к таблице (SELECT/INSERT/DELETE/UPDATE) на нагруженных проектах предпочтительнее, нежели MyISAM.
MyISAM может блокировать только на уровне всей таблицы, а InnoDB - на уровне строк. Я как-то уже на эти грабли наступал, проект с очень большой посещаемость регулярно висел, в списках процессов были кучи запросов, ожидающих своей очереди к несчастной таблице MyISAM.
Если Вы до сих пор не понимаете в чём проблема, то самостоятельно смоделируйте ситуацию:
script1.php
<?php
$db->query('SELECT `timestamp` FROM ...');
if( $db->result() < time() )
{
sleep(25);
$db->query('UPDATE ... SET `timestamp` = ' . ( time() + 60 ));
$db->query('INSERT INTO test_table (page_num) VALUES(1)');
}
?>
|
script2.php
<?php
$db->query('SELECT `timestamp` FROM ...');
if( $db->result() < time() )
{
$db->query('UPDATE ... SET `timestamp` = ' . ( time() + 60 ));
$db->query('INSERT INTO test_table (page_num) VALUES(2)');
}
?>
|
Сначала запустите script1.php, затем script2.php. Задача состоит в том, чтобы в test_table оказалась лишь одна запись: 1
P.S.
Я надеюсь Вы не начнёте придираться к этому коду: это скорее псевдокод, просто показана логика. Оформите запросы, структуры таблиц как Ваша душа пожелает.
P.P.S. Да, про чат и ботов я зря, извиняюсь. Надо было сразу как-то абстрагироваться... | |
|
|
|
|
|
|
|
для: Fractured#
(30.09.2009 в 01:01)
| | В MySQL предусмотрена возможность создания таблиц без транзакций, что необходимо приложениям, требующим максимально возможной скорости работы. Эти таблицы могут храниться в памяти, относиться к типу HEAP-таблиц или дисковых MyISAM.
Блокировка таблиц, применяющаяся в нетранзакционных таблицах MyISAM, во многих случаях работает быстрее, нежели блокировки на уровне страниц, строк или контроль версий. Недостаток этого подхода в том, что если не учитывать механизм работы блокирования таблиц, один длительный запрос может надолго заблокировать таблицу. Обычно этого эффекта можно избежать, приняв соответствующие меры при разработке приложения. Если это не удастся, всегда можно изменить тип таблицы и сделать ее транзакционной.
_____
http://www.mysql.ru/docs/man/
возможно информация несколько устарела, спорить не буду
_____
и ещё раз повторюсь, что понятия не имею что вы там творите, но для того чтобы вставить сообщение от бота в чат скорее всего вообще не нужна никакая блокировка таблиц...
погуглил по поводу не родного (!) для MySQL типа таблиц InnoDB сообщения похожи друг на друга как "диктанты первоклассников" что настораживает.
Но грубо говоря, если Вас устраивает полученный результат, то и дискутировать собсно не о чем. | |
|
|
|
|
|
|
|
для: Valick
(30.09.2009 в 10:29)
| | Я понимаю Ваше желание как-то оправдаться и представить меня глупым человеком. Именно поэтому Вы решили проигнорировать мою новую, абсолютно чёткую формулировку, в посте от 30.09.2009 в 01:01. Хорошо, не можете найти решение - не надо, мне оно не требуется (оно требуется Вам, чтобы в дальнейшем не говорить лишнего). | |
|
|
|
|
|
|
|
для: Fractured
(17.08.2009 в 02:32)
| | может
добавить еще одно поле в таблицу с сообщениями
`bot_time` INT NULL DEFAULT NULL
дать ему уникальный ключ.
UNIQUE (`bot_time`)
если комнат несколько то
UNIQUE (`room`, `bot_time`)
и записывать примерно так:
<?
$bot_time = 300; // 5 мин
$bot_time = floor(time() / $bot_time); // получаем сколько целых частей
// запрос
$sql = '
INSERT IGNORE INTO
`tbl`
SET
`bot_time` = '.$bot_time.',
`nick` = "Бот",
`msg` = "'.$msg.'"
';
?>
|
уникальный ключ - не даст повтора,
IGNORE - не вернет ошибку запроса.
NULL - для обычных сообщений юзеров | |
|
|
|