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时又会查询数据库。这个算法太美了。

其详细原理可以参照源码和下面的链接对比:

http://hi.baidu.com/sai5d/blog/item/88e5f4db09e90277d0164e30.html

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值