Categorygithub.com/Datadog/rueidis
modulepackage
1.0.25
Repository: https://github.com/datadog/rueidis.git
Documentation: pkg.go.dev

# README

rueidis

Go Reference CircleCI Go Report Card codecov

A fast Golang Redis client that does auto pipelining and supports client side caching.

Features

Getting Started

package main

import (
	"context"
	"github.com/redis/rueidis"
)

func main() {
	client, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}})
	if err != nil {
		panic(err)
	}
	defer client.Close()

	ctx := context.Background()
	// SET key val NX
	err = client.Do(ctx, client.B().Set().Key("key").Value("val").Nx().Build()).Error()
	// HGETALL hm
	hm, err := client.Do(ctx, client.B().Hgetall().Key("hm").Build()).AsStrMap()
}

Checkout more examples: Command Response Cheatsheet

Developer Friendly Command Builder

client.B() is the builder entrypoint to construct a redis command:

Developer friendly command builder
Recorded by @FZambia Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library

Once a command is built, use either client.Do() or client.DoMulti() to send it to redis.

Constructed commands will be recycled to underlying sync.Pool by default and you ❗️SHOULD NOT❗️ reuse them across multiple client.Do() or client.DoMulti() calls. To reuse a command, use Pin() after Build() and it will prevent the command being recycled.

Auto Pipelining

All concurrent non-blocking redis commands (such as GET, SET) are automatically pipelined through connections, which reduces the overall round trips and system calls, and gets higher throughput. You can easily get the benefit of pipelining technique by just calling client.Do() from multiple goroutines. For example:

func BenchmarkPipelining(b *testing.B, client rueidis.Client) {
	// the below client.Do() operations will be issued from
	// multiple goroutines and thus will be pipelined automatically.
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			client.Do(context.Background(), client.B().Get().Key("k").Build()).ToString()
		}
	})
}

Benchmark comparison with go-redis v9

Comparing to go-redis, Rueidis has higher throughput across 1, 8, and 64 parallelism settings.

It is even able to achieve ~14x throughput over go-redis in a local benchmark of Macbook Pro 16" M1 Pro 2021. (see parallelism(64)-key(16)-value(64)-10)

client_test_set

Benchmark source code: https://github.com/rueian/rueidis-benchmark

A benchmark result performed on two GCP n2-highcpu-2 machines also shows that rueidis can achieve higher throughput with lower latencies: https://github.com/redis/rueidis/pull/93

Pipelining Bulk Operations Manually

Though all concurrent non-blocking commands are automatically pipelined, you can still pipeline commands manually with DoMulti():

cmds := make(rueidis.Commands, 0, 10)
for i := 0; i < 10; i++ {
    cmds = append(cmds, client.B().Set().Key("key").Value("value").Build())
}
for _, resp := range client.DoMulti(ctx, cmds...) {
    if err := resp.Error(); err != nil {
        panic(err)
    }
}

Client Side Caching

The opt-in mode of server-assisted client side caching is enabled by default, and can be used by calling DoCache() or DoMultiCache() with pairs of a readonly command and a client side TTL.

client.DoCache(ctx, client.B().Hmget().Key("myhash").Field("1", "2").Cache(), time.Minute).ToArray()
client.DoMultiCache(ctx,
    rueidis.CT(client.B().Get().Key("k1").Cache(), 1*time.Minute),
    rueidis.CT(client.B().Get().Key("k2").Cache(), 2*time.Minute))

Cached responses will be invalidated when being notified by redis or their client side ttl is reached.

Benchmark

Client Side Caching can boost read throughput just like having a redis replica right inside your application:

client_test_get

Benchmark source code: https://github.com/rueian/rueidis-benchmark

Client Side Caching Helpers

Use CacheTTL() to check the remaining client side TTL in seconds:

client.DoCache(ctx, client.B().Get().Key("k1").Cache(), time.Minute).CacheTTL() == 60

Use IsCacheHit() to verify that if the response came from the client side memory:

client.DoCache(ctx, client.B().Get().Key("k1").Cache(), time.Minute).IsCacheHit() == true

MGET/JSON.MGET Client Side Caching Helpers

rueidis.MGetCache and rueidis.JsonMGetCache are handy helpers fetching multiple keys across different slots through the client side caching. They will first group keys by slot to build MGET or JSON.MGET commands respectively and then send requests with only cache missed keys to redis nodes.

Broadcast Mode Client Side Caching

Although the default is opt-in mode, you can use broadcast mode by specifying your prefixes in ClientOption.ClientTrackingOptions:

client, err := rueidis.NewClient(rueidis.ClientOption{
	InitAddress:           []string{"127.0.0.1:6379"},
	ClientTrackingOptions: []string{"PREFIX", "prefix1:", "PREFIX", "prefix2:", "BCAST"},
})
if err != nil {
	panic(err)
}
client.DoCache(ctx, client.B().Get().Key("prefix1:1").Cache(), time.Minute).IsCacheHit() == false
client.DoCache(ctx, client.B().Get().Key("prefix1:1").Cache(), time.Minute).IsCacheHit() == true

Please make sure that commands passed to DoCache() and DoMultiCache() are covered by your prefixes. Otherwise, their client-side cache will not be invalidated by redis.

Client Side Caching with Cache Aside Pattern

Cache-Aside is a widely used pattern to cache other data sources into Redis. For example:

client, err := rueidisaside.NewClient(rueidisaside.ClientOption{
    ClientOption: rueidis.ClientOption{InitAddress: []string{"127.0.0.1:6379"}},
})
if err != nil {
    panic(err)
}
val, err := client.Get(context.Background(), time.Minute, "mykey", func(ctx context.Context, key string) (val string, err error) {
    if err = db.QueryRowContext(ctx, "SELECT val FROM mytab WHERE id = ?", key).Scan(&val); err == sql.ErrNoRows {
        val = "_nil_" // cache nil to avoid penetration.
        err = nil     // clear err in case of sql.ErrNoRows.
    }
    return
})
// ...

Please refer to the full example at rueidisaside.

Disable Client Side Caching

Some Redis provider doesn't support client-side caching, ex. Google Cloud Memorystore. You can disable client-side caching by setting ClientOption.DisableCache to true. This will also fall back client.DoCache() and client.DoMultiCache() to client.Do() and client.DoMulti().

Context Cancellation

client.Do(), client.DoMulti(), client.DoCache() and client.DoMultiCache() can return early if the context is canceled or the deadline is reached.

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
client.Do(ctx, client.B().Set().Key("key").Value("val").Nx().Build()).Error() == context.DeadlineExceeded

Please note that though operations can return early, the command is likely sent already.

Pub/Sub

To receive messages from channels, client.Receive() should be used. It supports SUBSCRIBE, PSUBSCRIBE and Redis 7.0's SSUBSCRIBE:

err = client.Receive(context.Background(), client.B().Subscribe().Channel("ch1", "ch2").Build(), func(msg rueidis.PubSubMessage) {
    // handle the msg
})

The provided handler will be called with received message.

It is important to note that client.Receive() will keep blocking and return only when the following cases:

  1. return nil when received any unsubscribe/punsubscribe message related to the provided subscribe command.
  2. return rueidis.ErrClosing when the client is closed manually.
  3. return ctx.Err() when the ctx is done.
  4. return non-nil err when the provided subscribe command failed.

While the client.Receive() call is blocking, the Client is still able to accept other concurrent requests, and they are sharing the same tcp connection. If your message handler may take some time to complete, it is recommended to use the client.Receive() inside a client.Dedicated() for not blocking other concurrent requests.

Alternative PubSub Hooks

The client.Receive() requires users to provide a subscription command in advance. There is an alternative Dedicatedclient.SetPubSubHooks() allows users to subscribe/unsubscribe channels later.

c, cancel := client.Dedicate()
defer cancel()

wait := c.SetPubSubHooks(rueidis.PubSubHooks{
	OnMessage: func(m rueidis.PubSubMessage) {
		// Handle message. This callback will be called sequentially, but in another goroutine.
	}
})
c.Do(ctx, c.B().Subscribe().Channel("ch").Build())
err := <-wait // disconnected with err

If the hooks are not nil, the above wait channel is guaranteed to be close when the hooks will not be called anymore, and produce at most one error describing the reason. Users can use this channel to detect disconnection.

CAS Pattern

To do a CAS operation (WATCH + MULTI + EXEC), a dedicated connection should be used, because there should be no unintentional write commands between WATCH and EXEC. Otherwise, the EXEC may not fail as expected.

client.Dedicated(func(c rueidis.DedicatedClient) error {
    // watch keys first
    c.Do(ctx, c.B().Watch().Key("k1", "k2").Build())
    // perform read here
    c.Do(ctx, c.B().Mget().Key("k1", "k2").Build())
    // perform write with MULTI EXEC
    c.DoMulti(
        ctx,
        c.B().Multi().Build(),
        c.B().Set().Key("k1").Value("1").Build(),
        c.B().Set().Key("k2").Value("2").Build(),
        c.B().Exec().Build(),
    )
    return nil
})

Or use Dedicate() and invoke cancel() when finished to put the connection back to the pool.

c, cancel := client.Dedicate()
defer cancel()

c.Do(ctx, c.B().Watch().Key("k1", "k2").Build())
// do the rest CAS operations with the `client` who occupying a connection

However, occupying a connection is not good in terms of throughput. It is better to use Lua script to perform optimistic locking instead.

Memory Consumption Consideration

Each underlying connection in rueidis allocates a ring buffer for pipelining. Its size is controlled by the ClientOption.RingScaleEachConn and the default value is 10 which results into each ring of size 2^10.

If you have many rueidis connections, you may find that they occupy quite amount of memory. In that case, you may consider reducing ClientOption.RingScaleEachConn to 8 or 9 at the cost of potential throughput degradation.

You may also consider setting the value of ClientOption.PipelineMultiplex to -1, which will let rueidis use only 1 connection for pipelining to each redis node.

Lua Script

The NewLuaScript or NewLuaScriptReadOnly will create a script which is safe for concurrent usage.

When calling the script.Exec, it will try sending EVALSHA first and fallback to EVAL if the server returns NOSCRIPT.

script := rueidis.NewLuaScript("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}")
// the script.Exec is safe for concurrent call
list, err := script.Exec(ctx, client, []string{"k1", "k2"}, []string{"a1", "a2"}).ToArray()

Redis Cluster, Single Redis and Sentinel

To connect to a redis cluster, the NewClient should be used:

client, err := rueidis.NewClient(rueidis.ClientOption{
    InitAddress: []string{"127.0.0.1:7001", "127.0.0.1:7002", "127.0.0.1:7003"},
    ShuffleInit: true,
})

To connect to a single redis node, still use the NewClient with one InitAddress

client, err := rueidis.NewClient(rueidis.ClientOption{
    InitAddress: []string{"127.0.0.1:6379"},
})

To connect to sentinels, specify the required master set name:

client, err := rueidis.NewClient(rueidis.ClientOption{
    InitAddress: []string{"127.0.0.1:26379", "127.0.0.1:26380", "127.0.0.1:26381"},
    Sentinel: rueidis.SentinelOption{
        MasterSet: "my_master",
    },
})

Redis URL

You can use ParseURL or MustParseURL to construct a ClientOption:

// connect to a redis cluster
client, err = rueidis.NewClient(rueidis.MustParseURL("redis://127.0.0.1:7001?addr=127.0.0.1:7002&addr=127.0.0.1:7003"))
// connect to a redis node
client, err = rueidis.NewClient(rueidis.MustParseURL("redis://127.0.0.1:6379/0"))
// connect to a redis sentinel
client, err = rueidis.NewClient(rueidis.MustParseURL("redis://127.0.0.1:26379/0?master_set=my_master"))

The url must be started with either redis://, rediss:// or unix://.

Currently supported parameters dial_timeout, write_timeout, protocol, client_cache, client_name, max_retries

Arbitrary Command

If you want to construct commands that are absent from the command builder, you can use client.B().Arbitrary():

// This will result into [ANY CMD k1 k2 a1 a2]
client.B().Arbitrary("ANY", "CMD").Keys("k1", "k2").Args("a1", "a2").Build()

Working with JSON, Raw []byte, and Vector Similarity Search

The command builder treats all the parameters as Redis strings, which are binary safe. This means that users can store []byte directly into Redis without conversion. And the rueidis.BinaryString helper can convert []byte to string without copy. For example:

client.B().Set().Key("b").Value(rueidis.BinaryString([]byte{...})).Build()

Treating all the parameters as Redis strings also means that the command builder doesn't do any quoting, conversion automatically for users.

When working with RedisJSON, users frequently need to prepare JSON string in Redis string. And rueidis.JSON can help:

client.B().JsonSet().Key("j").Path("$.myStrField").Value(rueidis.JSON("str")).Build()
// equivalent to
client.B().JsonSet().Key("j").Path("$.myStrField").Value(`"str"`).Build()

When working with vector similarity search, users can use rueidis.VectorString32 and rueidis.VectorString64 to build queries:

cmd := client.B().FtSearch().Index("idx").Query("*=>[KNN 5 @vec $V]").
    Params().Nargs(2).NameValue().NameValue("V", rueidis.VectorString64([]float64{...})).
    Dialect(2).Build()
n, resp, err := client.Do(ctx, cmd).AsFtSearch()

Command Response Cheatsheet

It is hard to remember what message type is returned from redis and which parsing method should be used with. So, here is some common examples:

// GET
client.Do(ctx, client.B().Get().Key("k").Build()).ToString()
client.Do(ctx, client.B().Get().Key("k").Build()).AsInt64()
// MGET
client.Do(ctx, client.B().Mget().Key("k1", "k2").Build()).ToArray()
// SET
client.Do(ctx, client.B().Set().Key("k").Value("v").Build()).Error()
// INCR
client.Do(ctx, client.B().Incr().Key("k").Build()).AsInt64()
// HGET
client.Do(ctx, client.B().Hget().Key("k").Field("f").Build()).ToString()
// HMGET
client.Do(ctx, client.B().Hmget().Key("h").Field("a", "b").Build()).ToArray()
// HGETALL
client.Do(ctx, client.B().Hgetall().Key("h").Build()).AsStrMap()
// ZRANGE
client.Do(ctx, client.B().Zrange().Key("k").Min("1").Max("2").Build()).AsStrSlice()
// ZRANK
client.Do(ctx, client.B().Zrank().Key("k").Member("m").Build()).AsInt64()
// ZSCORE
client.Do(ctx, client.B().Zscore().Key("k").Member("m").Build()).AsFloat64()
// ZRANGE
client.Do(ctx, client.B().Zrange().Key("k").Min("0").Max("-1").Build()).AsStrSlice()
client.Do(ctx, client.B().Zrange().Key("k").Min("0").Max("-1").Withscores().Build()).AsZScores()
// ZPOPMIN
client.Do(ctx, client.B().Zpopmin().Key("k").Build()).AsZScore()
client.Do(ctx, client.B().Zpopmin().Key("myzset").Count(2).Build()).AsZScores()
// SCARD
client.Do(ctx, client.B().Scard().Key("k").Build()).AsInt64()
// SMEMBERS
client.Do(ctx, client.B().Smembers().Key("k").Build()).AsStrSlice()
// LINDEX
client.Do(ctx, client.B().Lindex().Key("k").Index(0).Build()).ToString()
// LPOP
client.Do(ctx, client.B().Lpop().Key("k").Build()).ToString()
client.Do(ctx, client.B().Lpop().Key("k").Count(2).Build()).AsStrSlice()
// SCAN
client.Do(ctx, client.B().Scan().Cursor(0).Build()).AsScanEntry()
// FT.SEARCH
client.Do(ctx, client.B().FtSearch().Index("idx").Query("@f:v").Build()).AsFtSearch()
// GEOSEARCH
client.Do(ctx, client.B().Geosearch().Key("k").Fromlonlat(1, 1).Bybox(1).Height(1).Km().Build()).AsGeosearch()

Supporting Go mod 1.18

To support the old Go 1.18 at least until Go 1.21 comes, there will be a special build tagged with -go1.18 for each release.

Such releases remove RedisResult.AsBytes() and other related functionalities provided by later go versions.

# go.mod
module mymodule

go 1.18

require github.com/redis/rueidis v1.0.25-go1.18

Contributing

Contributions are welcome, including issues, pull requests, and discussions. Contributions mean a lot to us and help us improve this library and the community!

Generate command builders

Command builders are generated based on the definitions in ./hack/cmds by running:

go generate

Testing

Please use the ./dockertest.sh script for running test cases locally. And please try your best to have 100% test coverage on code changes.

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Functions

BinaryString convert the provided []byte into a string without copy.
CT is a shorthand constructor for CacheableTTL.
IsRedisErr is a handy method to check if error is a redis ERR response.
IsRedisNil is a handy method to check if error is a redis nil response.
JSON convert the provided parameter into a JSON string.
JsonMGet is a helper that consults redis directly with multiple keys by grouping keys within same slot into JSON.MGETs or multiple JSON.GETs.
JsonMGetCache is a helper that consults the client-side caches with multiple keys by grouping keys within same slot into multiple JSON.GETs.
JsonMSet is a helper that consults redis directly with multiple keys by grouping keys within same slot into JSON.MSETs or multiple JOSN.SETs.
MDel is a helper that consults the redis directly with multiple keys by grouping keys within same slot into DELs.
MGet is a helper that consults the redis directly with multiple keys by grouping keys within same slot into MGET or multiple GETs.
MGetCache is a helper that consults the client-side caches with multiple keys by grouping keys within same slot into multiple GETs.
MSet is a helper that consults the redis directly with multiple keys by grouping keys within same slot into MSETs or multiple SETs.
MSetNX is a helper that consults the redis directly with multiple keys by grouping keys within same slot into MSETNXs or multiple SETNXs.
No description provided by the author
NewClient uses ClientOption to initialize the Client for both cluster client and single client.
NewLuaScript creates a Lua instance whose Lua.Exec uses EVALSHA and EVAL.
NewLuaScriptReadOnly creates a Lua instance whose Lua.Exec uses EVALSHA_RO and EVAL_RO.
NewSimpleCacheAdapter converts a SimpleCache into CacheStore.
ParseURL parses a redis URL into ClientOption.
ToVector32 reverts VectorString32.
ToVector64 reverts VectorString64.
VectorString32 convert the provided []float32 into a string.
VectorString64 convert the provided []float64 into a string.

