Klusteri-uniikit id-arvot Java EE/EJB/JPA

Uniikkien avainten kuten Primary Key-arvojen generointi on hauska juttu. Siihen on tarve lähes joka projektissa, ja siihen on lukemattomia eri tapoja. Yksi hurjimmista on laavalampun kuvioiden käyttö satunnaisluvun generointiin – mutta konservatiivisemmalta puolen löytyy mm. sequence, identity keinot jotka ovat kantakohtaisia. Korkeamman tason abstraktiot kuten JPA abstraktoivat halutessaan myös tämän: GeneratedValue-annotaatio antaa kannan päättää mikä kolmesta id-generointitavasta on fiksuin.

Tähän tulee kuitenkin vähän pykälää lisää jos ei voida syystä tai toisesta käyttää JPA generointia. Kenties halutaan ottaa itse avaimen generointi hallintaan koska siihen liittyy erityissääntöjä. Kenties on tarpeen generoida identity-kentän ohella toinen, uniikki avain joka ei kuitenkaan ole primary key. Pikkasen lisää haastetasoa saadaan jos homma pitää vielä tehdä klusterissa – silloin mikään muistinvarainen ratkaisu ei piisaa – ellei muistia synkronoida verkon yli sopivalla tapaa.

JPA:sta ja Hibernatesta löytyy kyllä valmiina generaattoreita, mutta ainakin JPA standardipuolella pääsy niiden mekanismeihin erillään primary key autogeneroinnista on heikko.Mitä? Minä olen ainakin uniikki!

Joten tähän vähän omia mietelmiäni ja koodia siirrettävästä geneerisestä klusteriystävällisestä primary key generoinnista. Tämä on iteraatio 1 joten päättelyssä ja toteutuksessa voi olla aukkoja, mutta päätin silti paljastaa itseni maailmalle – yksi mukava juttu blogeissa on että niitä voi kommentoida ja palaute on tässäkin tervetullutta jos jokin pistää silmään.

Ensiksi tarvitaan taulu kantaan, josta voi saada niitä arvoja. Sen voi hoitaa esim. JPA Entity Objectilla tähän tapaan:

 

@Entity
public class GenTableEntity implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  private String tableName;
  private Long lastId;

  public String getTableName() {
    return tableName;
  }

  public void setTableName(String tableName) {
    this.tableName = tableName;
  }

  public Long getLastId() {
    return lastId;
  }

  public void setLastId(Long lastUsed) {
    this.lastId = lastId;
  }
}

Tästä esimerkistä jätetty pois Javan rakkaat equals, hashCode ja toString toteukset sekä muut hienosäädöt. Kyseessä on siis taulu jossa on String primary key, ja Long arvo jota voidaan (transaktiossa) paukutella.

Seuraava elementti: Singleton, joka pitää yllä id listaa, ja imaisee tarvittaessa serveriltä lisää.

@Singleton
public class UniqueIdGeneratorSingleton {

  private long ceiling; 
  private long current; 
  private static final long RANGE = 1000; 
  private static final String SEQUENCE_NAME = "myitem";

  @PersistenceContext
  private EntityManager entityManager;

  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public long getUniqueId() {
    if (ceiling == 0 || current > ceiling) {
     GenTableEntity entity = entityManager.find(GenTableEntity.class,
        SEQUENCE_NAME, LockModeType.PESSIMISTIC_WRITE);
      if (entity == null) {
        entity = new GenTableEntity();
        entity.setTableName(SEQUENCE_NAME);
        entity.setLastId(0L); // Reserve space
        entityManager.persist(entity);
      }
      current = entity.getLastId() + 1;
      ceiling = entity.getLastId() + RANGE; 
      entity.getLastId(ceiling); 
    }
    return current++;
 }
}

Mitä täällä tapahtuu? Kyseessä on generaattori-EJB joka hakee ja tallettaa kantaan mihin lukemaan asti on id:t käytössä. Tässä on optimointijippo: kannasta ei haeta yksi id kerrallaan, vaan tässä koodissa tuhannen erissä. Yhtälailla erä voi olla sata, tai kymmenentuhatta. Ceiling-arvo ja kannassa oleva lastId arvo pitävät kirjaa mihin asti id avaruutta on varattu. Singleton jakelee ensin omasta jäsenmuuttuja-avaruudestaan kaikki arvot, ja kun ne loppuvat, haetaan kannasta seuraava tuhannen viipale.

