1. 全局唯一 ID
1. 1 问题:针对优惠券秒杀模块全局唯一 ID 的作用是什么?
作为优惠券订单 id
1.2 问题:为什么不使用数据库自增 ID ?
考虑分布式场景下ID的全局唯一性
① 数据库自增 id:先从一个数据库表中获取自增id,再根据该 id 往对应的分库分表中写入
问题:生成id的数据库表高并发瓶颈
适用场景:并发不高,数据量太大导致的分库分表
② uuid:UUID.randomUUID()
问题:不适用于实际的业务需求,生成的订单号UUID字符串看不出与订单相关的有用信息。长字符串【存储性能差,查询耗时】
③ 获取系统当前时间:系统时间作为主键
问题:秒级并发时,会有重复情况
适用场景:业务字段与当前时间拼接,组成全局唯一编号
④ redis:redis 的 incr 实现 ID 原子性自增
问题:redis 持久化过程中出现宕机时,RDB持久化会出现重复 id 的情况,AOF 持久化会导致重启恢复数据时间过长 【问题:什么原因导致了两者的差异?】
⑤ 雪花算法:时间戳 + 机器 id + 序列号
问题:强依赖机器时钟
视频中采用 redis 生成全局唯一 id
1.3 如何利用 redis 生成全局唯一 id
① 业务名称前缀 + 当前日期 作为 redis 自增长参数 key
//2.2 自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
② 全局唯一id = 当前时间戳【高 32 位】+ redis 自增长序列【低 32 位】
//3.拼接并返回
private static final int COUNT_BITS = 32;
return timestamp << COUNT_BITS | count;
完整代码:
@Component
public class RedisIdWorker {
// 开始时间戳
private static final long BEGIN_TIMESTAMP = 1648857600L;
// 序列号的位数
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix){
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
// 时间戳 = 当前时间 - 开始时间
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//2.生成序列号
//2.1 获取当前日期
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2 自增长:redis 自增长key: 前缀 + 当前日期
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
实现效果:
实现效果分析:高 32 位为 时间戳,低 32 位为 redis 中精确到 天的记录。同一天的记录可以根据时间戳前缀唯一标识,同时在redis 中可以直观看到与相关业务逻辑以及日期相关的信息
2. 优惠券秒杀下单
流程说明:
① 判断秒杀是否开始或结束,若尚未开始或已经结束则无法下单
② 库存是否充足,不足则无法下单
2.1 问题:如何实现优惠券下单
① 扣减库存
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).update();
② 创建订单并保存到数据库
// 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);
2.2 如何添加优惠券?
使用 postman添加优惠券:如下图所示,在 2 号商铺中添加优惠券
2.3 添加的优惠券是如何保存到数据库的?
通过 postman 添加优惠券后
会将优惠券信息同时写入 tb_voucher 和 tb_seckill_voucher 表中
tb_voucher :记录优惠券的店铺,描述,面值等信息
tb_seckill_voucher :记录优惠券秒杀开始,结束时间以及库存
实现过程:
① 通过 postman 发送请求到 /voucher/seckill
请求被分发到 Controller 层的 addSeckillVoucher(@RequestBody Voucher voucher)
@RestController
@RequestMapping("/voucher")
public class VoucherController {
/**
* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
}
② Service 层 VoucherServiceImpl.java 中的 addSeckillVouocher(Voucher voucher) 实现保存秒杀券信息到数据库中
@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券
save(voucher);
// 保存秒杀信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
}
优惠券秒杀核心代码:
// VoucherOrderServiceImpl.java
@Override
@Transactional
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("库存不足!");
}
// 5.扣减库存
boolean success = seckillVoucherService.update()
.setSql(