一.什么是全局ID
当用户抢购时,就会生成订单并保存到DB中,而订单表如果使用数据库自增ID就存在一些问题:
- id的规律性太明显
- 受单表数据量的限制
场景分析:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。
场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。
二.全局ID的构成
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
ID的组成部分:符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
三.全局ID的工具类
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
// 全局唯一ID
@Component
public class RedisIdWorker {
/**
* 开始时间戳
* LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0); 2022-01-01T00:00
* long nowSecond = time.toEpochSecond(ZoneOffset.UTC); 1640995200
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
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.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回 timestamp向左移动32位
return timestamp << COUNT_BITS | count;
}
}
四.测试
@Resource
private RedisIdWorker redisIdWorker;
@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));
}
五.补充 Countdownlatch
5.1 Countdownlatch是什么?
CountDownLatch是一个同步工具类,在java.util.cucurrent包中,是JUC编程中较为常用的一个工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。
它的的实现简单来说是通过一个计数器,初始化的时候给计时器一个指定值,然后在子线程中当执行完规定的逻辑后,计数器会进行减1操作,当计数器为0时,那么在阻塞等待的线程则会被唤醒恢复执行。
5.2 方法详解
- CountDownLatch(int count)
有参构造方法,构造一个以给定计数 CountDownLatch CountDownLatch。
count为计数器的初始值(一般需要多少个线程执行,count就设为几)。
- countDown()
减少锁存器的计数,如果计数达到零,释放所有等待的线程。
如果当前计数大于零,则它将递减。 如果新计数为零,则所有等待的线程都将被重新启用以进行线程调度。
如果当前计数等于零,那么没有任何反应。
- getCount()
返回当前计数。
- await()
等待计数器变为0,即等待所有异步线程执行完毕
- await(long timeout, TimeUnit unit)
①此方法至多会等待指定的时间,超时后会自动唤醒,若 timeout 小于等于零,则不会等待
②boolean 类型返回值:若计数器变为零了,则返回 true;若指定的等待时间过去了,则返回 false
5.3 应用场景
1. 某个线程需要在其他n个线程执行完毕后再向下执行
2. 多个线程并行执行同一个任务,提高响应速度