CC 咖啡猫的工作空间 Coding Space

业务ID生成实践指南

一、业务ID的基本概念

1.1 什么是业务ID?

业务ID是指在业务系统中用于唯一标识某个业务实体的编号,如订单号、物流单号、交易流水号等。与数据库自增ID不同,业务ID通常需要具备以下特点:

  • 可读性:对用户友好,便于记忆和沟通
  • 唯一性:在业务范围内保证唯一
  • 安全性:避免泄露业务信息或被恶意猜测
  • 扩展性:支持高并发和分布式环境

1.2 业务ID vs 技术ID

特性 业务ID 技术ID(数据库主键)
用途 用户可见,业务标识 系统内部,数据关联
格式 有业务含义,可读性强 通常为纯数字或UUID
唯一性范围 全局业务唯一 表内唯一
生成时机 业务创建时生成 数据插入时生成
示例 ORDER20240101000001 123456789

二、分布式ID生成范畴

2.1 什么是分布式ID?

分布式ID是指在分布式系统中能够保证全局唯一性的ID生成方案。当系统从单机扩展到多机集群时,传统的自增ID无法满足全局唯一性要求,因此需要专门的分布式ID生成策略。

2.2 业务ID是否属于分布式ID范畴?

答案是:取决于具体实现方式和业务需求

属于分布式ID范畴的情况:

  • 跨服务唯一性要求:多个微服务需要生成全局唯一的业务ID
  • 高并发场景:单机ID生成器无法满足性能要求
  • 无状态服务:服务实例可以动态扩缩容,无法依赖本地状态

不属于分布式ID范畴的情况:

  • 单体应用:只有一个应用实例,使用数据库序列即可
  • 局部唯一性:只需要在单个业务模块内保证唯一性
  • 低并发场景:QPS较低,单机方案足够应对

2.3 分布式ID的核心要求

  • 全局唯一性:在整个分布式系统中保证ID不重复
  • 高性能:支持高并发ID生成请求
  • 高可用:ID生成服务不能成为单点故障
  • 趋势递增:ID具有时间顺序性,便于数据库索引优化
  • 安全性:避免ID被恶意猜测或伪造

三、号段模式(Segment Mode)详解

3.1 号段模式基本原理

号段模式是一种批量分配 + 本地缓存的分布式ID生成策略。其核心思想是:

  1. 批量获取:从中心存储(如数据库)一次性获取一个ID号段(如1000-2000)
  2. 本地分配:在应用内存中逐个分配ID,无需每次访问中心存储
  3. 异步补充:当号段使用率达到阈值时,异步申请新的号段
应用实例A: [1000-1999] → 1000, 1001, 1002...
应用实例B: [2000-2999] → 2000, 2001, 2002...
应用实例C: [3000-3999] → 3000, 3001, 3002...

3.2 号段模式实现方案

3.2.1 数据库号段模式

