虽然本文是针对黑马点评的优惠券秒杀业务的实现,但是是适用于各种抢购活动,保证线程安全。

摘要:本文先讲了抢购问题,指出其中会出现的多线程问题,提出解决方案采用悲观锁和乐观锁两种方式进行实现,然后发现在抢购过程中容易出现一人多单现象,为保证优惠券不会被【黄牛】抢到,因此我们在保证多线程安全的情况下实现了一人一单业务,最后指出本文的实现在集群情况下的不足之处。在本专栏的另一篇文章中提出集群或者分布式系统的解决方案


【前端页面】

在代金券发放后,多个用户会进行优惠券抢购,在抢购时需要判断两点:

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

下单核心逻辑分析:

当用户开始进行下单,我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件

比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

【逻辑图】

【代码实现】

@Override
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("stock= stock -1").eq("voucher_id", voucherId).update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}

【分析代码】

  • 从上述的逻辑图中我们可以知道,要扣减库存,并且要保存订单,因此需要事务业务
  • 在第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("库存不足!");}

【采用锁】解决上述超卖问题。

悲观锁:

悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等

乐观锁:

乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas

乐观锁的典型代表:就是cas,利用cas进行无锁化机制加锁,var5 是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值

其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。

修改代码方案

我们的乐观锁保证stock大于0 即可,如果查询逻辑stock不能保证大于0,则会出现 success为false我们在后文进行判断即可。

boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0if (!success) {//扣减库存return Result.fail("库存不足!");}

代码写到这里,我们就解决了多线程安全问题(优惠券超卖)


一人一单

但是我们在检查数据库数据时,我们发现一个人可以购买多个优惠券。

因此我们可以在抢购前,判断该用户是否已经购买过该优惠券,如果购买过则直接返回。

【逻辑图】红框内的是新增逻辑。


@Override
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.一人一单逻辑// 5.1.用户idLong userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了return Result.fail("用户已经购买过一次!");}//6,扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update();if (!success) {//扣减库存return Result.fail("库存不足!");}//7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);voucherOrder.setUserId(userId);// 7.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}

【分析代码】---仍然会出现多线程问题。

        存在问题:现在的问题还是和之前一样,并发过来,查询数据库,都不存在订单,所以我们还是需要加锁,但是乐观锁比较适合更新数据,而现在是插入数据,所以我们需要使用悲观锁操作

【注意事项】

  • 事务应该包含在锁的内部。
  • 锁的粒度,锁的对象应该是用户级别的,而不是整个抢购优惠券级别的,因此我们不会直接将synchronized加到方法上。
  • 锁对象的细节处理,使用userId.toString().intern()保证对象唯一。
  • 获取代理对象调用切入事务
package com.hmdp.service.impl;import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** <p>* 服务实现类* </p>** @author msf* @since 2022-10-29*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisWorker redisWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1. 查询优惠券信息SeckillVoucher voucherOrder = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucherOrder.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("抢购尚未开始");}if (voucherOrder.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("抢购已经结束");}// 3.判断库存是否充足if (voucherOrder.getStock() < 1) {return Result.fail("您来晚了,票已被抢完");}Long userId = UserHolder.getUser().getId();// 事务应该在synchronized里面synchronized (userId.toString().intern()) {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId,userId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId,Long userId) {// 4. 一人一单逻辑// 4.1 根据优惠券id和用户id查询订单Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 4.2 订单存在,直接返回if (count > 0) {return Result.fail("用户已经购买一次");}// 5. 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").gt("stock", 0).eq("voucher_id", voucherId).update();if (!success) {return Result.fail("库存不足");}// 6.创建订单VoucherOrder order = new VoucherOrder();// 6.1 设置idorder.setId(redisWorker.nextId("order"));// 6.2 设置订单idorder.setVoucherId(voucherId);// 6.3 设置用户idorder.setUserId(userId);save(order);// 7. 返回订单idreturn Result.ok(order);}
}

展望

虽然我们利用锁和事务解决单体系统下的秒杀功能,但是现在的业务一般是在集群和分布式系统协作完成,因此我们在测试系统在集群部署时,仍会出现一人多单问题,稍后我们将更新文章,分析问题出现原因,并利用分布式锁的方式解决该问题。

