كتابة اوامر Redis
احنا كتبنا الـ Serializer و عرفنا ازاي هنقدر نرد علي الـ Client بعد كدا لما يبعتلنا اي commands.
الجزء ده هنبني الـ CommandsHandler و نبدا نكتب شويه commands من Redis.
الـ CommandsHandler
اتكلمنا قبل كدا ان الريكوست اللي هيجيلنا من ال Client هيبقي RESP Array بتقولنا ايه الكوماند اللي هيبعتلنا.
ف احنا دايما الـ Handler عندنا هنعرفه عن طريق الـ command name اللي هو اول عنصر في الـ RESP Array و الباقي هيكون الـ arguments.
ف خلينا نعمل ملف جديد handler.go و نعرف فيه الـ Handler بتاعنا
var Handlers = map[string]func([]Value) Value
و هنكتب دلوقتي اول Command اللي هو الـ PING.
func ping(args []Value) Value {
return Value{typ: "string", str: "PONG"}
}
و هنزود ف الhandler map اللي عملناها الـ PING command ده
var Handlers = map[string]func([]Value) Value{
"PING": ping,
}
لاحظ اننا كتبنا الاسم capital لان الاوامر دي في redis بتبقي case insensitive.
و علشان نجرب PING
command هنبدا نغير في الـ main.go انه يبعت الـ Value object لل handler
for {
resp := NewResp(conn)
value, err := resp.Read()
if err != nil {
fmt.Println(err)
return
}
if value.typ != "array" {
fmt.Println("Invalid request, expected array")
continue
}
if len(value.array) == 0 {
fmt.Println("Invalid request, expected array length > 0")
continue
}
command := strings.ToUpper(value.array[0].bulk)
args := value.array[1:]
writer := NewWriter(conn)
handler, ok := Handlers[command]
if !ok {
fmt.Println("Invalid command: ", command)
writer.Write(Value{typ: "string", str: ""})
continue
}
result := handler(args)
writer.Write(result)
}
اللي احنا عملناه اننا استقبلنا الـ Request الاول واخدنا منه الـ Value object. بعدها عملنا شويه validations علشان نتاكد ان الكوماند ده array و مش فاضي. وبعدها اخدنا اول عنصر في الarray وحولناه uppercase علشان ده هيبقي اسم الcommand. و ال arguments هيبقي باقي عناصر الـarray
ولو الكلام مش واضح بالنسبالك، ف ده شكل Value object لما نقول مثلا SET name Ahmed
Value{
typ: "array",
array: []Value{
Value{typ: "bulk", bulk: "SET"},
Value{typ: "bulk", bulk: "name"},
Value{typ: "bulk", bulk: "Ahmed"},
},
}
بالتالي الكود اللي فوق ده هخلي ال command و الـ args شكلهم كدا
command := Value{typ: "bulk", bulk: "SET"}.bulk // "SET"
args := []Value{
Value{typ: "bulk", bulk: "name"},
Value{typ: "bulk", bulk: "Ahmed"},
}
طيب في Redis الـ PING command بتاخد argument وبترجعه بدل كلمه PONG لو موجود. ف لو جينا نطبق ده
func ping(args []Value) Value {
if len(args) == 0 {
return Value{typ: "string", str: "PONG"}
}
return Value{typ: "string", str: args[0].bulk}
}
الـ SET & GET commands
الخطوات اللي جايه هتبقي كلها سهله و معتمده علي معرفتك بـ Go والـ Datastructures. لان الجزء الصعب احنا خلصناه لما عملنا الـ RESP والـ Handler
الـ Set في redis عباره عن key value. في اي وقت هتعمل SET لـ key بـ value معينه. و ف اي وقت تقدر تجيبها تاني بالـ GET command.
ف الـ Datastructure اللي هنستخدمها هي Hash Map او map[string]string
ف هنروح فالـ handler.go و هنعرف الـ SET command
var SETs = map[string]string{}
var SETsMu = sync.RWMutex{}
func set(args []Value) Value {
if len(args) != 2 {
return Value{typ: "error", str: "ERR wrong number of arguments for 'set' command"}
}
key := args[0].bulk
value := args[1].bulk
SETsMu.Lock()
SETs[key] = value
SETsMu.Unlock()
return Value{typ: "string", str: "OK"}
}
بنستخدم الـ sync.RWMutex لان المفروض ان الـ Server بتاعنا هيستقبل الـ requests بشكل concurrent. و بنستخدم الـ RWMutex علشان نتاكد ان الـ SETs map مش هتتعدل من اكثر من thread في نفس الوقت.
اتوقع انك متخيل الـ GET command هيبقي عامل ازاي. بس بكل بساطه لو لقينا الـ Key هنرجعه .. ولو مش موجود هنرجع nil
func get(args []Value) Value {
if len(args) != 1 {
return Value{typ: "error", str: "ERR wrong number of arguments for 'get' command"}
}
key := args[0].bulk
SETsMu.RLock()
value, ok := SETs[key]
SETsMu.RUnlock()
if !ok {
return Value{typ: "null"}
}
return Value{typ: "bulk", bulk: value}
}
وبعد ما عملنا الـ SET & GET commands هنسجلهم في الـ Handlers map
var Handlers = map[string]func([]Value) Value{
"PING": ping,
"SET": set,
"GET": get,
}
الـ HSET & HGET commands
بكل بساطة دي عبارة عن Hash Map جواها Hash Map. وهي عبارة عن map[string]map[string]string
. بتاخد اسم الـ hash و بعدين الـ key و الـ value. علشان تخزن بيانات زي
{
"users": {
"u1": "Ahmed",
"u2": "Mohamed",
},
"posts": {
"p1": "Hello World",
"p2": "Welcome to my blog",
},
}
ف مثلا لو هنخزن الـ user اللي اسمه Ahmed بـ ID u1. هنستخدم الـ HSET command
HSET users u1 Ahmed
ولو هنخزن الـ post اللي اسمه Hello World بـ ID u1. هنستخدم الـ HSET command
HSET posts u1 Hello World
ولو هنعمل GET لـ user Ahmed هنستخدم الـ HGET command
HGET users u1
الكود هيبقي قريب جدا من الـ SET & GET commands. الفرق بينهم ان الـ HSETs هيكون map[string]map[string]string
var HSETs = map[string]map[string]string{}
var HSETsMu = sync.RWMutex{}
func hset(args []Value) Value {
if len(args) != 3 {
return Value{typ: "error", str: "ERR wrong number of arguments for 'hset' command"}
}
hash := args[0].bulk
key := args[1].bulk
value := args[2].bulk
HSETsMu.Lock()
if _, ok := HSETs[hash]; !ok {
HSETs[hash] = map[string]string{}
}
HSETs[hash][key] = value
HSETsMu.Unlock()
return Value{typ: "string", str: "OK"}
}
والـ HGET هيبقي عامل كدا
func hget(args []Value) Value {
if len(args) != 2 {
return Value{typ: "error", str: "ERR wrong number of arguments for 'hget' command"}
}
hash := args[0].bulk
key := args[1].bulk
HSETsMu.RLock()
value, ok := HSETs[hash][key]
HSETsMu.RUnlock()
if !ok {
return Value{typ: "null"}
}
return Value{typ: "bulk", bulk: value}
}
كدا احنا عملنا الـ HSET & HGET commands. فيه كوماند زياده المره دي عن الـSETS اسمه HGETALL. وده بيرجع كل الـ keys و الـ values في الـ hash
> HGETALL users
u1 Ahmed
u2 Mohamed
ولكن تطبيقه هيبقي سهل لان الـ Value object بيقدر يشيل الـ bulk data و يرجعها كـ map[string]string
func hgetall(args []Value) Value {
if len(args) != 1 {
return Value{typ: "error", str: "ERR wrong number of arguments for 'hgetall' command"}
}
hash := args[0].bulk
HSETsMu.RLock()
value, ok := HSETs[hash]
HSETsMu.RUnlock()
if !ok {
return Value{typ: "null"}
}
values := []Value{}
for k, v := range value {
values = append(values, Value{typ: "bulk", bulk: k})
values = append(values, Value{typ: "bulk", bulk: v})
}
return Value{typ: "array", array: values}
}
بكل بساطه بعد ما نجيب الـ key,value من ال Hash. هنعمل loop علي كل key, value و هنخزنهم في array. وبعدين هنرجع الـ array كـ Value object.
اخر خطوه محتاجينها اننا نسجل الـ commands دي في الـ Handlers map
var Handlers = map[string]func([]Value) Value{
"PING": ping,
"SET": set,
"GET": get,
"HSET": hset,
"HGET": hget,
"HGETALL": hgetall,
}
الخاتمه
لغايه الجزء ده. احنا كدا عملنا InMemory database و نقدر نقول انها compatible مع الـ Redis Server في الـ commands اللي احنا بنعملها support. الاختلاف مع ريدس هو باقي الـ commands اللي احنا لسه مش بنعملها support. و دي هنا ممكن تعتبر challenge ليك انك تبدا تشوف ايه الـ commands اللي انت حابب تعملها بنفسك و تجرب.
المشكله الحاليه ان البيانات اللي بنخزنها بنفقدها تمام لاننا معتمدين عالـ memory فقط. ف ان شاء الله الجزء اللي جاي هيكون اخر خطوه محتاجينها وهو الـ Data persistence وهنشوف ازاي ممكن نستخدم الـ AOF علشان نحفظ الـ data في الـ disk. وازاي نقدر نقراها تاني لما الـ server يعمل bootstrap.