分布式全局序列ID方案之Flicker优化方案

1 Flicker的解决方案

MySQL中id自增的特性,可以借此来生成全局的序列号,Flicker在解决全局ID生成方案里就采用了MySQL自增长ID的机制(auto_increment + replace into + MyISAM)。一个生成64位ID方案具体就是这样的: 先创建单独的数据库,然后创建一个表:

CREATE TABLE borrow_order (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(1) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM

当我们插入记录后,执行SELECT * from borrow_order ,查询结果就是这样的:

+-------------------+------+
| id | stub |
+-------------------+------+
| 1 | 192.168.100.102      |
+-------------------+------+

在我们的应用端需要做下面这两个操作,在一个事务会话里提交:

REPLACE INTO borrow_order (stub) VALUES ('192.168.100.102');
SELECT LAST_INSERT_ID();

上述操作,通过 replace into 操作,首先尝试插入数据到表中,如果发现表中已经有此行数据则先删除此行数据,然后插入新的数据。 如果没有此行数据的话,直接插入新数据。注意:插入的行字段需要有主键索引或者唯一索引,否则会出错

通过上述方式,就可以拿到不重复且自增的ID了。 到上面为止,我们只是在单台数据库上生成ID,从高可用角度考虑,接下来就要解决单点故障问题:Flicker启用了两台数据库服务器来生成ID,通过区分auto_increment的起始值和步长来生成奇偶数的ID。

DBServer1:
auto-increment-increment = 2
auto-increment-offset = 1
 
DBServer2:
auto-increment-increment = 2
auto-increment-offset = 2

最后,在客户端只需要通过轮询方式取ID就可以了。

优点:充分借助数据库的自增ID机制,提供高可靠性,生成的ID有序。 缺点:占用两个独立的MySQL实例,有些浪费资源,成本较高 数据库中记录过多,每次生成id都需要请求数据库

2 优化方案

采用批量生成的方式,内存缓存号段,降低数据库的写压力,提升整体性能

2.1 方案1 mysql双主架构

采用双主架构的方式来显示高可用,数据库只值存在已备用号段的最大值。 我们新建一张表

CREATE TABLE `id_generator` (
  `id` int(10) NOT NULL,
  `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',
  `increment_step` int(10) NOT NULL COMMENT '步幅长度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

如此我们可以通过 getIdService服务 来对单实例数据库批量获取id,具体步骤如下: 号段逻辑

优缺点 >优点 数据库只保存一条记录 性能极大增强

缺点: 如getIdService重启,内存中未使用的ID号段未分配,导致ID空洞 服务没有做HA,无法保证高可用

问题解决方案: 数据库使用双主架构保证高可用,服务通过多个getIdService服务去获取Id,减少ID空洞的数量

上述方案还是存在问题,当多个服务去访问数据库,或者同一服务同时多个线程去访问就会产生竞态条件,产生并发安全问题。 在当前读多写少的场景下,我们可以使用数据库CAS乐观锁去解决并发问题来保证原子性。

假设 同时有两个服务获取到current_max_id的最大值都是3000,然后生成号段后,
回写数据库update id_generator set current_max_id=4000; 此时就会产生的id就会重复,
我们通过CAS方式改写为:update id_generator set current_max_id=4000 where max_id=3000;
来保证生成的id全局唯一

该方案的好处是:

水平扩展达到分布式ID生成服务性能 使用CAS简洁的保证不会生成重复的ID

缺点: 由于有多个service,生成的ID 不是绝对递增的,而是趋势递增的

2.2 方案2 多实例

基于Flicker方案

REPLACE INTO borrow_order (stub) VALUES ('192.168.1.1');
SELECT LAST_INSERT_ID();

当多个服务器的时候,这个表是这样的:

id stub 5 192.168.1.1 2 192.168.1.2 3 192.168.1.3 4 192.168.1.4

每台服务器设置好增幅只更新自己的那条记录,保证了单线程操作单行记录。这时候每个机器拿到的分别是5,2,3,4这4个id。这方案直接通过服务器隔离,解决原子性获得id的问题。

2.3 服务层原子性操作CAS

我们可以AtomicLong来实现ID自增,流程图如下:

后台服务id获取流量

伪代码实现

    private static AtomicLong atomicLong;

    private static Long currentMaxId;

    public synchronized static void deployIdService(String[] args) {

        //判断是否为空调用id生成服务
        getIdService();
        //判断内存中的号段是否使用完
        if (atomicLong.get() < currentMaxId) {
            //获取id
            long id = atomicLong.getAndIncrement();
        }else {
            //使用重新分配
            atomicLong.set(200);
            currentMaxId = 300L;
        }  
 }

这里有个小问题,就是在服务器重启后,因为号码缓存在内存,会浪费掉一部分用户ID没有发出去,所以在可能频繁发布的应用中,尽量减小号段放大的步长n,能够减少浪费。 如果再追求极致,可以监听spring或者servlet上下文的销毁事件,把当前即将发出去的用户ID保存起来,下次启动时候加载进入内存。

Reference

浅谈CAS在分布式ID生成方案上的应用 分布式架构系统生成全局唯一序列号的一个思路

文章目录

COMMENTS