Skip to content

coder/redjet

redjet

Go Reference ci Coverage Status Go Report Card

redjet is a high-performance Go library for Redis. Its hallmark feature is a low-allocation, streaming API. See the benchmarks section for more details.

Unlike redigo and go-redis, redjet does not provide a function for every Redis command. Instead, it offers a generic interface that supports all commands and options. While this approach has less type-safety, it provides forward compatibility with new Redis features.

In the aim of both performance and ease-of-use, redjet attempts to provide an API that closely resembles the protocol. For example, the Command method is really a Pipeline of size 1.

Table of Contents

Basic Usage

Install:

go get github.com/coder/redjet@latest

For the most part, you can interact with Redis using a familiar interface:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/coder/redjet"
)

func main() {
    client := redjet.New("localhost:6379")
    ctx := context.Background()

    err := client.Command(ctx, "SET", "foo", "bar").Ok()
    // check error

    got, err := client.Command(ctx, "GET", "foo").Bytes()
    // check error
    // got == []byte("bar")
}

Streaming

To minimize allocations, call (*Pipeline).WriteTo instead of (*Pipeline).Bytes. WriteTo streams the response directly to an io.Writer such as a file or HTTP response.

For example:

_, err := client.Command(ctx, "GET", "big-object").WriteTo(os.Stdout)
// check error

Similarly, you can pass in a value that implements redjet.LenReader to Command to stream larger values into Redis. Unfortunately, the API cannot accept a regular io.Reader because bulk string messages in the Redis protocol are length-prefixed.

Here's an example of streaming a large file into Redis:

bigFile, err := os.Open("bigfile.txt")
// check error
defer bigFile.Close()

stat, err := bigFile.Stat()
// check error

err = client.Command(
    ctx, "SET", "bigfile",
    redjet.NewLenReader(bigFile, stat.Size()),
).Ok()
// check error

If you have no way of knowing the size of your blob in advance and still want to avoid large allocations, you may chunk a stream into Redis using repeated APPEND commands.

Pipelining

redjet supports pipelining via the (*Client).Pipeline method. This method accepts a Pipeline, potentially that of a previous, open command.

// Set foo0, foo1, ..., foo99 to "bar", and confirm that each succeeded.
//
// This entire example only takes one round-trip to Redis!
var p *Pipeline
for i := 0; i < 100; i++ {
    p = client.Pipeline(p, "SET", fmt.Sprintf("foo%d", i), "bar")
}

for r.Next() {
    if err := p.Ok(); err != nil {
        log.Fatal(err)
    }
}
p.Close() // allow the underlying connection to be reused.

PubSub

redjet suports PubSub via the NextSubMessage method. For example:

// Subscribe to a channel
sub := client.Command(ctx, "SUBSCRIBE", "my-channel")
sub.NextSubMessage() // ignore the first message, which is a confirmation of the subscription

// Publish a message to the channel
n, err := client.Command(ctx, "PUBLISH", "my-channel", "hello world").Int()
// check error
// n == 1, since there is one subscriber

// Receive the message
sub.NextSubMessage()
// sub.Payload == "hello world"
// sub.Channel == "my-channel"
// sub.Type == "message"

Note that NextSubMessage will block until a message is received. To interrupt the subscription, cancel the context passed to Command.

Once a connection enters subscribe mode, the internal pool does not re-use it.

It is possible to subscribe to a channel in a performant, low-allocation way via the public API. NextSubMessage is just a convenience method.

JSON

redjet supports convenient JSON encoding and decoding via the (*Pipeline).JSON method. For example:

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// Set a person
// Unknown argument types are automatically encoded to JSON.
err := client.Command(ctx, "SET", "person", Person{
    Name: "Alice",
    Age:  30,
}).Ok()
// check error

// Get a person
var p Person
client.Command(ctx, "GET", "person").JSON(&p)
// check error

// p == Person{Name: "Alice", Age: 30}

Connection Pooling

Redjet provides automatic connection pooling. Configuration knobs exist within the Client struct that may be changed before any Commands are issued.

If you want synchronous command execution over the same connection, use the Pipeline method and consume the Pipeline after each call to Pipeline. Storing a long-lived Pipeline offers the same functionality as storing a long-lived connection.

Benchmarks

On a pure throughput basis, redjet will perform similarly to redigo and go-redis. But, since redjet doesn't allocate memory for the entire response object, it consumes far less resources when handling large responses.

Here are some benchmarks (reproducible via make gen-bench) to illustrate:

