基于springboot实现秒杀电商项目

前言 本文主要就电商秒杀系统所涉及的相关技术进行探究,相关Demo地址如下: 个人实现版本:https://github,com/MrSorrow/seckill Github Star较高的版本

本文包含相关资料包-----> 点击直达获取<-------

前言

本文主要就电商秒杀系统所涉及的相关技术进行探究,相关Demo地址如下:

  • 个人实现版本:https://github.com/MrSorrow/seckill

  • Github Star较高的版本,第一版也是基于慕课网的:https://github.com/qiurunze123/miaosha

本Demo实现了电商项目的秒杀功能,主要内容包含了用户登录、浏览商品、秒杀抢购、创建订单等功能,着重解决秒杀系统的并发问题。项目利用JMeter工具进行压力测试,着重对比采用缓存、消息队列等手段对于提高系统响应速度并发能力的效果。

项目视频参考地址:https://www.bilibili.com/video/av50818180/

一、快速开始

「环境准备」

安装Docker;

  1. 安装Docker;
  2. 安装MySQL的Docker环境 (注意设置容器时区和MySQL时区);

sh docker pull mysql:5.7.23 docker run --name e3-mall-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234 -e TZ=Asia/Shanghai -d mysql:5.7.23 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-time_zone='+8:00'

3.安装Redis的Docker环境

sh docker pull redis:3.2 docker run -d -p 6379:6379 --name e3-mall-redis -e TZ=Asia/Shanghai redis:3.2

「导入项目」

  • 克隆本项目 https://github.com/MrSorrow/seckill

  • 项目采用Maven构建;

  • 通过IDEA直接以Maven方式导入项目即可。

「秒杀业务介绍」

假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有:

  1. 对现有网站业务造成冲击

秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。

解决方案:将秒杀系统独立部署,甚至 使用独立域名,使其与网站完全隔离

  1. 高并发下的应用、数据库负载

用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。

解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面, 页面内容静态化,用户请求不需要经过应用服务

  1. 突然增加的网络及服务器带宽

假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。

解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力, 需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽

  1. 直接下单

秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。

解决方案:为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在 下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到

  1. 如何控制秒杀商品页面购买按钮的点亮

购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还 是点亮,但是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。

解决方案:使用JavaScript脚本控制, 在秒杀商品静态页面中加入一个JavaScript文件引用 ,该JavaScript文件中包含 秒杀开始标志为否;当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是, 加入下单页面的URL及随机数参数(这个随机数只会产生一个,

这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。 即所有人看到的URL都是同一个,``服务器端可以用redis这种分布式缓存服务器来保存随机数) ,并被用户浏览器加载,控制秒杀商品页面的展示。 这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存

  1. 如何只允许第一个提交的订单被发送到订单子系统

由于最终能够成功秒杀到商品的用户只有一个,因此需要在用户提交订单时,检查是否已经有订单提交。如果已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。事实上,由于最终能够成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力, 可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户直接进入秒杀结束页面。

解决方案:假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。在还没有人提交订单成功之前,如果一台服务器已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面, 可以考虑通过cookie的方式来应对,符合一致性原则 。当然可以 采用最少连接的负载均衡算法 ,出现上述情况的概率大大降低。

  1. 如何进行下单前置检查

  2. 下单服务器检查本机已处理的下单请求数目:

如果超过10条,直接返回已结束页面给用户;

如果未超过10条,则用户可进入填写订单及确认页面;

  • 检查全局已提交订单数目:

已超过秒杀商品总数,返回已结束页面给用户;

未超过秒杀商品总数,提交到子订单系统;

  1. 秒杀一般是定时上架

该功能实现方式很多。不过目前比较好的方式是:提前设定好商品的上架时间,用户可以在前台看到该商品,但是无法点击“立即购买”的按钮。但是需要考虑的是, 有人可以绕过前端的限制,直接通过URL的方式发起购买 ,这就需要在前台商品页面,以及bug页面到后端的数据库,都要进行时钟同步。越在后端控制,安全性越高。

定时秒杀的话,就要避免卖家在秒杀前对商品做编辑带来的不可预期的影响。这种特殊的变更需要多方面评估。一般禁止编辑,如需变更,可以走数据订正多的流程。

  1. 减库存的操作

有两种选择,一种是 拍下减库存 另外一种是 付款减库存 ;目前采用的 “拍下减库存” 的方式,拍下就是一瞬间的事,对用户体验会好些。

  1. 库存会带来“超卖”的问题:售出数量多于库存数量

    由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。方案: 采用乐观锁

    update auction_auctions set quantity = #inQuantity#` where auction_id = #itemId# and quantity = #dbQuantity#

    还有一种方式,会更好些,叫做尝试扣减库存,扣减库存成功才会进行下单逻辑:

    1. update auction_auctions set
    2. quantity = quantity-#count#
    3. where auction_id = #itemId# and quantity >= #count#
  2. 秒杀器的应对

    秒杀器一般下单个购买及其迅速,根据购买记录可以甄别出一部分。可以通过校验码达到一定的方法,这就要求校验码足够安全,不被破解,采用的方式有: 秒杀专用验证码,电视公布验证码,秒杀答题

「秒杀架构原则」

  1. 尽量将请求拦截在系统上游

传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0】。

  1. 读多写少的常用多使用缓存

这是一个典型的 读多写少 的应用场景【一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%】, 非常适合使用缓存

「秒杀架构设计」

秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,

而不是商品详情等用户体验细节,因此秒杀系统的页面设计应尽可能简单。

商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击。

下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;

只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面。

要做一个这样的秒杀系统,业务会分为两个阶段, 第一个阶段是秒杀开始前某个时间到秒杀开始 , 这个阶段可以称之为 准备阶段

用户在准备阶段等待秒杀; 第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果 , 这个就称为 秒杀阶段 吧。

二、功能实现

I. 用户模块

「建立用户表」

