范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

微服务SpringBoot整合Redis实现优惠卷秒杀一人一单

  文章目录:partly_sunny:全局唯一ID  :zap:Redis实现全局唯一ID  :hourglass_flowing_sand:问题分析  :watch: 乐观锁解决库存超卖:  :white_check_mark:Jmeter 测试  五、优惠卷秒杀 实现一人一单  一、什么是全局唯一ID:partly_sunny:全局唯一ID
  在分布式系统中,经常需要使用   全局唯一ID   查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。
  全局唯一ID在数据库中一般会被设成   主播   ,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。
  这样全局唯一ID就需要保证这两个需求:  全局唯一  趋势有序
  我们的场景是 优惠卷秒杀抢购, 当用户抢购时,就会生成订单 并保存到 数据库 的订单表中,而订单表 如果使用数据库自增ID就会存在以下问题  id的规律性太明显  受单表数据量限制
  场景分析: 如果我们的id具有太明显的规则,   用户或者说商业对手很容易猜测出来我们的一些敏感信息   ,比如 商城在一天时间内,卖出了多少单 ,这明显不合适。
  场景分析二:随着我们商城规模越来越大,   MySQL 的单表的容量不宜超过500W   ,数据量过大之后,我们要   进行拆库拆表   ,但拆分表了之后,他们从   逻辑上讲他们是同一张表,所以他们的id是不能一样的   , 于是乎我们需要保证id的唯一性。
  全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
  为了   增加ID的安全性,我们可以不直接使用Redis自增的数值   ,而是拼接一些其它信息:
  ID的组合为符号位:   1bit,永远为0  时间戳:   31bit,以秒为单位可以使用69年  序列号:   32bit,秒内的计数器,支持每秒产生   2^32    个 不同ID :zap:Redis实现全局唯一ID编写工具类@Component public class RedisIdWorker {       /**      * 开始时间戳      */     private static final long BEGIN_TIMESTAMP = 1640995200L;     /**      * 序列号的位数      */     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.自增长         long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);          // 3.拼接并返回         return timestamp << COUNT_BITS | count;     } }测试存入Redis@Autowired private RedisIdWorker redisIdWorker;  private ExecutorService es = Executors.newFixedThreadPool(500);   @Test public void testWorkerId() throws InterruptedException {       CountDownLatch latch = new CountDownLatch(300);     Runnable task = () -> {           for (int i = 0; i < 100; i++) {               long id = redisIdWorker.nextId("order");             System.out.println("id = " + id);         }         latch.countDown();     };      long begin = System.currentTimeMillis();     for (int i = 0; i < 300; i++) {           es.submit(task);     }     latch.await();     long end = System.currentTimeMillis();     System.out.println("times = " + (end- begin));  }这里用到了 CountDownlatch,简单的介绍一下:
  CountDownLatch名为信号枪:主要的作用是   同步协调在多线程的等待与唤醒问题
  我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch
  CountDownLatch 中有两个最重要的方法  countDown  await
  await 是阻塞方法,   我们担心线程没有执行完时,main线程就执行,所以可以   使用await就阻塞主线程 , 那么什么时候main线程不再阻塞呢? 当 CountDownLatch 内部维护的变量为0时,就不再阻塞,直接放行 。
  什么时候 CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1 ,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。  二、环境准备需要搭建登录环境,基础环境代码和sql文件均已上传 GitCode 链接:基础环境和SQL三、实现秒杀下单添加优惠卷VoucherServiceImpl 核心代码@Service public class VoucherServiceImpl extends ServiceImpl implements IVoucherService {         // 该类无代码,直接MyBatis-Plus继承实现类 即可,自动完成持久化     @Autowired     private ISeckillVoucherService seckillVoucherService;      @Override     public ResultBean> queryVoucherOfShop(Long shopId) {           // 查询优惠券信息         List vouchers = getBaseMapper().queryVoucherOfShop(shopId);         // 返回结果         return ResultBean.create(0, "success", vouchers);     }      @Override     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);     } }VoucherController 接口层@RestController @CrossOrigin @RequestMapping("/voucher") public class VoucherController {        @Autowired     private IVoucherService voucherService;          /**      * 新增秒杀券      * @param voucher 优惠券信息,包含秒杀信息      * @return 优惠券id      */     @PostMapping("seckill")     public ResultBean addSeckillVoucher(@RequestBody Voucher voucher) {           voucherService.addSeckillVoucher(voucher);         return Result.ok(voucher.getId());     } }编写下单业务VoucherOrderServiceImpl 优惠卷订单核心业务类@Service public class VoucherOrderServiceImpl extends ServiceImpl  implements IVoucherOrderService {         @Autowired     private ISeckillVoucherService seckillVoucherService;      @Autowired     private RedisIdWorker redisIdWorker;      @Override     @Transactional     public Result seckillVoucher(Long voucherId) {           //1. 查询优惠卷         SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);         //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购         if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {               return Result.fail("秒杀尚未开始!");         }         //3. 判断秒杀是否结束         if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {               return Result.fail("秒杀已经结束!");         }         //4. 判断库存是否充足         if (seckillVoucher.getStock() < 1) {               return Result.fail("库存不足!");         }          Long userId = UserHolder.getUser().getId();         //5. 查询订单         //5.1 查询订单         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();         long orderId = redisIdWorker.nextId("order");         voucherOrder.setId(orderId);         voucherOrder.setUserId(userId);         voucherOrder.setVoucherId(voucherId);         save(voucherOrder);         //8. 返回订单id         return Result.ok(orderId);     } }VoucherOrderController 接口层@RestController @CrossOrigin @RequestMapping("/voucher_order") public class VoucherOrderController {        @Autowired     private IVoucherOrderService voucherOrderService;      @PostMapping("seckill/{id}")     public Result seckillVoucher(@PathVariable("id") Long voucherId) {           return voucherOrderService.seckillVoucher(voucherId);     } }测试抢购秒杀优惠卷ApiFox 新增以下接口添加秒杀卷
  测试返回成功即可。抢购秒杀优惠卷接口
  测试无误,抢购成功!四、库存超卖问题:hourglass_flowing_sand:问题分析
  有关超卖问题分析:在我们原有代码中是这么写的  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("库存不足!");     }
  假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。
  超卖问题是典型的多线程安全问题,这种情况下   常见的解决方案就是 加 锁   :而对于加锁,我们   通常有两种解决方案   :
  悲观锁:
  悲观锁   可以实现对于数据的串行化执行   ,比如syn,和lock都是悲观锁的代表,同时, 悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等  乐观锁:
  会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,**如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,**如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas
  乐观锁的典型代表:就是   CAS   ,利用CAS   进行无锁化机制加锁   ,varNum是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值
  其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。  int varNum; do {       varNum = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  return var5;我们采用的方式为:
  在操作时, 对版本号进行+1 操作,然后要求version 如果是1 的情况下,才能操作 ,那么第一个线程在操作后,数据库中的version变成了2,但是他自己满足version=1 ,所以没有问题,此时线程2执行, 线程2 最后也需要加上条件version =1 ,但是现在由于线程1已经操作过了,所以线程2,操作时就不满足version=1 的条件了,所以线程2无法执行成功
  :watch: 乐观锁解决库存超卖加入以下代码解决超卖问题
  之前的方式要修改前后都保持一致,但是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变一下,改成stock大于0 即可  boolean success = seckillVoucherService.update()             .setSql("stock= stock -1")             .eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0知识拓展针对CAS中的自旋压力过大,我们可以使用Longaddr这个类去解决
  Java8   提供的一个对AtomicLong改进后的一个类,LongAdder
  大量线程并发更新一个原子性的时候,天然的问题就是自旋,会导致并发性问题,当然这也比我们直接使用syn来的好
  所以利用这么一个类,LongAdder来进行优化
  如果获取某个值,   则会对cell和base的值进行递增,最后返回一个完整的值
  以上的解决方式,依然有些问题,下面使用Jmeter进行测试:white_check_mark:Jmeter 测试添加线程组
  添加JSON断言,我们认为返回结果为false的就是请求失败
  在线程组右击选择断言 --> JSON 断言
  加入以下判断
  判断success字段,值是否为true,是true就是返回成功~反之失败  查看结果树、HTTP信息请求头、汇总报告、聚合报告等均在http请求右击添加即可启动,查看返回的结果
  查看聚合报告
  异常率这么高,再来看数据库
  数量正确,我们再看订单表
  id都一样,这可不行啊,我们 真实场景下,发放优惠卷不会让一个用户去抢购所有的订单秒杀优惠卷,这样商家就太亏了 ,全让黄牛给抢走了,这可不行,我们   需要限制用户的抢购数量。  五、优惠卷秒杀 实现一人一单初步实现int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) {       return Result.fail("用户已经购买过!"); }
  存在问题:现在的问题还是和之前一样,并发过来,查询数据库,都不存在订单,所以我们还是需要加锁,但是乐观锁比较适合更新数据,而现在是插入数据,所以我们需要使用悲观锁操作
  注意:在这里提到了非常多的问题,我们需要慢慢的来思考,首先我们的初始方案是封装了一个createVoucherOrder方法,同时为了确保他线程安全,在方法上添加了一把synchronized 锁  加上悲观锁@Override     public Result seckillVoucher(Long voucherId) {           //1. 查询优惠卷         SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);         //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购         if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {               return Result.fail("秒杀尚未开始!");         }         //3. 判断秒杀是否结束         if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {               return Result.fail("秒杀已经结束!");         }         //4. 判断库存是否充足         if (seckillVoucher.getStock() < 1) {               return Result.fail("库存不足!");         }          Long userId = UserHolder.getUser().getId();         synchronized (userId.toString().intern()) {               IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();             return proxy.createVoucherOrder(voucherId, userId);         }     }      @Transactional     @Override     public Result createVoucherOrder(Long voucherId, Long userId) {           //5. 查询订单         //5.1 查询订单         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).gt("stock", 0).                 update();         if (!success) {               return Result.fail("库存不足!");         }           //7. 创建订单         VoucherOrder voucherOrder = new VoucherOrder();         long orderId = redisIdWorker.nextId("order");         voucherOrder.setId(orderId);         voucherOrder.setUserId(userId);         voucherOrder.setVoucherId(voucherId);         save(voucherOrder);         //8. 返回订单id         return Result.ok(orderId);     }在启动类加入以下注解,启动AspectJ@EnableAspectJAutoProxy(exposeProxy = true)以上代码,采用悲观锁解决了高并发下,一人多单的场景,同时,也解决了事务失效。引入了AspectJ解决!Jmeter 测试再次测试,查看结果
  可见返回的结果异常率如此高,再看请求信息
  可见已经成功的拦截了错误请求,JSON断言正确。  查看数据库 信息
  优惠卷数量
  可见成功的完成了 在高并发请求下 的一人一单功能。:boat:小结
  以上就是【   Bug 终结者   】对 微服务Spring Boot 整合Redis 实现优惠卷秒杀 一人一单 的简单介绍, 在分布式系统下,高并发的场景下,会出现此类库存超卖问题,本篇文章介绍了采用乐观锁来解决,但是依然是有弊端,下章节,我们将继续进行优化,持续关注!

中国富豪欧美银行销户取钱涌入香港?香港金管局回应近日,一则大量富豪正在把钱从美国瑞士转回中国香港和新加坡的消息在互联网上被大量转发。该消息称,中国富豪的数量达到百万级,而涉及金额达到了2400亿美金。随后,汇丰银行传出消息,在香平安私人银行守望家国先锋人物科技创新,坚守实业报国初心百年前,国家民族危亡之际,民族企业家们以实业兴邦。今天,在迈向高质量发展的道路上,中国企业家继续主动担当积极作为,把企业发展与国家发展结合起来,投身实体经济,不断推动技术攻关,实现美经济学家预警186家银行恐暴雷眼下,美国正迎来真正的大危机,随着国内多家银行相继倒闭,美国不少经济学家推测,接下来有将近200家银行或也将暴雷,中国富豪可谓是损失惨重。前不久,美国第16大银行硅谷银行倒闭,这一ESPN罗比尼奥自愿上交护照,以表明自己没有潜逃的意愿直播吧3月24日讯据ESPN报道,罗比尼奥的辩护律师向高等法院提交了球员自愿上交护照的请求,罗比尼奥希望以此表明自己的诚意并且他不存在潜逃离开巴西的意愿。辩护律师表示,罗比尼奥仍然英国研究人员预测未来旅游心跳识别取代传统护照,酒店3D打印自助餐极目新闻记者孙喆能想到50年后的旅行是什么样吗?3月24日,一份由英国廉价航空公司易捷航空发布的2070年旅行报告引发广泛关注。据英国每日邮报报道,英国一些研究人员和未来学家预测,如何提高自己的魅力和吸引力?魅力是一种内在的气质,能让人感受到你的独特性个性和风格而吸引力是一种外在的效果,能让人对你产生好感兴趣和欣赏。有些人天生就有魅力和吸引力,而有些人则需要后天培养和提升。那么,如何提调查发现,超过三分之一的澳大利亚人担心他们的手机正在监听他们超过三分之一的澳大利亚人担心他们的手机正在监听他们,因为他们看到了他们曾经谈论过但从未搜索过的目标广告。最近的一项调查发现,37的受访者认为他们看到了电视或谈话中提到的项目的广告,又见无理打压!美国商务部将14家中国实体列入出口管制未经核实清单又见无理打压!美国商务部将14家中国实体列入出口管制未经核实清单财联社3月24日电,美国商务部工业与安全局(BIS)当地时间3月23日发布公告,将14家中国实体列入出口管制未经核实在爱里长大是什么体验?美国3岁小公主视频爆火让网友泪奔知乎上经常能看到一类问题,为什么有些父母经常对子女冷嘲热讽?很多望子成龙的中国父母,都喜欢对孩子用打压式教育。一点小事动辄打骂,几乎不夸奖孩子在这种环境中生活的孩子,长大后,往往会宋丽珏读妈妈教授带着娃搞学术那些事妈妈教授在学术界实现工作与家庭的平衡,美瑞秋康奈利克里斯汀戈德西著,李明倩宋丽珏译,上海交通大学出版社雅理,2023年2月出版,305页,69。00元我巴不得进医院,至少能休息一阵中国恒大恒大汽车面临停产风险,寻求超290亿元融资Tech星球3月23日消息,中国恒大3月22日公告,恒驰5于2022年9月16日正式量产,首批量产车已于2022年10月29日开始交付。截至本公告发布之日,恒大新能源汽车已交付超过
海啸兄弟重聚?库里招揽杜老二!阿杜我愿意辅佐他重建王朝近日,根据一份最新的报道,在杜兰特向篮网提出申请交易后,很多球迷都在盼望杜兰特能够重回勇士。而ESPN记者FOXSports声称,斯蒂芬库里正在竭尽全力实现这一目标。库里已经多次打SeeroFit库里第一个FMVP,NBA夺冠之路背后是运动康复6月17日,在全NBA最著名的球馆之一TD花园球馆里,库里攻下34分7篮板7助攻,一举斩获第四个NBA总冠军和第一个FMVP,成为全场最耀眼的明星。而就在不久前,6月9日总决赛G3努比亚Z40SPro外观公布努比亚将于7月20日发布新机努比亚Z40SPro。努比亚官方公布了这款手机的外观,努比亚Z40SPro正面采用居中打孔直屏,边框看起来很窄,后置三颗摄像头,加上环形闪光灯呈方形排列10换1!马刺追求杜兰特筹码曝光,重建之路何时能走向正轨马刺是NBA历史上唯一的小球市王朝,就是这样一支队伍,在联盟大获成功。但自2016年邓肯退役后,马刺就已经江河日下,不过好在有伦纳德,这是他们未来的基石。本应继续前景光明,可惜他们拍杂志还得是Angelababy,置身森林演绎精灵少女,湿发造型好抢眼Angelababy是家喻户晓的大明星,平时在影视剧或者综艺节目里都能看到她的倩影,一张精致的脸蛋,一双灵动的眼眸,自带一种混血气质,让baby在内娱混得风生水起,很多人都觉得她的46岁舒淇最新大片造型封神!穿少女粉一点不媚俗,气质高级太少有今年46岁的舒淇,凭借独一份的气质吸粉无数,明明已经是快50岁的人了,但是却一点也不显老,甚至整个人的状态,比年轻时候还要好,脸部紧致,线条流畅,可以说是逆生长的典范,再加上绝佳的18家药企荣登2022财富中国500强,6家营收超千亿(人民日报健康客户端陈琳辉)7月12日,财富PlusAPP发布今年的财富中国500强排行榜。据人民日报健康客户端不完全统计,有18家医药企业上榜,其中6家营收超1000亿,分别是国10轮中超,实力可分6档,三镇1档,山东2档,大连获肯定,广州难中超中超第10轮已经结束,第一阶段也已经结束,接下来在第二阶段各队可能还有很大的变化,目前从第一阶段的表现来看,可以看出各队的实力排行,按现在打出来的实力,中超可以分成6个档。武汉意甲600万欧!罗马报价迪巴拉,小魔仙点头,穆里尼奥笑了目前迪巴拉的团队只收到了来自罗马一家俱乐部正式报价,他们为阿根廷球星提供了一份为期600万欧元队内的顶薪,对于这份报价迪巴拉还是比较满意的,再加上罗马主教练穆里尼奥一直非常欣赏小魔安切洛蒂官宣!皇马夏窗关闭,2签花1。1亿,捡漏切尔西压利物浦7月15日,皇马主帅安切洛蒂在与墨西哥美洲的热身赛赛前新闻发布会上证实,俱乐部的夏窗引援已经结束。今年夏窗动作最快的两家豪门球队莫过于皇家马德里和利物浦,前者在引援姆巴佩失败后迅速上半年GDP同比增长2。5今天(7月15日)上午,国务院新闻办公室举行新闻发布会,介绍2022年上半年国民经济运行情况。初步核算,上半年国内生产总值562642亿元,按不变价格计算,同比增长2。5。分产业看