分布式ID

背景

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。业务系统对ID号的要求如下:

  1. 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  2. 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  3. 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  4. 信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。
  5. 含时间戳:如果ID中含有时间戳,可以通过ID直接看出这个ID的生成时间

上述123对应三类不同的场景,3和4需求还是互斥的,无法使用同一个方案满足。同时除了对ID号码自身的要求,业务还对ID号生成系统的可用性要求极高,如果ID生成系统瘫痪,整个需要使用分布式ID的功能都无法执行,因此ID生成系统应该做到如下几点:

  1. 平均延迟和TP999延迟都要尽可能低;
  2. 可用性5个9;
  3. 高QPS。

分布式ID生成策略

UUID

UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000使用如下:

import java.util.UUID;

public class Uuid {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) { 
            String uuid = UUID.randomUUID().toString();
    }
}

优点:

  • 性能非常高:本地生成,没有网络消耗。

缺点:

  • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
  • 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露
  • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用(①因为MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求;② 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。)

 

Mysql数据库主键

 

使用Mysql数据库的auto_increment属性,每次设置ID的值就使用LAST_INSERT_ID来设置,如下:

REPLACE INTO T_TEST (value) VALUES('b')

SELECT LAST_INSERT_ID();

LAST_INSERT_ID即为最后插入的ID值,根据MySQL的官方手册说明,它有2种使用方法:

  • 不带参数:LAST_INSERT_ID(),这种方法和AUTO_INCREMENT属性一起使用,当往带有AUTO_INCREMENT属性字段的表中新增记录时,LAST_INSERT_ID()即返回该字段的值;
  • 带有表达式:如LAST_INSERT_ID(value+1),它返回的是表达式的值,即value+1;

优点:

  • 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
  • ID号单调自增,可以实现一些对ID有特殊要求的业务。

缺点:

  • 强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
  • ID发号性能瓶颈限制在单台MySQL的读写性能。

对于MySQL性能问题,可用如下方案解决:在分布式系统中我们可以多部署几台机器,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器。则设置步长step为2,节点1的初始值为1(1,3,5,7,9,11…)、节点2的初始值为2(2,4,6,8,10…),两个节点每次发号之后都递增2。假设我们要部署N台机器,步长需设置为N,每台的初始值依次为0,1,2…N-1那么整个架构就变成了如下图所示:

image

 

