- 浏览: 104027 次
文章分类
最新评论
-
kaizi1992:
嗯嗯。是的。@cs6641468 谢谢!希望多提意见
spring boot框架学习之重要注解3注解方式读取外部资源配置文件 -
cs6641468:
1. Spring Boot引入文件配置,优先考虑推荐的@Co ...
spring boot框架学习之重要注解3注解方式读取外部资源配置文件
Redis实战11-实现优惠券秒杀下单
- 博客分类:
- 凯哥Java
本篇,咱们来实现优惠券秒杀下单功能。通过本篇学习,我们将会有如下收获:
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的支持。
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的支持。
发表评论
-
【架构设计】多级缓存:应用案例与问题解决策略
2024-09-17 09:23 172【架构设计】多级缓存:应用案例与问题解决策略 ... -
Nginx使用篇:实现负载均衡、限流与动静分离
2024-09-16 09:23 227Nginx使用篇:实现负 ... -
怎么在Windows操作系统部署阿里开源版通义千问(Qwen2)
2024-09-01 22:18 639怎么在Windows操作系统部署阿里开源版通义千问(Qwen ... -
Redis实战-session共享之修改登录拦截器
2023-02-11 15:36 420在上一篇中Redis实战之session共享,我们知道了通过 ... -
Redis实战之session共享
2023-02-06 14:24 529当线上集群时候,会出现session共享问题。 当线上集群 ... -
分库分表后全局唯一ID的四种生成策略对比
2023-02-02 09:00 441分库分表之后,ID主键如何处理? 当业务量大的时候,数据库 ... -
Redis快速入门
2023-01-31 08:30 456Redis快速入门,分两个客户端:Jedis和SpringD ... -
Redis实战9-全局唯一ID
2023-01-29 09:45 531发布优惠券的时候,每个店铺都可以发布优惠券,当用户抢购的时候 ... -
【图文教程】Centos单机安装Redis
2023-01-13 21:49 6261.1.安装Redis依赖 Redis是基于C语言编写 ... -
docker系列教程:docker图形化工具安装及docker系列教程总结
2022-12-31 21:00 536通过前面的学习,我们已经掌握了docker-compose容 ... -
docker高级篇-docker-compose容器编排介绍及实战
2022-12-30 09:42 0Docker-compose是什么?能干嘛?解决了哪些痛点? ... -
Docker网络下-自定义网络实战
2022-12-29 10:02 522通过前面两篇的学习,我们对docker网络及四大网络类型都了 ... -
Docker网络下-自定义网络实战
2022-12-29 09:54 491通过前面两篇的学习,我们对docker网络及四大网络类型都了 ... -
Docker网络中篇-docker网络的四种类型
2022-12-28 09:51 480通过上一篇学习,我们对docker网络有了初步的了解。本篇, ... -
Docker网络上篇-网络介绍
2022-12-27 10:28 0通过前面的学习,我们已经可以把自己写的微服务项目通过dock ... -
docker高级篇:实战-自己开发的微服务怎么在docker上面运行?
2022-12-26 21:06 473通过前面的一系列学习,我们已经知道怎么制作dockerfil ... -
docker的虚悬镜像是什么?
2022-12-25 13:39 545虚悬镜像是什么? 答:仓库名、标签都是<none&g ... -
docker高级篇第三章-dockerfile案例之制作自己的centos镜像
2022-12-24 15:27 498在上一篇文章中《Dockerfile介绍及常用保留指令》,我 ... -
Dockerfile介绍及常用保留指令
2022-12-23 11:03 382从本文开始,咱们将介绍docker的另外一个技术点:dock ... -
Docker高级:Redis集群实战!4主4从缩容到3主3从,怎么处理?
2022-12-22 09:10 328在上一篇,我们学会了redis集群的扩容。从3主3从扩容到4 ...
相关推荐
redis-stack-server-7.2.0-v9.arm64.snap redis-stack-server-7.2.0-v9.bionic.arm64.tar.gz redis-stack-server-7.2.0-v9.bionic.x86_64.tar.gz redis-stack-server-7.2.0-v9.bullseye.x86_64.tar.gz redis-stack-...
配合 `redis-shake` 工具,可以实现更全面的数据迁移和同步。`redis-shake` 是一个用于在不同 Redis 版本之间或者不同 Redis 实例间进行数据迁移的工具,支持 RDB 和 AOF 格式,同时也可以实时同步数据。 在使用 `...
标题中的"php_redis-2.2.7-5.6-nts-vc11-x86"和"php_redis-2.2.5-5.6-ts-vc11-x86"指的是PHP的Redis扩展的不同版本,适用于PHP 5.6。这里的"2.2.7"和"2.2.5"是Redis扩展的版本号,"5.6"对应的是PHP的版本号。"nts"和...
在压缩包中的两个jar文件,`tomcat8-redis-session-manager-2.0.0.jar`和`tomcat7-redis-session-manager-2.0.0.jar`,分别对应Tomcat8和Tomcat7的实现。这两个jar包包含了实现session与Redis交互的所有必要组件,如...
`tomcat-redis-session-manager`就是这样一款解决方案,它将Tomcat的session管理与Redis相结合,实现了跨服务器的session共享。 首先,让我们理解`tomcat-redis-session-manager`的核心概念。这是一个开源项目,它...
"tomcat-redis-session-manager"是一个解决方案,它将用户的Session信息存储在Redis缓存服务器中,从而实现跨服务器的Session共享。本篇文章将深入探讨这个话题,包括它的原理、配置以及实际应用。 **一、Session...
【标题】"tomcat-redis-session-manager包集合下载(tomcat8)"涉及的主要知识点是将Redis集成到Tomcat中管理会话(session),以提高Web应用的性能和可扩展性。 【描述】中提到的"所需的tomcat-redis-session-...
php_redis-2.2.7-5.5-nts-vc11-x86.dll php_igbinary-1.2.1-5.5-ts-vc11-x86.pdb 解压缩后,将php_redis.dll和php_redis.pdb拷贝至php的ext目录下, 修改php.ini,在该文件中加入: ; php_redis extension=...
本文将深入探讨PHP的Redis扩展,特别是针对“php_redis-2.2.5-5.6-ts-vc11-x64”这一版本,它专为PHP 5.6版本、线程安全(TS)、Visual C++ 11编译器以及64位(x64)系统设计。 首先,我们来理解PHP Redis扩展的...
这里提到的"session 共享 tomcat-redis-session-manager"就是一种解决方案,它利用Redis作为中央存储来实现Tomcat容器中的Session共享。 首先,我们来看看标题所提及的"session 共享 tomcat-redis-session-manager...
此外,`redis-py-cluster`支持异步模式,可以与`asyncio`或`concurrent.futures`一起使用,以实现非阻塞I/O。 `redis-py-cluster`还提供了一些高级功能,如批量操作、发布/订阅以及pipeline,这些都是Redis的基本...
这里的 "redis-stack-server-6.2.6-v7.rhel7.x86-64.tar.gz" 文件是一个针对 Red Hat Enterprise Linux 7 (RHEL7) 平台的 64 位版本的 Redis Stack 6.2.6 包。这个压缩包包含了运行 Redis Stack 所需的所有组件,...
- 解压"redis-windows-7.2.4.zip",找到`redis-server.exe`启动文件。 - 运行`redis-server.exe`,默认情况下,Redis监听6379端口。 - 可以通过配置文件`redis.windows.conf`修改默认设置,如端口、内存限制、...
"Redis++使用说明,windows下编译Redis-Plus-Plus" 在这篇文章中,我们将详细介绍如何在Windows平台下编译Redis++,包括编译hiredis.lib和Win32_Interop.lib静态库文件的过程,然后安装Cmake并编译Redis++,最后...
1. 下载源码包:`redis-2.8.13.tar.gz` 是Redis的源码包,解压后进行编译和安装。 2. 解压:`tar -zxvf redis-2.8.13.tar.gz` 3. 编译:`cd redis-2.8.13`,然后`make` 4. 安装:`sudo make install` 5. 启动Redis...
压缩文件包括tomcat-redis-session-manager-master-2.0.0.jar、jedis-2.7.3.jar、commons-pool2-2.3.jar三个jar包使用方法请参照https://github.com/jcoleman/tomcat-redis-session-manager。apache-tomcat-8.5.33....
Redis的核心组件包括`redis-server.exe`(服务器进程)、`redis-cli.exe`(命令行客户端)以及`redis-benchmark.exe`(性能测试工具)等。用户需要通过`redis-server.exe`启动服务,并通过`redis-cli.exe`进行交互式...
本文将深度剖析`Tomcat-Redis-Session-Manager`的源码,揭示其如何实现Tomcat与Redis之间的会话同步。 首先,我们来看`Tomcat-Redis-Session-Manager`的核心功能:它将Tomcat默认的内存会话管理替换为基于Redis的...
在Windows环境下安装Redis,可以借助于提供的压缩包"redis-windows-7.2.5.zip"进行。以下是关于Redis及其在Windows上的安装和使用的详细知识: 1. **Redis特性** - **键值对存储**:Redis的核心是键值对模型,其中...
该压缩包“php_redis-2.2.7-5.6-ts-vc11-x64.zip.zip”包含了适用于64位Windows环境的PHP Redis扩展,具体版本为2.2.7,它针对PHP 5.6版本进行了编译,并且是线程安全(TS)版本,使用了Visual C++ 11编译器(VC11)...