前言

本章节主要实现限时、限量优惠券秒杀功能,并利用分布式锁解决《超卖问题》、《一人一单问题》。

一.优惠券下单基本功能实现

1.功能介绍及流程图


2.代码实现

@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdworker redisIdworker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);if (voucher==null){return Result.fail("优惠券不存在!");}// 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();// 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);// 7.返回订单idreturn Result.ok(orderId);}

一.超卖问题

1.问题分析

线程1,执行完,查询,查到库存为1,判断库存大于0,然后进行扣减库存操作,在这之间,线程2,线程3,也都进行了查询,查询到的库存也都是1,判断库存也都大于0,都进行了扣减库存操作,导致库存只有1个,卖出了3次。

2.解决方案(悲观锁、乐观锁)

悲观锁: 添加同步锁,让线程串行执行
    优点: 简单粗暴
    缺点: 性能一般
乐观锁: 不加锁,在更新时判断是否有其它线程在修改
    优点: 性能好
    缺点: 存在成功率低的问题


悲观锁是通过加锁的方式,让原本并发执行的变成串行执行,保证了线程安全,但是大大降低了执行效率,悲观锁实现较为简单,本文主要研究乐观锁的实现方式。

2.1 乐观锁实现方式-版本号法


版本号法: 数据库冗余一个版本字段,每次查询库存的时候,就将这个版本字段也查询出来,在更新的时候,版本号加一,条件加上版本号等于查询到的版本,如果版本被根据了,数据库的update语句就会执行失败。
如上图,线程1,已查询到的库存为1,版本号为1,同时线程2也查询到库存为1,版本号为1,线程1在更新的时候,将版本加1 ,version = version + 1,同时更新条件加上 and version =1,更新完成,version 值变成2,线程2更新操作的时候,同样会将版本加1,version = version + 1,更新条件加上and version =1,但是此时的version已经被线程1更新为2,导致线程2的更新操作会失败,保证了线程安全。

2.2 乐观锁实现方式-CSA法


CSA法: 用需要修改的数据本身来判断数据是否已修改,利用库存本身的数据,来代替了版本,如上图线程1查到的库存是1,更新库存时,update语句加上and stock =1,执行成功,库存变成0,线程2,查到库存是1,同样更新库存时,update语句加上and stock =1,但此时数据已经被线程1改为0,导致线程2的更新操作会失败,保证了线程安全。

2.2.1 代码实现

只需要对更新数据库的语句进行修改

         // 5.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock=stock -1.eq("voucher_id", voucherId).eq("stock",voucher.getStock())//where id=? and stock=?.update();

该方法解决了线程安全问题,但是带来了新的问题,失败率将会大大提升,如库存为100,100个线程并发执行,同时查到了库存为100,更新时,99个线程都会失败,只有一个会成功,按照正常的业务流程,100个库存,100个线程并发执行,应该都会成功,下面对扣减库存的逻辑进一步优化,解决失败率高的问题。
执行update语句时不用and stock =查到的值,只需要将条件改为 and stock >0,就解决了失败率高的问题。

 // 5.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock=stock -1.eq("voucher_id", voucherId).gt("stock",voucher.getStock())//where id=? and stock > 0.update();

二.一人一单

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

1.单机应用下通过synchronized解决一人一单

引入依赖

  <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>

启动类开启暴露代理对象

@EnableAspectJAutoProxy(exposeProxy = true) //开启暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}
}

具体实现

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdworker redisIdworker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);if (voucher == null) {return Result.fail("优惠券不存在!");}// 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.1一人一单Long userId = UserHolder.getUser().getId();// 同一个用户加锁,不同用户加不同的锁,toString()底层每次都new了一个新的对象,// 会造成同一个用户加的是不同的锁// intern()方法是去常量池找跟字符串值一样的地址,避免同一个用户加了不同的锁synchronized (userId.toString().intern()) {// 事务要生效,需要spring对当前类做了动态代理,拿到代理对象,用代理对象做了事务处理,如果用this调用方法,就是用的是// 当前对象,造成实务无效,需要获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId, voucher, userId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId, SeckillVoucher voucher, Long userId) {// 5.2 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucher).count();// 5.3 判断订单是否存在if (count > 0) {return Result.fail("用户已购买过一次!");}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock=stock -1.eq("voucher_id", voucherId).gt("stock", voucher.getStock())//where id=? and stock > 0.update();// 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);// 8.返回订单idreturn Result.ok(orderId);}
}

2.分布式系统下通过Redis分布式锁解决一人一单

分布式系统每个服务会部署很多个实例,每个实例对一个一个独立的JVM,在每个实例内部能通过synchronized实现线程的互斥,但是实例和实例直接就无法试下线程互斥,只能通过分布式锁来解决。

2.1 代码实现

锁的接口类

public interface ILock {/*** 尝试获取锁* @param timeoutSec* @return*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}

实现类

package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;/*** @auther Kou* @date 2022/7/11 22:33*/
public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";@Overridepublic boolean tryLock(long timeoutSec) {// 1.获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 2.获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);// 3.success是Boolean包装类型,而方法的返回值是基本类型boolean,直接返回success会进行拆箱,// 如果success是null,就会报空指针return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 1.获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 2.获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 3.判断线程标识和锁标识是否一样if (threadId.equals(id)) {stringRedisTemplate.delete(KEY_PREFIX + name);}}
}

