Ein Stadion hat 60.000 Plätze. Wenn eine große Tour in den Verkauf geht, kommen Millionen Menschen in derselben Minute, und alle wollen dieselben paar tausend guten Plätze. Im November 2022 meldete Ticketmaster während des Presales der Eras Tour insgesamt 3,5 Milliarden Systemanfragen, viermal so viele wie bei seiner bisherigen Spitze. 60.000 Zeilen zu speichern ist trivial. Millionen konkurrierender Anfragen um diese Zeilen fair zu entscheiden, ohne je einen Platz zweimal zu verkaufen, ist der schwierige Teil.
Dieser Beitrag entwirft ein Ticketbuchungssystem für einen solchen Verkaufsstart: Anforderungen, die Form des Traffics, einen virtuellen Warteraum, Platzreservierungen mit Ablaufzeit, die Buchungstransaktion, Zahlungen, Hot Seats und Bots, mit einem Architekturdiagramm.
Anforderungen
Funktional:
- Events und einen Saalplan mit Verfügbarkeit durchsuchen.
- Plätze auswählen und sie während der Zahlung reservieren.
- Bezahlen, Tickets erhalten und Plätze freigeben, wenn die Zahlung ausbleibt.
Nicht funktional:
- Nie einen Platz zweimal verkaufen. Das ist die eine Regel, die sich nicht biegen lässt.
- Menschen fair behandeln: Ankunftsreihenfolge oder eine veröffentlichte Zufallsziehung, nicht, wer am schnellsten neu lädt.
- In den ersten Minuten eines Verkaufsstarts Traffic überstehen, der ein bis zwei Größenordnungen über dem Normalen liegt.
- Schnell und klar scheitern: Ein schnelles „ausverkauft“ ist besser als eine Seite, die sich endlos dreht.
Die Form eines Verkaufsstarts
Nimm 60.000 Plätze und 2 Millionen Menschen an, die innerhalb von zehn Minuten nach dem Start ankommen. Das sind etwa 3.300 neue Ankünfte pro Sekunde und mehr als 30 Menschen pro Platz. Keine Kapazität der Welt verschafft allen ein Ticket. Die eigentliche Aufgabe des Systems ist, eine verkraftbare Zahl von Menschen gleichzeitig einkaufen zu lassen, allen anderen schnell abzusagen und den Bestand unter extremer Konkurrenz korrekt zu halten.
Damit zerfällt das Design in zwei Teile: ein Tor, das die Menge auffängt, und einen Buchungskern, der immer nur so viele Käufer sieht, wie er bewältigen kann.
Der virtuelle Warteraum
Setz eine Warteschlange vor den Buchungsablauf:
- Fans, die vor dem Start ankommen, erhalten eine zufällige Position unter allen Frühankömmlingen, sodass es nichts bringt, um 9:59 hundert Tabs zu öffnen. Spätere Ankömmlinge reihen sich der Reihe nach ein.
- Der Warteraum ist eine statische Seite aus einem CDN, sodass Millionen wartender Browser fast nichts kosten.
- Fans werden in Gruppen eingelassen, mit einer Rate, die auf die gemessene Buchungskapazität abgestimmt ist, zum Beispiel 2.000 pro Minute.
- Jeder eingelassene Fan erhält ein signiertes, kurzlebiges Token, das an seine Session gebunden ist. Die Buchungs-API lehnt Requests ohne gültiges Token ab.
Verwaltete Produkte wie Cloudflare Waiting Room und Queue-it erledigen das. Eine selbst gebaute Variante ist ein Redis Sorted Set mit Ankunftszeiten als Scores, in dem die Position der Rang des Mitglieds ist:
ZADD queue:evt42 1793606400123 session:9f2c # score = arrival time in ms
ZRANK queue:evt42 session:9f2c # position, 0-based
ZPOPMIN queue:evt42 2000 # admit the next batch
Für den Pool der Frühankömmlinge vergibst du statt Ankunftszeiten zufällige Scores. So oder so verwandelt die Warteschlange eine unbegrenzte Spitze in einen gleichmäßigen Fluss, auf den der Rest des Systems ausgelegt ist.
Das Bestandsmodell
Nummerierte Plätze brauchen eine Zeile pro Platz:
| Spalte | Beispiel | Zweck |
|---|---|---|
event_id, seat_id |
evt42, S112-R7-14 |
Identität; gemeinsam eindeutig |
status |
available, held, sold |
Aktueller Zustand |
hold_id, held_until |
h_81f, Zeitstempel |
Wer reserviert und bis wann |
order_id |
ord_5521 |
Wird beim Verkauf gesetzt |
Freie Platzwahl ist einfacher: statt einzelner Plätze ein Zähler pro Preiszone, der mit einem bedingten Update heruntergezählt wird.
Platzreservierungen mit Ablaufzeit
Eine Reservierung hält Plätze für einen Käufer ein paar Minuten lang frei, während er bezahlt. Der sicherste Ort für Reservierungen ist dieselbe Datenbank, die die Verkäufe erfasst, mit einem bedingten Update:
UPDATE seats
SET status = 'held', hold_id = $3, held_until = now() + interval '10 minutes'
WHERE event_id = $1
AND seat_id = ANY($2)
AND (status = 'available' OR (status = 'held' AND held_until < now()));
-- If fewer rows changed than seats requested, roll back: someone else got one.
Zwei Käufer können diese Anweisung in derselben Millisekunde für denselben Platz senden. Die Datenbank serialisiert die Updates auf dieser Zeile, eines davon ändert sie, und das andere sieht null Zeilen und bekommt eine klare Antwort: „Platz gerade vergeben“. Abgelaufene Reservierungen werden in derselben Anweisung zurückgeholt, sodass der Ablauf einer Reservierung nie davon abhängt, dass ein Hintergrundjob pünktlich läuft. Zwei Requests für überlappende Platzmengen können gelegentlich in einen Deadlock geraten; die Datenbank erkennt das und bricht einen ab, also wiederhole bei einem Deadlock-Fehler einmal.
Bei sehr großem Maßstab setzen manche Teams einen schnellen Filter davor: SET seat:evt42:S112-R7-14 h_81f NX PX 600000 in Redis, das den Schlüssel nur setzt, wenn er fehlt, mit zehn Minuten Ablaufzeit. Das nimmt der Datenbank Last ab, aber die Redis-Replikation ist asynchron, sodass ein Failover Reservierungen verlieren kann. Betrachte Redis als Optimierung; die Entscheidung trifft weiterhin die Datenbank.
Die letzte Verteidigungslinie ist ein Unique Constraint auf (event_id, seat_id) in der Tickettabelle. Selbst wenn jede Schicht darüber einen Bug hat, verweigert die Datenbank das zweite Ticket.
Hot Seats und Best Available
Alle wollen die vorderen Reihen, also zielen Tausende Requests auf dieselben paar hundert Zeilen, und die meisten scheitern. Drei Techniken verringern die Konkurrenz:
- Best Available. Statt alle aus dem Saalplan wählen zu lassen, lass Käufer einen Block und eine Anzahl wählen und vergib die Plätze serverseitig mit
SELECT … FOR UPDATE SKIP LOCKED, sodass gleichzeitige Requests verschiedene Zeilen greifen, statt sich an denselben anzustellen. - Partitioniere den Bestand nach Block, sodass verschiedene Blöcke verschiedene Zeilen sperren und auf verschiedenen Datenbank-Shards liegen können.
- Cache den Saalplan mit kurzer Ablaufzeit und akzeptiere, dass er ein paar Sekunden veraltet sein kann. Maßgeblich für die Verfügbarkeit ist die Reservierung, nicht der Plan.
Zahlung ohne doppelte Belastung
- Leg eine Bestellung im Zustand
pendingan, die mit der Reservierung verknüpft ist. - Ruf den Zahlungsanbieter mit einem Idempotency Key auf, der aus der Bestell-ID abgeleitet ist. Bei Stripe zum Beispiel sind Idempotency Keys bis zu 255 Zeichen lang, und ein wiederholter Request mit demselben Key liefert das ursprüngliche Ergebnis, statt erneut zu belasten.
- Markiere bei Erfolg in einer einzigen Datenbanktransaktion die Plätze als verkauft, hänge sie an die Bestellung und markiere die Bestellung als bezahlt.
- Lausche auf die Webhooks des Anbieters, um den Fall abzufangen, dass die Zahlung erfolgreich war, dein Service die Antwort aber nie gesehen hat.
Der Ablauf einer Reservierung darf nicht mit der Zahlung um die Wette laufen. Verlängere die Reservierung, wenn die Zahlung beginnt, oder nimm Plätze mit offener Bestellung vom Ablauf aus. Sonst kann ein Platz ablaufen, an jemand anderen verkauft werden, und danach geht seine erste Zahlung doch noch durch. Das Diagramm zur Payment-Gateway-Integration behandelt Webhooks und Abgleich im Detail.
Abgelaufene Reservierungen freigeben
Reservierungen laufen lazy ab, über die Bedingung held_until < now() oben, und aktiv, über einen Sweeper, der alle paar Sekunden läuft und abgelaufene Reservierungen als verfügbar markiert, damit der Saalplan nachzieht. Bau den Ablauf nicht auf Redis-Keyspace-Notifications: Redis dokumentiert sein Pub/Sub als „Fire and Forget“, ein Subscriber, der die Verbindung verliert, verpasst also Events.
Bots und Missbrauch
Der Warteraum ist die wichtigste Verteidigung, weil er Geschwindigkeit bedeutungslos macht. Ergänze Kauflimits pro Konto, Rate Limits pro Konto, IP und Gerät, eine Registrierung vor dem Verkaufsstart bei stark nachgefragten Events und Challenges im Warteraum. Das Systemdesign eines Rate Limiters behandelt das Begrenzen selbst.
Das Ticketbuchungs-Diagramm
Diagramminhalt
- Fans
- Waiting room (static, on CDN)
- Queue positions + admissions
- API gateway: token check
- Booking service
- Seats, holds, orders
- Hold sweeper, every 5 s
- Payment provider
- order.confirmed events
- Ticket delivery
- arrive at 10:00
- ZADD / ZRANK
- batch + signed token
- admitted?
- hold seats, pay
- hold: conditional UPDATE, 10 min
- release expired holds
- charge + Idempotency-Key
- webhook
- order paid
- send tickets
So liest du es:
- Fans kommen im Warteraum im CDN an, der sie festhält und in Gruppen mit signierten Tokens einlässt.
- API-Gateway und Buchungsservice sehen nur eingelassene Fans. Redis speichert Warteschlangenpositionen und Einlassstatus.
- Postgres hält die Plätze. Das bedingte Update erzeugt Reservierungen, und der Unique Constraint schützt die Verkäufe.
- Der Zahlungsanbieter wird mit einem Idempotency Key aufgerufen, und seine Webhooks kommen zum Buchungsservice zurück.
- Die Sweeper-Funktion gibt abgelaufene Reservierungen frei; die Bestell-Queue speist die Ticketzustellung per E-Mail und in der App.
- Der Markdown-Block listet die Zahlen des Verkaufsstarts und die Reservierungsregeln auf.
Buchungssysteme so zeichnen, dass Reviewer die Race Conditions finden
- Zeichne das Tor getrennt vom Kern. Warteraum und Buchungsservice haben unterschiedliche Skalierungsmodelle und Verantwortliche.
- Schreib Reservierungsdauer und Eindeutigkeitsregel ins Diagramm. Diese zwei Fakten erklären das Design.
- Setz den Idempotency Key an die Zahlungskante. Er ist der Grund, warum Wiederholungen sicher sind.
- Zeig den Ablaufpfad. Wo Plätze zurückkommen, verstecken sich viele Doppelverkaufs-Bugs.
Zur allgemeinen Methode sieh dir das System-Design-Diagramm-Beispiel an. Die Bestell-Events lassen sich mit dem Transactional-Outbox-Pattern veröffentlichen und die Ticket-E-Mails über eine Pipeline wie im Systemdesign für Benachrichtigungen.
In Archboard nachbauen
- Melde dich mit Google oder GitHub an und leg ein Diagramm an.
- Füge eine Person aus dem C4-Modell-Pack für die Fans und einen CDN-Node aus dem Cloud-Pack für den Warteraum hinzu.
- Füge ein API-Gateway und einen Recheninstanz-Node aus dem Cloud-Pack für den Buchungsservice hinzu.
- Füge einen Redis-Node aus dem Daten-Pack für den Warteschlangenstatus und einen Postgres-Node für Plätze, Reservierungen und Bestellungen hinzu.
- Füge ein Externes System aus dem C4-Modell-Pack für den Zahlungsanbieter, eine Funktion aus dem Cloud-Pack für den Reservierungs-Sweeper, eine Warteschlange für Bestell-Events und einen weiteren Recheninstanz-Node für die Ticketzustellung hinzu.
- Beschrifte die Kanten mit Tokens, Reservierungsregeln und dem Idempotency Key und füge einen Markdown-Block aus dem Basis-Pack mit den Zahlen hinzu.
Das Beispiel nutzt 11 Diagramm-Nodes und bleibt damit im Free-Plan.
FAQ
Wie verhindern Ticketingsysteme Doppelbuchungen? Mit einem bedingten Update auf der Platzzeile, sodass nur ein Request einen Platz von verfügbar auf reserviert setzen kann, und einem Unique Constraint auf der Tickettabelle als letzte Absicherung. Caches und Queues senken die Last, aber die Datenbank entscheidet.
Wie lange sollte eine Platzreservierung dauern? Lang genug, um bequem zu bezahlen, und kurz genug, dass verlassene Plätze schnell zurückkommen. Fünf bis zehn Minuten sind ein üblicher Bereich. Verlängere die Reservierung, wenn die Zahlung beginnt, damit der Ablauf nicht mit einer erfolgreichen Belastung kollidieren kann.
Was ist ein virtueller Warteraum? Eine Warteschlange vor dem Buchungsablauf, die Nutzer mit einer Rate einlässt, die das System bewältigen kann. Meist ist es eine statische Seite in einem CDN, mit zufällig vergebenen Positionen für Frühankömmlinge und danach der Reihe nach.
Sollten Platzreservierungen in Redis oder in der Datenbank liegen? Die Datenbank sollte die Quelle der Wahrheit sein, weil die Redis-Replikation asynchron ist und ein Failover Schlüssel verlieren kann. Redis eignet sich gut als schneller Filter davor, solange die Datenbank weiterhin die Eindeutigkeit durchsetzt.
Wie gehst du mit einer Zahlung um, die nach Ablauf der Reservierung erfolgreich ist? Verhindere sie, indem du Reservierungen während der Zahlung verlängerst. Passiert es trotzdem, erstatte automatisch oder biete einen gleichwertigen Platz an, und erkenne den Fall über die Webhooks des Anbieters.