本篇,咱们来实现优惠券秒杀下单功能。通过本篇学习,我们将会有如下收获:

1:优惠券领券业务逻辑;

2:分析在高并发情况下,出现超卖问题产生的原因;

3:解决超卖问题两种方案:版本号法及CAS法

4:乐观锁弊端改进方案;

本文涉及内容比较多,篇幅会比较长,同时有大量截图。希望大家能耐心看完。好了,话不对说,咱们开始go go go~

小福利:

凯哥自己开发的,领取外卖、打车、咖啡、买菜、各大电商的优惠券的公¥众¥号。如下图:

一:基本的秒杀实现

下单时候需要判断:

1:秒杀是否开始或结束,如果尚未开始或者已经结束则无法下单;

2:库存是否充足,不充足无法下单

业务:

根据上图逻辑,我们可以得到代码相关逻辑:

1:查下优惠券、2:判断是否秒杀开始;3:判断秒杀是否结束;4:判断库存是否充足;5:扣减库存;6:创建订单;

相关代码如下:

二:分析上面代码是否存在问题

我们使用JMeter模拟200个用户去秒杀抢优惠券。运行结果:

异常是45.5%。这个不对啊,按照我们预期的应该是50%的用户失败才对。这45.5%,说明优惠券超卖出了9个。是吗?我们来查查优惠券表:

库存为-9.再来查询订单表:发现订单是109条。在高并发的情况下,还真的是超卖出了9个呢。

来分析为什么会出现这种情况呢?

来看看代码,扣减库存的相关代码:

我们来分享下扣除库存流程:

两个线程来抢,假设当前就库存就剩下一个了。线程1和线程2来抢这个库存。流程如下:

在高并发的情况下,线程谁先执行,还真不好说。在高并发情况下,可能执行的顺序就如下图:

超卖问题分析:

T1的时候,线程1执行从数据库查询操作,查询结果为1;然后CPU让出,线程2来执行,在T2时候,线程2也去执行数据库查询操作,查询结果也是1.然后线程2,让出CPU,T3时候,线程1得到了CPU执行权,执行扣除库存操作。T4时候线程得到了CPU执行权,同样执行扣除库存操作。当两个线程都执行完成后,数据库中的库存就成了-1了。

这只是有2个线程,当高并发的时候,有多个线程来查询库存,扣除库存。如果出现了上面情况,就会出现超卖情况。

超卖问题场景的解决方案

超卖问题就是典型的多线程安全问题,针对这一问题常见的解决方案就是加锁。锁分为乐观锁和悲观锁。我们来看看:

悲观锁:认为线程安全问题一定会发生的,因此在操作数据之前,先获取锁,确保线程串行执行。

例如:Synchronized、Lock都是悲观锁。

因为让线程串行了,所以,悲观锁的效率低。

乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据的时候,判断有没有其他线程对数据做了修改。

如果没有修改,则认为是安全的,自己才更新数据;

如果已经被其他线程修改了说明发生了安全问题,此时可以重试或者抛出异常。

乐观锁的关键是判断之前查询得到的数据是否被修改过,常见的方式有两种:

1:版本号法

每当数据被修改,版本号就+1

我们来看看还是上面多线程抢优惠券情况下,版本号法执行流程:

线程1,执行扣除库存后,版本号+1后,就是2。如下图:

我们再来看看线程2执行流程:

版本号法优化:

我们从上图的逻辑中可以看出,在查询库存的时候,同时把版本号也查询出来,在更新的时候,库存-1,版本号也-1.where条件是版本号=查询库存的时候的版本号。我们只需要观察版本号和库存关系:同时查询出来、同时-1.那么,我们可不可以优化下,只使用一个字段来实现呢?答案是可以的:我们就把库存作为版本号概念,在更新的时候,where 条件中的version=查询库存的时候的版本号这个条件换成:where id =10 and stock = #{stock}。这样就剩下一个字段。

其实,上面这个思路就是大名鼎鼎的CAS思想,也就是第二种常见的方案。

2:CAS法

我们来看看CAS法逻辑图:

知识小扩展:

针对CAS中自旋压力过大,我们可以使用Longadder这个类来解决。在Java8中提供了一个对AtomicLong改进的一个类:LongAdder.大量线程并发更新一个原子性的时候,天然的问题就是自旋,会导致并发性能问题,当然这个也比我们直接使用sync来得好。所以可以利用这个类,LongAdder来进行优化。

