赞
踩
秒杀限流操作既可以在内部网关Zuul中完成,又可以在外部网关Nginx中完成。内部网关Zuul可以通过ZuulFilter过滤器的形式对获取秒杀令牌的请求进行拦截,然后通过Redis令牌桶限流服务实现分布式限流。
从前面的内容可知,Redis中存储限流令牌桶信息的是一个哈希表结构,其内部的键值对包括max_permits、curr_permits、rate、last_mill_second四个hash key,而整个令牌桶哈希表结构的缓存key的格式为rate_limiter:seckill:1(1为商品ID),其中重要的部分是秒杀商品ID,该ID表示限流统计的范围是针对一个秒杀商品的,而不是针对整个秒杀接口。
秒杀商品(假设ID为1)的限流令牌桶的Redis哈希表结构如图10-12所示。
图10-12 存储令牌桶限流信息的Redis哈希表结构
在秒杀没有开始之前需要初始化限流令牌桶的Redis哈希表结构,虽然真正的初始化工作是在rate_limit.lua脚本中完成的,但是需要通过Java程序进行调用,并传入相关的初始化参数。什么时候进行限流令牌桶的初始化呢?生产环境上的秒杀开始之前应该有一个秒杀商品暴露(或者启动)的动作,该动作可以手动或者自动完成,限流的初始化工作可以在秒杀暴露时完成。
下面是一个限流的初始化的简单示例:
- package com.crazymaker.springcloud.seckill.controller;
- //省略import
- @RestController
- @RequestMapping("/api/seckill/good/")
- @Api(tags = "秒杀练习 商品管理")
- public class SeckillGoodController
- {
- /**
- *开启商品秒杀
- *
- *@param dto商品id
- *@return商品goodDTO
- */
- @PostMapping("/expose/v1")
- @ApiOperation(value = "开启商品秒杀")
- RestOut<SeckillGoodDTO> expose(@RequestBody SeckillDTO dto)
- {
- Long goodId = dto.getSeckillGoodId();
- SeckillGoodDTO goodDTO = seckillService.findGoodByID(goodId);
- if (null != goodDTO)
- {
- //初始化秒杀的限流器
- rateLimitService.initLimitKey(
- "seckill",
- String.valueOf(goodId),
- SeckillConstants.MAX_ENTER,
- SeckillConstants.PER_SECKOND_ENTER
- );
- /**
- *缓存限流lua脚本的sha1编码,方便在其他地方获取
- */
- rateLimitService.cacheSha1();
- /**
- *缓存秒杀lua脚本的sha1编码,方便在其他地方获取
- */
- redisSeckillServiceImpl.cacheSha1();
- return RestOut.success(goodDTO).setRespMsg("秒杀开启成功");
- }
- return RestOut.error("秒杀开启失败");
- }
- ...
- }
限流器初始化之后,就可以在Zuul内部网关或者Nginx外部网关进行请求拦截时使用分布式限流器进行限流。Zuul内部网关的限流拦截过程如图10-13所示。
图10-13 Zuul内部网关限流拦截示意图
Zuul网关限流过滤器类SeckillRateLimitFilter的代码如下:
- package com.crazymaker.springcloud.cloud.center.zuul.filter;
- //省略import
- @Slf4j
- @ConditionalOnBean(RedisRateLimitImpl.class)
- @Component
- public class SeckillRateLimitFilter extends ZuulFilter
- {
- /**
- *Redis限流服务实例
- */
- @Resource(name = "redisRateLimitImpl")
- RateLimitService redisRateLimitImpl;
- @Override
- public String filterType()
- {
- return "pre"; //路由之前
- }
- /**
- *过滤的顺序
- */
- @Override
- public int filterOrder()
- {
- return 0;
- }
- /**
- *这里可以编写逻辑判断是否要过滤,true为永远过滤
- */
- @Override public boolean shouldFilter()
- {
- RequestContext ctx = RequestContext.getCurrentContext();
- HttpServletRequest request = ctx.getRequest();
- /**
- *如果请求已经被其他的过滤器终止,本过滤器就不做处理
- **/
- if (!ctx.sendZuulResponse())
- {
- return false;
- }
- /**
- *对秒杀令牌进行限流
- */
- if (request.getRequestURI().startsWith
- ("/seckill-provider/api/seckill/redis/token/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)
- {
- String cacheKey = "seckill:" + goodId;
- Boolean limited = redisRateLimitImpl.tryAcquire(cacheKey);
- if (limited)
- {
- /**
- *被限流后的降级
- */
- String msg = "参与抢购的人太多,请稍后再试一试";
- fallback(ctx, msg);
- return null;
- }
- return null;
- } else
- {
- /**
- *参数输入错误时的降级处理
- */
- String msg = "必须输入抢购的商品";
- fallback(ctx, msg);
- return null;
- }
- }
- /**
- *被限流后的降级处理
- *
- *@param ctx
- *@param msg
- */
- private void fallback(RequestContext ctx, String msg)
- {
- ctx.setSendZuulResponse(false);
- try
- {
- ctx.getResponse().setContentType("text/html;charset=utf-8");
- ctx.getResponse().getWriter().write(msg);
- } catch (Exception e)
- { e.printStackTrace();
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。