Koska kyseessä on Singleton bean, metodiin pääsee yksi säie kerrallaan – serverissä. Klusterissa voi kuitenkin olla useampi serveri, joissa voi käydä niin huonosti että jokaisessa ajetaan juuri samaa riviä samasta singleton-koodista. Siksi tässä on päällä vielä transaktiot, ja pessimistinen lukulukko. Kun yksi säie on lukenut rivin kannasta, se lukitaan ja seuraavan kerran siihen pääsee käsiksi vasta transaktion päätyttyä  -kun arvoa on onnistuneesti muutettu.

Huom. tässä mallissa SEQUENCE_NAME on kovakoodattu arvoon ’myitem’ – joten kaikki generoitavat id:t ovat osa samaa, suurta, globaalia arvoavaruutta. Tätä voi muokata helposti siten että parametrina annetaan taulunimi, silloin joutuu tosin id-cachen rakentamaan Map-muotoiseksi.

Testailin tätä hieman eri kanteilta. Suorituskyky riippuu hyvin paljon range-arvosta. Id-generointi ilman kantaosumaa on muutamia millisekunteja, kannan kanssa jutellessa niitä alkaa palamaan satakertaisesti. Range arvona tuhat on aika mukava, turhan suuret range arvot voivat syödä avaruutta turhankin nopeasti, etenkin jos servereitä tiheään buuttaillaan, päivitellään, tai ne kaatuilevat useita kertoja päivässä (No jos niin käy, tämä on pienimpiä ongelmista).

Tätä on tietysti kiva testata myös rinnakkaisesti. Testausta helpottaa kovasti jos tässä on esim. REST api edes hetkellisesti päällä. Testasin tätä restassured + java concurrency kirjastolla esim. näin:

 

@Test
public void getUniqueIdShouldReturnTwentyUniqueValuesWithParallelExecution() throws Exception {

 final int setSize = 2000;

 Callable<Set<Long>> c = new Callable() {
    @Override
    public Set<Long> call() throws Exception {
      Set<Long> idSet = new HashSet<>();
      for (int i = 0; i < setSize; i++) {
        idSet.add(fetchUniqueId());
      }
      return idSet;
    }
  };

  Set<Long> masterSet = new HashSet<>();

  Future<Set<Long>>[] futures = new Future[10];

  for (int i = 0; i < 10; i++) {
    futures[i] = Executors.newCachedThreadPool().submit(c);
  }

  for (int i = 0; i < futures.length; i++) {
    Set<Long> keys = futures[i].get();
    masterSet.addAll(keys);
  }

   // Set only accepts unique values, duplicates are not added
   // as long as equals() and hashcode() are implemented properly
   assertEquals(setSize * 10, masterSet.size());
}

private Long fetchUniqueId() {
  Response response = given()
    .when()
    .get("version/uniqueid")
    .then()
    .statusCode(200)
    .extract().response();
  
    Long id1 = response.jsonPath().getLong("id");
    return id1;
}

Eli ihan mukavasti tuo toimii, hyvä niksi hihassa. Aika armottomasti yritin tätä paukutella nurin mutta tarpeettomankin vakaasti pelittää. En löytänyt taas pikaisella googletuksella suoranaisesti tällaista mistään, joten kirjoittelin itselleni muistiin.

Mutta palautetta tulemaan jos tulee jotain omia ajatuksia mieleen! Tämä on taas semmoinen juttu attä aivan varmasti joku jossain on jo paremmin tehnyt. Toisaalta 90% JPA käyttäjistä ei asiaa koskaan mietikään koska @GeneratedValue. :=)

 

 

 

 

 

Mainokset

Vastaa

Täytä tietosi alle tai klikkaa kuvaketta kirjautuaksesi sisään:

WordPress.com-logo

Olet kommentoimassa WordPress.com -tilin nimissä. Log Out / Muuta )

Twitter-kuva

Olet kommentoimassa Twitter -tilin nimissä. Log Out / Muuta )

Facebook-kuva

Olet kommentoimassa Facebook -tilin nimissä. Log Out / Muuta )

Google+ photo

Olet kommentoimassa Google+ -tilin nimissä. Log Out / Muuta )

Muodostetaan yhteyttä palveluun %s