这种架构貌似能够满足性能的需求,但有以下几个缺点:

  • 系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,比如14(假设在扩容时间之内第一台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数。然后摘掉第一台,把ID值保留为奇数,比如7,然后修改第一台的步长为2。让它符合我们定义的号段标准,对于这个例子来说就是让第一台以后只能产生奇数。扩容方案看起来复杂吗?貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。
  • ID没有了单调递增的特性,只能趋势递增,这个缺点对于一般业务需求不是很重要,可以容忍。
  • 数据库压力还是很大,每次获取ID都得读写一次数据库,只能靠堆机器来提高性能。

雪花算法snowflake

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的,结构如下:

号段解析

第一部分:1bit

不用,因为二进制最高位是符号位,1表示负数,0表示正数,生成的id一般都是用整数,所以最高位固定为0

第二部分:41bit

用来记录时间戳,毫秒级,表示时间戳位,最大值41个1,即表示2^41-1个毫秒值,即2^41-1毫秒=69年,表示的时间是从1970-2039年的毫秒值,所以snowflake可以用到2039年,但是在判断的时候可以通过减去过去的时间,来提高使用的时间,如下算法的twepoch值1288834974657L表示的时间是Thu, 04 Nov 2010 01:42:54 GMT,即可用从2010-2079年。

第三部分:10bit

10位的工作进程号,即用来表示工作机器id,可以分别表示1024台机器。一般分为5,5个(可以按照实际设定),包括5位的数据中心datacenter(idc)和5位机器码 workerId(机器码),如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

第三部分:12bit

序列号,用来记录同毫秒值内产生的不同ID,12bit表示的最大正整数是2^12-1=4095,可以用0-4094共4095个数字来表示同一个机器在同一时间戳(毫秒值)内产生4095个ID序号

SnowFlake可以保证:

经测试,snowflake每秒能产生26万个自增可排序的ID。

1.Twitter的snowflake生成ID能够按照时间有序生成,所有生成的id按时间趋势递增

2.snowFlake算法生成ID的结构是一个64bit大小的整数,64bit大小的整数是一个Long类型,转换成字符串后长度最多19

3.分布式系统中不会产生ID碰撞,由datacenter和workid作区分,并且效率高

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

算法实现

//snowflake源码java版本
public class IdWorker {
 
    protected static final Logger LOG = LoggerFactory.getLogger(IdWorker.class);
 
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
 
    private long twepoch = 1288834974657L;
 
    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private long sequenceBits = 12L;
 
    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);
 
    private long lastTimestamp = -1L;
 
    public IdWorker(long workerId, long datacenterId) {
        // sanity check for workerId
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
        LOG.info(String.format("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId));
    }
 
    public synchronized long nextId() {
        long timestamp = timeGen();
 
        if (timestamp < lastTimestamp) {
            LOG.error(String.format("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp));
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
 
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
 
        lastTimestamp = timestamp;
 
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
    }
 
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
 
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

//snowflake源码hutool版本
public class Snowflake implements Serializable {
    private static final long serialVersionUID = 1L;
    private final long twepoch;
    private final long workerIdBits;
    private final long dataCenterIdBits;
    private final long maxWorkerId;
    private final long maxDataCenterId;
    private final long sequenceBits;
    private final long workerIdShift;
    private final long dataCenterIdShift;
    private final long timestampLeftShift;
    private final long sequenceMask;
    private final long workerId;
    private final long dataCenterId;
    private final boolean useSystemClock;
    private long sequence;
    private long lastTimestamp;

    public Snowflake(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, false);
    }

    public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
        this((Date)null, workerId, dataCenterId, isUseSystemClock);
    }

    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
        this.workerIdBits = 5L;
        this.dataCenterIdBits = 5L;
        this.maxWorkerId = 31L;
        this.maxDataCenterId = 31L;
        this.sequenceBits = 12L;
        this.workerIdShift = 12L;
        this.dataCenterIdShift = 17L;
        this.timestampLeftShift = 22L;
        this.sequenceMask = 4095L;
        this.sequence = 0L;
        this.lastTimestamp = -1L;
        if (null != epochDate) {
            this.twepoch = epochDate.getTime();
        } else {
            this.twepoch = 1288834974657L;
        }

        if (workerId <= 31L && workerId >= 0L) {
            if (dataCenterId <= 31L && dataCenterId >= 0L) {
                this.workerId = workerId;
                this.dataCenterId = dataCenterId;
                this.useSystemClock = isUseSystemClock;
            } else {
                throw new IllegalArgumentException(StrUtil.format("datacenter Id can't be greater than {} or less than 0", new Object[]{31L}));
            }
        } else {
            throw new IllegalArgumentException(StrUtil.format("worker Id can't be greater than {} or less than 0", new Object[]{31L}));
        }
    }

    public long getWorkerId(long id) {
        return id >> 12 & 31L;
    }

    public long getDataCenterId(long id) {
        return id >> 17 & 31L;
    }

    public long getGenerateDateTime(long id) {
        return (id >> 22 & 2199023255551L) + this.twepoch;
    }

    public synchronized long nextId() {
        long timestamp = this.genTime();
        if (timestamp < this.lastTimestamp) {
            if (this.lastTimestamp - timestamp >= 2000L) {
                throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{this.lastTimestamp - timestamp}));
            }

            timestamp = this.lastTimestamp;
        }

        if (timestamp == this.lastTimestamp) {
            this.sequence = this.sequence + 1L & 4095L;
            if (this.sequence == 0L) {
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
        } else {
            this.sequence = 0L;
        }

        this.lastTimestamp = timestamp;
        return timestamp - this.twepoch << 22 | this.dataCenterId << 17 | this.workerId << 12 | this.sequence;
    }

    public String nextIdStr() {
        return Long.toString(this.nextId());
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp;
        for(timestamp = this.genTime(); timestamp == lastTimestamp; timestamp = this.genTime()) {
        }

        if (timestamp < lastTimestamp) {
            throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{lastTimestamp - timestamp}));
        } else {
            return timestamp;
        }
    }

    private long genTime() {
        return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
    }
}

