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

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. 【Hive报错】在hue上执行自定义的hive函数报错 Error while compiling statement:FAILED:SemanticException [Error 10011]: Invalid function default.sqlServerdes

    在 Hive客户端中使用自定义创建UDF函数时,报"ERROR 10011","invalid function"的异常: 在Hive上自定义创建了一个函数,在 ...

  2. SQL Server大量插入 Java

    在Java中向数据库执行大量插入操作,通常需要考虑性能和效率.对于大量数据的插入,有几种方法可以提高性能,比如使用批处理(Batch Insert).JDBC的批处理API.或者使用SQL Serve ...

  3. 解决方案 | 在 Tkinter 中导入 pywinauto/pyautogui 时窗口大小发生变化

    上面问题也可以换一个说法,pywinauto/pyautogui 时改变了tkinter的原有的窗口大小.这个问题困扰了我好几天而且网上有这样的问题但是并没有答案,今天摸索出答案给大家分享下.解决方法 ...

  4. linux mysql 允许进行远程连接 比如 navicat

    出于安全方面考虑默认只允许本机(localhost, 127.0.0.1)来连接访问.所以开启远程访问权限.登录mysqlmysql -uroot -pxxxxxx 1:GRANT ALL PRIVI ...

  5. [oeasy]python0015_键盘改造_将esc和capslock对调_hjkl_移动_双手正位

    键盘改造 回忆上次内容 上次练习了复制粘贴 按键 作用 <kbd>y</kbd><kbd>y</kbd> 复制光标行代码 到剪贴板 <kbd> ...

  6. C语言数据类型转换(自动类型转换+强制类型转换)

    自动类型转换 1) 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如: float f = 100; int n = f; f 是 float 类型的数据,需要先转换为 int 类 ...

  7. 题解:P10520 [XJTUPC2024] 榕树之心

    题意 给予你 \(x\) 和 \(y\),将 \(x,y\) 代入. 前面的一大堆都无用. 思路 将题目中的公式代入即可. 代码 #include<bits/stdc++.h> using ...

  8. 题解:AT_xmascon21_b Bad Mood

    AT_xmascon21_b Bad Mood 题意 给定你一个 \(n\times m\) 的矩形. 以一条对角线为基础上,制作一个无向图,该图的顶点对应于格子的共有 \((m+1) \times ...

  9. 图灵课堂netty 仿微信开发

    通信的图文示例 以下是需要实现的前端界面, 准备工作:开始实现前需要技术关健字解释 第一步,这儿直接建一个maven 项目 就好,只要是可能用maven 管理包的环境就行,课程使用的版本是 java ...

  10. Aic 应用开发基础一(概念与场景)

    Agi通用人工智能应用概念与场景 大家看到,自美国OpenAI主导的GPT发布以来,全球科技领域掀起了革命性的浪潮.比如最近看到的aigc 人工绘图,智能机器人等行业,很多新概念掘起, 随着人工智能技 ...