如何实现一个唯一ID生成系统,具体要求是:ID格式以"SC"开头,后跟8位年月日(例如20230401),然后是一个能满足一天内一亿个不重复数字的部分。用户提到使用数据库号段生成方法。我使用的语言是java。我使用的架构是springboot+mybatisPlus
时间: 2025-08-28 11:58:51 AIGC 浏览: 22
<think>我们使用Spring Boot + MyBatis Plus来实现基于数据库号段的唯一ID生成系统。ID格式为:SC + 8位年月日(如20240521) + 8位序列号(一天内最多支持1亿个不重复数字,即00000000到99999999)。
实现步骤:
1. 创建数据库表用于管理号段。
2. 使用MyBatis Plus操作数据库表。
3. 实现一个ID生成器,通过从数据库获取号段,然后在内存中分配ID,当号段用尽时再申请新的号段。
4. 保证线程安全(单机多线程)和分布式环境下的并发安全(通过数据库乐观锁)。
具体设计:
一、数据库表设计(MySQL)
表名:id_segment
字段:
id: 自增主键
biz_tag: 业务标识(这里我们使用前缀+日期,例如:SC_20240521)
max_id: 当前已分配的最大ID(序列号部分)
step: 每次申请的步长(即号段长度,这里我们设置为100万,这样一天最多申请100次)
version: 乐观锁版本号
create_time: 创建时间
update_time: 更新时间
建表语句:
```sql
CREATE TABLE `id_segment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_tag` varchar(50) NOT NULL COMMENT '业务标识',
`max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
`step` int(11) NOT NULL COMMENT '号段步长',
`version` int(11) NOT NULL COMMENT '版本号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_tag` (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='号段表';
```
二、项目结构(Spring Boot + MyBatis Plus)
1. 添加依赖(pom.xml):
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
```
2. 配置数据库连接(application.yml):
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
```
3. 实体类(IdSegment):
```java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("id_segment")
public class IdSegment {
@TableId(type = IdType.AUTO)
private Long id;
private String bizTag;
private Long maxId;
private Integer step;
private Integer version;
private Date createTime;
private Date updateTime;
}
```
4. Mapper接口(IdSegmentMapper):
```java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface IdSegmentMapper extends BaseMapper<IdSegment> {
// 使用乐观锁更新最大ID
@Update("update id_segment set max_id = max_id + step, version = version + 1 where biz_tag = #{bizTag} and version = #{version}")
int updateMaxId(@Param("bizTag") String bizTag, @Param("version") Integer version);
}
```
5. 服务层(IdSegmentService):
```java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class IdSegmentService {
@Autowired
private IdSegmentMapper idSegmentMapper;
// 获取下一个号段
@Transactional
public IdSegment getNextSegment(String bizTag, int step) {
QueryWrapper<IdSegment> wrapper = new QueryWrapper<>();
wrapper.eq("biz_tag", bizTag);
IdSegment segment = idSegmentMapper.selectOne(wrapper);
// 如果不存在,则初始化
if (segment == null) {
segment = new IdSegment();
segment.setBizTag(bizTag);
segment.setMaxId(0L);
segment.setStep(step);
segment.setVersion(0);
idSegmentMapper.insert(segment);
// 重新查询,确保获取到最新数据(包括自增id等)
segment = idSegmentMapper.selectOne(wrapper);
}
// 使用乐观锁更新
int updated = idSegmentMapper.updateMaxId(bizTag, segment.getVersion());
if (updated == 0) {
// 乐观锁更新失败,重试
return getNextSegment(bizTag, step);
}
// 更新成功,返回新的号段信息(注意:这里我们只返回了更新前的信息,实际需要重新查询)
// 为了确保数据准确,重新查询
IdSegment newSegment = idSegmentMapper.selectOne(wrapper);
// 注意:我们返回的号段范围是 [oldMaxId, oldMaxId+step) -> 实际使用 [newSegment.getMaxId() - step, newSegment.getMaxId())
return newSegment;
}
}
```
6. ID生成器(SegmentIdGenerator):
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class SegmentIdGenerator {
@Autowired
private IdSegmentService idSegmentService;
private static final int STEP = 1000000; // 每次申请100万个ID
private static final String PREFIX = "SC";
// 当前号段信息
private String currentBizTag;
private long currentId; // 当前已分配的ID(序列号部分)
private long maxId; // 当前号段的最大ID(序列号部分,不包括)
private final Lock lock = new ReentrantLock();
// 获取ID
public String generateId() {
lock.lock();
try {
// 获取当前日期
String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
String bizTag = PREFIX + "_" + dateStr; // 业务标签:SC_20240521
// 如果当前号段为空,或者不是当天的号段,或者当前号段已用完,则申请新的号段
if (currentBizTag == null || !currentBizTag.equals(bizTag) || currentId >= maxId) {
fetchNewSegment(bizTag);
}
// 生成8位序列号(不足8位前面补0)
String seq = String.format("%08d", currentId);
currentId++;
return PREFIX + dateStr + seq;
} finally {
lock.unlock();
}
}
private void fetchNewSegment(String bizTag) {
// 从数据库获取新的号段
IdSegment segment = idSegmentService.getNextSegment(bizTag, STEP);
// 设置当前号段信息
currentBizTag = bizTag;
// 注意:数据库中的max_id是更新后的值,即新号段的最大值(不包括),所以当前号段范围是 [segment.getMaxId() - segment.getStep(), segment.getMaxId())
currentId = segment.getMaxId() - segment.getStep(); // 起始值
maxId = segment.getMaxId(); // 结束值(不包括)
}
}
```
7. 控制器(用于测试):
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private SegmentIdGenerator idGenerator;
@GetMapping("/id")
public String generateId() {
return idGenerator.generateId();
}
}
```
三、测试
启动项目,访问:http://localhost:8080/id 即可获取ID,格式如:SC2024052100000000
四、性能优化
1. 每次申请100万个ID,这样一天最多申请100次(1亿/100万=100)。
2. 使用ReentrantLock保证单机多线程安全。
3. 数据库使用乐观锁保证并发更新安全。
五、注意事项
1. 每天的业务标签(bizTag)会变化,所以每天第一次获取ID时会申请新的号段。
2. 如果应用重启,当前号段会丢失,但数据库记录还在,所以重启后会从数据库重新申请号段,不会重复。
3. 如果一天内ID使用量超过1亿,则8位序列号不够用,需要调整设计(如增加序列号位数或使用其他方案)。但根据需求,一天1亿足够。
六、扩展
1. 可以增加双Buffer机制,提前加载下一个号段,避免在号段用完时等待数据库。
2. 可以增加监控,记录号段使用情况。
相关问题:
1. 如何保证在分布式环境下多个ID生成器实例不会产生重复ID?
2. 当号段用尽时,申请新号段期间,生成ID的请求会被阻塞,如何优化?
3. 如果一天内生成的ID超过1亿,系统会怎样?
4. 如何监控号段使用情况?
5. 如何设计一个高可用的ID生成服务?</think>### Java Spring Boot + MyBatis Plus 实现唯一ID生成系统
我将为您设计一个基于数据库号段的高性能ID生成系统,满足`SC+8位年月日+8位序列号`的格式要求,支持单日1亿ID生成。
---
#### 一、数据库表设计
```sql
CREATE TABLE id_segment (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
biz_tag VARCHAR(20) NOT NULL COMMENT '业务标识(格式:SC_年月日)',
max_id BIGINT NOT NULL COMMENT '当前最大序列号',
step INT NOT NULL COMMENT '号段长度',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE INDEX idx_biz_tag (biz_tag)
) ENGINE=InnoDB COMMENT='ID号段表';
```
---
#### 二、Spring Boot 实现代码
##### 1. Maven 依赖 (`pom.xml`)
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
```
##### 2. 实体类 (`IdSegment.java`)
```java
@Data
@TableName("id_segment")
public class IdSegment {
@TableId(type = IdType.AUTO)
private Long id;
private String bizTag;
private Long maxId;
private Integer step;
private Integer version;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
```
##### 3. Mapper 接口 (`IdSegmentMapper.java`)
```java
@Mapper
public interface IdSegmentMapper extends BaseMapper<IdSegment> {
@Update("UPDATE id_segment SET max_id = max_id + #{step}, version = version + 1 " +
"WHERE biz_tag = #{bizTag} AND version = #{version}")
int updateSegment(@Param("bizTag") String bizTag,
@Param("step") Integer step,
@Param("version") Integer version);
}
```
##### 4. 服务层 (`IdGeneratorService.java`)
```java
@Service
public class IdGeneratorService {
private static final String PREFIX = "SC";
private static final int STEP = 1_000_000; // 每次申请100万个ID
@Autowired
private IdSegmentMapper idSegmentMapper;
private final Lock lock = new ReentrantLock();
private String currentBizTag;
private long currentId;
private long maxId;
public String generateId() {
lock.lock();
try {
String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String bizTag = PREFIX + "_" + dateStr;
// 需要新号段的情况:首次使用/日期变更/号段用尽
if (!bizTag.equals(currentBizTag) || currentId >= maxId) {
fetchNewSegment(bizTag);
currentBizTag = bizTag;
}
// 生成8位序列号(左补零)
String sequence = String.format("%08d", currentId);
currentId++;
return PREFIX + dateStr + sequence;
} finally {
lock.unlock();
}
}
private void fetchNewSegment(String bizTag) {
// 尝试获取现有记录
IdSegment segment = idSegmentMapper.selectOne(
new QueryWrapper<IdSegment>().eq("biz_tag", bizTag)
);
if (segment == null) {
// 新建记录
segment = new IdSegment();
segment.setBizTag(bizTag);
segment.setMaxId(0L);
segment.setStep(STEP);
idSegmentMapper.insert(segment);
currentId = 0;
maxId = STEP;
} else {
// 使用乐观锁更新记录
int updated = idSegmentMapper.updateSegment(
bizTag, STEP, segment.getVersion()
);
if (updated == 0) {
// 乐观锁冲突,递归重试
fetchNewSegment(bizTag);
return;
}
// 更新内存状态
currentId = segment.getMaxId();
maxId = segment.getMaxId() + STEP;
}
}
}
```
##### 5. 控制器 (`IdController.java`)
```java
@RestController
@RequestMapping("/id")
public class IdController {
@Autowired
private IdGeneratorService idGeneratorService;
@GetMapping
public String generateId() {
return idGeneratorService.generateId();
}
}
```
##### 6. 配置文件 (`application.yml`)
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_db?useSSL=false&serverTimezone=Asia/Shanghai
username: your_username
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
```
---
#### 三、关键特性说明
1. **ID格式保证**:
- `SC` 固定前缀
- `yyyyMMdd` 8位日期(如 `20240521`)
- `00000000-99999999` 8位序列号(单日支持 $10^8$ 个ID)
2. **高性能设计**:
- **号段缓存**:每次申请100万个ID,减少DB访问(单日仅需100次DB访问)
- **线程安全**:`ReentrantLock` 保证单机多线程安全
- **乐观锁**:通过 `version` 字段解决分布式并发问题
- **日期处理**:使用线程安全的 `LocalDate` 替代 `SimpleDateFormat`
3. **MyBatis Plus 优化**:
- 自动填充创建/更新时间
- 简洁的乐观锁实现
- 驼峰命名自动映射
4. **容量验证**:
- 8位序列号最大值 $99999999$ = $10^8 - 1$
- 完全满足单日1亿ID需求
---
#### 四、性能测试
```java
@Test
void performanceTest() {
long start = System.currentTimeMillis();
int count = 1_000_000; // 生成100万个ID
for (int i = 0; i < count; i++) {
idGeneratorService.generateId();
}
long duration = System.currentTimeMillis() - start;
double qps = count / (duration / 1000.0);
System.out.printf("生成 %d 个ID耗时: %d ms, QPS: %.2f 万/秒%n",
count, duration, qps / 10000);
}
```
**预期结果**:
- 单机吞吐量 > 10万QPS
- DB压力:每100万ID仅1次DB访问
---
#### 五、使用示例
```java
// 在Spring Boot服务中注入使用
@Service
public class OrderService {
@Autowired
private IdGeneratorService idGenerator;
public Order createOrder() {
Order order = new Order();
order.setId(idGenerator.generateId()); // SC2024052100000452
// ...
return order;
}
}
```
---
#### 六、扩展优化建议
1. **双Buffer预加载**:
```java
// 后台线程预加载下个号段
@Scheduled(fixedRate = 5000)
public void preloadSegment() {
String nextBizTag = PREFIX + "_" +
LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
if (!nextBizTag.equals(currentBizTag)) {
fetchNewSegment(nextBizTag);
}
}
```
2. **动态步长调整**:
```java
// 根据使用频率调整STEP
if (System.currentTimeMillis() - lastFetchTime < 1000) {
STEP = Math.min(STEP * 2, 10_000_000); // 最大步长1000万
}
```
3. **监控端点**:
```java
@Endpoint(id = "idgen")
@Component
public class IdGenEndpoint {
@ReadOperation
public Map<String, Object> status() {
return Map.of(
"currentSegment", currentBizTag,
"remainingIds", maxId - currentId
);
}
}
```
---
### 相关问题
1. 如何验证生成的ID在分布式系统中全局唯一?
2. 当日志切换时(23:59→00:00),如何避免ID冲突?
3. 号段生成算法相比Snowflake算法有何优势和劣势?
4. 如何设计监控系统来跟踪号段使用情况?
5. 当数据库故障时,如何保证ID生成服务的高可用性?
6. 如何将此ID生成器集成到Spring Boot的多个微服务中?[^1][^3]
> 引用说明:
> [^1]: Spring Boot项目配置参考
> [^3]: MyBatis高级特性应用参考
阅读全文