抢购场景下的高并发问题的简单讲解和分析

此文章涉及的技术点

·SpringBoot
·Redis
·nginx https://blog.****.net/weixin_44012722/article/details/105466791
·docker (如果不会不影响此文章的学习) https://blog.****.net/weixin_44012722/article/details/105468637

先让我们来思考一个问题 当我是小白的时候老师让我写一个商品抢购的时候我的想法是
当一个请求过来 就直接在数据库库存减一 这时候就会发生并发问题 结果过程如下

pom文件的依赖我先说我用了什么

spring-boot-starter-web

spring-boot-starter-data-redis

commons-lang3

普通的抢购写法

Controller

抢购场景下的高并发问题的简单讲解和分析

Service

抢购场景下的高并发问题的简单讲解和分析

Service实现类

抢购场景下的高并发问题的简单讲解和分析

结果

让我们来看看结果 这里我使用一个jmeter的工具 它是用来测试软件并发的,我们可以用它来模拟同一时间访问抢购的场景
具体jmeter怎么下载使用 请看我另一篇博客介绍
接下来我们用它模拟抢购的场景

抢购场景下的高并发问题的简单讲解和分析

抢购场景下的高并发问题的简单讲解和分析

这时候会发现97出现多次,这是什么问题导致,就是并发问题所导致的,因为redis是单线程是线程安全,但是你的逻辑代码不是线程安全的,所以当
同一时间出现多个请求拿到相同的库存数据,减一再放入redis数据库,这时候就是出现多个相同的数据的重复覆盖 这就会造成一个商品多卖的问题
那我们怎么解决呢? 很简单加上一个synchronize关键字的锁 一次只能一个线程执行 这样就可以解决 这样的线程并发问题

synchronize关键字的抢购写法

Controller

抢购场景下的高并发问题的简单讲解和分析

Service

抢购场景下的高并发问题的简单讲解和分析

Service实现类

抢购场景下的高并发问题的简单讲解和分析

结果

再让我们用jmeter测试一下结果

抢购场景下的高并发问题的简单讲解和分析
抢购场景下的高并发问题的简单讲解和分析

这时候我们可以看到并发问题就没有出现了 因为我们用synchronize关键字锁住我们的逻辑代码 一次只能一个线程拿到对象锁
进入逻辑代码中执行 执行解释后归还钥匙到锁池中 让其他线程争夺

这时候问题又来了 我们解决了并发问题但是这样写代码 给分布式的项目埋下一个坑 理由是当我们发布单个springboot应用时不会发生并发问题,但

当我们发布分布式springboot应用时并发问题又会出现了 接下来我将演示分布式带来的并发问题

首先 我们要有一台安装Nginx的linux服务器(推荐阿里云学生服务器 每月 9 块钱 白给) nginx的了解和安装详细讲解在这 https://blog.****.net/weixin_44012722/article/details/105466791

可通过docker容器技术去安装nginx,如果对docker技术不了解 可以先看看我这个入入门 https://blog.****.net/weixin_44012722/article/details/105468637

如果以上两种都不具备条件的 我们采用安装nginx到window 这次我就使用这一种方式

到nginx官网下载 http://nginx.org/en/download.html 解压修改配置文件nginx.conf即可使用

下面是我的配置文件的添加修改的内容

抢购场景下的高并发问题的简单讲解和分析

在nginx的文件目录下的终端命令窗口输入nginx.exe打开nginx

然后我们把我们的springboot项目再复制一份到我们父工程中改application.yml中的服务器端口 同时启动两个springboot项目

接下来我们开始用jmeter压力测试一下

抢购场景下的高并发问题的简单讲解和分析

让我们看看两个springboot的控制台 很容易看到有重复的抢购,分布式下synchronize的并发问题就出现了 原因是synchronize关键字只能锁住本项目的逻辑代码 并

锁不住另一个项目的逻辑代码 这个还是很容易理解的 所以有了下面的解决方案抢购场景下的高并发问题的简单讲解和分析

抢购场景下的高并发问题的简单讲解和分析

redis简单锁的抢购写法

redis简单锁的实现
这里try catch finally的原因是当拿到锁后面的逻辑代码出现错误时 如果没有trycatch就会发生死锁
而这里的给锁设置过期时间是防止 程序在拿到锁后还没有释放锁时就服务器宕机或者被杀死进程 发生的死锁情况

抢购场景下的高并发问题的简单讲解和分析

修改添加到两个项目中我们再进行Jmeter测试一下

抢购场景下的高并发问题的简单讲解和分析

这时候这个分布式下的并发问题就解决了 但随之又来了一个问题就在超并发的场景下 数据库压力服务器压力导致 执行时间边长

假设我购买个商品或者其他操作 这个过程在拿到锁之后要执行15s 这时候当第10s时锁因为到达过期时间而被释放掉锁 当到11s第二个线程就可以拿到锁开始执行

当到15s时第一个线程操作完毕释放掉锁 这时候释放的锁是第二个线程的锁 以此类推第三个线程 … 就会发生 锁永久失效

在这里又有一个解决此类事情的方案

给锁的value设置一个uuid 这样子就不会出现释放掉其他线程的锁 但是一旦执行时间超过10s 锁还是会自动释放 所以启动一个守护线程每10秒去检测锁还在不在,再的话续约

锁的过期时间,一旦主线程结束 守护线程也会跟着结束

这里面用到的守护进程 用的是commons-lang3依赖的BasicThreadFactory创建的线程池

根据阿里巴巴的开发手册 Timer不推荐使用具体原因是因为Timer不支持并发编程,而阿里巴巴创建ScheduledExecutorService推荐手动创建线程池具体原因参考以下网址,总共三种方法

https://blog.****.net/qq_26869339/article/details/90141656

抢购场景下的高并发问题的简单讲解和分析

总结

总的来说解决了大部分的并发问题,可能有人担心内存开销,注意锁只有一把所以守护线程只有一个主线程消亡守护线程也会随着死掉所以不必担心内存开销

但是又有个问题就是我每一个并发编程我都要这么去编写的话 花费的精力会很大而且容易出现意想不到的bug

所以redisson这个框架出现就是帮我们简便解决这类问题

下面我们进入redisson的学习