5年的项目,使用的雪花算法生成的id居然出现了重复?

5年项目雪花算法生成id重复问题排查

背景

今天线上出现了数据表插入数据失败的情况。

异常日志如下:

即出现了主键冲突。

需注意:该服务在线上环境部署了6个实例。

问题排查

根据此主键id查询数据表,结果确实显示已存在主键为该id的数据,并且此数据由其它用户插入。

由于主键id是由雪花算法生成,因此初步判断是雪花算法生成了重复id。

雪花算法配置

项目使用的是hutool工具的雪花算法。相关配置代码如下:

@Component
public class SnowflakeConfig {

    /**
     * 数据中心ID
     */
    private final long datacenterId = 1;
    /**
     * 为终端ID
     */
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private long workerId = 0;
    private final Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);

    @PostConstruct
    public void init() {
        workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
    }

    public synchronized long getId() {
        return snowflake.nextId();
    }

}

查看代码提交记录发现,上述代码已有5年的历史。

预期配置效果:根据每个实例所属的机器ip配置雪花算法的workId,以保证雪花算法在每个实例上workId与datacenterId组合的唯一性。

上述代码存在两处错误。

Snowflake实例初始化时机错误

如果你了解spring bean的生命周期,显然会发现:

Snowflake 实例在类初始化时创建(snowflake = IdUtil.createSnowflake(workerId, datacenterId)),但 workerId 的值是在 @PostConstructinit() 方法中赋值的。此时 workerId 的初始值为 0,导致 snowflake 实例使用的 workerId 始终为 0,而非预期的IP转换值。

影响:所有生成的ID的 workerId 固定为0,在分布式环境下不同节点生成的ID可能冲突

WorkerId取值范围超限

雪花算法中,workerIddatacenterId 的位数通常各为 5位(取值范围 0~31)。而 NetUtil.ipv4ToLong() 返回的IPv4地址的long值(如 192.168.1.13232235777)远大于 31,导致实际 workerId 无效。

hutool工具有对配置的workerIddatacenterId进行范围校验。不在取值范围里将会出现异常。

解决方案

必要操作:将 snowflake 的初始化逻辑移至 init() 方法中,确保使用正确的 workerId。

workerId生成方式

workerId的生成方式有以下几种方案,但均有不足。目前采用的是随机数方式生成workId。

使用实例的ip地址

IP地址进行哈希取模,确保workId在有效范围内。

影响

如果实例是部署在不同的机器上,上述方案基本没啥问题。可是咨询运维发现,在资源紧张的情况下,多个实例有可能部署在同一机器上,这就会导致上述方案生成的workId可能重复。

使用实例容器的id

因为每个实例最终都会生成一个容器id,所以可依据容器id来配置workId。由于对项目改动较大,并且对每个实例的容器id进行哈希取模后,生成的workId也不能保证绝对不重复,所以暂未使用此方案。

随机数

直接生成0-31的随机数作为workId。

影响

随机数的生成无法控制,照样可能出现workId重复的情况。

最终代码

最终雪花算法的配置代码如下:

@Component
@Slf4j
public class SnowflakeConfig {

    /**
     * 数据中心ID
     */
    private final long datacenterId = 1;
    /**
     * 为终端ID
     */
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private long workerId = 0;
    private Snowflake snowflake;

    @PostConstruct
    public void init() {
        Random random = new Random();
        workerId = random.nextInt(32);
        log.info("当前实例生成的workerId为:{}", workerId);
        snowflake = IdUtil.createSnowflake(workerId, datacenterId);
    }

    public long getId() {
        return snowflake.nextId();
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值