Redis实现全局唯一id

410 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

1.为什么需要redis来实现全局唯一id

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

  1. 唯一性。唯一性自不必多讲,id通常需要是唯一的,用来区别不同的记录。如果用数据库自动递增的话,后期数据一多,需要分表,可能出现重复id。
  2. 高可用。需要确保任何时候都能生成id或者订单号(生成的id也可以用做订单号)。
  3. 高性能。生成的id速度要快,你不能说生成一个id要等它几秒。
  4. 递增性。要确保整体逐渐变大,有利于数据库创建索引,提高查找速度。
  5. 安全性。数据库自动递增确实简单,但是不够安全。比如今天你的订单号是1,明天你的订单号是10,那么别人就知道该商城相关订单有多少,泄露信息。

为了增加ID的安全性,我们不可以直接使用Redis自增的数值,而是拼接一些其它信息:下图来自黑马程序员redis相关课程。

  1. 从下图我们可以看到有64个比特位。
  2. 第一个比特位是0,代表我们的id永远是正数。
  3. 31比特位作为时间戳。作用是增加id复杂性,不是单纯redis递增。这个时间戳以秒为单位,所以需要31比特位。比如:我们从2020年1月1号开始,算下自1970-01-01T00:00:00Z以来的秒数,然后算下当前下单时间自1970-01-01T00:00:00Z以来的秒数,接着用当前的秒数-2020年1月1号的秒数,最后将这个值作为时间戳。31位算下来,这个秒数可以用69年。
  4. 32比特位是redis自增的值。

image.png

2.redis实现全局唯一id代码

  1. 开始时间戳的值就是下面man方法生成的,生成后这个main方法不需要使用,所以注释掉了
  2. 看完前面的介绍,我们知道这个全局唯一id就是符号位+时间戳+redis自增实现的。根据分析,我们是用redis自增长的方式来获取这部分数据,用java代码就是stringRedisTemplate.opsForValue().increment(key);这里解释下为什么我们代码里key定义成这样"icr:" + keyPrefix + ":" + date。原因是你的key最好有前缀,所以加了"icr:" + keyPrefix + ":",后面的date是当天时间(精确到年月日)。这么做的好处是每天的key都是不一样的,而且定义成这样,还方便我们统计,比如统计2月份多少订单等。之所以要每天用不同的key还有别的原因,那就是key的自增其实是有上线的,所以最好每天都是不同的key。
  3. 拼接的时候,使用了位运算。其实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));
}

image.png

image.png