美团Leaf方案

Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话: >There are no two identical leaves in the world > “世界上没有两片相同的树叶”,综合对比上述几种方案,每种方案都不完全符合我们的要求。所以Leaf分别在上述数据库和snowflake方案上做了相应的优化,实现了Leaf-segment和Leaf-snowflake方案。

Leaf-snowflake算法

对于10bit的workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。但是当服务规模较大,动手配置成本太高。可以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。步骤如下的:

  1. 启动snowflake节点(使用分布式ID)服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
  2. 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA

解决时钟问题

因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

image

参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

  1. 若写过,则用自身系统时间与leaf_forever/${self}节点记录时间做比较,若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。
  2. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
  3. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。
  4. 否则认为本机系统时间发生大步长偏移,启动失败并报警。
  5. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP时间同步也会造成秒级别的回退,建议可以直接关闭NTP时间同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警,如下:

 //发生了回拨,此刻时间小于上次发号时间
 if (timestamp < lastTimestamp) {
  			  
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                	//时间偏差大小小于5ms,则等待两倍时间
                    wait(offset << 1);//wait
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                       //还是小于,抛异常并上报
                        throwClockBackwardsEx(timestamp);
                      }    
                } catch (InterruptedException e) {  
                   throw  e;
                }
            } else {
                //throw
                throwClockBackwardsEx(timestamp);
            }
        }
 //分配ID  

Leaf-segment数据库方案

Leaf-segment方案是在使用数据库的基础上做了如下改变: 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值保存在本地内存。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。 不同的业务可以使用不同的biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

数据库表设计如下:

+-------------+--------------+------+-----+-------------------+-----------------------------+
| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag     | varchar(128) | NO   | PRI |                   |                             |
| max_id      | bigint(20)   | NO   |     | 1                 |                             |
| step        | int(11)      | NO   |     | NULL              |                             |
| desc        | varchar(256) | YES  |     | NULL              |                             |
| update_time | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+

重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step,大致架构如下图所示:

image

比如test_tag的业务需求,test_tag在第一台Leaf机器上是1~1000的号段,当这个号段用完时,会去加载另一个长度为step=1000的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是3001~4000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000,更新号段的SQL语句如下:

UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx

这种模式有以下优缺点:

优点:

  • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
  • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。

缺点:

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
  • DB宕机会造成整个系统不可用。

双buffer优化

对于第二个缺点,Leaf-segment做了一些优化,简单的说就是:Leaf 取号段的时机是在号段消耗完的时候进行的,在号段用完时,这时候请求会去从新获取下一个号段,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,影响不大的,如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。我们希望DB取号段的过程能够做到无阻塞,不能在DB取号段的时候阻塞请求线程,所以可以当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。

采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。

  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

Leaf高可用容灾

对于第三点DB可用性问题,我们目前采用一主两从的方式,同时分机房部署,Master和Slave之间采用半同步方式同步数据。同时使用公司Atlas数据库中间件(已开源,改名为DBProxy)做主从切换。当然这种方案在一些情况会退化成异步模式,甚至在非常极端情况下仍然会造成数据不一致的情况,但是出现的概率非常小。如果你的系统要保证100%的数据强一致,可以选择使用类Paxos算法实现的强一致MySQL方案,如MySQL 5.7前段时间刚刚GA的MySQL Group Replication。但是运维成本和精力都会相应的增加,根据实际情况选型即可。同时Leaf服务分IDC部署,服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。

 

实际落地的唯一ID

采用数据库segment获取值,数据库中建表如下,pk_seq_name:代表不同的业务,current_val:表示当前使用的值,increment_val:表示步长。

