(1)数据库自增ID存在的问题
当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:
- id的规律性太明显 场景分析一:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感业务信息,比如商城在一天时间内,卖出了多少单,这明显不合适。
- 受单表数据量的限制 场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行分库分表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性: 
(2)基于Redis自增器实现分布式全局ID
分布式ID的实现方式:
- UUID(生成16进制字符串,字符串id不利于数据库作为索引查询)
- Redis自增(自定义的方式实现:时间戳+序列号+数据库自增)
- 数据库自增(单独维护一个全局id表,作为多张表的全局id)
- snowflake算法(雪花算法)
这里我们基于Redis的自增器实现生成分布式ID,为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是在Redis自增器基础上拼接一些其它信息:

我们本次使用数值型id,也就是Java中的long类型,共64位
ID组成:
- 符号位:1bit,永远位0
- 时间戳(id生成时间 - 初始时间的秒数):31bit,以秒为单位,大概可以支持使用69年(2^31/3600/24/365≈69)
- 序列号(Redis自增后的value):32bit,秒内的计数器,支持每秒产生2^32个不同ID
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息,比如时间戳、UUID、业务关键词。
Redis负责生成序列号count,保证同一个Redis key下,自增数字不重复,同一时间戳下来了多个请求,通过Redis自增序号保证不同。
Java负责生成时间戳,让ID带有时间信息,把时间戳和Redis的count拼成一个long类型ID,每天Redis序号都从1开始,但最终id不能相同,所以需要时间戳。
例如,计算下单时间戳,利用下单时间 - 初始时间的时间差秒数作为时间戳。首先我们需要生成一个初始时间:
JAVA
/**
* 执行示例测试。
*
* 作用:
* 1.将时间戳转化为秒数;
*
* @param args args参数
* @return无返回值
*/
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);
}- RedisIdWorker
JAVA
private static final long BEGIN_TIMESTAMP=1640995200L; //固定起始时间戳,大概对应2022-01-01 00:00:00
private static final long COUNT_BITS=32;
private StringRedisTemplate stringRedisTemplate;
/**
* 执行RedisIdWorker相关业务逻辑。
*
* @param stringRedisTemplate stringRedisTemplate参数
* @return处理结果
*/
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 注意:单个key我们需要保证key不相同,即使是同一个业务内的key也不能相同。
* Redis中对同一个key做自增,value最多到2^64,而我们的全局唯一ID策略里,真正记录序列号的位数最多只有32bit
* 因此我们在业务key前缀后面拼接一个当天日期,这样设计保证每天都自增的是一个新key的id,而一天的下单量不可能超过2^32个
*/
public long nextId(String keyPrefix){ //long类型为64位
//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.拼接并返回
//左移32位,拼接count,低32位全是0,对count进行或运算,得到的结果与count相同
return timestamp<< COUNT_BITS |count;
}- 测试生成分布式ID效果
java
@Resource
private RedisIdWorker redisIdWorke;
private ExecutorService es= Executors.newFixedThreadPool(500);
/**
* 执行testIdWorker相关业务逻辑。
*
* @return无返回值
*/
@Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch=new CountDownLatch(300);
Runnable task=()->{
for (int i = 0; i < 100; i++) {
long id=redisIdWorke.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));
}