ID生成器--随机&雪花&redis自增

在当今的数字化世界中,无论是在社交媒体平台、电子商务网站,还是在企业级应用中,唯一标识符(Unique Identifier,简称ID)的生成都是至关重要的。ID生成器在许多场景中都有着广泛的应用,包括但不限于数据库的主键生成、订单号生成、用户会话管理、分布式系统中的资源标识等。有效的ID生成器不仅需要保证ID的唯一性,还需要考虑到生成速度、可排序性、可读性等因素。

ID生成器的重要性

  1. 唯一性:在大多数应用场景中,ID需要是全局唯一的。这是因为ID通常被用作资源的唯一标识符,如果两个资源有相同的ID,将会导致数据混淆和错误。

  2. 生成速度:在高并发的系统中,ID生成器需要能够快速地生成新的ID,以满足系统的需求。

  3. 可排序性:在某些场景中,如日志记录、订单处理等,我们可能需要根据ID的生成顺序对资源进行排序。因此,ID生成器需要能够生成可排序的ID。

  4. 可读性:虽然这不是所有场景都需要的,但在某些情况下,如调试和故障排查,可读性强的ID会非常有用。

应用场景

  1. 数据库主键生成:在数据库中,每条记录都需要一个唯一的主键。使用ID生成器,我们可以确保每条新的记录都有一个唯一的主键。

  2. 订单号生成:在电子商务网站中,每个订单都需要一个唯一的订单号。通过ID生成器,我们可以为每个新的订单生成一个唯一的订单号。

  3. 用户会话管理:在Web应用中,每个用户会话都需要一个唯一的会话ID。使用ID生成器,我们可以为每个新的用户会话生成一个唯一的会话ID。

  4. 分布式系统中的资源标识:在分布式系统中,每个资源(如文件、消息等)都需要一个唯一的标识符。通过ID生成器,我们可以为每个新的资源生成一个唯一的标识符。

背景

随着互联网的快速发展,应用的用户量和数据量都在爆炸式增长。在这种情况下,传统的ID生成方法(如数据库的自增主键)已经无法满足需求,因为它们无法在分布式环境中生成唯一的ID。因此,我们需要新的ID生成方法,如随机数+时间戳、雪花算法和Redis自增等,来满足在分布式环境中生成唯一ID的需求。

以下主要是为了介绍随机数+时间戳、雪花算法和Redis自增的实现跟优劣。

  1. 随机数+时间戳生成器

    在现代应用中,生成唯一ID的需求无处不在,如数据库的主键,订单号,甚至是游戏中的角色ID等。而随机数+时间戳的ID生成方法是一种简单而易于实现的方式。它通过将当前时间的时间戳与一个随机数相结合,产生一个独一无二的ID。这种方式的优势在于它不依赖于任何外部系统,只需要本地的时间和随机数生成器,增加日期之后可读性较高,例如:Order_20240322011809221234。但同时,它也有一个主要的缺点,那就是在高并发的情况下可能无法保证产生的ID的唯一性。

public class FinCodeIdWorker implements FinIdWorker {
    //默认随机长度
    private static final int DEFALUT_LEGTH = 6;
    private static final String yyyyMMddHHmmss = "yyyyMMddHHmmss";
    private static char[] chr = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};


    public  String nextId(){
       return nextId("");
    }

    /**
     * 指定前缀
     * @param prefix
     * @return
     */
    public  String nextId(String prefix){
        StringBuilder builder = new StringBuilder();
        builder.append(StrUtil.isEmpty(prefix)?"":prefix).append(DateUtil.format(new Date(), yyyyMMddHHmmss)).append(generateRandom(DEFALUT_LEGTH));
        return builder.toString();
    }

    /**
     * 指定前缀+随机数长度
     * @param prefix
     * @param suffixRandomLength
     * @return
     */
    public static String nextId(String prefix,int suffixRandomLength){
        StringBuilder builder = new StringBuilder();
        builder.append(StrUtil.isEmpty(prefix)?"":prefix).append(DateUtil.format(new Date(), yyyyMMddHHmmss)).append(generateRandom(suffixRandomLength));
        return builder.toString();
    }


    /**
     * 产生随机数
     *
     * @param suffixRandomLength 后缀随机长度
     * @return 随机数
     */
    public static String generateRandom(int suffixRandomLength) {
        StringBuilder bld = new StringBuilder();
        for (int i = 0; i < suffixRandomLength; i++) {
            bld.append(chr[RandomUtil.randomInt(62)]);
        }
        return bld.toString();
    }

2.雪花算法

雪花算法是Twitter公司开源的分布式ID生成算法,它可以在任何分布式系统中生成唯一的ID。雪花算法的强大之处在于它结合了时间戳,工作节点的标识和一个递增序列,以确保每一个生成的ID都是唯一的。这种方式的优势在于它能够在任何分布式环境下保证ID的唯一性,而且生成的ID是趋势递增的,对于需要排序的场景非常有用。但是,它也有一个潜在的问题,那就是如果系统的时间被调整,可能会造成ID的冲突。


