背景
今天线上出现了数据表插入数据失败的情况。
异常日志如下:
即出现了主键冲突。
需注意:该服务在线上环境部署了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
的值是在 @PostConstruct
的 init()
方法中赋值的。此时 workerId
的初始值为 0,导致 snowflake
实例使用的 workerId
始终为 0,而非预期的IP转换值。
影响:所有生成的ID的 workerId
固定为0,在分布式环境下不同节点生成的ID可能冲突。
WorkerId取值范围超限
雪花算法中,workerId
和 datacenterId
的位数通常各为 5位(取值范围 0~31)。而 NetUtil.ipv4ToLong()
返回的IPv4地址的long值(如 192.168.1.1
→ 3232235777
)远大于 31,导致实际 workerId
无效。
hutool工具有对配置的workerId
和 datacenterId进行范围校验。不在取值范围里将会出现异常。
解决方案
必要操作:将 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();
}
}