雪花算法(Snowflake)原理详解
雪花算法是由Twitter提出的一种分布式唯一ID生成算法,旨在为每个请求生成一个全局唯一的64位整数ID。这个ID由以下几个部分组成:
- 符号位:1位,始终为0,保证生成的ID为正数。
- 时间戳:41位,表示自定义纪元以来的时间戳(毫秒级别),可以提供约69年的时间跨度。
- 数据中心ID:5位,支持最多31个数据中心。
- 机器ID:5位,在同一数据中心内区分不同的机器或服务实例,支持最多31个实例。
- 序列号:12位,用于在同一毫秒内生成的多个ID之间进行区分,最大值为4095。
因此,一个典型的Snowflake ID结构如下:
0 - 41 bits for timestamp - 5 bits for data center id - 5 bits for worker id - 12 bits for sequence number
时间回拨处理
由于Snowflake依赖于系统时间来生成ID,因此如果服务器时间被调整(例如向后调整),可能会导致生成重复的ID。为了防止这种情况,通常会采取以下措施之一:
- 等待直到系统时间超过上次记录的时间戳。
- 使用序列号临时解决轻微的时间回拨问题。
在Spring Boot和MyBatis中的实现与集成
SnowflakeIdWorker.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SnowflakeIdWorker {
private static final Logger logger = LoggerFactory.getLogger(SnowflakeIdWorker.class);
// 自定义纪元时间戳,这里设置为当前时间
private final long twepoch = 1712636400000L; // 2025-04-09 00:00:00 UTC 时间戳
// 机器id所占的位数
private final long workerIdBits = 5L;
// 数据标识id所占的位数
private final long datacenterIdBits = 5L;
// 支持的最大机器id,结果是31 (这个值的结果应该是31,因为max值是1左移5位再减1,也就是(2^5)-1=31)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大数据标识id,结果也是31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列在id中占的位数
private final long sequenceBits = 12L;
// 工作机器ID(0~31)
private final long workerId;
// 数据中心ID(0~31)
private final long datacenterId;
// 毫秒内序列(0~4095)
private volatile long sequence = 0L;
// 上次生成ID的时间截
private volatile long lastTimestamp = -1L;
/**
* 构造函数,初始化workerId和datacenterId
* @param workerId 工作机器ID
* @param datacenterId 数据中心ID
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
* @return 下一个全局唯一的ID
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
logger.error(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & ((1L << sequenceBits) - 1);
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 时间戳改变,毫秒内序列重置
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << (sequenceBits + workerIdBits + datacenterIdBits)) |
(datacenterId << (sequenceBits + workerIdBits)) |
(workerId << sequenceBits) |
sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间戳
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
SnowflakeConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SnowflakeConfig {
/**
* 创建SnowflakeIdWorker Bean
* @return SnowflakeIdWorker实例
*/
@Bean
public SnowflakeIdWorker snowflakeIdWorker() {
// 根据实际情况调整workerId和datacenterId
return new SnowflakeIdWorker(1, 1); // 这里根据你的实际环境配置
}
}
UserMapper.java
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper {
/**
* 插入用户信息
* @param id 用户ID
* @param name 用户名
* @param email 用户邮箱
*/
@Insert("INSERT INTO users(id, name, email) VALUES(#{id}, #{name}, #{email})")
void insertUser(@Param("id") Long id, @Param("name") String name, @Param("email") String email);
}
UserService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private SnowflakeIdWorker snowflakeIdWorker;
/**
* 创建用户
* @param name 用户名
* @param email 用户邮箱
*/
public void createUser(String name, String email) {
// 使用Snowflake算法生成唯一ID
Long id = snowflakeIdWorker.nextId();
// 调用Mapper插入用户信息
userMapper.insertUser(id, name, email);
}
}
注意事项与最佳实践
时间同步:
- 确保所有服务器的时间同步非常重要。建议使用NTP服务来保证所有节点的时间一致性。
workerId和datacenterId的分配:
- 必须确保每个服务实例的
workerId
和datacenterId
是唯一的,以避免ID冲突。
- 必须确保每个服务实例的
性能优化:
- 对于高并发场景,考虑减少锁的粒度或者采用无锁编程技术。
- 可以考虑批量ID生成策略,即预先生成一批ID并在内存中缓存起来供后续请求使用,从而减少对全局锁的竞争。
日志记录与监控:
- 增加详细的日志记录,便于调试和维护。
- 设置监控指标,如生成ID的速度、失败率等,并在出现问题时触发报警。可以使用Prometheus、Grafana等工具进行监控。