引言
本文代码已提交至Github(版本号:
dd8742a8348b4c64a8ca794d544a3271e94365a9
),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop
秒杀系统的代码在前面博客已经实现了,有兴趣的同学可以参阅下:
- 《淘东电商项目(73) -秒杀系统(前端优化)》
- 《淘东电商项目(74) -秒杀系统(库存超卖解决方案》
- 《淘东电商项目(75) -秒杀系统(用户操作频率限制)》
- 《淘东电商项目(76) -秒杀系统(完整代码实现)》
- 《淘东电商项目(77) -秒杀系统(小结)》
秒杀的逻辑代码完成了,剩下还有一个问题,就是关于服务的保护了,本文来讲解。
本文目录结构:
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》。