Skip to content
DAILY QUOTE

“ ”

(1)数据库自增ID存在的问题

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显 场景分析一:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感业务信息,比如商城在一天时间内,卖出了多少单,这明显不合适。
  • 受单表数据量的限制 场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行分库分表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

(2)基于Redis自增器实现分布式全局ID

分布式ID的实现方式:

  1. UUID(生成16进制字符串,字符串id不利于数据库作为索引查询)
  2. Redis自增(自定义的方式实现:时间戳+序列号+数据库自增
  3. 数据库自增(单独维护一个全局id表,作为多张表的全局id)
  4. 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));

}