使用mysql号段方式生成唯一ID
前言
之前项目中使用的唯一ID生成方式为雪花算法,但是该方式存在“需要动态分配机器标识”和”时钟回拨“的问题,且解决起来比较复杂,所以考虑切换实现方式为数据库号段,该方式参考美团开源的项目Leaf。具体原理为
- 每个服务实例第一次从数据库加载一个号段放到内存中,比如第一个实例1-1000,第二个实例1001-2000
- 下次先从内存加载,号段用完了再从数据库加载
代码实现
数据库初始化sql
CREATE TABLE `id_segment` (
`biz_tag` int(11) NOT NULL COMMENT '业务标识',
`max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
`step` int(11) NOT NULL COMMENT '步长',
`description` varchar(256) NULL COMMENT '描述',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`biz_tag`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='ID段表'
;
-- 初始化
INSERT INTO id_segment(`biz_tag`,`max_id`,`step`,`description`) VALUES (1,0,1000,'订单ID');
实体类
import lombok.Data;
import java.sql.Timestamp;
@Data
public class IdSegment {
private Integer bizTag;
private Long maxId;
private Long step;
private String description;
private Timestamp updateTime;
}
号段包装类,使用线程安全的AtomicLong
import lombok.Data;
import java.sql.Timestamp;
import java.util.concurrent.atomic.AtomicLong;
@Data
public class IdSegmentWrapper {
private IdSegment idSegment;
private AtomicLong currentId;
public IdSegmentWrapper(IdSegment idSegment) {
this.idSegment = idSegment;
this.currentId = new AtomicLong(idSegment.getMaxId() - idSegment.getStep());
}
public long getNextId() {
return currentId.incrementAndGet();
}
public long getMaxId() {
return idSegment.getMaxId();
}
}
数据库层代码,注意要加事务注解
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class IdSegmentDao {
@Autowired
private IdSegmentMapper idSegmentMapper;
@Transactional
public IdSegmentWrapper getNext(int bizTag) {
long startTime = System.nanoTime();;
idSegmentMapper.updateMaxIdByBizTag(bizTag);
IdSegment idSegment = idSegmentMapper.selectMaxIdByBizTag(bizTag);
long endTime = System.nanoTime();
log.info("获取到ID段:{},耗时:{} ns", JSON.toJSONString(idSegment), endTime - startTime);
return new IdSegmentWrapper(idSegment);
}
}
服务层代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class IdGeneratorService {
@Autowired
private IdSegmentDao idSegmentDao;
private Map<Integer, IdSegmentWrapper> cache = new ConcurrentHashMap<>();
/**
* 获取下一个全局唯一ID,不同的业务标识起始ID不同,比如bizTag=1,则起始ID为72057594037927936
*
* @param bizTag 业务标识
*/
public Long getNextId(Integer bizTag) {
long id = generateIdByBizTag(bizTag);
id |= Long.valueOf(bizTag) << 56; //其实就是相加
return id;
}
/**
* 同一个服务实例内生成递增的唯一ID
*
* @param bizTag 业务标识
*/
private synchronized Long generateIdByBizTag(Integer bizTag) {
{
IdSegmentWrapper segment = cache.get(bizTag);
if (Objects.isNull(segment)) {
segment = idSegmentDao.getNext(bizTag);
cache.put(bizTag, segment);
}
long id = segment.getNextId();
if (id < segment.getMaxId()) {
return id;
} else {
segment = idSegmentDao.getNext(bizTag);
cache.put(bizTag, segment);
return segment.getNextId();
}
}
}
}
验证代码为
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/id")
@Slf4j
public class IdSegmentController {
@Autowired
private IdGeneratorService idGeneratorService;
@GetMapping("/get")
public String getId(@RequestParam(name = "count") Integer count) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
Set<Long> ids = new ConcurrentSkipListSet<>();
CountDownLatch latch = new CountDownLatch(count);
long start = System.nanoTime();
for (int i = 0; i < count; i++) {
executorService.submit(() -> {
try {
Long id = idGeneratorService.getNextId(1);
ids.add(id);
} finally {
latch.countDown();
}
});
}
// wait until all tasks finish or timeout
boolean completed = latch.await(30, TimeUnit.SECONDS);
long end = System.nanoTime();
executorService.shutdown();
executorService.awaitTermination(5, TimeUnit.SECONDS);
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(end - start);
log.info("生成ID数量:{}", ids.size());
log.info("生成 {} 个ID耗时:{} ms{}", count, elapsedMs, completed ? "" : " (超时未全部完成)");
return "ID生成任务已提交,耗时:" + elapsedMs + " ms";
}
}
总结
使用多线程生成100000个ID,平均耗时1200ms,这个生成速率已经能满足大部分场景了,如果想继续优化,那就需要预加载,内存号段快用完时,自动从数据库加载。

浙公网安备 33010602011771号