sql CREATE TABLE miaosha_user ( id BIGINT(20) NOT NULL PRIMARY KEY COMMENT "用户ID,手机号码", nickname VARCHAR(255) NOT NULL COMMENT "昵称", password VARCHAR(32) DEFAULT NULL COMMENT "MD5(MD5(pass明文, 固定SALT), 随机SALT)", salt VARCHAR(10) DEFAULT NULL, head VARCHAR(128) DEFAULT NULL COMMENT "头像,云存储ID", register_date datetime DEFAULT NULL COMMENT "注册时间", last_login_date datetime DEFAULT NULL COMMENT "上一次登录时间", login_count int(11) DEFAULT '0' COMMENT "登录次数" ) ENGINE INNODB DEFAULT CHARSET=utf8mb4;

「两次MD5密码加密」

  • 客户端登录时避免明文密码在网络中传输,所以进行客户端第一次MD5;
  • MD5的密码传输至服务端时,需要随机生成salt进行二次MD5,保存salt和两次MD5结果至数据库中。

「分布式Session」

  • UUID方式生成Token,Redis保存Token-User的键值信息模拟Session;
  • 将Token写到Cookie中,设置Path为顶级域名之下。

「注册登录功能实现」

  • 封装服务端响应对象 guo.ping.seckill.result.ServerResponse 以及状态码消息对象 guo.ping.seckill.result.CodeMsg;
  • 利用JSR-303注解校验参数,并自定义手机号校验注解 guo.ping.seckill.validator.IsMobile 以及验证器 guo.ping.seckill.validator.IsMobileValidator;
  • 实现用户登录,批量注册用户逻辑;
  • 自定义方法参数解析器用于获取请求中包含的Token值,并查询Redis封装成User;

  • 具体代码参考 Commits on May 3, 2019 和 Commits on May 5, 2019

II. 商品模块

「建立商品与订单表」

数据库表包含普通商品表、普通订单表,以及专门用于秒杀商品的秒杀商品表和秒杀订单表;

普通商品表:

sql CREATE TABLE goods ( id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT "商品id", goods_name VARCHAR(16) DEFAULT NULL COMMENT "商品名称", goods_title VARCHAR(64) DEFAULT NULL COMMENT "商品标题", goods_img VARCHAR(64) DEFAULT NULL COMMENT "商品图片", goods_detail LONGTEXT COMMENT "商品详情", goods_price DECIMAL(10, 2) DEFAULT '0.00' COMMENT "商品单价", goods_stock INT(11) DEFAULT 0 COMMENT "-1表示没有限制" ) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

秒杀商品表:

sql CREATE TABLE miaosha_goods ( id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT "秒杀商品id", goods_id BIGINT(20) DEFAULT NULL COMMENT "商品id", miaosha_price DECIMAL(10, 2) DEFAULT '0.00' COMMENT "商品秒杀单价", stock_count INT(11) DEFAULT NULL COMMENT "库存数量", start_date DATETIME DEFAULT NULL COMMENT "秒杀开始时间", end_date DATETIME DEFAULT NULL COMMENT "秒杀结束时间" ) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

普通订单表:

sql CREATE TABLE order_info ( id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT "订单id", user_id BIGINT(20) DEFAULT NULL COMMENT "用户id", goods_id BIGINT(20) DEFAULT NULL COMMENT "商品id", goods_name VARCHAR(16) DEFAULT NULL COMMENT "反范式冗余的商品名称", goods_count INT(11) DEFAULT '0' COMMENT "商品数量", goods_price DECIMAL(10, 2) DEFAULT '0.00' COMMENT "商品单价", delivery_addr_id BIGINT(20) DEFAULT NULL COMMENT "收货地址id", order_channel TINYINT(4) DEFAULT '0' COMMENT "1-pc,2-android,3-ios", status TINYINT(4) DEFAULT '0' COMMENT "订单状态 0-新建未支付,1-已支付,2-已发货,3-已收货,4-已退款,5-已完成", create_date DATETIME DEFAULT NULL COMMENT "创建订单时间", pay_date DATETIME DEFAULT NULL COMMENT "支付时间" ) ENGINE=INNODB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

秒杀订单表:

sql CREATE TABLE miaosha_order ( id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT "秒杀订单id", user_id BIGINT(20) DEFAULT NULL COMMENT "用户id", order_id BIGINT(20) DEFAULT NULL COMMENT "订单id", goods_id BIGINT(20) DEFAULT NULL COMMENT "商品id" ) ENGINE=INNODB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

「展示商品列表及详情」

插入商品数据:

```sql INSERT INTO goods VALUES (1, "iPhoneX", "Apple iPhone X (A1865) 64GB 深空灰色 移动联通电信4G手机", "/img/iphonex.png", "Apple iPhone X (A1865) 64GB 深空灰色 移动联通电信4G手机【五月特惠】大屏性价比iPhone7Plus4199元,iPhone8低至3499元,iPhoneXR低至4799元!更多特价、优惠券,点此查看!选移动,享大流量,不换号购机!", 5999, 100); INSERT INTO goods VALUES (2, "华为 P30", "华为 HUAWEI P30 Pro 超感光徕卡四摄10倍混合变焦麒麟980芯片屏内指纹", "/img/p30.png", "华为 HUAWEI P30 Pro 超感光徕卡四摄10倍混合变焦麒麟980芯片屏内指纹 8GB+256GB极光色全网通版双4G手机!", 5988, 55);

INSERT INTO miaosha_goods VALUES (1, 1, 0.01, 4, NOW(), NOW()), (2, 2, 0.01, 9, NOW(), NOW()); ```

通过连接普通商品表和秒杀商品表,查询出秒杀商品的全部信息; 将所有商品展示在 goods_list 中,单个商品的全部信息展示在商品详情页 goods_detail 中。

「下单秒杀商品」

下单秒杀商品包含减少库存、创建订单两步,需要保证两步操作的原子性; 通过Spring声明式事务,保证减少库存、创建普通订单以及创建秒杀订单三步的原子性; 秒杀倒计时刷新由前端完成,后端仅仅在查看商品详情时返回一次计算的剩余时间即可,保证所有客户端的秒杀时间能够同步,以后端为准; 具体代码参考 Commits on May 6, 2019

