Redis的优惠券秒杀问题(五)全局唯一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%会发生超卖的情况!!!