Event Sourcing
Uwe Schäfer & Jörg Adler
Mercateo AG
Mercateo
- Europas führende Beschaffungsplattform (B2B-Onlineshop)
- mehrere Tausend Lieferanten
- hunderte Millionen Artikel
- zur Zeit Neuaufbau der Transaktionsplattform
- komplett AWS
- komplett losgelöst vom Bestandssystem
Wer sind wir
Jörg Adler, Softwareentwickler
joerg.adler@mercateo.com
@joerg_adler
Uwe Schäfer, Softwareentwickler
uwe.schaefer@mercateo.com
@codesmell
Publikumsfrage
Welche SQL Statements löschen Daten?
Insert
Update
Delete
Wissen Softwareentwickler, welche Daten in Zukunft gebraucht werden und damit nicht gelöscht werden dürfen?
Szenario
- Wir betreiben einen kleinen Onlineshop
- Warenkorb ist Teil des Shops
- Wir wollen den Shop selbst programmieren
- Beispiele sind meist bewusst vereinfacht
Szenario Warenkorb
t
add 2 t-shirts
change to 1 t-shirt
add socks
remove socks
order
Lösung RDBMS
Article
Cart
Line-Item
ID
CREATION_TIME
CUSTOMER_ID
CLOSED
ID
CART_ID
ARTICLE_ID
QUANTITY
ID
NAME
DESCRIPTION
PRICE
Szenario Warenkorb
INSERT INTO CARTS VALUES (NEWID(), NOW(), customer_id, 0)
INSERT INTO LINE_ITEMS VALUES(NEWID(), cart_id, article_id_t_shirt, 2)
UPDATE LINE_ITEMS SET QUANTITY = 1 WHERE CART_ID = cart_id AND ARTICLE_ID = article_id_t_shirt
INSERT INTO LINE_ITEMS VALUES(NEWID(), cart_id, article_id_socks, 1)
DELETE FROM LINE_ITEMS WHERE CART_ID = cart_id AND ARTICLE_ID = article_id_socks
bei einem Artikelupdate:
UPDATE ARTICLES SET PRICE = 100 WHERE ID = article_id_t_shirt
INSERT INTO LINE_ITEMS VALUES(NEWID(), cart_id, article_id_t_shirt, 1)
UPDATE CARTS SET CLOSED = 1 WHERE ID = cart_id
Problem Datenverlust
- Modell repräsentiert Status zu genau einem Zeitpunkt
- Bei Änderung der Daten gehen diese verloren
- Wie war der Preis beim Legen in den Warenkorb?
- Welche Artikel waren im Warenkorb, wurden aber nicht gekauft?
-
Zur Beantwortung dieser Fragen:
- Änderung des Datenmodells für alle Nutzer
- Daten können nur für die Zukunft gesammelt werden
- Dazu mehr Daten in LINE_ITEMS
Problem zentrales Modell
- Modell ist meist Kompromiss aus allen Anforderungen
- Eine einzige Datenbanktechnologie
- Volltext-Suche nach Artikeln
- Dürfen sich Suche/Warenkorb beeinflussen
- Reports werden meist umständlich im DWH erzeugt
- Normalisierung der Daten
- Optimierung für das Schreiben
- Artikel + Warenkorb werden aber meist nur gelesen
- Optimierung auf das Lesen verlängert Zeit beim Schreiben
Lösung Eventsourcing
-
Schreiben aller Änderungen (Events) im System in ein Log
-
Events sind Fakten, die in der Vergangenheit liegen
-
Events sind unveränderlich
-
"Events erzählen eine Geschichte"
- treffen keine Annahmen über Zielmodell
Events
LineItemAddedToCart
LineItemRemovedFromCart
LineItemQuantityChanged
CheckoutStarted
cartId
quantity
articleId
...
cartId
articleId
...
shoppingCartId
quantity
articleId
...
cartId
...
Events
ArcticleImported
articleId
name
description
...
Szenario Warenkorb
add 2 t-shirts
change to 1 t-shirt
add socks
remove socks
order
LineItemAdded
ToCart
LineItemQuantity
Changed
LineItemAdded
ToCart
LineItemRemoved
FromCart
CheckoutStarted
t
Szenario Warenkorb
- kein Event für die Erzeugung des Warenkorbs?
- Events für tatsächlich stattgefundene Ereignisse
- möglichst wenig Annahmen über das Zielsystem
- natürliche Art und Weise beim Schreiben zu modellieren
- aufwändiger / komplexer
Lesemodelle
Log
Shop-Gui
Reporting
Artikelsuche
icons: https://icons8.com
Warenkörbe
Reports
Artikel-Index
Lesemodelle
- Bestimmung eines angepassten Modells für den Use-Case
- Auswahl der Datenbanktechnologie passend zum Modell
- Lesemodelle sind jederzeit wieder aus dem Log rekonstruierbar
- dadurch änder-/erweiterbar
Lesemodelle: Warenkorb
- Aufgabe: aktueller Warenkorb zu einer bestimmten Id soll bestimmt werden
- lesen aller Events und Aggregation für die bestimmte Id
- Folge: sehr langsam (Abhilfe: Filter-Queries)
- Zwischenspeichern möglich, da Events unveränderbar (rolling Snapshot)
- Zwischenspeicherung in beliebigem Datastore (InMem, SQL oder DokumentenDB oder...)
Lesemodelle: Report
- Aufgabe: Erzeugung Report
- lesen aller relevanten Events und Speichern der Daten in den Report
- Modell ist ein anderes
- muss nicht strikt konsistent sein
- Streaming aller relevanten Events (Subscription)
Lesemodelle: Artikelsuche
- Aufgabe: Volltextsuche über Artikel
- Streaming aller relevanten Events (Subscription)
- Modell ist ein anderes
- Integration in Elastic-Search
- kann nicht strikt konsistent sein
Zusammenfassung
- kein zentrales Modell mehr nötig
- keine zentrale Datenbank für alle Anwendungen
- neue Features mit Daten aus der Vergangenheit
- Lokale Optimierung von Schreib- und Leseseite getrennt möglich
Vorteile Eventsourcing
-
Status des Systems für jeden Zeitpunkt rekonstruierbar (time machine)
-
Eine Stelle der Wahrheit für das ganze System
-
jederzeit neue Lesemodelle aufbaubar
-
debugging ist sehr viel einfacher (auch lokal)
-
Auditing
Eventstore
- Einfaches Event-Log meist nicht ausreichend
- weitere Features benötigt:
- Subscriptions: EventStore benachrichtigt Konsumenten über neue Events
- Filter-Queries: Konsument kann festlegen, welcher Events ihn interessieren
- Implementierung = Eventstore
- EventStore™ https://eventstore.org/
- FactCast https://factcast.org/
Wie sieht ein Event aus?
- abhängig vom verwendeten EventStore
- zumeist JSON
- Event + Metadaten:
- FactCast: Header & Payload
- EventStore™: MetaData & EventData
- keine Java Serialisierung verwenden (wg. Erweiterbarkeit)
- Event-Konsumenten lesen oft nur den für sie 'interessanten Teil' des Events
Event-Anatomie
header:
{
"id" : "1bd92ce5-b419-4adc-879f-19b20c1bf9e0",
"ns" : "jugsaxony",
"type" : "camp.jugsaxony.es.event.cart.LineItemAdded",
"aggIds" : [
"5c000000-0000-0000-0000-000000000001"
]
}
payload:
{
"cartId" : "5c000000-0000-0000-0000-000000000001",
"customerId": "c0000000-0000-0000-0000-000000000001",
"articleId" : "a0000000-0000-0000-0000-000000000002",
"quantity" : 2
}
Beispiel FactCast / JSON
Event als Java Objekt
@Builder @Getter
public class LineItemAdded implements Event{
UUID customerId; // not stricly necessary, but helpful Context information
@AggregateId
UUID cartId;
UUID articleId;
int quantity;
}
LineItemAdded event = new LineItemAdded()
.articleId(tshirtId)
.cartId(cartId)
.customerId(customerId)
.quantity(2);
eventStore.publish( Util.toFact(event) );
Event-Verarbeitung
Pull Modelle
- lesen auf Anfrage Events aus dem Store und erzeugen ihren "aktuellen" Zustand
- Aktualisierung erfolgt synchron zur Anfrage
- strikt konsistent mit dem Log
Objekt-Modell
@Data
public class Cart {
final UUID customerId;
final UUID cartId;
final Map<UUID, LineItem> lineItems = new HashMap<>();
boolean closed = false;
}
@Data
public class LineItem {
final UUID articleId;
int quantity = 1;
}
ReadModel erstellen
class CartReadModel extends PullReadModel {
@Getter private Cart cart;
CartReadModel(FactCast eventStore, UUID cartId) {
super(eventStore,()->FactSpec.ns("jugsaxony").aggId(cartId));
cart = new Cart(cartId);
}
public void apply(LineItemAdded event) {
LineItem li = new LineItem(event.getArticleId());
li.setQuantity(event.getQuantity());
cart.getLineItems().put(li.getArticleId(), li);
}
public void apply(LineItemRemoved event) {
cart.getLineItems().remove(event.getArticleId());
}
public void apply(LineItemQuantityChanged fact) {
cart.getLineItems().get(fact.getArticleId()).setQuantity(fact.getQuantity());
}
public void apply(CheckoutStarted event) {
cart.setClosed(true);
}
}
PullReadModel
public class PullReadModel extends ReadModel {
public PullReadModel(FactCast eventStore, Supplier<FactSpec> spec) {
super(eventStore, spec);
}
private void pullFromScratch() {
eventStore.subscribeToFacts(
SubscriptionRequest.catchup(factsOfInterest).fromScratch(),
this)
.awaitComplete();
}
//...
}
Beispiel: Repository
public interface CartRepository {
Cart findByCartId(UUID cartId);
}
@Component @RequiredArgsConstructor
public class CartRepositoryImpl implements CartRepository {
final FactCast eventStore;
@Override
public Cart findByCartId(UUID cartId) {
return new CartReadModel(eventStore, cartId).pull();
}
}
Jedesmal alle Events auffinden?
Ist das nicht zu aufwändig?
Rolling Snapshot
@Service @RequiredArgsConstructor
public class CartSnapshotRepositoryImpl implements CartRepository {
final FactCast eventStore;
final Map<UUID, CartReadModel> modelCache = new LRUMap<>();
@Override
public Cart findByCartId(UUID cartId) {
CartReadModel readModel = modelCache.computeIfAbsent(cartId,
id -> new CartReadModel( eventStore, id ) );
readModel.pull();
return readModel.getCart();
}
}
PullReadModel (Snapshot)
public class PullReadModel extends ReadModel {
// ...
public void pull() {
if (lastFactConsumed == null) {
pullFromScratch();
} else {
eventStore.subscribeToFacts(
SubscriptionRequest.catchup(factsOfInterest)
.from(lastFactConsumed), this)
.awaitComplete();
}
}
}
Bei jedem Lesevorgang den Eventstore fragen?
Was tun bei großer Leselast?
Push Modelle
- registrieren sich am Eventstore
- lassen sich interessante Events zusenden, sobald sie auftreten
- Aktualisierung erfolgt asynchron zur Anfrage
- geringere Latenz
- nicht immer strikt konsistent
(Events können bei Anfrage gerade 'unterwegs' sein)
Push
@Service
public class RecommendedArticles extends PushReadModel {
final Map<UUID, Set<UUID>> recommendedArticlesByCustomer =
new ConcurrentHashMap<>();
public void apply(LineItemRemoved event) {
Set<UUID> list = getForCustomer(event.getCustomerId());
list.add(event.getArticleId());
}
public void apply(CheckoutStarted event) {
List<UUID> articlesInCart = event.getLineItems()
.stream()
.map(LineItem::getArticleId)
.collect(Collectors.toList());
Set<UUID> list = getForCustomer(event.getCustomerId());
list.removeAll(articlesInCart);
}
// ...
}
Push
public class PushReadModel extends ReadModel implements SmartInitializingSingleton {
public PushReadModel(FactCast eventStore, Supplier<FactSpec> spec) {
super(eventStore, spec);
}
@Override
public void afterSingletonsInstantiated() {
eventStore.subscribeToFacts(
SubscriptionRequest.follow(factsOfInterest).fromScratch(), this);
}
}
putting it all together
@service @RequiredArgsConstructor public class CartService {
public void addToCart(UUID cartId, UUID articleId, UUID customerId, int quantity) {
// acquire necessary locks
Cart currentCart = cartRepo.findByCartId(cartId);
if (currentCart.isClosed()) {
throw new IllegalStateException("Already closed");
}
if (currentCart.getLineItems().containsKey(articleId)) {
// increaseQuantity of the corresponding LineItem;
LineItem lineItem = currentCart.getLineItems().get(articleId);
LineItemQuantityChanged event = new LineItemQuantityChanged()...
eventStore.publish(Util.toFact(event));
} else {
// add new LineItem
LineItemAdded event = new LineItemAdded()...
eventStore.publish(Util.toFact(event));
}
// release locks
}
// ...
}
Links
- https://eventstore.org/
- https://factcast.org/
- https://gitlab.com/uweschaefer/jugsaxonycamp
Q & A
Lizenz
Diese Folien stehen unter CC BY-SA 4.0 Lizenz
https://creativecommons.org/licenses/by-sa/4.0/
Event Sourcing
By joergadler