Zuul内部网关实现秒杀限流

秒杀限流操作既可以在内部网关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所示。

nginx lua 网关认证_nginx lua 网关认证

图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所示。

nginx lua 网关认证_限流_02

图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();
 }
 }
}