/**
 * 默认Id生成器配置
 */
@Data
public class SnowFlakeIdWorkerProperties {

    /**
     * 业务id
     */
    private long businessId = 0L;

    /**
     * 机器id
     */
    private long machineId = 0L;
}


/**
 * 雪花算法生成器
 */
public class SnowFlakeIdWorker implements FinIdWorker {

    /**
     * 开始时间截 (2015-01-01)
     */
    private static final long TWEPOCH = 0x14a_a113_3c00L;

    /**
     * 机器id所占的位数
     */
    private static final long WORKER_ID_BITS = 5L;

    /**
     * 数据标识id所占的位数
     */
    private static final long DATACENTER_ID_BITS = 5L;

    /**
     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);

    /**
     * 支持的最大数据标识id,结果是31
     */
    private static final long MAX_DATACENTER_ID = -1L ^ (-1L << DATACENTER_ID_BITS);

    /**
     * 序列在id中占的位数
     */
    private static final long SEQUENCE_BITS = 12L;

    /**
     * 机器ID向左移12位
     */
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;

    /**
     * 数据标识id向左移17位(12+5)
     */
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    /**
     * 时间截向左移22位(5+5+12)
     */
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;

    /**
     * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
     */
    private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);

    /**
     * 工作机器ID(0~31)
     */
    private long workerId;

    /**
     * 数据中心ID(0~31)
     */
    private long datacenterId;

    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;

    /**
     * 上次生成ID的时间截
     */
    private long lastTimestamp = -1L;

    /**
     * 使用给定的配置构造
     *
     * @param properties 配置信息
     */
    public SnowFlakeIdWorker(SnowFlakeIdWorkerProperties properties) {
        if (properties.getBusinessId() > MAX_WORKER_ID || properties.getBusinessId() < 0) {
            throw new IllegalArgumentException(String.format("business Id can't be greater than %d or less than 0", MAX_WORKER_ID));
        }
        if (properties.getMachineId() > MAX_DATACENTER_ID || properties.getMachineId() < 0) {
            throw new IllegalArgumentException(String.format("machine Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
        }
        this.workerId = properties.getBusinessId();
        this.datacenterId = properties.getMachineId();
    }


    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowFlakeId
     */
    public synchronized String nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new IdWorkerException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        long seq = ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) //
                | (datacenterId << DATACENTER_ID_SHIFT) //
                | (workerId << WORKER_ID_SHIFT) //
                | sequence;
        return String.valueOf(seq);
    }

    public String nextId(String prefix) {
        return prefix.concat(String.valueOf(this.nextId()));
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }


    public static void main(String[] args) {
        SnowFlakeIdWorkerProperties properties = new SnowFlakeIdWorkerProperties();
        SnowFlakeIdWorker idWorker = new SnowFlakeIdWorker(properties);
        long l = System.currentTimeMillis();
        System.out.println(l);
        for (int i = 0; i < 10000; i++) {
            System.out.println(idWorker.nextId());
        }
        System.out.println(System.currentTimeMillis() -l);

        System.out.println(idWorker.nextId("JDS"));
    }
}

3. Redis自增ID生成器

Redis自增机制是一种利用Redis的原子性操作来生成唯一ID的方法。Redis是一种内存中的数据结构存储系统,它可以用作数据库、缓存和消息中介。利用Redis的INCR和INCRBY命令,我们可以在Redis中创建一个键,每次需要生成新的ID时,就将这个键的值增加1。这种方式的优势在于它简单,易于实现,而且因为Redis的高性能,它生成ID的速度非常快。但是,这种方式依赖于外部系统Redis,如果Redis服务出现问题,将无法生成新的ID。


/**
 * redis Id生成器
 */
@Slf4j
@Component
public class RedisIdWorker implements FinIdWorker {
    private static final String yyyyMMddHHmmss = "yyyyMMddHHmmss";
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    private static final Integer MAX_COUNT = 1000;
    private static final String DEFAULT_KEY = "test:idworker:default";
    private static final String DEFAULT_PREFIX = "test:idworker:";

    private Map<String,List<Long>> seqsMap = Maps.newHashMap();

    /**
     *
     * @param
     * @return
     */
    public String nextId() {
        //获取序列号
        Long redisNum = generateSeq(DEFAULT_KEY);
        //少于4位补0
        String redisSeq = StringUtils.leftPad(redisNum.toString(), 4, "0");
        // seq = 年月日+redis随机数(1s内生成1000个随机取一个)+随机数
        return DateUtil.DateToString(new Date(), yyyyMMddHHmmss) + redisSeq + generateRandom(2);
    }
    public String nextId(String prefix) {
        return prefix.concat(String.valueOf(this.nextId(DEFAULT_PREFIX.concat(prefix))));

    }

    private List<Long> createSeqs(String key) {
        List<Long> seqs = Lists.newArrayList();
        try{
            Long incNum = this.redisTemplate.opsForValue().increment(key, MAX_COUNT);
            //设置1s超时
            this.redisTemplate.expire(key, 1, TimeUnit.SECONDS);

            //生成一批序列号
            for (long i = incNum - MAX_COUNT; i < incNum ; i++) {
                seqs.add(i);
            }
        }catch (Exception e){
            throw new RuntimeException(e);
        }
        return seqs;
    }