// 数据库表结构
CREATE TABLE id_segment (
    biz_tag VARCHAR(64) PRIMARY KEY,  -- 业务标签,如'order'
    max_id BIGINT NOT NULL,           -- 当前最大ID
    step INT NOT NULL,                -- 步长(号段大小)
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

// ID段管理器
@Component
public class SegmentIdManager {
    
    @Autowired
    private IdSegmentMapper idSegmentMapper;
    
    // 缓存当前号段信息
    private final Map<String, AtomicLong> currentIds = new ConcurrentHashMap<>();
    private final Map<String, AtomicLong> maxIds = new ConcurrentHashMap<>();
    
    public long getNextId(String bizTag) {
        AtomicLong currentId = currentIds.computeIfAbsent(bizTag, k -> new AtomicLong(0));
        AtomicLong maxId = maxIds.computeIfAbsent(bizTag, k -> new AtomicLong(0));
        
        // 如果当前ID达到最大ID,需要获取新号段
        if (currentId.get() >= maxId.get()) {
            synchronized (bizTag.intern()) {
                if (currentId.get() >= maxId.get()) {
                    IdSegment segment = idSegmentMapper.getAndUpdateSegment(bizTag);
                    currentId.set(segment.getMaxId() - segment.getStep());
                    maxId.set(segment.getMaxId());
                }
            }
        }
        
        return currentId.incrementAndGet();
    }
}

// Mapper实现(MySQL)
@Update("UPDATE id_segment SET max_id = max_id + step WHERE biz_tag = #{bizTag}")
@Select("SELECT max_id, step FROM id_segment WHERE biz_tag = #{bizTag}")
public IdSegment getAndUpdateSegment(@Param("bizTag") String bizTag);

3.2.2 Redis号段模式

// 使用Redis INCRBY实现号段分配
@Component
public class RedisSegmentIdGenerator {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private final Map<String, SegmentCache> segmentCaches = new ConcurrentHashMap<>();
    
    public String generateBusinessId(String bizType) {
        SegmentCache cache = segmentCaches.computeIfAbsent(bizType, k -> new SegmentCache());
        long id = cache.getNextId();
        return bizType.toUpperCase() + id;
    }
    
    private class SegmentCache {
        private volatile long currentId = 0;
        private volatile long maxId = 0;
        private final Object lock = new Object();
        
        public long getNextId() {
            if (currentId >= maxId) {
                synchronized (lock) {
                    if (currentId >= maxId) {
                        // 从Redis获取新号段(比如每次获取1000个)
                        Long newMaxId = redisTemplate.opsForValue().increment("segment:" + bizType, 1000);
                        this.currentId = newMaxId - 1000;
                        this.maxId = newMaxId;
                    }
                }
            }
            return ++currentId;
        }
    }
}

3.3 号段模式 vs 其他分布式ID方案

方案 号段模式 雪花算法 UUID 数据库自增
全局唯一性 ❌(单表)
趋势递增
高性能 ✅(本地分配)
高可用 ✅(可降级) ⚠️(依赖时钟)
存储友好 ✅(数字) ✅(数字) ❌(字符串)
实现复杂度
适用场景 高并发业务ID 通用分布式ID 无序唯一标识 单机应用

3.4 号段模式的优势与劣势

优势:

  • 高性能:大部分ID分配在内存中完成,无需网络调用
  • 高可用:即使中心存储故障,已分配的号段仍可继续使用
  • 趋势递增:ID具有时间顺序性,利于数据库索引
  • 灵活配置:可根据业务需求调整号段大小

劣势:

  • ID浪费:服务重启可能导致未使用的ID浪费
  • 实现复杂:需要处理号段获取、缓存、同步等问题
  • 中心依赖:仍需要中心存储来分配号段
  • 内存占用:需要在内存中维护号段状态

3.5 号段模式最佳实践

号段大小选择:

  • 小号段(100-1000):适合低频业务,减少ID浪费
  • 大号段(10000+):适合高频业务,减少中心存储压力
  • 动态号段:根据业务流量动态调整号段大小

异步预加载:

// 在号段使用率达到80%时异步预加载新号段
private void preloadSegmentIfNeeded(String bizTag) {
    if (currentId.get() >= maxId.get() * 0.8) {
        CompletableFuture.runAsync(() -> {
            // 异步获取新号段
            fetchNewSegment(bizTag);
        });
    }
}

多级降级策略:

public long getNextIdWithFallback(String bizTag) {
    try {
        // 1. 优先使用号段模式
        return segmentIdManager.getNextId(bizTag);
    } catch (Exception e) {
        log.warn("Segment ID generation failed", e);
        try {
            // 2. 降级到雪花算法
            return snowflakeIdGenerator.nextId();
        } catch (Exception ex) {
            log.error("Snowflake ID generation failed", ex);
            // 3. 最终降级到UUID(转换为数字)
            return uuidToLong(UUID.randomUUID());
        }
    }
}

四、常见业务ID生成场景

4.1 电商订单号

  • 特点:包含日期、业务类型、序列号
  • 示例ORDER20240101000001
  • 要求:按天重置、可排序、防重复
  • 推荐方案:Redis原子递增(按天分key)

4.2 物流单号

  • 特点:包含快递公司编码、日期、校验码
  • 示例SF123456789CN
  • 要求:符合行业标准、包含校验位
  • 推荐方案:自定义编码规则 + 校验位

4.3 支付流水号

  • 特点:包含商户号、时间戳、随机数
  • 示例20240101123456789012
  • 要求:全局唯一、不可预测、高并发安全
  • 推荐方案:时间戳 + 随机数 或 号段模式

4.4 用户账号

  • 特点:包含业务前缀、随机字符
  • 示例USER_abc123def
  • 要求:唯一性、安全性、易输入
  • 推荐方案:Base62编码 + 随机因子

4.5 优惠券码

  • 特点:短小精悍、包含校验位
  • 示例ABC123XYZ
  • 要求:防碰撞、易输入、美观
  • 推荐方案:自定义编码 + 校验位

五、业务ID生成策略

5.1 时间戳基础型

5.1.1 纯时间戳

// 简单时间戳(精度到毫秒)
public String generateOrderId() {
    return "ORDER" + System.currentTimeMillis();
}
// 示例:ORDER1704067200123

优点:简单、有序、天然去重
缺点:可预测、暴露业务量、高并发可能重复

5.1.2 时间戳 + 随机数

// 时间戳 + 随机数
public String generatePaymentId() {
    String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
    String random = String.format("%06d", new Random().nextInt(1000000));
    return timestamp + random;
}
// 示例:20240101123456789012345

优点:不可预测、高并发安全
缺点:无序、长度较长

5.2 序列号型

5.2.1 数据库自增序列

// 使用数据库序列(MySQL示例)
@Transactional
public String generateDailyOrderId() {
    // 获取当日序列号
    Integer sequence = orderSequenceMapper.getNextSequenceForDate("ORDER", LocalDate.now());
    String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
    return "ORDER" + dateStr + String.format("%06d", sequence);
}
// 示例:ORDER20240101000001

优点:严格有序、按天重置、易读
缺点:数据库压力、性能瓶颈、分布式复杂

5.2.2 Redis原子递增

// 使用Redis INCR命令
public String generateDailyOrderIdWithRedis() {
    String dateKey = "order:seq:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
    Long sequence = redisTemplate.opsForValue().increment(dateKey);
    // 设置过期时间(第二天自动清理)
    redisTemplate.expire(dateKey, 2, TimeUnit.DAYS);
    
    String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
    return "ORDER" + dateStr + String.format("%06d", sequence);
}

优点:高性能、分布式友好、自动过期
缺点:依赖Redis、可能丢失(Redis故障)

5.3 分布式ID生成器

5.3.1 雪花算法(Snowflake)

// Twitter Snowflake算法
public class SnowflakeIdGenerator {
    private final long workerId;
    private final long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards");
        }
        
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & 0xFFF;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        return ((timestamp - 1288834974657L) << 22) |
               (datacenterId << 17) |
               (workerId << 12) |
               sequence;
    }
    
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

