黑马点评–优惠卷秒杀
全局ID生成器:
是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
Redis自增ID策略:
- 每天一个key,方便统计订单量
- iD结构是时间戳+计数器
private static final long BEGIN_TIMESTAMP = 1640995200;
private static final int COUNT_BITS = 32;
@Resource
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix) {
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
return timestamp << COUNT_BITS | count;
}
public static void main(String[] args) {
LocalDateTime time &#61; LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long second &#61; time.toEpochSecond(ZoneOffset.UTC);
System.out.println(second);
}
测试ID自增生成策略&#xff1a;
500个线程每一个线程生成100id一共50000个id花的时间&#xff1a;
private ExecutorService service &#61; Executors.newFixedThreadPool(500);
&#64;Test
void testIdWorker() throws InterruptedException {
CountDownLatch latch &#61;new CountDownLatch(500);
Runnable task &#61; ()->{
for (int i&#61;0;i<100;i&#43;&#43;){
long id&#61; redisIdWorker.nextId("order");
System.out.println("id&#61;" &#43;id);
}
latch.countDown();
};
long start&#61;System.currentTimeMillis();
for (int i&#61;0;i<500;i&#43;&#43;){
service.submit(task);
}
latch.await();
long end&#61;System.currentTimeMillis();
System.out.println("end&#61;" &#43;(end-start));
}
每个店铺都可以发布优惠卷&#xff1a;
当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就会存在一些问题&#xff1a;
全局唯一ID生成策略&#xff1a;
- UUID
- Redis自增
- sonwflake算法
- 数据库自增
实现优惠券秒杀下单
每个店铺都可以发布优惠券&#xff0c;分为平价卷和特价卷。平价劵可以任意购买&#xff0c;而特价劵需要秒杀抢购&#xff1a;
表关系如下&#xff1a;
- tb_voucher:优惠劵的基本信息&#xff0c;优惠金额&#xff0c;使用规则等
- tb_seckill_voucher:优惠劵的库存&#xff0c;开始抢购时间&#xff0c;结束抢购时间。特价优惠券才需要填写这些信息
在VoucherController中提供一个接口&#xff0c;可以添加秒杀优惠券&#xff1a;http://localhost:8081/voucher/seckill
{
"shopId":1,
"title":"100元代金券",
"subTitle":"周一至周五均可使用",
"rules": "全场通用\\n无需预约\\n可无限叠加\\不兑换、不找零\\n仅限堂食",
"payValue": 8000,
"actualValue": 10000,
"type": 1,
"stock": 100,
"beginTime": "2022-10-25T12:09:04",
"endTime": "2022-12-25T12:09:04"
}
可以通过postman调用
实现秒杀下单&#xff1a;
下单时需要判断两点&#xff1a;
- 秒杀是否开始或结束&#xff0c;如果尚未开始或已经结束无法下单
- 库存是否充足&#xff0c;不足则无法下单
库存超卖问题分析&#xff1a;
超卖问题是典型的多线程安全问题&#xff0c;针对这一问题的常见解决方案就是加锁&#xff1a;
乐观锁&#xff1a;
乐观锁解决超卖问题&#xff1a;
SeckillVoucher voucher &#61; iSeckillVoucherService.getById(voucherId);
LocalDateTime beginTime &#61; voucher.getBeginTime();
if (beginTime.isAfter(LocalDateTime.now())) {
return Result.fail("活动尚未开始");
}
LocalDateTime endTime &#61; voucher.getEndTime();
if (LocalDateTime.now().isAfter(endTime)) {
return Result.fail("活动已经结束");
}
if (voucher.getStock() < 1) {
return Result.fail("库存不足!");
}
boolean success &#61;iSeckillVoucherService.update()
.setSql("stock &#61;stock -1")
.eq("voucher_id",voucherId).gt("stock",0).
update();
if (!success) {
return Result.fail("库存不足!");
}
VoucherOrder voucherOrder &#61; new VoucherOrder();
long orderId &#61; redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
Long userId &#61; UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
超卖这样的线程安全问题&#xff0c;解决方案有哪些&#xff1f;
1.悲观锁&#xff1a;添加同步锁&#xff0c;让线程串行执行
- 优点&#xff1a;简单粗暴
- 缺点&#xff1a;性能一般
2.乐观锁&#xff1a;不加锁&#xff0c;在更新时判读是否有其它线程在修改
一人一单&#xff1a;
需求&#xff1a;修改秒杀业务&#xff0c;要求同一个优惠券&#xff0c;一个用户只能下一单
&#64;Autowired
private ISeckillVoucherService iSeckillVoucherService;
&#64;Autowired
private RedisIdWorker redisIdWorker;
&#64;Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher voucher &#61; iSeckillVoucherService.getById(voucherId);
LocalDateTime beginTime &#61; voucher.getBeginTime();
if (beginTime.isAfter(LocalDateTime.now())) {
return Result.fail("活动尚未开始");
}
LocalDateTime endTime &#61; voucher.getEndTime();
if (LocalDateTime.now().isAfter(endTime)) {
return Result.fail("活动已经结束");
}
if (voucher.getStock() < 1) {
return Result.fail("库存不足!");
}
Long userId &#61; UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
IVoucherOrderService proxy &#61; (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
&#64;Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId &#61; UserHolder.getUser().getId();
int count &#61; query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经购买过一次");
}
boolean success &#61; iSeckillVoucherService.update()
.setSql("stock &#61;stock -1")
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
if (!success) {
return Result.fail("库存不足!");
}
VoucherOrder voucherOrder &#61; new VoucherOrder();
long orderId &#61; redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
一人一单的并发安全问题&#xff1a;
通过加锁可以解决在单机情况下的一人一单安全问题&#xff0c;但是在集群模式下就不行了。
1.我们将服务启动两份&#xff0c;端口分别为8081和8082&#xff1a;
2.修改nginx的conf目录下的nginx.conf文件&#xff0c;配置反向代理和负载均衡&#xff1a;
现在&#xff0c;用户请求会在这两个节点上负载均衡&#xff0c;再次测试下是否存在线程安全问题。
一人一单的并发安全问题&#xff1a;