Сначала различают чтение и изменение данных: два чтения сами по себе не создают потерянного обновления. Для потоков одного процесса подходит общий Mutex; для разных процессов с общей БД — атомарный SQL, блокировка строки в транзакции либо optimistic locking с обработкой конфликта. В Rails можно использовать with_lock. Повторы задач требуют идемпотентности; очередь сама по себе не гарантирует эксклюзивный доступ.
Что делать, если два воркера одновременно обращаются к одним данным?
Защиту выбирают по типу операции и месту хранения данных. Mutex не согласует разные процессы, а одна очередь не исключает повторы или параллельную обработку.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Сначала уточняют, что значит «воркер»: поток одного процесса, отдельный процесс или экземпляр на другом сервере. Затем определяют операцию. Два чтения сами по себе не вызывают потерянного обновления, хотя требования к согласованному снимку могут потребовать транзакции. Опасна последовательность «прочитать значение — вычислить новое — записать», если другой исполнитель делает то же самое.
Для общего объекта в памяти потокам одного процесса помогает один и тот же Mutex. Это не межпроцессная блокировка: отдельные экземпляры mutex в разных процессах не согласуют доступ к БД. Thread::Mutex
Для простого увеличения счётчика предпочтителен атомарный SQL вида SET counter = counter + 1, а не вычисление нового значения из устаревшего Ruby-объекта. Rails предоставляет, например, update_counters; такие операции обходят обычные callbacks и validations, что нужно учитывать. Атомарное изменение счётчиков
Если проверка и изменение образуют бизнес-операцию, можно блокировать строку в транзакции:
# stock — неотрицательное число единиц товара
product = Product.find(product_id)
product.with_lock do
raise 'Нет остатка' if product.stock < quantity
product.update!(stock: product.stock - quantity)
end
Здесь quantity заранее проверено как положительное целое. with_lock перечитывает объект с блокировкой до выполнения блока. Все конкурирующие пути изменения должны соблюдать тот же инвариант; произвольная запись устаревшего значения вне протокола всё ещё опасна. Pessimistic locking
При редких конфликтах подходит optimistic locking через lock_version: StaleObjectError обрабатывают, перечитывают запись и заново проверяют бизнес-условия. Повторы ограничивают, а не запускают бесконечно. Optimistic locking
Наконец, блокировка не предотвращает повторное списание при повторной доставке задачи. Нужны идемпотентность и, например, уникальный идентификатор операции с ограничением уникальности в БД. Очередь сама по себе не гарантирует ни единственного исполнителя для этих данных, ни выполнения ровно один раз. Рекомендации Sidekiq