Hibernate--Increment和Hilo主鍵生成策略原理
最近專案中遇到叢集問題,比如我們有兩個叢集節點,在正常情況下只有一個節點工作(A),當出現異常時切換到另一個叢集節點(B)上。專案中使用Hibernate的increment作為資料庫主鍵生成策略。它的原理如下:
Hibernate初始化完成後,當獲取主鍵時,會查詢一次資料庫將最大的Id查詢出來,之後的操作就全部是在記憶體中維護主鍵的自增,儲存時更新到資料庫,其原始碼如下:
package org.hibernate.id; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.dialect.Dialect; import org.hibernate.engine.SessionFactoryImplementor; import org.hibernate.engine.SessionImplementor; import org.hibernate.exception.JDBCExceptionHelper; import org.hibernate.jdbc.Batcher; import org.hibernate.mapping.Table; import org.hibernate.type.Type; import org.hibernate.util.StringHelper; public class IncrementGenerator implements IdentifierGenerator, Configurable { private static final Log log = LogFactory.getLog(IncrementGenerator.class); private long next; private String sql; private Class returnClass; public synchronized Serializable generate(SessionImplementor session, Object object) throws HibernateException { if (this.sql != null) { getNext(session); } return IdentifierGeneratorFactory.createNumber(this.next++, this.returnClass); } public void configure(Type type, Properties params, Dialect dialect) throws MappingException { String tableList = params.getProperty("tables"); if (tableList == null) tableList = params.getProperty("identity_tables"); String[] tables = StringHelper.split(", ", tableList); String column = params.getProperty("column"); if (column == null) column = params.getProperty("target_column"); String schema = params.getProperty("schema"); String catalog = params.getProperty("catalog"); this.returnClass = type.getReturnedClass(); StringBuffer buf = new StringBuffer(); for (int i = 0; i < tables.length; ++i) { if (tables.length > 1) { buf.append("select ").append(column).append(" from "); } buf.append(Table.qualify(catalog, schema, tables[i])); if (i >= tables.length - 1) continue; buf.append(" union "); } if (tables.length > 1) { buf.insert(0, "( ").append(" ) ids_"); column = "ids_." + column; } this.sql = "select max(" + column + ") from " + buf.toString(); } private void getNext(SessionImplementor session) { log.debug("fetching initial value: " + this.sql); try { PreparedStatement st = session.getBatcher().prepareSelectStatement(this.sql); try { ResultSet rs = st.executeQuery(); try { if (rs.next()) { this.next = (rs.getLong(1) + 1L); if (rs.wasNull()) this.next = 1L; } else { this.next = 1L; } this.sql = null; log.debug("first free id: " + this.next); } finally { rs.close(); } } finally { session.getBatcher().closeStatement(st); } } catch (SQLException sqle) { throw JDBCExceptionHelper.convert(session.getFactory().getSQLExceptionConverter(), sqle, "could not fetch initial value for increment generator", this.sql); } } } /* Location: D:\Workspace\HibernateTest\bin\lib\hibernate3.jar * Qualified Name: org.hibernate.id.IncrementGenerator * Java Class Version: 1.4 (48.0) * JD-Core Version: 0.5.3 */
大家請看其generate方法,當sql語句不等於Null的時候,獲取下一個版本,也就是從資料庫拿最大的id,然後就將sql置為null。這樣下次獲取id時,就不會從資料庫拿,而是在記憶體++。那麼這樣就會產生問題。比如系統現在在A節點上工作,當next走到10的時候,突然網路斷了,於是系統切換到了B節點上工作,B節點獲取id時查詢資料庫拿到最大值,並且順利執行next走到了20,此時又切回到了A節點,然而A節點的next此時為10,並且sql語句為null,於是A不查詢資料庫直接取next的值,那麼將導致從10開始到20的id都會發生主鍵衝突,必須重啟A節點才能解決問題。
因此如果涉及到使用hibernate的叢集一定不能使用increment做為主鍵生成策略。從上面分析我們可以看出如果想解決該問題,必須解決兩個程序之間的記憶體共享,也就是共享next變數,但是實現起來很複雜。最後採用Hilo的主鍵生成策略解決。其部分原始碼如下:
public Serializable doWorkInCurrentTransaction(Connection conn, String sql) throws SQLException { int result; int rows; do { sql = this.query; SQL.debug(this.query); PreparedStatement qps = conn.prepareStatement(this.query); try { ResultSet rs = qps.executeQuery(); if (!(rs.next())) { String err = "could not read a hi value - you need to populate the table: " + this.tableName; log.error(err); throw new IdentifierGenerationException(err); } int result = rs.getInt(1); rs.close(); } catch (SQLException sqle) { throw sqle; } finally { qps.close(); } sql = this.update; SQL.debug(this.update); PreparedStatement ups = conn.prepareStatement(this.update); int rows; try { ups.setInt(1, result + 1); ups.setInt(2, result); rows = ups.executeUpdate(); } catch (SQLException sqle) { throw sqle; } finally { ups.close(); } } while (rows == 0); return new Integer(result); } }
上面程式碼說明獲取主鍵時,hibernate都會在同一個事務中從資料庫中拿出主鍵,並將該主鍵更新。這樣保證另外的程序從資料庫取值時能夠獲取最大值。
關鍵是下面的程式碼:
public class TableHiLoGenerator extends TableGenerator
{
public static final String MAX_LO = "max_lo";
private long hi;
private int lo;
private int maxLo;
private Class returnClass;
private static final Log log = LogFactory.getLog(TableHiLoGenerator.class);
public void configure(Type type, Properties params, Dialect d) {
super.configure(type, params, d);
this.maxLo = PropertiesHelper.getInt("max_lo", params, 32767);
this.lo = (this.maxLo + 1);
this.returnClass = type.getReturnedClass();
}
public synchronized Serializable generate(SessionImplementor session, Object obj) throws HibernateException
{
if (this.maxLo < 1)
{
int val = ((Integer)super.generate(session, obj)).intValue();
return IdentifierGeneratorFactory.createNumber(val, this.returnClass);
}
if (this.lo > this.maxLo) {
int hival = ((Integer)super.generate(session, obj)).intValue();
this.lo = ((hival == 0) ? 1 : 0);
this.hi = (hival * (this.maxLo + 1));
log.debug("new hi value: " + hival);
}
return IdentifierGeneratorFactory.createNumber(this.hi + this.lo++, this.returnClass);
}
}
我們可以看到 ,查詢主鍵時將資料庫的的主鍵+1儲存,如果這個事物沒有成功,那麼this.lo將永遠大於this.maxLo,即便是此時切換到了B,當再次回到A時,首先會查詢一次資料庫,這樣保證this.hi永遠不會相等。如果成功那麼當從B切換到A時,由於B的hi值比A的hi值至少大this.maxLo+1,因此,即便A保持hi不變從內從中拿低位,也不會和B相同,因為當this.lo大於this.maxLo時又會查詢資料庫。這個演算法太美了。
其詳細原理可以參照原始碼和下面的連結對比: