疯狂创客圈 Java 高并发【 亿级流量聊天室实战】实战系列之15 【博客园总入口 】
前言
疯狂创客圈(笔者尼恩创建的高并发研习社群)Springcloud 高并发系列文章,将为大家介绍三个版本的 高并发秒杀:
一、版本1 :springcloud + zookeeper 秒杀
二、版本2 :springcloud + redis 分布式锁秒杀
三、版本3 :springcloud + Nginx + Lua 高性能版本秒杀
以及有关Springcloud 几篇核心、重要的文章:
二、Springcloud 中 SpringBoot 配置全集 , 收藏版
三、Feign Ribbon Hystrix 三者关系 , 史上最全 深度解析
四、SpringCloud gateway 详解 , 史上最全
本文:是**第一个版本 springcloud + zookeeper 秒杀 **实现,文章比较长,大家可以挑选感兴趣的部分,选择性阅读。
本文的秒杀效果图:

提示: 本文内容,稍微有些陈旧,最新的源码和最新内容,请关注高并发社群—— 疯狂创客圈
1 为何要以秒杀做为高并发实战案例?
时间调到在单体架构还是主流的年代,那时候,大家学习J2EE技术的综合性实战案例,一般来说,就是从0开始实现,一行一行代码的,磊出来一个购物车应用。这个案例能对J2EE有一个全面的练习,包括前台的脚本、MVC框架、事务、数据库等各个方法的技术。
时代在变,技术的复杂度变了,前后的分工也在变。
现在和以前不同了,现在已经进入到微服务的时代,前后台程序员已经有比较明确的分工,在前后台分离的团队,后台程序员专门做Java开发,前台程序员专门做前台的开发。后台程序员可以不需要懂前台的技术如 Vue、TypeScript 等等,前台的程序员就更不一定需要懂后台技术了。
对于后台来说,现在的分布式开发场景,在技术难度上,要比单体服务时代大多了。首先面临一大堆分布式、高性能中间件的学习,比如 Netty 、Zookeeper、RabbitMq、SpringCloud、Redis 等等。而且,在分布式环境下,要掌握如何发现解决数据一致性、高可靠性等问题,因为在高并发场景下,本来很正常的代码,也会跑出很多的性能相关的问题,所以,像Jmeter这类压力测试,也已经成为每一个后台程序员所必须掌握的工具。
所以,这里以秒杀程序作为实战案例,简单来说就是继往开来。继承单体架构时代的购物车应用的知识体系,开启高并发时代的Netty 、Zookeeper、RabbitMq、SpringCloud、Redis、Jmeter等新技术体系的学习。
1.1 业务场景和特点
秒杀案例在生活中几乎随处可见:比如商品抢购,比如春运抢票,还是就是随处可见的红包也是类似的。
另外,在跳槽很频繁的IT行业,大家都会有面试的准备要求。在面试中, 秒杀业务或者秒杀中所用到的分布式锁、分布式ID、数据一致性、高并发限流等问题,一般都是成为重点题目和热门题目,为面试官和应聘者津津乐道.
从下单的角度来说,秒杀业务非常简单:根据先后顺序,下订单减库存。
秒杀的特点:(1)瞬时大流量:秒杀时网站的面临访问量瞬时大增;(2)只有部分用户能够成功,秒杀时购买的请求数量远远大于库存。
1.1.1 详解:秒杀系统的业务流程
从系统角度来说,秒杀系统的业务流程如图1所示,分成两大维度:
(1)商户维度的业务流程;
(2)用户维度的业务流程。

图1 秒杀系统的业务流程
一、商户维度的业务流程,主要涉及两个操作:
(1)增加秒杀
通过后台的管理界面,增加特定商品、特定数量、特定时段的秒杀。
(2)暴露秒杀
将符合条件的秒杀,暴露给用户,以便互联网用户能参与商品的秒杀。这个操作可以是商户手动完成,更合理的方式是系统自动维护。
二、用户维度的业务流程,主要涉及两个操作:
(1)减库存
减少库存,简单说就是减少被秒杀到的商品的库存数量,这也是秒杀系统中一个处理难点的地方。为什么呢? 这不仅仅需要考虑如何避免同一用户重复秒杀的行为,而且在多个微服务并发情况下,需要保障库存数据的一致性,避免超卖的情况发生。
(2)下订单
减库存后,需要下订单,也就是在订单表中添加订单记录,记录购买用户的姓名、手机号、购买的商品ID等。与减库存相比,下订单相对比较简单。
特别说明下:为了聚焦高并发技术知识体系的学习,这里对秒杀的业务进行了馊身,去掉了一些其他的、但是也非常重要的功能,比如支付功能、提醒功能等等。
1.1.2 难点:秒杀系统面临的技术难题
秒杀业务一般就是下订单减库存,流程比较简单。那么,难点在哪里呢?
(1)秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功,这种场景下,需要借助分布式锁等保障数据一致性。
(2)秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。这就需要进行削峰和限流。
总体来说,秒杀系统面临的技术难题,大致有如下几点:
(1)限流:
鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
(2)削峰
对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。
(3)异步处理
秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
(4)内存缓存
秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
(5)可拓展
秒杀系统,一定是可以弹性拓展。如果流量来了,可以按照流量预估,进行服务节点的动态增加和摘除。比如淘宝、京东等双十一活动时,会增加大量机器应对交易高峰。
1.2 基于Zuul和Zookeeper的秒杀架构
从能力提供的角度来说,基于Zuul和Zookeeper的秒杀架构,大致如所示。

图2 从能力提供的角度展示Zuul和Zookeeper的秒杀架构
在基于Zuul和Zookeeper的秒杀架构中,Zuul网关负责路由和限流,而Zookeeper 作为幕后英雄,提供分布式计数器、分布式锁、分布式ID的生成器的基础能力。
分布式计数器、分布式锁、分布式ID的生成器等基础的能力,也是大家所必须系统学习和掌握的知识,超出了这里介绍的范围,如果对这一块不了解,请翻阅尼恩所编著的另一本高并发基础书籍《Netty、Zookeeper、Redis 高并发实战》。
1.2.1 分层详解:基于微服务的秒杀架构
从分层的角度来说,基于Zuul和Zookeeper的微服务秒杀系统,在架构上可以分成三层,如图3所示:
(1)客户端
(2)微服务接入层
(3)微服务业务层
一、客户端的功能
(1)秒杀页面静态化展示:
在桌面浏览器、移动端APP展示秒杀的商品。不论在哪个屏幕展示,秒杀的活动元素,需要尽可能全部静态化,并尽量减少动态元素。这样,就可以通过CDN来抗峰值。
(2)禁止重复秒杀
用户在客户端操作过程中,客户端需要具备用户行为的控制能力。比如,在用户提交秒杀之后,可以将用户秒杀的按钮置灰,禁止重复提交。
二、微服务接入层功能
(1)将请求拦截在系统上游,降低下游压力
秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。
拦截用户的限流方式,有很多种。这里是秒杀的第一个版本,出于学习目的,本版仅仅介绍使用Zookeeper 的计数器能力进行限流,在后面的第二个秒杀版本,将会详细介绍如何使用Redis+Lua进行更高效率的限流,在更加后面的第三个秒杀版本,将会详细介绍使用Nginx+Lua 进行更加更加(两个更加)高效率的限流。
(2)消息队列削峰
上面只拦截了一部分访问请求,当秒杀的用户量很大时,即使每个用户只有一个请求,到服务层的请求数量还是很大。比如我们有100W用户同时抢100台手机,服务层并发请求压力至少为100W。
使用消息队列可以削峰,将为后台缓冲大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取秒杀消息进行业务处理。
这个版本,不做消息队列削峰的介绍。在更加后面的第三个秒杀版本,将会详细介绍使用RabbitMq进行秒杀的削峰。

