Svenska

Lär dig hur Event Sourcing kan revolutionera din implementering av granskningsspår, vilket erbjuder oöverträffad spårbarhet, dataintegritet och systemåterhämtning. Utforska praktiska exempel och implementeringsstrategier.

Event Sourcing: Implementera granskningsspår för robusta och spårbara system

I dagens komplexa och sammankopplade digitala landskap är det av största vikt att upprätthålla ett robust och omfattande granskningsspår. Det är inte bara ofta ett lagstadgat krav, utan det är också avgörande för felsökning, säkerhetsanalys och förståelse av systemets utveckling. Event Sourcing, ett arkitektoniskt mönster som fångar alla ändringar i en applikations tillstånd som en sekvens av händelser, erbjuder en elegant och kraftfull lösning för att implementera granskningsspår som är pålitliga, granskningsbara och utbyggbara.

Vad är Event Sourcing?

Traditionella applikationer lagrar vanligtvis bara det aktuella tillståndet för data i en databas. Detta tillvägagångssätt gör det svårt att rekonstruera tidigare tillstånd eller förstå den serie av händelser som ledde till det aktuella tillståndet. Event Sourcing, i motsats till detta, fokuserar på att fånga varje betydande ändring av applikationens tillstånd som en oföränderlig händelse. Dessa händelser lagras i en händelselagringsplats där man endast kan lägga till (append-only), vilket bildar en komplett och kronologisk registrering av alla åtgärder inom systemet.

Tänk på det som en bankkontos reskontra. Istället för att bara registrera det aktuella saldot registreras varje insättning, uttag och överföring som en separat händelse. Genom att spela upp dessa händelser kan du rekonstruera kontots tillstånd vid vilken tidpunkt som helst.

Varför använda Event Sourcing för granskningsspår?

Event Sourcing erbjuder flera övertygande fördelar för att implementera granskningsspår:

Implementera Event Sourcing för granskningsspår: En steg-för-steg-guide

Här är en praktisk guide för att implementera Event Sourcing för granskningsspår:

1. Identifiera nyckelhändelser

Det första steget är att identifiera de nyckelhändelser som du vill fånga i ditt granskningsspår. Dessa händelser bör representera betydande ändringar av applikationens tillstånd. Tänk på åtgärder som:

Exempel: För en e-handelsplattform kan nyckelhändelser inkludera "OrderCreated", "PaymentReceived", "OrderShipped", "ProductAddedToCart" och "UserProfileUpdated."

2. Definiera händelsestruktur

Varje händelse bör ha en väldefinierad struktur som inkluderar följande information:

Exempel: Händelsen "OrderCreated" kan ha följande struktur:

{
  "eventType": "OrderCreated",
  "eventData": {
    "orderId": "12345",
    "customerId": "67890",
    "orderDate": "2023-10-27T10:00:00Z",
    "totalAmount": 100.00,
    "currency": "USD",
    "shippingAddress": {
      "street": "123 Main St",
      "city": "Anytown",
      "state": "CA",
      "zipCode": "91234",
      "country": "USA"
    }
  },
  "timestamp": "2023-10-27T10:00:00Z",
  "userId": "user123",
  "transactionId": "tx12345",
  "correlationId": "corr123",
  "metadata": {
    "ipAddress": "192.168.1.1",
    "browser": "Chrome",
    "location": {
       "latitude": 34.0522,
       "longitude": -118.2437
    }
  }
}

3. Välj en händelselagring

Händelselagringen är det centrala lagret för att lagra händelser. Det bör vara en databas där man endast kan lägga till (append-only) som är optimerad för att skriva och läsa sekvenser av händelser. Flera alternativ är tillgängliga:

När du väljer en händelselagring, överväg faktorer som:

4. Implementera händelsepublicering

När en händelse inträffar måste din applikation publicera den till händelselagringen. Detta innebär vanligtvis följande steg:

Exempel (med en hypotetisk EventStoreService):

public class OrderService {

  private final EventStoreService eventStoreService;

  public OrderService(EventStoreService eventStoreService) {
    this.eventStoreService = eventStoreService;
  }