三、 压测优化

I. 压力测试

利用JMeter对接口进行压测。压测文件保存在 preesure_test 文件夹下。

测试时MySQL、Redis连接池配置参数如下:

```yaml

druid pool config

druid: initial-size: 100 max-active: 1000 min-idle: 500 max-wait: 5000

redis pool config

redis: timeout: 5000 lettuce: pool: max-active: 2000 max-idle: 200 max-wait: 3 ```

由于设置的数据库连接池连接数量较大,需要对Docker-MySQL的最大连接数进行设置:

sh set GLOBAL max_connections=2000; set GLOBAL max_user_connections=1500;

未采取任何优化措施前,部分核心接口测试性能如下:

II. 性能优化

系统性能瓶颈在于MySQL的读写性能,所以主要利用各种技术减少对数据库层面的访问。

从缓存实现性能优化角度,用户的请求首先经过CDN优化,然后经过Nginx服务器缓存,再进入应用程序实现的页面级缓存,然后经过对象级缓存,最后可能经过数据库缓存。前面的层层缓存使得用户请求读写数据库的压力减小很多。 页面缓存优化技术包括:

  • 页面缓存+URL缓存+对象缓存
  • 页面静态化,前后端分离
  • 静态资源优化
  • CDN优化

以上是否通用接口的优化方式,针对秒杀场景,较短时间内大量的秒杀请求访问系统,根据其读多写少的业务特性,一旦商品秒杀结束,后续的秒杀请求一律返回秒杀失败即可,无需再访问缓存或数据库。 秒杀接口优化策略包括:

  • Redis预减库存减少对数据库的访问,只有库存数量的请求才会真正访问数据库进行下单;
  • 通过内存标记减少Redis访问。如果Redis预减库存已经减完,则之后的请求无需继续访问Redis继续预减;
  • Redis放行的请求先进入消息队列进行缓冲,异步请求数据库下单,增强用户体验
  • 其他提高系统并发能力的策略主要就是服务的水平复制与垂直拆分,构建分布式集群应用,通过多个节点分担压力,负载均衡等。

「模板引擎页面缓存」

页面缓存主要是指 Controller 方法返回的不再是包含数据的 Json,而是直接返回最终的HTML页面; 通过页面模板引擎将 Service 层得到的数据直接手动渲染页面,将整个页面缓存至Redis中; 缓存的过期时间可以设定较短,保证用户看到的数据不至于过旧。

「模板引擎URL缓存」

URL缓存主要是指针对不同的请求URL,缓存对应的HTML页面,基本和页面缓存相似; 通过以不同的URL为缓存的键来存储不同的页面; 同样缓存的淘汰策略也是自动过期。

「对象缓存」

对象缓存主要是指将 Service 层得到的数据存入Redis中,这样每次请求先进行缓存查询,如果命中则直接返回; 对象缓存一般没有变化则常驻于缓存,或设置较久的过期时间。数据在更新时,需要对缓存进行处理,往往先更新数据库,再删除缓存。

「页面静态化」

主要针对商品详情页,可以采取页面静态化的方式,直接生成静态HTML页面,保存在本地,额外用高性能HTTP服务器进行访问; 页面静态化方式可以使得前后端分离,一种方式是后端只提供RESTful API接口,前端编写半静态页面,通过Ajax获取数据再进行页面填充; 另一种页面静态化方式可以直接在添加商品时,利用页面模板引擎生成完全静态的商品详情网页,如果数据更改再重新生成新的页面。

「浏览器缓存」

通过浏览器端的缓存,可以降低对服务器的访问次数。Spring Boot对于静态资源的浏览器缓存相关配置如下:

```properties

SPRING RESOURCES HANDLING (ResourceProperties)

spring.resources.add-mappings=true # Whether to enable default resource handling. spring.resources.cache.cachecontrol.cache-private= # Indicate that the response message is intended for a single user and must not be stored by a shared cache. spring.resources.cache.cachecontrol.cache-public= # Indicate that any cache may store the response. spring.resources.cache.cachecontrol.max-age= # Maximum time the response should be cached, in seconds if no duration suffix is not specified. spring.resources.cache.cachecontrol.must-revalidate= # Indicate that once it has become stale, a cache must not use the response without re-validating it with the server. spring.resources.cache.cachecontrol.no-cache= # Indicate that the cached response can be reused only if re-validated with the server. spring.resources.cache.cachecontrol.no-store= # Indicate to not cache the response in any case. spring.resources.cache.cachecontrol.no-transform= # Indicate intermediaries (caches and others) that they should not transform the response content. spring.resources.cache.cachecontrol.proxy-revalidate= # Same meaning as the "must-revalidate" directive, except that it does not apply to private caches. spring.resources.cache.cachecontrol.s-max-age= # Maximum time the response should be cached by shared caches, in seconds if no duration suffix is not specified. spring.resources.cache.cachecontrol.stale-if-error= # Maximum time the response may be used when errors are encountered, in seconds if no duration suffix is not specified. spring.resources.cache.cachecontrol.stale-while-revalidate= # Maximum time the response can be served after it becomes stale, in seconds if no duration suffix is not specified. spring.resources.cache.period= # Cache period for the resources served by the resource handler. If a duration suffix is not specified, seconds will be used. spring.resources.chain.cache=true # Whether to enable caching in the Resource chain. spring.resources.chain.compressed=false # Whether to enable resolution of already compressed resources (gzip, brotli). spring.resources.chain.enabled= # Whether to enable the Spring Resource Handling chain. By default, disabled unless at least one strategy has been enabled. spring.resources.chain.html-application-cache=false # Whether to enable HTML5 application cache manifest rewriting. spring.resources.chain.strategy.content.enabled=false # Whether to enable the content Version Strategy. spring.resources.chain.strategy.content.paths=/ # Comma-separated list of patterns to apply to the content Version Strategy. spring.resources.chain.strategy.fixed.enabled=false # Whether to enable the fixed Version Strategy. spring.resources.chain.strategy.fixed.paths=/ # Comma-separated list of patterns to apply to the fixed Version Strategy. spring.resources.chain.strategy.fixed.version= # Version string to use for the fixed Version Strategy. spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/ # 静态资源路径 ```

