Sådan designer du en transaktionsnøgleværdibutik i Go

Hvis du vil designe en interaktiv skal, der giver adgang til en transaktionsnøgle / værdilager i hukommelsen, er du på det rette sted.

Lad os gå sammen og designe en nu.

Baghistorie

Systemdesign spørgsmål har altid interesseret mig, fordi de lader dig være kreativ.

For nylig læste jeg Uduaks blog, hvor han delte sin oplevelse med at lave et 30-dages interviewmaraton, hvilket var ret spændende. Jeg kan varmt anbefale at læse det.

Under alle omstændigheder fik jeg at vide om dette interessante systemdesign spørgsmål, han blev stillet under interviewet.

Udfordringen

Spørgsmålet er som følger:

Byg en interaktiv skal, der giver adgang til en "transaktionsnøgle i hukommelsen / værdilager".

Bemærk : Spørgsmålet omformuleres for bedre forståelse. Det blev givet som et "take home" -projekt under ovennævnte forfatters interview.

Skallen skal acceptere følgende kommandoer:

Kommando Beskrivelse
SET Indstiller den givne nøgle til den angivne værdi. En nøgle kan også opdateres.
GET Udskriver den aktuelle værdi for den angivne nøgle.
DELETE Sletter den givne nøgle. Hvis nøglen ikke er indstillet, skal du ignorere.
COUNT Returnerer antallet af taster, der er indstillet til den angivne værdi. Hvis ingen taster er indstillet til den værdi, udskrives 0.
BEGIN Starter en transaktion. Disse transaktioner giver dig mulighed for at ændre systemets tilstand og foretage eller tilbageføre dine ændringer.
END Afslutter en transaktion. Alt, der udføres inden for den "aktive" transaktion, går tabt.
ROLLBACK Smider ændringer foretaget inden for rammerne af den aktive transaktion. Hvis ingen transaktion er aktiv, udskrives "Ingen aktiv transaktion".
COMMIT Foretager de ændringer, der er foretaget inden for rammerne af den aktive transaktion, og afslutter den aktive transaktion.

Vi er i arenaen?

Inden vi begynder, kan vi stille nogle yderligere spørgsmål som:

Q1. Vedvarer dataene, når den interaktive shell-session slutter?

Q2. Reflekterer operationer på dataene den globale skal?

Q3. Genspejler forpligtelsesændringer i en indlejret transaktion også bedsteforældre?

Dine spørgsmål kan variere, hvilket er perfekt. Jo flere spørgsmål du stiller, jo bedre forstår du problemet.

Løsning af problemet afhænger stort set af de stillede spørgsmål, så lad os definere, hvad vi vil antage, mens vi bygger vores nøgleværdibutik:

  1. Data er ikke-vedvarende (dvs. så snart shell-sessionen slutter, går data tabt).
  2. Nøgleværdier kan kun være strenge (vi kan implementere grænseflader til brugerdefinerede datatyper, men det er uden for denne tutorial).

Lad os nu prøve at forstå den vanskelige del af vores problem.

Forståelse af en "transaktion"

En transaktion oprettes med BEGINkommandoen og skaber en kontekst for de andre operationer. For eksempel:

> BEGIN // Creates a new transaction > SET X 200 > SET Y 14 > GET Y 14 

Dette er den aktuelle aktive transaktion, og alle operationer fungerer kun indeni den.

Indtil den aktive transaktion er begået ved hjælp af COMMITkommandoen, fortsætter disse operationer ikke. Og ROLLBACKkommandoen smider de ændringer, der er foretaget af disse operationer i forbindelse med den aktive transaktion. For at være mere præcis sletter den alle nøgleværdipar fra kortet.

For eksempel:

> BEGIN //Creates a new transaction which is currently active > SET Y 2020 > GET Y 2020 > ROLLBACK //Throws away any changes made > GET Y Y not set // Changes made by SET Y have been discarded 

En transaktion kan også indlejres, dvs. have børnetransaktioner også:

Den nyligt givne transaktion arver variablerne fra den overordnede transaktion, og ændringer foretaget i sammenhæng med en underordnet transaktion vil også afspejle sig i modertransaktionen.

For eksempel:

> BEGIN //Creates a new active transaction > SET X 5 > SET Y 19 > BEGIN //Spawns a new transaction in the context of the previous transaction and now this is currently active > GET Y Y = 19 //The new transaction inherits the context of its parent transaction** > SET Y 23 > COMMIT //Y's new value has been persisted to the key-value store** > GET Y Y = 23 // Changes made by SET Y 19 have been discarded** 

Jeg skød det lige efter jeg havde læst bloggen. Lad os se, hvordan vi kan løse dette.

Lad os designe

Vi diskuterede, at transaktioner også kan have underordnede transaktioner, og vi kan bruge stack-datastrukturen til at generalisere dette:

  • Hvert stakelement er en transaktion .
  • Øverst på stakken gemmer vores nuværende "aktive" transaktion.
  • Hvert transaktionselement har sit eget kort. Vi kalder det "lokal butik", der fungerer som en lokal cache - hver gang vi SETen variabel inde i en transaktion opdateres denne butik.
  • Når ændringerne er BINNET i en transaktion, bliver værdierne i denne "lokale" butik skrevet til vores globale kortobjekt.

Vi bruger en Linked-list implementering af stack. Vi kan også opnå dette ved hjælp af dynamiske arrays, men det er lektier for læseren:

package main import ( "fmt" "os" "bufio" "strings" ) /*GlobalStore holds the (global) variables*/ var GlobalStore = make(map[string]string) /*Transaction points to a key:value store*/ type Transaction struct { store map[string]string // every transaction has its own local store next *Transaction } /*TransactionStack maintains a list of active/suspended transactions */ type TransactionStack struct { top *Transaction size int // more meta data can be saved like Stack limit etc. } 
  • Vores stak er repræsenteret af en struktur, TransactionStackder kun gemmer en markør til topstakken. sizeer en strukturvariabel, der kan bruges til at bestemme størrelsen på vores stak, dvs. finde antallet af suspenderede og aktive transaktioner (helt valgfri - du kan undlade at erklære dette).
  • Den Transactionstruct har en butik, som vi definerede tidligere som et kort og en pointer til den næste transaktion i hukommelsen.
  • GlobalStore is a map which is shared by all the transactions in the stack. This is how we achieve a parent-child relationship, but more on this later.

Now let's write the push and pop methods for our TransactionStack.

 /*PushTransaction creates a new active transaction*/ func (ts *TransactionStack) PushTransaction() { // Push a new Transaction, this is the current active transaction temp := Transaction{store : make(map[string]string)} temp.next = ts.top ts.top = &temp ts.size++ } /*PopTransaction deletes a transaction from stack*/ func (ts *TransactionStack) PopTransaction() { // Pop the Transaction from stack, no longer active if ts.top == nil { // basically stack underflow fmt.Printf("ERROR: No Active Transactions\n") } else { node := &Transaction{} ts.top = ts.top.next node.next = nil ts.size-- } } 
  • With every BEGIN operation, a new stack element is pushed into the TransactionStack and updates top to this value.
  • For every COMMIT or END operation, the active transaction is popped from the stack and the next element of the stack is assigned to top. Hence the parent transaction is now our current active transaction.

If you are new to Go, note that PushTransaction() and PopTransaction() are methods and not functions of receiver type (*TransactionStack).

In languages like JavaScript and Python, the receiver method invocation is achieved by the keywords this and self, respectively.

However in Go this is not the case. You can name it anything you want. To make it easier to understand we choose ts to refer to the transaction stack.

Now we create a Peek method to return us the top element from the stack:

/*Peek returns the active transaction*/ func (ts *TransactionStack) Peek() *Transaction { return ts.top } 

Note that we are returning a pointer variable of type Transaction.

COMMITing a transaction will involve "copying" all the new and/or updated values from the transaction local store to our GlobalStore:

/*Commit write(SET) changes to the store with TranscationStack scope Also write changes to disk/file, if data needs to persist after the shell closes */ func (ts *TransactionStack) Commit() { ActiveTransaction := ts.Peek() if ActiveTransaction != nil { for key, value := range ActiveTransaction.store { GlobalStore[key] = value if ActiveTransaction.next != nil { // update the parent transaction ActiveTransaction.next.store[key] = value } } } else { fmt.Printf("INFO: Nothing to commit\n") } // write data to file to make it persist to disk // Tip: serialize map data to JSON } 

Rolling back a transaction is pretty easy. Just delete all the keys from the map (the local map of a transaction):

/*RollBackTransaction clears all keys SET within a transaction*/ func (ts *TransactionStack) RollBackTransaction() { if ts.top == nil { fmt.Printf("ERROR: No Active Transaction\n") } else { for key := range ts.top.store { delete(ts.top.store, key) } } } 

And finally, here are the GET and SET functions:

/*Get value of key from Store*/ func Get(key string, T *TransactionStack) { ActiveTransaction := T.Peek() if ActiveTransaction == nil { if val, ok := GlobalStore[key]; ok { fmt.Printf("%s\n", val) } else { fmt.Printf("%s not set\n", key) } } else { if val, ok := ActiveTransaction.store[key]; ok { fmt.Printf("%s\n", val) } else { fmt.Printf("%s not set\n", key) } } } 

While SETing a variable, we also have to consider the case when the user might not run any transactions at all. This means that our stack will be empty, that is, the user is SETing variables in the global shell itself.

> SET F 55 > GET F 55 

In this case we can directly update our GlobalStore:

/*Set key to value */ func Set(key string, value string, T *TransactionStack) { // Get key:value store from active transaction ActiveTransaction := T.Peek() if ActiveTransaction == nil { GlobalStore[key] = value } else { ActiveTransaction.store[key] = value } } 

Are you still with me? Don't go!

vi er i slutspillet nu

We are pretty much done with our key-value store, so let's write the driver code:

 func main(){ reader := bufio.NewReader(os.Stdin) items := &TransactionStack{} for { fmt.Printf("> ") text, _ := reader.ReadString('\n') // split the text into operation strings operation := strings.Fields(text) switch operation[0] { case "BEGIN": items.PushTransaction() case "ROLLBACK": items.RollBackTransaction() case "COMMIT": items.Commit(); items.PopTransaction() case "END": items.PopTransaction() case "SET": Set(operation[1], operation[2], items) case "GET": Get(operation[1], items) case "DELETE": Delete(operation[1], items) case "COUNT": Count(operation[1], items) case "STOP": os.Exit(0) default: fmt.Printf("ERROR: Unrecognised Operation %s\n", operation[0]) } } } 

The COUNT and DELETE operations are fairly easy to implement if you stuck with me until now.

I encourage you to do this as homework, but I have provided my implementation below if you get stuck somewhere.

Time for testing ⚔.

zoe-demo

And let me leave you with my source code - you can give the repo a star if you want to support my work.

If you liked this tutorial, you can read more of my stuff at my blog.

Er du i tvivl, er der noget galt, eller har du feedback? Opret forbindelse til mig på Twitter eller e-mail dem direkte til mig.

Gophers af MariaLetta / free-gophers-pack

Glad læring?