Redis解决优惠券秒杀相关推荐

  1. Redis 基础 - 优惠券秒杀《分布式锁(初级)》

    参考 Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 Redis 基础 - 优惠券秒杀 ...

  2. Redis 基础 - 优惠券秒杀《初步优化(异步秒杀)》

    Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 Redis 基础 - 优惠券秒杀< ...

  3. Redis(4)优惠券秒杀

    优惠券秒杀 全局ID生成器 优惠券秒杀 秒杀实现 库存超卖 乐观锁实现 一人一单 分布式锁 分布式锁版本一 Redis分布式锁误删情况 解决分布式锁误删 分布式锁原子性问题 解决原子性问题 利用Jav ...

  4. Redis 基础 - 优惠券秒杀《非集群》

    参考 Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 摘要 用Redis生成保证唯一性 ...

  5. SpringBoot整合Redis实现优惠券秒杀服务(笔记+优化思路版)

    本文属于看黑马的redis的学习笔记,记录了思路和优化流程,精简版最终版请点击这里查看. 文章目录 一.全局ID生成器 1.1 理论 1.1.1 全局唯一ID生成策略 1.2 代码(Redis自增) ...

  6. Redis实现优惠券秒杀

    优惠券秒杀 全局唯一ID 问题 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题 id的规律性太明显 受单表数据量的限制 解决办 ...

  7. Spring Boot + redis解决商品秒杀库存超卖,看这篇文章就够了

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:涛哥谈篮球 来源:toutiao.com/i68366119 ...

  8. redis如何解决秒杀超卖java_Spring Boot + redis解决商品秒杀库存超卖,看这篇文章就够了...

    作者:涛哥谈篮球 来源:toutiao.com/i6836611989607809548 问题描述 在众多抢购活动中,在有限的商品数量的限制下如何保证抢购到商品的用户数不能大于商品数量,也就是不能出现 ...

  9. Redis解决商品秒杀与超卖

    这个是实体店我们会看到的场景,100件商品,1人1件,最后200人中只有100人能买到商品,剩下100人只能空手而归.如果您开了家网店,同样你开起了秒杀的活动,可能同时会有1000人通过不同的终端访问 ...

最新文章

  1. nginx 常用命令 和 配置
  2. 【Ubuntu入门到精通系列讲解】远程管理常用命令速查
  3. Vysor:安卓手机放到电脑上用
  4. 域电脑不能显示桌面_学会这些电脑操作,工作更高效,摸鱼更舒适!
  5. docker fig mysql_docker管理工具 Fig 配置文件fig.yml的详解
  6. matlab晶闸管整流电路,采用Matlab/Simulink对三相桥式全控整流电路的仿真分析
  7. 深度学习《EBGAN》
  8. HBuilder制作WAP2APP之给APP增加或注入分享功能
  9. Newsgroups数据集介绍
  10. 递归-Hanoi塔问题
  11. 同一Inputstream的父类和子类对象请维持最具体的子类对象,不要混合使用
  12. 在infoWindow中显示Geocode server(地理编码服务)
  13. [jQuery] 按比例缩小图片
  14. vue事件发射与接收(可实现页面传值和非父子组件传值)
  15. 一个Lua脚本的解密过程
  16. Vmlogin防关联超级浏览器Selenium浏览器自动化详细教程
  17. python卡路里计算器_python习题20190130
  18. SAP系统中的几种系统级消息(用户登录后消息弹窗SM02,TH_POPUP,以及系统邮件)
  19. 阿里云域名配置和https证书(ssl证书)配置内容
  20. 电脑老系统怎么换新系统?

热门文章

  1. 我的UC/OS,我做主
  2. 阿里云ecs服务器挂载oss
  3. 基于GAN的小目标检测算法总结(1)——Perpetual GAN
  4. Android开发指南!Android事件分发机制收藏这一篇就够了,2年以上经验必看
  5. 2 个python美化表格数据输出结果的工具,摸鱼简直心安理得~
  6. C++ 不高兴的津津
  7. 重磅分享:微软面试100题2010年版全部答案完整亮相
  8. 41.Java HashMap有序集合
  9. Spark RDD使用详解--RDD原理
  10. vue-router之addRoutes(动态路由/权限路由)使用分析