「静态资源优化」

  • 对JS/CSS文件压缩,减少流量;
  • 多个JS/CSS进行组合为一个文件,减少请求资源时建立的TCP连接数;
  • 利用WebPack可以进行前端资源打包。

「CDN就近访问优化」

  • CDN:内容分发网络(Content Delivery Network),CDN系统能够实时的根据网络流量和各节点的连接、负载情况选择从最近的节点将数据返回给用户;
  • 购买现有的如阿里云CDN,腾讯CDN,百度CDN等等服务。

「消息队列削峰」

系统初始化,把商品库存数量加载到Redis中; 当用户进行秒杀请求时,Redis预减库存,如果库存不足,可以直接返回商品秒杀完成; 如果还有库存,则用户所有的秒杀请求全部进入消息队列,页面先显示排队中; 秒杀请求出队,减少数据库中的库存并生成秒杀订单; 客户端则需要通过js不断轮询是否秒杀成功,轮询方式为查询Redis中是否有订单记录以及Redis中设置的秒杀活动是否结束标记。

III. 安全优化

针对秒杀活动的安全问题,需要防止用户使用工具刷秒杀地址,所以需要隐藏秒杀接口地址,秒杀地址依靠请求服务端返回。加入验证码机制也可以防止机器人恶意刷新,同时限制瞬时请求数量。后端相关服务进行限流,限制用户一段时间内的最高访问次数。

「隐藏秒杀接口」

秒杀抢购时,先请求服务端生成随机的秒杀地址,服务端缓存随机秒杀地址用于校验; 然后再通过请求拿到的随机秒杀地址,服务端先进行校验(和缓存对比),再决定是否进行秒杀商品。

「接口限流防刷」

利用Redis记录用户对于某个请求的一段时间内的访问次数; 如果超时,Redis对于某个请求的访问次数缓存会失效(重新计时); 自定义限流注解 @AccessLimit,利用Spring提供的拦截器对每一个请求方法进行判断,是否包含限流注解。

四、打包部署

I. 项目打包

1.将Spring Boot项目打包成Jar包,指定打包路径和打包名称;

xml <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <finalName>seckill-springboot</finalName> <directory>/Users/guoping/Projects/Intelljidea/seckill/seckill/</directory> </build>

2.编写Dockerfile构建镜像,参考官方文档;

```sh

基础镜像

FROM java:8

挂载点为/tmp,jar包就会存在这里

VOLUME /tmp

拷贝打包好的jar包

COPY seckill-springboot.jar seckill-springboot.jar

暴露端口

EXPOSE 8080

容器创建后执行jar

ENTRYPOINT ["java","-jar","/seckill-springboot.jar"] ```

3.构建镜像创建实例并运行服务

sh docker build -t guoping/seckill:1.0 . docker run -d -p 80:8080 --name e3-mall-seckill guoping/seckill:1.0

4.测试服务。

II. 水平复制

1.启动3个实例,分别端口映射到宿主机的8081、8082、8083,打开宿主机防火墙;

sh docker run -d -p 8081:8080 --name e3-mall-seckill-1 -e TZ=Asia/Shanghai guoping/seckill:1.0 docker run -d -p 8082:8080 --name e3-mall-seckill-2 -e TZ=Asia/Shanghai guoping/seckill:1.0 docker run -d -p 8083:8080 --name e3-mall-seckill-3 -e TZ=Asia/Shanghai guoping/seckill:1.0

2.利用Nginx进行反向代理,宿主机上传好 nginx.conf 配置文件,其中配置好多节点负载均衡;

```markdown events { worker_connections 6000; }

.......

upstream e3-mall-seckill-servers { server 192.168.2.113:8081 weight=1; server 192.168.2.113:8082 weight=1; server 192.168.2.113:8083 weight=1; }

server { listen 80; server_name localhost;

location / {
    proxy_pass   http://e3-mall-seckill-servers;
    index  index.html index.htm;
}

}

```

3.下载Nginx镜像,创建实例反向代理Nginx服务器。

sh docker pull nginx docker run --name e3-mall-nginx -v /root/nginx.conf:/etc/nginx/nginx.conf:ro -p 80:80 -d -e TZ=Asia/Shanghai nginx

```sh [root@localhost ~]# ls anaconda-ks.cfg Dockerfile seckill-springboot.jar [root@localhost ~]# docker build -t guoping/seckill:1.0 . Sending build context to Docker daemon 38.35MB Step 1/5 : FROM java:8 8: Pulling from library/java 5040bd298390: Pull complete fce5728aad85: Pull complete 76610ec20bf5: Pull complete 60170fec2151: Pull complete e98f73de8f0d: Pull complete 11f7af24ed9c: Pull complete 49e2d6393f32: Pull complete bb9cdec9c7f3: Pull complete Digest: sha256:c1ff613e8ba25833d2e1940da0940c3824f03f802c449f3d1815a66b7f8c0e9d Status: Downloaded newer image for java:8 ---> d23bdf5b1b1b Step 2/5 : VOLUME /tmp ---> Running in c846eebffe0c Removing intermediate container c846eebffe0c ---> 11169eefdf99 Step 3/5 : COPY seckill-springboot.jar seckill-springboot.jar ---> b4c86a1b8ade Step 4/5 : EXPOSE 8080 ---> Running in 3215803d2ea8 Removing intermediate container 3215803d2ea8 ---> ceaab0ec1c5d Step 5/5 : ENTRYPOINT ["java","-jar","/seckill-springboot.jar"] ---> Running in a27bb88d19d4 Removing intermediate container a27bb88d19d4 ---> b184325f919e Successfully built b184325f919e Successfully tagged guoping/seckill:1.0 [root@localhost ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE guoping/seckill 1.0 b184325f919e 9 seconds ago 682MB mysql 5.7.23 1b30b36ae96a 6 months ago 372MB redis 3.2 87856cc39862 6 months ago 76MB java 8 d23bdf5b1b1b 2 years ago 643MB [root@localhost ~]# docker run -d -p 80:8080 --name e3-mall-seckill guoping/seckill:1.0 8ff680628f8434f4c8318061188c722d87655293f75957551f6bd87e159c4abc [root@localhost ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8ff680628f84 guoping/seckill:1.0 "java -jar /seckill-…" 4 minutes ago Up 4 minutes 0.0.0.0:80->8080/tcp e3-mall-seckill dd194cf5bad6 mysql:5.7.23 "docker-entrypoint.s…" 23 hours ago Up 23 hours 0.0.0.0:3306->3306/tcp, 33060/tcp e3-mall-mysql 1a244491f479 redis:3.2 "docker-entrypoint.s…" 8 days ago Up 8 days 0.0.0.0:6379->6379/tcp e3-mall-redis

```

