黑马点评学习笔记08(分布式ID生成器)生成订单ID,优惠券秒杀下单

黑马点评学习笔记08(分布式ID生成器)生成订单ID,优惠券秒杀下单

前言

用Redis完成登录界面和店铺查询功能后,来看看与优惠卷相关的操作。其中包括新增优惠券,查询店铺的优惠券列表,优惠券秒杀下单,前面的功能都是简单的数据库查询,这里主要讲优惠券秒杀下单功能。

先来看看优惠卷分为普通优惠卷和秒杀优惠卷,以及用户订单的表结构:

普通优惠券:

秒杀优惠券:

用户订单:

普通优惠卷好说,没有开始和结束时间,用户什么时候都可以抢购,抢购过后保存在用户订单中,而秒杀优惠券是在普通优惠券的基础上,加上了有效期限,用户不是所有的时间都能获得,所以我们主要讲讲秒杀券的抢购操作,(其实是普通券抢购功能的实现视频里没有讲🤪)。

再来看看具体的业务流程

在做秒杀优惠券这种很多人同时抢的项目时,有很多要注意的地方,当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  1. 自增ID太过简单,单一不安全。(如果有攻击者,可以通过分析ID规律来推断出其他用户的ID,从而进行未授权的访问或操纵。)
  2. 受表单数据量限制。(数据库自增的ID是有数量限制的,当用户过多时,数据库便不会再存储了,就要分表)

因此用户的订单ID是需要满足一定特点的,我们需要使用分布式ID(全局唯一ID):

常见全局ID生成方案对比:

这里我们使用自定义的方式实现:时间戳+序列号+数据库自增

来看看用分布式ID生成器生成用户订单ID的代码:
package ***.hmdp.utils;

import lombok.val;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.***ponent;

import javax.annotation.Resource;
import java.time.LocalDateTime;

@***ponent
public class RedisIdWorker {
    // 起始时间戳
    private static final long BEGIN_TIMESTAMP = 1735689600L;
    // 序列号位数
    private static final long COUNT_BITS = 32;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 生成唯一id(全局ID生成器)
     * @param keyPrefix
     * @return
     */
    public long nextId(String keyPrefix) {
        //1. 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(java.time.ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        //2. 生成序列号
        //2.1 获取当前日期
        String date = now.format(java.time.format.DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2 自增
        Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        //3. 拼接并返回
        return timestamp << COUNT_BITS | increment;
        
    }

    public static void main(String[] args) {
        //1.设置初始时间
        LocalDateTime time= LocalDateTime.of(2025, 1, 1, 0, 0, 0);
        long second = time.toEpochSecond(java.time.ZoneOffset.UTC);
        System.out.println("second = " + second);
    }

}

步骤 1️⃣:生成时间戳(相对于自定义起始时间)

1. main方法:计算起始时间戳:

public static void main(String[] args) {
    LocalDateTime time = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
    long second = time.toEpochSecond(ZoneOffset.UTC);
    System.out.println("second = " + second); // 输出 1735689600
}

将运行结果 1735689600赋值给 BEGIN_TIMESTAMPBEGIN_TIMESTAMP是自定义的“纪元时间”(epoch),(这里是 2025-01-01 00:00:00 UTC)。这样做是为了让生成的时间戳更紧凑(避免使用 Unix 时间戳从 1970 年开始的巨大数值)。
COUNT_BITS = 32:表示用 低 32 位 存储序列号(sequence number),高 32 位存储时间戳。

2. 常量定义

// 常量定义
private static final long BEGIN_TIMESTAMP = 1735689600L; // 起始时间戳
private static final long COUNT_BITS = 32;               // 序列号位数

3. 计算当前时间戳

LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
  • 获取当前 UTC 时间,并转为秒级时间戳。
  • 减去 BEGIN_TIMESTAMP,得到“相对秒数”。
    • 例如:如果现在是 2025-01-01 00:00:01,则 timestamp = 1。

**⚠️ 注意:**这里用的是 秒,不是毫秒!这和 Twitter Snowflake(用毫秒)不同,意味着 每秒最多生成 2^32 个 ID(约 42 亿),理论上足够,但要注意精度

步骤 2️⃣:生成每日递增的序列号(利用 Redis)
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

为当前业务(keyPrefix)在当天生成一个唯一的、递增的序列号(从1开始),并利用 Redis 保证这个序列号在分布式环境下也是唯一的。

