Како дизајнирати трансакциону продавницу кључева и вредности у Го-у

Ако желите да дизајнирате интерактивну љуску која омогућава приступ трансакционом складишту кључева / вредности у меморији, онда сте на правом месту.

Идемо заједно и осмислимо један сада.

Претприча

Питања о дизајну система увек су ме занимала јер вам омогућавају да будете креативни.

Недавно сам прочитао Удуаков блог на којем је поделио своје искуство током 30-дневног маратона са интервјуима, што је било прилично узбудљиво. Топло препоручујем да га прочитате.

У сваком случају, упознао сам се са овим занимљивим питањем дизајна система које му је постављено током интервјуа.

Изазов

Питање је следеће:

Направите интерактивну љуску која омогућава приступ „трансакцијском меморијском кључу / вредности складишта“.

Напомена : Питање је преформулирано ради бољег разумевања. Дати је као пројекат „однеси кући“ током горенаведеног ауторског интервјуа.

Љуска треба да прихвати следеће наредбе:

Команда Опис
SET Поставља дати кључ на наведену вредност. Кључ се такође може ажурирати.
GET Штампа тренутну вредност наведеног кључа.
DELETE Брише дати кључ. Ако кључ није постављен, занемарите.
COUNT Враћа број кључева који су постављени на наведену вредност. Ако ниједан кључ није подешен на ту вредност, исписује се 0.
BEGIN Покреће трансакцију. Ове трансакције вам омогућавају да измените стање система и извршите или вратите своје промене.
END Завршава трансакцију. Све што је урађено у оквиру „активне“ трансакције је изгубљено.
ROLLBACK Одбацује промене направљене у контексту активне трансакције. Ако ниједна трансакција није активна, исписује се „Нема активне трансакције“.
COMMIT Обавезује промене извршене у контексту активне трансакције и завршава активну трансакцију.

Ми смо у арени?

Пре него што започнемо, можемо поставити нека додатна питања попут:

К1. Да ли се подаци задржавају и након завршетка интерактивне љуске?

К2. Да ли се операције са подацима одражавају на глобалну љуску?

К3. Да ли се ОДРЖАВАЊЕ промена у угнежђеној трансакцији одражава и на баке и деке?

Ваша питања се могу разликовати, што је савршено. Што више питања поставите, то боље разумете проблем.

Решавање проблема у великој мери ће зависити од постављених питања, па хајде да дефинишемо шта ћемо претпоставити док градимо нашу продавницу кључева и вредности:

  1. Подаци нису постојани (односно, чим се сесија љуске заврши, подаци се губе).
  2. Кључеви / вредности могу бити само низови (можемо применити интерфејсе за прилагођене типове података, али то је изван опсега овог водича).

Покушајмо сада да разумемо шкакљиви део нашег проблема.

Разумевање „трансакције“

Трансакција се креира BEGINнаредбом и ствара контекст за остале операције. На пример:

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

Ово је тренутно активна трансакција и све операције раде само у њој.

Док се активна трансакција не изврши помоћу COMMITнаредбе, те операције не настављају. И је ROLLBACKкоманда баца све измене направљене од тих операција у контексту активне трансакције. Тачније, брише све парове кључ / вредност са мапе.

На пример:

> 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 

Трансакција такође може бити угнежђена, односно имати и подређене трансакције:

Новопојављена трансакција наслеђује променљиве из надређене трансакције, а промене направљене у контексту подређене трансакције одразиће се и на надређену трансакцију.

На пример:

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

Снимио сам га чим сам прочитао блог. Да видимо како то можемо решити.

Хајде да дизајнирамо

Разговарали смо да трансакције могу имати и подређене трансакције, можемо користити структуру података стека да бисмо ово генерализовали:

  • Сваки елемент стека је трансакција .
  • На врху стека налази се наша тренутна „активна“ трансакција.
  • Сваки елемент трансакције има своју мапу. Назваћемо је „локална продавница“ која се понаша као локална кеш меморија - сваки пут када SETпроменљиву у трансакцији ажурирамо.
  • Једном када се промене ОБВЕЗЕ унутар трансакције, вредности у овом „локалном“ складишту записују се у наш глобални објекат мапе.

Користићемо имплементацију стека са повезаном листом. То такође можемо постићи користећи динамичке низове, али то је домаћи задатак за читаоца:

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. } 
  • Наш стек је представљен структуром TransactionStackкоја чува само показивач на topстек. sizeје структурна променљива која се може користити за одређивање величине нашег стека, односно за проналажење броја суспендованих и активних трансакција (потпуно опционално - ово можете изоставити).
  • Структура Transactionима спремиште које смо раније дефинисали као мапу и показивач на следећу трансакцију у меморији.
  • GlobalStoreје мапа коју деле све трансакције у стеку. Тако постижемо однос родитеља и детета, али о томе касније.

Хајде сада да напишемо пусх и поп методе за нашу 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-- } } 
  • Са сваком BEGINоперацијом, нови елемент стека се убацује у TransactionStackи ажурира topна ову вредност.
  • За сваку операцију COMMITили ENDактивну активност активна трансакција се искаче из стека и додељује јој се следећи елемент стека top. Отуда је надређена трансакција сада наша тренутно активна трансакција.

Ако сте нови у Иди, имајте на уму да PushTransaction()и PopTransaction()су методе и не функције типа пријемника ( *TransactionStack).

У језицима као што су ЈаваСцрипт и Питхон, пријемник метод призивање постиже кључне речи thisи self, респективно.

Међутим, у Го-у то није случај. Можете му дати име како год желите. Да бисмо га лакше разумели, одлучили смо tsсе позвати на стек трансакција.

Сада креирамо Peekметоду која нам враћа topелемент из стека:

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

Имајте на уму да враћамо променљиву показивача типа Transaction.

ИЗВРШЕЊЕ трансакције подразумеваће „копирање“ свих нових и / или ажурираних вредности из локалног складишта трансакција у нашу 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 } 

Враћање трансакције је прилично лако. Само избришите све кључеве са мапе (локална мапа трансакције):

/*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) } } } 

И на крају, ту су GETи SETфункције:

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

сада смо у завршници

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 ⚔.

зое-демо

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.

Имате сумње, нешто није у реду или имате повратне информације? Повежите се са мном на Твиттер-у или ми их директно пошаљите е-поштом.

Гопхерс би МариаЛетта / фрее-гопхерс-пацк

Срећно учење?