Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

关于优惠秒杀问题的Redis实现章节总览

全局唯一ID 

场景分析 

不能用自增的原因

id的规律性太明显

受单表数据量的限制

全局唯一ID的条件

全局唯一ID的Redis实现

代码实现

单元测试 

其它全局唯一ID的生成策略 

秒杀下单 

场景分析 

优惠券秒杀的下单功能的实现

代码实现 

存在问题


Redis的优惠券秒杀问题(五)全局唯一ID 以及 秒杀下单

关于优惠秒杀问题的Redis实现章节总览

我们要讲述的问题大致如下所示,根据黑马程序员视频教程,会分离出Redis关于秒杀问题的核心知识点进行讲解!

  • 全局唯一ID
  • 实现优惠券秒杀下单  
  • 超卖问题  
  • 一人一单
  • 分布式锁  
  • Redis优化秒杀  
  • Redis消息队列实现异步秒杀

全局唯一ID 

场景分析 

首先,我们依照黑马的项目来进行分析,在什么情况下要使用到这个全局唯一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生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

全局唯一ID的Redis实现

为了增加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 

其它全局唯一ID的生成策略 

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

秒杀下单 

场景分析 

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

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%会发生超卖的情况!!!

posted @ 2022-11-16 17:10  金鳞踏雨  阅读(46)  评论(0编辑  收藏  举报  来源