Hrvatski

Saznajte kako Event Sourcing revolucionira revizijske zapise, nudeći neusporedivu sljedivost, integritet podataka i otpornost sustava. Istražite primjere i strategije implementacije.

Event Sourcing: Implementacija revizijskih zapisa za robusne sustave koji se mogu pratiti

U današnjem složenom i međusobno povezanom digitalnom okruženju, održavanje robusnog i sveobuhvatnog revizijskog zapisa od najveće je važnosti. To nije samo često regulatorni zahtjev, već je i ključno za otklanjanje grešaka, sigurnosnu analizu i razumijevanje evolucije vašeg sustava. Event Sourcing, arhitektonski obrazac koji bilježi sve promjene stanja aplikacije kao niz događaja, nudi elegantno i moćno rješenje za implementaciju revizijskih zapisa koji su pouzdani, revizijski i proširivi.

Što je Event Sourcing?

Tradicionalne aplikacije obično pohranjuju samo trenutno stanje podataka u bazu podataka. Ovaj pristup otežava rekonstrukciju prošlih stanja ili razumijevanje niza događaja koji su doveli do trenutnog stanja. Event Sourcing, nasuprot tome, fokusira se na bilježenje svake značajne promjene stanja aplikacije kao nepromjenjivog događaja. Ti se događaji pohranjuju u repozitorij događaja koji se može samo nadopunjavati, tvoreći potpunu i kronološku evidenciju svih radnji unutar sustava.

Zamislite to kao bankovnu knjigu. Umjesto jednostavnog bilježenja trenutnog stanja, svaki polog, isplata i prijenos bilježe se kao zaseban događaj. Ponovnim reproduciranjem tih događaja možete rekonstruirati stanje računa u bilo kojem trenutku.

Zašto koristiti Event Sourcing za revizijske zapise?

Event Sourcing nudi nekoliko uvjerljivih prednosti za implementaciju revizijskih zapisa:

Implementacija Event Sourcinga za revizijske zapise: Vodič korak po korak

Ovo je praktični vodič za implementaciju Event Sourcinga za revizijske zapise:

1. Identificirajte ključne događaje

Prvi korak je identificirati ključne događaje koje želite zabilježiti u svom revizijskom zapisu. Ti događaji trebaju predstavljati značajne promjene u stanju aplikacije. Razmotrite radnje kao što su:

Primjer: Za platformu e-trgovine, ključni događaji mogu uključivati "NarudžbaKreirana", "PlaćanjePrimljeno", "NarudžbaPoslana", "ProizvodDodanuKošaricu" i "KorisničkiProfilAžuriran".

2. Definirajte strukturu događaja

Svaki događaj treba imati dobro definiranu strukturu koja uključuje sljedeće informacije:

Primjer: Događaj "OrderCreated" može imati sljedeću strukturu:

