Redis实现全局唯一id

当我们在做秒杀活动时,每当有一个用户抢购,我们就要与之对应的生成一张订单并保存到数据库中,但是如果你使用数据库自增id时,会发现有一些问题。

  • 问题一:规律性过于明显。假如你早上买了一件商品-编号为1000,中午买了一件商品-编号为1500,晚上又买了一件商品-编号为2000,你会很容易发现这是有规律的,你甚至可以大胆推断这个商家总共卖了多少件商品,这显然不是卖家所希望看到的。

  • 随着我们的规模越来越大,单表的容量有一个最佳限制,如果数据量过大,我们肯定要进行拆表,那问题来了,他们逻辑上是一张表,所以id肯定需要具有唯一性,但这种方式我们并不能保证。
而全局id生成器能够帮助我们解决这个问题。

全局唯一id生成器需要满足以下特性:

  • 唯一性(redis独立于数据库,只有一个,无论谁访问,自增都是唯一的,所以满足)
  • 高可用(集群、主从、哨兵等方案可以保证,所以满足)
  • 高性能(redis本来就以性能著称,所以满足)
  • 递增性(redis本来就是采用递增方案,所以满足)
  • 安全性(而安全性需要我们自己来考虑如何应对,因为单单只靠redis自带的自增方案,并不能保证安全性)


    这里我们使用一个64位的long类型的数据作为我们的全局唯一id
    id组成部分:1位符号位 + 31位时间戳 + 32位序列号

  1. 首先,生成31位的时间戳
// 获取 2022年1月1日0时0分0秒的一个LocalDateTime实例
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
// 本地时间和偏移量结合
long second = time.toEpochSecond(ZoneOffset.UTC); 
System.out.println("second = " + second);

结果为:

second = 1640995200

并定义为一个静态常量:

private static final long BEGIN_TIMESTAMP = 1640995200L;

生成时间戳:

long timeStamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP;
  1. 生成序列号
// 2.1 获取当前日期,精确到天
// 这里yyyy:MM:dd 之所以使用冒号的更重要一个原因是可以更方便统计,因为redis可以通过冒号进行分级
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
 // 2.2 自增长
long count = stringRedisTemplate.opsForValue().increment("custom:" + keyPrefix + ":" + date);
  1. 进行拼接
return timeStamp << COUNT_BITS | count;
  1. 完整代码
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;

/**
 * @author ClownMing
 * Redis实现全局唯一id
 */
@Component
@SuppressWarnings({"all"})
public class RedisIdWorker {

    /**
     * 开始时间戳
     */
    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){ // 这个key 可以看做是业务key,不同的业务对应不同的key
        // 1. 生成时间戳
        long timeStamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP;
        // 2. 生成序列号
        // 2.1 获取当前日期,精确到天
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2 自增长
        // 这里即便没有对应的key,redis也会自动帮我们创建
        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);
    }
}
  1. 生成一定数量的id,进行测试
 @Test
    void testRedisIdWorker() throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(200);
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            downLatch.countDown();
        };
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 200; i++) {
            es.submit(task);
        }
        downLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("总耗时: " + (end - begin));
    }

Redis实现全局唯一id_第1张图片
因为是异步执行,所以编号并不总是有顺序的。

测试结果正确!

你可能感兴趣的:(redis,数据库,java)