  public void createOrder(Order order, String userId) {
    // ... business logic to create the order ...

    OrderCreatedEvent event = new OrderCreatedEvent(
        order.getOrderId(),
        order.getCustomerId(),
        order.getOrderDate(),
        order.getTotalAmount(),
        order.getCurrency(),
        order.getShippingAddress()
    );

    eventStoreService.appendEvent("order", order.getOrderId(), event, userId);
  }
}

public class EventStoreService {

  public void appendEvent(String streamName, String entityId, Object event, String userId) {
    // Create an event object
    EventRecord eventRecord = new EventRecord(
        UUID.randomUUID(), // eventId
        streamName,  // streamName
        entityId,   // entityId
        event.getClass().getName(), // eventType
        toJson(event),  // eventData
        Instant.now().toString(), // timestamp
        userId  // userId
    );

    // Serialize the event
    String serializedEvent = toJson(eventRecord);

    // Append the event to the event store (implementation specific to the chosen event store)
    storeEventInDatabase(serializedEvent);

    // Publish the event to subscribers (optional)
    publishEventToMessageQueue(serializedEvent);
  }

  // Placeholder methods for database and message queue interaction
  private void storeEventInDatabase(String serializedEvent) {
    // Implementation to store the event in the database
    System.out.println("Storing event in database: " + serializedEvent);
  }

  private void publishEventToMessageQueue(String serializedEvent) {
    // Implementation to publish the event to a message queue
    System.out.println("Publishing event to message queue: " + serializedEvent);
  }

  private String toJson(Object obj) {
    // Implementation to serialize the event to JSON
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.writeValueAsString(obj);
    } catch (Exception e) {
      throw new RuntimeException("Error serializing event to JSON", e);
    }
  }
}


class EventRecord {
  private final UUID eventId;
  private final String streamName;
  private final String entityId;
  private final String eventType;
  private final String eventData;
  private final String timestamp;
  private final String userId;

  public EventRecord(UUID eventId, String streamName, String entityId, String eventType, String eventData, String timestamp, String userId) {
    this.eventId = eventId;
    this.streamName = streamName;
    this.entityId = entityId;
    this.eventType = eventType;
    this.eventData = eventData;
    this.timestamp = timestamp;
    this.userId = userId;
  }

  // Getters

  @Override
  public String toString() {
    return "EventRecord{" +
        "eventId=" + eventId +
        ", streamName='" + streamName + '\'' +
        ", entityId='" + entityId + '\'' +
        ", eventType='" + eventType + '\'' +
        ", eventData='" + eventData + '\'' +
        ", timestamp='" + timestamp + '\'' +
        ", userId='" + userId + '\'' +
        '}';
  }
}

class OrderCreatedEvent {
    private final String orderId;
    private final String customerId;
    private final String orderDate;
    private final double totalAmount;
    private final String currency;
    private final String shippingAddress;

    public OrderCreatedEvent(String orderId, String customerId, String orderDate, double totalAmount, String currency, String shippingAddress) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
        this.shippingAddress = shippingAddress;
    }

    // Getters for all fields

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getOrderDate() { return orderDate; }
    public double getTotalAmount() { return totalAmount; }
    public String getCurrency() { return currency; }
    public String getShippingAddress() { return shippingAddress; }

    @Override
    public String toString() {
        return "OrderCreatedEvent{" +
                "orderId='" + orderId + '\'' +
                ", customerId='" + customerId + '\'' +
                ", orderDate='" + orderDate + '\'' +
                ", totalAmount=" + totalAmount +
                ", currency='" + currency + '\'' +
                ", shippingAddress='" + shippingAddress + '\'' +
                '}';
    }
}

class Order {
  private final String orderId;
  private final String customerId;
  private final String orderDate;
  private final double totalAmount;
  private final String currency;
  private final String shippingAddress;

