背景
新公司用mysql开发,对于建表什么的规定特别严格,主键id必须是自增,但是取模分表的过程中,id是不能重复的,所以对于分库分表这种场景,在全局实现一个唯一id显得特别重要。
方法
想到唯一id,我第一想到的就是uuid,32位字符串,但是考虑到uuid字符串比纯数字的id性能和空间有些许浪费(可能这个并没有很明显的区别,但是公司规范上能用纯数字就避免用长字符串),所以这个方案被pass了。
其他方法比如雪花算法,这个也是本地生成,但是有个弊端,很依赖机器的自带时间,只是有点了解,想到不是最佳方案,也被pass掉了。
这里着重讲一下redis显示全局唯一id
思路
利用redis单线程实现自增不重复。
13位毫秒时间戳 + 5位自增数字(前面不够5位的补0)+ 1位随机数(0,9),组成19位不重复的数字,并且趋势递增。
把13位毫秒时间戳作为key存到redis,设置过期时间为2s,在同一个key的时候,后5为从1自增,也就是说,同一毫秒内最大支持99999个请求,可以满足需求了。
正文:
- 一、全局唯一ID
-
- 二、Redis生成全局唯一ID
1、snowflake算法全局唯一ID策略
2、Redis自增全局唯一ID策略
-
- 三、代码实现
一、全局唯一ID
1、全局唯一ID特点
- ① 全局唯一性
- ② 高性能
- ③ 单调递增
- ④ 信息安全
- ⑤ 高可用
2、全局唯一ID生成策略
- ① UUID
- ② Redis自增
- ③ snowflake算法
- ④ 数据库自增
- ⑤ Zookeeper的znode版本生成ID
二、Redis生成全局唯一ID
1、snowflake算法全局唯一ID策略
此处我们参考snowflake算法的ID策略
,其具体策略如下:
① 1位,固定位;
② 41位,用来记录时间戳(毫秒),接近69年;
③ 10位,用来记录工作机器id;
④ 12位,序列号,用来记录同毫秒内产生的不同id;
2、Redis自增全局唯一ID策略
自定义我们自己的Redis自增的ID策略
,具体如下:
① 1位,固定位;
② 31位,用来记录时间戳(秒),接近69年;
③ 32位,序列号,用来记录同一秒内产生的不同id;
三、代码实现
1、获取开始时间戳
LocalDateTime#of(int year, int month, int dayOfMonth, int hour, int minute, int second)
方法,传入自己需要的起始年月日时分秒;
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022,1,1,0,0,0);
long l = time.toEpochSecond(ZoneOffset.UTC);
System.out.println(l);
}
如:2022年1月1日 00点00分00秒,为1640995200L
。
2、编写ID生成工具类
@Component注解
,将工具类注册到Spring容器当中,方便使用;
BEGIN_TIMESTAMP
自定义起始的时间戳;
COUNT_BITS
位移量,后续拼接全局唯一ID时使用;
@Component
public class RedisIdWorker {
// 开始时间戳
private static final long BEGIN_TIMESTAMP = 1640995200L;
// 位移量
private static final long COUNT_BITS = 32L;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix){...}
}
timestamp
获取当前设置起始时间戳的偏移量;
拼接key
,例如:incr:order:20220325;
位运算拼接全局ID,timestamp << COUNT_BITS | count;
;
public long nextId(String keyPrefix){
//1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//2. 生成序列号
//2.1 获取当前日期,精确到天
String currentDate = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2 自增长
// 拼接的key,例如:incr:order:20220325
long count = stringRedisTemplate.opsForValue().increment(
RedisConstants.INCR_ID
+ keyPrefix
+ RedisConstants.SEPARATE
+ currentDate);
//3. 拼接并返回
return timestamp << COUNT_BITS | count;
}
3、测试生成全局唯一ID
注入RedisIdWorker对象,用于测试;
@Resource
private RedisIdWorker redisIdWorker;
编写测试方法:
① 使用线程池
模拟并发调用,此处简单处理;
private ExecutorService es = Executors.newFixedThreadPool(500);
② 使用CountDownLatch
控制线程执行;
CountDownLatch latch = new CountDownLatch(300); //设置300阈值
Runnable task = () -> {
...
latch.countDown();//每次循环结束,latch -1
};
... //开始时间
... //执行任务
latch.await();//等待直到 latch 变为 0
... //结束时间
完整的测试方法如下:
@Test
public 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);
}
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("time = " + (end - begin));
}
四、结尾
以上即为Redis全局唯一ID的基础内容