Pyramid REST API: как безопасно обрабатывать одновременный доступ к данным?
Я работаю над REST API для веб-сервиса, использующего Pyramid и Cornice; данные на стороне сервера обрабатываются с использованием SQLAlchemy и MySQL. Веб-сервер nginx использует uwsgi, и он настроен для запуска нескольких процессов Python:
[uwsgi]
socket = localhost:6542
plugins = python34
...
processes = 2 # spawn the specified number of workers/processes
threads = 2 # run each worker in prethreaded mode with the specified number of threads
проблема
Предположим, стол customers
на стороне сервера. Используя API, можно читать данные клиентов, изменять их или удалять. В дополнение к этому есть другие функции API, которые читают данные клиента.
Я мог бы сделать несколько вызовов API одновременно, которые затем конкурируют за один и тот же ресурс клиента:
# Write/modify the customer {id} data
curl --request POST ... https://some.host/api/customer/{id}
# Delete customer {id} and all of its associated data
curl --request DELETE https://some.host/api/customer/{id}
# Perform some function which reads customer {id}
curl --request GET ... https://some.host/api/do-work
По сути, это проблема Readers-Writers, но поскольку задействовано более одного процесса, традиционная синхронизация потоков с использованием блокировок / мьютексов / семафоров здесь не будет работать.
Вопрос
Я хотел бы понять, как лучше реализовать блокировку и синхронизацию для такого веб-API на основе Pyramid, чтобы одновременные вызовы, как в приведенном выше примере, обрабатывались безопасно и эффективно (т.е. без ненужной сериализации).
Решения (?)
- Я не думаю, что имеет смысл отмечать / отмечать клиента
{id}
какlocked
потому что SQLAlchemy кеширует такие модификации, иflush()
не кажется достаточно атомным в этом контексте? - В этой статье описывается использование HTTP ETag для управления общими ресурсами.
- Можно также использовать Redis в качестве распределенного диспетчера блокировки для спин-блокировки, чтобы обернуть функцию просмотра?
- А как насчет менеджера транзакций Pyramid?
2 ответа
Я предполагаю, что вы имеете дело с одной базой данных MySQL, и ваши блокировки не должны покрывать другие ресурсы (Redis, сторонние API и т. Д.). Я также предполагаю, что вашим функциям на стороне клиента не нужно работать с данными транзакций (поддерживать сеанс через несколько вызовов API), вы просто хотите предотвратить одновременный доступ API к ошибкам в вашей базе данных.
Существует два вида блокировок: пессимистическая и оптимистическая.
Пессимистическая блокировка - это то, что большинство людей обычно знают по блокировке - вы создаете и приобретаете блокировки заранее, программно в коде. Это и есть менеджер распределенных блокировок.
Оптимистическая блокировка - это то, что вы можете легко избежать с помощью баз данных SQL. Если две транзакции конкурируют из одного и того же ресурса, база данных фактически обрекает одну из транзакций, и среда приложения (в данном случае Pyramid + pyramid_tm) может повторить транзакцию N раз, прежде чем отказаться.
Оптимистическая блокировка является более идеальным решением с точки зрения разработки, так как она не создает когнитивной нагрузки на разработчика приложений, который не забывает правильно блокировать ресурсы или создавать внутренние механизмы блокировки. Вместо этого разработчик полагается на фреймворк и базу данных для повтора и управления ситуациями параллелизма. Однако оптимистическая блокировка не так хорошо известна среди веб-разработчиков, потому что выполнять оптимистическую блокировку в широко распространенных средах PHP сложно из-за отсутствия гибкости в языке программирования.
pyramid_tm
реализует решение для оптимистичной блокировки, и я бы порекомендовал вам использовать его или какое-либо другое решение для оптимистической блокировки, если вы не знаете очень конкретной причины, по которой вы не хотите.
pyramid_tm
связывает жизненный цикл транзакции с HTTP-запросом, что очень естественно с точки зрения веб-разработчикаpyramid_tm
может связать другие события с успешными транзакциями, напримерpyramid_mailer
отправляет электронную почту пользователям, только если транзакции совершаютpyramid_tm
хорошо протестирован и основан на ZODBtransaction
менеджер транзакций, который используется в производстве с начала 2000 годаУбедитесь, что в сеансе SQLAlchemy установлен уровень SERIALIZABLE SQL - вы начинаете с самой высокой модели согласованности. Вы можете снизить это требование к производительности, если знаете, что вызовы API допускают это - например, вызовы, выполняющие статистику только для чтения.
Оптимистическая блокировка обычно работает лучше при "нормальном" большом количестве операций чтения - лишь немногие записывают рабочие нагрузки, где редко возникает конфликт (два вызова API обновляют одного и того же пользователя один раз). Штраф за повторную транзакцию срабатывает только в случае конфликта.
Если транзакция в конечном итоге завершится неудачно после N попыток, например, в необычной ситуации с высокой нагрузкой, это должно быть решено на стороне потребителя API, сообщающей, что данные на стороне сервера изменились, и пользователь должен проверить или снова заполнить форму
дальнейшее чтение
Пример SQLAlchemy + pyramid_tm. Примечание: старайтесь избегать глобального объекта DBSession и использовать
request.dbsession
вместо.
Обычно вы начинаете с определения того, какая модель согласованности является приемлемой. Чем слабее ваши требования согласованности, тем легче становится эта проблема на стороне сервера.
Например:
Можно ли сойти с оптимистичного параллелизма? Т.е. предположить, что у вас есть блокировка, выполнить свою операцию, но определить, когда возникает ситуация с параллелизмом, чтобы вы могли правильно восстановиться? Это может быть хорошим вариантом, если вы не ожидаете много столкновений. Sqlalchemy должен быть в состоянии обнаружить, что он обновляет строку, которая уже была изменена, например.
Если это неприемлемо, вы можете использовать распределенную блокировку в Redis. Возможно, вы могли бы использовать это, чтобы придумать некоторую форму синхронизации.