{
  "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. Odaberite repozitorij događaja

Repozitorij događaja je središnje spremište za pohranu događaja. Trebala bi biti baza podataka koja se može samo nadopunjavati, optimizirana za pisanje i čitanje nizova događaja. Dostupno je nekoliko opcija:

Pri odabiru repozitorija događaja, razmotrite čimbenike kao što su:

4. Implementirajte objavljivanje događaja

Kada se dogodi događaj, vaša ga aplikacija treba objaviti u repozitoriju događaja. To obično uključuje sljedeće korake:

Primjer (koristeći hipotetski EventStoreService):

public class OrderService {

  private final EventStoreService eventStoreService;

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

  public void createOrder(Order order, String userId) {
    // ... poslovna logika za kreiranje narudžbe ...

    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) {
    // Stvorite objekt događaja
    EventRecord eventRecord = new EventRecord(
        UUID.randomUUID(), // eventId
        streamName,  // streamName
        entityId,   // entityId
        event.getClass().getName(), // eventType
        toJson(event),  // eventData
        Instant.now().toString(), // timestamp
        userId  // userId
    );

    // Serijalizirajte događaj
    String serializedEvent = toJson(eventRecord);

    // Dodajte događaj u repozitorij događaja (implementacija specifična za odabrani repozitorij događaja)
    storeEventInDatabase(serializedEvent);

    // Objavite događaj pretplatnicima (opcionalno)
    publishEventToMessageQueue(serializedEvent);
  }

  // Zamjenske metode za interakciju s bazom podataka i redom poruka
  private void storeEventInDatabase(String serializedEvent) {
    // Implementacija za pohranu događaja u bazu podataka
    System.out.println("Pohranjivanje događaja u bazu podataka: " + serializedEvent);
  }

  private void publishEventToMessageQueue(String serializedEvent) {
    // Implementacija za objavljivanje događaja u red poruka
    System.out.println("Objavljivanje događaja u red poruka: " + serializedEvent);
  }

  private String toJson(Object obj) {
    // Implementacija za serijalizaciju događaja u JSON
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.writeValueAsString(obj);
    } catch (Exception e) {
      throw new RuntimeException("Greška pri serijalizaciji događaja u 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. Izgradite modele za čitanje (Projekcije)

Iako repozitorij događaja pruža potpunu povijest svih promjena, često nije učinkovito izravno ga pretraživati za operacije čitanja. Umjesto toga, možete izgraditi modele za čitanje, poznate i kao projekcije, koji su optimizirani za specifične obrasce upita. Ti se modeli za čitanje izvode iz toka događaja i ažuriraju se asinkrono kako se objavljuju novi događaji.

Primjer: Možete stvoriti model za čitanje koji sadrži popis svih narudžbi za određenog kupca, ili model za čitanje koji sažima podatke o prodaji za određeni proizvod.

Za izgradnju modela za čitanje, pretplatite se na tok događaja i obradite svaki događaj. Za svaki događaj, ažurirajte model za čitanje u skladu s tim.

Primjer:

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);
    }

    // Ostali rukovatelji događajima za PaymentReceivedEvent, OrderShippedEvent itd.
}

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. Osigurajte repozitorij događaja

Repozitorij događaja sadrži osjetljive podatke, stoga je ključno pravilno ga osigurati. Razmotrite sljedeće sigurnosne mjere:

7. Implementirajte reviziju i izvještavanje

Nakon što ste implementirali Event Sourcing, možete koristiti tok događaja za generiranje revizijskih izvještaja i provođenje sigurnosne analize. Možete pretraživati repozitorij događaja kako biste pronašli sve događaje povezane s određenim korisnikom, transakcijom ili entitetom. Također možete koristiti tok događaja za rekonstrukciju stanja sustava u bilo kojem trenutku.

Primjer: Možete generirati izvještaj koji prikazuje sve promjene napravljene na određenom korisničkom profilu tijekom određenog vremenskog razdoblja, ili izvještaj koji prikazuje sve transakcije pokrenute od strane određenog korisnika.

Razmotrite sljedeće mogućnosti izvještavanja:

Izazovi Event Sourcinga

Iako Event Sourcing nudi mnoge prednosti, također predstavlja i neke izazove:

Najbolje prakse za Event Sourcing

Kako biste ublažili izazove Event Sourcinga, slijedite ove najbolje prakse:

Primjeri Event Sourcinga iz stvarnog svijeta

Event Sourcing se koristi u raznim industrijama i aplikacijama, uključujući:

Zaključak

Event Sourcing je moćan arhitektonski obrazac koji može revolucionirati vašu implementaciju revizijskih zapisa. Pruža neusporedivu sljedivost, integritet podataka i otpornost sustava. Iako predstavlja neke izazove, prednosti Event Sourcinga često nadmašuju troškove, posebno za složene i kritične sustave. Slijedeći najbolje prakse navedene u ovom vodiču, možete uspješno implementirati Event Sourcing i izgraditi robusne sustave koji se mogu revidirati.

Daljnje čitanje