// 转换为业务ID
public String generateBusinessId() {
    SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
    long id = generator.nextId();
    return "ORDER" + id;
}
// 示例:ORDER1704067200123456789

优点:高性能、分布式、有序
缺点:依赖时钟、可预测、长度固定

5.3.2 美团Leaf(号段模式实现)

美团Leaf是一个开源的分布式ID生成服务,其中的Segment模式就是典型的号段模式实现。

Leaf Segment模式特点

  • 双Buffer优化:一个Buffer使用时,另一个异步加载
  • 批量获取:每次从数据库获取step大小的ID段
  • 高可用:支持降级到Snowflake模式
  • 高性能:本地分配,减少数据库压力

5.4 编码型ID

5.4.1 Base62编码

// 将数字ID转换为Base62字符串
public class Base62Encoder {
    private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    
    public static String encode(long num) {
        StringBuilder sb = new StringBuilder();
        while (num > 0) {
            sb.append(BASE62.charAt((int) (num % 62)));
            num /= 62;
        }
        return sb.reverse().toString();
    }
    
    public static long decode(String str) {
        long result = 0;
        for (int i = 0; i < str.length(); i++) {
            result = result * 62 + BASE62.indexOf(str.charAt(i));
        }
        return result;
    }
}

// 使用示例
public String generateShortOrderId(long databaseId) {
    return "O" + Base62Encoder.encode(databaseId);
}
// 示例:O1A2b3C → 对应数据库ID 123456789

优点:缩短长度、美观、可逆
缺点:需要额外编码/解码、不直观

