Optimistic vs Pessimistic Locking und Transaktionssicherheit

Was ist Transaktionssicherheit und wozu ist sie gut? Die klassischen relationalen Datenbanksysteme wie MySQL, Postgres, … nutzen von Haus aus ACID (Atomicity, Consistency, Isolation, Durability). Wenn wir eine Transaktion starten, mehrere Operationen in der Datenbank vornehmen und eine davon schief geht, findet ein Rollback statt und keine der Änderungen wird übernommen. Erst wenn alle Operationen in der Datenbank erfolgreich durchgeführt wurden, wird die ganze Transaktion committed und in die Datenbank geschrieben. Das schützt uns schon mal vor Inkonsistenz der Daten. Das bezieht sich jetzt jedoch auf eine einzelne Transaktion, bei optimistic und pessimistic Locking geht es jedoch darum, wenn mehrere Leute auf die gleichen Objekte zugreifen möchten. Bei einer Applikation mit einer Datenbank im Hintergrund (also so gut wie jede Applikation) mit einer höheren Nutzeranzahl, ist es wahrscheinlich, dass mal zwei Nutzer gleichzeitig versuchen, ein Objekt zu beschreiben und erst recht zu lesen. Im weiteren Verlauf dieses Artikels beschreibe ich Optimistic vs Pessimistic Locking und erkläre die Unterschiede.


 Optimistic Locking

Beim Optimistic Locking hat jeder Eintrag in der Datenbank eine Versionsnummer. Beim Abfragen eines Eintrages weiß das abgefragte Objekt, welches die aktuelle Version ist. Wenn das Objekt wieder in die Datenbank geschrieben wird, wird die Version erneut geprüft. Wenn die Version immer noch die alte Version ist, wurde das Objekt nicht verändert und kann problemlos beschrieben werden. Wenn sich die Version verändert hat (dirty), wird die Transaktion abgebrochen und der Nutzer muss die Aktion erneut ausführen (im Normalfall wird eine Exception geworfen). Optimistic Locking wird deutlich häufiger genutzt und eignet sich für Systeme mit hoher Last, in der man nicht viele Kollisionen erwartet. Neben der Strategie mit der Versionsnummer kann man auch noch einen Timestamp oder einen Rowstate (Status einer einzelnen Zeile) nutzen. Lesevorgänge werden nicht beeinflusst.

In Java lässt sich das Ganze mittels JPA umsetzen. Dazu wird in der Entität eine version-Property mit der Annotation @Version benötigt. Getter und Setter sind nicht notwendig.

@Entity
public class MyEntity implements Serializable {    

    @Id
    @GeneratedValue
    private Long id;

    private String otherProperty;

    @Version
    private Long version;

    // Getter/Setter für ID und andere Eigenschaften
}

In der Datenbank wird dann eine neue Spalte „version“ benötigt. Um die Versionsnummer muss man sich jetzt nicht mehr kümmern, das passiert alles automatisch. Wenn man Optimistic Locking für alle Entitäten realisieren möchte, bietet sich auch eine abstrakte Klasse mit der Annotation @MappedSuperclass an.

import java.io.Serializable;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

@MappedSuperclass
public abstract class AbstractEntity implements Serializable
{

  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  @Version
  private Long version;

  public Long getId()
  {
    return id;
  }

  public void setId(Long id)
  {
    this.id = id;
  }

  @Override
  public int hashCode()
  {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj)
  {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    AbstractEntity other = (AbstractEntity) obj;
    if (id == null)
    {
      if (other.id != null)
        return false;
    }
    else if (!id.equals(other.id))
      return false;
    return true;
  }

}

Unsere Entitäten erben dann von der AbstractEntity.

public class MyEntity extends AbstractEntity
{

// Eigenschaften

}

Pessimistic Locking

Pessimistic Locking hingegen blockiert die Datensätze/den Datensatz für eine exklusive Benutzung solange, bis der Lock wieder freigegeben wird. Das bietet eine deutlich höhere Integrität als Optimistic Locking kann jedoch auch bei schlechter Softwarearchitektur bzw. schlechtem Design der Applikation einen Deadlock produzieren (Daten sind gar nicht mehr abrufbar). Um Pessimstic Locking zu nutzen ist entweder eine direkte Datendankverbindung notwendig oder eine unabhängige Transaktionsid zur Identifikation. Anhand dieser Transaktionsid wird die exklusive Nutzung sichergestellt. Auch in diesem Fall wird die Transaktion zurückgesetzt (Rollback), wenn eine Pessimstic Lock Exception auftritt.

Wenn man mit vielen Kollisionen rechnet, welche die Synchronisation und Konsistenz der Daten stören würden, ist Pessimistic Locking angebracht. Pessimistic Locking ist zwar vorhersehbar, verringert jedoch die Parallelität (Concurrency).

Man unterscheidet zwischen drei verschiedenen Lock-Typen

  • PESSIMISTIC_READ – Wenn die Transaktion die Entität liest, wird diese blockiert, erlaubt jedoch weitere Lesevorgänge von andereren Transaktionen
  • PESSIMISTIC_WRITE – Wenn die Transaktion die Entität aktualisiert, wird diese blockiert und verbietet weitere Lese-, Schreib- oder Löschvorgänge
  • PESSIMISTIC_FORCE_INCREMENT – Wenn die Transaktion die Entität liest, wird das version-Attribut (falls vorhanden) erhöht
@PersistenceContext
  private EntityManager entityManager;

  public void decreaseStock(String id) {
      Product product = 
          entityManager.find(Product.class, id);
      entityManager.lock(product,
          LockModeType.OPTIMISTIC_WRITE);
      product.setStock(product.getStock() - 1);
  }

Comments

comments

Leave a Reply