Kuinka suunnitella tapahtuman avainarvokauppa Go-palvelussa

Jos haluat suunnitella interaktiivisen kuoren, joka sallii pääsyn tapahtuman sisäiseen muistiavaimen / arvon varastoon, olet oikeassa paikassa.

Mennään yhdessä ja suunnitellaan yksi nyt.

Tausta

Järjestelmäsuunnittelukysymykset ovat aina kiinnostaneet minua, koska ne antavat sinun olla luovia.

Luin äskettäin Uduakin blogin, jossa hän kertoi kokemuksestaan ​​30 päivän haastattelumaratonista, mikä oli aika jännittävää. Suosittelen sen lukemista.

Joka tapauksessa sain tietää tästä mielenkiintoisesta järjestelmäsuunnittelukysymyksestä, jonka häneltä kysyttiin haastattelun aikana.

Haaste

Kysymys on seuraava:

Rakenna interaktiivinen kuori, joka sallii pääsyn "tapahtuman sisäiseen muistiavaimen / arvon varastoon".

Huomaa : Kysymys muotoillaan uudelleen ymmärtämisen parantamiseksi. Se annettiin "ota kotiin" -projektina yllä mainitun kirjoittajan haastattelun aikana.

Kuoren tulisi hyväksyä seuraavat komennot:

Komento Kuvaus
SET Asettaa annetun avaimen määritettyyn arvoon. Avain voidaan myös päivittää.
GET Tulostaa määritetyn avaimen nykyisen arvon.
DELETE Poistaa annetun avaimen. Jos avainta ei ole asetettu, ohita.
COUNT Palauttaa asetettujen avainten määrän määritettyyn arvoon. Jos kyseiseen arvoon ei ole asetettu avaimia, tulostaa 0.
BEGIN Aloittaa tapahtuman. Näiden tapahtumien avulla voit muokata järjestelmän tilaa ja sitouttaa tai palauttaa muutokset.
END Lopettaa tapahtuman. Kaikki "aktiivisessa" tapahtumassa tehty menetetään.
ROLLBACK Heittää pois aktiivisen tapahtuman yhteydessä tehdyt muutokset. Jos mikään tapahtuma ei ole aktiivinen, tulostaa "Ei aktiivista tapahtumaa".
COMMIT Sitoo aktiivisen tapahtuman yhteydessä tehdyt muutokset ja lopettaa aktiivisen tapahtuman.

Olemme areenalla?

Ennen kuin aloitamme, voimme esittää joitain lisäkysymyksiä, kuten:

Q1. Pysyvätkö tiedot interaktiivisen kuori-istunnon jälkeen?

Q2. Heijastavatko datan toiminnot globaalia kuorta?

Q3. Pohdittaako sisäkkäisen tapahtuman muutosten sitoutuminen myös isovanhempiin?

Kysymyksesi voivat olla erilaisia, mikä on täydellistä. Mitä enemmän kysyt, sitä paremmin ymmärrät ongelman.

Ongelman ratkaiseminen riippuu suurelta osin esitetyistä kysymyksistä, joten määritellään, mitä aiomme ottaa rakentaessamme avainarvokauppaa:

  1. Tiedot ovat pysyviä (ts. Heti kun shell-istunto päättyy, tiedot menetetään).
  2. Avainarvot voivat olla vain merkkijonoja (voimme toteuttaa rajapintoja mukautetuille tietotyypeille, mutta se ei kuulu tämän opetusohjelman piiriin).

Yritetään nyt ymmärtää ongelmamme hankala osa.

Tapahtuman ymmärtäminen

Tapahtuma luodaan BEGINkomennolla ja luo kontekstin muille toiminnoille. Esimerkiksi:

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

Tämä on nykyinen aktiivinen tapahtuma, ja kaikki toiminnot toimivat vain sen sisällä.

Ennen kuin aktiivinen tapahtuma on suoritettu COMMITkomennolla, nämä toiminnot eivät jatku. Ja ROLLBACKkomento heittää pois kaikki näiden toimintojen tekemät muutokset aktiivisen tapahtuman yhteydessä. Tarkemmin sanottuna se poistaa kaikki avainarvoparit kartalta.

Esimerkiksi:

> 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 

Tapahtuma voidaan myös sisäkkäin, toisin sanoen olla myös alatapahtumia:

Äskettäin syntynyt tapahtuma perii muuttujat vanhemmasta tapahtumastaan, ja lapsitapahtuman yhteydessä tehdyt muutokset heijastuvat myös vanhempaan tapahtumaan.

Esimerkiksi:

> 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** 

Annoin sille kuvan heti kun luin blogin. Katsotaanpa, kuinka voimme ratkaista tämän.

Suunnittelemme

Keskustelimme, että tapahtumilla voi olla myös alitapahtumia, voimme yleistää tämän pino-tietorakenteella:

  • Jokainen pinoelementti on tapahtuma .
  • Pinon yläosassa on nykyinen "aktiivinen" kauppa.
  • Jokaisella tapahtumaelementillä on oma kartta. Kutsumme sitä "lähikaupaksi", joka toimii kuin välimuisti - joka kerta, SETkun muuttuja tapahtuman sisällä, tämä myymälä päivitetään.
  • Kun muutokset on TEHTY tapahtuman sisällä, tämän "paikallisen" myymälän arvot kirjoitetaan globaaliin karttaobjektiimme.

Käytämme pinon linkitetyn luettelon toteutusta. Voimme saavuttaa tämän myös käyttämällä dynaamisia taulukoita, mutta se on lukijan kotitehtävä:

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. } 
  • Pinoamme edustaa rakenne, TransactionStackjoka tallentaa vain osoittimen toppinoon. sizeon rakennemuuttuja, jota voidaan käyttää pinon koon määrittämiseen eli keskeytettyjen ja aktiivisten tapahtumien lukumäärän löytämiseen (täysin valinnainen - voit jättää tämän ilmoittamatta).
  • TransactionStruct on tallentaa, joka määrittelimme aikaisemmin kartta ja osoitin seuraavaan tapahtuman muistiin.
  • 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!

olemme nyt loppupelissä

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.

Onko sinulla epäilyksiä, jotain on vialla, vai onko sinulla palautetta? Ota yhteyttä minuun Twitterissä tai lähetä sähköpostia minulle suoraan.

MariaLettan Gophers / free-gophers-pack

Hyvää oppimista?