Wednesday, May 5, 2021

go-redis using SCAN command in a Redis Cluster



  

In this post we will present how to use redis SCAN command in a cluster environment and go-redis library.

The go-redis library automatically handled a single key commands such as GET, SET. It recognizes the location of each slot on a relevant master, and address the master (or slave) that hold the specific key.

However, the SCAN command is a multi-keys related, and hence the go-redis does not handle it.

The way to handle it is using the ForEachMaster command, and run the SCAN command on each master, and finally aggregate the result. 

Additional item to handle is to maintain a cursor per each master, and detect end of data in all of the masters.

The cursor per master structure is listed below:



type cursorData struct {
locations map[string]uint64
endOfData map[string]bool
}

func (d *cursorData) EndOfData() bool {
for _, end := range d.endOfData {
if !end {
return false
}
}
return true
}



Next we can use the ForEachMaster to run SCAN:


func Scan(
client *redis.ClusterClient,
cursor redisclients.CursorInterface,
match string,
count int64,
) ([]string, redisclients.CursorInterface) {
var cursorPerMaster *cursorData
if cursor == nil {
cursorPerMaster = &cursorData{
locations: make(map[string]uint64),
endOfData: make(map[string]bool),
}
} else {
var ok bool
cursorPerMaster, ok = (cursor).(*cursorData)
if !ok {
panic("conversion failed")
}
}

allKeys := make([]string, 0)
mutex := sync.Mutex{}

err := client.ForEachMaster(context.Background(), func(ctx context.Context, master *redis.Client) error {
key := master.String()

mutex.Lock()
alreadyDone := cursorPerMaster.endOfData[key]
mutex.Unlock()

if alreadyDone {
return nil
}

mutex.Lock()
masterCursor := cursorPerMaster.locations[key]
mutex.Unlock()

cmd := master.Scan(ctx, masterCursor, match, count)
err := cmd.Err()
if err != nil {
return err
}

keys, nextCursor, err := cmd.Result()
if err != nil {
return err
}

mutex.Lock()
allKeys = append(allKeys, keys...)
cursorPerMaster.locations[key] = nextCursor
cursorPerMaster.endOfData[key] = nextCursor == 0
mutex.Unlock()

return nil
})

if err != nil {
panic(err)
}

return allKeys, cursorPerMaster
}



We should hide out implementation using an interface:


type CursorInterface interface {
EndOfData() bool
}



An example for using this API is:


firstTime := true
var cursor redisclients.CursorInterface
for {
var keys []string
count := int64(10000)
match := "*"
if firstTime {
firstTime = false
keys, cursor = Scan(client, nil, match, count)
} else {
keys, cursor = Scan(client, cursor, match, count)
}
fmt.Print(keys)
if cursor.EndOfData() {
return
}
}


In case you want a simpler usage, for debug and test environment only, checkout the KEYS implementation in this post.






No comments:

Post a Comment