Šā raksta mērķis ir izveidot stingru vienlaicīgu vadības paneli Go, kas var: Pārvaldīt tūkstošiem vienlaicīgu atjauninājumu no vairākiem goroutines. Efektīvi apkalpo biežas Top-N pieprasījumus. Saglabājiet prognozējamus rezultātu snapshots, neskatoties uz vienlaicīgiem rakstiem. Esi praktisks un gatavs ražošanai, ieskaitot norādījumus par mērogu, atmiņas izmantošanu un paplašinājumiem. Jūs ne tikai redzēsiet kodu, bet arī sapratīsiet, kāpēc katrs dizaina lēmums ir svarīgs saskaņā ar vienlaicīgām darba slodzēm. Live leaderboards ievieš vairākas sarežģītības: Augstas frekvences raksta: Daudzi lietotāji var tikt atjaunināti vienlaicīgi. Efektīvi Top-N vaicājumi: Visu datu kopu šķirošana katrā ierakstā nav iespējama. Atmiņas efektivitāte: Leaderboards var saturēt simtiem tūkstošu lietotāju. Konsekventas snapshots: Lietotāji sagaida, ka Top-N vaicājums atspoguļos nozīmīgu tabulas stāvokli, pat ja notiek atjauninājumi. Vienlaicīgas rakstīšanas un biežas lasīšanas kombinācija prasa rūpīgu Go sinhronizācijas primitivu un datu struktūru izmantošanu. Izprast problēmu Vadītājs parasti atbalsta divas galvenās operācijas: AddScore (userID string, score int): glabā jaunu rezultātu lietotājam. Vienkāršības labad mēs izsekosim rezultātiem, nevis lietotājiem, kas nozīmē, ka ir atļauti vairāki augsti rezultāti no viena un tā paša lietotāja. Top(n int): Iegūstiet top N augstākos rezultātus. Turklāt ir arī darbības apsvērumi: Atjauninājumi ir jāsaskaņo ar konkurenci. Top-N vaicājumiem jābūt efektīviem, ideāli ātrākiem nekā O (kopējais lietotāju ierakstu kopējais lietotāju skaits). Slēdzenēm vajadzētu samazināt strīdu, ļaujot vairākiem rakstniekiem turpināt, neierobežojot viens otru. Konkurences izaicinājumi Augstas frekvences rakstīšana: Bez sagriežamām vai vienlaicīgi apzinātajām datu struktūrām katrs atjauninājums tiek serializēts ar vienu atslēgu. Efektīvas Top-N vaicājumi: Naiva pieeja būtu, lai sakārtotu visus lietotājus katram vaicājumam. par 100 000 lietotājiem, tas var aizņemt desmitiem milisekundes uz vaicājumu - nepieņemami dzīvajām sistēmām, kas prasa milisekundes līmeņa reaģēšanu. Atmiņas efektivitāte: Palīglīdzekļu struktūru, piemēram, pūļu vai šķirošanas diapazonu, uzturēšana katram lietotājam prasa rūpīgu atmiņas pārvaldību. Konsekventa snapshots: Lietotāji sagaida, ka tabulā ir jēga.Ja Top-N vaicājums lasa nesaskaņotas vērtības vairākos fragmentos, tas var atgriezt svārstīgus vai nepareizus rezultātus. Dizaina apsvērumi Pirms mēs sākam kodēšanu, mums ir jāizlemj, kā organizēt datu struktūras, lai apmierinātu šīs prasības. Vienota karte ar Global Mutex type Leaderboard struct { mu sync.RWMutex scores map[string]int } • Vienkārši un viegli saprast. Pros Slikta mērogojamība smagos rakstos - visi atjauninājumi tiek serializēti caur vienu muteksu. Cons Izmantošanas gadījums: zems konkurences līmenis vai mazi datu kopumi. Izmanto sync.Map Go ir nodrošina vienlaicīgu karti ar lock-free lasījumiem: sync.Map var leaderboard sync.Map leaderboard.Store("user123", 100) score, ok := leaderboard.Load("user123") : Pros Bezslēgšanas lasījumi ļauj vienlaicīgi lasīt vairākus goroutīnus. Raksts ir atoms un drošs vienlaicīgai lietošanai. : Cons Iterācija ir vāji konsekventa. Bieža rakstīšana samazina veiktspēju. Neatbalsta efektīvus Top-N vaicājumus, kas padara to suboptimālu dzīvajām vadības tabulām. Sharded kartes dizains Lai mērogotu ar vairākiem rakstītājiem, mēs varam sadalīt vadošo tabulu fragmentos. Katram fragmentam ir sava karte un mutex. Rakstīšana dažādām daļām notiek neatkarīgi. Lasījumi var notikt vienlaicīgi starp gabaliņiem. Top-N vaicājumi apvieno per-shard rezultātus. Šis dizains nodrošina augstu konkurences līmeni, vienlaikus saglabājot kodu salīdzinoši vienkāršu, tāpēc mēs to ievērosim mūsu īstenošanā. Heap-Based Top-N par Shard Katrā shard, mums ir nepieciešams sekot līdzi top N rezultātus efektīvi. Mēs varētu sakārtot visu shard par katru vaicājumu, bet tas būtu traks. Vai arī mēs varētu sekot līdzi top N rezultātus sakārtotā sarakstā, tad jaunu rezultātu varētu ievietot (vai noraidīt) O(N) laikā. Bet mēs faktiski varam darīt vēl labāk, ar palīdzību no . Top-N min-heap pārskats Min-kopa ir pilnīgs binārais koks, kurā katra mezgla vērtība ir mazāka vai vienāda ar tā bērnu vērtībām: Šī īpašība padara to efektīvu, lai iegūtu minimālo elementu (sakni) un ievietotu jaunus elementus, vienlaikus saglabājot kaudzes struktūru. Tas ir top-N min-heap, jo mēs saglabājam tikai top N rezultātus kaudzē. Kad nāk jauns rezultāts, ja tas ir mazāks par kaudzes sakni, kas ir mazākais top N rezultāts, mēs to noraidām. Ja tas ir lielāks, mēs aizstājam sakni ar jaunu rezultātu (mēs varam to darīt, jo sakņu elements būs ārpus augšējā N) un atkārtoti heapify (pārstrukturēt kaudzē). Tas nodrošina, ka mums vienmēr ir top N rezultāti kaudzē. Šī diagramma parāda, kas notiek ar iekļaušanu: Katrs gabals saglabā vietējo top-N kausu, un globālais Top-N tiek aprēķināts, apvienojot šos kausus. Leaderboard ieviešana Ar visu teoriju zem mūsu jostasvietas, pieņemsim to! Atdzesējiet savu iecienītāko IDE un sāciet ar shard struktūras un galveno līderplates veidu definēšanu: // leaderboard/leaderboard.go package leaderboard import "sync" // Shard represents a portion of the leaderboard // It maintains a top-N min-heap of high scores type shard struct { mu sync.RWMutex topN *TopNMinHeap } type Leaderboard struct { shards []*shard n int // global top-N size } Heap īstenošana Tālāk mēs īstenosim top-N min-heap struktūru un metodes, lai to pārvaldītu.Tas ietver augšējo N rezultātu ievietošanu, noraidīšanu un atgūšanu. Mēs varētu īstenot savu kaudzi, lai iegūtu nelielu veiktspējas pieaugumu, bet tikai pie papildu sarežģītības rēķina - varbūt citā rakstā. container/heap // leaderboard/topnminheap.go package leaderboard import ( "container/heap" ) // ScoreEntry represents a score and its associated player. type ScoreEntry struct { PlayerID string Score int } // TopNMinHeap is a min-heap that stores the top-N high scores with player IDs. type TopNMinHeap struct { scores []ScoreEntry maxN int } // Len implements heap.Interface func (h TopNMinHeap) Len() int { return len(h.scores) } // Less implements heap.Interface (min-heap) func (h TopNMinHeap) Less(i, j int) bool { return h.scores[i].Score < h.scores[j].Score } // Swap implements heap.Interface func (h TopNMinHeap) Swap(i, j int) { h.scores[i], h.scores[j] = h.scores[j], h.scores[i] } // Push implements heap.Interface func (h *TopNMinHeap) Push(x any) { h.scores = append(h.scores, x.(ScoreEntry)) } // Pop implements heap.Interface func (h *TopNMinHeap) Pop() any { old := h.scores n := len(old) x := old[n-1] h.scores = old[:n-1] return x } // NewTopNMinHeap creates a TopNMinHeap with a specified maximum size. func NewTopNMinHeap(maxN int) *TopNMinHeap { return &TopNMinHeap{ scores: make([]ScoreEntry, 0, maxN), maxN: maxN, } } // Add inserts a new score into the heap, maintaining the top-N property. func (h *TopNMinHeap) Add(playerID string, score int) { entry := ScoreEntry{PlayerID: playerID, Score: score} if h.Len() < h.maxN { heap.Push(h, entry) } else if score > h.scores[0].Score { h.scores[0] = entry heap.Fix(h, 0) } } Pirmkārt, mums ir jāīsteno Kas definē , , , un Tad mēs izveidojam konstruktīvu Sākotnēji, lai atbrīvotos no tārpiem, galu galā, Metode nodarbojas ar jaunu rezultātu ievietošanu, vienlaikus saglabājot augšējo N īpašību: ja kaudze nav pilna, mēs vienkārši nospiežam jaunu rezultātu. Tātad ) heap.Interface Len Less Swap Push Pop NewTopNMinHeap Add heap.Fix Shard operācijas: rezultātu atjauninājumi un lasījumi Katram fragmentam jāatklāj metodes, kas droši pievieno jaunus rezultātus un iegūst pašreizējo top-N snapshot. nodrošina, ka shard vienlaicīgi atjauninājumi ir droši. mu // leaderboard/leaderboard.go ... // AddScore adds a new score to the shard's top-N heap. func (s *shard) AddScore(playerID string, score int) { s.mu.Lock() defer s.mu.Unlock() s.topN.Add(playerID, score) } // Top returns a snapshot of the top-N scores for this shard. func (s *shard) Top() []ScoreEntry { s.mu.RLock() defer s.mu.RUnlock() // Return a copy to avoid exposing internal slice top := make([]ScoreEntry, len(s.topN.scores)) copy(top, s.topN.scores) return top } bloķē shard rakstīšanai, pēc tam pievieno rezultātu kaudzei, izmantojot Mūsu iepriekš aprakstītais metožu kopums. AddScore Add bloķē fragmentu lasīšanai un atgriež kopiju no kaudzes gabala, lai izsaucēji nevarētu nejauši mainīt iekšējo kaudzi. Top Izmanto ļauj vairākiem goroutīniem lasīt top-N vienlaicīgi, kamēr raksti tiek serializēti. RWMutex Leaderboard sākotnējā izveide Tagad, kad katrs gabals dara savu lietu, mēs varam inicializēt vadošo tabulu ar noteiktu gabalu skaitu un globālo top-N izmēru: // leaderboard/leaderboard.go // NewLeaderboard creates a sharded leaderboard with `numShards` shards and global top-N size `n`. func NewLeaderboard(numShards, n int) *Leaderboard { lb := &Leaderboard{ shards: make([]*shard, numShards), n: n, } for i := 0; i < numShards; i++ { lb.shards[i] = &shard{ topN: NewTopNMinHeap(n), } } return lb } Šī funkcija rada a Atgriežot rādītāju, tiek nodrošināts, ka visi goroutīni darbojas vienā un tajā pašā koplietojamā vadoņa instancē, kas ir būtiski vienlaicīgiem atjauninājumiem. Leaderboard Pievienojiet rezultātus Kad mēs pievienojam rezultātu līderbordam, mums ir jānosaka, kurš shard jāatjaunina. Mēs izmantojam playerID FNV-1a hash, lai piešķirtu spēlētāju shardam, kas nodrošina gandrīz vienmērīgu spēlētāju sadalījumu pa shards. Tas ir svarīgi, lai izvairītos no izplatīšanas novirzes, kas var izraisīt dažu shards pārslodzes, bet citi ir nepietiekami izmantoti. Ņemiet vērā, ka tas pats playerID vienmēr tiks kartēts uz to pašu shard, kas mūsu pašreizējam dizainam nav izšķirošs, bet tas varētu būt svarīgi, ja vēlāk vēlamies atbalstīt per-player operācijas. // leaderboard/leaderboard.go import "hash/fnv" ... // getShard returns the shard for a given playerID. func (lb *Leaderboard) getShard(playerID string) *shard { h := fnv.New32a() h.Write([]byte(playerID)) idx := int(h.Sum32()) % len(lb.shards) return lb.shards[idx] } ar Tagad mēs varam viegli ieviest Metode, kā pievienot rezultātus līderbordam: getShard AddScore // leaderboard/leaderboard.go // AddScore adds a new score to the appropriate shard. func (lb *Leaderboard) AddScore(playerID string, score int) { s := lb.getShard(playerID) s.AddScore(playerID, score) } zvanīt lai atrastu pareizo shard, tad pievieno rezultātu, izmantojot Katrs gabals apstrādā savu bloķēšanu, tāpēc tas mēro ar gabalu skaitu. AddScore getShard shard.AddScore Globālā top-n atgriešanās Tagad, kad mēs varam pievienot rezultātus, ir tikai viena lieta, kas jādara: atgūt globālos top-N rezultātus visās daļās. Mēs varam to izdarīt, apvienojot top-N kaudzes no katras daļas. Tā kā katras daļas top-N jau ir sakārtots (kā min-heap), mēs varam efektīvi apvienot tos, izmantojot max-heap, lai izsekotu kopējam top-N. (Ļoti lielu skaitu daļu gadījumā var īstenot sarežģītāku k-way apvienošanu, bet parastām daļām šī pieeja ir pietiekama.) // leaderboard/leaderboard.go // Top returns the global top-N across all shards. func (lb *Leaderboard) Top() []ScoreEntry { // Temporary heap to compute global top-N globalHeap := NewTopNMinHeap(lb.n) for _, s := range lb.shards { shardTop := s.Top() // thread-safe snapshot for _, entry := range shardTop { globalHeap.Add(entry.PlayerID, entry.Score) } } // Copy to slice top := make([]ScoreEntry, len(globalHeap.scores)) copy(top, globalHeap.scores) // Sort descending (highest score first) for i, j := 0, len(top)-1; i < j; i, j = i+1, j-1 { top[i], top[j] = top[j], top[i] } return top } Katra daļiņa atgriež savu top-N snapshot, tāpēc mēs neuzglabājam slēdzenes vairākās daļās vienlaicīgi. Mēs ievietojam visus shard top-N ierakstus īslaicīgā n izmēra min-kopā, lai efektīvi uzturētu globālo top-N. Tā kā min-kopā saknē tiek saglabāts mazākais top-N rezultāts, mēs pagriežam šķēli, lai vispirms atgrieztos augstākie rezultāti. Izmēģiniet to, ko mēs esam izveidojuši Tagad, kad esam pabeiguši savu tabulu, redzēsim, kā tas darbojas. Šeit ir vienkārša testa programma, kas demonstrē rezultātu pievienošanu un top-N atgūšanu vienlaicīgi: // main.go package main import ( "fmt" "math/rand" "sync" "time" "./leaderboard" ) func main() { const ( numShards = 8 topN = 10 numPlayers = 50 numUpdates = 200 updateDelay = 10 * time.Millisecond ) lb := leaderboard.NewLeaderboard(numShards, topN) var wg sync.WaitGroup // Spawn concurrent score updates for i := 0; i < numPlayers; i++ { wg.Add(1) playerID := fmt.Sprintf("player%02d", i) go func(pid string) { defer wg.Done() for j := 0; j < numUpdates; j++ { score := rand.Intn(50000) lb.AddScore(pid, score) time.Sleep(updateDelay) } }(playerID) } // Spawn a goroutine to print live top-N periodically done := make(chan struct{}) go func() { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: top := lb.Top() fmt.Println("Top Scores:") for i, entry := range top { fmt.Printf("%2d: %s = %d\n", i+1, entry.PlayerID, entry.Score) } fmt.Println("-----") case <-done: return } } }() // Wait for all updates to finish wg.Wait() close(done) // Print final top-N fmt.Println("Final Top Scores:") top := lb.Top() for i, entry := range top { fmt.Printf("%2d: %s = %d\n", i+1, entry.PlayerID, entry.Score) } } Šī programma izveido līderlogu ar 8 gabaliem un top-10 izmēru. Tā rada 50 goroutines, katra simulējot spēlētāju, kurš atjaunina savu rezultātu 200 reizes ar nejaušām vērtībām. Jūs varat izmantot šo programmu ar , iznākums būs kaut kas šāds: go run main.go Top Scores: 1: player05 = 49830 2: player07 = 49873 3: player46 = 49966 4: player24 = 49800 5: player25 = 49961 6: player10 = 49802 7: player30 = 49812 8: player02 = 49726 9: player19 = 49750 10: player46 = 49718 ----- ... ----- Final Top Scores: 1: player10 = 49971 2: player45 = 49977 3: player00 = 49992 4: player40 = 49979 5: player29 = 49990 6: player19 = 49967 7: player46 = 49966 8: player18 = 49974 9: player25 = 49961 10: player39 = 49960 Secinājums Šajā rakstā mēs esam izveidojuši augstas veiktspējas vienlaicīgu tiešraides tabulu Go no pamatnes. Sākot ar pamatproblēmu, mēs apspriedām izaicinājumus, ko rada augstas frekvences rakstīšana, efektīvas top-N vaicājumi un snapshot konsekvence vienlaicīgi. Mēs esam izpētījuši vairākas dizaina iespējas: Viena karte ar globālu muteksu: vienkārša, bet slikta mērogojamība. sync.Map: piemērots vienlaicīgai lasīšanai, bet ierobežots top-N vaicājumiem. Sharded leaderboard ar per-shard top-N min-heaps: mūsu izvēlētā pieeja, līdzsvarojot līdzsvaru, efektivitāti un vienkāršību. Mēs īstenojām: Shard līmeņa struktūras ar lasīšanas un rakstīšanas slēdzenēm. Top-N min-kopas par gabalu ātrai ievietošanai un noraidīšanai. Globālie top-N vaicājumi, kas efektīvi apvieno katra gabala kaudzes, bloķējot vienlaicīgus atjauninājumus. Demo / testēšanas vītne, kas ilustrē tiešraides atjauninājumus, vienlaikus rakstītus ierakstus un periodiskus līderplānojuma snapshots. Atslēgas vārdi Takeaways: Vairāki goroutīni var atjaunināt rezultātus vienlaicīgi ar minimālu bloķēšanu. Min-kopas efektīvi uztur top-N. Tikai visatbilstošākie rezultāti tiek saglabāti, saglabājot operācijas O(log N). Globālā top-N apvienošana ir praktiska. Apvienojot katra gabala kaudzes, mēs izvairāmies no visas datu kopas šķirošanas un uzturam ātrus vaicājumus. Konkurences drošība ir vienkārša ar per-shard slēdzenēm. Jums nav nepieciešami sarežģīti bezslēdzeņu algoritmi lielākajai daļai tiešraides lietotņu. Palielinot fragmentu skaitu, samazinās strīdi, un uz kaudzes balstītā pieeja nodrošina atmiņas efektivitāti. Izmantojot šo fondu, jūs varat paplašināt vadības paneli, lai atbalstītu: Dynamic top-N par shard vai vairāku līmeņu leaderboards. Integrācija ar pastāvīgo uzglabāšanu vai izplatītām sistēmām lielākiem pielietojumiem. Papildu rādītāji, piemēram, laika zīmes, rindas vai sasniegumi. Šī praktiskā, praktiskā pieeja dod jums priekšstatu par to, kā efektīvi pārvaldīt reālās pasaules vienlaicīgas darba slodzes.Tagad jums ir rīki, lai ieviestu, salīdzinātu un paplašinātu ražošanas gatavas vienlaicīgas sistēmas Go. Šā raksta kods ir pieejams GitHub: gkoos/article-leaderboard. Gkoos / raksta vadības panelis