一、优惠券秒杀:全局唯一ID
每个店铺都可以发布优惠券;
当用户抢购,就会生成订单并保存在tb_voucher_order这张表中,订单表如果使用数据库自增id会让
1、id的规律性太明显;
2、受单表数据量的限制;
全局id生成器:全局唯一id。利用redis的incre自增属性
为了增加redis的安全性,也可以不直接使用redis自增的数值,而是拼接一下其他信息
public long generateOnlyId(String prefix){
// 1.生成时间戳(返回long,可以利用当前时间戳-开始时间戳,得到一个插值)觉得也可以利用当前时间戳
long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIME_STAMP;
// 2、生成序列号(利用当前时间-精确到天生成,可以方便以后统计按时间日期统计,而且一天的订单量往往不会超过2的32次方)
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 这里的number永远不会为null值
long number = redisTemplate.opsForValue().increment("icr:" + prefix + ":" + date);
// 3、拼接 时间戳 + 序列号 (利用位运算)1的时间戳往左移动32位,然后用 | 运算,因为 | 运算只要有一个为真返回真;
// 1的时间戳往左移动32位之后,后面32都填充为0,0 | date 返回为date;
return timeStamp << BIT_NUMBER | number;
}
二、实现优惠券的秒杀下单
三、库存超卖问题
高并发情况,多个线程操作同一个优惠券,在线程A执行更新操作的过程中,线程B、C、D对查询优惠券,并同样进行的扣减库存的操作;导致商品超卖;
解决方法:
因为需要一个个的操作,所以悲观锁的性能较差。大部分是采用乐观锁。
乐观锁的方法:
- 版本号法:利用表中的版本字段version,只要有一个线程对其进行操作,version + 1;其他线程会去查询version,对比version是否发生变化,如果version改变了,不让其他线程进行修改操作;
2.CAS法:Compare and Switch:比较修改。在版本号的基础上,既然用version字段前后可以比较得出这条数据是否发生变化,那同样,直接用stock库存本身来比较,stock前后是否发生了变化;
乐观锁的缺点: 成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
四、优惠券秒杀:一人一单
不对用户限制,最后造成优惠券被同一个人全部买走的情况;
/**
* 单体项目下,悲观锁锁用户id 实现一人一单;
* 集群模式下无效
*/
@Transactional
public Result createVoucherOrder(Long userId, Long voucherId) {
// 5、扣减库存
// 5.1 实现一人一单功能 查询当前用户id和优惠券id是否存在
// todo 在高并发的情况下,100个线程同时查,查询到的都是0,所以依然存在同一个用户抢了多个优惠券的情况;
// 由于是插入操作,无法比较,所以最好加悲观锁
// 要保证锁的是一个对象,userId.toString()每次会是一个全新的对象,
// .intern()返回字符串的规范表示,去常量池寻找是否有这个值,确保用户id一样,值就一样
synchronized (userId.toString().intern()) {
int count = iSeckillVoucherService
.query()
.eq("user_id", userId)
.eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("你已下过单");
}
boolean sucess = iSeckillVoucherService.update()
// CAS 法 Compare and Switch**:比较修改。在版本号的基础上,
// 既然用version字段前后可以比较得出这条数据是否发生变化,那同样,
// 直接用stock库存本身来比较,stock前后是否发生了变化;
.setSql("stock = stock -1") // set stock = stock -1
.eq("voucher_id", voucherId).gt("stock", 0)
// 乐观锁的缺点:** 成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,
// 其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
// 所以这里这样判断stock > 0即可
.update();
if (!sucess) {
return Result.fail("库存不足");
}
// 6、将数据存入优惠券订单表
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(redisIdWorker.generateOnlyId("order"));
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(voucherOrder.getId());
}
}