.fullname: Get/1_B-10
 │   redjet    │               redigo               │           go-redis            │               rueidis                │
 │   sec/op    │   sec/op     vs base               │   sec/op     vs base          │    sec/op     vs base                │
   908.2n ± 2%   962.4n ± 1%  +5.97% (p=0.000 n=10)   913.8n ± 3%  ~ (p=0.280 n=10)   1045.0n ± 1%  +15.06% (p=0.000 n=10)

 │    redjet     │                redigo                │            go-redis             │               rueidis                │
 │      B/s      │      B/s       vs base               │      B/s       vs base          │     B/s       vs base                │
   1074.2Ki ± 2%   1015.6Ki ± 1%  -5.45% (p=0.000 n=10)   1069.3Ki ± 2%  ~ (p=0.413 n=10)   937.5Ki ± 1%  -12.73% (p=0.000 n=10)

 │  redjet   │            redigo            │           go-redis            │            rueidis            │
 │   B/op    │    B/op     vs base          │    B/op      vs base          │    B/op      vs base          │
   0.00 ± 0%   41.00 ± 0%  ? (p=0.000 n=10)   275.50 ± 2%  ? (p=0.000 n=10)   249.00 ± 0%  ? (p=0.000 n=10)

 │   redjet   │            redigo            │           go-redis           │           rueidis            │
 │ allocs/op  │ allocs/op   vs base          │ allocs/op   vs base          │ allocs/op   vs base          │
   0.000 ± 0%   3.000 ± 0%  ? (p=0.000 n=10)   4.000 ± 0%  ? (p=0.000 n=10)   2.000 ± 0%  ? (p=0.000 n=10)

.fullname: Get/1.0_kB-10
 │   redjet    │               redigo                │              go-redis               │               rueidis               │
 │   sec/op    │   sec/op     vs base                │   sec/op     vs base                │   sec/op     vs base                │
   1.302µ ± 2%   1.802µ ± 1%  +38.42% (p=0.000 n=10)   1.713µ ± 3%  +31.58% (p=0.000 n=10)   1.645µ ± 1%  +26.35% (p=0.000 n=10)

 │    redjet    │                redigo                │               go-redis               │               rueidis                │
 │     B/s      │     B/s       vs base                │     B/s       vs base                │     B/s       vs base                │
   750.4Mi ± 2%   542.1Mi ± 1%  -27.76% (p=0.000 n=10)   570.3Mi ± 3%  -24.01% (p=0.000 n=10)   593.8Mi ± 1%  -20.87% (p=0.000 n=10)

 │    redjet    │             redigo             │            go-redis            │            rueidis             │
 │     B/op     │     B/op      vs base          │     B/op      vs base          │     B/op      vs base          │
   0.000Ki ± 0%   1.039Ki ± 0%  ? (p=0.000 n=10)   1.392Ki ± 0%  ? (p=0.000 n=10)   1.248Ki ± 1%  ? (p=0.000 n=10)

 │   redjet   │            redigo            │           go-redis           │           rueidis            │
 │ allocs/op  │ allocs/op   vs base          │ allocs/op   vs base          │ allocs/op   vs base          │
   0.000 ± 0%   3.000 ± 0%  ? (p=0.000 n=10)   4.000 ± 0%  ? (p=0.000 n=10)   2.000 ± 0%  ? (p=0.000 n=10)

.fullname: Get/1.0_MB-10
 │   redjet    │            redigo             │              go-redis               │            rueidis            │
 │   sec/op    │   sec/op     vs base          │   sec/op     vs base                │   sec/op     vs base          │
   472.5µ ± 7%   477.3µ ± 2%  ~ (p=0.190 n=10)   536.8µ ± 6%  +13.61% (p=0.000 n=10)   475.3µ ± 6%  ~ (p=0.684 n=10)

 │    redjet    │             redigo             │               go-redis               │            rueidis             │
 │     B/s      │     B/s       vs base          │     B/s       vs base                │     B/s       vs base          │
   2.067Gi ± 8%   2.046Gi ± 2%  ~ (p=0.190 n=10)   1.819Gi ± 6%  -11.98% (p=0.000 n=10)   2.055Gi ± 6%  ~ (p=0.684 n=10)

 │   redjet    │                    redigo                    │                   go-redis                   │                   rueidis                    │
 │    B/op     │      B/op        vs base                     │      B/op        vs base                     │      B/op        vs base                     │
   51.00 ± 12%   1047849.50 ± 0%  +2054506.86% (p=0.000 n=10)   1057005.00 ± 0%  +2072458.82% (p=0.000 n=10)   1048808.50 ± 0%  +2056387.25% (p=0.000 n=10)

 │   redjet   │               redigo                │              go-redis               │               rueidis               │
 │ allocs/op  │ allocs/op   vs base                 │ allocs/op   vs base                 │ allocs/op   vs base                 │
   1.000 ± 0%   3.000 ± 0%  +200.00% (p=0.000 n=10)   4.000 ± 0%  +300.00% (p=0.000 n=10)   2.000 ± 0%  +100.00% (p=0.000 n=10)

Limitations

  • redjet does not have convenient support for client side caching. But, the redjet API is flexible enough that a client could implement it themselves by following the instructions here.
  • RESP3 is not supported. Practically, this means that connections aren't multiplexed, and other Redis libraries may perform better in high-concurrency scenarios.
  • Certain features have not been tested but may still work:
    • Redis Streams
    • Monitor