图3 Zuul和Zookeeper的秒杀架构分层示意
三、微服务业务层功能
单体的秒杀服务,完成到达后台的秒杀下单的前台请求。然后,基于Springcloud的服务编排能力,进行多个单体服务的集群,使得整个系统具备可以动态扩展的能力。
其实,上面的图中,没有将数据库层列出,因为这是众所周知的。数据库层,也是最脆弱的一层,数据库层只承担“能力范围内”的访问请求。所以,需要在上游的接入层、服务层引入队列机制和缓存机制,让最底层的数据库高枕无忧。
1.2.2 简介:总体的项目结构
分成两个部分,介绍基于Zuul和Zookeeper的秒杀系统项目结构:
(1)Zuul网关与微服务基础能力的项目结构
(2)秒杀服务的项目结构
一:Zuul网关与微服务基础能力的项目结构
网关的路由能力,由Zuul和Eureka整合起来的微服务基础框架Ribben提供;网关的限流能力,主要在Zuul的过滤器类 —— ZkRateLimitFilter类中提供。
Zuul网关与微服务基础能力的项目结构如图4所示,具体请参见源码。

图4 Zuul网关与微服务基础能力的项目结构
二:秒杀微服务的项目结构
秒杀微服务是一个标准的SpringBoot项目,分成controller、service、dao三层,如图5所示。,更加具体的项目结构学习,请参见源码。

图5 秒杀服务的项目结构
1.2.3 接入层:使用Zuul进行路由
前面详细介绍Zuul的使用,这里不做大多的技术介绍。仅仅介绍一下,Zuul和seckill-provider秒杀服务的路由配置,具体如下:
#服务网关配置
zuul:
ribbonIsolationStrategy: THREAD
host:
connect-timeout-millis: 60000
socket-timeout-millis: 60000
#路由规则
routes:
# user-service:
# path: /user/**
# serviceId: user-provider
seckill-provider:
path: /seckill-provider/**
serviceId: seckill-provider
message-provider:
path: /message-provider/**
serviceId: message-provider
urlDemo:
path: /user-provider/**
url: http://127.0.0.1/user-provider
1.2.4 接入层:使用Zookeeper分布式计数器进行限流
理论上,接入层的限流有多个维度:
(1)用户维度限流:
在某一时间段内只允许用户提交一次请求,比如可以采取IP或者UserID限流。采取IP限流,可以拦截了浏览器访问的请求,但针对某些恶意攻击或其它插件,在接入层需要针对同一个访问UserID,限制访问频率。
(2)商品维度的限流
对于同一个抢购,在某一时间段内只允许一定数量的请求进入,利用这种简单的方式,防止后台的秒杀服务雪崩。
无论是那个维度的限流,掌握其中的一个,其他维度的限流,在技术实现上都是差不多的。这里,仅仅实现商品维度的限流,用户维度限流,大家可以自己去实现。
这里,为了完成商品维度的限流,实现了一个Zuul的过滤器类 —— ZkRateLimitFilter类,通过对秒杀的请求 “/seckill-provider/api/seckill/do/v1” 进行拦截,然后通过Zookeeper计数器,对当前的参与商品的秒杀人数进行判断,如果超出,则进行拦截。
ZkRateLimitFilter类的源码如下:
package com.crazymaker.springcloud.cloud.center.zuul.filter;
import com.crazymaker.springcloud.common.distribute.rateLimit.RateLimitService;
import com.crazymaker.springcloud.seckill.contract.constant.SeckillConstants;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.recipes.atomic.DistributedAtomicInteger;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* zookeeper 秒杀限流
*/
@Slf4j
@Component
public class ZkRateLimitFilter extends ZuulFilter {
@Resource(name="zkRateLimitServiceImpl")
RateLimitService rateLimitService;
@Override
public String filterType() {
// pre:路由之前
// routing:路由之时
// post: 路由之后
// error:发送错误调用
return "pre";
}
/**
* 过滤的顺序
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 这里可以写逻辑判断,是否要过滤,true为永远过滤。
*/
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if(request.getRequestURI().startsWith("/seckill-provider/api/seckill/do/v1"))
{
return true;
}
return false;
}
/**
* 过滤器的具体逻辑
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String goodId = request.getParameter("goodId");
if (goodId != null) {
DistributedAtomicInteger counter= rateLimitService.getZookeeperCounter(goodId);
try {
log.info( "参与抢购的人数:" + counter.get().preValue());
if(counter.get().preValue()> SeckillConstants.MAX_ENTE

最低0.47元/天 解锁文章
3336





