Лучший способ справиться с проблемами параллелизма

У меня есть среда LAPP (linux, apache, postgresql и php), но вопрос практически одинаков как в Postgres, так и в Mysql.

У меня есть приложение cms, которое я разработал, которое обрабатывает клиентов, документы (оценки, счета и т. Д.) И другие данные, структурированные в 1 БД postgres со многими схемами (по одной для каждого нашего клиента, использующего приложение); Давайте предположим, что около 200 схем, каждая из которых используется одновременно 15 человек (в среднем).

РЕДАКТИРОВАТЬ: у меня есть поле отметки времени с именем last_update в каждой таблице, и триггер, который обновляет отметку времени при каждом обновлении строки.

Ситуация такова:

  1. Сотрудники Foo и Bar редактируют документ 0001, используя форму с подробностями каждого документа.
  2. Например, измените детали отправки.
  3. Панель изменения телефонных номеров, а также некоторые элементы в документе.
  4. Нажмите кнопку "Сохранить", приложение обновит базу данных.
  5. Панель нажмите кнопку "Сохранить" после панели, повторно отправив форму со старыми данными о доставке.
  6. В базе данных изменения Foo были потеряны.

Ситуация, которую я хочу иметь:

  1. Люди Фу, Бар, Джон, Мэри, Пауль редактируют документ 0001, используя форму с подробностями каждого документа.
  2. Например, измените детали отправки.
  3. Бар и другие меняют что-то еще.
  4. Нажмите кнопку "Сохранить", приложение обновит базу данных.
  5. Бар и другие получают предупреждение "Внимание! этот документ был изменен кем-то другим. Нажмите здесь, чтобы загрузить фактические данные ".

Я хотел бы использовать Ajax для этого; просто используя скрытое поле с идентификатором документа и отметкой времени последнего обновления, каждые 5 секунд проверяйте, совпадает ли время последнего обновления, и ничего не делая, в противном случае показывайте диалоговое окно предупреждения.

Итак, страница check-last-update.php должна выглядеть примерно так:

<?php
//[connect to db, postgres or mysql]
$documentId = isset($_POST['document-id']) ? $_POST['document-id'] : 0;
$lastUpdateTime = isset($_POST['last-update-time']) ? $_POST['last-update-time'] : 0;
//in the real life i sanitize the data and use prepared statements;
$qr = pg_query("
    SELECT
        last_update_time
    FROM
        documents
    WHERE
        id = '$documentId'
");
$ray = pg_fetch_assoc($qr);
if($ray['last_update_time'] > $lastUpdateTime){
    //someone else updated the document since i opened it!
    echo 'reload';
}else{
    echo 'ok';
}
?>

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

Итак, что может быть другим эффективным решением без уничтожения БД?

Я думал использовать файлы, создавая, например, пустой текстовый файл для каждого документа, и каждый раз, когда документ обновляется, я "прикасаюсь" к файлу, обновляющему также "время последнего изменения"... но я предполагаю, что это будет медленнее чем дБ и создавать проблемы, когда у меня много пользователей, редактирующих один и тот же документ.

Если у кого-то есть идея или предложение получше, опишите это подробно!

* - - - - - ОБНОВИТЬ - - - - - *

Я определенно решил НЕ нажимать на БД для проверки "отметки времени последнего обновления", не против, если запрос будет довольно быстрым, у (основного) сервера базы данных есть другие задачи для выполнения, не нравится идея увеличить его перегрузку для этой вещи,

Итак, я беру этот путь:

  1. Каждый раз, когда кто-то обновляет документ, я должен что-то делать, чтобы подписать новую метку времени вне среды db, например, не спрашивая db. Мои идеи:
    1. Файловая система: для каждого документа я создаю текстовые файлы empry, названные в качестве идентификатора документа, каждый раз, когда документ обновляется, я "прикасаюсь" к файлу. Я ожидаю иметь тысячи этих пустых файлов.
    2. APC, php-кеш: вероятно, это будет более гибкий способ, чем первый, но мне интересно, если хранение тысяч и тысяч данных на постоянной основе в apc не замедлит сам процесс php или не израсходует память сервера. Я немного боюсь выбрать этот путь.
    3. Другой db, sqlite или mysql (который быстрее и легче с простыми структурами db) используется для хранения только идентификатора документов и временных меток.
  2. Какой бы способ я не выбрал (файлы, apc, sub-db), я серьезно думаю об использовании другого веб-сервера (lighttp?) На поддомене для обработки всех этих.. длинных запросов.

ДАЙТЕ ДРУГОЕ РЕДАКТИРОВАНИЕ:

Путь к файлу не сработает.

APC может быть решением.

Удар по БД также может быть решением, создавая таблицу только для обработки временных меток (только с двумя столбцами, document_id и last_update_timestamp), которые должны быть максимально быстрыми и легкими.

Длинный опрос: это способ, который я выберу, используя lighttpd под apache для загрузки статических файлов (изображения, css, js и т. Д.), И только для этого типа длинного опроса; Это облегчит загрузку apache2, особенно для опроса.

Apache проксирует все эти запросы на lighttpd.

Теперь мне остается только выбрать решение между БД и APC.

ps: спасибо всем, кто уже ответил мне, вы были действительно полезны!

11 ответов

Решение

Я согласен, что я, вероятно, не попал бы в базу данных для этого. Я полагаю, что я бы использовал кэш APC (или какой-либо другой кэш в памяти) для поддержки этой информации. То, что вы описываете - это явно оптимистичная блокировка на уровне подробных записей. Чем выше уровень в структуре базы данных, тем меньше вам нужно иметь дело с. Похоже, вы хотите проверить с несколькими таблицами в структуре.

Я бы поддерживал кэш (в APC) идентификаторов и отметок времени последнего обновленного времени, обозначенного именем таблицы. Так, например, у меня может быть массив имен таблиц, где каждая запись имеет ключ по идентификатору, а фактическим значением является последняя обновленная отметка времени. Вероятно, есть много способов установить это с помощью массивов или других структур, но вы поняли идею. Я бы, вероятно, добавил бы тайм-аут в кэш, чтобы записи в кеше удалялись через определенный промежуток времени, т. Е. Я бы не хотел, чтобы кэш увеличивался, и предполагал, что записи за 1 день больше не нужны).

С этой архитектурой вам нужно будет сделать следующее (в дополнение к настройке APC):

  • при любом обновлении любой (применимой) таблицы обновите запись кэша APC с новой отметкой времени.

  • в ajax просто зайдите "php" назад (чтобы получить кэш APC для проверки записи), а не "назад" в базу данных.

Я думаю, что вы можете использовать условие в выражении UPDATE, например, WHERE ID=? AND LAST_UPDATE=?.

Идея состоит в том, что вам удастся обновить только тогда, когда вы последний раз читаете эту строку. Если кто-то что-то совершил, вы потерпите неудачу, и как только вы узнаете, что потерпели неудачу, вы можете запросить изменения.

Для этого Hibernate использует поле версии. Дайте каждой таблице такое поле и используйте триггер для увеличения его при каждом обновлении. Сохраняя обновление, сравните текущую версию с версией, когда данные были прочитаны ранее. Если они не совпадают, выведите исключение. Используйте транзакции, чтобы сделать проверку и обновление атомарными.

Вам понадобится какой-нибудь тип поля штампа для каждой записи. Что это такое, не имеет значения, если вы можете гарантировать, что внесение любого изменения в запись приведет к тому, что штамп версии будет другим. Рекомендуется проверять и проверять, что штамп версии загруженной записи совпадает со штампом версии в БД, когда пользователь нажимает сохранить, и, если он отличается, обрабатывать его.

Как вы справляетесь с этим, зависит от вас. По крайней мере, вы хотите предложить перезагрузить базу данных, чтобы пользователь мог убедиться, что он все еще хочет сохранить. Один из них - попытаться объединить их изменения с новой записью в БД, а затем попросить их проверить, что объединение работает правильно.

Если вы хотите периодически опрашивать любую БД, способную обрабатывать вашу систему, она должна принять нагрузку опроса. 10 пользователей опрашивают каждые 5 секунд - 2 транзакции в секунду. Это тривиальная нагрузка, и не должно быть никаких проблем. Чтобы средняя нагрузка была близка к фактической нагрузке, просто слегка дрожите время опроса (вместо того, чтобы делать это каждые 5 секунд, например, каждые 4-6 секунд).

