1. 程式人生 > >Hibernate--Increment和Hilo主鍵生成策略原理

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時又會查詢資料庫。這個演算法太美了。

其詳細原理可以參照原始碼和下面的連結對比: