note
This repo takes over from the original omega-red since it received no updates for a long time. and most of the original authors are no longer working on it. This fork is a continuation of the project, with breaking changes.
Rather than implementing a function for each Redis command, Omega Red uses vector-based API:
[:command-as-keyword arg1 arg2 arg3 ...]
To send these commands to Redis, use omega-red.redis/execute
, omega-red.redis/execute-pipeline
or omega-red.redis/transact
functions.
(execute conn [:command + args])
- for single commands(execute-pipeline conn [ [:command1 & args] [:command2 & args]...])
- for pipeline operations - increases performance by sending and reading
multiple commands in one go, but doesn't come with any consistency guarantees(transact conn [ [:command1 & args] [:command2 & args]...])
- for transactions - all commands are executed in a transaction, and if any of them fails, the whole transaction is rolled back.where conn
is an instance of a client component created with omega-red.client/create
.
Jedis' internals are based on sendCommand
method implemented in all connection/connection-like classes. This allows Omega Red to use
the same method to send commands to Redis, while keeping the efficient connection pooling and full Redis protocol support.
Omega Red will automatically serialize and deserialize Clojure data structures using Transit, so you can pass Clojure data structures directly to Redis commands and receive them back when reading.
note
Only basic Clojure datas tructures are supported - strings, numbers, lists, vectors, maps and sets. Currently serializing other types or Java classes is not supported.
To create a client Component, call omega-red.client/create
with an arg map, the following options are accepted:
:uri
- full Redis connection URI:key-prefix
- optional, a string or keywor to prefix all keys used in write & read commands issued by this client (see below):ping-on-start?
- optional, if set to true
, the client will attempt to ping the Redis server on start:connection-pool
- either instance of JedisPoolConfig
or a map which configures the connection pool, the keys and their default values are:
:max-total
- 100, usually a sane default even for small Redis instances:max-idle
- 50% of max-total
:min-idle
- 10% of max-total
:max-wait-millis
- default of -1, meaning wait indefinitely - usually safe to set, but it depends on your setupOnce the component is created and started, you can call omega-red.redis/execute
with the component and a vector of Redis commands, like so:
(ns omega-red.redis-test
(:require [omega-red.redis :as redis]
[omega-red.client :as redis.client]
[com.stuartsierra.component :as component]))
(def client (component/start
(redis.client/create {:uri "redis://localhost:6379"})))
;; simple commands, with transparent clojure data serialization
(redis/execute client [:set "some-data" {:a :map "is" #{"supported"}}])
(redis/execute client [:get "some-data"]) ;; => {:a :map "is" #{"supported"}}
;; no magic here, just clojure data
(redis/execute client [:sadd "some-set" "a" "b" "c"])
(into #{} (reddis/execute client [:smembers "some-set"])) ;; => #{"a" "b" "c"}
;; pipelining:
(redis/execute-pipeline client [[:set "a" "1"]
[:set "b" "2"]
[:set "c" "3"]])
(redis/execute client [:mget "a" "b" "c"]) ; => ["1" "2" "3"]
;; transactions
(redis/transact client [[:set "a" "1"]
[:set "b" "2"]
[:set "c" "3"]])
(redis/execute client [:mget "a" "b" "c"]) ; => ["1" "2" "3"]
;; to help with building keys, a `key` function is provided:
(redis/key "some" :cool "stuff" ) ;; => "some:cool:stuff"
(redis/key :my.domain/thing) ;; => "my.domain/thing"
;; this way you can do things like this:
(redis/execute-pipeline [[:get (redis/key ::widgets some-id)]
[:hmgetall (redis/key ::counters)]])
Redis' specs use the term 'token' to describe the arguments of a command. Some commands support named arguments, such as SET
's EX
or NX
.
To make working with the DSL more convinient Omega Red supports passing these tokens as strings or keywords, so you can write:
(redis/execute con [:set "foobar" :ex 10]) ;; => "OK"
Enforcing consistent key prefixes is often used when several applications share the same Redis instance. It's also helpful if you need to version your keys or separate them by environment/workload to avoid collisions.
When key prefixing is configred, Omega Red will figure out for you which parts of Redis commands are keys and will apply the prefix automatically.
warning
Automatic key prefixing is implemented by using Redis' own command specification to figure out which arguments are keys, however it's not perfect
Due to inconsistencies of Redis specs and general lack of information how command processing should be implemented
100% command coverage is not guaranteed. If you find a command that doesn't work as expected, please file an issue.
Currently known commands that are not prefixed are EVAL
, SCRIPT
as and others. To find out supported commands
eval (sort (keys omega-red.redis.command/key-processors))
in the REPL.
Auto-prefixing is enabled by setting :key-prefix
in options map when creating the client component:
(ns omega-red.redis-test
(:require [omega-red.redis :as redis]
[omega-red.client :as redis.client]
[com.stuartsierra.component :as component]))
(def srv1-client (component/start
(redis.client/redis-client {:uri "redis://localhost:6379"
:key-prefix "srv1"})))
(def srv2-client (component/start
(redis.client/redis-client {:uri "redis://localhost:6379"
:key-prefix ::srv2})))
(redis/execute srv1-client [:set "foo" "1"]) ;; => "OK", would set key "srv1:foo"
(redis/execute srv2-client [:set "foo" "2"]) ;; => "OK", would set key "srv2:foo"
;; HOWEVER:
(redis/execute srv1-client [:keys "foo*"]) ;; => [] - because of autoprefixing!
Omega Red provides helpers for common use cases, such as "return from cache on hit or fetch from data source and populate on miss" workflow.
These helpers are provided by omega-red.cache
namespace.
Example:
(ns omega-red.redis-test
(:require [omega-red.redis :as redis]
[omega-red.cache :as cache]
[omega-red.client :as redis.client]
[com.stuartsierra.component :as component]))
(let [conn (componet/start (redis.client/create {:uri "127.0.0.1:6379"}))
;; caching example
fetch! (fn []
(cache/get-or-fetch conn {:fetch (fn [] (slurp "http://example.com"))
:cache-set (fn [conn fetch-res]
(redis/execute conn [:set "example" fetch-res "EX" 10])
:cache-get (fn [conn]
(redis/execute conn [:get "example"]))}))]
(fetch!) ;; => returns contents of http://example.com as a result of direct call
(fetch!) ;; => pulls from cache
(fetch!) ;; => pulls from cache
(Thread/sleep (* 10 1000)) ;; wait 10s
(fetch!) ;; => makes http request again
;; Convinence function for memoization:
;; memoize-replacement - DATA WILL STICK AROUND UNLESS SOMETHING ELSE DELETES THE KEY
(cache/memoize conn {:key "example.com"
:fetch-fn #(slurp "http://example.com")})
;; memoize with expiry
(cache/memoize conn {:key "example.com"
:fetch-fn #(slurp "http://example.com")
:expiry-s 30}))
A lock Component is provided. It uses Lua scripts to implement locking and unlocking. Implementation is based on Carmine's and jedis-tools implementation.
note
Redis locks are Good Enough :tm: for most use cases, but they are not perfect. They're really effective when using a single Redis instance, however clustered deployments are not guaranteed to behave correctly. Consider a distributed lock implementation based on Consul, Zookeeper or even Postgres-based optimistic locking
Options supported by omega-red.lock/create
:
:lock-key
- the key to use for the lock, e.g "db-migration" or "widget-data-sync", if client has as :key-prefix
set, the prefix will be applied to the lock key:expiry-ms
- the lock expiry time in milliseconds, default is 1 minute, this is the upper bound for how long the lock will be held, even if the process holding it dies:acquire-timeout-ms
- the time to wait for the lock to be acquired, default is 10 seconds:acquire-resolution-ms
- the time to wait between attempts to acquire the lock, default is 100msThe api for working with locks is a set of functions:
(acquire lock)
- acquire the lock for expiry-ms
milliseconds, returns true
if the lock was acquired, false
otherwise. This is a non-blocking call.(acquire-with-timeout lock)
- same as acquire
but will wait for the lock to be available up to acquire-timeout-ms
milliseconds.
(acquire-with-timeout lock {:acquire-timeout-ms 5000})
- allows for overriding the default :acquire-timeout-ms
value.(release lock)
- immediately release the lock, always returns true
(renew lock)
- if the lock instance is the holder, it can be renewed, this will extend the lock expiry time to expiry-ms
milliseconds from nowA set of functions used to inspect the state of lock can be used:
(is-lock-holder? lock)
- check if current instance is the lock holder(lock-expiry-in-ms lock)
- check how long the lock will be held for, in millisecondsFor best practices, you want to acquire the lock just long enough to do the work while the lock is held, and release it as soon as possible. If your code anticipates work taking longer than initial expiry, use renew
to extend the lease.
The locks are re-entrant, meaning a lock holder can acquire the same lock multiple times. Each acquire
call should be matched with a release
call.
with-lock
macro implements simple acquire-with-timeout
+ finally release
pattern. It will return a map of {:status .. :?result }
(require '[omega-red.redis.client]
'[omega-red.lock])
(def sys-map
{:conn (omega-red.client/create {:uri "redis://localhost:6379"})
:lock (component/using
(omega-red.lock/create {:lock-key "my-db-migration-sync-process"})
[:conn])})
;; .... get the system working....
(let [{:keys [lock]} @sys]
(when (omega-red.lock/acquire-with-timeout lock)
(try
;; do your db migration here
(finally
(omega-red.lock/release lock)))))
;; there's a convinient macro for using the lock
(lock/with-lock lock
;; do the db ops here
)
;; => {:status :omega-red.lock/acquired-and-released :result ...} when work was performed
;; or {:status :omega-red.lock/not-acquired } if the lock was not acquired
;; Usage without component
(let [jedis (Jedis. "redis://localhost:6379")
lock (-> (omega-red.lock/create {:lock-key "my-db-migration-sync-process"})
(assoc :conn jedis :lock-id (str "my-lock-" (random-uuid))))]
(when (omega-red.lock/acquire-with-timeout lock)
(try
;; do your db migration here
(finally
(omega-red.lock/release lock)))))
If you can't/don't want to use Component, you can use Omega Red without it. Create an instance of Jedis
or JedisPool
and
pass it to execute
or execute-pipeline
functions under :pool
key:
(import (java.clients.jedis Jedis))
(def client (omega-red.client/create {:uri "<ignore me>"}))
(with-open [jedis (Jedis. "redis://localhost:6379")]
(omega-red.redis/execute {:pool jedis} [:set "foo" "bar"])
(omega-red.redis/execute {:pool jedis} [:get "foo"]))
When :key-prefix
is set, Omega Red will prefix all keys in Redis commands with the value of :key-prefix
- this is safe because Omega Red uses Redis' own command specification to implement key processing.
However, that doesn't apply to certain commands like keys
or scan
- return values of these commands will include a prefix, which might lead to some confusion, see this example:
;; assuming `conn` was created with a key prefix of "bananas":
(r/execute conn [:set "foo:bar" "baz"])
(r/execute conn [:set "foo:baz" "qux"])
;; works as expected:
(r/execute conn [:keys "foo:*"]) ;; => ["bananas:foo:bar" "bananas:foo:baz"]
;; however, if you want to use output of `keys` to do something, you'll need to strip the prefix yourself, otherwise
;; this happens:
(->> (r/execute conn [:keys "foo:*"])
(mapv #(r/execute conn [:type %])))
;; => ["none" "none"]
;; to make this work, you'll need to strip the prefix yourself:
(->> (r/execute conn [:keys "foo:*"])
(mapv #(r/execute conn [:type (str/replace % #"bananas:" "" )])))
;; => ["string" "string"]
2.5.0 - (in progress)
with-lock
macro2.3.0 - bugfix release
2.2.0 - 2025/03/10
2.2.0-SNAPSHOT - 2025/02/26 Breaking changes
2.1.0-SNAPSHOT - 2025/02/07 - Unreleased exploratory version Breaking changes
refactors internals
2.0.0 - 2025/01/09 - Breaking changes:
1.1.0 - 2022/03/08 - Clean up and cache helper
1.0.2 - Dependency updates
1.0.0-SNAPSHOT - Breaking change! Changes signature of execute
to accept a vector, and execute-pipeline
to accept a vector of vectors. This makes it easier to work with variadic Redis commands (hmset
etc) and compose commands
0.1.0- 2019/10/23 - Initial Public Offering
Can you improve this documentation? These fine people already did:
Łukasz Korecki & ŁukaszEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close