Redis-利用redis生成分布式ID(全局唯一)

668 阅读2分钟

分布式系统中,由于服务之间是隔离的,所以有些业务场景下会需要一个全局唯一的ID,在各服务之间流转,一般来讲简单的使用UUID也能达到一样的效果。但是有些场景,比如做分库分表操作,如果分表之后,显然不能使用数据库的单调递增方案,因为一定会出现重复。即使不是分库分表操作,如果我们不希望主键ID被外部轻易就能猜测到它的规律,那么还是需要自己去生成的。此时UUID明显不适合,其一,不利于索引;其二,会导致页分裂,影响查询效率。此时我们希望主键ID还是数字,且单调递增,这样方便索引建立,也方便查询,同时还能自己控制生成的方式,防止ID太简单而被猜到。

基于此,定义如下规则:使用一个64位的长度来存储分布式ID,前32位以时间戳(精确到秒)来标识,后32位靠redis的自增来生成一个序列号,然后将时间戳与序列号拼接得到最终的分布式ID。代码如下:

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

/**
 * 生成64位分布式ID,前32位时间戳,后32位序列号,序列号由redis自增生成
 */
@Component
public class DistributedIdGenerateWorker {

    /**
     * 指定的开始日期的秒数 2023.1.1:0:0:0
     */
    private static final long BEGIN_SECONDS = 1672531200L;

    /**
     * 序列号按天生成,防止溢出,使用 ':' 分割方便以后按年月日统计
     */
    private static final String DATE_FORMAT = "yyyy:MM:dd";

    /**
     * 序列号的位数
     */
    private static final int SERIAL_NUM_BITS = 32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @SuppressWarnings("all")
    public Long generate(String keyPrefix) {
        // 获取时间戳 当前时间离指定的开始时间的毫秒数
        LocalDateTime now = LocalDateTime.now();
        long nowSeconds = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSeconds - BEGIN_SECONDS;
        String dateFormat = now.format(DateTimeFormatter.ofPattern(DATE_FORMAT));
        // 获取序列号,key不存在会自动创建,key在这里是每天生成一个,一方面方便后续可能有统计需求,
        // 另一方面如果一直就1个key的话,业务量较大,可能过些时间就能把32位占满
        Long serialNum = stringRedisTemplate.opsForValue().increment("incr:" + keyPrefix + ":" + dateFormat);
        // 拼接生成分布式ID 通过将时间戳左移32位到高位,用或运算将序列号放在剩下的32位
        return timestamp << SERIAL_NUM_BITS | serialNum;
    }

}