Spring Boot项目中MyBatis-Plus多容器分布式部署ID重复问题深度剖析
Spring Boot项目中MyBatis-Plus多容器分布式部署ID重复问题深度剖析
一、引言
在微服务架构或容器化部署环境下,往往会将同一个 Spring Boot 应用镜像在多台机器或多个容器中运行,以实现高可用与负载均衡。若项目使用 MyBatis-Plus 默认的自增主键策略(AUTO_INCREMENT
),多容器并发写入数据库时,就会出现 ID 冲突或重复的问题,严重影响数据一致性。本文将从问题产生的根本原因出发,结合代码示例与图解,深入剖析常见的 ID 生成方案,并演示如何在 MyBatis-Plus 中优雅地解决分布式部署下的 ID 重复问题。
二、问题背景与分析
2.1 单实例 vs 多容器部署的差异
- 单实例部署:Spring Boot 应用只有一个实例访问数据库,使用
AUTO_INCREMENT
主键时,数据库会为每条插入操作自动分配连续且唯一的主键,几乎不存在 ID 冲突问题。 - 多容器部署:在 Kubernetes 或 Docker Swarm 等环境下,我们可能将相同应用运行多份,容器 A 和容器 B 同时向同一张表批量插入数据。如果依赖数据库自增字段,就需要确保所有写请求串行化,否则在高并发下仍会依赖数据库锁定机制。尽管数据库会避免同一时刻分配相同自增值,但在水平扩展且读写分离、分库分表等场景中,自增 ID 仍然可能产生冲突或不连续(例如各库自增起始值相同)。
另外,如果采用了分库分表,数据库层面的自增序列在不同分表间并不能保证全局唯一。更重要的是,在多副本缓存层、分布式消息队列中回写数据时,单纯的自增 ID 也会带来重复风险。
2.2 MyBatis-Plus 默认主键策略
MyBatis-Plus 的 @TableId
注解默认使用 IdType.NONE
,若数据库表主键列是自增类型(AUTO_INCREMENT
),MyBatis-Plus 会从 JDBC 执行插入后获取数据库生成的自增 ID。参考代码:
// 实体类示例
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String name;
// ... Getter/Setter ...
}
上述映射在单实例场景下工作正常,但无法在多容器分布式部署中避免 ID 重复。
三、常见分布式ID生成方案
3.1 UUID
- 原理:通过
java.util.UUID
或UUID.randomUUID()
生成一个全局唯一的 128 位标识(字符串格式),几乎不会重复。 优缺点:
- 优点:不需集中式协调,简单易用;
- 缺点:UUID 较长,存储与索引成本高;对于数字型主键需要额外转换;无法按顺序排列,影响索引性能。
示例代码:
// 在实体类中使用 UUID 作为 ID
public class Order {
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
private BigDecimal amount;
// ...
}
MyBatis-Plus IdType.ASSIGN_UUID
会在插入前调用 UUID.randomUUID().toString().replace("-", "")
,得到 32 位十六进制字符串。
3.2 数据库全局序列(Sequence)
- 多数企业数据库(如 Oracle、PostgreSQL)支持全局序列。每次从序列获取下一个值,保证全局唯一。
- 缺点:MySQL 直到 8.0 才支持
CREATE SEQUENCE
,很多旧版 MySQL 仍需通过“自增表”或“自增列+段值”来模拟序列,略显麻烦。且跨分库分表场景下,需要集中式获取序列,略损性能。
MyBatis-Plus 在 MySQL 上也可通过以下方式使用自定义序列:
// 在数据库中创建一个自增表 seq_table(id BIGINT AUTO_INCREMENT)
@TableId(value = "id", type = IdType.INPUT)
private Long id;
// 插入前通过 Mapper 获取 seq_table 的下一个自增值
Long nextId = seqTableMapper.nextId();
user.setId(nextId);
userMapper.insert(user);
3.3 Redis 全局自增
- 利用 Redis 的
INCR
或INCRBY
操作,保证在单个 Redis 实例或集群的状态下,自增序列全局唯一。 优缺点:
- 优点:性能高(内存操作),可集群部署;
- 缺点:Redis 宕机或分区时需要方案保证可用性与数据持久化,且 Redis 也是单点写。
示例代码(Spring Boot + Lettuce/Redisson):
@Autowired
private StringRedisTemplate redisTemplate;
public Long generateOrderId() {
return redisTemplate.opsForValue().increment("global:order:id");
}
// 在实体插入前设置 ID
Long id = generateOrderId();
order.setId(id);
orderMapper.insert(order);
3.4 Twitter Snowflake 算法
- 原理:Twitter 开源的 Snowflake 算法生成 64 位整型 ID,结构为:1 位符号(0),41 位时间戳(毫秒)、10 位机器标识(datacenterId + workerId,可自定义位数),12 位序列号(同一毫秒内自增)。
优缺点:
- 优点:整体性能高、单机无锁,支持多节点同时生成;ID 有时间趋势,可按时间排序。
- 缺点:需要配置机器 ID 保证不同实例的 datacenterId+workerId 唯一;时间回拨会导致冲突。
MyBatis-Plus 内置对 Snowflake 的支持,只需将 @TableId(type = IdType.ASSIGN_ID)
或 IdType.ASSIGN_SNOWFLAKE
应用在实体类上。
四、MyBatis-Plus 中使用 Snowflake 的实战演示
下面以 Snowflake 为例,演示如何在 Spring Boot + MyBatis-Plus 多容器分布式环境中确保 ID 唯一。示例将演示:
- 配置 MyBatis-Plus 使用 Snowflake
- 生成唯一的
workerId
/datacenterId
- 在实体中声明
@TableId(type = IdType.ASSIGN_ID)
- 演示两个容器同时插入数据不冲突
4.1 Spring Boot 项目依赖
在 pom.xml
中引入 MyBatis-Plus:
<dependencies>
<!-- MyBatis-Plus Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
4.2 创建一个雪花算法 ID 生成器 Bean
在 Spring Boot 启动类或单独的配置类中,注册 MyBatis-Plus 提供的 IdentifierGenerator
实现:
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SnowflakeConfig {
/**
* MyBatis-Plus 默认的雪花算法实现 DefaultIdentifierGenerator
* 使用前请确保在 application.properties 中配置了以下属性:
* mybatis-plus.snowflake.worker-id=1
* mybatis-plus.snowflake.datacenter-id=1
*/
@Bean
public IdentifierGenerator idGenerator() {
return new DefaultIdentifierGenerator();
}
}
DefaultIdentifierGenerator
会读取 Spring 环境变量 mybatis-plus.snowflake.worker-id
和 mybatis-plus.snowflake.datacenter-id
来初始化 Snowflake 算法实例,workerId
与 datacenterId
需要保证在所有容器实例中不重复。
4.3 application.yml / application.properties 配置
假设使用 YAML,分别为不同实例配置不同的 worker-id
:
spring:
application:
name: mybatisplus-demo
mybatis-plus:
snowflake:
worker-id: ${WORKER_ID:0}
datacenter-id: ${DATACENTER_ID:0}
global-config:
db-config:
id-type: ASSIGN_ID
${WORKER_ID:0}
允许通过环境变量注入,每个容器通过 Docker 或 Kubernetes 环境变量指定不同值。id-type: ASSIGN_ID
表示全局主键策略为 MyBatis-Plus 内置雪花算法生成。
启动时,在容器 A 中设置 WORKER_ID=1
,在容器 B 中设置 WORKER_ID=2
,二者保证不同,即可避免冲突。
4.4 实体类示例
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
@TableName("user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private String email;
// 自动填充示例(可选)
private LocalDateTime createTime;
private LocalDateTime updateTime;
// Getter/Setter...
}
@TableId(type = IdType.ASSIGN_ID)
:MyBatis-Plus 在插入前会调用默认的IdentifierGenerator
(即DefaultIdentifierGenerator
),按 Snowflake 算法生成唯一Long
值。
4.5 Mapper 接口与 Service 层示例
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 BaseMapper 即可具有基本 CRUD 操作
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User createUser(String username, String email) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
userMapper.insert(user);
return user;
}
}
不需要手动设置 id
,MyBatis-Plus 会自动调用 Snowflake 生成。
4.6 演示多容器插入
启动两个容器实例:
- 容器 A(
WORKER_ID=1
) - 容器 B(
WORKER_ID=2
)
同时发送如下 HTTP 请求(假设 REST API 已暴露):
POST /users 请求体: {"username":"alice","email":"alice@example.com"}
- 在容器 A 中处理时,Snowflake 算法产生的
id
例如140xxxxx0001
- 在容器 B 中处理时,Snowflake 算法产生的
id
例如140xxxxx1001
两者不会重复;如“图:多容器部署中基于Snowflake的ID生成示意图”所示,分别对应不同workerId
的实例同时向同一个共享数据库插入数据,主键不会冲突。
五、图解:多容器部署中 Snowflake ID 生成示意图
(上方已展示“图:多容器部署中基于Snowflake的ID生成示意图”)
- Container1(workerId=1) 和 Container2(workerId=2)
- 各自使用 Snowflake 算法,通过高位的
workerId
区分,生成不同 ID - 两者同时插入到共享数据库,不会产生重复的主键
六、其他分布式ID生成方案对比与选型
6.1 UUID vs Snowflake
方案 | 唯一性 | 长度 | 时间趋势 | 索引效率 | 配置复杂度 |
---|---|---|---|---|---|
UUID (String) | 极高 | 36/32 字符 | 无 | 较差 | 低 |
Snowflake | 极高 | 64 位数值 | 有 | 高 | 中 |
- 如果对 ID 长度与排序性能要求高,推荐 Snowflake。
- 若对二进制 ID 不能接受、只需简单唯一值,可使用 UUID。
6.2 Redis 全局自增 vs Snowflake
方案 | 唯一性 | 性能 | 单点压力 | 配置复杂度 |
---|---|---|---|---|
Redis INCR | 极高 | 高 | Redis 单点写 | 中 |
Snowflake | 极高 | 高 | 无单点写 | 中 |
- Redis 需考虑高可用切换与持久化,对运维要求高;Snowflake 纯 Java 实现,无额外依赖,更易水平扩展。
七、总结与实践建议
- 避免数据库自增主键
多容器部署时不要再依赖单一数据库自增,应选用分布式 ID 生成方案。 选择合适的方案
- Snowflake:大多数场景下的首选,性能高、可排序;
- UUID:对性能与索引要求不高、需要跨语言兼容时可采纳;
- Redis:需谨慎考虑 Redis 高可用与分区容错。
- 环境变量注入
workerId
在 Kubernetes 中可通过 ConfigMap 或 Deployment 环境变量注入不同的WORKER_ID
,确保各实例唯一。 - 注意时钟回拨问题
如果服务器时间被回调,会导致 Snowflake 生成重复或回退 ID,请使用 NTP 保证时钟一致或引入时间回拨处理逻辑。 - 回源策略
如果数据库或 ID 服务不可用,应对插入操作进行失败重试或降级,避免影响业务可用性。
综上所述,通过在 Spring Boot + MyBatis-Plus 中使用 Snowflake(IdType.ASSIGN_ID
)或其他分布式 ID 生成器,可以有效避免多容器部署下的 ID 重复问题,保障系统高可用与数据一致性。
评论已关闭