基于Redis实现的分布式全局唯一ID

achong
2023-07-28 / 0 评论 / 5 阅读 / 正在检测是否收录...

分布式id的要求

  • 唯一性:可以在多个系统之间保持唯一性。
  • 高性能:redis是基于内存的数据库,性能极高。
  • 高可用:redis支持集群。
  • 递增性:具有单调递增的特性
  • 安全性:id拼接了其他信息,递增的规律性不会太明显。

基于redis的id的组成

id由三个部分组成,符号位,时间戳,序列号。

使用long类型存储,长整型占八个字节,1个字节等于8个比特位,即64位。

第1位表示符号位,永远是0;

第2~32位表示时间戳,以秒为单位,可以使用69年;

剩下的33~64位表示序列号,秒内的计数器,支持每秒产生2^32个不同的id;

代码实现

1、生成时间戳

  1. 需要事先准备一个固定的起始时间戳,先运行main方法里面的代码获得。
  2. 每次生成id时,获取当前时间戳。通过“起始时间戳”减去“当前时间戳”,获得组成id的时间戳。
// 1、
private static final long BEGIN_TIMESTAMP = 1672531200L;
public static void main(String[] args){
    // 获得一个时间戳,时间为2023年1月1日0点0分0秒
    LocalDateTime time = LocalDateTime.of(2023, 1, 1, 0, 0, 0);
    long timestamp = time.toEpochSecond(ZoneOffset.UTC);
    System.out.println(timestamp);  // 1672531200
}

// 2、
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond = BEGIN_TIMESTAMP;

2、生成序列号

其实就是在redis里设置一个key,通过对key自增,每次获得id使用自增后的值,组成序列号部分。

需要特别注意key的组成,因为redis中key的自增上限是2^64次方,虽然已经很大了,但毕竟还是有上限。所以解决办法就是“key+日期”。

这里将key拼接上生成id时的年月日,这样做的好处是,key会随着年月日变化;还有利于统计,某天的key自增的值就代表当前生成的id的数量。

/**
*    “icr”:自增key的标识,可改
*    “keyPrefix”:业务id
*    “date”:当前年月日
*
*    例如:icr:order:2023:07:28
*/
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);

3、组成id

long类型是占64个bit,此时将其左移32位,原来的32个位置上会自动补0。然后再将结果和序列号进行“或运算”,就得到了完整id。

long id = timestamp << 32 | count;
return id;

4、图解:

原始时间戳
时间戳1.png

左移32位的时间戳
时间戳2.png

或运算之后的时间戳
时间戳3.png

此时id的前32位是符号位和时间戳组成,后32位是存储在redis里的那个自增key的值。id会随时间戳而变化,而时间戳是精确到秒,所以理论上,就算在极高的并发下,只要一秒内并发不超过2^32次方,id就是唯一的。

完整代码

@Component
public class RedisIdWorker {
    // 开始时间戳
    private static final long BEGIN_TIMESTAMP = 1672531200L;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextID(String keyPrefix){
        // 1、生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond = BEGIN_TIMESTAMP;

        // 2、生成序列号
        // 获取当前日期,精确到天。用以解决redis中,单key自增上限问题,还有利于统计。redis单key自增上限为2^64次方
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);

        // 3、拼接并返回
            //生成的时间戳左移32位,然后或上自增长的值
        long id = timestamp << 32 | count;
        return id;
    }
}

性能测试

用线程池模拟并发,for循环提交任务。

    private ExecutorService es = Executors.newFixedThreadPool(500);
    @Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch cdl = new CountDownLatch(500);
        
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextID("order");
                System.out.println(id);
            }
            cdl.countDown();
        };
        
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < 500; i++) {
            es.submit(task);
        }
        
        cdl.await();
        stopWatch.stop();
        
        // time = 3961
        System.out.println("time = " + stopWatch.getTotalTimeMillis());
    }

本地电脑测试结果:5万个id,用时3秒多。

0

评论 (0)

取消