This post is a kind of warn for Hibernate users.
I sincerely don’t know which was the reason but accidentally someone annotated every entity on a software with:
@SequenceGenerator(allocationSize=1)
Doing a brainstorm to find the reason we can speculate:
- Minimise sequence gaps due to server reboots (unlikely)
- Misunderstanding of what the parameter really does. Probably the developer thought it had to do with the increment by parameter of the underlying database sequence (more likely)
Whichever the reason the truth is that whenever you annotate an entity with @SequenceGenerator Hibernate (at least I know up to 3.3.1.GA) delegates sequence generation to org.hibernate.id.SequenceHiLoGenerator which in turn generate method is as follows:
public synchronized Serializable generate(SessionImplementor session, Object obj) throws HibernateException { if (maxLo < 1) { //keep the behavior consistent even for boundary usages long val = ( (Number) super.generate(session, obj) ).longValue(); if (val == 0) val = ( (Number) super.generate(session, obj) ).longValue(); return IdentifierGeneratorFactory.createNumber( val, returnClass ); } if ( lo>maxLo ) { long hival = ( (Number) super.generate(session, obj) ).longValue(); lo = (hival == 0) ? 1 : 0; hi = hival * ( maxLo+1 ); if ( log.isDebugEnabled() ) log.debug("new hi value: " + hival); } return IdentifierGeneratorFactory.createNumber( hi + lo++, returnClass ); }
If you compare the generate sequence above with the one from org.hibernate.id.SequenceGenerator below you’ll notice that this one in turn is not synchronized:
public Serializable generate(SessionImplementor session, Object obj) throws HibernateException { try { PreparedStatement st = session.getBatcher().prepareSelectStatement(sql); try { ResultSet rs = st.executeQuery(); try { rs.next(); Serializable result = IdentifierGeneratorFactory.get( rs, identifierType ); if ( log.isDebugEnabled() ) { log.debug("Sequence identifier generated: " + result); } return result; } finally { rs.close(); } } finally { session.getBatcher().closeStatement(st); } } catch (SQLException sqle) { throw JDBCExceptionHelper.convert( session.getFactory().getSQLExceptionConverter(), sqle, "could not get next sequence value", sql ); } }
The synchronized keyword on the previous one isn’t any surprise since SequenceHiLoGenerator does some of its sequence generation in memory and it may be accessed concurrently by multiple threads.
But the fact is that SequenceHiLoGenerator wasn’t designed to have an allocationSize=1 (strangely enough it has a separate flow for this situation).
Reproducing this scenario seems to be very easy, sincerely I have not tried to do a syntetic reproduction but I guess that spawning a few threads with a simple entity configured with allocationSize=1 and having all the threads call a Session.persist might do the trick. All you will need to do is have a thread dump after a few minutes (3 or 5) of execution and import it into a dump analyser (IBM Support Assistant if you are running on an IBM JVM) and you’ll notice that a great deal of threads will be waiting on a lock (before entering the generate method on AbstractSaveEventListener.saveWithGeneratedId).
But what would be possible alternatives for this?
If you want to have the entities IDs reflecting the value of the database sequence use the following annotation:
@GenericGenerator(strategy="sequence")
Another possibility is removing the allocationSize (which leads to the default value of 50) or configuring it with a greater value. But, in this case you’ll need to update the database sequence so that you don’t end up with a gap on your IDs since SequenceHiLoGenerator employs the following formula for sequence generation:
DBSequence*allocationSize<= IDs < (DBSequence+1)*allocationSize
And upon startup it already increments sequence by one acquiring a new slot of IDs.
So you’ll have to restart your sequence with the following value:
select round(max(id)/allocationSize) from table;