5.4.2 自定义编码规则

// 包含业务信息的编码
public String generateLogisticsNumber(String courierCode, LocalDate date) {
    // 快递公司编码 + 日期 + 随机序列 + 校验码
    String dateStr = date.format(DateTimeFormatter.ofPattern("yyMMdd"));
    String randomSeq = String.format("%06d", new Random().nextInt(1000000));
    String rawCode = courierCode + dateStr + randomSeq;
    String checkCode = calculateCheckCode(rawCode);
    return rawCode + checkCode;
}

private String calculateCheckCode(String code) {
    // 简单的Luhn算法校验码
    int sum = 0;
    boolean alternate = false;
    for (int i = code.length() - 1; i >= 0; i--) {
        int n = Character.getNumericValue(code.charAt(i));
        if (alternate) {
            n *= 2;
            if (n > 9) n = (n % 10) + 1;
        }
        sum += n;
        alternate = !alternate;
    }
    return String.valueOf((10 - (sum % 10)) % 10);
}
// 示例:SF2401011234567

优点:包含业务信息、有校验、标准化
缺点:生成复杂、长度较长

六、业务ID设计原则

6.1 唯一性保证

  • 单机环境:时间戳 + 序列号
  • 分布式环境:雪花算法、Redis原子操作、数据库唯一约束
  • 兜底方案:生成后验证唯一性,冲突时重试

6.2 安全性考虑

  • 避免连续性:不要使用纯自增ID,防止业务量泄露
  • 增加随机性:在ID中加入随机因子
  • 长度控制:避免过长影响用户体验
  • 校验机制:重要ID加入校验位,防止输入错误

6.3 性能优化

  • 批量生成:预生成一批ID缓存使用
  • 异步生成:非关键路径异步生成ID
  • 本地缓存:减少对外部服务的依赖
  • 分段分配:每次分配一个ID段,本地递增使用

6.4 可维护性

  • 统一接口:封装ID生成逻辑,提供统一调用接口
  • 配置化:通过配置文件控制ID格式和生成策略
  • 监控告警:监控ID生成失败率和性能指标
  • 版本兼容:考虑ID格式变更的向后兼容

七、具体实现方案

7.1 通用ID生成器框架

// ID生成器接口
public interface IdGenerator {
    String generate(String businessType);
}

// 工厂模式
@Component
public class IdGeneratorFactory {
    
    @Autowired
    private Map<String, IdGenerator> generators;
    
    public IdGenerator getGenerator(String businessType) {
        return generators.getOrDefault(businessType, generators.get("default"));
    }
}

// 订单ID生成器
@Component("order")
public class OrderIdGenerator implements IdGenerator {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Override
    public String generate(String businessType) {
        String dateKey = "order:seq:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        Long sequence = redisTemplate.opsForValue().increment(dateKey);
        redisTemplate.expire(dateKey, 2, TimeUnit.DAYS);
        
        String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        return "ORDER" + dateStr + String.format("%06d", sequence);
    }
}

// 支付ID生成器
@Component("payment")
public class PaymentIdGenerator implements IdGenerator {
    
    @Override
    public String generate(String businessType) {
        String timestamp = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        String random = String.format("%06d", new SecureRandom().nextInt(1000000));
        return timestamp + random;
    }
}

// 使用示例
@Service
public class OrderService {
    
    @Autowired
    private IdGeneratorFactory idGeneratorFactory;
    
    public void createOrder(Order order) {
        String orderId = idGeneratorFactory.getGenerator("order").generate("order");
        order.setOrderId(orderId);
        // 保存订单
    }
}

7.2 配置化ID生成

# application.yml
id-generator:
  order:
    prefix: "ORDER"
    date-format: "yyyyMMdd"
    sequence-length: 6
    reset-daily: true
  payment:
    length: 20
    include-timestamp: true
    include-random: true
  user:
    prefix: "USER_"
    length: 10
    charset: "abcdefghijklmnopqrstuvwxyz0123456789"
@ConfigurationProperties(prefix = "id-generator")
@Data
public class IdGeneratorProperties {
    private Map<String, IdConfig> configs = new HashMap<>();
    
