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

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