  • 第一行代码:格式化当前日期

    • now 是当前时间,比如:今天是 2025年11月7日,那么:date = “2025:11:07”
  • 第二行代码:Redis 自增生成序列号

    1. 主要是Java方法:stringRedisTemplate.opsForValue().increment(“key”) 的理解
    2. 功能:原子地将 key 的值 +1,并返回新值,这是 Spring Data Redis 对 Redis 原生命令 INCR 的封装。
    3. 示例演示:
stringRedisTemplate.opsForValue().set("counter", "10"); // 先设为10
Long val1 = stringRedisTemplate.opsForValue().increment("counter"); // INCR counter
Long val2 = stringRedisTemplate.opsForValue().increment("counter");
  1. 对应 Redis 操作:
SET counter "10"
INCR counter  # 返回 11
INCR counter  # 返回 12

最终:

val1 = 11L
val2 = 12L
Redis 中 counter 的值是字符串 “12”

步骤 3️⃣:拼接时间戳和序列号
return timestamp << COUNT_BITS | increment;
  • 将 timestamp 左移 32 位,腾出低 32 位给 increment。
  • 用位或 | 合并两者。

例如:

  • timestamp = 100 → 二进制左移 32 位
  • increment = 5
  • 最终 ID = (100 << 32) + 5

这样生成的 ID 是 单调递增、全局唯一、包含时间信息 的

🧪 举个完整例子:

场景:

  • 当前时间:2025-01-01 00:00:01(UTC)
  • 你要生成一个 订单 ID
  • 这是今天第 3 次 调用 nextId(“order”)

执行过程:

  • 起始时间戳:

    • BEGIN_TIMESTAMP = 1735689600L(对应 2025-01-01 00:00:00 UTC)
    • nowSecond = 1735689601(当前时间:2025-01-01 00:00:01(UTC))
    • timestamp = nowSecond - BEGIN_TIMESTAMP = 1
  • 序列号:

    • date = “2025:01:01”
    • Redis key = “icr:order:2025:01:01”
    • 假设之前已经调用过两次,Redis 中该 key 的值是 2
    • 执行 increment:
      • Redis 将其变为 3
      • Java 变量 increment = 3L
  • 拼接时间戳与序列号:

    • timestamp = 1
    • COUNT_BITS = 32
    • 1 << 32 = 4294967296(即 2³²)
    • increment = 3
ID = (1 << 32) | 3 
   = 4294967296 + 3 
   = 4294967299

💡二进制视角:

时间戳(高32位): 00000000 00000000 00000000 00000001
序列号(低32位): 00000000 00000000 00000000 00000011
合并后(64位)  : 00000000 00000000 00000000 00000001 00000000 00000000 00000000 00000011

完成用户订单ID的生成后,来看看优惠券秒杀下单功能是怎么实现的?

 /*
    查询领取秒杀券
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        //2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }

        //3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已结束");
        }

        //4.判断库存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("库存不足");
        }
        
   //6.扣减库存
            boolean su***ess = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>()
                .eq(SeckillVoucher::getVoucherId, voucherId)
                .setSql("stock = stock -1"));

            if (!su***ess) {
                //扣减库存失败
                return Result.fail("库存不足");
            }

            //7.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            //7.1.订单ID
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            //7.2.优惠券ID
            voucherOrder.setVoucherId(voucherId);
            //7.3.用户ID
            voucherOrder.setUserId(userId);

            //写入数据库
            save(voucherOrder);

            //6.返回订单ID
            return Result.ok(orderId);

本文是学习黑马程序员—黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)

…。。。。。。。。。。。…

…。。。。。。。。。。。…

转载请说明出处内容投诉
CSS教程网 » 黑马点评学习笔记08(分布式ID生成器)生成订单ID,优惠券秒杀下单

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买