    @Data
    public static class IdConfig {
        private String prefix = "";
        private String dateFormat = "yyyyMMdd";
        private int sequenceLength = 6;
        private boolean resetDaily = false;
        private int length = 16;
        private boolean includeTimestamp = false;
        private boolean includeRandom = false;
        private String charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    }
}

7.3 高可用保障

// 多级降级策略
@Component
public class HighAvailableIdGenerator {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private IdSequenceMapper idSequenceMapper;
    
    public String generateOrderId() {
        try {
            // 1. 优先使用Redis
            return generateWithRedis();
        } catch (Exception e) {
            log.warn("Redis ID generation failed, fallback to database", e);
            try {
                // 2. 降级到数据库
                return generateWithDatabase();
            } catch (Exception ex) {
                log.error("Database ID generation failed, fallback to timestamp", ex);
                // 3. 最终降级到时间戳
                return generateWithTimestamp();
            }
        }
    }
    
    private String generateWithRedis() {
        // Redis实现
    }
    
    private String generateWithDatabase() {
        // 数据库实现
    }
    
    private String generateWithTimestamp() {
        // 时间戳实现
    }
}

八、常见问题与解决方案

8.1 时钟回拨问题

问题:雪花算法依赖系统时钟,时钟回拨会导致ID重复
解决方案

  • 监控时钟同步状态
  • 时钟回拨时等待或抛出异常
  • 使用其他ID生成策略作为备选

8.2 Redis单点故障

问题:Redis故障导致ID生成失败
解决方案

  • Redis集群部署
  • 多级降级策略(Redis → 数据库 → 时间戳)
  • 本地缓存预分配ID段

8.3 数据库性能瓶颈

问题:高并发下数据库序列成为瓶颈
解决方案

  • 批量获取序列号(一次获取1000个)
  • 使用Redis替代数据库
  • 分库分表,按业务类型分散压力

8.4 ID泄露业务信息

问题:连续ID暴露业务量和增长趋势
解决方案

  • 在ID中加入随机因子
  • 使用Base62等编码方式混淆
  • 不使用纯自增序列

九、最佳实践总结

9.1 场景选择指南

业务场景 推荐方案 理由
订单号 Redis原子递增 + 日期前缀 按天重置、有序、高性能
支付流水号 时间戳 + 随机数 全局唯一、不可预测、高并发安全
用户ID 雪花算法 + Base62编码 分布式、缩短长度、美观
优惠券码 自定义编码 + 校验位 短小精悍、防错、易输入
物流单号 行业标准编码 符合规范、包含校验
高并发业务ID 号段模式 高性能、高可用、趋势递增

9.2 开发checklist

  • 唯一性验证:确保ID在业务范围内唯一
  • 性能测试:验证高并发下的生成性能
  • 安全性评估:检查是否泄露敏感信息
  • 降级方案:准备故障时的备用方案
  • 监控告警:建立ID生成的监控体系
  • 文档说明:记录ID格式和生成规则

9.3 避免的陷阱

  • 纯自增ID:暴露业务量,安全性差
  • 无校验机制:重要ID缺少校验位
  • 单点依赖:过度依赖单一服务(如Redis)
  • 硬编码格式:ID格式写死在代码中,难以维护
  • 忽略时区问题:跨时区业务的日期处理

十、扩展思考

10.1 全球化考虑

  • 时区处理:全球业务使用UTC时间还是本地时间?
  • 字符集支持:是否支持Unicode字符?
  • 长度限制:不同国家对ID长度的要求?

10.2 合规性要求

  • GDPR:ID是否包含个人身份信息?
  • 金融监管:支付ID是否需要特定格式?
  • 行业标准:物流、医疗等行业是否有标准?

10.3 未来演进

  • 区块链ID:是否需要去中心化的ID生成?
  • AI生成:利用AI生成更智能的ID?
  • 量子安全:考虑量子计算对ID安全的影响?

总结:业务ID生成看似简单,实则涉及唯一性、安全性、性能、可维护性等多个维度的考量。号段模式作为一种高效的分布式ID生成方案,特别适合高并发的业务ID生成场景。选择合适的生成策略需要根据具体的业务场景、技术架构和安全要求来决定。建议从简单方案开始,逐步演进到更复杂的方案,同时始终保留降级和兜底机制。