Я новичок в 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
, представляется более целесообразным.
'create-lock-var' имеет условие гонки, потому что проверка для var и создание var не являются одной атомной операцией – noisesmith
Да, у меня было это чувство, а также - спасибо за подтверждение! Как я могу сделать эту конкретную операцию атомой? –
Планируете ли вы развернуть несколько экземпляров вашего приложения за балансировщиком нагрузки для обеспечения высокой доступности? Если вы это сделаете, вам нужно переосмыслить свой подход, чтобы использовать распределенную блокировку или изменить свою архитектуру. –