2016-08-13 3 views
0

Я новичок в Clojure и пишу веб-приложение. Он включает в себя функцию fn, выполненную на пользователе user-id, которая включает в себя несколько этапов чтения и записи в базу данных и файловую систему. Эти шаги не могут выполняться одновременно несколькими потоками (это приведет к несогласованности базы данных и файловой системы), и я не думаю, что они могут быть выполнены с использованием транзакции базы данных. Однако они специфичны для одного пользователя и поэтому могут выполняться одновременно для разных пользователей.Достижение нескольких замков в clojure

Таким образом, если запрос HTTP сделан для выполнения fn для конкретного user-id мне нужно, чтобы убедиться, что она будет завершена, прежде чем любые запросы HTTP могут выполнять fn для этого user-id

Я пришел с решением который, похоже, работает в REPL, но еще не пробовал его на веб-сервере. Однако, будучи неопытным с Clojure и программированием с резьбой, я не уверен, является ли это хорошим или безопасным способом решения проблемы. Следующий код был разработан методом проб и ошибок и использует функцию locking, которая, похоже, противоречит философии «нет блокировок» Clojure.

(ns locking.core)  

;;; Check if var representing lock exists in namespace 
;;; If not, create it. Creating a new var if one already 
;;; exists seems to break the locking. 
(defn create-lock-var 
    [var-name value] 
    (let [var-sym (symbol var-name)] 
    (do 
     (when (nil? (ns-resolve 'locking.core var-sym)) 
     (intern 'locking.core var-sym value)) 
     ;; Return lock var 
     (ns-resolve 'locking.core var-sym)))) 

;;; Takes an id which represents the lock and the function 
;;; which may only run in one thread at a time for a specific id 
(defn lock-function 
    [lock-id transaction] 
    (let [lock (create-lock-var (str "lock-id-" lock-id) lock-id)] 
    (future 
     (locking lock 
     (transaction))))) 

;;; A function to test the locking 
(defn test-transaction 
    [transaction-count sleep] 
    (dotimes [x transaction-count] 
    (Thread/sleep sleep) 
    (println "performing operation" x))) 

Если открыть три окна в РЕПЛ и выполнять эти функции, она работает

repl1 > (lock-function 1 #(test-transaction 10 1000)) ; executes immediately 
repl2 > (lock-function 1 #(test-transaction 10 1000)) ; waits for repl1 to finish 
repl2 > (lock-function 2 #(test-transaction 10 1000)) ; executes immediately because id=2 

Является ли это надежно? Есть ли лучшие способы решения проблемы?

UPDATE

Как отмечалось, создание переменной блокировки не является атомарной. Я переписал функцию lock-function и, кажется, не работает (нет необходимости в create-lock-var)

(def locks (atom {})) 

(defn lock-transaction 
    [lock-id transaction] 
    (let [lock-key (keyword (str "lock-id-" lock-id))] 
    (do 
     (compare-and-set! locks (dissoc @locks lock-key) (assoc @locks lock-key lock-id)) 
     (future 
     (locking (lock-key @locks) 
      (transaction)))))) 

Примечание: переименовал функцию lock-transaction, представляется более целесообразным.

+0

'create-lock-var' имеет условие гонки, потому что проверка для var и создание var не являются одной атомной операцией – noisesmith

+0

Да, у меня было это чувство, а также - спасибо за подтверждение! Как я могу сделать эту конкретную операцию атомой? –

+0

Планируете ли вы развернуть несколько экземпляров вашего приложения за балансировщиком нагрузки для обеспечения высокой доступности? Если вы это сделаете, вам нужно переосмыслить свой подход, чтобы использовать распределенную блокировку или изменить свою архитектуру. –

ответ

5

Не используйте N vars в пространстве имен, используйте атом, обернутый вокруг 1 хэш-карты, отображающий N символов для N блокировок. Это фиксирует ваше текущее состояние гонки, избегает создания кучи глупых варов и в любом случае легче писать.

+0

Спасибо! Это было очень полезно. Я переписал функцию 'lock-function' и обновил свой вопрос с помощью новой функции. Я надеюсь/думаю, что теперь он атомный. –

1

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

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

Для распределенной блокировки вы можете использовать что-то вроде Zookeeper. Если вы не хотите настраивать целый кластер Zookeeper только для этого, возможно, вы можете пойти на компромисс, используя базу данных Redis (библиотека Carmine дает вам распределенные блокировки из коробки), хотя last time I heard Блокировка Redis не на 100% надежна.

Теперь мне кажется, что блокировка не является особенно требованием, и это не лучший подход, особенно если вы стремитесь к идиоматическому Clojure.Как насчет использования очереди? Некоторые популярные брокеры сообщений JVM (такие как HornetQ и ActiveMQ) дают вам Message Grouping, что гарантирует, что сообщения одного и того же идентификатора группы будут обрабатываться (поочередно) одним и тем же пользователем. Все, что вам нужно сделать, это прослушивание отдельных потоков в правильной очереди и установка идентификатора пользователя в качестве идентификатора группы для ваших сообщений.

HACK: Если вы не хотите настраивать брокера распределенных сообщений, возможно, вы можете обойтись, включив липкие сеансы в балансировщик нагрузки и используя такой брокер сообщений в виртуальной машине.

Кстати, не назовите свою функцию fn :).

+0

Благодарим вас за отзыв! Многие проблемы с блокировкой, которые вы указали, фактически заставили меня реструктурировать старую базу данных, с которой мы работаем. Теперь необходимость блокировок была удалена. –

Смежные вопросы