业务代码改造

   @Resourceprivate StringRedisTemplate stringRedisTemplate;public Result seckillVoucher01(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);if (voucher == null) {return Result.fail("优惠券不存在!");}// 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.1一人一单Long userId = UserHolder.getUser().getId();// 6.创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);// 7.获取锁boolean isLock = lock.tryLock(1200);// 8.判断是否获取锁成功if (!isLock) {return Result.fail("不允许重复下单");}try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId, voucher, userId);} finally {// 9.释放锁lock.unlock();}}

【使用Redis分布式锁实现优惠券秒杀功能】-Redis学习笔记05相关推荐

  1. redis分布式锁java代码_基于redis实现分布式锁

    " 在上一篇文章中介绍了动态配置定时任务,其中的原理跟spring 定时任务注解@Scheduled一样的,都是通过线程池和定义执行时间来控制.来思考一个问题,如果我们的定时任务在分布式微服 ...

  2. redis分布式锁的8大坑【Redis分布式锁】

    在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中. 但不是说用了redis分布式锁,就可以高枕无忧了,如果没有用好或者用对,也会引来一些 ...

  3. Redis分布式锁原理(一)——redis分布式锁需要注意的问题

    下一篇:Redis分布式锁原理(二)--Redisson分布式锁源码浅析 虽然目前Redisson框架已经帮我们封装好了分布式锁的实现逻辑,我们可以直接像调用本地锁一样使用即可,但本文并不直接剖析Re ...

  4. 探讨Redis分布式锁解决优惠券拼抢问题

    一.什么是分布式锁 分布式锁是控制不同系统之间访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来确保数据一致性. 二.为什么需要分布式锁 ...

  5. Redis分布式锁实现并发秒杀商品设计思路

    1.何为分布式锁 通俗的讲,分布式锁就是说,缓存中存入一个值(key-value),谁拿到这个值谁就可以执行代码.在并发环境下,我们通过锁住当前的库存,来确保数据的一致性.知道信息存入缓存.库存-1之 ...

  6. 微服务架构之:Redis的分布式锁---搭建生产可用的Redis分布式锁

    Redis分布式锁 集群架构下的并发问题 分布式锁的实现原理和不同方式的实现对比 基于Redis实现的分布式锁 Redis分布式锁1.0版 基于Redis分布式锁1.0版的误删问题 解决误删问题,Re ...

  7. Redis分布式锁(图解 - 秒懂 - 史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  8. Redis分布式锁介绍及多方案实现

    一.分布式锁的作用 分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现.如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性. 分布 ...

  9. 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

    ‍‍‍‍‍‍‍‍‍‍‍‍阅读本文大约需要 20 分钟. 大家好,我是 Kaito. 这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题. Redis 分布式锁的话题,很多文章已经写烂了 ...

最新文章

  1. 人工神经网络在行人轨迹预测上的应用
  2. C#开发微信门户及应用(5)--用户分组信息管理
  3. python核心编程第二版pdf_Python Book电子书pdf版合集 Python核心高级编程第二版
  4. django-vue-admin前端设置后台接口地址为127.0.0.1产生跨域问题解决办法
  5. CS这么难申,小哥哥你怎么拿到全美最高额度奖学金的?
  6. 网狐框架分析八--web登录游戏大厅流程
  7. Halcon例程(基于多个标定图的单目相机标定)详解—— Camera_calibration_multi_image.hdev
  8. 蓝桥杯 平面切分(欧拉定理)
  9. jsr303自定义验证_JSR 310新日期/时间API的自定义JSR 303 Bean验证约束
  10. 阿里云杜欢:云上Serverless开发能力将成为前端的“金手指”
  11. 支付宝封杀比特币,可以说是毫不留情!
  12. leetcode 236. 二叉树的最近公共祖先LCA(后序遍历,回溯)
  13. 机器学习代码实战——线性回归(单变量)(Linear Regression)
  14. xp系统计算机启动时灰色界面,windows xp系统下屏幕开始按钮变成灰色怎么处理
  15. 学生成绩管理系统简单c语言源代码,c语言学生成绩管理系统源代码
  16. linux下nfs安装配置
  17. java jtable 单元格合并_JTable单元格合并AAA
  18. 中小企业网站优化推广思路方法技巧
  19. selenium和requests实现12306登录及余票查询
  20. nodejs中https和ca证书

热门文章

  1. 云计算的特点与产生、云计算体系结构、新摩尔定律、云计算优势
  2. 诗仙诗圣,你还知道诗什么
  3. hdu 2086 A1 = ?(递推)
  4. Java声效计算器(带有十进制转二进制和十六进制的功能)
  5. SpringBoot详解(一)
  6. 修改计算机ip地址cmd,win7系统通过命令提示符将系统修改为静态IP地址的方法【图文】...
  7. codeforces contest 869 problem C(组合数)
  8. 一款恋爱星座男女配对微信小程序源码
  9. java微信分享朋友圈_java怎么实现微信分享到朋友圈功能
  10. 英语发音规则---O字母