Springboot项目之mybatis-plus多容器分布式部署id重复问题之源码解析
Springboot项目之mybatis-plus多容器分布式部署id重复问题之源码解析
一、背景与问题
在分布式系统中,多个容器实例同时运行时,mybatis-plus的ID生成机制可能会出现重复问题。这种问题在电商系统、即时通讯系统等高并发场景中尤为常见。例如:
// 业务代码示例
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void createOrder(Order order) {
order.setId(IdGenerateUtils.generateId());
orderMapper.insert(order);
}
}当多个容器实例同时运行时,可能出现以下问题:
- 雪花算法的workerId重复导致ID冲突
- 数据库自增主键在分布式环境下出现重复
- 分布式锁失效导致ID生成逻辑异常
二、基本原理
1. mybatis-plus的ID生成机制
mybatis-plus默认使用的是雪花算法(Snowflake),其核心原理如下:
64位结构:
| 1位 | 4位 | 5位 | 10位 | 12位 | 12位 |
| sign | datacenterId | workerId | timestamp | sequence | sequence |其中:
- sign:符号位(0)
- datacenterId:数据中心ID(默认0)
- workerId:机器ID(关键问题点)
- timestamp:时间戳(毫秒级)
- sequence:序列号(解决同一毫秒的ID冲突)
2. 分布式环境下的问题根源
当多个容器实例部署时,workerId的配置可能重复,导致生成的ID在不同实例之间出现冲突。例如:
// 错误配置示例
@Configuration
public class MyBatisPlusConfig {
@Bean
public IdWorker idWorker() {
return new SnowflakeIdWorker(1, 1); // 两个实例都配置为1
}
}3. 数据库自增主键的缺陷
部分项目使用数据库自增主键时,可能出现:
-- MySQL自增主键配置
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
...
);在分布式环境下,多个实例同时插入数据时,MySQL的auto_increment机制无法保证全局唯一性。
三、环境准备
1. 开发环境要求
- JDK 1.8+
- Spring Boot 2.7.x
- mybatis-plus-boot-starter 3.5.1
- MySQL 8.0+
- Redis(用于分布式锁)
2. 项目结构示例
src/main/java
├── com.example.demo
│ ├── config
│ │ └── IdGenerateConfig.java
│ ├── service
│ │ └── OrderService.java
│ └── entity
│ └── Order.java
└── application.yml四、核心实现
1. 自定义ID生成器
// IdGenerateConfig.java
@Configuration
public class IdGenerateConfig {
@Bean
public IdGenerator idGenerator() {
return new CustomIdGenerator();
}
}
// CustomIdGenerator.java
public class CustomIdGenerator implements IdGenerator {
private final IdWorker idWorker;
public CustomIdGenerator() {
// 使用UUID作为workerId,避免重复
String workerId = UUID.randomUUID().toString().substring(0, 8);
this.idWorker = new SnowflakeIdWorker(0, Long.parseLong(workerId, 16));
}
@Override
public Long nextId() {
return idWorker.nextId();
}
}2. 分布式锁实现
// DistributedLockUtil.java
public class DistributedLockUtil {
private static final RedisTemplate<String, String> redisTemplate;
static {
redisTemplate = (RedisTemplate<String, String>) SpringContextUtils.getBean("redisTemplate");
}
public static boolean tryLock(String lockKey, String requestId, long expireTime) {
String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"return 1 end return 0";
return (Long) redisTemplate.execute(
RedisScript.of(script, String.class), Arrays.asList(lockKey), requestId, expireTime) == 1;
}
public static void unlock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"redis.call('del', KEYS[1]) " +
"return 1 end return 0";
redisTemplate.execute(
RedisScript.of(script, String.class), Arrays.asList(lockKey), requestId);
}
}3. ID生成逻辑封装
// IdGenerateUtils.java
public class IdGenerateUtils {
private static final IdGenerator idGenerator = SpringContextUtils.getBean(IdGenerator.class);
private static final String LOCK_KEY = "id_generate_lock";
public static Long generateId() {
try {
String requestId = UUID.randomUUID().toString();
if (DistributedLockUtil.tryLock(LOCK_KEY, requestId, 30 * 1000)) {
try {
return idGenerator.nextId();
} finally {
DistributedLockUtil.unlock(LOCK_KEY, requestId);
}
}
return idGenerator.nextId();
} catch (Exception e) {
throw new RuntimeException("ID生成失败", e);
}
}
}五、完整案例
1. 项目结构说明
src/main/java
├── com.example.demo
│ ├── config
│ │ └── IdGenerateConfig.java
│ ├── service
│ │ └── OrderService.java
│ └── entity
│ └── Order.java
└── application.yml2. 数据库配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver3. 实体类定义
// Order.java
@Entity
public class Order {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
private String orderNo;
private String userId;
// 省略getter/setter
}4. 服务层实现
// OrderService.java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void createOrder(String userId) {
Order order = new Order();
order.setId(IdGenerateUtils.generateId());
order.setOrderNo("ORDER-" + System.currentTimeMillis());
order.setUserId(userId);
orderMapper.insert(order);
}
}六、源码解析
1. SnowflakeIdWorker源码分析
// SnowflakeIdWorker.java
public class SnowflakeIdWorker {
private final long twepoch = 1234567890L;
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > 31 || workerId < 0) {
throw new IllegalArgumentException("workerId must be less than 32");
}
if (datacenterId > 31 || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId must be less than 32");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timestamp();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return (timestamp - twepoch) << TIMESTAMPShift |
datacenterId << DATACENTERSHIFT |
workerId << WORKERSHIFT |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timestamp();
while (timestamp <= lastTimestamp) {
timestamp = timestamp();
}
return timestamp;
}
private long timestamp() {
return System.currentTimeMillis();
}
}2. 关键代码解释
workerId和datacenterId的取值范围限制:确保在分布式环境中不会出现冲突sequence字段:用于处理同一毫秒内生成多个ID的场景- 时钟回拨检测:防止因系统时间调整导致的ID冲突
twepoch参数:用于处理早期生成的ID与后续生成的ID之间的兼容性
七、进阶使用
1. 分布式锁优化
在高并发场景下,建议增加锁的超时时间:
public static boolean tryLock(String lockKey, String requestId, long expireTime) {
String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"return 1 end return 0";
return (Long) redisTemplate.execute(
RedisScript.of(script, String.class), Arrays.asList(lockKey), requestId, expireTime) == 1;
}2. ID生成策略切换
根据业务需求选择不同的ID生成策略:
public enum IdGenerationStrategy {
SNOWFLAKE, UUID, DATABASE
}
public class DynamicIdGenerator {
private static final Map<IdGenerationStrategy, IdGenerator> generators = new HashMap<>();
static {
generators.put(IdGenerationStrategy.SNOWFLAKE, new SnowflakeIdGenerator());
generators.put(IdGenerationStrategy.UUID, new UUIDGenerator());
generators.put(IdGenerationStrategy.DATABASE, new DatabaseIdGenerator());
}
public static void setStrategy(IdGenerationStrategy strategy) {
generators.put(currentStrategy, null);
currentStrategy = strategy;
}
public static Long generateId() {
return generators.get(currentStrategy).nextId();
}
}八、性能与工程实践
1. 性能优化方案
| 优化措施 | 说明 | 效果 |
|---|---|---|
| 预生成ID缓存 | 缓存最近生成的ID | 减少数据库访问 |
| 增加序列号位数 | 支持更多并发 | 提高并发能力 |
| 使用Redis缓存 | 缓存热点数据 | 提高查询效率 |
2. 异常处理机制
public class IdGenerateUtils {
public static Long generateId() {
try {
return idGenerator.nextId();
} catch (RuntimeException e) {
// 记录日志
logger.error("ID生成异常", e);
// 尝试重新生成
return retryGenerateId();
}
}
private static Long retryGenerateId() {
// 增加重试机制
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(100);
return idGenerator.nextId();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
throw new RuntimeException("多次尝试生成ID失败");
}
}3. 安全风险分析
- workerId泄露风险:建议使用UUID生成workerId,避免直接暴露敏感信息
- 分布式锁失效风险:需要确保Redis集群的高可用性
- ID预测攻击:建议对敏感业务字段进行加密处理
九、常见问题与踩坑
1. 常见错误及解决方案
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| ID重复 | workerId配置重复 | 使用UUID生成workerId |
| 时钟回拨 | 系统时间调整 | 增加时钟回拨处理逻辑 |
| 分布式锁失效 | Redis连接异常 | 使用哨兵或集群模式部署Redis |
| 性能下降 | 高并发下频繁获取锁 | 增加锁的超时时间 |
2. 典型错误示例
// 错误示例:未处理时钟回拨
public long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
// 未处理回拨,导致ID冲突
}
// ...其他逻辑
}3. 高频问题解决方案
- 使用分布式ID生成服务(如Snowflake、UUID、Redis自增)
- 对关键业务字段进行加密处理
- 实现完善的监控告警机制
- 使用分布式事务保证数据一致性
十、最佳实践
1. 推荐方案
- 分布式场景:建议使用Snowflake算法,配置唯一workerId
- 数据库自增:仅适用于单机部署或低并发场景
- ID格式要求:如需要特定格式,可使用UUID或自定义生成器
2. 实施建议
- 开发阶段:使用UUID作为workerId,避免配置错误
- 测试阶段:模拟多实例环境验证ID生成逻辑
- 生产阶段:部署Redis集群并配置监控告警
- 运维阶段:定期检查ID生成日志,确保无重复
3. 安全建议
- 在配置文件中使用加密存储敏感参数
- 对workerId进行加密处理,避免直接暴露
- 对关键业务字段进行加密处理
- 实现完善的日志审计机制
十一、总结
在分布式系统中,mybatis-plus的ID生成问题是一个需要特别关注的点。本文深入解析了雪花算法的原理,分析了多容器部署时出现ID重复的根本原因,并提供了完整的解决方案。通过自定义ID生成器、分布式锁机制和性能优化方案,可以有效解决分布式环境下的ID冲突问题。同时,本文也指出了在不同场景下应采用的ID生成策略,帮助开发者根据实际业务需求选择合适的方案。在实际开发中,还需要注意安全风险和性能优化,确保系统的稳定性和安全性。
评论已关闭