该博文主要记录学习IT老齐的<<千万级电商高并发与秒杀实战项目>>课程的学习笔记。
代码1中的项目是通过SpringBoot编写的一个B/S架构项目,前端采用freemarker模板引擎,功能非常简单就是通过Mybatis建立和数据库的映射,从数据库中获取前端网页需要展示的数据,并和模板引擎的数据进行绑定,实现网页的动态展示。
项目结构如下:
通过游览器访问 http://localhost:8969/goods?gid=1333
地址,运行效果如下:
上面的访问一切都正常,如果该网站同时有100人进行访问,每人访问100次会怎么样呢,我们通过Apache JMeter进行模拟:
启动项目项目启动后,启动Apache JMeter进行访问:
如下图所示,100个用户同时访问时最长时间为1s。(最长时间为1s表示100个用户中会有一个用户访问时需要等待1s钟才能够获取网页的信息)
如果网站有1万人访问,每人访问1次,又会怎么样?
通过下面测试的结果,我们可以看出最长时间达到了23.669s,并且吞吐为270.8,意味着某个用户访问网站时要等待23.669s才能够访问到该网站,并且吞吐率270.8实在是太低了,这是一件非常可怕的问题。
像一些购物的商品信息,上架后基本上不怎么进行修改,修改的往往是一些局部的数据,比如价格这些,我们可以把这些不怎么进行变化的用户又经常需要进行访问的数据可以放到缓存中去,当缓存中有用户需要访问的数据时就从缓存中取数据,如果缓存中没有数据就从数据库中获取,并更新数据到缓存中,这样可以大大的减少对数据库服务器的访问。
代码2在代码1的基础上引入了缓存技术Redis,使用的是SpringBoot中的SpringCache声明式缓存。
具体的步骤如下:
导入缓存的依赖:
在application.yaml中配置redis:
在启动类中使用@EnableCaching注解开启声明式缓存
在业务层的业务代码中使用 @Cacheable 注解:
启动Reedis服务器:
在代码2中的Redis文件下有一个redis-server.exe,点击运行即可:
启动项目:
再次进行测试:
第一次运行缓存中没有数据,Max达到了79.3049秒,吞吐315.2:
缓存中有数据了:
然后再次进行访问,会发现max只有1.222秒了,并且吞吐达到1318:
备注:如果应用的用户访问量比较大的话,我们可以将一些用户不经常做操作的数据放入到缓存中,这样就可以大大的提高访问效率。
除了采用缓存技术以外.,我们可以通过网页静态化处理的方式提高访问效率,就是将用户需要访问的网页生成静态网页,用户访问静态网页少了网页和数据的交互的过程,访问速度会非常的快。
代码3就是在代码2的基础上多了一个生成静态网页的方法:
运行项目:
生成静态网页(先删除缓存中的数据):
生成成功(需要在该目录下放入网站的样式文件):
使用Nginx进行映射,当用户访问时访问静态网页:
上面的方式虽然可以生成静态网页文件但是不够智能化,需要手动进行操作,这样非常的不方便,我们可以通过资源调度的方式,按时执行,并添加相应的条件,比如说用户修改数据后5分钟后的网页文件进行静态化生成。
代码4在代码3的基础上增加了一个Scheduled进行按时资源调度:
StaticTask的代码如下:
其中cron表达式表示每5分钟执行一次,其中的goodsService.getLast5M();返回的是最近5分钟的数据集合,具体的SQL映射如下:
在数据库中修改一下goods数据表中的last_update_time,增加几组数据:
启动应用:
等待5分钟:
生成成功:
前二种静态化方式都是直接生成静态化文件,不涉及到一些动态的数据,如果一个网页中既有不经常进行修改的数据又有需要进行频繁修改的数据,比如商品中的评论,商品的数据不怎么进行修改,但是商品的评论会频繁的进行更新。这时候我们可以对不进行修改的数据和前端一起生成静态网页,需要进行修改的数据通过前端发送Ajax请求获取数据,这样就可以实现动静数据的分离。
代码5在代码4的基础上对前端网页进行了简单的修改,并新增了一个获取评论数据的后端接口:
后端响应前端Ajax的请求:
获取数据库中的评论数据的SQL映射语句如下:
动态数据并不是用户直接获取,而是通过nginx服务器代理获取:
启动应用程序,并通过static_all生成所有的静态文件:
生成成功,并且生成的静态网页是包括静态数据和动态数据(执行前需要启动redis):
启动nginx服务器,访问http://localhost/goods/739.html:
动态数据评论通过前端发送的Ajax请求获取:
超卖问题就是一件商品本来只卖10件,但是由于并发问题卖出了20件,这就是我们所说的超卖问题。
代码6中有二个项目,其中seckill-sample为展示超卖问题和上锁方式解决超卖的项目,而babytun-seckill为通过redis方式解决超卖问题项目。
项目代码非常简单,在ProductDAO类中用static修饰的count为某个商品的库存情况,前端用户抢购的时候调用getProduct()方法较少count的数量:
用户通过业务层的getProduct()实现抢购:
运行项目:
用户通过访问http://localhost:8969/pr抢购商品:
执行10次,感觉一切都正常:
如果有10000个用户同时进行访问呢?
重启应用,启动JMeter进行测试:
观察控制台发现出现超卖:
在学习多线程的时候,我们知道这个原因就是执行时同一个状态下(如:count=10)的多个线程同时执行了减1操作,没有在前一个状态改变后(10减1后的状态)再进行减1操作,
在多线程中我们可以使用同步锁的方式解决该问题,就是对count这个资源进行上锁来保持状态的一致性,等上一个修改后再进行修改避免在同一个状态下进行修改,上锁后的代码:
再次重启应用,并使用jMeter再次进行测试:
观察控制台发现问题解决了:
除了上锁解决超卖外,我们还可以通过缓存技术解决超卖,执行过程就是获取用户需要进行售卖的商品并分别把商品一件一件的放入到redis缓存中的list中去,用户抢购的时候再通过list弹出数据,并将抢购商品的用户的信息放入到redis缓存中的set中去这样我们就可以利用redis通过单线程处理的方式解决并发问题,而且redis对高并发,分布式有很好的支持。
抢购商品的数据表:
步骤如下:
业务层中的processSeckill方法的逻辑很简单,首先判断商品是否可以进行抢购,不能够抢购的条件有抢购商品不存在,商品秒杀还没开始,商品秒杀活动已经结束,用户已经进行了抢购;如果都不满足上面的条件,就让存放在缓存中的list集合中的数据弹出,如果弹出成功表示用户已经抢购了此商品,并将用户的信息存放在缓存中的set集合中;如果弹出数据失败说明缓存中的list集合已经没有数据了,表示商品已经被抢完了。
list中的数据如何存放的呢,其实是通过资源调度实现每5秒中判断一下数据库中商品的抢购信息是否开始,如果开始就把商品一件一件的存放到List集合中:
获取抢购商品列表数据的SQL映射如下:
启动应用:
修改数据库中抢购商品中的结束时间和status模拟商品准备开始上线抢购:
修改后,控制台就会输出描述活动已启动:
redis缓存中就会多了这10件抢购商品的list集合信息:
网页访问 http://localhost:8969/secKill.html抢购商品 :
立即抢购按钮绑定了点击事件如下,会向后端发送一个Ajax的请求:
点击立即抢购后弹出抢购情况:
此时redis缓存数据中就会多了一个用户抢购商品的用户set集合数据,下图表明用户u03抢购了抢购id=1的商品:
并且存放商品的list集合中就会少一条商品信息:
当u03用户再次点击抢购时就会出现用户已经抢购的提示:
下面通过Jmeter模拟万人访问的情况:
首先添加一个CSV的数据配置文件,存放在代码6中的userid.csv:
启动JMeter进行模拟:
观察redis缓存中的数据会发现没有抢购商品的list集合了,只有一个存放抢购用户信息的set集合数据,数据里刚好存放了10记录,这10条记录就是1万人中抢购成功的用户:
在我的微信公众号后台回复 并发实战
就可以获取本篇博文相关的源代码了,如果有什么疑问后台给为留言,我看见会第一时间回复你的。