基于数据库的分布式发号器-viemall-sequence

简述:

     在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。公司的各种产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单、用户、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。概括下来,那业务系统对ID号的要求有哪些呢?    

    1. 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。

    2.  趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

    3. 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。

    4. 信息安全:有些业务的ID必须是无规则的ID,比如订单编号。

业界采用了很多的分布式ID生成方式,

     基于数据库的自增字段,自增字段完全依赖于数据库,这在数据库移植,扩容,洗数据,分库分表等操作时带来了很多麻烦。

     比如基于:Zookeeper/redis方式, 基于Snowflake算法的唯一ID生成器;详细的可以看《分布式ID生成解决方式》,但是都有各种的优点和缺点;

本文自身采用常规的解决方案,类似与Hibernate TableGenerator,简单实用的解决思路:

    这种方式比较常用,每一次都请求数据库,通过程序维护数据库的自增ID来获取全局唯一ID,对于小系统来说,这是一个简单有效的方案,这种的生成方式还是比较依赖于数据库,每次获取ID都需要经过一次数据库的调用,性能损耗很大(但可以基于步长方式来补偿,这样会导致Client出现宕机获取其他原因,会丢失这些步长,使至ID不连贯)。还有在数据库的水平扩展上比较麻烦的。

  优点:ID连贯、服务稳定,已经很满足大多数的公司分表分库后的ID业务需求了

  缺点:基于数据库性能有瓶颈。

 

    对于MySQL性能问题,可用如下方案解决:在分布式系统中我们可以多部署几台机器服务,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器。设置步长step为2,TicketServer1的初始值为1(1,3,5,7,9,11…)、TicketServer2的初始值为2(2,4,6,8,10…)。这是Flickr团队在2010年撰文介绍的一种主键生成策略()。如下所示,为了实现上述方案分别设置两台机器对应的参数,TicketServer1从1开始发号,TicketServer2从2开始发号,两台机器每次发号之后都递增2。

    假设我们要部署N台机器,步长需设置为N,每台的初始值依次为0,1,2…N-1那么整个架构就变成了如下图所示:

 image.png

实现的思路:

  1.采用类似与Hibernate TableGenerator ,设置步长和sequene_name和value值;

  2.通过发号器来维护这个sequene表,jdbc 业务ID新增的时候通过采用sequene服务获取新增的ID值;

  3.基于性能考虑,每个部署的服务都可以设置不同的步长,维护在本地内存上,提高效率,也可以设置不同的增长因子从而满足水平扩展需求;

   第一步:创建一张sequence对应的表。

image.png

     几张逻辑表需要声明几个sequence,也可以采用在项目启动的时候,去自动新建sequence值;

第二步:配置sequenceDao

image.png

sequence生成器核心代码:

public SequenceRange nextRange(String name) throws Exception {
           if (name == null) {
               throw new IllegalArgumentException("序列名称不能为空");
           }
 
           long oldValue;
           long newValue;
 
           Connection conn = null;
           PreparedStatement stmt = null;
           ResultSet rs = null;
 
           for (int i = 0; i < retryTimes + 1; ++i) {
               try {
                   conn = dataSource.getConnection();
                   stmt = conn.prepareStatement(getSelectSql());
                   stmt.setString(1, name);
                   rs = stmt.executeQuery();
                   rs.next();
                   oldValue = rs.getLong(1);
 
                   if (oldValue < 0) {
                       StringBuilder message = new StringBuilder();
                       message.append("Sequence value cannot be less than zero, value = ").append(oldValue);
                       message.append(", please check table ").append(getTableName());
 
                       throw new Exception(message.toString());
                   }
 
                   if (oldValue > Long.MAX_VALUE - DELTA) {
                       StringBuilder message = new StringBuilder();
                       message.append("Sequence value overflow, value = ").append(oldValue);
                       message.append(", please check table ").append(getTableName());
 
                       throw new Exception(message.toString());
                   }
 
                   newValue = oldValue + getStep();
               } catch (Exception e) {
                   throw new Exception(e);
               } finally {
                   closeResultSet(rs);
                   rs = null;
                   closeStatement(stmt);
                   stmt = null;
                   closeConnection(conn);
                   conn = null;
               }
 
               try {
                   conn = dataSource.getConnection();
                   stmt = conn.prepareStatement(getUpdateSql());
                   stmt.setLong(1, newValue);
                   stmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
                   stmt.setString(3, name);
                   stmt.setLong(4, oldValue);
                   int affectedRows = stmt.executeUpdate();
                   if (affectedRows == 0) {
                       // retry
                       continue;
                   }
 
                   return new SequenceRange(oldValue + 2, newValue);
               } catch (Exception e) {
                   throw new Exception(e);
               } finally {
                   closeStatement(stmt);
                   stmt = null;
                   closeConnection(conn);
                   conn = null;
               }
           }
 
           throw new Exception("Retried too many times, retryTimes = " + retryTimes);
       }

发号器测试:

image.png

参考资料:

   Leaf——美团点评分布式ID生成系统》

项目源代码:

    https://download.csdn.net/download/tang06211015/10350126