使用mysql号段方式生成唯一ID

前言

之前项目中使用的唯一ID生成方式为雪花算法,但是该方式存在“需要动态分配机器标识”和”时钟回拨“的问题,且解决起来比较复杂,所以考虑切换实现方式为数据库号段,该方式参考美团开源的项目Leaf。具体原理为

  1. 每个服务实例第一次从数据库加载一个号段放到内存中,比如第一个实例1-1000,第二个实例1001-2000
  2. 下次先从内存加载,号段用完了再从数据库加载

代码实现

数据库初始化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,这个生成速率已经能满足大部分场景了,如果想继续优化,那就需要预加载,内存号段快用完时,自动从数据库加载。

参考

Leaf:美团分布式ID生成服务开源

posted @ 2026-01-01 13:03  strongmore  阅读(10)  评论(0)    收藏  举报