如果获取某个值,则会对cell和base值进行递增,最后返回一个完整的值。

好了,秒杀超卖问题分析完了,解决方案也有了。那么接下来,我们就来实现解决超卖问题的代码。

其实,我们只需要修改扣减库存的逻辑,只添加一个where条件即可。如下图:

修改完成之后,我们再使用JMeter模拟200个用户去秒杀抢优惠券。运行结果:

异常竟然是89.9%。比没修改前,异常率还增加了。我们再来看看结果树情况:

一上来,就库存不足了。我们z看看数据库中,库存情况:

优惠券领券了21张。为什么会出现这种情况呢?200个人来抢购100张优惠券,竟然才有21个人抢到了。这个肯定不是我们想要的结果。这个是什么原因导致的呢?其实这个就涉及到了CAS乐观锁的弊端了。我们重新分析:

如上图,假设刚开始,就有3个线程同时抢夺资源,其中线程3先执行了更新,将100更新成了99,然后线程1和线程2,就更新失败了。三个线程,只有一个更新成功了,就如同,我们在结果树上看到的一样。如下图:

那么失败的这两个,就抢不到了,导致我们库存有剩余。但是,咱们从真正的业务上来说,抢不到的依据是库存等于0,才算抢不到,而不是说我抢到之后,在修改的时候,别人不能够在抢成功了。我们线程1和线程2在抢的时候,库存还剩余99啊,这个是不符合实际业务的。这就是乐观锁方案的问题所在--成功率太低了。那么,我们对乐观锁法进行改进。

乐观锁法弊端改进

改进思路:在更新的时候,不再判断库存是否等于我手里的库存值。而是判断,库存是否大于0.如果大于,就执行扣除操作。

修改扣除库存相关代码:

修改完成之后,我们再使用JMeter模拟200个用户去秒杀抢优惠券。运行结果:

从上图中,我们看到异常率是50%。符合我们的预期。我们看看数据库中的库存:

订单表中也是100条订单。商品没有超卖,订单数量也正常。这样是不是很完美解决了超卖问题?

答案:否。我们可以看到,这个方案,直接是由数据库来处理的。我们知道,数据库本来就是比较宝贵的资源,在高并发情况下,这种方案,肯定是不行的。我们继续往下学习。

小总结

我们来总结下超卖这样线程安全问题,解决方案有哪些?

下一篇预告:

在下一篇中咱们将实现另外一个功能:一人一单的功能。在下一篇中,您将有如下收获:

1:悲观锁、乐观锁的使用场景;

2:synchronized关键字,在不同位置,锁的颗粒度是不同的,怎么优化呢;

3:toString方法之后,不能保证唯一,如果要保证唯一,需要在调用String的intern方法;

4:对spring事务有更深入了解-解决spring事务失效一种情况;

5:spring boot怎么开启对AspectJ的支持。

Redis实战11-实现优惠券秒杀下单的更多相关文章

  1. Redis 实战 —— 11. 实现简单的社交网站

    简介 前面介绍了广告定向的实现,它是一个查询密集型 (query-intensive) 程序,所以每个发给它的请求都会引起大量计算.本文将实现一个简单的社交网站,则会尽可能地减少用户在查看页面时系统所 ...

  2. Redis分布式锁实现简单秒杀功能

    这版秒杀只是解决瞬间访问过高服务器压力过大,请求速度变慢,大大消耗服务器性能的问题. 主要就是在高并发秒杀的场景下,很多人访问时并没有拿到锁,所以直接跳过了.这样就处理了多线程并发问题的同时也保证了服 ...

  3. Redis实战

    大约一年多前,公司同事开始使用Redis,不清楚是配置,还是版本的问题,当时的Redis经常在使用一段时间后,连接爆满且不释放.印象中,Redis 2.4.8以下的版本由于设计上的主从库同步问题,就会 ...

  4. Redis实战之Redis + Jedis

    用Memcached,对于缓存对象大小有要求,单个对象不得大于1MB,且不支持复杂的数据类型,譬如SET 等.基于这些限制,有必要考虑Redis! 相关链接: Redis实战 Redis实战之Redi ...

  5. Redis实战之征服 Redis + Jedis + Spring (一)

    Redis + Jedis + Spring (一)—— 配置&常规操作(GET SET DEL)接着需要快速的调研下基于Spring框架下的Redis操作. 相关链接: Redis实战 Re ...

  6. Redis实战之Redis + Jedis[转]

    http://blog.csdn.net/it_man/article/details/9730605 2013-08-03 11:01 1786人阅读 评论(0) 收藏 举报   目录(?)[-] ...

  7. SpringMVC集成rabbitmq:优化秒杀下单环节

    前言 上一篇在springboot中基于自动配置集成了rabbitmq.那么回到最初的话题中就是想在秒杀下单环节增加排队机制,从而达到限流的目的. 优化秒杀下单流程 之前是在控制器里拿到客户端请求后直 ...

  8. 【SpringBoot】整合Redis实战

    ========================9.SpringBoot2.x整合Redis实战 ================================ 1.分布式缓存Redis介绍 简介: ...

  9. SpringBoot2.x整合Redis实战 4节课

    1.分布式缓存Redis介绍      简介:讲解为什么要用缓存和介绍什么是Redis,新手练习工具 1.redis官网 https://redis.io/download          2.新手 ...

  10. 【高并发】Redis如何助力高并发秒杀系统,看完这篇我彻底懂了!!

    写在前面 之前,我们在<[高并发]高并发秒杀系统架构解密,不是所有的秒杀都是秒杀!>一文中,详细讲解了高并发秒杀系统的架构设计,其中,我们介绍了可以使用Redis存储秒杀商品的库存数量.很 ...