五、实现细节

I. 数据库连接问题

当对接口进行压测时,瞬时启动5000个线程去请求接口。我们查看MySQL容器日志时发现,无法创建数据库连接,如下所示。

这是由于我们在 application.yaml 中配置了1000个数据库连接请求,默认的MySQL容器最大连接数根本没有这么多,只有151。

可以通过下面SQL语句查询数据库最大连接数。更改最大连接数目用下面两个SQL语句即可,但缺点是MySQL重启即会失效。

sh show global variables like "max%connections"; set GLOBAL max_connections=2000; set GLOBAL max_user_connections=1500;

设置结果如下所示:

设置完成后,MySQL容器便不会报错,同时可以发现系统启动时Druid连接池初始化数据库连接时长也加长了很多。

下图是测试商品列表接口时,宿主机的性能反映,可以看出消耗最高的两个分别是我们的应用Java进程和查询MySQL进程。

II. JMeter报错问题

这一部分并没有解决,原本想照着视频上启动5000个线程循环10次,但发现,JMeter压测到半途总是报错,请求异常,错误都是网络连接的问题。以下是尝试解决方案,并没有解决问题。

修改操作系统注册表

1、打开注册表:regedit 2、找到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters 3、新建 DWORD值,name:TcpTimedWaitDelay,value:30(十进制) ——> 设置为30秒(默认240) 4、新建 DWORD值,name:MaxUserPort,value:65534(十进制) ——> 设置最大连接数65534 5、重启系统就行了

III. 商品超卖问题

商品超卖问题是秒杀系统中除了性能优化,另一个最为关注的问题之一。由于我们的代码在生产环境中面临着超高并发的挑战,考虑业务需求我们也几乎不会对控制层代码进行加锁保证线程安全。所以,不做其他操作的情况下,极有可能发生商品超卖的问题。

秒杀商品和正常下单的逻辑是一致的,一旦确定可以下单了,业务逻辑主要包含两件事:

扣减库存 (前提库存程序判断肯定是够的,注意是程序,不代表真的够); 创建秒杀订单,或者正常订单,反正是要落地的。 上面两步必须要么全部成功,要么全部失败。很容易想到,我们可以通过事务机制来保证这一原子特性。

「秒杀接口逻辑」

我们实现对应的控制层秒杀接口,可以看到,我们的业务规则首先是判断用户是否登录,然后查看商品的库存是否充足,充足的情况下再判断该用户是否已经成功秒杀过该商品 (每人限秒一个)。上述过程都满足,我们才会去进行秒杀商品,也就是包括减库存和下单两件事。

```java //SecKillController.java

@RequestMapping("/kill") public String secKill(Model model, User user, @RequestParam("goodsId") Long goodsId) { if (user == null) { return "login"; } model.addAttribute("user", user);

GoodsVO goods = goodsService.getGoodsDetailById(goodsId);  // 《第一处》
// 判断库存
if (goods.getStockCount() <= 0) {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_OVER.getMsg());
    return "kill_fail";
}
// 判断是否已经秒杀到商品,防止一人多次秒杀成功
SecKillOrder order = orderService.getSecKillOrderByUserIdAndGoodsId(user.getId(), goodsId);  // 《第二处》
if (order != null) {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_REPEAT.getMsg());
    return "kill_fail";
}

// 正常进入秒杀流程:1.减少库存,2.创建订单,3-写入秒杀订单 三步需要原子操作
OrderInfo orderInfo = secKillService.secKill(user, goods);

// 进入订单详情页
model.addAttribute("orderInfo", orderInfo);
model.addAttribute("goods", goods);
return "order_detail";

}

//SecKillService.java

/* * 减少库存,下订单,写入秒杀订单 * 需要用事务保证原子性 * @param user * @param goods * @return / @Transactional public OrderInfo secKill(User user, GoodsVO goods) { // 减少库存 goodsService.reduceStock(goods); // 秒杀下单 return orderService.createSecKillOrder(user, goods); }

```

减少库存的 Service 层和 Dao 层方法如下:

```java //GoodsService.java

/* * 减小库存 * @param goods 对goods进行的库存进行更新 / public int reduceStock(GoodsVO goods) { SecKillGoods g = new SecKillGoods(); g.setGoodsId(goods.getId()); g.setStockCount(goods.getStockCount() - 1); // 设置库存值 return goodsDao.updateStock(g); }

// GoodsDao.java

/* * 更新库存 * @param g * @return / @Update("UPDATE miaosha_goods SET stock_count = #{stockCount} WHERE goods_id = #{goodsId}") int updateStock(SecKillGoods g);

```

上面的实现方式在JMeter压测下,果然发生了超卖现象,超卖到下了几千个订单。

原因是什么呢?