Ответ Донни (опрос), вероятно, ваш лучший вариант - простой и работает. Он будет охватывать почти все случаи (маловероятно, что простой поиск по PK повредит производительности даже на очень популярном сайте).

Для полноты и если вы хотите избежать опроса, вы можете использовать push-модель. В статье в Википедии описаны различные способы. Если вы можете поддерживать сквозной кэш (каждый раз, когда вы обновляете запись, вы обновляете кеш), то вы можете почти полностью исключить загрузку базы данных.

Не используйте столбец отметки времени "last_updated". Редактирование в течение одной секунды не является неслыханным. Вы можете обойтись без этого, если добавите дополнительную информацию (сервер, который выполнил обновление, удаленный адрес, порт и т. Д.), Чтобы гарантировать, что, если два запроса поступили в одну и ту же секунду, к одному и тому же серверу, вы сможете обнаружить разницу. Однако если вам нужна такая точность, вы также можете использовать уникальное поле ревизии (оно не обязательно должно быть целым приращением, просто уникальным в пределах срока службы этой записи).

Кто-то упомянул постоянные соединения - это уменьшило бы стоимость установки запросов на опрос (естественно, каждое соединение потребляет ресурсы в базе данных и на хост-машине). Вы должны держать одно соединение (или как можно меньше) открытым постоянно (или как можно дольше) и использовать его (в сочетании с кэшированием и запоминанием, если это необходимо).

Наконец, есть операторы SQL, которые позволяют добавить условие для UPDATE или INSERT. Мой SQl действительно ржавеет, но я думаю, что-то вроде UPDATE ... WHERE ..., Чтобы соответствовать этому уровню защиты, вы должны будете выполнить собственную блокировку строки перед отправкой обновления (и всю обработку ошибок и очистку, которые могут повлечь за собой). Вряд ли вам это понадобится; Я просто упоминаю это для полноты.

Редактировать:

Ваше решение звучит нормально (временные метки кеша, запросы опроса прокси к другому серверу). Единственное изменение, которое я хотел бы сделать, - обновлять кэшированные метки времени при каждом сохранении. Это сохранит кэш более свежим. Я также проверил бы метку времени непосредственно из базы данных при сохранении, чтобы предотвратить проникновение сохранения из-за устаревших данных кэша.

Если вы используете APC для кэширования, то второй HTTP-сервер не имеет смысла - вам придется запускать его на той же машине (APC использует общую память). Эту работу будет выполнять тот же физический компьютер, но с дополнительными издержками второго HTTP-сервера. Если вы хотите отключить загрузку запросов опроса на второй сервер (в вашем случае lighttpd), то было бы лучше настроить lightttpd перед Apache на втором физическом компьютере и использовать общий сервер кэширования (memcache), чтобы Сервер lighttpd может читать кэшированные метки времени, а Apache может обновлять кэшированные метки времени. Основанием для размещения lighttpd перед Apache является, если большинство запросов являются запросами на опрос, во избежание более тяжелого использования процесса Apache.

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

Аааа, хотя это было проще.

Итак, давайте сделаем вывод: у меня есть общая база данных (pgsql или mysql не имеет значения), которая содержит много общих объектов.

У меня есть точная копия этой базы данных в размере $x (на самом деле $x = 200, но она растет, надеюсь, скоро достигнет 1000), и для каждого из них до 20 (в среднем 10) пользователей по 9 часов в день.

Если один из этих пользователей просматривает запись, любую запись, я должен сообщить ему, если кто-то отредактирует ту же запись.

Допустим, Фу смотрит документ 0001, садится за кофе, Бар открывает и редактирует тот же документ, когда Фу возвращается, он должен увидеть "Предупреждение, кто-то еще редактировал этот документ! нажмите здесь, чтобы обновить страницу.

Это все, что мне нужно, возможно, я расширю эту ситуацию, добавив способ увидеть изменения и откат, но это не главное.

Некоторые из вас предложили проверять отметку времени "последнего обновления" только тогда, когда foo пытается сохранить документ; Может быть решение тоже, но мне нужно что-то в режиме реального времени ( 10 секунд задержки).

