From e2266052f2266947987913649f1e9c5b8b5e377c Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Fri, 3 Oct 2025 13:41:20 +0000 Subject: [PATCH 01/11] Initial plan Implement slot-based MGET batching for cluster clients Co-authored-by: SoulPancake <70265851+SoulPancake@users.noreply.github.com> --- helper.go | 74 ++++++++++++++++++++++++++++++++++++++++++-------- helper_test.go | 70 +++++++++++++++++++++++++++++------------------ 2 files changed, 105 insertions(+), 39 deletions(-) diff --git a/helper.go b/helper.go index 333eb7d9..2791fa00 100644 --- a/helper.go +++ b/helper.go @@ -50,12 +50,7 @@ func MGet(client Client, ctx context.Context, keys []string) (ret map[string]Red return clientMGet(client, ctx, client.B().Mget().Key(keys...).Build(), keys) } - cmds := mgetcmdsp.Get(len(keys), len(keys)) - defer mgetcmdsp.Put(cmds) - for i := range cmds.s { - cmds.s[i] = client.B().Get().Key(keys[i]).Build() - } - return doMultiGet(client, ctx, cmds.s, keys) + return clusterMGet(client, ctx, keys) } // MSet is a helper that consults the redis directly with multiple keys by grouping keys within the same slot into MSETs or multiple SETs @@ -139,12 +134,7 @@ func JsonMGet(client Client, ctx context.Context, keys []string, path string) (r return clientMGet(client, ctx, client.B().JsonMget().Key(keys...).Path(path).Build(), keys) } - cmds := mgetcmdsp.Get(len(keys), len(keys)) - defer mgetcmdsp.Put(cmds) - for i := range cmds.s { - cmds.s[i] = client.B().JsonGet().Key(keys[i]).Path(path).Build() - } - return doMultiGet(client, ctx, cmds.s, keys) + return clusterJsonMGet(client, ctx, keys, path) } // JsonMSet is a helper that consults redis directly with multiple keys by grouping keys within the same slot into JSON.MSETs or multiple JSON.SETs @@ -277,6 +267,66 @@ func arrayToKV(m map[string]RedisMessage, arr []RedisMessage, keys []string) map return m } +func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[string]RedisMessage, err error) { + ret = make(map[string]RedisMessage, len(keys)) + slotCmds := intl.MGets(keys) + if len(slotCmds) == 0 { + return ret, nil + } + cmds := make([]Completed, 0, len(slotCmds)) + for _, cmd := range slotCmds { + cmds = append(cmds, cmd.Pin()) + } + resps := client.DoMulti(ctx, cmds...) + defer resultsp.Put(&redisresults{s: resps}) + for i, resp := range resps { + if err := resp.NonRedisError(); err != nil { + return nil, err + } + arr, err := resp.ToArray() + if err != nil { + return nil, err + } + commands := cmds[i].Commands() + cmdKeys := commands[1:] + ret = arrayToKV(ret, arr, cmdKeys) + } + for _, cmd := range cmds { + intl.PutCompletedForce(cmd) + } + return ret, nil +} + +func clusterJsonMGet(client Client, ctx context.Context, keys []string, path string) (ret map[string]RedisMessage, err error) { + ret = make(map[string]RedisMessage, len(keys)) + slotCmds := intl.JsonMGets(keys, path) + if len(slotCmds) == 0 { + return ret, nil + } + cmds := make([]Completed, 0, len(slotCmds)) + for _, cmd := range slotCmds { + cmds = append(cmds, cmd.Pin()) + } + resps := client.DoMulti(ctx, cmds...) + defer resultsp.Put(&redisresults{s: resps}) + for i, resp := range resps { + if err := resp.NonRedisError(); err != nil { + return nil, err + } + arr, err := resp.ToArray() + if err != nil { + return nil, err + } + commands := cmds[i].Commands() + cmdKeys := commands[1 : len(commands)-1] + ret = arrayToKV(ret, arr, cmdKeys) + } + for _, cmd := range cmds { + intl.PutCompletedForce(cmd) + } + return ret, nil +} + // ErrMSetNXNotSet is used in the MSetNX helper when the underlying MSETNX response is 0. // Ref: https://redis.io/commands/msetnx/ var ErrMSetNXNotSet = errors.New("MSETNX: no key was set") diff --git a/helper_test.go b/helper_test.go index f23bdb06..e3c6945c 100644 --- a/helper_test.go +++ b/helper_test.go @@ -179,18 +179,22 @@ func TestMGetCache(t *testing.T) { t.Fatalf("unexpected err %v", err) } t.Run("Delegate DisabledCache DoCache", func(t *testing.T) { - keys := make([]string, 100) - for i := range keys { - keys[i] = strconv.Itoa(i) - } + keys := []string{"{slot1}a", "{slot1}b", "{slot2}a", "{slot2}b"} m.DoMultiFn = func(cmd ...Completed) *redisresults { result := make([]RedisResult, len(cmd)) - for i, key := range keys { - if !reflect.DeepEqual(cmd[i].Commands(), []string{"GET", key}) { - t.Fatalf("unexpected command %v", cmd) + for i, c := range cmd { + // Each command should be MGET with keys from the same slot + commands := c.Commands() + if commands[0] != "MGET" { + t.Fatalf("expected MGET command, got %v", commands) return nil } - result[i] = newResult(strmsg('+', key), nil) + // Build response array with values matching the keys + values := make([]RedisMessage, len(commands)-1) + for j := 1; j < len(commands); j++ { + values[j-1] = strmsg('+', commands[j]) + } + result[i] = newResult(slicemsg('*', values), nil) } return &redisresults{s: result} } @@ -200,7 +204,7 @@ func TestMGetCache(t *testing.T) { } for _, key := range keys { if vKey, ok := v[key]; !ok || vKey.string() != key { - t.Fatalf("unexpected response %v", v) + t.Fatalf("unexpected response for key %s: %v", key, v) } } }) @@ -358,18 +362,22 @@ func TestMGet(t *testing.T) { t.Fatalf("unexpected err %v", err) } t.Run("Delegate Do", func(t *testing.T) { - keys := make([]string, 100) - for i := range keys { - keys[i] = strconv.Itoa(i) - } + keys := []string{"{slot1}a", "{slot1}b", "{slot2}a", "{slot2}b"} m.DoMultiFn = func(cmd ...Completed) *redisresults { result := make([]RedisResult, len(cmd)) - for i, key := range keys { - if !reflect.DeepEqual(cmd[i].Commands(), []string{"GET", key}) { - t.Fatalf("unexpected command %v", cmd) + for i, c := range cmd { + // Each command should be MGET with keys from the same slot + commands := c.Commands() + if commands[0] != "MGET" { + t.Fatalf("expected MGET command, got %v", commands) return nil } - result[i] = newResult(strmsg('+', key), nil) + // Build response array with values matching the keys + values := make([]RedisMessage, len(commands)-1) + for j := 1; j < len(commands); j++ { + values[j-1] = strmsg('+', commands[j]) + } + result[i] = newResult(slicemsg('*', values), nil) } return &redisresults{s: result} } @@ -379,7 +387,7 @@ func TestMGet(t *testing.T) { } for _, key := range keys { if vKey, ok := v[key]; !ok || vKey.string() != key { - t.Fatalf("unexpected response %v", v) + t.Fatalf("unexpected response for key %s: %v", key, v) } } }) @@ -1162,18 +1170,26 @@ func TestJsonMGet(t *testing.T) { t.Fatalf("unexpected err %v", err) } t.Run("Delegate Do", func(t *testing.T) { - keys := make([]string, 100) - for i := range keys { - keys[i] = strconv.Itoa(i) - } + keys := []string{"{slot1}a", "{slot1}b", "{slot2}a", "{slot2}b"} m.DoMultiFn = func(cmd ...Completed) *redisresults { result := make([]RedisResult, len(cmd)) - for i, key := range keys { - if !reflect.DeepEqual(cmd[i].Commands(), []string{"JSON.GET", key, "$"}) { - t.Fatalf("unexpected command %v", cmd) + for i, c := range cmd { + // Each command should be JSON.MGET with keys from the same slot and path at the end + commands := c.Commands() + if commands[0] != "JSON.MGET" { + t.Fatalf("expected JSON.MGET command, got %v", commands) return nil } - result[i] = newResult(strmsg('+', key), nil) + if commands[len(commands)-1] != "$" { + t.Fatalf("expected $ as last parameter, got %v", commands) + return nil + } + // Build response array with values matching the keys (exclude the path) + values := make([]RedisMessage, len(commands)-2) + for j := 1; j < len(commands)-1; j++ { + values[j-1] = strmsg('+', commands[j]) + } + result[i] = newResult(slicemsg('*', values), nil) } return &redisresults{s: result} } @@ -1183,7 +1199,7 @@ func TestJsonMGet(t *testing.T) { } for _, key := range keys { if vKey, ok := v[key]; !ok || vKey.string() != key { - t.Fatalf("unexpected response %v", v) + t.Fatalf("unexpected response for key %s: %v", key, v) } } }) From c79902d5b6547d50011a0c1176b677c8c1bb43ce Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Fri, 10 Oct 2025 09:47:52 +0530 Subject: [PATCH 02/11] feat: optimise slices --- helper.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/helper.go b/helper.go index 2791fa00..510b3818 100644 --- a/helper.go +++ b/helper.go @@ -273,11 +273,14 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str if len(slotCmds) == 0 { return ret, nil } - cmds := make([]Completed, 0, len(slotCmds)) + cmds := mgetcmdsp.Get(len(slotCmds), len(slotCmds)) + defer mgetcmdsp.Put(cmds) + i := 0 for _, cmd := range slotCmds { - cmds = append(cmds, cmd.Pin()) + cmds.s[i] = cmd.Pin() + i++ } - resps := client.DoMulti(ctx, cmds...) + resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) for i, resp := range resps { if err := resp.NonRedisError(); err != nil { @@ -287,12 +290,12 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str if err != nil { return nil, err } - commands := cmds[i].Commands() + commands := cmds.s[i].Commands() cmdKeys := commands[1:] ret = arrayToKV(ret, arr, cmdKeys) } - for _, cmd := range cmds { - intl.PutCompletedForce(cmd) + for i := range cmds.s { + intl.PutCompletedForce(cmds.s[i]) } return ret, nil } @@ -303,11 +306,14 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str if len(slotCmds) == 0 { return ret, nil } - cmds := make([]Completed, 0, len(slotCmds)) + cmds := mgetcmdsp.Get(len(slotCmds), len(slotCmds)) + defer mgetcmdsp.Put(cmds) + i := 0 for _, cmd := range slotCmds { - cmds = append(cmds, cmd.Pin()) + cmds.s[i] = cmd.Pin() + i++ } - resps := client.DoMulti(ctx, cmds...) + resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) for i, resp := range resps { if err := resp.NonRedisError(); err != nil { @@ -317,12 +323,12 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str if err != nil { return nil, err } - commands := cmds[i].Commands() + commands := cmds.s[i].Commands() cmdKeys := commands[1 : len(commands)-1] ret = arrayToKV(ret, arr, cmdKeys) } - for _, cmd := range cmds { - intl.PutCompletedForce(cmd) + for i := range cmds.s { + intl.PutCompletedForce(cmds.s[i]) } return ret, nil } From 8657b769ac98d9d81aef5030e5187959a70bbcf8 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 29 Oct 2025 15:04:29 +0530 Subject: [PATCH 03/11] Apply suggestion from @rueian Co-authored-by: Rueian --- helper.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/helper.go b/helper.go index 510b3818..a6b0025e 100644 --- a/helper.go +++ b/helper.go @@ -283,9 +283,6 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) for i, resp := range resps { - if err := resp.NonRedisError(); err != nil { - return nil, err - } arr, err := resp.ToArray() if err != nil { return nil, err From 7e9862ac5e5f352ff3dcce02e28fb7258a71d29a Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 29 Oct 2025 15:04:39 +0530 Subject: [PATCH 04/11] Apply suggestion from @rueian Co-authored-by: Rueian --- helper.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/helper.go b/helper.go index a6b0025e..1d9bf871 100644 --- a/helper.go +++ b/helper.go @@ -313,9 +313,6 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) for i, resp := range resps { - if err := resp.NonRedisError(); err != nil { - return nil, err - } arr, err := resp.ToArray() if err != nil { return nil, err From 39a9e9f585bc98cbeca9ae46e24379c94325ec81 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Wed, 29 Oct 2025 15:37:59 +0530 Subject: [PATCH 05/11] feat: address comments --- helper.go | 108 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/helper.go b/helper.go index 1d9bf871..e75ce1ef 100644 --- a/helper.go +++ b/helper.go @@ -9,6 +9,69 @@ import ( intl "github.com/redis/rueidis/internal/cmds" ) +var crc16tab = [256]uint16{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, +} + +func crc16(key string) (crc uint16) { + for i := 0; i < len(key); i++ { + crc = (crc << 8) ^ crc16tab[(uint8(crc>>8)^key[i])&0x00FF] + } + return crc +} + +func slot(key string) uint16 { + var s, e int + for ; s < len(key); s++ { + if key[s] == '{' { + break + } + } + if s == len(key) { + return crc16(key) & 16383 + } + for e = s + 1; e < len(key); e++ { + if key[e] == '}' { + break + } + } + if e == len(key) || e == s+1 { + return crc16(key) & 16383 + } + return crc16(key[s+1:e]) & 16383 +} + // MGetCache is a helper that consults the client-side caches with multiple keys by grouping keys within the same slot into multiple GETs func MGetCache(client Client, ctx context.Context, ttl time.Duration, keys []string) (ret map[string]RedisMessage, err error) { if len(keys) == 0 { @@ -269,16 +332,18 @@ func arrayToKV(m map[string]RedisMessage, arr []RedisMessage, keys []string) map func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slotCmds := intl.MGets(keys) - if len(slotCmds) == 0 { - return ret, nil + slotGroups := make(map[uint16][]string) + for _, key := range keys { + ks := slot(key) + slotGroups[ks] = append(slotGroups[ks], key) } - cmds := mgetcmdsp.Get(len(slotCmds), len(slotCmds)) + cmds := mgetcmdsp.Get(0, len(slotGroups)) defer mgetcmdsp.Put(cmds) - i := 0 - for _, cmd := range slotCmds { - cmds.s[i] = cmd.Pin() - i++ + var cmdKeys [][]string + for _, group := range slotGroups { + cmd := client.B().Mget().Key(group...).Build().Pin() + cmds.s = append(cmds.s, cmd) + cmdKeys = append(cmdKeys, group) } resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) @@ -287,9 +352,7 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str if err != nil { return nil, err } - commands := cmds.s[i].Commands() - cmdKeys := commands[1:] - ret = arrayToKV(ret, arr, cmdKeys) + ret = arrayToKV(ret, arr, cmdKeys[i]) } for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) @@ -299,16 +362,21 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str func clusterJsonMGet(client Client, ctx context.Context, keys []string, path string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slotCmds := intl.JsonMGets(keys, path) - if len(slotCmds) == 0 { + slotGroups := make(map[uint16][]string) + for _, key := range keys { + ks := slot(key) + slotGroups[ks] = append(slotGroups[ks], key) + } + if len(slotGroups) == 0 { return ret, nil } - cmds := mgetcmdsp.Get(len(slotCmds), len(slotCmds)) + cmds := mgetcmdsp.Get(0, len(slotGroups)) defer mgetcmdsp.Put(cmds) - i := 0 - for _, cmd := range slotCmds { - cmds.s[i] = cmd.Pin() - i++ + var cmdKeys [][]string + for _, group := range slotGroups { + cmd := client.B().JsonMget().Key(group...).Path(path).Build().Pin() + cmds.s = append(cmds.s, cmd) + cmdKeys = append(cmdKeys, group) } resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) @@ -317,9 +385,7 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str if err != nil { return nil, err } - commands := cmds.s[i].Commands() - cmdKeys := commands[1 : len(commands)-1] - ret = arrayToKV(ret, arr, cmdKeys) + ret = arrayToKV(ret, arr, cmdKeys[i]) } for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) From 2ee5f550616384b58c6b98d42562034a7a3b2373 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 30 Oct 2025 01:25:22 +0530 Subject: [PATCH 06/11] feat: address comment --- helper.go | 63 ++++---------------------------------------------- helper_test.go | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 58 deletions(-) diff --git a/helper.go b/helper.go index 9885a859..079d1624 100644 --- a/helper.go +++ b/helper.go @@ -9,67 +9,14 @@ import ( intl "github.com/redis/rueidis/internal/cmds" ) -var crc16tab = [256]uint16{ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, -} - -func crc16(key string) (crc uint16) { - for i := 0; i < len(key); i++ { - crc = (crc << 8) ^ crc16tab[(uint8(crc>>8)^key[i])&0x00FF] - } - return crc +// Slot computes the Redis Cluster slot for a given key. +// It follows the Redis Cluster specification: https://redis.io/topics/cluster-spec +func Slot(key string) uint16 { + return intl.Slot(key) } func slot(key string) uint16 { - var s, e int - for ; s < len(key); s++ { - if key[s] == '{' { - break - } - } - if s == len(key) { - return crc16(key) & 16383 - } - for e = s + 1; e < len(key); e++ { - if key[e] == '}' { - break - } - } - if e == len(key) || e == s+1 { - return crc16(key) & 16383 - } - return crc16(key[s+1:e]) & 16383 + return intl.Slot(key) } // MGetCache is a helper that consults the client-side caches with multiple keys by grouping keys within the same slot into multiple GETs diff --git a/helper_test.go b/helper_test.go index e3c6945c..536e8440 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1682,3 +1682,46 @@ func TestScannerIter2(t *testing.T) { } }) } + +func TestSlot(t *testing.T) { + tests := []struct { + name string + key string + want uint16 + }{ + { + name: "simple key", + key: "key", + want: 12539, + }, + { + name: "key with hash tag", + key: "{user1000}.following", + want: 3443, + }, + { + name: "key with empty hash tag", + key: "foo{}{bar}", + want: 8363, + }, + { + name: "key with no closing brace", + key: "foo{bar", + want: 15278, + }, + { + name: "same slot keys", + key: "{user1000}.followers", + want: 3443, // Same as {user1000}.following + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Slot(tt.key) + if got != tt.want { + t.Errorf("Slot(%q) = %d, want %d", tt.key, got, tt.want) + } + }) + } +} From 76f0fd95f458e7846971cf7b534d701f18ecc9f1 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 30 Oct 2025 01:52:36 +0530 Subject: [PATCH 07/11] feat: remove Slot public func --- helper.go | 6 ------ helper_test.go | 43 ------------------------------------------- 2 files changed, 49 deletions(-) diff --git a/helper.go b/helper.go index 079d1624..55dbbb8e 100644 --- a/helper.go +++ b/helper.go @@ -9,12 +9,6 @@ import ( intl "github.com/redis/rueidis/internal/cmds" ) -// Slot computes the Redis Cluster slot for a given key. -// It follows the Redis Cluster specification: https://redis.io/topics/cluster-spec -func Slot(key string) uint16 { - return intl.Slot(key) -} - func slot(key string) uint16 { return intl.Slot(key) } diff --git a/helper_test.go b/helper_test.go index 536e8440..e3c6945c 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1682,46 +1682,3 @@ func TestScannerIter2(t *testing.T) { } }) } - -func TestSlot(t *testing.T) { - tests := []struct { - name string - key string - want uint16 - }{ - { - name: "simple key", - key: "key", - want: 12539, - }, - { - name: "key with hash tag", - key: "{user1000}.following", - want: 3443, - }, - { - name: "key with empty hash tag", - key: "foo{}{bar}", - want: 8363, - }, - { - name: "key with no closing brace", - key: "foo{bar", - want: 15278, - }, - { - name: "same slot keys", - key: "{user1000}.followers", - want: 3443, // Same as {user1000}.following - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := Slot(tt.key) - if got != tt.want { - t.Errorf("Slot(%q) = %d, want %d", tt.key, got, tt.want) - } - }) - } -} From cf97d4267b3549ff269de3b3589c83f63d3ce290 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 30 Oct 2025 11:19:13 +0530 Subject: [PATCH 08/11] feat: address comment --- helper.go | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/helper.go b/helper.go index 55dbbb8e..1ef39882 100644 --- a/helper.go +++ b/helper.go @@ -273,18 +273,21 @@ func arrayToKV(m map[string]RedisMessage, arr []RedisMessage, keys []string) map func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slotGroups := make(map[uint16][]string) - for _, key := range keys { - ks := slot(key) - slotGroups[ks] = append(slotGroups[ks], key) + slots := make(map[uint16][]int, len(keys)/2) + for i, key := range keys { + s := slot(key) + slots[s] = append(slots[s], i) } - cmds := mgetcmdsp.Get(0, len(slotGroups)) + cmds := mgetcmdsp.Get(0, len(slots)) defer mgetcmdsp.Put(cmds) - var cmdKeys [][]string - for _, group := range slotGroups { - cmd := client.B().Mget().Key(group...).Build().Pin() - cmds.s = append(cmds.s, cmd) - cmdKeys = append(cmdKeys, group) + groups := make([][]string, 0, len(slots)) + for _, group := range slots { + gkeys := make([]string, 0, len(group)) + for _, i := range group { + gkeys = append(gkeys, keys[i]) + } + cmds.s = append(cmds.s, client.B().Mget().Key(gkeys...).Build().Pin()) + groups = append(groups, gkeys) } resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) @@ -293,7 +296,7 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str if err != nil { return nil, err } - ret = arrayToKV(ret, arr, cmdKeys[i]) + ret = arrayToKV(ret, arr, groups[i]) } for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) @@ -303,21 +306,24 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str func clusterJsonMGet(client Client, ctx context.Context, keys []string, path string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slotGroups := make(map[uint16][]string) - for _, key := range keys { - ks := slot(key) - slotGroups[ks] = append(slotGroups[ks], key) + slots := make(map[uint16][]int, len(keys)/2) + for i, key := range keys { + s := slot(key) + slots[s] = append(slots[s], i) } - if len(slotGroups) == 0 { + if len(slots) == 0 { return ret, nil } - cmds := mgetcmdsp.Get(0, len(slotGroups)) + cmds := mgetcmdsp.Get(0, len(slots)) defer mgetcmdsp.Put(cmds) - var cmdKeys [][]string - for _, group := range slotGroups { - cmd := client.B().JsonMget().Key(group...).Path(path).Build().Pin() - cmds.s = append(cmds.s, cmd) - cmdKeys = append(cmdKeys, group) + groups := make([][]string, 0, len(slots)) + for _, group := range slots { + gkeys := make([]string, 0, len(group)) + for _, i := range group { + gkeys = append(gkeys, keys[i]) + } + cmds.s = append(cmds.s, client.B().JsonMget().Key(gkeys...).Path(path).Build().Pin()) + groups = append(groups, gkeys) } resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) @@ -326,7 +332,7 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str if err != nil { return nil, err } - ret = arrayToKV(ret, arr, cmdKeys[i]) + ret = arrayToKV(ret, arr, groups[i]) } for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) From e4d6b504b7b26d93908f6ecb3fc166cfaf67f612 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Sat, 15 Nov 2025 13:07:14 +0530 Subject: [PATCH 09/11] feat: eliminate temp slice alloc --- helper.go | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/helper.go b/helper.go index 1ef39882..57f8b587 100644 --- a/helper.go +++ b/helper.go @@ -273,18 +273,19 @@ func arrayToKV(m map[string]RedisMessage, arr []RedisMessage, keys []string) map func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slots := make(map[uint16][]int, len(keys)/2) - for i, key := range keys { - s := slot(key) - slots[s] = append(slots[s], i) + slotCount := make(map[uint16]int, len(keys)/2) + for _, key := range keys { + slotCount[slot(key)]++ } - cmds := mgetcmdsp.Get(0, len(slots)) + cmds := mgetcmdsp.Get(0, len(slotCount)) defer mgetcmdsp.Put(cmds) - groups := make([][]string, 0, len(slots)) - for _, group := range slots { - gkeys := make([]string, 0, len(group)) - for _, i := range group { - gkeys = append(gkeys, keys[i]) + groups := make([][]string, 0, len(slotCount)) + for s, count := range slotCount { + gkeys := make([]string, 0, count) + for _, key := range keys { + if slot(key) == s { + gkeys = append(gkeys, key) + } } cmds.s = append(cmds.s, client.B().Mget().Key(gkeys...).Build().Pin()) groups = append(groups, gkeys) @@ -306,21 +307,22 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str func clusterJsonMGet(client Client, ctx context.Context, keys []string, path string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slots := make(map[uint16][]int, len(keys)/2) - for i, key := range keys { - s := slot(key) - slots[s] = append(slots[s], i) + slotCount := make(map[uint16]int, len(keys)/2) + for _, key := range keys { + slotCount[slot(key)]++ } - if len(slots) == 0 { + if len(slotCount) == 0 { return ret, nil } - cmds := mgetcmdsp.Get(0, len(slots)) + cmds := mgetcmdsp.Get(0, len(slotCount)) defer mgetcmdsp.Put(cmds) - groups := make([][]string, 0, len(slots)) - for _, group := range slots { - gkeys := make([]string, 0, len(group)) - for _, i := range group { - gkeys = append(gkeys, keys[i]) + groups := make([][]string, 0, len(slotCount)) + for s, count := range slotCount { + gkeys := make([]string, 0, count) + for _, key := range keys { + if slot(key) == s { + gkeys = append(gkeys, key) + } } cmds.s = append(cmds.s, client.B().JsonMget().Key(gkeys...).Path(path).Build().Pin()) groups = append(groups, gkeys) From 616dd27a1ce43007f7482ff1e997818902f9116a Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Wed, 26 Nov 2025 19:00:56 +0530 Subject: [PATCH 10/11] fix: reduce allocations --- helper.go | 50 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/helper.go b/helper.go index 57f8b587..6cd1e0f3 100644 --- a/helper.go +++ b/helper.go @@ -279,16 +279,20 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str } cmds := mgetcmdsp.Get(0, len(slotCount)) defer mgetcmdsp.Put(cmds) - groups := make([][]string, 0, len(slotCount)) - for s, count := range slotCount { - gkeys := make([]string, 0, count) + for s := range slotCount { + var builder any = client.B().Mget() + var first = true for _, key := range keys { if slot(key) == s { - gkeys = append(gkeys, key) + if first { + builder = builder.(intl.Mget).Key(key) + first = false + } else { + builder = builder.(intl.MgetKey).Key(key) + } } } - cmds.s = append(cmds.s, client.B().Mget().Key(gkeys...).Build().Pin()) - groups = append(groups, gkeys) + cmds.s = append(cmds.s, builder.(intl.MgetKey).Build().Pin()) } resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) @@ -297,7 +301,14 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str if err != nil { return nil, err } - ret = arrayToKV(ret, arr, groups[i]) + s := cmds.s[i].Slot() + var j int + for _, key := range keys { + if slot(key) == s { + ret[key] = arr[j] + j++ + } + } } for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) @@ -316,16 +327,20 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str } cmds := mgetcmdsp.Get(0, len(slotCount)) defer mgetcmdsp.Put(cmds) - groups := make([][]string, 0, len(slotCount)) - for s, count := range slotCount { - gkeys := make([]string, 0, count) + for s := range slotCount { + var builder any = client.B().JsonMget() + var first = true for _, key := range keys { if slot(key) == s { - gkeys = append(gkeys, key) + if first { + builder = builder.(intl.JsonMget).Key(key) + first = false + } else { + builder = builder.(intl.JsonMgetKey).Key(key) + } } } - cmds.s = append(cmds.s, client.B().JsonMget().Key(gkeys...).Path(path).Build().Pin()) - groups = append(groups, gkeys) + cmds.s = append(cmds.s, builder.(intl.JsonMgetKey).Path(path).Build().Pin()) } resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) @@ -334,7 +349,14 @@ func clusterJsonMGet(client Client, ctx context.Context, keys []string, path str if err != nil { return nil, err } - ret = arrayToKV(ret, arr, groups[i]) + s := cmds.s[i].Slot() + var j int + for _, key := range keys { + if slot(key) == s { + ret[key] = arr[j] + j++ + } + } } for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) From 2d0d7c571d909ee7cbb3d0f1df47953f7705f2d3 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Sat, 29 Nov 2025 16:44:16 +0530 Subject: [PATCH 11/11] feat: address comments --- helper.go | 125 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/helper.go b/helper.go index 6cd1e0f3..524b712b 100644 --- a/helper.go +++ b/helper.go @@ -273,43 +273,52 @@ func arrayToKV(m map[string]RedisMessage, arr []RedisMessage, keys []string) map func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slotCount := make(map[uint16]int, len(keys)/2) + if len(keys) == 0 { + return ret, nil + } + + slotIdx := make(map[uint16]int, len(keys)/2) + var builders []any for _, key := range keys { - slotCount[slot(key)]++ + s := slot(key) + idx, ok := slotIdx[s] + if !ok { + idx = len(builders) + slotIdx[s] = idx + builders = append(builders, client.B().Mget()) + } + switch b := builders[idx].(type) { + case intl.Mget: + builders[idx] = b.Key(key) + case intl.MgetKey: + builders[idx] = b.Key(key) + } } - cmds := mgetcmdsp.Get(0, len(slotCount)) + + cmds := mgetcmdsp.Get(0, len(builders)) defer mgetcmdsp.Put(cmds) - for s := range slotCount { - var builder any = client.B().Mget() - var first = true - for _, key := range keys { - if slot(key) == s { - if first { - builder = builder.(intl.Mget).Key(key) - first = false - } else { - builder = builder.(intl.MgetKey).Key(key) - } - } - } - cmds.s = append(cmds.s, builder.(intl.MgetKey).Build().Pin()) + cmds.s = cmds.s[:len(builders)] + for i, b := range builders { + cmds.s[i] = b.(intl.MgetKey).Build().Pin() } + resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) + for i, resp := range resps { - arr, err := resp.ToArray() + builders[i], err = resp.ToArray() if err != nil { return nil, err } - s := cmds.s[i].Slot() - var j int - for _, key := range keys { - if slot(key) == s { - ret[key] = arr[j] - j++ - } - } } + + pos := make([]int, len(builders)) + for _, key := range keys { + idx := slotIdx[slot(key)] + ret[key] = builders[idx].([]RedisMessage)[pos[idx]] + pos[idx]++ + } + for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) } @@ -318,46 +327,52 @@ func clusterMGet(client Client, ctx context.Context, keys []string) (ret map[str func clusterJsonMGet(client Client, ctx context.Context, keys []string, path string) (ret map[string]RedisMessage, err error) { ret = make(map[string]RedisMessage, len(keys)) - slotCount := make(map[uint16]int, len(keys)/2) - for _, key := range keys { - slotCount[slot(key)]++ - } - if len(slotCount) == 0 { + if len(keys) == 0 { return ret, nil } - cmds := mgetcmdsp.Get(0, len(slotCount)) - defer mgetcmdsp.Put(cmds) - for s := range slotCount { - var builder any = client.B().JsonMget() - var first = true - for _, key := range keys { - if slot(key) == s { - if first { - builder = builder.(intl.JsonMget).Key(key) - first = false - } else { - builder = builder.(intl.JsonMgetKey).Key(key) - } - } + + slotIdx := make(map[uint16]int, len(keys)/2) + var builders []any + for _, key := range keys { + s := slot(key) + idx, ok := slotIdx[s] + if !ok { + idx = len(builders) + slotIdx[s] = idx + builders = append(builders, client.B().JsonMget()) + } + switch b := builders[idx].(type) { + case intl.JsonMget: + builders[idx] = b.Key(key) + case intl.JsonMgetKey: + builders[idx] = b.Key(key) } - cmds.s = append(cmds.s, builder.(intl.JsonMgetKey).Path(path).Build().Pin()) } + + cmds := mgetcmdsp.Get(0, len(builders)) + defer mgetcmdsp.Put(cmds) + cmds.s = cmds.s[:len(builders)] + for i, b := range builders { + cmds.s[i] = b.(intl.JsonMgetKey).Path(path).Build().Pin() + } + resps := client.DoMulti(ctx, cmds.s...) defer resultsp.Put(&redisresults{s: resps}) + for i, resp := range resps { - arr, err := resp.ToArray() + builders[i], err = resp.ToArray() if err != nil { return nil, err } - s := cmds.s[i].Slot() - var j int - for _, key := range keys { - if slot(key) == s { - ret[key] = arr[j] - j++ - } - } } + + pos := make([]int, len(builders)) + for _, key := range keys { + idx := slotIdx[slot(key)] + ret[key] = builders[idx].([]RedisMessage)[pos[idx]] + pos[idx]++ + } + for i := range cmds.s { intl.PutCompletedForce(cmds.s[i]) }