优惠券秒杀
1
在各类购物App中,都会遇到商家发放的优惠券
当用户抢购商品时,生成的订单会保存到tb_voucher_order表中,而订单表如果使用数据库自增ID就会存在一些问题
id规律性太明显
受单表数据量的限制
如果我们的订单id有太明显的规律,那么对于用户或者竞争对手,就很容易猜测出我们的一些敏感信息,例如商城一天之内能卖出多少单,这明显不合适
随着我们商城的规模越来越大,MySQL的单表容量不宜超过500W,数据量过大之后,我们就要进行拆库拆表,拆分表了之后,他们从逻辑上讲,是同一张表,所以他们的id不能重复,于是乎我们就要保证id的唯一性
那么这就引出我们的全局ID生成器了
全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足一下特性
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息
ID组成部分
符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年(2^31秒约等于69年)
序列号:32bit,秒内的计数器,支持每秒传输2^32个不同ID
那我们就根据我们分析的ID生成策略,来编写代码
业务编写
步骤一:全局唯一ID生成器
点击查看代码
package com.hmdp.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Component
public class RedisIdWorker {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//设置起始时间,我这里设定的是2022.01.01 00:00:00
public static final Long BEGIN_TIMESTAMP = 1640995200L;
//序列号长度
public static final Long COUNT_BIT = 32L;
//Redis唯一ID生成策略
public long nextId(String keyPrefix){
//1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long currentSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = currentSecond - BEGIN_TIMESTAMP;
//2. 生成序列号---------------------同一个业务加入date也就是说明一天一个key
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);//key不存在会自动创建一个
//3. 拼接并返回,简单位运算
return timeStamp << COUNT_BIT | count;
}
}
点击查看代码
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
@Resource
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}
点击查看代码
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.秒杀是否开启
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
//已经结束
return Result.fail("秒杀已经结束");
}
//3.库存
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足");
}
//4.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.update();
if (!success) {
//扣减失败
return Result.fail("库存不足");
}
//5.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setUserId(orderId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//6.返回订单id
return Result.ok(orderId);
}
超卖问题
限制一人一单
但这会涉及多个问题:乐观锁,悲观锁
涉及一个Spring的锁
一个用户一个锁加上之后,如果redis放入集群,这个锁会失败,两个单独的不会共享这一把锁导致还是重复订单


浙公网安备 33010602011771号