The Redis facade runs Redis commands of your own: a counter, a leaderboard,
a lock that outlives a request, a message to another service. The cache, the
queue, and the rate limiter already talk to Redis through their drivers; the
facade is for everything else. It is Laravel's Redis facade over named
connections, with typed methods for the common commands and the redis
client underneath.
use Redis;
let redis = connection?;
redis.set.await?;
let greeting = redis.get.await?; // Some("hello")
let visits = redis.incr.await?;
Connections
The default connection reaches the server REDIS_URL names, and
redis://127.0.0.1:6379 when it is unset. The database index is the URL's
path: redis://127.0.0.1:6379/2 is database 2. To use other servers or
databases, name them in the bootstrap:
use Redis;
pub async
Redis::define takes redis://, rediss://, and unix:// URLs, with a
user and password when the server needs them. A rediss:// URL connects
over TLS and trusts the certificate authorities of the system; so do the
cache, queue, and rate limiter URLs. Redis::define replaces any
connection of that name, default included. A name already resolved stays
resolved, on the new URL, while the handles taken before keep the server
they had. A URL that is not a Redis URL is an error that names the
connection, never the URL, since the URL may hold a password.
For what a URL cannot say, such as a private certificate authority, a
client certificate, or a server a Sentinel names, build the client with
the re-exported redis crate and give it to
Redis::define_client(name, client):
use Redis;
use ;
let client = build_with_tls?;
define_client;
TLS uses rustls. The framework installs ring as rustls's crypto provider
before the first Redis client opens, unless your application installed one
first.
Redis::connection(name) returns the connection. Resolving it sends
nothing: the connection opens on its first command, and opens again on its
own after the server drops it. Every clone of a connection shares the one
connection to the server.
Redis::connections() lists the names resolved so far.
Redis::purge(name) forgets one; it closes once nothing holds it, and the
next Redis::connection(name) opens a new one. A name that no define
gave, other than default, is an error.
Commands
The common commands are typed methods. Values go in and come out as text:
| Kind | Methods |
|---|---|
| Strings | get, set, set_ex, mget, incr, decr |
| Keys | del, exists, expire, ttl, scan |
| Hashes | hset, hget, hgetall, hdel |
| Lists | lpush, rpush, lpop, rpop, lrange |
| Sets | sadd, srem, smembers |
| Sorted sets | zadd, zrange, zrangebyscore |
| Other | publish, eval |
use Redis;
let redis = connection?;
redis.zadd.await?;
redis.zadd.await?;
let top = redis.zrange.await?;
redis.set_ex.await?;
let keys = redis.scan.await?;
scan walks every cursor and returns every matching key, so the server is
never blocked the way KEYS blocks it.
command runs any other command and returns the reply as a RedisValue:
use ;
let redis = connection?;
redis.command.await?;
if let Int = redis.command.await?
command sends a blocking command, such as BLMPOP or XREAD with
BLOCK, on a connection of its own, as the typed blocking methods do.
WAIT and WAITAOF wait for the writes of the connection that sends them,
so they run on the shared connection and hold it while they wait; keep
their timeouts short.
command refuses the commands that would change the connection every
other command shares, and the error names what to use instead:
subscribe for SUBSCRIBE and its family, transaction for MULTI and
EXEC, the URL for SELECT and AUTH, and a connection of your own from
suprnova::redis::Client for WATCH, MONITOR, CLIENT REPLY, and
CLIENT TRACKING. A pipeline and a transaction refuse them too.
A RedisValue is Nil, Int, Bytes for a bulk string, Status for
a reply such as OK, Array, or, from a server speaking RESP3, Map,
Double, or Bool. as_str and as_int read the common cases. Binary
values go through command, whose arguments are bytes.
For what the facade does not cover, client() gives the connection the
commands use, as the redis crate's ConnectionManager. The crate is
re-exported as suprnova::redis, so you don't need to add it to your
dependencies:
use Redis;
let redis = connection?;
let mut client = redis.client?;
let length: usize = cmd
.arg
.query_async
.await?;
The shared connection waits up to five seconds for a reply. A command that blocks for longer belongs on a connection of its own; see Blocking commands.
Lost connections
When the server drops the connection, a read is sent again on the new
connection: once, and once more for each retry REDIS_COMMAND_RETRIES
adds. The typed reads retry, and so do the reads Laravel lists as safe to
retry when they go through command: GET, MGET, HGETALL, LRANGE,
SMEMBERS, ZRANGE, TTL, EXISTS, and the rest. Laravel's list also
holds three writes, MSET, HMSET, and SET without options; Suprnova
leaves them out. A write is never sent again, since the server may have
applied it before the connection dropped, and a second one would undo a
write another client made in between. It returns the error, and the next
command reaches the new connection. A reply that takes longer than the
five-second limit counts as a lost one too.
Pipelines and transactions
pipeline sends every command its closure queues before it reads a reply,
and returns the replies in order. It saves a round trip per command:
use Redis;
let redis = connection?;
let replies = redis
.pipeline
.await?;
A pipeline is not atomic: another client's commands can run between its
own. transaction sends the same commands inside MULTI and EXEC, so
they run together, and when Redis rejects one as it is queued, an unknown
command or a wrong number of arguments, none of them runs:
use Redis;
let redis = connection?;
let replies = redis
.transaction
.await?;
The closure queues with command and with these typed methods: set,
set_ex, get, del, incr, decr, expire, hset, lpush, rpush,
sadd, zadd, and publish. A pipeline refuses a blocking command, which
would hold up the shared connection; inside a transaction Redis runs one
without blocking. Neither is sent again after a lost connection.
Subscriptions
subscribe and psubscribe open a connection of their own and return a
subscription. Its next waits for the next message:
use Redis;
let redis = connection?;
let mut orders = redis.psubscribe.await?;
spawn;
redis.publish.await?;
The shared connection keeps running commands while a subscription is open.
Dropping the subscription closes its connection. When the server closes
it instead, on a restart, a CLIENT KILL, or a full output buffer, next
opens a new connection and subscribes again, waiting for the server as
long as it takes. Messages published in between are lost, since Pub/Sub
keeps none. next returns None only when the server refuses the new
subscription.
Blocking commands
blpop, brpop, blmove, brpoplpush, bzpopmin, and bzpopmax each run
on a connection of their own, opened for the call. A call that waits never
holds up another command, and it waits for its whole timeout; a zero
timeout waits until something arrives:
use Duration;
use ;
let redis = connection?;
if let Some = redis.blpop.await?
let moved = redis
.blmove
.await?;
Events
Redis::enable_events() reports each command a connection runs to the
listeners Redis::listen adds, with the connection's name, the command, its
arguments, and how long it took. Redis::listen_for_failures hears the
commands that fail, with the error. Events are off until enabled, and
commands inside a pipeline or a transaction are not reported:
use Redis;
listen;
listen_for_failures;
enable_events;
SUBSCRIBE and PSUBSCRIBE are reported too. A command whose reply the
typed method cannot read, such as a GET of a value that is not UTF-8,
reaches the failure listeners, since the caller sees an error. A listener
runs on the task that sent the command, so keep it quick.
Testing
The facade talks to a real server. To test code that uses it, give
default, or the connection the code names, a database of its own in the
test:
use Redis;
async
Connections are process-global, like the other facades. A connection opened
in one test's runtime opens again in the next test's, so every
#[tokio::test] can use it.
Why Suprnova diverges
- Connections are named in the bootstrap. Laravel reads them from
config/database.php;Redis::definedoes the same job in code, with the URL holding the host, port, database, user, and password. - No key prefix. Laravel's phpredis client prefixes the keys of every
command because it knows which arguments are keys.
commandcannot know that, so a prefix would apply to some commands and not others. Put the prefix in the key:format!("myapp:{key}"). - Only reads retry. With
command_retriesset, Laravel sends any command again after a lost connection, writes included, and its list of retryable commands holds three writes. Suprnova never sends a write twice. - Some commands are refused. Every task shares one connection, so
SELECT,MULTI,WATCH,SUBSCRIBE,CLIENT REPLY, and the like would change it for all of them. Laravel runs them on the connection of the process that sends them. - A subscription is a value. Laravel's
subscribetakes a callback and blocks the process; here it returns a subscription that a task reads. - Blocking commands get their own connection. In Laravel they block the connection every other command uses.
- Typed methods take and return text. Binary values go through
commandandclient.
Next
- Cache - caching through the Redis driver
- Queues - the Redis queue driver
- Rate Limiting - limits kept in Redis
