Suomi

Event Sourcing parantaa auditointipolkujen jäljitettävyyttä, tietojen eheyttä ja järjestelmän joustavuutta. Tutustu käytännön esimerkkeihin ja strategioihin.

Tapahtumalähtöisyys: Auditointipolkujen toteuttaminen vankkoihin ja jäljitettäviin järjestelmiin

Nykypäivän monimutkaisessa ja toisiinsa kytkeytyneessä digitaalisessa maisemassa vankan ja kattavan auditointipolun ylläpitäminen on ensiarvoisen tärkeää. Se ei ole ainoastaan usein lainsäädännöllinen vaatimus, vaan myös ratkaisevan tärkeä virheenkorjauksessa, tietoturva-analyysissä ja järjestelmän kehityksen ymmärtämisessä. Tapahtumalähtöisyys (Event Sourcing), arkkitehtuurimalli, joka tallentaa kaikki sovelluksen tilan muutokset tapahtumasarjana, tarjoaa elegantin ja tehokkaan ratkaisun luotettavien, auditoitavien ja laajennettavien auditointipolkujen toteuttamiseen.

Mitä on tapahtumalähtöisyys?

Perinteiset sovellukset tallentavat tyypillisesti vain tietojen nykyisen tilan tietokantaan. Tämä lähestymistapa tekee menneiden tilojen rekonstruoinnista tai nykyiseen tilaan johtaneiden tapahtumasarjojen ymmärtämisestä vaikeaa. Tapahtumalähtöisyys sen sijaan keskittyy tallentamaan jokaisen merkittävän muutoksen sovelluksen tilaan muuttumattomana tapahtumana. Nämä tapahtumat tallennetaan vain lisättävään tapahtumavarastoon, muodostaen täydellisen ja kronologisen kirjanpidon kaikista järjestelmän toiminnoista.

Ajattele sitä kuin pankkitilin tiliotetta. Sen sijaan, että kirjattaisiin vain nykyinen saldo, jokainen talletus, nosto ja siirto kirjataan erilliseksi tapahtumaksi. Näiden tapahtumien toistamisella voit rekonstruoida tilin tilan missä tahansa ajankohdassa.

Miksi käyttää tapahtumalähtöisyyttä auditointipolkuihin?

Tapahtumalähtöisyys tarjoaa useita houkuttelevia etuja auditointipolkujen toteuttamiseen:

Tapahtumalähtöisyyden toteuttaminen auditointipolkuihin: Vaiheittainen opas

Tässä on käytännön opas tapahtumalähtöisyyden toteuttamiseen auditointipolkuihin:

1. Tunnista avaintapahtumat

Ensimmäinen vaihe on tunnistaa avaintapahtumat, jotka haluat tallentaa auditointipolkuusi. Näiden tapahtumien tulisi edustaa merkittäviä muutoksia sovelluksen tilaan. Harkitse seuraavia toimintoja:

Esimerkki: Verkkokauppaympäristössä avaintapahtumia voivat olla "OrderCreated" (Tilaus luotu), "PaymentReceived" (Maksu vastaanotettu), "OrderShipped" (Tilaus lähetetty), "ProductAddedToCart" (Tuote lisätty ostoskoriin) ja "UserProfileUpdated" (Käyttäjäprofiili päivitetty).

2. Määritä tapahtuman rakenne

Jokaisella tapahtumalla tulisi olla hyvin määritelty rakenne, joka sisältää seuraavat tiedot:

Esimerkki: "OrderCreated"-tapahtumalla voi olla seuraava rakenne:

{
  "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. Valitse tapahtumavarasto

Tapahtumavarasto on tapahtumien tallennuksen keskusarkisto. Sen tulisi olla vain lisättävä tietokanta, joka on optimoitu tapahtumasarjojen kirjoittamiseen ja lukemiseen. Saatavilla on useita vaihtoehtoja:

Tapahtumavarastoa valittaessa huomioi seuraavat tekijät:

4. Toteuta tapahtuman julkaiseminen

Kun tapahtuma tapahtuu, sovelluksesi täytyy julkaista se tapahtumavarastoon. Tämä sisältää tyypillisesti seuraavat vaiheet:

Esimerkki (käyttäen hypoteettista 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. Luo lukumallit (projektiot)

Vaikka tapahtumavarasto tarjoaa täydellisen historian kaikista muutoksista, sitä ei usein ole tehokasta kysellä suoraan lukuoperaatioita varten. Sen sijaan voit rakentaa lukumalleja, joita kutsutaan myös projektioiksi, jotka on optimoitu tietyille kyselymalleille. Nämä lukumallit johdetaan tapahtumavirrasta ja päivitetään asynkronisesti uusien tapahtumien julkaisun myötä.

Esimerkki: Voit luoda lukumallin, joka sisältää luettelon kaikista tietyn asiakkaan tilauksista, tai lukumallin, joka tiivistää tietyn tuotteen myyntitiedot.

Lukumallin rakentamiseksi tilaat tapahtumavirran ja käsittelet jokaisen tapahtuman. Jokaiselle tapahtumalle päivität lukumallia vastaavasti.

Esimerkki:

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. Suojaa tapahtumavarasto

Tapahtumavarasto sisältää arkaluonteisia tietoja, joten on ratkaisevan tärkeää suojata se asianmukaisesti. Harkitse seuraavia tietoturvatoimenpiteitä:

7. Toteuta auditointi ja raportointi

Kun olet toteuttanut tapahtumalähtöisyyden, voit käyttää tapahtumavirtaa auditointiraporttien luomiseen ja tietoturva-analyysien tekemiseen. Voit kysellä tapahtumavarastosta löytääksesi kaikki tapahtumat, jotka liittyvät tiettyyn käyttäjään, transaktioon tai entiteettiin. Voit myös käyttää tapahtumavirtaa järjestelmän tilan rekonstruoimiseen missä tahansa ajankohdassa.

Esimerkki: Voit luoda raportin, joka näyttää kaikki tiettyyn käyttäjäprofiiliin tehdyt muutokset tietyn ajanjakson aikana, tai raportin, joka näyttää kaikki tietyn käyttäjän aloittamat transaktiot.

Harkitse seuraavia raportointiominaisuuksia:

Tapahtumalähtöisyyden haasteet

Vaikka tapahtumalähtöisyys tarjoaa monia etuja, se sisältää myös joitakin haasteita:

Parhaat käytännöt tapahtumalähtöisyyteen

Haasteiden lieventämiseksi noudata näitä parhaita käytäntöjä:

Todellisen maailman esimerkkejä tapahtumalähtöisyydestä

Tapahtumalähtöisyyttä käytetään useilla toimialoilla ja sovelluksissa, mukaan lukien:

Yhteenveto

Tapahtumalähtöisyys on tehokas arkkitehtuurimalli, joka voi mullistaa auditointipolkujesi toteutuksen. Se tarjoaa vertaansa vailla olevaa jäljitettävyyttä, tietojen eheyttä ja järjestelmän joustavuutta. Vaikka se esittää joitakin haasteita, tapahtumalähtöisyyden edut usein ylittävät kustannukset, erityisesti monimutkaisissa ja kriittisissä järjestelmissä. Noudattamalla tässä oppaassa esitettyjä parhaita käytäntöjä voit onnistuneesti toteuttaa tapahtumalähtöisyyden ja rakentaa vankkoja ja auditoitavia järjestelmiä.

Lisätietoa

Tapahtumalähtöisyys: Auditointipolkujen toteuttaminen vankkoihin ja jäljitettäviin järjestelmiin | MLOG