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);
    }
}

当多个容器实例同时运行时,可能出现以下问题:

  1. 雪花算法的workerId重复导致ID冲突
  2. 数据库自增主键在分布式环境下出现重复
  3. 分布式锁失效导致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.yml

2. 数据库配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

3. 实体类定义

// 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. 关键代码解释

  1. workerIddatacenterId的取值范围限制:确保在分布式环境中不会出现冲突
  2. sequence字段:用于处理同一毫秒内生成多个ID的场景
  3. 时钟回拨检测:防止因系统时间调整导致的ID冲突
  4. 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. 安全风险分析

  1. workerId泄露风险:建议使用UUID生成workerId,避免直接暴露敏感信息
  2. 分布式锁失效风险:需要确保Redis集群的高可用性
  3. ID预测攻击:建议对敏感业务字段进行加密处理

九、常见问题与踩坑

1. 常见错误及解决方案

问题现象原因解决方案
ID重复workerId配置重复使用UUID生成workerId
时钟回拨系统时间调整增加时钟回拨处理逻辑
分布式锁失效Redis连接异常使用哨兵或集群模式部署Redis
性能下降高并发下频繁获取锁增加锁的超时时间

2. 典型错误示例

// 错误示例:未处理时钟回拨
public long nextId() {
    long timestamp = System.currentTimeMillis();
    if (timestamp < lastTimestamp) {
        // 未处理回拨,导致ID冲突
    }
    // ...其他逻辑
}

3. 高频问题解决方案

  1. 使用分布式ID生成服务(如Snowflake、UUID、Redis自增)
  2. 对关键业务字段进行加密处理
  3. 实现完善的监控告警机制
  4. 使用分布式事务保证数据一致性

十、最佳实践

1. 推荐方案

  1. 分布式场景:建议使用Snowflake算法,配置唯一workerId
  2. 数据库自增:仅适用于单机部署或低并发场景
  3. ID格式要求:如需要特定格式,可使用UUID或自定义生成器

2. 实施建议

  1. 开发阶段:使用UUID作为workerId,避免配置错误
  2. 测试阶段:模拟多实例环境验证ID生成逻辑
  3. 生产阶段:部署Redis集群并配置监控告警
  4. 运维阶段:定期检查ID生成日志,确保无重复

3. 安全建议

  1. 在配置文件中使用加密存储敏感参数
  2. 对workerId进行加密处理,避免直接暴露
  3. 对关键业务字段进行加密处理
  4. 实现完善的日志审计机制

十一、总结

在分布式系统中,mybatis-plus的ID生成问题是一个需要特别关注的点。本文深入解析了雪花算法的原理,分析了多容器部署时出现ID重复的根本原因,并提供了完整的解决方案。通过自定义ID生成器、分布式锁机制和性能优化方案,可以有效解决分布式环境下的ID冲突问题。同时,本文也指出了在不同场景下应采用的ID生成策略,帮助开发者根据实际业务需求选择合适的方案。在实际开发中,还需要注意安全风险和性能优化,确保系统的稳定性和安全性。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日