一、分布式系统
分布式系统是指将多个独立的计算节点通过网络连接,协同完成同一目标的系统架构。其核心特征是:
- 多个独立节点:每个节点都是一个可独立运行的服务实例
- 网络通信:节点间通过网络协议(如HTTP、RPC)交换数据
- 协同工作:共同完成统一的业务目标(如处理请求、存储数据)
二、分布式环境下 Spring Boot 项目的部署方法
(一) Docker多节点部署
- 实现方式:将同一个Spring Boot JAR包构建为Docker镜像,然后在多个服务器(物理机/虚拟机)上启动多个容器实例。
- 示例场景:
服务器A:运行Docker容器实例1(端口8081) 服务器B:运行Docker容器实例2(端口8082) 服务器C:运行Docker容器实例3(端口8083)
- 关键配置:
- 每个容器需分配唯一的雪花算法参数(
dataCenterId
和machineId
) - 通过Nginx等负载均衡器将请求分发到不同容器
- 示例雪花算法配置(动态获取容器ID):
// 从Docker环境变量获取节点标识(需在docker run时通过-e参数传入) long dataCenterId = Long.parseLong(System.getenv("DATACENTER_ID")); long machineId = Long.parseLong(System.getenv("MACHINE_ID"));
- 每个容器需分配唯一的雪花算法参数(
(二)微服务架构下的多服务节点
- 场景:一个大型系统拆分为多个独立服务,每个服务部署为分布式节点
- 用户服务(user-service):部署3个节点 - 订单服务(order-service):部署5个节点 - 商品服务(product-service):部署2个节点
- 雪花算法应用:
- 每个服务集群分配独立的
dataCenterId
(如用户服务为1,订单服务为2) - 每个服务节点分配唯一的
machineId
(如节点1、节点2)
- 每个服务集群分配独立的
三、分布式环境下雪花算法的使用问题
(一) 节点ID分配问题
- 挑战:如何确保分布式节点的
dataCenterId
和machineId
唯一 - 解决方案:
- 配置文件静态分配:适用于节点固定的场景(如手动编辑
application.yml
)snowflake: data-center-id: 1 # 数据中心ID(0-31) machine-id: 2 # 机器ID(0-31)
- 分布式协调服务动态分配:适用于动态扩缩容场景(如ZooKeeper)
// 通过ZooKeeper获取唯一节点ID String nodePath = zkClient.createEphemeralSequential("/snowflake/nodes/", "node-"); long machineId = Long.parseLong(nodePath.substring(nodePath.lastIndexOf("-") + 1));
- 配置文件静态分配:适用于节点固定的场景(如手动编辑
(二)时钟回拨问题
- 场景:某节点因硬件故障或时区调整导致系统时间回退
- 解决方案:
- 雪花算法已内置时钟回拨处理(如示例中的
waitNextMillis
方法) - 更严格的方案:结合Redis等分布式锁,在时钟回拨时阻塞生成ID
明白了!我将修改雪花算法的实现,使其适应你描述的分布式用户服务场景。关键是要为每个节点分配唯一的dataCenterId
和machineId
。
- 雪花算法已内置时钟回拨处理(如示例中的
四、示例:雪花算法实现
SnowflakeIdGenerator
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 分布式环境下的雪花算法ID生成器
* 支持从配置文件或环境变量读取dataCenterId和machineId
*/
@Component
public class SnowflakeIdGenerator {
// 起始时间戳 (2020-01-01)
private final long startTimeStamp = 1577836800000L;
// 各部分占用的位数
private final long dataCenterIdBits = 5L;
private final long machineIdBits = 5L;
private final long sequenceBits = 12L;
// 各部分的最大值
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); // 31
private final long maxMachineId = -1L ^ (-1L << machineIdBits); // 31
private final long maxSequence = -1L ^ (-1L << sequenceBits); // 4095
// 各部分向左的位移
private final long machineIdShift = sequenceBits;
private final long dataCenterIdShift = sequenceBits + machineIdBits;
private final long timestampShift = sequenceBits + machineIdBits + dataCenterIdBits;
private final long dataCenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
/**
* 构造函数,从配置中读取dataCenterId和machineId
*/
public SnowflakeIdGenerator(
@Value("${snowflake.datacenter-id:1}") long dataCenterId,
@Value("${snowflake.machine-id:1}") long machineId
) {
// 检查ID是否合法
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException("DataCenter ID must be between 0 and " + maxDataCenterId);
}
if (machineId > maxMachineId || machineId < 0) {
throw new IllegalArgumentException("Machine ID must be between 0 and " + maxMachineId);
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
System.out.printf("初始化雪花算法ID生成器: datacenterId=%d, machineId=%d%n", dataCenterId, machineId);
}
// 生成ID的核心方法保持不变
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 处理时钟回拨
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " +
(lastTimestamp - currentTimestamp) + " milliseconds");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
// 当前毫秒内序列号已用完,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,重置序列号
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 按位或组合生成ID
return ((currentTimestamp - startTimeStamp) << timestampShift) |
(dataCenterId << dataCenterIdShift) |
(machineId << machineIdShift) |
sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
User
@Data
public class User {
private Long id;
private String username;
private String createTime;
private String updateTime;
}
UserController
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
UserService
public interface UserService {
/**
* 创建用户
* @param user 用户信息
* @return 创建后的用户对象(包含生成的ID)
*/
User createUser(User user);
/**
* 根据ID获取用户
* @param id 用户ID
* @return 用户对象
*/
User getUserById(Long id);
}
UserServiceImpl
createUser方法:
- 调用idGenerator.nextId()生成全局唯一 ID
- 设置用户的创建时间和更新时间
- 调用 MyBatis 的 Mapper方法将用户信息插入数据库
确保了在分布式环境下,即使多个用户服务节点同时处理创建用户请求,生成的 ID 也不会冲突。
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private SnowflakeIdGenerator idGenerator;
@Autowired
private UserMapper userMapper;
@Override
public User createUser(User user) {
// 生成全局唯一ID
long userId = idGenerator.nextId();
user.setId(userId);
// 设置创建时间和更新时间
LocalDateTime now = LocalDateTime.now();
user.setCreateTime(now);
user.setUpdateTime(now);
// 插入数据库
userMapper.insert(user);
return user;
}
@Override
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
配置文件
application-node1.yml
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
# 雪花算法配置
snowflake:
datacenter-id: 1 # 数据中心ID
machine-id: 1 # 机器ID (节点A)
application-node2.yml
server:
port: 8082
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
# 雪花算法配置
snowflake:
datacenter-id: 1 # 数据中心ID
machine-id: 2 # 机器ID (节点B)
application-node3.yml
server:
port: 8083
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
# 雪花算法配置
snowflake:
datacenter-id: 1 # 数据中心ID
machine-id: 3 # 机器ID (节点C)