Длинный опрос, плохой путь, но, кажется, единственный.

Итак, что я сделал:

  1. Установил Lighttp на мою машину (и php5 как fastcgi);
  2. Загруженный прокси-модуль apache2 (ошибка all или 403 вас ударит);
  3. Изменен порт lighttpd с 80 (который используется apache2) на 81;
  4. Настроил apache2 для передачи запроса от mydomain.com/polling/* на polling.mydomain.com (обслуживается Lighttp)
  5. Теперь у меня есть еще один http-сервис, который я буду использовать как для опроса, так и для загрузки статического контента (изображений и т. Д.), Чтобы уменьшить нагрузку на apache2.
  6. Поскольку я не хочу обнулять базу данных для проверки метки времени, я пробовал некоторые системы кэширования (которые можно вызвать из php).
    1. APC: довольно простой в установке и управлении, очень легкий и быстрый, это был бы мой первый выбор... если бы только кеш разделялся между двумя процессами cgi (мне нужно сохранить в кэше значение из процесса php apache2 и прочитать его из php процесса lighttpd)
    2. Memcached: примерно в 4-5 раз медленнее, чем APC, но работает как единый процесс, который можно затронуть везде в моей среде. Я пойду с этим, атм. (даже если медленнее, использование, которое я сделаю, относительно просто).

Теперь мне просто нужно попробовать эту систему, загрузив несколько тестовых данных, чтобы увидеть, как он будет двигаться "под давлением" и оптимизировать его.

Я полагаю, что эта среда будет работать для других ситуаций длительного опроса (чат?)

Спасибо всем, кто дал мне услышать!

Ваш подход к базе данных является лучшим. Если вы делаете это каждые 5 секунд и у вас есть 15 одновременных пользователей, то вы смотрите ~3 запроса в секунду. Это также должен быть очень маленький запрос, возвращающий только одну строку данных. Если ваша база данных не может обрабатывать 3 транзакции в секунду, то вам, возможно, придется посмотреть на лучшую базу данных, потому что 3 запроса в секунду - ничто.

Отметьте время записи в таблице, чтобы вы могли быстро увидеть, изменилось ли что-нибудь, не анализируя каждое поле.

Во-первых, обновляйте только те поля, которые были изменены при записи в базу данных, это уменьшит нагрузку на базу данных.

Во-вторых, запросите метку времени последнего обновления, если у вас есть более старая метка времени, тогда текущая версия в базе данных затем выдаст предупреждение клиенту.

В-третьих, необходимо как-то передать эту информацию клиенту, хотя это и является постоянным соединением с сервером, что позволяет одновременно выполнять двустороннее соединение.

Опрос редко является хорошим решением.
Вы можете выполнить проверку метки только тогда, когда пользователь (с открытым документом) делает что-то активное с документом, например, прокрутку, наведение мыши на него или начинает редактирование. Затем пользователь получает предупреждение, если документ был изменен.

.....
Я знаю, что это было не то, что вы просили, но... почему не синглтон редактирования?
Синглтон может быть столбцом userID в таблице документов.
Если пользователь хочет редактировать документ, документ заблокирован для редактирования другими пользователями.

Или есть edit-singletons на отдельных полях / группах информации.

Только один пользователь может редактировать документ одновременно. Если другой пользователь открыл документ и хочет отредактировать одну проверку метки времени, обнаружите, что документ был изменен и перезагружен.

С одноэлементным опросом нет опроса и только одна временная отметка, когда пользователь "касается" и / или хочет редактировать документ.

Но, возможно, одноэлементный механизм не подходит для вашей системы.

С уважением
Sigersted

Я предлагаю: когда вы впервые запрашиваете запись, которая может быть изменена, повесьте на локальную копию. При "обновлении" сравните копию в заблокированной таблице / строке с вашей копией и, если она была изменена, отправьте ее обратно пользователю.

Это немного не по теме, но вы можете использовать пакет PEAR (или пакет PECL, я забыл, какой) xdiff чтобы отправить хорошее руководство пользователя, когда вы получите столкновение.

Другие вопросы по тегам