开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情
1.为什么需要redis来实现全局唯一id
全局id生成器,是一种在分布式系统下用来生成全局唯一id的工具,一般需要满足下列特性:
- 唯一性。唯一性自不必多讲,id通常需要是唯一的,用来区别不同的记录。如果用数据库自动递增的话,后期数据一多,需要分表,可能出现重复id。
- 高可用。需要确保任何时候都能生成id或者订单号(生成的id也可以用做订单号)。
- 高性能。生成的id速度要快,你不能说生成一个id要等它几秒。
- 递增性。要确保整体逐渐变大,有利于数据库创建索引,提高查找速度。
- 安全性。数据库自动递增确实简单,但是不够安全。比如今天你的订单号是1,明天你的订单号是10,那么别人就知道该商城相关订单有多少,泄露信息。
为了增加ID的安全性,我们不可以直接使用Redis自增的数值,而是拼接一些其它信息:下图来自黑马程序员redis相关课程。
- 从下图我们可以看到有64个比特位。
- 第一个比特位是0,代表我们的id永远是正数。
- 31比特位作为时间戳。作用是增加id复杂性,不是单纯redis递增。这个时间戳以秒为单位,所以需要31比特位。比如:我们从2020年1月1号开始,算下自1970-01-01T00:00:00Z以来的秒数,然后算下当前下单时间自1970-01-01T00:00:00Z以来的秒数,接着用当前的秒数-2020年1月1号的秒数,最后将这个值作为时间戳。31位算下来,这个秒数可以用69年。
- 32比特位是redis自增的值。
2.redis实现全局唯一id代码
- 开始时间戳的值就是下面man方法生成的,生成后这个main方法不需要使用,所以注释掉了
- 看完前面的介绍,我们知道这个全局唯一id就是符号位+时间戳+redis自增实现的。根据分析,我们是用redis自增长的方式来获取这部分数据,用java代码就是stringRedisTemplate.opsForValue().increment(key);这里解释下为什么我们代码里key定义成这样"icr:" + keyPrefix + ":" + date。原因是你的key最好有前缀,所以加了"icr:" + keyPrefix + ":",后面的date是当天时间(精确到年月日)。这么做的好处是每天的key都是不一样的,而且定义成这样,还方便我们统计,比如统计2月份多少订单等。之所以要每天用不同的key还有别的原因,那就是key的自增其实是有上线的,所以最好每天都是不同的key。
- 拼接的时候,使用了位运算。其实redis生成全局id的代码直接复制粘贴即可。详情大家可以去搜黑马的redis课程。
@Component
public class RedisIdWorker {
//开始时间戳
private static final long BEGIN_TIMESTAMP = 1640995200L;
//序列号的位数
private static final int COUNT_BITS = 32;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
*
* @param keyPrefix 用前缀区分不同业务
* @return
*/
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 date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2 redis设置key,并返回自增长的值
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
return timestamp << COUNT_BITS | count;
}
/*public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022,1,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("time = " + (end-begin));
}