MM-59932: Migrate remaining caches to Redis (#27880)
- We introduce 2 new APIs: 1. Scan: this allows incremental iteration without blocking the Redis server and is the recommended way to iterate over keys. With this, we have entirely removed the need for Keys. 2. RemoveMulti: this allows deletion of multiple keys in a single operation which optimizes network round trips. - While here, we make a small improvement to GetStatusFromCache, where we remove the shallow copy which wasn't necessary because we always serialize the data from the cache. - We do not use Redis for session cache because of frequent requests to iterate the entire cache which leads to a lot of `SCAN` calls. - Avoid broadcasting status update messages for Redis case. - Setting cache expiry for status cache - Removing .Set method altogether to prevent any chances of setting an item with no expiry. https://mattermost.atlassian.net/browse/MM-59932 ```release-note NONE ```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e5842e67a8
Коммит
a1012d33eb
86
server/platform/services/cache/redis.go
поставляемый
86
server/platform/services/cache/redis.go
поставляемый
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -38,20 +39,7 @@ func NewRedis(opts *CacheOptions, client rueidis.Client) (*Redis, error) {
|
||||
}
|
||||
|
||||
func (r *Redis) Purge() error {
|
||||
// TODO: move to scan
|
||||
keys, err := r.Keys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Del().
|
||||
Key(keys...).
|
||||
Build(),
|
||||
).Error()
|
||||
}
|
||||
|
||||
func (r *Redis) Set(key string, value any) error {
|
||||
return r.SetWithExpiry(key, value, 0)
|
||||
return r.Scan(r.RemoveMulti)
|
||||
}
|
||||
|
||||
// SetWithDefaultExpiry adds the given key and value to the store with the default expiry. If
|
||||
@@ -156,10 +144,9 @@ func (r *Redis) GetMulti(keys []string, values []any) []error {
|
||||
}()
|
||||
|
||||
errs := make([]error, len(keys))
|
||||
newKeys := make([]string, len(keys))
|
||||
for i := range keys {
|
||||
newKeys[i] = r.name + ":" + keys[i]
|
||||
}
|
||||
newKeys := sliceMapper(keys, func(elem string) string {
|
||||
return r.name + ":" + elem
|
||||
})
|
||||
vals, err := r.client.DoCache(context.Background(),
|
||||
r.client.B().Mget().
|
||||
Key(newKeys...).
|
||||
@@ -227,7 +214,7 @@ func (r *Redis) Remove(key string) error {
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Del", elapsed)
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Remove", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -238,22 +225,61 @@ func (r *Redis) Remove(key string) error {
|
||||
).Error()
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache.
|
||||
func (r *Redis) Keys() ([]string, error) {
|
||||
func (r *Redis) RemoveMulti(keys []string) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Keys", elapsed)
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "RemoveMulti", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO: migrate to a function that works on a batch of keys.
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newKeys := sliceMapper(keys, func(elem string) string {
|
||||
return r.name + ":" + elem
|
||||
})
|
||||
|
||||
return r.client.Do(context.Background(),
|
||||
r.client.B().Keys().
|
||||
Pattern(r.name+":*").
|
||||
r.client.B().Del().
|
||||
Key(newKeys...).
|
||||
Build(),
|
||||
).AsStrSlice()
|
||||
).Error()
|
||||
}
|
||||
|
||||
func (r *Redis) Scan(f func([]string) error) error {
|
||||
now := time.Now()
|
||||
defer func() {
|
||||
if r.metrics != nil {
|
||||
elapsed := time.Since(now).Seconds()
|
||||
r.metrics.ObserveRedisEndpointDuration(r.name, "Scan", elapsed)
|
||||
}
|
||||
}()
|
||||
|
||||
var scan rueidis.ScanEntry
|
||||
var err error
|
||||
for more := true; more; more = scan.Cursor != 0 {
|
||||
scan, err = r.client.Do(context.Background(),
|
||||
r.client.B().Scan().
|
||||
Cursor(scan.Cursor).
|
||||
Match(r.name+":*").
|
||||
Count(100).
|
||||
Build()).AsScanEntry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
removed := sliceMapper(scan.Elements, func(elem string) string {
|
||||
return strings.TrimPrefix(elem, r.name+":")
|
||||
})
|
||||
err = f(removed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
@@ -285,3 +311,11 @@ func (r *Redis) GetInvalidateClusterEvent() model.ClusterEvent {
|
||||
func (r *Redis) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func sliceMapper[S ~[]E, E, R any](slice S, mapper func(E) R) []R {
|
||||
newSlice := make([]R, len(slice))
|
||||
for i, v := range slice {
|
||||
newSlice[i] = mapper(v)
|
||||
}
|
||||
return newSlice
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user