引言
本文代码已提交至Github(版本号:
dd8742a8348b4c64a8ca794d544a3271e94365a9
),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop
秒杀系统的代码在前面博客已经实现了,有兴趣的同学可以参阅下:
- 《淘东电商项目(73) -秒杀系统(前端优化)》
- 《淘东电商项目(74) -秒杀系统(库存超卖解决方案》
- 《淘东电商项目(75) -秒杀系统(用户操作频率限制)》
- 《淘东电商项目(76) -秒杀系统(完整代码实现)》
- 《淘东电商项目(77) -秒杀系统(小结)》
秒杀的逻辑代码完成了,剩下还有一个问题,就是关于服务的保护了,本文来讲解。
本文目录结构:
l____引言
l____ 1.服务保护
l________ 1.1 限流
l________ 1.2 降级、熔断、限流概念
l____ 2.代码
l________ 2.1 网关限流
l________ 2.2 服务保护
l____ 3.测试
l________ 3.1 网关限流测试
l________ 3.2 服务保护测试
1.服务保护
服务保护在《互联网并发与安全》栏目有讲解过,有兴趣的同学可以参阅下:
- 《互联网并发与安全系列教程(01) - 基于Hystrix实现服务隔离与降级》
- 《互联网并发与安全系列教程(02) - 服务限流》
- 《互联网并发与安全系列教程(03) - RateLimiter使用AOP方式实现限流》
下面来简单的描述下。
1.1 限流
常见限流算法常用的限流算法有:令牌桶算法、漏桶算法
- 令牌桶算法:在秒杀活动中,用户的请求速率是不固定的,这里我们假定为10r/s,令牌按照5个每秒的速率放入令牌桶,桶中最多存放20个令牌。仔细想想,是不是总有那么一部分请求被丢弃。
- 漏桶算法:漏桶算法的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量
市面上常用实现限流框架:
- Nginx+Lua、Guava、hystrix等
1.2 降级、熔断、限流概念
服务雪崩效应:服务雪崩效应产生与服务堆积在同一个线程池中,因为所有的请求都是同一个线程池进行处理,这时候如果在高并发情况下,所有的请求全部访问同一个接口,这时候可能会导致其他服务没有线程进行接受请求,这就是服务雪崩效应效应。
服务降级:在高并发情况下,防止用户一直等待,使用服务降级方式(直接返回一个友好的提示给客户端,调用fallBack方法)。
服务熔断:熔断机制目的为了保护服务,在高并发的情况下,如果请求达到一定极限(可以自己设置阔值)如果流量超出了设置阈值,让后直接拒绝访问,保护当前服务。使用服务降级方式返回一个友好提示,服务熔断和服务降级一起使用。
服务隔离:因为默认情况下,只有一个线程池会维护所有的服务接口,如果大量的请求访问同一个接口,达到tomcat 线程池默认极限,可能会导致其他服务无法访问。
解决服务雪崩效应:使用服务隔离机制(线程池方式和信号量),使用线程池方式实现隔离的原理: 相当于每个接口(服务)都有自己独立的线程池,因为每个线程池互不影响,这样的话就可以解决服务雪崩效应。
- 线程池隔离:每个服务接口,都有自己独立的线程池,每个线程池互不影响。
- 信号量隔离:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返回成功后计数器-1。
2.代码
2.1 网关限流
网关限流,淘东电商项目采用的是基于谷歌RateLimiter实现限流。Google的Guava工具包中就提供了一个限流工具类——RateLimiter,本文也是通过使用该工具类来实现限流功能,RateLimiter是基于“令牌桶算法”来实现限流的。之前的的网关是使用“责任链设计模式”来重新构造了,本文也使用责任链模式添加RateLimiter来实现限流。
①添加 guava maven依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
②添加限流Handler:
/**
* description: 服务限流
* create by: YangLinWei
* create time: 2020/5/26 3:00 下午
*/
@Component
@Slf4j
public class CurrentLimitHandler extends BaseHandler implements GatewayHandler {
private RateLimiter rateLimiter = RateLimiter.create(1);
@Autowired
private GenerateToken generateToken;
@Override
public Boolean service(RequestContext ctx, String ipAddres, HttpServletRequest request, HttpServletResponse response) {
// 1.用户限流频率设置 每秒中限制1个请求
boolean tryAcquire = rateLimiter.tryAcquire(0, TimeUnit.SECONDS);
if (!tryAcquire) {
resultError(ctx, "There are too many people snapping up goods now. Please wait a moment!");
return Boolean.FALSE;
}
// 2.使用redis限制用户访问频率
String seckillId = request.getParameter("seckillId");
String seckillToken = generateToken.getListKeyToken(seckillId + "");
if (StringUtils.isEmpty(seckillToken)) {
log.info(">>>seckillId:{}, The second kill has sold out, please come again next time!", seckillId);
resultError(ctx, "The second kill has sold out, please come again next time!");
return Boolean.FALSE;
}
if (gatewayHandler != null) {
gatewayHandler.service(ctx, ipAddres, request, response);
}
return Boolean.TRUE;
}
}
③工厂定义网关过滤步骤:
public class FactoryHandler {
public static GatewayHandler getHandler() {
// 1.黑名单拦截
GatewayHandler handler1 = (GatewayHandler) SpringContextUtil.getBean("blackListHandler");
// 2.API接口参数接口验签
GatewayHandler handler2 = (GatewayHandler) SpringContextUtil.getBean("verifySignHandler");
handler1.setNextHandler(handler2);
// 3.参数过滤
GatewayHandler handler3 = (GatewayHandler) SpringContextUtil.getBean("filterParamHandler");
handler1.setNextHandler(handler3);
//4.服务限流
GatewayHandler handler4 = (GatewayHandler) SpringContextUtil.getBean("currentLimitHandler");
handler3.setNextHandler(handler4);
//5.验证accessToken
GatewayHandler handler5 = (GatewayHandler) SpringContextUtil.getBean("apiAuthorityHandler");
handler4.setNextHandler(handler5);
return handler1;
}
}
2.2 服务保护
①添加Hystrix maven依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
②启动类开启Hystrix
③服务接口保护
@Override
@Transactional
@HystrixCommand(fallbackMethod = "spikeFallback")
public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
// 1.参数验证
if (StringUtils.isEmpty(phone)) {
return setResultError("手机号码不能为空!");
}
if (seckillId == null) {
return setResultError("商品库存id不能为空!");
}
// 2.从redis从获取对应的秒杀token
String seckillToken = generateToken.getListKeyToken(seckillId + "");
if (StringUtils.isEmpty(seckillToken)) {
log.info(">>>seckillId:{}, 亲,该秒杀已经售空,请下次再来!", seckillId);
return setResultError("亲,该秒杀已经售空,请下次再来!");
}
// 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存
sendSeckillMsg(seckillId, phone);
return setResultSuccess("正在排队中.......");
}
private BaseResponse<JSONObject> spikeFallback(String phone, Long seckillId) {
return setResultError("服务器忙,请稍后重试!");
}
3.测试
3.1 网关限流测试
浏览器访问http://localhost/api-spike/spike?phone=13800000001&seckillId=100001,可以看到:
在1秒钟之内继续访问,可以看到限流如下:
3.2 服务保护测试
服务保护测试采用JMeter来测试,由于测试条件的不允许,本文不再演示。如果要看演示的效果,可以参考之前写的博客《微服务技术系列教程(22) - SpringCloud- 服务保护机制Hystrix》。