^und viele Schreib- und Lesevorgänge

03.04.2015

Nachdem wir uns mit Hibernate einer wochenlangen Problemanalyse stellen mussten, will ich euch für die Zukunft ein paar Erkenntnisse mitgeben, damit euch das vielleicht nicht auch passiert.

Herausforderung: Gehe eine Datenbank mit etwa 50.000 Zeilen durch und erzeuge aus einer nicht relationalen Datenstruktur eine relationale Objekt-Datenstruktur, welche auch durch ein ORM abgebildet werden kann. Hierbei wurde zunächst für die Verknüpfung der Objekte untereinander auf FKs gesetzt und auf die Auto-Increment-Funktion von MySQL gesetzt, um bei Neu-Einträgen eine neue ID zu bekommen. Also musste für jeden „Parent“-Eintrag auch ein Commit auf die DB ausgeführt werden, mit anschließendem SELECT, damit im Hibernate ein entsprechendes „geupdatedes“ Objekt mit gültiger ID vorlag. Die Verschachtelungstiefe pro Eintrag aus dieser Datenbank ist zwar nicht hoch (5), man kann sich aber durchaus vorstellen, dass es hier dennoch zu vielen Schreib- und Lesezyklen kommt.

Durch die vielen Commits scheint es wohl nun so zu sein, dass bei etwa der Hälfte der Records (20.000) erfolgreich geschriebene Einträge (Hibernate meldet dies zumindest) bei einem SELECT definitiv nicht in der Datenbank zu finden sind. Ein kurzes Warten löste zwar zunächst das Problem, war aber so unzuverlässig, dass damit das Problem nur nach hinten verschoben wurde, bis man schlussendlich pro Eintrag mehrere Minuten warten musste, bis dieser in der DB gefunden werden konnte. Session und Transaction wurden bei allen Einträgen geschlossen und auch als erfolgreich geschlossen gemeldet. Dies scheint auch genau das Problem zu sein. Hierbei werden im JDBC-Treiber von MySQL mehrere Threads für einzelne Sessions geöffnet, welche asynchron verarbeitet werden und während die Verbindung zum Treiber zwar schon geschlossen wurde, arbeitet der Treiber weiter und schreibt irgendwann den Eintrag schlussendlich in die DB.

Lösung:

  1. Wir haben die ID-Generierung Java-seitig durch einen eigenen ID-Generator ersetzt und den AI für die PK-Spalte abgeschaltet:

   @Id
@GenericGenerator(name = „seq_id“, strategy = „d.b.e.d.r.d.c.d.u.SimpleIdGenerator“)
@GeneratedValue(generator = „seq_id“)

Dieser holt sich zunächst den letzten Eintrag in der Tabelle, um die neu zu generierende ID zu ermitteln und synchronisiert sich alle X-Einträge wieder. Nachteil an dieser Lösung ist, dass zwei Prozesse nicht gleichzeitig so einen Import fahren dürfen. Da dies aber in unserem Umfeld garantiert wird haben wir uns für diese Lösung entschieden.

  1. Hierdurch war es nun möglich, die Transaktionen nur noch zeilenweise zu commiten => Verringerung der Schreib- und Lesezugriffe
  2. Einbau eines Caching-Mechanismus, um bereits geschrieben Einträge Java-seitig vorzuhalten und nicht erneut aus der Datenbank auslesen zu müssen (inkl. Prüfung, ob Eintrag bereits vorhanden)
  3. Hibernate.cfg.xml um folgende Einstellungen erweitert:
  • <property name=“hibernate.jdbc.fetch_size“>1000</property> http://webmoli.com/2009/02/01/jdbc-performance-tuning-with-optimal-fetch-size/

Hier ist der Default-Wert bei 10 Einträgen. Das bedeutet, dass bei einem Fetch über mehr als 10 Werten im Treiber mehrere Schleifen und Verbindungen zum DB-Server aufgebaut werden, um alle Einträge zu erhalten. Durch Setzen des Wertes auf etwas Größeres werden mehr Einträge auf einmal an den Treiber übermittelt und dadurch die Response-Zeit verringert.

Dieser Eintrag sorgt dafür, dass automatisch nach 100 Einträgen ein Commit auf die DB ausgeführt wird (Session und Transaktionsunabhängig). Hierbei ist darauf zu achten, dass es beim Debugging zu Nachteilen kommen kann, da man den konkreten Fehler unter Umständen erst später bekommt und diesen dann nicht genau zuordnen kann. Für Debugging also lieber ausschalten.

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*