{"id":19176984,"url":"https://github.com/joomcode/redispipe","last_synced_at":"2025-05-16T02:10:01.713Z","repository":{"id":38897198,"uuid":"116828456","full_name":"joomcode/redispipe","owner":"joomcode","description":"High-throughput Redis client for Go with implicit pipelining","archived":false,"fork":false,"pushed_at":"2025-04-16T18:02:42.000Z","size":329,"stargazers_count":246,"open_issues_count":6,"forks_count":16,"subscribers_count":119,"default_branch":"master","last_synced_at":"2025-04-16T22:39:10.283Z","etag":null,"topics":["go","pipeline","redis"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joomcode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-09T14:44:41.000Z","updated_at":"2025-04-16T18:02:42.000Z","dependencies_parsed_at":"2024-04-26T09:45:38.590Z","dependency_job_id":"9538cd88-ffaa-4990-af64-02157fd3b826","html_url":"https://github.com/joomcode/redispipe","commit_stats":{"total_commits":246,"total_committers":11,"mean_commits":"22.363636363636363","dds":0.1422764227642277,"last_synced_commit":"4dd473806795f3c4528567d7577a6b684f2ada0a"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joomcode%2Fredispipe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joomcode%2Fredispipe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joomcode%2Fredispipe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joomcode%2Fredispipe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joomcode","download_url":"https://codeload.github.com/joomcode/redispipe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254453667,"owners_count":22073618,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","pipeline","redis"],"created_at":"2024-11-09T10:31:30.558Z","updated_at":"2025-05-16T02:10:01.668Z","avatar_url":"https://github.com/joomcode.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# RedisPipe\n\nRedisPipe – is a client for redis that uses \"implicit pipelining\" for highest performance.\n\n[![Github Actions Build Status](https://github.com/joomcode/redispipe/workflows/CI/badge.svg)](https://github.com/joomcode/redispipe/actions)\n[![GoDoc](https://godoc.org/github.com/joomcode/redispipe?status.svg)](https://godoc.org/github.com/joomcode/redispipe)\n[![Report Card](https://goreportcard.com/badge/github.com/joomcode/redispipe)](https://goreportcard.com/report/github.com/joomcode/redispipe)\n\n- [Highlights](#highlights)\n- [Introduction](#introduction)\n- [Performance](#performance)\n- [Limitations](#limitations)\n- [Installation](#installation)\n- [Usage](#usage)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Highlights\n- scalable: the more throughput you try to get, the more efficient it is.\n- cares about redis: redis needs less CPU to perform same throughput.\n- thread-safe: no need to lock around connection, no need to \"return to pool\", etc.\n- pipelining is implicit.\n- transactions are supported (but without `WATCH`).\n- hook for custom logging.\n- hook for request timing reporting.\n\n## Introduction\n\nhttps://redis.io/topics/pipelining\n\nPipelining improves the maximum throughput that redis can serve, and reduces CPU usage both on\nredis server and on the client side. Mostly it comes from saving system CPU consumption.\n\nBut it is not always possible to use pipelining explicitly: usually there are dozens of\nconcurrent goroutines, each sends just one request at a time. To handle the usual workload,\npipelining has to be implicit.\n\n\"Implicit pipelining\" is used in many drivers for other languages:\n- https://github.com/NodeRedis/node_redis , https://github.com/h0x91b/redis-fast-driver ,\n  and probably, other nodejs clients,\n- https://github.com/andrew-bn/RedisBoost - C# connector,\n- some C/C++ clients,\n- all Dart clients ,\n- some Erlang and Elixir clients,\n- https://github.com/informatikr/hedis - Haskel client.\n- http://aredis.sourceforge.net/ - Java client explicitly made for transparent pipelining,\n- https://github.com/lettuce-io/lettuce-core - Java client capable for transparent pipelining,\n- https://github.com/aio-libs/aioredis - Python's async connector, and some of other async\n  python clients\n- Ruby's EventMachine related connectors,\n- etc\n\nAt the moment this connector were created there was no such connector for Golang.\nAll known Golang redis connectors use a connection-per-request model with a connection pool,\nand provide only explicit pipelining.\n\nThis connector was created as implicitly pipelined from the ground up to achieve maximum performance\nin a highly concurrent environment. It writes all requests to single connection to redis, and\ncontinuously reads answers from another goroutine.\n\nNote that it trades a bit of latency for throughput, and therefore could be not optimal for\nlow-concurrent low-request-per-second usage. Write loop latency is configurable as `WritePause`\nparameter in connection options, and could be disabled at all, or increased to higher values\n(150µs is the value used in production, 50µs is default value, -1 disables write pause). Implicit\nruntime latency for switching goroutines still remains, however, and could not be removed.\n\n## Performance\n\n### Single redis\n\n```\ngoos: linux\ngoarch: amd64\npkg: github.com/joomcode/redispipe/rediscluster\ncpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz\nBenchmarkSerialGetSet/radix_pause0-12              17691             63132 ns/op              68 B/op          4 allocs/op\nBenchmarkSerialGetSet/redigo-12            19519             60064 ns/op             239 B/op         13 allocs/op\nBenchmarkSerialGetSet/redispipe-12           504           2661790 ns/op             290 B/op         12 allocs/op\nBenchmarkSerialGetSet/redispipe_pause0-12                  13669             84925 ns/op             208 B/op         12 allocs/op\nBenchmarkParallelGetSet/radix-12                          621036              1817 ns/op              78 B/op          4 allocs/op\nBenchmarkParallelGetSet/redigo-12                           7466            153584 ns/op            4008 B/op         20 allocs/op\nBenchmarkParallelGetSet/redispipe-12                      665428              1599 ns/op             231 B/op         12 allocs/op\n```\n\nYou can see a couple of things:\n- first, redispipe has highest performance in Parallel benchmarks,\n- second, redispipe has lower performance for single-threaded cases.\n\nThat is true: redispipe trades latency for throughput. Every single request has additional\nlatency for hidden batching in a connector. But thanks to batching, more requests can be sent\nto redis and answered by redis in an interval of time.\n\n`SerialGetSet/redispipe_pause0` shows single-threaded results with disabled additional latency\nfor \"batching\" (`WritePause: -1`). This way redispipe is quite close to other connectors in\nperformance, though there is still small overhead of internal design. But I would not recommend\ndisable batching (unless your use case is single threaded), because it increases CPU usage under\nhighly concurrent load both on client and on redis-server.\n\nTo be honestly, github.com/mediocregopher/radix/v3 is also able to perform implicit pipelining\nand does it by default. Therefore it is almost as fast as redispipe in ParallelGetSet.\nSerialGetSet is tested with disabled pipelining, because otherwise it will be as slow as\nredispipe without pause0.\n\n### Cluster\n\n```\ngo test -count 1 -tags=debugredis -run FooBar -bench . -benchmem -benchtime 5s ./rediscluster\ngoos: linux\ngoarch: amd64\npkg: github.com/joomcode/redispipe/rediscluster\nBenchmarkSerialGetSet/radixv2-8           200000    53585 ns/op   1007 B/op   31 allocs/op\nBenchmarkSerialGetSet/redigo-8            200000    40705 ns/op    246 B/op   12 allocs/op\nBenchmarkSerialGetSet/redispipe-8          30000   279838 ns/op    220 B/op   12 allocs/op\nBenchmarkSerialGetSet/redispipe_pause0-8  200000    56356 ns/op    216 B/op   12 allocs/op\nBenchmarkParallelGetSet/radixv2-8        1000000     9245 ns/op   1268 B/op   32 allocs/op\nBenchmarkParallelGetSet/redigo-8         1000000     6886 ns/op    399 B/op   13 allocs/op\nBenchmarkParallelGetSet/redispipe-8      5000000     1636 ns/op    219 B/op   12 allocs/op\n```\n\nWith cluster configuration, internal cluster meta-info management adds additional overhead\ninside of the Go process. And redispipe/rediscluster attempts to provide almost lockless cluster\ninfo handling on the way of request execution.\n\nWhile `redigo` is almost as fast in Parallel tests, it also happens to be limited by Redis's CPU\nusage (three redis processes eats whole 3 cpu cores). It uses a huge number of connections,\nand it is not trivial to recognize non-default setting that should be set to achieve this result\n(both KeepAlive and AliveTime should be set as high as 128).\n( [github.com/chasex/redis-go-cluster](https://github.com/chasex/redis-go-cluster) is used).\n\nEach Redis uses less than 60% CPU core when `redispipe` is used, despite serving more requests.\n\n### Practice\n\nIn practice, performance gain is lesser, because your application does other useful work aside\nfrom sending requests to Redis. But gain is still noticeable. At our setup, we have around 10-15%\nless CPU usage on Redis (ie 50%CPU-\u003e35%CPU), and 5-10% improvement on the client side.\n`WritePause` is usually set to higher value (150µs) than default.\n\n## Limitations\n\n- by default, it is not allowed to send blocking calls, because it will block the whole pipeline:\n  `BLPOP`, `BRPOP`, `BRPOPLPUSH`, `BZPOPMIN`, `BZPOPMAX`, `XREAD`, `XREADGROUP`, `SAVE`.\n  However, you could set `ScriptMode: true` option to enable these commands.\n  `ScriptMode: true` also turns default `WritePause` to -1 (meaning it practically disables forced\n  batching).\n- `WATCH` is also forbidden by default: it is useless and even harmful when concurrent goroutines\n  use the same connection.\n  It is also allowed with `ScriptMode: true`, but you should be sure you use connection only\n  from a single goroutine.\n- `SUBSCRIBE` and `PSUBSCRIBE` commands are forbidden. They switch connection work mode to a\n  completely different mode of communication, therefore it could not be combined with regular\n  commands. This connector doesn't implement subscribing mode.\n\n## Installation\n\n- Single connection: `go get github.com/joomcode/redispipe/redisconn`\n- Cluster connection: `go get github.com/joomcode/redispipe/rediscluster`\n\n## Usage\n\nBoth `redisconn.Connect` and `rediscluster.NewCluster` creates implementations of `redis.Sender`.\n`redis.Sender` provides asynchronous api for sending request/requests/transactions. That api\naccepts `redis.Future` interface implementations as an argument and fullfills it asynchronously.\nUsually you don't need to provide your own `redis.Future` implementation, but rather use\nsynchronous wrappers.\n\nTo use convenient synchronous api, one should wrap \"sender\" with one of wrappers:\n- `redis.Sync{sender}` - provides simple synchronouse api\n- `redis.SyncCtx{sender}` - provides same api, but all methods accepts `context.Context`, and\n  methods returns immediately if that context is closed.\n- `redis.ChanFutured{sender}` - provides api with future through channel closing.\n\nTypes accepted as command arguments: `nil`, `[]byte`, `string`, `int` (and all other integer types),\n`float64`, `float32`, `bool`. All arguments are converted to redis bulk strings as usual (ie\nstring and bytes - as is; numbers - in decimal notation). `bool` converted as \"0/1\",\n`nil` converted to empty string.\n\nIn difference to other redis packages, no custom types are used for request results. Results\nare de-serialized into plain go types and are returned as `interface{}`:\n\nredis        | go\n-------------|-------\nplain string | `string`\nbulk string  | `[]byte`\ninteger      | `int64`\narray        | `[]interface{}`\nerror        | `error` (`*errorx.Error`)\n\nIO, connection, and other errors are not returned separately, but as result (and has same\n`*errorx.Error` underlying type).\n\n```go\npackage redispipe_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/joomcode/redispipe/redis\"\n\t\"github.com/joomcode/redispipe/rediscluster\"\n\t\"github.com/joomcode/redispipe/redisconn\"\n)\n\nconst databaseno = 0\nconst password = \"\"\n\nvar myhandle interface{} = nil\n\nfunc Example_usage() {\n\tctx := context.Background()\n\tcluster := false\n\n\tSingleRedis := func(ctx context.Context) (redis.Sender, error) {\n\t\topts := redisconn.Opts{\n\t\t\tDB:       databaseno,\n\t\t\tPassword: password,\n\t\t\tLogger:   redisconn.NoopLogger{}, // shut up logging. Could be your custom implementation.\n\t\t\tHandle:   myhandle,               // custom data, useful for custom logging\n\t\t\t// Other parameters (usually, no need to change)\n\t\t\t// IOTimeout, DialTimeout, ReconnectTimeout, TCPKeepAlive, Concurrency, WritePause, Async\n\t\t}\n\t\tconn, err := redisconn.Connect(ctx, \"127.0.0.1:6379\", opts)\n\t\treturn conn, err\n\t}\n\n\tClusterRedis := func(ctx context.Context) (redis.Sender, error) {\n\t\topts := rediscluster.Opts{\n\t\t\tHostOpts: redisconn.Opts{\n\t\t\t\t// No DB\n\t\t\t\tPassword: password,\n\t\t\t\t// Usually, no need for special logger\n\t\t\t},\n\t\t\tName:   \"mycluster\",               // name of a cluster\n\t\t\tLogger: rediscluster.NoopLogger{}, // shut up logging. Could be your custom implementation.\n\t\t\tHandle: myhandle,                  // custom data, useful for custom logging\n\t\t\t// Other parameters (usually, no need to change):\n\t\t\t// ConnsPerHost, ConnHostPolicy, CheckInterval, MovedRetries, WaitToMigrate, RoundRobinSeed,\n\t\t}\n\t\taddresses := []string{\"127.0.0.1:20001\"} // one or more of cluster addresses\n\t\tcluster, err := rediscluster.NewCluster(ctx, addresses, opts)\n\t\treturn cluster, err\n\t}\n\n\tvar sender redis.Sender\n\tvar err error\n\tif cluster {\n\t\tsender, err = ClusterRedis(ctx)\n\t} else {\n\t\tsender, err = SingleRedis(ctx)\n\t}\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer sender.Close()\n\n\tsync := redis.SyncCtx{sender} // wrapper for synchronous api\n\n\tres := sync.Do(ctx, \"SET\", \"key\", \"ho\")\n\tif err := redis.AsError(res); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Printf(\"result: %q\\n\", res)\n\n\tres = sync.Do(ctx, \"GET\", \"key\")\n\tif err := redis.AsError(res); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Printf(\"result: %q\\n\", res)\n\n\tres = sync.Send(ctx, redis.Req(\"HMSET\", \"hashkey\", \"field1\", \"val1\", \"field2\", \"val2\"))\n\tif err := redis.AsError(res); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tres = sync.Send(ctx, redis.Req(\"HMGET\", \"hashkey\", \"field1\", \"field2\", \"field3\"))\n\tif err := redis.AsError(res); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfor i, v := range res.([]interface{}) {\n\t\tfmt.Printf(\"%d: %T %q\\n\", i, v, v)\n\t}\n\n\tres = sync.Send(ctx, redis.Req(\"HMGET\", \"key\", \"field1\"))\n\tif err := redis.AsError(res); err != nil {\n\t\tif rerr := redis.AsErrorx(res); rerr != nil \u0026\u0026 rerr.IsOfType(redis.ErrResult) {\n\t\t\tfmt.Printf(\"expected error: %v\\n\", rerr)\n\t\t} else {\n\t\t\tfmt.Printf(\"unexpected error: %v\\n\", err)\n\t\t}\n\t} else {\n\t\tfmt.Printf(\"unexpected missed error\\n\")\n\t}\n\n\tresults := sync.SendMany(ctx, []redis.Request{\n\t\tredis.Req(\"GET\", \"key\"),\n\t\tredis.Req(\"HMGET\", \"hashkey\", \"field1\", \"field3\"),\n\t})\n\t// results is []interface{}, each element is result for corresponding request\n\tfor i, res := range results {\n\t\tfmt.Printf(\"result[%d]: %T %q\\n\", i, res, res)\n\t}\n\n\tresults, err = sync.SendTransaction(ctx, []redis.Request{\n\t\tredis.Req(\"SET\", \"a{x}\", \"b\"),\n\t\tredis.Req(\"SET\", \"b{x}\", 0),\n\t\tredis.Req(\"INCRBY\", \"b{x}\", 3),\n\t})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfor i, res := range results {\n\t\tfmt.Printf(\"tresult[%d]: %T %q\\n\", i, res, res)\n\t}\n\n\t// Output:\n\t// result: \"OK\"\n\t// result: \"ho\"\n\t// 0: []uint8 \"val1\"\n\t// 1: []uint8 \"val2\"\n\t// 2: \u003cnil\u003e %!q(\u003cnil\u003e)\n\t// expected error: WRONGTYPE Operation against a key holding the wrong kind of value (ErrResult {connection: *redisconn.Connection{addr: 127.0.0.1:6379}})\n\t// result[0]: []uint8 \"ho\"\n\t// result[1]: []interface {} [\"val1\" \u003cnil\u003e]\n\t// tresult[0]: string \"OK\"\n\t// tresult[1]: string \"OK\"\n\t// tresult[2]: int64 '\\x03'\n}\n```\n\n## Contributing\n\n- Ask questions in [Issues](https://github.com/joomcode/redispipe/issues)\n- Ask questions on [StackOverflow](https://stackoverflow.com/questions/ask?tags=go+redis).\n- Report about bugs using github [Issues](https://github.com/joomcode/redispipe/issues),\n- Request new features or report about intentions to implement feature using github\n[Issues](https://github.com/joomcode/redispipe/issues),\n- Send [pull requests](https://github.com/joomcode/redispipe/pulls) to fix reported bugs or\nto implement discussed features.\n- Be kind.\n- Be lenient to our misunderstanding of your problem and our unwillingness to bloat library.\n\n## License\n\n[MIT License](https://github.com/joomcode/redispipe/blob/master/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoomcode%2Fredispipe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoomcode%2Fredispipe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoomcode%2Fredispipe/lists"}