一.使用原因
1.全局ID生成器:是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
唯一性 高可用 高性能 递增性 安全性
2.如果使用数据库自增ID就存在一些问题:
-
id的规律性太明显
-
受单表数据量的限制
3.业务方面:
随着我们规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。
4.如果使用redis来实现全局ID,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
通过拼接时间戳和序列号来组成唯一ID
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
二.方法代码
代码:
package com.hmdp.utils;
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;
/**
* 生产redis全局唯一id
*/
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP=1709251201L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS=32;
@Resource
private StringRedisTemplate stringRedisTemplate;
//业务前缀
public long nextId(String keyPrefix){
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long tempTimes=nowSecond-BEGIN_TIMESTAMP;
//2.生产序列号
//2.1获取当前日期,精确到天(一直用一个key仍有可能出现超过上线,因此添加每日的日期去解决该问题,也方便于统计每年/月/日的总数据个数)
String data=now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2自增长
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + data);
//3.拼接并返回
return tempTimes<<COUNT_BITS|count;
}
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2024, 3, 1, 0, 0, 0);
long second = time.toEpochSecond(ZoneOffset.UTC);
System.out.println("second = " + second);
}
}
三.测试代码
测试代码:
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task=()->{
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("begin = " + begin);
System.out.println("end = " + end);
}
测试结果
四.原理
-
对于多线程并发调用的情况,多个线程同时执行
nextId
方法时,由于stringRedisTemplate.opsForValue().increment
操作是原子的,保证了同一时刻只有一个线程能够自增count
。即使多个线程同时调用increment
方法,Redis 会保证最终结果的一致性,每个线程得到的count
值是唯一的。 -
由于每个线程调用
nextId
方法时的data
是通过时间戳获取的,而在同一秒内,多个线程生成的时间戳是相同的。这就保证了多个线程生成的 ID 在高位的时间戳部分是相同的,而在低位的序列号部分是不同的。
因此共享的 stringRedisTemplate
实例的原子自增操作,以及时间戳和序列号的组合,保证了在多线程并发调用时,每一个返回的 ID 都是不同的。