    private synchronized long generateSeq(String key){
        List<Long> seqs = seqsMap.get(key);
        //为空或者null
        if(CollectionUtils.isEmpty(seqs)){
            //生成一批序列号
            seqs = createSeqs(key);
            //乱序
            Collections.shuffle(seqs);
            seqsMap.put(key,seqs);
        }
        //返回第一个
        return seqs.remove(0);

    }

    public static String generateRandom(int length) {
        double pow = Math.pow(10, length);
        //产生length位的随机数(不足length位前加零)
        int randomNum = RandomUtil.randomInt((int) pow);
        StringBuilder bld = new StringBuilder().append(randomNum);
        int randLength = bld.length();
        if (randLength < length) {
            for (int i = 1; i <= length - randLength; i++) {
                bld.append("0");
            }
        }
        return bld.toString();
    }

}

&lt;think&gt;好的,我现在需要回答用户关于数据库自主键实现方式和使用方法的问题。首先,我需要回顾用户提供的引用资料,整理出关键点。引用1提到分布式环境下自主键的问题和解决方案,比如分片和分布式缓存。引用2讨论了不同数据库在JDBC 3.0之前获取自主键的方法。引用3具体说明了达梦数据库如何使用序列和触发器实现自主键。引用4则解释了大厂禁止使用自主键的原因,主要是尾部热点问题。 接下来,我需要将这些信息结构化。首先介绍常见的自主键实现方式,如MySQL的AUTO_INCREMENT、Oracle的序列、SQL Server的IDENTITY,以及达梦的序列+触发器。然后说明使用方法,包括DDL语句中的设置和编程中的注意事项,比如JDBC获取主键的方式。最后,要提到使用自主键的注意事项,比如分布式环境下的问题和替代方案。 需要注意引用资料的编号,例如引用[3]中的达梦数据库设置方法,引用[2]中关于JDBC获取主键的问题,以及引用[4]中的尾部热点问题。在回答时要自然地插入引用标识,确保每个关键点都有对应的引用支持。同时,用户可能需要相关的问题,需要生成几个相关问题,如不同数据库的实现差异、分布式解决方案、优缺点等。 最后,检查是否遵循了系统级指令,比如数学公式的格式是否正确,是否有中文回答,结构是否清晰。确保没有使用任何Markdown格式,语言口语化,分步骤说明思考过程。&lt;/think&gt;### 数据库自主键的实现方式及使用方法 #### 一、自主键的实现方式 1. **内置自机制** - **MySQL**:通过`AUTO_INCREMENT`关键字实现,建表时指定主键字段即可自动生成递值。 ```sql CREATE TABLE user (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); ``` - **SQL Server**:使用`IDENTITY(1,1)`属性定义自主键,初始值为1,步长为1。 ```sql CREATE TABLE user (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50)); ``` - **Oracle**:通过序列(Sequence)和触发器(Trigger)实现,需手动创建序列并绑定到表的主键字段[^3]。 2. **序列+触发器(通用方案)** 部分数据库(如达梦)需显式创建序列和触发器实现自功能[^3]: ```sql CREATE SEQUENCE user_seq START WITH 1 INCREMENT BY 1; CREATE TRIGGER user_trigger BEFORE INSERT ON user FOR EACH ROW BEGIN SELECT user_seq.NEXTVAL INTO :NEW.id FROM DUAL; END; ``` 3. **分布式场景下的实现** 在高并发或分布式系统中,自主键可能引发性能瓶颈和尾部热点问题[^4]。解决方案包括: - **雪花算法(Snowflake)**:生成包含时间戳、机器ID和序列号的全局唯一ID- **分段缓存**:预分配ID段到客户端,减少数据库访问压力。 #### 二、使用方法及注意事项 1. **DDL语句设置** 在表结构定义时直接声明自属性(如MySQL的`AUTO_INCREMENT`),插入数据时无需显式指定主键值: ```sql INSERT INTO user (name) VALUES (&#39;Alice&#39;); -- id自动生成 ``` 2. **编程获取自主键** - **JDBC 3.0+**:通过`Statement.RETURN_GENERATED_KEYS`获取插入后的主键值。 ```java PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ResultSet rs = ps.getGeneratedKeys(); // 获取自主键 ``` - **旧版本JDBC**:需执行额外查询(如MySQL的`LAST_INSERT_ID()`)[^2]。 3. **使用限制与优化** - **分库分表场景**:自主键可能导致主键冲突,需改用分布式ID生成方案[^1]。 - **性能优化**:避免单节点写入热点,可通过哈希分片或时间戳前缀分散数据分布[^4]。 #### 三、典型问题与解决方案 - **尾部热点问题**:单调递主键导致写入集中在某个分片,可改用随机前缀或复合主键[^4]。 - **扩展性问题**:分库分表时使用分布式ID生成器(如Redis原子操作、UUID)替代自主键[^1]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值