我们分析控制层的代码,第一处判断商品库存,多个线程获取到商品库存后可能执行时间片被其他线程抢占,等到别的线程秒杀完,原来的线程并不会更新最新的库存,而是使用的大于0的旧值,也就会继续第二处判断是否包含订单。 同样判断是否有订单,这里对于大多数用户来说确实是没有订单记录的,因为秒杀成功的只有少数人。就算有记录,和第一处一样,同一个用户连续请求两次,第一次判断没有记录,然后线程等待,第二次也判断没有记录,线程也等待,然后第一次的线程继续下单秒杀成功,此时对于第二个线程还是不知道的,会继续下单,重复秒杀。 至于我们扣减库存,下单的逻辑也仅仅只是保证了操作的原子性,并没有保证商品超卖现象不会发生。由于第一步多线程获取秒杀商品的信息不是最新的,这样在 g.setStockCount(goods.getStockCount() - 1); 设置更新库存值时也不是最新的,所以才会出现那么多超卖订单的出现。

初步解决超卖问题:

我们先来解决更新的库存值不是最新的问题,其实我们完全可以利用数据库来解决这个问题,每次来一个下单请求,我们将数据库中的库存减1即可,数据库的悲观锁会保证我们库存数是不断减1的。

```java /* * 减小库存 * @param goods 对goods进行的库存进行更新 / public int reduceStock(GoodsVO goods) { SecKillGoods g = new SecKillGoods(); g.setGoodsId(goods.getId()); return goodsDao.updateStock(g); }

/* * 更新库存 * @param g * @return / @Update("UPDATE miaosha_goods SET stock_count = stock_count - 1 WHERE goods_id = #{goodsId}") int updateStock(SecKillGoods g);

```

再次压测,问题仍然存在。

解决库存为负数的问题:

我们已经保证了每次库存的更新是严格按照每次减1的顺序进行更新的,那么还会超卖说明是我们确切的有超过数量的线程数成功经过前面两步的判断放行过来的。那么自然的我们在写多线程程序的时候可能会想到加锁等保证可见性,保证前两步的判断不会出错,但其实我们的最终目标只是不要超卖,保证固定数量的线程数成功即可。所以我们可以在数据库更新库存的时候增加判断条件 stock_count > 0。

```java / 更新库存,解决商品超卖问题的关键所在 * @param g * @return / @Update("UPDATE miaosha_goods SET stock_count = stock_count - 1 WHERE goods_id = #{goodsId} AND stock_count > 0") int updateStock(SecKillGoods g);

```

这样就可以确保我们的库存不会变成负数,利用的是数据库的悲观锁机制。

但是压测时,我们发现,创建的订单数还是超了,这是为什么呢?

我们的减库存、下单代码如下:

```java /* * 减少库存,下订单,写入秒杀订单 * 需要用事务保证原子性,默认传播方式是REQUIRED,没事务创建事务,有事务事务 * @param user * @param goods * @return / @Transactional public OrderInfo secKill(User user, GoodsVO goods) { // 减少库存 int updateRows = goodsService.reduceStock(goods); logger.info("当前库存为:" + goods.getStockCount() + ",减少库存更新记录数为:" + updateRows); // 秒杀下单 return orderService.createSecKillOrder(user, goods); }

```

减库存我们用的是数据库保证了数据库层面数据的正确性,但是代码里仍然是存在问题的。我们查看日志信息,可以清晰的发现,后面库存实际为0的时候,goods 对象的库存数还是之前大于0的旧值。其实减少库存的操作已经没有成功,但是我们没有判断,所以继续下单导致超卖。

解决下单超出问题:

解决这个问题仅仅需要保证数据库减库存操作成功,便可以创建订单,否则不创建订单。

```java // SecKillController.java

@RequestMapping("/kill") public String secKill(Model model, User user, @RequestParam("goodsId") Long goodsId) { if (user == null) { return "login"; } model.addAttribute("user", user);

GoodsVO goods = goodsService.getGoodsDetailById(goodsId);
// 判断库存
if (goods.getStockCount() <= 0) {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_OVER.getMsg());
    return "kill_fail";
}
// 判断是否已经秒杀到商品,防止一人多次秒杀成功
SecKillOrder order = orderService.getSecKillOrderByUserIdAndGoodsId(user.getId(), goodsId);
if (order != null) {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_REPEAT.getMsg());
    return "kill_fail";
}

// 正常进入秒杀流程:1.减少库存,2.创建订单,3-写入秒杀订单 三步需要原子操作
OrderInfo orderInfo = secKillService.secKill(user, goods);

if (orderInfo != null) {
    // 进入订单详情页
    model.addAttribute("orderInfo", orderInfo);
    model.addAttribute("goods", goods);
    return "order_detail";
} else {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_OVER.getMsg());
    return "kill_fail";
}

}

//GoodsService.java

/* * 减少库存,下订单,写入秒杀订单 * 需要用事务保证原子性,默认传播方式是REQUIRED,没事务创建事务,有事务事务 * @param user * @param goods * @return / @Transactional public OrderInfo secKill(User user, GoodsVO goods) { // 减少库存 int updateRows = goodsService.reduceStock(goods); logger.info("当前库存为:" + goods.getStockCount() + ",减少库存更新记录数为:" + updateRows); // 减少库存成功才进行秒杀下单 if (updateRows == 1) { return orderService.createSecKillOrder(user, goods); } // 如果库存没有更新成功,则不能进行下单 else { return null; } }

```

再重新进行压测,便解决商品超卖问题。

问题真的解决了吗?

压测时,我们缩减为3个用户多次请求进行抢购。我们可以看到用户重复秒杀同一商品的情形。为什么出现呢,我们之前已经分析过了,判断用户是否已经秒杀到商品是线程不安全的,而等到后面进行秒杀下单时也没有再进行判断。

如果判断用户是否重复秒杀呢,其实我们可以给用户与商品添加上一个组合唯一索引,这样重复创建订单就会插入纪录失败,这样由于事务的存在,库存减少也会回滚。

解决重复秒杀问题:

jav create index userid_goodsid_unique_index on miaosha_order(user_id, goods_id);

再次压测结果:

这次我们看到订单记录也是没有问题的。

IV. 秒杀接口优化

上面实现商品不超卖的方案是从数据库悲观锁层面保证的,每一次秒杀请求都会进行数据库的访问,这其实在秒杀场景下是扛不住访问压力的。我们可以采用Redis预减库存+MQ异步下单+接口限流的方案提高系统的性能,缓和瞬时请求量庞大的问题。

首先访问内存标记,是否已经秒杀结束。然后通过Redis预减库存的方式放行库存数量的请求通过,接下来再采取之前的方式通过数据库保证不超卖。

```java // SecKillController.java

@Autowired private MQSender mqSender; @Autowired private RedisTemplate redisTemplate;

/* * 内存标记Map,key为商品id,value为是否秒杀结束,用于减少对Redis的访问 * 该内存标记非线程安全,但不会影响功能,只是有多个线程多次复写某商品卖完 / private HashMap goodsSecKillOverMap = new HashMap<>();

@RequestMapping("/asynckill") public String asyncSecKill(Model model, User user, @RequestParam("goodsId") Long goodsId) { if (user == null) { return "login"; } model.addAttribute("user", user);

// 先访问内存标记,查看是否卖完
if (goodsSecKillOverMap.containsKey(goodsId) && goodsSecKillOverMap.get(goodsId)) {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_OVER.getMsg());
    return "kill_fail";
}

// Redis预减库存
Long nowStock = redisTemplate.opsForValue().decrement(GoodsKey.goodsStockKey.getPrefix() + ":" + goodsId);
logger.info("商品" + goodsId + "预减库存完Redis当前库存数量为:" + nowStock);

// 如果库存预减完毕,则直接返回秒杀失败
if (nowStock < 0) {
    // 记录当前商品秒杀完毕
    goodsSecKillOverMap.put(goodsId, true);
    model.addAttribute("errorMsg", CodeMsg.SECKILL_OVER.getMsg());
    return "kill_fail";
}

// 判断是否已经秒杀到商品,防止一人多次秒杀成功
SecKillOrder order = orderService.getSecKillOrderByUserIdAndGoodsId(user.getId(), goodsId);
if (order != null) {
    model.addAttribute("errorMsg", CodeMsg.SECKILL_REPEAT.getMsg());
    return "kill_fail";
}

// 正常进入秒杀流程:入队进行异步下单
mqSender.sendSecKillMessage(new SecKillMessage(user, goodsId));
model.addAttribute("killMsg", CodeMsg.SECKILL_WAITTING.getMsg());
return "kill_wait";

}

```

「异步下单」

下单的时候这里仅仅是通过发送一个下单请求消息进入队列,然后立即返回排队中的结果给用户。异步下单的方式,将系统的瞬时请求进行削峰。

```java //MQSender.java

/* * 发送秒杀请求消息,包含用户和商品id * @param secKillMessage / public void sendSecKillMessage(SecKillMessage secKillMessage) { logger.info("用户" + secKillMessage.getUser().getId() + "发起秒杀" + secKillMessage.getGoodsId() + "商品请求"); rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE, secKillMessage); }

//MQReceiver.java @RabbitHandler public void receiveSecKillMessage(@Payload SecKillMessage secKillMessage) { User user = secKillMessage.getUser(); Long goodsId = secKillMessage.getGoodsId();

logger.info("收到用户" + user.getId() + "秒杀" + goodsId + "商品请求");

// 判断库存
GoodsVO goods = goodsService.getGoodsDetailById(goodsId);
if (goods.getStockCount() <= 0) {
    return;
}
// 判断是否已经秒杀到商品,防止一人多次秒杀成功
SecKillOrder order = orderService.getSecKillOrderByUserIdAndGoodsId(user.getId(), goodsId);
if (order != null) {
    return;
}

// 正常进入秒杀流程:1.减少库存,2.创建订单,3-写入秒杀订单 三步需要原子操作
OrderInfo orderInfo = secKillService.secKill(user, goods);

if (orderInfo != null) {
    // 秒杀成功

} else {
    // 秒杀失败
}

}

```

利用JMeter压测时打印的日志如下:

```sh INFO 70092 --- [o-8080-exec-124] SecKillController : 商品2预减库存完Redis当前库存数量为:5 INFO 70092 --- [o-8080-exec-168] SecKillController : 商品2预减库存完Redis当前库存数量为:6 INFO 70092 --- [nio-8080-exec-2] SecKillController : 商品2预减库存完Redis当前库存数量为:4 INFO 70092 --- [o-8080-exec-194] SecKillController : 商品2预减库存完Redis当前库存数量为:3 INFO 70092 --- [io-8080-exec-76] SecKillController : 商品2预减库存完Redis当前库存数量为:2 INFO 70092 --- [o-8080-exec-130] SecKillController : 商品2预减库存完Redis当前库存数量为:1 INFO 70092 --- [o-8080-exec-138] SecKillController : 商品2预减库存完Redis当前库存数量为:0 INFO 70092 --- [io-8080-exec-26] SecKillController : 商品2预减库存完Redis当前库存数量为:-1 INFO 70092 --- [io-8080-exec-79] SecKillController : 商品2预减库存完Redis当前库存数量为:-2 INFO 70092 --- [io-8080-exec-14] SecKillController : 商品2预减库存完Redis当前库存数量为:-3 INFO 70092 --- [o-8080-exec-201] SecKillController : 商品2预减库存完Redis当前库存数量为:-4 INFO 70092 --- [o-8080-exec-204] SecKillController : 商品2预减库存完Redis当前库存数量为:-5 INFO 70092 --- [o-8080-exec-202] SecKillController : 商品2预减库存完Redis当前库存数量为:-6 INFO 70092 --- [o-8080-exec-205] SecKillController : 商品2预减库存完Redis当前库存数量为:-7 INFO 70092 --- [o-8080-exec-206] SecKillController : 商品2预减库存完Redis当前库存数量为:-8 INFO 70092 --- [o-8080-exec-221] SecKillController : 商品2预减库存完Redis当前库存数量为:-9 INFO 70092 --- [o-8080-exec-225] SecKillController : 商品2预减库存完Redis当前库存数量为:-10 INFO 70092 --- [o-8080-exec-211] SecKillController : 商品2预减库存完Redis当前库存数量为:-11 INFO 70092 --- [o-8080-exec-224] SecKillController : 商品2预减库存完Redis当前库存数量为:-12 INFO 70092 --- [o-8080-exec-220] SecKillController : 商品2预减库存完Redis当前库存数量为:-14 INFO 70092 --- [o-8080-exec-208] SecKillController : 商品2预减库存完Redis当前库存数量为:-13 INFO 70092 --- [o-8080-exec-207] SecKillController : 商品2预减库存完Redis当前库存数量为:-15 INFO 70092 --- [o-8080-exec-218] SecKillController : 商品2预减库存完Redis当前库存数量为:-16 INFO 70092 --- [o-8080-exec-222] SecKillController : 商品2预减库存完Redis当前库存数量为:-17 INFO 70092 --- [o-8080-exec-219] SecKillController : 商品2预减库存完Redis当前库存数量为:-18 INFO 70092 --- [o-8080-exec-223] SecKillController : 商品2预减库存完Redis当前库存数量为:-19 INFO 70092 --- [o-8080-exec-216] SecKillController : 商品2预减库存完Redis当前库存数量为:-20 INFO 70092 --- [o-8080-exec-217] SecKillController : 商品2预减库存完Redis当前库存数量为:-21 INFO 70092 --- [o-8080-exec-213] SecKillController : 商品2预减库存完Redis当前库存数量为:-22 INFO 70092 --- [o-8080-exec-215] SecKillController : 商品2预减库存完Redis当前库存数量为:-23 INFO 70092 --- [o-8080-exec-212] SecKillController : 商品2预减库存完Redis当前库存数量为:-24 INFO 70092 --- [o-8080-exec-214] SecKillController : 商品2预减库存完Redis当前库存数量为:-25 INFO 70092 --- [o-8080-exec-203] SecKillController : 商品2预减库存完Redis当前库存数量为:-26 INFO 70092 --- [o-8080-exec-210] SecKillController : 商品2预减库存完Redis当前库存数量为:-27 INFO 70092 --- [o-8080-exec-209] SecKillController : 商品2预减库存完Redis当前库存数量为:-28 INFO 70092 --- [o-8080-exec-124] MQSender : 用户13000000004发起秒杀2商品请求 INFO 70092 --- [o-8080-exec-168] MQSender : 用户13000000001发起秒杀2商品请求 INFO 70092 --- [nio-8080-exec-2] MQSender : 用户13000000000发起秒杀2商品请求 INFO 70092 --- [o-8080-exec-194] MQSender : 用户13000000006发起秒杀2商品请求 INFO 70092 --- [o-8080-exec-130] MQSender : 用户13000000005发起秒杀2商品请求 INFO 70092 --- [o-8080-exec-138] MQSender : 用户13000000003发起秒杀2商品请求 INFO 70092 --- [io-8080-exec-76] MQSender : 用户13000000002发起秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000002秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:7,减少库存更新记录数为:1 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000005秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:6,减少库存更新记录数为:1 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000003秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:5,减少库存更新记录数为:1 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000004秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:4,减少库存更新记录数为:1 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000001秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:3,减少库存更新记录数为:1 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000006秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:2,减少库存更新记录数为:1 INFO 70092 --- [cTaskExecutor-1] MQReceiver : 收到用户13000000000秒杀2商品请求 INFO 70092 --- [cTaskExecutor-1] SecKillService : 当前库存为:1,减少库存更新记录数为:1

```

「接口限流」

通过自定义注解+Spring方法拦截器+Redis访问次数统计的方式,可以对需要限流访问的接口进行限流。

自定义接口 @AccessLimit:

```java /* * @description: 请求限流注解 * @author: guoping wang * @email: Kingdompin@163.com * @date: 2019/5/10 2:11 PM * @project: seckill / @Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.METHOD) public @interface AccessLimit {

/**
 * 指定时间
 * @return
 */
int seconds() default 60;

/**
 * seconds时间内最多允许访问maxValue次数
 * @return
 */
int maxValue();

/**
 * 该请求是否需要登录
 * @return
 */
boolean needLogin() default true;

}

```

参考文献

  • 某订单秒杀系统的设计与实现(北京邮电大学·侯成程)
  • 在线购物系统的设计与实现(华中科技大学·杨显)
  • 基于微服务架构的秒杀系统服务端设计与实现(东南大学·徐冉)
  • 在线购物系统的设计与实现(华中科技大学·杨显)
  • 在线购物系统的设计与实现(华中科技大学·杨显)
  • 电子商务M站系统的设计与实现(北京交通大学·胡霞)
  • 电子商务M站系统的设计与实现(北京交通大学·胡霞)
  • 基于SSH架构体系的电子拍卖系统的设计与实现(山东大学·李琪)
  • 基于微服务架构之电商秒杀系统的设计与实现(华东师范大学·闫保正)
  • 基于微服务架构之电商秒杀系统的设计与实现(华东师范大学·闫保正)
  • 基于MVC模式网上拍卖系统的设计与实现(电子科技大学·徐琳)
  • 基于SSH架构体系的电子拍卖系统的设计与实现(山东大学·李琪)
  • 基于MVC模式网上拍卖系统的设计与实现(电子科技大学·徐琳)
  • 基于SSH架构体系的电子拍卖系统的设计与实现(山东大学·李琪)
  • 一个基于微服务的个性化电商系统的设计与实现(华中科技大学·胡梦婷)

本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:毕设向导 ,原文地址:https://bishedaima.com/yuanma/35702.html

相关推荐

发表回复

登录后才能评论