前言
用Redis完成登录界面和店铺查询功能后,来看看与优惠卷相关的操作。其中包括新增优惠券,查询店铺的优惠券列表,优惠券秒杀下单,前面的功能都是简单的数据库查询,这里主要讲优惠券秒杀下单功能。
先来看看优惠卷分为普通优惠卷和秒杀优惠卷,以及用户订单的表结构:
普通优惠券:
秒杀优惠券:
用户订单:
普通优惠卷好说,没有开始和结束时间,用户什么时候都可以抢购,抢购过后保存在用户订单中,而秒杀优惠券是在普通优惠券的基础上,加上了有效期限,用户不是所有的时间都能获得,所以我们主要讲讲秒杀券的抢购操作,(其实是普通券抢购功能的实现视频里没有讲🤪)。
再来看看具体的业务流程
在做秒杀优惠券这种很多人同时抢的项目时,有很多要注意的地方,当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:
- 自增ID太过简单,单一不安全。(如果有攻击者,可以通过分析ID规律来推断出其他用户的ID,从而进行未授权的访问或操纵。)
- 受表单数据量限制。(数据库自增的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_TIMESTAMP ,BEGIN_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 自增生成序列号
- 主要是Java方法:stringRedisTemplate.opsForValue().increment(“key”) 的理解
- 功能:原子地将 key 的值 +1,并返回新值,这是 Spring Data Redis 对 Redis 原生命令 INCR 的封装。
- 示例演示:
stringRedisTemplate.opsForValue().set("counter", "10"); // 先设为10
Long val1 = stringRedisTemplate.opsForValue().increment("counter"); // INCR counter
Long val2 = stringRedisTemplate.opsForValue().increment("counter");
- 对应 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);
本文是学习黑马程序员—黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)
…。。。。。。。。。。。…
…。。。。。。。。。。。…