【Redis-全局唯一ID:snowflake算法,Redis自增】

时间:2021-09-19 00:51:42

背景

新公司用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;

【Redis-全局唯一ID:snowflake算法,Redis自增】


2、Redis自增全局唯一ID策略

自定义我们自己的​​Redis自增的ID策略​​,具体如下:

① 1位,固定位;
② 31位,用来记录时间戳(秒),接近69年;
③ 32位,序列号,用来记录同一秒内产生的不同id;

【Redis-全局唯一ID:snowflake算法,Redis自增】


三、代码实现

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的基础内容