一、背景
公司各业务线系统中,数据库的表 ID 生成均采用了雪花算法方式。当应用宿主系统使用 NTP 同步时钟时,有可能会发生时钟回拨(Clock moved backwards)的情况,即本地时钟小于远端时钟。雪花算法强依赖于服务器系统时间,一旦发生这种情况,算法会直接抛出异常,中断业务执行。该问题近期发生较为频繁,对业务影响呈逐渐严重态势。
二、技术调研
1、NTP
(1)NTP 概述
网络中的计算机或其他设备保持准确的时间非常重要,时间的准确影响到计算机系统的大部分基于时间判定的逻辑的正确运行,比如:TLS证书的有效性校验,个人密码的过期,crontab任务的执行等等,分布式系统和一些分布式协议也必须基于准确的时间。
计算机的时间可从硬件时钟获取,硬件时钟是存储在 CMOS 里的时钟,关机后该时钟依然运行,主板的电池为它供电。硬件时钟依照主板石英晶体振荡器频率工作,在启动系统后,系统从该时钟读取时间信息,之后独立运行。由于每台计算机设备的频率受温度等影响不是固定的,导致时间快慢不同,计算机或其他设备需要同步一个准确的时间。
时间通常采用 UTC(协调世界时) 格式,准确的 UTC 时间可使用多种不同的方法得到,包括无线电和卫星系统等,这些也被称为参考时钟,接收机能够从参考时钟获取时间,但如果每台计算机都安装接收机是不实际的,作为替代,NTP 协议就是用于协调同步设备的 UTC 时间的。
NTP 作为一种网络基础设施同 DNS 类似,是互联网早期的协议之一。NTP 意图将所有互联网中计算机的协调世界时(UTC)时间同步到几毫秒的误差内。NTP 通常可以在公共互联网保持几十毫秒的误差,在理想的局域网环境中可以实现超过不超过1毫秒的精度。
在我们使用的操作系统比如:windows,MacOS、Android,都配置有自己的NTP时间服务器来定期同步设备上的时间。
(2)NTP 网络体系结构
由于网络中需要同步时间的设备非常多,如果都连接到一台服务器上面是不现实的,所以 NTP 采用分层模型(如下图)。
每个分层成为 stratum(层级),根据到参考时钟源的距离将分层分别定义为 stratum 1、stratum 2,一直到 stratum 15,stratum 0 表示时钟源,即参考时钟。
stratum 0 是高精度的计时设备,例如原子钟(如铯、铷)、GPS/北斗卫星等,它们生成非常精确的脉冲秒信号,触发所连接计算机上的中断和时间戳,它们也称为参考 (基准) 时钟。本身不具属于 NTP。
stratum 1 的设备直接与 stratum 0 连接,误差一般在几微秒,stratum 1 的服务器也可与其他 stratum 1 服务器对等连接。在 stratum 1 的服务器也被称为主时间服务器。
stratum 2 服务器(二级时间服务器)与 stratum 1 服务器连接并同步,stratum 2 的服务器可查询多个 stratum 1 的服务器,也可与其他 stratum 2 的服务器对等连接,从而互相同步。
stratum 3 的服务器与 stratum 2 的服务器同步,同步原理与 stratum 2 相同并为下一层提供时间同步,以此类推。
层级的上限为 15,层级 16 表示设备未同步,实际上不会大于 6 级, 级别越低,精确度越高。
参考:https://zhuanlan.zhihu.com/p/640742327
2、Mybatis Plus
参考:https://baomidou.com/
(1)框架
(2)Mybatis Plus ID 生成机制
mybatis plus 框架支持类型
自定义生成组件方式
- IKeyGenerator 方式
查询数据库方式生产自增序列
/** 自定义id生成组件 */
@Component
public class SnowflakeKeyGenerator implements IKeyGenerator {
@Override
public String executeSql(String incrementerName) {
return "select " + IdUtils.getSnowflakeId() + " from dual";
}
}
/** 实体类定义 */
@KeySequence(value = "snowflake", clazz = Long.class)
public class Foo { }
- IdentifierGenerator 方式(3.3.0+)
3.3.0 后标准做法,可自定义实现 id 生成方式
/** 声明为 Bean 供 Spring 扫描注入 */
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
//可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
String bizKey = entity.getClass().getName();
//根据bizKey调用分布式ID生成
long id = ....;
//返回生成的id值即可.
return id;
}
}
- MetaObjectHandler 方式
将 IdType 申明为 INPUT 类型,并引入下面的组件
@Component
public class InputIdFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
TableInfo tableInfo = metaObject.hasGetter(Constants.MP_OPTLOCK_ET_ORIGINAL) ?
TableInfoHelper.getTableInfo(metaObject.getValue(Constants.MP_OPTLOCK_ET_ORIGINAL).getClass())
: TableInfoHelper.getTableInfo(metaObject.getOriginalObject().getClass());
if (Objects.nonNull(tableInfo)
&& !StringUtils.isEmpty(tableInfo.getKeyProperty())
&& IdType.INPUT == tableInfo.getIdType()
) {
Object idValue = metaObject.getValue(tableInfo.getKeyProperty());
if (StringUtils.checkValNull(idValue)) {
metaObject.setValue(tableInfo.getKeyProperty(), IdUtils.getSnowflakeId());
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
// Do nothing.
}
}