Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单
我们要讲述的问题大致如下所示,根据黑马程序员视频教程,会分离出Redis关于秒杀问题的核心知识点进行讲解!
首先,我们依照黑马的项目来进行分析,在什么情况下要使用到这个全局唯一ID。
在黑马点评这个项目中,使用的商品其实也就是优惠券

当用户抢购时,就会生成订单并保存到 tb_voucher_order 这张表中
- CREATE TABLE `tb_voucher_order` (
- `id` bigint NOT NULL COMMENT '主键',
- `user_id` bigint UNSIGNED NOT NULL COMMENT '下单的用户id',
- `voucher_id` bigint UNSIGNED NOT NULL COMMENT '购买的代金券id',
- `pay_type` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
- `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
- `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
- `pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
- `use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
- `refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
但是,这张SQL表里面的主键id,是不可以使用自增的!!!
如果使用自增的话,用户可以根据两笔订单的ID,来判断这段时间内订单的量。
订单的数据量一般很大,一天可能会有几百万,如果使用自增ID,就很难分库分表了!
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息

符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
RedisIdWorker-Redis全局ID生成器工具类
- /**
- * Redis的全局ID生成器
- */
- @Component
- public class RedisIdWorker {
-
- /**
- * 开始时间戳
- * 2022.1.1的时间戳
- */
- private static final long BEGIN_TIMESTAMP = 1640995200L;
-
- /**
- * 序列号的位数
- */
- private static final int COUNT_BITS = 32;
-
- private StringRedisTemplate stringRedisTemplate;
-
- // 构造器注入
- public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
- this.stringRedisTemplate = stringRedisTemplate;
- }
-
- /**
- * 生成全局ID
- * Long 类型 8个字节 64个bit
- * 符号位(1bit) + 时间戳(31bit) + 序列号(32bit)
- * @param keyPrefix
- * @return
- */
- public long nextId(String keyPrefix) {
- // 1.生成时间戳
- LocalDateTime now = LocalDateTime.now();
- long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
- // ID的时间戳
- long timestamp = nowSecond - BEGIN_TIMESTAMP;
-
- // 2.生成序列号
- // 2.1.获取当前日期,精确到天
- String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
-
- // 2.2.自增长
- // icr表示自增长,keyPrefix表示业务类型,一天一个key
- // 例如: icr:order:2022:11:16
- long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
-
- // 3.拼接并返回 (位运算)
- return timestamp << COUNT_BITS | count;
- }
- }
位运算实现字符串拼接
timestamp << COUNT_BITS | count
将 timestamp 向左移动32位,在与 count 做“或”运算(一个为真就为真!)
我们可以看看并发情况下,该工具类的性能怎么样
- @Resource
- private RedisIdWorker redisIdWorker;
-
- private ExecutorService es = Executors.newFixedThreadPool(500);
-
- @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 start = System.currentTimeMillis();
- for (int i = 0; i < 300; i++) {
- es.submit(task);
- }
- latch.await();
- long end = System.currentTimeMillis();
-
- System.out.println(end - start);
- }
运行结果如下

生成3万个订单ID

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购

tb_voucher:优惠券的基本信息,优惠金额、使用规则等(普通券+秒杀券)
- CREATE TABLE `tb_voucher` (
- `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
- `shop_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '商铺id',
- `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代金券标题',
- `sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '副标题',
- `rules` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '使用规则',
- `pay_value` bigint UNSIGNED NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',
- `actual_value` bigint NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',
- `type` tinyint UNSIGNED NOT NULL DEFAULT 0 COMMENT '0,普通券;1,秒杀券',
- `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '1,上架; 2,下架; 3,过期',
- `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息(秒杀券拓展字段)
- CREATE TABLE `tb_seckill_voucher` (
- `voucher_id` bigint UNSIGNED NOT NULL COMMENT '关联的优惠券的id',
- `stock` int NOT NULL COMMENT '库存',
- `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
- `begin_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '生效时间',
- `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '失效时间',
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
- PRIMARY KEY (`voucher_id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀优惠券表,与优惠券是一对一关系' ROW_FORMAT = COMPACT;

- 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
- 库存是否充足,不足则无法下单

- @Resource
- private ISeckillVoucherService seckillVoucherService;
-
- @Resource
- private RedisIdWorker redisIdWorker;
-
- @Override
- @Transactional
- public Result seckillVoucher(Long voucherId) {
-
- // 1. 查询优惠券
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- LocalDateTime nowTime = LocalDateTime.now();
-
- // 2. 判断秒杀是否开始
- if (nowTime.isBefore(voucher.getBeginTime())) {
- return Result.fail("活动未开始!");
- }
-
- // 3. 判断秒杀是否结束
- if (nowTime.isAfter(voucher.getEndTime())) {
- return Result.fail("活动已结束!");
- }
-
- // 4. 判断库存
- if (voucher.getStock() < 1) {
- return Result.fail("已买完!");
- }
-
- // 5. 减库存
- boolean success = seckillVoucherService.update()
- .setSql("stock = stock - 1")
- .eq("voucher_id", voucherId).update();
-
- if (!success) {
- return Result.fail("库存不足");
- }
-
- // 6. 创建订单
- VoucherOrder voucherOrder = new VoucherOrder();
-
- // 6.1 订单id
- long orderId = redisIdWorker.nextId("order");
- voucherOrder.setId(orderId);
- // 6.2 用户id
- Long userId = UserHolder.getUser().getId();
- voucherOrder.setUserId(userId);
- // 6.3代金券id
- voucherOrder.setVoucherId(voucherId);
- save(voucherOrder);
-
- // 7. 返回订单
- return Result.ok(orderId);
- }
上述代码是写好了,运行起来看起来页没有什么问题,但是在多线程,高并发的场景下就会出现大问题,100%会发生超卖的情况!!!