Narazil jsem i na tento problém. V podstatě se to scvrkává na to, že máte v klauzuli IN proměnlivý počet hodnot a hibernace se snaží tyto plány dotazů uložit do mezipaměti.
Na toto téma jsou dva skvělé blogové příspěvky. První:
Použití Hibernate 4.2 a MySQL v projektu s dotazem v klauzuli, jako je:
select t from Thing t where t.id in (?)
Hibernate ukládá tyto analyzované HQL dotazy do mezipaměti. Konkrétně Hibernate
SessionFactoryImpl
máQueryPlanCache
pomocíqueryPlanCache
aparameterMetadataCache
. To se však ukázalo jako problém, když je počet parametrů pro klauzuli velký a mění se.Tyto mezipaměti rostou pro každý odlišný dotaz. Takže tento dotaz s 6000parametry není stejný jako 6001.
Dotaz v klauzuli je rozšířen na počet parametrů v kolekci. Metadata jsou součástí plánu dotazů pro každý parametr v dotazu, včetně vygenerovaného názvu jako x10_, x11_ atd.
Představte si 4000 různých variací v počtu počtů parametrů v klauzuli, z nichž každý má v průměru 4000 parametrů. Metadata dotazu pro každý parametr se rychle sčítají v paměti a zaplňují hromadu, protože nelze shromažďovat odpadky.
Toto pokračuje, dokud nejsou všechny různé varianty v parametru dotazu uloženy do mezipaměti nebo dokud JVM nedojde paměť haldy a nezačne throwingjava.lang.OutOfMemoryError:Java halda prostor.
Možností je vyhnout se klauzulím a také použít pevnou velikost kolekce pro parametr (nebo alespoň menší velikost).
Pro konfiguraci maximální velikosti mezipaměti plánu dotazů viz vlastnost
hibernate.query.plan_cache_max_size
, výchozí nastavení je2048
(snadno nástroj pro dotazy s mnoha parametry).
A druhý (také odkazovaný z prvního):
Hibernate interně používá mezipaměť, která mapuje příkazy HQL (asstring) na plány dotazů. Mezipaměť se skládá z ohraničené mapy, která je standardně omezena na 2048 prvků (lze konfigurovat). Všechny dotazy HQL se načítají prostřednictvím této mezipaměti. V případě netrefení se záznam automaticky přidá do mezipaměti. Díky tomu je velmi náchylná k thrashingu – scénáři, ve kterém neustále vkládáme nové položky do mezipaměti, aniž bychom je znovu použili, a tím bráníme, aby mezipaměť přinesla jakékoli zvýšení výkonu (dokonce přidává určitou režii správy mezipaměti). A co je horší, je těžké tuto situaci náhodou odhalit - musíte si cache explicitně vyprofilovat, abyste si všimli, že tam máte problém. Později řeknu pár slov o tom, jak by se to dalo udělat.
Takže vyrovnávání mezipaměti je výsledkem nových dotazů, které jsou generovány s vysokou rychlostí. To může být způsobeno řadou problémů. Dvě nejběžnější, které jsem viděl, jsou - chyby v režimu spánku, které způsobují, že se parametry vykreslují v příkazu JPQL namísto předávání jako parametry a použití klauzule "in".
Kvůli některým nejasným chybám v hibernaci dochází k situacím, kdy parametry nejsou zpracovány správně a jsou vykreslovány do JPQLquery (jako příklad se podívejte na HHH-6280). Pokud máte dotaz, který je ovlivněn takovými defekty a je spouštěn vysokou rychlostí, rozbije vaši mezipaměť plánu dotazů, protože každý vygenerovaný dotaz JPQL je téměř jedinečný (například obsahující ID vašich entit).
Druhý problém spočívá ve způsobu, jakým hibernace zpracovává dotazy s klauzulí „in“ (např. dejte mi všechny entity osob, jejichž pole id společnosti je jedno z 1, 2, 10, 18). Pro každý odlišný počet parametrů v klauzuli "in" vytvoří hibernace jiný dotaz - např.
select x from Person x where x.company.id in (:id0_)
pro 1 parametr,select x from Person x where x.company.id in (:id0_, :id1_)
pro 2 parametry a tak dále. Všechny tyto dotazy jsou považovány za odlišné, pokud jde o mezipaměť plánu dotazů, což opět vede k cachethrashingu. Tento problém byste pravděpodobně mohli obejít tak, že napíšete třídu autility, která vytvoří pouze určitý počet parametrů - např. 1,10, 100, 200, 500, 1000. Pokud například předáte 22 parametrů, vrátí seznam 100 prvků s 22 parametry zahrnutými init a zbývajících 78 parametrů nastavených na nemožnou hodnotu (např. -1 pro ID používané pro cizí klíče). Souhlasím, že je to ošklivý hack, ale mohl by to udělat. V důsledku toho budete mít ve své mezipaměti pouze maximálně 6 jedinečných dotazů, a tím omezíte mlácení.Jak tedy zjistíte, že máte problém? Můžete napsat nějaký další kód a vystavit metriky s počtem záznamů v mezipaměti, např. přes JMX, vyladit protokolování a analyzovat protokoly atd. Pokud nechcete (nebo nemůžete) aplikaci upravovat, můžete prostě vypsat haldu a spustit proti ní tento dotaz OQL (např. pomocí mat):
SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l
. Vypíše všechny dotazy aktuálně umístěné v libovolné mezipaměti plánu dotazů na vaší hromadě. Mělo by být docela snadné zjistit, zda se vás netýká některý z výše uvedených problémů.Co se týče dopadu na výkon, těžko říct, protože to závisí na příliš mnoha faktorech. Viděl jsem velmi triviální dotaz, který způsobil 10-20 msof režii vynaloženou na vytvoření nového plánu dotazů HQL. Obecně platí, že pokud je někde keška, musí to mít dobrý důvod - chyba je pravděpodobně drahá, takže byste se měli pokusit co nejvíce vyvarovat chyb. V neposlední řadě bude vaše databáze muset zpracovat také velké množství jedinečných příkazů SQL – což způsobí, že je bude analyzovat a možná pro každý z nich vytvoří různé plány provádění.