Sequel (Ruby), как безопасно увеличивать и использовать счетчик БД?

Я нашел 4 "правильных" способа сделать это:

  1. В шпаргалке для пользователей ActiveRecord предполагаются заменители ActiveRecord increment и increment_counter быть album.values[:column] -= 1 # or += 1 for increment и album.update(:counter_name=>Sequel.+(:counter_name, 1))
  2. В решении SO update_sql предлагается тот же эффект s[:query_volume].update_sql(:queries => Sequel.expr(3) + :queries)
  3. В случайной ветке я нашел вот это dataset.update_sql(:exp => 'exp + 10'.lit)
  4. В документах Sequels API для обновления я нашел это решение http://sequel.jeremyevans.net/rdoc/classes/Sequel/Dataset.html#method-i-update

однако ни одно из решений фактически не обновляет значение и не возвращает результат безопасным атомарным способом.

Решения, основанные на «добавлении значения, а затем сохранении», должны, на самом деле, недетерминированно давать сбой в многопроцессорных средах, что приводит к таким ошибкам, как:

  1. счетчик альбома равен 0
  2. поток A и поток B извлекают album
  3. поток A и поток B увеличивают значение в хэше/модели/и т. д.
  4. поток A и поток B обновляют счетчик до одного и того же значения
  5. в результате: A и B оба устанавливают счетчик на 1 и работают со значением счетчика 1

Sequel.expr и Sequel.+, с другой стороны, на самом деле не возвращают значение, но Sequel::SQL::NumericExpression и (на самом деле) у вас нет возможности получить его, кроме как выполнить еще один круговой обход БД, что означает, что это может произойти:

  1. счетчик альбома равен 0
  2. поток A и B увеличивают значение, значение увеличивается на 2
  3. поток A и B извлекают строку из БД
  4. в результате: A и B оба устанавливают счетчик на 2 и работают со значением счетчика 2

Итак, за исключением написания пользовательского кода блокировки, какое решение? Если его нет, если не считать написания пользовательского кода блокировки :), как лучше всего это сделать?

Обновление 1

Обычно меня не устраивают ответы о том, что я хочу слишком многого от жизни, как предполагает 1 ответ :)

Альбомы являются просто примером из документов.

Представьте, например, что у вас есть счетчик транзакций в POS-терминале электронной коммерции, который может одновременно принимать 2 транзакции на разных хостах, и в банк вам нужно отправить их с целочисленным счетчиком, уникальным за 24 часа (называемым systan), отправить 2 trx с тем же систаном и 1 будет отклонен, или, что еще хуже, будут предупреждены пробелы в подсчетах (потому что они намекают на «отсутствующие транзакции»), поэтому невозможно использовать значение идентификатора БД.

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

Последовательности DB также не годятся, потому что это означало бы выполнение DDL при добавлении каждого терминала, а здесь мы говорим о тысячах. Даже в моем менее серьезном случае использования DDLing для действий веб-портала по-прежнему является PITA и может даже не работать в зависимости от приведенной ниже схемы кэширования (из-за реализации ActiveRecord и Sequel - и в моем случае я использую оба - может потребоваться перезапуск сервера только зарегистрировать продавца).

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


person bbozo    schedule 02.08.2016    source источник


Ответы (2)


Если вы используете PostgreSQL, вы можете использовать UPDATE RETURNING: DB[:table].returning(:counter).update(:counter => Sequel.expr(1) + :counter)

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

person Jeremy Evans    schedule 02.08.2016
comment
Вау, привет :D спасибо :D это подойдет в моем случае :) Было бы неплохо иметь общий способ сделать это. - person bbozo; 02.08.2016

Ответ таков: в многопоточной среде не используйте счетчики БД. Столкнувшись с этой дилеммой:

  1. Если мне нужен уникальный целочисленный счетчик, используйте потокобезопасный генератор счетчиков, который распределяет счетчики по мере необходимости потоков. Это может быть простое целое число или что-то более сложное, например генератор Twitter Snowflake.
  2. Если мне нужен уникальный идентификатор, я использую что-то вроде uuid

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

Обновление 1:

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

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

person mcfinnigan    schedule 02.08.2016
comment
@bbozo ответил. Надеюсь, кто-то с большим опытом может помочь вам здесь :) - person mcfinnigan; 02.08.2016
comment
Это безумие, что в 2016 году сохранение большого количества счетчиков остается серьезной проблемой. - person bbozo; 02.08.2016