实时解答SpringCloud+Nginx秒杀实战,Zuul内部网关实现秒杀限流
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所示。
图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();
}
}
}