六种方式实现全局唯一发号器

概念

  发号器,也就是在系统全局生成绝对唯一的唯一id生成器,比如订单号、流水号等场景。类似于身份证号,它需要保证全局唯一,尤其是在分布式机器中,不同机器不能生成一样的号牌。我们需要通过一些算法或方式实现这个小功能。

雪花算法

  由Twitter提出,基于对long的高低位分配实现,几乎可以理解为发号器的最优实现,目前美团、百度等开源发号器大多基于或参考了这种分配形式。

   雪花算法将一个long的除符号位外63位分成了下面的三个部分:

  • 41位的毫秒时间戳(可以使用69年左右)
  • 10位的机器号标识(10位的长度最多支持部署1024个节点)
  • 12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)

👍 优点

1. 时间戳在高位,这样产生的将会是趋势递增的数字,适合用于订单号、流水号等场景;

2. 性能高,可以在短时间生成大量号牌,而且几乎不依赖任何IO;

3. 可以根据自身业务需要分配bit位。

🚩 缺点

1. 依赖系统时钟,如果系统时钟回拨可能导致故障。尤其是要注意闰秒场景,JDK1.7之前没有解决,可能系统回拨而导致生成重复号牌;

2. 由于依赖系统时钟,如果多机部署且这几台机器时钟不一致可能导致全局不是递增的。

  

UUID

  UUID是一个堪称经典的唯一ID生成方式,UUID具有很多个不同版本,整体上思路差不多,都是通过时间戳、MAC地址等信息处理拼接之后分成一个全球唯一的id,比如下面这样的形式:

00000000-0000-0000-0000-000000000000

  当然,这是一个特殊的UUID,正常情况时不可能全为0的,我们将上面这个全为0的UUID称为Nil UUID,一般我们用不到它。

👍 优点

1. 本地运算,性能高,低延时,可拓展性高;

🚩 缺点

1. 不是趋势递增的,不具有有序性;

2. UUID太长了,一般采用32位编码。不适合在分库分表等场景使用,因为UUID做数据库主键太长了;

3. UUID存在很多版本,部分版本可能存在使用随机数生成,在特别极端的情况下可能存在重复;

   

MongoDB发号器

  MongDB实现了一种发号器机制,和推特的雪花算法类似,也是通过位运算进行的。可以直接看源码:mongo-java-driver/ObjectId.java at master · mongodb/mongo-java-driver · GitHub

   可以看到MongDB采用了类似于雪花算法的形式,它的分配形式是:

32位:秒级时间戳,从1970年开始计数,目前来看可以支持136年

24位:递增数字,这里和雪花算法有一点不同,它即使时间戳变化也不会归零。如果超过了24位就截取后24位;

24位:机器标识,将服务器上全部网卡的Mac地址拼接后计算HashCode,截取24位;

16位:通过JMX(Java平台的管理和监控接口)获取进程号。

👍 优点

1. 一样能够实现趋势递增;

2. 高性能,本地生成无需远程调用,不依赖其他服务可靠性高。

🚩 缺点

1. 由于进程号获取失败、机器号获取失败都会用随机数等代替,可能在极端情况出现重复。

2. 也是比较长,不能存储进long类型等。

   

Flicker发号器

  据说是Flicker团队的解决思路,在某公司公众号看到的,但是目前网上并没有找到此团队相关资料,来源存疑。

  所以在这里只讲解实现思路。整体思路比较简单,采用了MySQL自增长ID的机制 和 replace into 语句。

  先建立一张表:

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

  每一次通过下面的语句获取最新的唯一ID:

REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

  REPLACE INTO 的逻辑和INSERT相似,他的逻辑是如果存在,就先删除再添加,所以能保证id自增。

  为了避免单点故障,一般会选择使用两台或以上MySQL服务器进行服务,通过区分步长实现区分,比如:

Server1:
auto-increment-increment = 2
auto-increment-offset = 1

Server2:
auto-increment-increment = 2
auto-increment-offset = 2

👍 优点

1. 能保证整体上的趋势递增;

2. 依靠数据库的自增ID,能够生成有序的ID。

🚩 缺点

1. 依靠数据库的可用性,如果数据库在极端场景下全部不可用可能导致系统不可用;

2. 没有之前的方法快。

    

步长方式

  来源已经无从考证。滴滴的开源发号器Tinyid、阿里开源分库分表组件TDDL生成全局ID、有道发号器(未开源)基于此算法,美团开源的开源发号器Leaf支持此算法(可使用配置修改)。

  原理相对简单,每一台机器都先领很多号牌,之后等到快发完再去领下一批,避免了每一次都去请求MySQL。

  以Tinyid为例,根据官方介绍,需要先建立下面数据表:

CREATE TABLE `tiny_id_info` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
  `begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
  `max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
  `step` int(11) DEFAULT '0' COMMENT '步长',
  `delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
  `remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
  `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';

   能够看出使用了乐观锁的形式,系统原理相对简单。

👍 优点

1. 相比于之前的Flicker方案,能够大大降低数据库压力;

2. 生成ID性能大大提升;

3. 符合我们通常需要的趋势递增需求。

🚩 缺点

1. 强依赖于数据库系统,数据库服务宕机将导致发号功能不可用。

    

中间件

  可以通过引入 Redis 或 Zookeeper 实现这种功能。例如通过Zookeeper的顺序节点版本号生成全局唯一的顺序递增id。

  但是我们一般情况并不希望引入更多的中间件来解决这个不大的问题。

👍 优点

1. 绝对依次递增,之前方式都只能保证整体上趋势递增;

🚩 缺点

1. 速度上比不上本机生成或大量缓存的形式;

2. 强依赖中间件,中间件宕机将导致服务不可用;

3. 实现比上面的形式复杂。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java中,可以通过自定义异常处理程序来处理唯一性异常。以下是一个简单的示例,演示如何在全局处理唯一性异常: 首先,创建一个自定义异常类,例如UniqueConstraintException: ```java public class UniqueConstraintException extends RuntimeException { public UniqueConstraintException(String message) { super(message); } } ``` 然后,创建一个全局异常处理程序,例如UniqueConstraintExceptionHandler: ```java @ControllerAdvice public class UniqueConstraintExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = {UniqueConstraintException.class}) protected ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) { String bodyOfResponse = "Duplicate entry found"; return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request); } } ``` 在这个处理程序中,我们使用@ControllerAdvice注释来定义一个全局异常处理程序,并使用@ExceptionHandler注释来定义要处理的异常类型。在这个示例中,我们处理UniqueConstraintException类型的异常。在处理程序中,我们使用ResponseEntityExceptionHandler类来处理异常,并返回一个HTTP响应实体,其中包含错误消息和响应状态码。 最后,在应用程序中使用这个处理程序,例如: ```java @SpringBootApplication public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } @Autowired private UniqueConstraintExceptionHandler uniqueConstraintExceptionHandler; @PostConstruct public void init() { Thread.setDefaultUncaughtExceptionHandler(uniqueConstraintExceptionHandler); } } ``` 在这个示例中,我们在应用程序的main方法中注入UniqueConstraintExceptionHandler,并将其设置为默认的未捕获异常处理程序。这样,在应用程序中发生UniqueConstraintException异常时,处理程序将被调用,并返回一个HTTP响应实体。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员麻薯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值