CREATE TABLE `t_sequence` (
  `pk_seq_name` varchar(50) NOT NULL,
  `current_val` bigint(20) NOT NULL,
  `increment_val` int(11) NOT NULL DEFAULT '1',
  PRIMARY KEY (`pk_seq_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

思路如下:

初始化t_sequence表,设置pk_seq_name= test,current_val=0,increment_val=1000

test业务的节点1获取唯一ID,添加数据库行锁,防止其他节点脏读,此时设置当前值为1000,保存在本地缓存中,从1000开始递增读取,当超过2000时,从新读取一段。其他节点也是这样操作,分段读取,利用同步锁和行锁限制同一节点和不同节点之间的不会出现数据问题。具体代码如下:

@Slf4j
@Component
public class ChannelNoService {

  private SequenceRepository sequenceRepository;

  private NextValueService nextSequenceGenerator;

  ChannelNoService(SequenceRepository sequenceRepository, NextValueService nextValueService) {
    this.sequenceRepository = sequenceRepository;
    this.nextSequenceGenerator = nextValueService;
  }

  private static final int CACHE_NO_NUM = 1000;//步长

  private ConcurrentHashMap<String, Long> currentByChannelMap = new ConcurrentHashMap<>();

  private ConcurrentHashMap<String, Long> sequenceByChannelMap = new ConcurrentHashMap<>();

  /**
   * Generate unique sequence no.
   * 同步方法,同一个节点内同步获取
   */
  public synchronized long generateUnique(String sequenceName) {
    Long cacheStart = sequenceByChannelMap.get(sequenceName);
    long now = currentByChannelMap.getOrDefault(sequenceName, 0L);

    log.info("ChannelRequestNo generator: sequenceName: [{}], cacheStart: [{}], now : [{}]",
        sequenceName, cacheStart, now);

    if (isFetchingNewBlock(cacheStart, now)) {//当没有初始化或者已经缓存的segment用完
      if (isExceedingMaxLongValue(cacheStart)) {//判断是否超过最大值
        sequenceRepository.clearCurVal(sequenceName);//超过最大值,继续从0开始
      }
      //数据库行锁,不同节点之间,拿到的初始化分段值不一样,节点1拿到0-1000,节点2拿到1000-2000...
      cacheStart = nextSequenceGenerator.getNextVal(sequenceName);
      sequenceByChannelMap.put(sequenceName, cacheStart);//放入本地缓存中
      log.info("ChannelRequestNo from new block: now : [{}] , next value : [{}]", now, cacheStart);
      now = cacheStart;
    } else {
      ++now;//本地获取时,直接从网上取
    }

    if (now < cacheStart) {
      log.error("Generate channelRequestNo failed. now: {} < start: {}", now, cacheStart);
      throw new ThirdPartyPaymentException("序列号生成器错误: now:" + now  + " < start:" + cacheStart);
    }

    currentByChannelMap.put(sequenceName, now);
    return now;
  }
  
  private boolean isExceedingMaxLongValue(Long cacheStart) {
    return cacheStart != null && cacheStart >= Long.MAX_VALUE - CACHE_NO_NUM;
  }

  /**
   * 当当前缓存序列号区间耗尽,需向数据库序列号表申请下一区间序列号.
   */
  private boolean isFetchingNewBlock(Long cacheStart, long now) {
    return cacheStart == null || cacheStart == 0 || now >= cacheStart + CACHE_NO_NUM - 1;
  }
}



@Slf4j
@Component
public class NextValueService {

  private SequenceRepository sequenceRepository;

  NextValueService(SequenceRepository sequenceRepository) {
    this.sequenceRepository = sequenceRepository;
  }

  /**
   * getNextVal.
   */
  @Transactional
  public long getNextVal(String name) {
    sequenceRepository.lock(name);
    sequenceRepository.updateNextVal(name);
    return sequenceRepository.getCurVal(name);
  }
  
}


public interface SequenceRepository extends Repository<Sequence, String> {

  @Transactional
  @Query(value = GET_CUR_VAL, nativeQuery = true)
  Long getCurVal(@Param("sequenceName") String sequenceName);

  @Query(value = LOCK, nativeQuery = true)
  void lock(@Param("sequenceName") String sequenceName);

  @Transactional
  @Modifying
  @Query(value = UPDATE_NEXT_VAL, nativeQuery = true)
  void updateNextVal(@Param("sequenceName") String sequenceName);

  @Transactional
  @Modifying
  @Query(value = CLEAR_CUR_VAL, nativeQuery = true)
  void clearCurVal(@Param("sequenceName") String sequenceName);

  String UPDATE_NEXT_VAL = "update t_sequence set current_val "
      + "= current_val + increment_val  where pk_seq_name = :sequenceName";

  String CLEAR_CUR_VAL = "update t_sequence set current_val = 0 where pk_seq_name = :sequenceName";

  String LOCK = "select * from t_sequence where pk_seq_name = :sequenceName for update";

  String GET_CUR_VAL = "select current_val from t_sequence where pk_seq_name = :sequenceName";
}

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值