35
loading...
This website collects cookies to deliver better user experience
Append()
function. I'll be using Golang and this redis library to do it:func Append(client *redis.Client, key string, elements []string, index int64) error {
length, _ := client.LLen(context.TODO(), key).Result()
if length > index {
_, err := client.LTrim(context.TODO(), key, 0, index-1).Result()
if err != nil {
return err
}
}
_, err := client.RPush(context.TODO(), key, elements).Result()
if err != nil {
return err
}
return nil
}
Append()
operation has 3 sub-operations in it: LLen()
, LTrim()
, RPush()
. Each sub-operation is atomic in itself and contains a whole trip from the application running the code to the Redis cluster. But there is a problem with this approach: the Append()
operation isn't atomic, even though its sub-operations are, which can lead to a few synchronization issues. For example, consider this code:var INITIAL_VALUES = []string{"1", "2", "3", "4", "5"}
var ELEMENTS_TO_APPEND = []string{"4", "5"}
// create client
ctx := context.TODO()
client := redis.NewClient(&redis.Options{
Addr: "0.0.0.0:6379",
Username: "",
Password: "",
DB: 0,
})
// initialize key
key := uuid.NewString()
log.Printf("Initializing %s", key)
client.RPush(ctx, key, INITIAL_VALUES).Result()
// call Append() twice right about the same time
go Append(client, key, ELEMENTS_TO_APPEND, 3) // #1
go Append(client, key, ELEMENTS_TO_APPEND, 3) // #2
// wait for both Append() calls to finish and check result
time.Sleep(time.Second)
elements := client.LRange(ctx, key, 0, -1).Val()
log.Printf("Final elements: %v", elements)
» go run main.go
Initializing 256ceca4-180f-4746-a568-2b0d384038cf
Final elements: [1 2 3 4 5]
» go run main.go
Initializing 34dacaa7-127d-45ca-8b47-78fd74ca01ee
Final elements: [1 2 3 4 5 4 5]
[1 2 3 4 5 4 5]
? That's not correct. How did we end up with that? Let's break the operations down:var APPEND_SCRIPT = `
local length = redis.call('LLEN', KEYS[1])
local upto = tonumber(ARGV[1])
if length > upto then
redis.call('LTRIM', KEYS[1], 0, (upto - 1))
end
local elements = {unpack(ARGV)}
table.remove(elements, 1)
return redis.call('RPUSH', KEYS[1], unpack(elements))
`
func AppendWithScript(client *redis.Client, key string, elements []string, index int64) error {
keys := []string{key}
arguments := buildEvalArguments(elements, index)
_, err := client.Eval(context.TODO(), APPEND_SCRIPT, keys, arguments).Result()
if err != nil {
return err
}
return nil
}
func buildEvalArguments(logs []string, index int64) []interface{} {
arguments := make([]interface{}, len(logs)+1)
arguments[0] = index
for i, v := range logs {
arguments[i+1] = v
}
return arguments
}
func Benchmark__Append(b *testing.B) {
for i := 0; i < b.N; i++ {
Append(client, key, ELEMENTS_TO_APPEND, 3)
}
}
func Benchmark__AppendWithScript(b *testing.B) {
for i := 0; i < b.N; i++ {
AppendWithScript(client, key, ELEMENTS_TO_APPEND, 3)
}
}
» go test -bench=.
goos: darwin
goarch: amd64
pkg: redis-playground
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
Benchmark__Append-8 282 4323202 ns/op
Benchmark__AppendWithScript-8 712 1556933 ns/op
PASS
ok redis-playground 3.124s
Append()
has an average speed of ~4.32ms while AppendWithScript()
executes at ~1.55ms, ~278% times faster.func LoadScript(client *redis.Client) (string, error) {
scriptSha, err := client.ScriptLoad(context.TODO(), APPEND_SCRIPT).Result()
if err != nil {
return "", err
}
return scriptSha, nil
}
func AppendWithLoadedScript(client *redis.Client, scriptSha string, key string, elements []string, index int64) error {
keys := []string{key}
arguments := buildEvalArguments(elements, index)
_, err := client.EvalSha(context.TODO(), scriptSha, keys, arguments).Result()
if err != nil {
return err
}
return nil
}
» go test -bench=.
goos: darwin
goarch: amd64
pkg: redis-playground
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
Benchmark__Append-8 268 4288026 ns/op
Benchmark__AppendWithScript-8 710 1555603 ns/op
Benchmark__AppendWithLoadedScript-8 746 1532615 ns/op
PASS
ok redis-playground 4.381s