随机推荐

  1. power bi 如何删除敏感度标签

    经验证,此方法不够彻底,我的office excel打开后还是要添加敏感度标签,即使我把敏感度标签删掉也不行. 当我把创建敏感度标签的管理员账户删掉之后,虽然打开excel还是会显示敏感度标签,但是已 ...

  2. 配置hive环境步骤(zookeeper高可用集群已搭建)

    安装mysql:1. 检查当前环境是否安装mysql服务(命令:rpm -qa | grep -i mysql)2. 卸载自带的mysql3. 卸载软件:rpm -e --nodeps mysql-l ...

  3. 深度长文解析SpringWebFlux响应式框架15个核心组件源码

    Spring WebFlux 介绍 Spring WebFlux 是 Spring Framework 5.0 版本引入的一个响应式 Web 框架,它与 Spring MVC 并存,提供了一种全新的编 ...

  4. Mysql的Innodb和MyISAM引擎的区别

    区别项 Innodb MyISAM  事务  支持  不支持 锁粒度  行锁,适合高并发 表锁,不适合高并发  是否默认  默认  非默认  支持外键  支持外键  不支持  适合场景  读写均衡,写 ...

  5. 在Django中,多数据操作,你可以编写测试来查询另一个数据库服务器中的数据,并将结果导入当前Django项目的数据库表中

    在Django中,你可以编写测试来查询另一个数据库服务器中的数据,并将结果导入当前Django项目的数据库表中.下面是一个简单的示例: 假设你有一个Django应用程序,名为myapp,并且你希望从另 ...

  6. 从输入URL到页面展示到底发生了什么?--01

    在浏览器中输入一个URL并按下回车键后,会发生一系列复杂且有条不紊的步骤,从请求服务器到最终页面展示在你的屏幕上.这个过程可以分为以下几个关键步骤: URL 解析 DNS 查询 TCP 连接 发送 H ...

  7. 张高兴的 MicroPython 入门指南:(三)使用串口通信

    目录 什么是串口 使用方法 使用板载串口相互通信 硬件需求 电路 代码 使用板载的 USB 串口 参考 什么是串口 串口是串行接口的简称,这是一个非常大的概念,在嵌入式中串口通常指 UART(Univ ...

  8. 写写Redis十大类型hyperloglog(基数统计)的常用命令

    hyperloglog处理问题的关键所在和bitmap差不多,都是为了减少对sql的写操作,提高性能,用于基数统计的算法.基数就是一种数据集,用于收集去重后内容的数量.会有0.81%的误差 hyper ...

  9. Win11不在C盘安装WSL2(Linux环境),安装Nvidia驱动和默认使用Win11的网络代理服务

    众所周知,WSL 2 为 Windows 用户提供了一个强大.高效且灵活的 Linux 环境,特别适合开发者使用.它结合了 Windows 和 Linux 的优点,为用户提供了更加全面和高效的工作环境 ...

  10. 【C3】03 如何构建

    既然你已经了解了什么是CSS,以及使用CSS的基础知识,是时候更深入的了解该语言本身的结构了. 我们已经见过了本页讨论的很多概念:如果在之后对某些概念感到困惑的话,可以返回至此进行回顾. 前置知识 在 ...