# Constants

DefaultCacheBytes is the default value of ClientOption.CacheSizeEachConn, which is 128 MiB.
DefaultDialTimeout is the default value of ClientOption.Dialer.Timeout.
DefaultPoolSize is the default value of ClientOption.BlockingPoolSize.
DefaultReadBuffer is the default value of bufio.NewReaderSize for each connection, which is 0.5MiB.
DefaultRingScale is the default value of ClientOption.RingScaleEachConn, which results into having a ring of size 2^10 for each connection.
DefaultTCPKeepAlive is the default value of ClientOption.Dialer.KeepAlive.
DefaultWriteBuffer is the default value of bufio.NewWriterSize for each connection, which is 0.5MiB.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Variables

ErrClosing means the Client.Close had been called.
ErrDoCacheAborted means redis abort EXEC request or connection closed.
ErrMSetNXNotSet is used in the MSetNX helper when the underlying MSETNX response is 0.
ErrNoAddr means the ClientOption.InitAddress is empty.
ErrNoCache means your redis does not support client-side caching and must set ClientOption.DisableCache to true.
ErrNoSlot indicates that there is no redis node owns the key slot.
No description provided by the author
ErrReplicaOnlyNotSupported means ReplicaOnly flag is not supported by current client.
ErrRESP2PubSubMixed means your redis does not support RESP3 and rueidis can't handle SUBSCRIBE/PSUBSCRIBE/SSUBSCRIBE in mixed case.
Nil represents a Redis Nil message.

# Structs

AuthCredentials is the output of AuthCredentialsFn.
AuthCredentialsContext is the parameter container of AuthCredentialsFn.
CacheableTTL is parameter container of DoMultiCache.
CacheStoreOption will be passed to NewCacheStoreFn.
ClientOption should be passed to NewClient to construct a Client.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
Lua represents a redis lua script.
LuaExec is a single execution unit of Lua.ExecMulti.
PubSubHooks can be registered into DedicatedClient to process pubsub messages without using Client.Receive.
PubSubMessage represent a pubsub message from redis.
PubSubSubscription represent a pubsub "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" event.
RedisMessage is a redis response message, it may be a nil response.
RedisResult is the return struct from Client.Do or Client.DoCache it contains either a redis response or an underlying error (ex.
ScanEntry is the element type of both SCAN, SSCAN, HSCAN and ZSCAN command response.
SentinelOption contains MasterSet,.
XRangeEntry is the element type of both XRANGE and XREVRANGE command response array.
ZScore is the element type of ZRANGE WITHSCORES, ZDIFF WITHSCORES and ZPOPMAX command response.

# Interfaces

CacheEntry should be used to wait for single-flight response when cache missed.
CacheStore is the store interface for the client side caching More detailed interface requirement can be found in cache_test.go.
Client is the redis client interface for both single redis instance and redis cluster.
CoreClient is the minimum interface shared by the Client and the DedicatedClient.
DedicatedClient is obtained from Client.Dedicated() and it will be bound to single redis connection and no other commands can be pipelined in to this connection during Client.Dedicated().
SimpleCache is an alternative interface should be paired with NewSimpleCacheAdapter to construct a CacheStore.

# Type aliases

Builder represents a command builder.
Cacheable represents a completed Redis command which supports server-assisted client side caching, and it should be created by the Cache() of command builder.
Commands is an exported alias to []Completed.
Completed represents a completed Redis command.
Incomplete represents an incomplete Redis command.
NewCacheStoreFn can be provided in ClientOption for using a custom CacheStore implementation.
No description provided by the author
RedisError is an error response or a nil message from redis instance.