  public Order(String orderId, String customerId, String orderDate, double totalAmount, String currency, String shippingAddress) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
        this.shippingAddress = shippingAddress;
    }

    // Getters for all fields

    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
    public String getOrderDate() { return orderDate; }
    public double getTotalAmount() { return totalAmount; }
    public String getCurrency() { return currency; }
    public String getShippingAddress() { return shippingAddress; }

    @Override
    public String toString() {
        return "Order{" +
                "orderId='" + orderId + '\'' +
                ", customerId='" + customerId + '\'' +
                ", orderDate='" + orderDate + '\'' +
                ", totalAmount=" + totalAmount +
                ", currency='" + currency + '\'' +
                ", shippingAddress='" + shippingAddress + '\'' +
                '}';
    }
}

5. Bygg läsmodeller (projektioner)

Medan händelselagringen ger en fullständig historik över alla ändringar, är det ofta inte effektivt att fråga den direkt för läsoperationer. Istället kan du bygga läsmodeller, även kända som projektioner, som är optimerade för specifika frågemönster. Dessa läsmodeller härleds från händelseströmmen och uppdateras asynkront när nya händelser publiceras.

Exempel: Du kan skapa en läsmodell som innehåller en lista över alla order för en specifik kund, eller en läsmodell som sammanfattar försäljningsdata för en viss produkt.

För att bygga en läsmodell prenumererar du på händelseströmmen och bearbetar varje händelse. För varje händelse uppdaterar du läsmodellen i enlighet därmed.

Exempel:

public class OrderSummaryReadModelUpdater {

    private final OrderSummaryRepository orderSummaryRepository;

    public OrderSummaryReadModelUpdater(OrderSummaryRepository orderSummaryRepository) {
        this.orderSummaryRepository = orderSummaryRepository;
    }

    public void handle(OrderCreatedEvent event) {
        OrderSummary orderSummary = new OrderSummary(
                event.getOrderId(),
                event.getCustomerId(),
                event.getOrderDate(),
                event.getTotalAmount(),
                event.getCurrency()
        );

        orderSummaryRepository.save(orderSummary);
    }

    // Other event handlers for PaymentReceivedEvent, OrderShippedEvent, etc.
}

interface OrderSummaryRepository {
    void save(OrderSummary orderSummary);
}

class OrderSummary {
    private final String orderId;
    private final String customerId;
    private final String orderDate;
    private final double totalAmount;
    private final String currency;

    public OrderSummary(String orderId, String customerId, String orderDate, double totalAmount, String currency) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.orderDate = orderDate;
        this.totalAmount = totalAmount;
        this.currency = currency;
    }
    //Getters
}

6. Säkra händelselagringen

Händelselagringen innehåller känslig data, så det är avgörande att säkra den ordentligt. Överväg följande säkerhetsåtgärder:

7. Implementera granskning och rapportering

När du har implementerat Event Sourcing kan du använda händelseströmmen för att generera granskningsrapporter och utföra säkerhetsanalys. Du kan fråga händelselagringen för att hitta alla händelser relaterade till en specifik användare, transaktion eller entitet. Du kan också använda händelseströmmen för att rekonstruera systemets tillstånd vid vilken tidpunkt som helst.

Exempel: Du kan generera en rapport som visar alla ändringar som gjorts i en specifik användarprofil under en tidsperiod, eller en rapport som visar alla transaktioner som initierats av en viss användare.

Överväg följande rapporteringsfunktioner:

Utmaningar med Event Sourcing

Medan Event Sourcing erbjuder många fördelar, presenterar det också vissa utmaningar:

Bästa metoder för Event Sourcing

För att mildra utmaningarna med Event Sourcing, följ dessa bästa metoder:

Verkliga exempel på Event Sourcing

Event Sourcing används i en mängd olika branscher och applikationer, inklusive:

Slutsats

Event Sourcing är ett kraftfullt arkitektoniskt mönster som kan revolutionera din implementering av granskningsspår. Det ger oöverträffad spårbarhet, dataintegritet och systemåterhämtning. Även om det presenterar vissa utmaningar, uppväger fördelarna med Event Sourcing ofta kostnaderna, särskilt för komplexa och kritiska system. Genom att följa de bästa metoderna som beskrivs i den här guiden kan du framgångsrikt implementera Event Sourcing och bygga robusta och granskningsbara system.

Vidare läsning