3.限流
如果有百万用户进行抢购,商品数量远远低于用户数量,如果我们使用请求入队列或者查询缓存。 对于最终结果没有任何意义。 我们要减少资源浪费, 减轻我们得后端压力。 我们对秒杀服务进行限流。
限流算法:
任何限流不是漫无目的。 常用限流算法 令牌桶 漏桶。
3.1 令牌桶
使用谷歌Guava的RateLimter提供基于令牌桶算法的实现类。 可以非常简单的完成限流特技。 并且可以根据我们系统的实际情况来调整我们的速率。
代码:
package com.etc.access;
import com.alibaba.fastjson.JSON;
import com.etc.common.AbnoNum;
import com.etc.common.ResultBean;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author Mr.findelist
* @program: seckillplus
* @Date 2020/8/11 10:44
* 使用谷歌Guava的RateLimter提供基于令牌桶算法的实现类。 可以非常简单的完成限流特技。
* 并且可以根据我们系统的实际情况来调整我们的速率。
**/
@Component
@Aspect
public class RateLimitAspect {
/**
* //用来存放不同接口 的令牌桶 (key 借口名字 value 令牌桶)
* // 线程安全的集合
*/
private ConcurrentHashMap<String, RateLimiter> map = new ConcurrentHashMap<>();
@Autowired
private HttpServletResponse response;
@Pointcut("execution(public * com.etc.controller.*.*(..))")
public void serciceLimit() {
}
/**
* 令牌 限流 谷歌
*/
private RateLimiter rateLimiter;
@Around("serciceLimit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = null;
//获取拦截的方法的名字
Signature sig = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
// 2处理目标对象
Object target = joinPoint.getTarget();
// 3获取注解的信息
Method method = target.getClass().getMethod(sig.getName(), methodSignature.getParameterTypes());
// 4获取美妙令牌的数量
RateLimite rate = method.getAnnotation(RateLimite.class);
if (rate == null) {
return joinPoint.proceed();
}
// 5校验令牌数量
int limitNum = rate.limitNum();
checkRateLimitNum(limitNum);
// 7获取接口的名字
String name = methodSignature.getName();
// 8判断map是否包含 接口的名字key
if (map.containsKey(name)) {
rateLimiter = map.get(name);
} else {
map.put(name, RateLimiter.create(limitNum));
rateLimiter = map.get(name);
}
try {
if (rateLimiter.tryAcquire()) {
obj = joinPoint.proceed();
} else {
render(AbnoNum.ACCESS_LIMIT_FREQUENTLY);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return obj;
}
private void render(AbnoNum cm) throws IOException {
response.setContentType("application/json; charset=UTF-8");
OutputStream outputStream = response.getOutputStream();
String str = JSON.toJSONString(ResultBean.error(cm));
outputStream.write(str.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
/**
* 检验令牌
*/
private void checkRateLimitNum(int linmit) {
if (linmit <= 0) {
throw new IllegalArgumentException("注解rate错误");
}
}
}
3.2 漏桶
3.3 应用限流
- Tomcat :在tomcat 容器中我们可以通过自定义线程池。 配置最大得链接数。 请求处理队列等参数来达到限流得目的。
<Executor name="tomcatThreadPool"
namePrefix="catalina-exec-"
maxThreads="150"
minSpareThreads="4"/>
name:线程名字
namePrefix: 每个允许线程有一个name字符串 ,前缀
maxThreads: 该线程可以容纳得最大得链接数: 默认150
minSpareThreads: 打开得不活跃得线程数
配置Connector
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
minProcessors="5"
maxProcessors="75"
acceptCount="1000"
/>
executor: 表示使用该参数值对应得线程池
minProcessors: 服务器在启动得时候处理得线程数
maxProcessors: 最大可以创建得线程数
acceptCount: 指定得所有可以处理的请求的线程数被使用是 。 可以放入到队列的请求数。 如果超过就不予处理。
3.4分布式限流:
Nginx
如何使用nginx的基本限流, 比如烁单个ip地址每秒可以访问 20次,nginx的限流模块。 一旦我们使用了限流的模块。 超过将返回503错误给客户端
配置nginx.conf
#统一在http域中进行配置
#限制请求
limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s;
#按ip配置一个连接 zone
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
#按server配置一个连接 zone
limit_conn_zone $server_name zone=perserver_conn:100m;
server {
listen 80;
server_name seckill.52itstyle.com;
index index.jsp;
location / {
#请求限流排队通过 burst默认是0
limit_req zone=api_read burst=5;
#连接数限制,每个IP并发请求为2
limit_conn perip_conn 2;
#服务所限制的连接数(即限制了该server并发连接数量)
limit_conn perserver_conn 1000;
#连接限速
limit_rate 100k;
proxy_pass http://seckill;
}
}
upstream seckill {
fair;
server 172.16.1.120:8080 weight=1 max_fails=2 fail_timeout=30s;
server 172.16.1.130:8080 weight=1 max_fails=2 fail_timeout=30s;
}
3.5OpenResty
人家也有一些开源的限流的方案。 自带了一个叫 lua_resty_limit_traffic 模块。使用起来更加方便了。
限制接口总并发数/请求数
显示接口时间窗的请求数
代码如下:
local limit_conn = require "resty.limit.conn"
--限制200个并发请求下的请求和一个100个并发的额外的突发请求。也就是我们延迟
--请求300个并发连接以内以及200个以上连接,并拒绝任何超过300的新请求连接。
--另外,我们假设默认请求时间为0.5秒,也就是说通过下面的log_by_lua中的leaving()调用动态调整。
--以上是官网给的配置参数的的说明介绍。("my_limit_conn_store", 200, 100, 0.5) 这个是官网给的参数
--我们可以调整参数为如下("my_limit_conn_store", 1, 0, 0.5)
-- 限制一个 ip 客户端最大 1 个并发请求
-- burst 设置为 0,如果超过最大的并发请求数,则直接返回503,
-- 如果此处要允许突增的并发数,可以修改 burst 的值(漏桶的桶容量)
-- 最后一个参数其实是你要预估这些并发(或者说单个请求)要处理多久,以便于对桶里面的请求应用漏桶算法
local lim, err = limit_conn.new("my_limit_conn_store", 200, 100, 0.5)
if not lim then
ngx.log(ngx.ERR,
"failed to instantiate a resty.limit.conn object: ", err)
return ngx.exit(500)
end
--以下调用必须是每个请求。 这里我们使用远程(IP)地址作为限制key
-- commit 为true 代表要更新shared dict中key的值,
-- false 代表只是查看当前请求要处理的延时情况和前面还未被处理的请求数
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
---- 如果请求连接计数等信息被加到shared dict中,则在ctx中记录下,
-- 因为后面要告知连接断开,以处理其他连接
if lim:is_committed() then
local ctx = ngx.ctx
ctx.limit_conn = lim
ctx.limit_conn_key = key
ctx.limit_conn_delay = delay
end
local conn = err
-- 其实这里的 delay 肯定是上面说的并发处理时间的整数倍,
-- 举个例子,每秒处理100并发,桶容量200个,当时同时来500个并发,则200个拒掉
-- 100个在被处理,然后200个进入桶中暂存,被暂存的这200个连接中,0-100个连接其实应该延后0.5秒处理,
-- 101-200个则应该延后0.5*2=1秒处理(0.5是上面预估的并发处理时间)
if delay >= 0.001 then
--请求超过200连接比但低于300个连接,所以我们故意将它延迟到这里以符合200连接限制。
ngx.sleep(delay)
end
代码实现:
/**
* 获取秒杀路径
* @param user
* @param goodsId
* @return
*/
@GetMapping("path")
@ResponseBody
public Result getMiaoShaPath(MiaoShaUser user, @RequestParam("goodsId") long goodsId,HttpServletRequest request) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//查询访问次数5秒 5次
String url = request.getRequestURI();
String key = url + user.getId();
Integer count = redisService.get(AccessKey.getUserUrlCount, key, Integer.class);
if (count == null){
redisService.set(AccessKey.getUserUrlCount, key, 1);
}else if(count < 5){
redisService.incr(AccessKey.getUserUrlCount, key);
}else {
return Result.error(CodeMsg.ACCESS_LIMIT_FREQUENTLY);
}
String miaoShaPath = iMiaoShaOrderService.createMiaoShaPath(user.getId(), goodsId);
return Result.success(miaoShaPath);
}
3.6 计数器方式
普通实现
@AccessLimit(seconds = 5,maxConunt = 5)
@GetMapping("path")
@ResponseBody
public Result getMiaoShaPath(MiaoShaUser user, @RequestParam("goodsId") long goodsId,HttpServletRequest request) {
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
// //查询访问次数5秒 5次
// String url = request.getRequestURI();
// String key = url + user.getId();
//
// Integer count = redisService.get(AccessKey.getUserUrlCount, key, Integer.class);
// if (count == null){
// redisService.set(AccessKey.getUserUrlCount, key, 1);
// }else if(count < 5){
// redisService.incr(AccessKey.getUserUrlCount, key);
// }else {
// return Result.error(CodeMsg.ACCESS_LIMIT_FREQUENTLY);
// }
String miaoShaPath = iMiaoShaOrderService.createMiaoShaPath(user.getId(), goodsId);
return Result.success(miaoShaPath);
}
注解拦截器方式:
自定义注解
package com.etc.access;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author Mr.findelist
* @program: seckillplus
* @Date 2020/8/10 15:08
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
//限流时间
int seconds();
//限流数量
int maxCount();
//判断是否必须登录
boolean nendLogin() default true;
}
配置拦截器
package com.etc.config;
import com.etc.access.AccessInterCeptor;
import com.etc.access.AccessLimit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @Author kalista
* aDescription自动义拦截器
* addInterceptors:拦截器
* addViewControllers:页面跳转
* addResourceHandLers:静态资源
* configureDefaultServletHandling:默认静态资源处理器
* configureViewResolvers:视图解析器
* configureContentNegotiation:配置内容裁决的一些参数
* addCorsMappings:跨域
* configureMessageConverters:信息转换器
* addArgumentResolvers自定义参数处理器
* @Date 2020/ 7/2016:53
*/
@Configuration
public class MvcConf implements WebMvcConfigurer {
@Autowired
private UserArgumentResolvers userArgumentResolvers;
@Autowired
private AccessInterCeptor accessInterCeptor;
/**
* 自定义参数处理器(不需要)
* @param resolvers
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolvers);
}
/**
* 拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterCeptor);
}
/**
* 设置默认登录页面(不需要)
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("index.html").setViewName("login");
registry.addViewController("login.html").setViewName("login");
}
}
判断令牌
package com.etc.access;
import com.alibaba.fastjson.JSON;
import com.etc.common.AbnoNum;
import com.etc.common.ResultBean;
import com.etc.domian.User;
import com.etc.redis.AccessKey;
import com.etc.redis.RedisService;
import com.etc.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
/**
* @Author Mr.findelist
* @program: seckillplus
* @Date 2020/8/10 15:12
**/
@Service
public class AccessInterCeptor extends HandlerInterceptorAdapter {
@Autowired
private UserServiceImpl userService;
@Autowired
private RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断是否来自方法体
if (handler instanceof HandlerMethod) {
User user = getUser(request, response);
HandlerMethod hm = (HandlerMethod) handler;
//包含哪个注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
//获取注解的值
int count = accessLimit.maxCount();
int seconds = accessLimit.seconds();
boolean nedlogin = accessLimit.nendLogin();
String key = request.getRequestURL().toString();
//用户登录状态
if (nedlogin) {
if (user == null) {
render(response, AbnoNum.USERNAME_IS_ENIST);
}
key += "_" + user.getId();
}
//从缓存获取key 次数
Integer cou = redisService.get(AccessKey.getUserUrlcount, key, Integer.class);
AccessKey accessKey = new AccessKey(seconds, "access:");
//没有就加入缓存 小于限流次数就加一 超过就 退出
if (cou == null) {
redisService.set(accessKey, key, 1);
} else if (cou < count) {
redisService.incr(accessKey, key);
} else {
render(response, AbnoNum.ACCESS_LIMIT_FREQUENTLY);
return false;
}
}
return true;
}
public void render(HttpServletResponse response, AbnoNum abnoNum) throws IOException {
//写入信息到页面上
response.setContentType("application/json; charset=UTF-8");
OutputStream outputStream = response.getOutputStream();
String s = JSON.toJSONString(ResultBean.error(abnoNum));
outputStream.write(s.getBytes("utf-8"));
outputStream.flush();
outputStream.close();
}
public User getUser(HttpServletRequest request, HttpServletResponse response) {
String parameter = request.getParameter("token");
String cookieValue = getCookieValue(request, "token");
if (StringUtils.isEmpty(parameter) && StringUtils.isEmpty(cookieValue)) {
return null;
}
String cookie = StringUtils.isEmpty(cookieValue) ? parameter : cookieValue;
return userService.getcookie(response, cookie);
}
/**
* 获取cookie中token值
*/
private String getCookieValue(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return null;
}
}