这是一个或许对你有用的社群 一对一交流/面试小册/简历优化/求职解惑欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料《项目实战视频》从书中学往事中“练”《互联网高频面试题》面朝简历学习春暖花开《架构 x 系统设计》摧枯拉朽掌控面试高频场景题《精进 Java 学习指南》系统学习互联网主流技术栈《必读 Java 源码专栏》知其然知其所以然这是一个或许对你有用的开源项目国产Star破10w的开源项目前端包括管理后台、微信小程序后端支持单体、微服务架构RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、IoT物联网等功能多模块https://gitee.com/zhijiantianya/ruoyi-vue-pro微服务https://gitee.com/zhijiantianya/yudao-cloud视频教程https://doc.iocoder.cn【国内首批】支持 JDK17/21SpringBoot3、JDK8/11Spring Boot2双版本一、接口幂等性1、Update操作的幂等性2、使用Token机制保证update、insert操作的幂等性二、分布式限流1、分布式限流的几种维度2、限流方案常用算法讲解3、分布式限流的主流方案一、接口幂等性接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的不会因为多次点击而产生了副作用。举个最简单的例子那就是支付用户购买商品后支付支付扣款成功但是返回结果的时候网络异常此时钱已经扣了用户再次点击按钮此时会进行第二次扣款返回结果成功用户查询余额返发现多扣钱了流水记录也变成了两条,这就没有保证接口的幂等性。幂等性的核心思想通过唯一的业务单号保障幂等性非并发的情况下查询业务单号有没有操作过没有则执行操作并发情况下这个操作过程需要加锁。1、Update操作的幂等性1根据唯一业务号去更新数据通过版本号的方式来控制update的操作的幂等性用户查询出要修改的数据系统将数据返回给页面将数据版本号放入隐藏域用户修改数据点击提交将版本号一同提交给后台后台使用版本号作为更新条件update set version version 1 ,xxx${xxx} where id xxx and version ${version};2、使用Token机制保证update、insert操作的幂等性1没有唯一业务号的update与insert操作进入到注册页时后台统一生成Token 返回前台隐藏域中用户在页面点击提交时将Token一同传入后台使用Token获取分布式锁完成Insert操作执行成功后不释放锁等待过期自动释放。基于 Spring Boot MyBatis Plus Vue Element 实现的后台管理系统 用户小程序支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能项目地址https://github.com/YunaiV/ruoyi-vue-pro视频教程https://doc.iocoder.cn/video/二、分布式限流1、分布式限流的几种维度时间 限流基于某段时间范围或者某个时间点也就是我们常说的“时间窗口”比如对每分钟、每秒钟的时间窗口做限定 资源 基于可用资源的限制比如设定最大访问次数或最高可用连接数上面两个维度结合起来看限流就是在某个时间窗口对资源访问做限制比如设定每秒最多100个访问请求。但在真正的场景里我们不止设置一种限流规则而是会设置多个限流规则共同作用主要的几种限流规则如下1QPS和连接数控制针对上图中的连接数和QPS(query per second)限流来说我们可以设定IP维度的限流也可以设置基于单个服务器的限流。在真实环境中通常会设置多个维度的限流规则比如设定同一个IP每秒访问频率小于10连接数小于5再设定每台机器QPS最高1000连接数最大保持200。更进一步我们可以把某个服务器组或整个机房的服务器当做一个整体设置更high-level的限流规则这些所有限流规则都会共同作用于流量控制。2传输速率对于“传输速率”大家都不会陌生比如资源的下载速度。有的网站在这方面的限流逻辑做的更细致比如普通注册用户下载速度为100k/s购买会员后是10M/s这背后就是基于用户组或者用户标签的限流逻辑。3黑白名单黑白名单是各个大型企业应用里很常见的限流和放行手段而且黑白名单往往是动态变化的。举个例子如果某个IP在一段时间的访问次数过于频繁被系统识别为机器人用户或流量攻击那么这个IP就会被加入到黑名单从而限制其对系统资源的访问这就是我们俗称的“封IP”。我们平时见到的爬虫程序比如说爬知乎上的美女图片或者爬券商系统的股票分时信息这类爬虫程序都必须实现更换IP的功能以防被加入黑名单。有时我们还会发现公司的网络无法访问12306这类大型公共网站这也是因为某些公司的出网IP是同一个地址因此在访问量过高的情况下这个IP地址就被对方系统识别进而被添加到了黑名单。使用家庭宽带的同学们应该知道大部分网络运营商都会将用户分配到不同出网IP段或者时不时动态更换用户的IP地址。白名单就更好理解了相当于御赐金牌在身可以自由穿梭在各种限流规则里畅行无阻。比如某些电商公司会将超大卖家的账号加入白名单因为这类卖家往往有自己的一套运维系统需要对接公司的IT系统做大量的商品发布、补货等等操作。4分布式环境所谓的分布式限流其实道理很简单一句话就可以解释清楚。分布式区别于单机限流的场景它把整个分布式环境中所有服务器当做一个整体来考量。比如说针对IP的限流我们限制了1个IP每秒最多10个访问不管来自这个IP的请求落在了哪台机器上只要是访问了集群中的服务节点那么都会受到限流规则的制约。从上面的例子不难看出我们必须将限流信息保存在一个“中心化”的组件上这样它就可以获取到集群中所有机器的访问状态目前有两个比较主流的限流方案网关层限流将限流规则应用在所有流量的入口处中间件限流将限流信息存储在分布式环境中某个中间件里比如Redis缓存每个组件都可以从这里获取到当前时刻的流量统计从而决定是拒绝服务还是放行流量2、限流方案常用算法讲解1令牌桶算法Token Bucket令牌桶算法是目前应用最为广泛的限流算法顾名思义它有以下两个关键角色令牌 获取到令牌的Request才会被处理其他Requests要么排队要么被直接丢弃桶 用来装令牌的地方所有Request都从这个桶里面获取令牌令牌生成这个流程涉及到令牌生成器和令牌桶前面我们提到过令牌桶是一个装令牌的地方既然是个桶那么必然有一个容量也就是说令牌桶所能容纳的令牌数量是一个固定的数值。对于令牌生成器来说它会根据一个预定的速率向桶中添加令牌比如我们可以配置让它以每秒100个请求的速率发放令牌或者每分钟50个。注意这里的发放速度是匀速也就是说这50个令牌并非是在每个时间窗口刚开始的时候一次性发放而是会在这个时间窗口内匀速发放。在令牌发放器就是一个水龙头假如在下面接水的桶子满了那么自然这个水令牌就流到了外面。在令牌发放过程中也一样令牌桶的容量是有限的如果当前已经放满了额定容量的令牌那么新来的令牌就会被丢弃掉。令牌获取每个访问请求到来后必须获取到一个令牌才能执行后面的逻辑。假如令牌的数量少而访问请求较多的情况下一部分请求自然无法获取到令牌那么这个时候我们可以设置一个“缓冲队列”来暂存这些多余的令牌。缓冲队列其实是一个可选的选项并不是所有应用了令牌桶算法的程序都会实现队列。当有缓存队列存在的情况下那些暂时没有获取到令牌的请求将被放到这个队列中排队直到新的令牌产生后再从队列头部拿出一个请求来匹配令牌。当队列已满的情况下这部分访问请求将被丢弃。在实际应用中我们还可以给这个队列加一系列的特效比如设置队列中请求的存活时间或者将队列改造为PriorityQueue根据某种优先级排序而不是先进先出。算法是死的人是活的先进的生产力来自于不断的创造在技术领域尤其如此。2漏桶算法Leaky Bucket漏桶算法的前半段和令牌桶类似但是操作的对象不同令牌桶是将令牌放入桶里而漏桶是将访问请求的数据包放到桶里。同样的是如果桶满了那么后面新来的数据包将被丢弃。漏桶算法的后半程是有鲜明特色的它永远只会以一个恒定的速率将数据包从桶内流出。打个比方如果我设置了漏桶可以存放100个数据包然后流出速度是1s一个那么不管数据包以什么速率流入桶里也不管桶里有多少数据包漏桶能保证这些数据包永远以1s一个的恒定速度被处理。漏桶 vs 令牌桶的区别根据它们各自的特点不难看出来这两种算法都有一个“恒定”的速率和“不定”的速率。令牌桶是以恒定速率创建令牌但是访问请求获取令牌的速率“不定”反正有多少令牌发多少令牌没了就干等。而漏桶是以“恒定”的速率处理请求但是这些请求流入桶的速率是“不定”的。从这两个特点来说漏桶的天然特性决定了它不会发生突发流量就算每秒1000个请求到来那么它对后台服务输出的访问速率永远恒定。而令牌桶则不同其特性可以“预存”一定量的令牌因此在应对突发流量的时候可以在短时间消耗所有令牌其突发流量处理效率会比漏桶高但是导向后台系统的压力也会相应增多。3、分布式限流的主流方案这里主要讲nginx和lua的限流gateway和hystrix放在后面springcloud中讲1Guava RateLimiter客户端限流1.引入mavendependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version18.0/version /dependency2.编写ControllerRestController Slf4j publicclass Controller{ //每秒钟可以创建两个令牌 RateLimiter limiter RateLimiter.create(2.0); //非阻塞限流 GetMapping(/tryAcquire) public String tryAcquire(Integer count){ //count 每次消耗的令牌 if(limiter.tryAcquire(count)){ log.info(成功允许通过速率为{},limiter.getRate()); returnsuccess; }else{ log.info(错误不允许通过速率为{},limiter.getRate()); returnfail; } } //限定时间的非阻塞限流 GetMapping(/tryAcquireWithTimeout) public String tryAcquireWithTimeout(Integer count, Integer timeout){ //count 每次消耗的令牌 timeout 超时等待的时间 if(limiter.tryAcquire(count,timeout,TimeUnit.SECONDS)){ log.info(成功允许通过速率为{},limiter.getRate()); returnsuccess; }else{ log.info(错误不允许通过速率为{},limiter.getRate()); returnfail; } } //同步阻塞限流 GetMapping(/acquire) public String acquire(Integer count){ limiter.acquire(count); log.info(成功允许通过速率为{},limiter.getRate()); returnsuccess; } }2基于Nginx的限流1.iP限流1.编写ControllerRestController Slf4j public class Controller{ //nginx测试使用 GetMapping(/nginx) public String nginx(){ log.info(Nginx success); } }2.修改host文件添加一个网址域名127.0.0.1 www.test.com3.修改nginx将步骤2中的域名添加到路由规则当中打开nginx的配置文件vim /usr/local/nginx/conf/nginx.conf添加一个服务#根据IP地址限制速度 #1$binary_remote_addr binary_目的是缩写内存占用remote_addr表示通过IP地址来限流 #2zoneiplimit:20m iplimit是一块内存区域记录访问频率信息20m是指这块内存区域的大小 #3rate1r/s 每秒放行1个请求 limit_req_zone $binary_remote_addr zoneiplimit:20m rate1r/s; server{ server_name www.test.com; location /access-limit/ { proxy_pass http://127.0.0.1:8080/; #基于ip地址的限制 #1zoneiplimit 引用limit_rep_zone中的zone变量 #2burst2 设置一个大小为2的缓冲区域当大量请求到来请求数量超过限流频率时将其放入缓冲区域 #3nodelay 缓冲区满了以后直接返回503异常 limit_req zoneiplimit burst2 nodelay; } }4.访问地址测试是否限流www.test.com/access-limit/nginx2.多维度限流1.修改nginx配置#根据IP地址限制速度 limit_req_zone $binary_remote_addr zoneiplimit:20m rate10r/s; #根据服务器级别做限流 limit_req_zone $server_name zoneserverlimit:10m rate1r/s; #根据ip地址的链接数量做限流 limit_conn_zone $binary_remote_addr zoneperip:20m; #根据服务器的连接数做限流 limit_conn_zone $server_name zoneperserver:20m; server{ server_name www.test.com; location /access-limit/ { proxy_pass http://127.0.0.1:8080/; #基于ip地址的限制 limit_req zoneiplimit burst2 nodelay; #基于服务器级别做限流 limit_req zoneserverlimit burst2 nodelay; #基于ip地址的链接数量做限流 最多保持100个链接 limit_conn zoneperip 100; #基于服务器的连接数做限流 最多保持100个链接 limit_conn zoneperserver 1; #配置request的异常返回504默认为503 limit_req_status 504; limit_conn_status 504; } location /download/ { #前100m不限制速度 limit_rate_affer 100m; #限制速度为256k limit_rate 256k; } }3基于RedisLua的分布式限流1.Lua脚本Lua是一个很小巧精致的语言它的诞生1993年甚至比JDK 1.0还要早。Lua是由标准的C语言编写的它的源码部分不过2万多行C代码甚至一个完整的Lua解释器也就200k的大小。Lua往大了说是一个新的编程语言往小了说就是一个脚本语言。对于有编程经验的同学拿到一个Lua脚本大体上就能把业务逻辑猜的八九不离十了。Redis内置了Lua解释器执行过程保证原子性2.Lua安装安装Lua1.参考http://www.lua.org/ftp/教程下载5.3.5_1版本本地安装如果你使用的是Mac那建议用brew工具直接执行brew install lua就可以顺利安装有关brew工具的安装可以参考https://brew.sh/网站使用brew安装后的目录在/usr/local/Cellar/lua/5.3.5_12.安装IDEA插件在IDEA-Preferences面板Plugins里面Browse repositories在里面搜索lua然后就选择同名插件lua。安装好后重启IDEA3.配置Lua SDK的位置IDEA-File-Project Structure,选择添加Lua路径指向Lua SDK的bin文件夹4.都配置好之后在项目中右键创建Module左侧栏选择lua点下一步选择lua的sdk下一步输入lua项目名完成3.编写hello luaprint Hello Lua4.编写模拟限流-- 模拟限流 -- 用作限流的key local key my key -- 限流的最大阈值 local limit 2 -- 当前限流大小 local currentLimit 2 -- 是否超过限流标准 if currentLimit 1 limit then printreject returnfalse else printaccept returntrue end5.限流组件封装1.添加mavendependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version18.0/version /dependency2.添加Spring配置不是重要内容就随便写点主要就是把reids配置一下server.port8080 spring.redis.database0 spring.redis.hostlocalhost spring.redis.port63763.编写限流脚本lua脚本放在resource目录下就可以了-- 获取方法签名特征 local methodKey KEYS[1] redis.log(redis.LOG_DEBUG,key is,methodKey) -- 调用脚本传入的限流大小 local limit tonumber(ARGV[1]) -- 获取当前流量大小 local count tonumber(redis.call(get,methodKey) or0) --是否超出限流值 if count 1 limit then -- 拒绝访问 returnfalse else -- 没有超过阈值 -- 设置当前访问数量1 redis.call(INCRBY,methodKey,1) -- 设置过期时间 redis.call(EXPIRE,methodKey,1) -- 放行 returntrue end4.使用spring-data-redis组件集成Lua和Redis创建限流类Service Slf4j publicclass AccessLimiter{ Autowired private StringRedisTemplate stringRedisTemplate; Autowired private RedisScriptBoolean rateLimitLua; public void limitAccess(String key,Integer limit){ boolean acquired stringRedisTemplate.execute( rateLimitLua,//lua脚本的真身 Lists.newArrayList(key),//lua脚本中的key列表 limit.toString()//lua脚本的value列表 ); if(!acquired){ log.error(Your access is blocked,key{},key); thrownew RuntimeException(Your access is blocked); } } }创建配置类Configuration public class RedisConfiguration{ public RedisTemplateString,String redisTemplate(RedisConnectionFactory factory){ return new StringRedisTemplate(factory); } public DefaultRedisScript loadRedisScript(){ DefaultRedisScript redisScript new DefaultRedisScript(); redisScript.setLocation(new ClassPathResource(rateLimiter.lua)); redisScript.setResultType(java.lang.Boolean.class); return redisScript; } }5.在Controller中添加测试方法验证限流效果RestController Slf4j public class Controller{ Autowired private AccessLimiter accessLimiter; GetMapping(test) public String test(){ accessLimiter.limitAccess(ratelimiter-test,1); return success; } }6.编写限流注解1.新增注解Target({ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented public interface AccessLimiterAop{ int limit(); String methodKey() default ; }2.新增切面Slf4j Aspect Component publicclass AccessLimiterAspect{ Autowired private AccessLimiter accessLimiter; //根据注解的位置自己修改 Pointcut(annotation(com.gyx.demo.annotation.AccessLimiter)) public void cut(){ log.info(cut); } Before(cut()) public void before(JoinPoint joinPoint){ //获取方法签名作为methodkey MethodSignature signature (MethodSignature) joinPoint.getSignature(); Method method signature.getMethod(); AccessLimiterAop annotation method.getAnnotation(AccessLimiterAop.class); if(annotation null){ return; } String key annotation.methodKey(); Integer limit annotation.limit(); //如果没有设置methodKey就自动添加一个 if(StringUtils.isEmpty(key)){ Class[] type method.getParameterType(); key method.getName(); if (type ! null){ String paramTypesArrays.stream(type) .map(Class::getName) .collect(Collectors.joining(,)); key #paramTypes; } } //调用redis return accessLimiter.limitAccess(key,limit); } }3.在Controller中添加测试方法验证限流效果RestController Slf4j public class Controller{ Autowired private AccessLimiter accessLimiter; GetMapping(test) AccessLImiterAop(limit 1) public String test(){ return success; } }欢迎加入我的知识星球全面提升技术能力。 加入方式“长按”或“扫描”下方二维码噢星球的内容包括项目实战、面试招聘、源码解析、学习路线。文章有帮助的话在看转发吧。 谢谢支持哟 (*^__^*