
2023年8月通过xianling哥的讲解终于大彻大悟分布式系统了分布式出现的思想为了用廉价的、普通的机器完成昂贵的大型主机无法完成的计算、存储任务。其目的是利用更多的机器处理更多的数据。一、其实你早就接触了什么是分布式、什么是中间件但凡有两台服务器不在一起就是分布式系统。分布式不是近几年出现的技术在2000年初期阿里的业务一个服务器就放不下了那时候就在研究分布式了。单体架构 VS 分布式架构缺点但凡是一个标点符号写错了都要整个系统重新编译发布维护成本太高一个系统的所有功能必须使用同一种语言开发如果想要换语言就要全部推翻重来。但是分布式系统的不同功能模块可以用不同的语言开发比如商品模块用Java订单模块用go语言因为消息中间件可以屏蔽语言之间的差异。二、理解分布式计算和分布式存储二者区别分布式计算应用服务器的解耦。比如阿里巴巴以前只用一台服务器做用户注册、订单、支付、售后等所有的服务。现在拆分成用户注册服务器、订单提交服务器、支付服务器等。以前不注重服务的可用性的时候订单系统可能只部署一个。但是我们现在考虑万一这个宕掉了怎么办 所以同一个服务至少要部署两个这里服务的意思就是整个project部署的时候不是以接口为单位而是以project为单位。当然部署了多个project那么一个接口也就有多个咯当然这两个可以在同一台机器上也可以在不同的机器上。一般是在不同的机器上因为万一机器宕掉了两个在同一台机器上的话不就都完蛋了吗分布式存储就是mysql集群。数据量太大了单机mysql放不了了就把数据分库分表放到10台mysql服务器上每台服务器各存1/10的数据。三、CAP和BASE理论四、分布式系统进程间通信分布式系统的核心分布式的核心内容是IPC进程间通信注意是服务器和服务器之间的通信有如下几种方式RPC 远程过程调用屏蔽底层细节调用远程方法就像调用本地方法一样同步client stub 和 server stubRMC 远程方法调用同步MOM 消息中间件不要求双方在线异步HTTPspring cloud NetFlix就是用HTTP通信既然是两台机器之间的通信为什么不直接用TCP/IP首先不是不用TCP/IP而是把TCP/IP作为通信的底层在TCP/IP之上构建RPC、RMC、和MOM的框架。因为TCP/IP的字段不能满足进程间的需求我们要新增一些字段所以就要在TCP/IP协议的基础上构造中间件自己的协议。换句话说消息中间件的底层协议还是TCP/IP协议其实可以用HTTP请求但是我们不用道理很简单比如出门防晒你其实可以把塑料袋套头上但是哪有戴帽子合适呀不用HTTP实现两个机器的通信是因为HTTP一般用于客户端对服务器的一次性请求建立短链接。但是分布式系统的进程间通信不是客户端和服务器的通信而是多个服务器之间的通信。比如有一个订单业务服务器一个会员业务服务器二者之间如何交互这是一个长期通信的过程需要建立长链接。我们需要在TCP/IP基础上自己重新构建一种通信框架也就是RPC、RMC和MOM。MOM分类分布式消息中间件ActiveMQ不用了RabbitMQ开源和spring是一家开发出来的和spring的适配性最高美团、滴滴都在用Kafka性能最好但不支持事务京东的JMQ就是在kafaka外面套了一个壳RocketMQ阿里和滴滴自研的消息中间件MOM理解RPC和RMC通信的缺点要求client和server同时在线client会被阻塞什么都不能干这是同步的一种特点异步的话就是两个线程各干各的谁也不需要等谁MOM的优点不要求client和server同时在线延迟隐藏。client请求完了以后直接去做下一件的任务感受不到client的阻塞MOM 都有哪些协议MOM如何实现的异步借助消息队列。MOM工作流程不同消息格式的转换client订购消息server推送消息五、分布式系统下如何生成UUID林哥给出的生成UUID唯一的方法法一MYSQL层校验法二在service层生成UUID去MySQL校验是否存在存在再重新生成for循环首先要明确一下一提到UUID本身指的就是分布式系统下的唯一ID在单体系统中我们可以用时间戳也就是当前时间距离1970年1月1日零点的毫秒数的方式实现ID但是也会存在同一毫秒内产生两个实体的情况所以我们采用毫秒数随机数浪潮就是这么实现的但是这样做缺少了验证这一步虽然99.9999%是唯一的但是还是不能保证百分之百哦所以林哥才会反复提到“MySQL层校验”。生成分布式UUID的常见方案借助数据库主键redis算法就是想个办法拼凑出来全局唯一的IDjava原生的UUID不推荐使用但是jd也在用改进java原生的UUIDjava 原生的UUID为36位 or 32位,太长. 这里提供一个位数较短的UUID. * UUID生成规则,当前时间减去零时的毫秒数 N位随机数,转变成62进制的String类型. * 当前配置可满足30年内每毫秒10^9分之一的碰撞. * 实测现在长度为13位,想要更短的话可以调整下方的几个参数Twitter开源的雪花算法没有两篇雪花是相同的。用时间戳机房ID机器ID每台机器上的自增数合成美团开源的落叶算法同样“没有两片相同的落叶”car-fence用的糊涂工具包hutool法一数据库主键自增如何让数据库主键自增呢就要每次索要UUID的时候向表里插入一条数据当然这条数据本身没有任何意义只是为了推动主键自增然后把生成的主键返回给java程序CREATE TABLE sequence_id ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, stub char(10) NOT NULL DEFAULT , PRIMARY KEY (id), UNIQUE KEY stub (stub) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;优点实现简单仅此而已缺点每次获取UUID都要访问数据库访问数据库可比访问redis缓存慢多了由此可见这种实现方式比不上接下来要将的借助redis实现。ID没有业务含义通过主键看不出这是个啥可以通过主键的差值判断一天产生了多少订单量这可是商业机密呀淘宝用户有上亿个每个用户都有UUID一个表肯定放不下那么就要分库分表分库分表也挺累的。法二数据库号段其实这个我没看懂哈批量获取然后存在在内存里面需要用到的时候直接从内存里面拿就舒服了主要是减少了访问数据库的次数。数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的Tinyidopen in new window 就是基于这种方式来做的。不过TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化1. 创建一个数据库表。CREATE TABLE sequence_id_generator ( id int(10) NOT NULL, current_max_id bigint(20) NOT NULL COMMENT 当前最大id, step int(10) NOT NULL COMMENT 号段的长度, version int(20) NOT NULL COMMENT 版本号, biz_type int(20) NOT NULL COMMENT 业务类型, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;一次获取step长度的id获取的批量 id 为current_max_id ~ current_max_idstepversion字段主要用于解决并发问题乐观锁2. 先插入一行数据INSERT INTO sequence_id_generator (id, current_max_id, step, version, biz_type) VALUES (1, 0, 100, 0, 101);3.通过 SELECT 获取指定业务下的批量唯一 ID业务和业务之间的UUID是独立的可以重复。可能后续会把biz_type字段拼到UUID里SELECT current_max_id, step,version FROM sequence_id_generator where biz_type 1014. 不够用的话更新之后重新 SELECT 即可。UPDATE sequence_id_generator SET current_max_id 0100, versionversion1 WHERE version 0 AND biz_type 101 SELECT current_max_id, step,version FROM sequence_id_generator where biz_type 101优点相比于上一个方法减少了访问数据库的次数缺点 就是上一个方法的另外3个缺点ID没有业务含义通过主键看不出这是个啥可以通过主键的差值判断一天产生了多少订单量这可是商业机密呀淘宝用户有上亿个每个用户都有UUID一个表肯定放不下那么就要分库分表分库分表也挺累的。法三redis的incr自增incr UUID法四 算法们上面也都说过了六、分布式锁先来看一个用Redis实现分布式锁的完整代码public ListNode getDataByRedisLock(){ ListNode nodeList null; //加锁 String uuid UUID.randomUUID().toString(); Boolean lock redisTemplate.opsForValue().setIfAbsent(redis-lock, uuid,100,TimeUnit.SECONDS); if (lock){ try { nodeList getNodesByMysql(); }finally { //解锁 //lua脚本 String script if redis.call(\get\,KEYS[1]) ARGV[1] then return redis.call(\del\,KEYS[1]) else return 0 end; redisTemplate.execute( new DefaultRedisScriptLong(script,Long.class), Arrays.asList(redis-lock), uuid ); } }else { //注意如果是用synchronize这种本地锁的话 //没获取到锁的时候会自动阻塞然后尝试下一次加锁 //这是synchronize底层帮我们写好了的但是这里redis得靠我们手动实现 try { System.out.println(没有获取锁重试); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //重试 getDataByRedisLock(); } return nodeList; }是不是很简单 虽然接下来我们要讲一大堆分布式锁的原理但是实际使用起来只有两行代码而已当然咯简单是因为底层封装的好我们当然要去看一下底层。分布式锁的实现有很多形式只不过这里选择了用Redis的setIfAbsent方法实现分布式锁底层其实是retranlock是一个悲观锁。6.1 为什么要有分布式锁或者说分布式锁和本地锁的区别在于其独特性在于分布式系统中相同的服务被部署在不同的机器上。单体系统下对多线程加锁生成的这个锁放在JVM运行时数据区里因为所有线程都能查看JVM运行空间所以大家都能访问到。但是分布式系统下我在一台机器上生成一个锁其他机器是访问不到的呀。所以分布式锁必须放在多台机器都能访问到的地方比如Redis、数据库、消息中间件这种所有机器都能访问到的公共存储区。6.2 分布式锁的实现方式常见分布式锁实现方案如下借助关系型数据库比如 MySQL 实现分布式锁×一般是依托于mysql的唯一索引或排它锁去实现性能太差借助分布式协调服务 ZooKeeper 实现分布式锁√借助分布式键值存储系统比如 Redis 实现分布式锁√6.3 我们今天主要讲的是用Redis实现分布式锁1、保证上锁的原子性可以自己先想一下如何实现。肯定不是用watch命令哈watch命令实现的是Redis级别的乐观锁但是我们现在要实现的java程序级别的分布式锁。加锁就是“占坑”告诉别人我已经开始用这个资源了并且加锁之前还要判断别人有没有这个锁是不是想起来了setnx命令因为setnx命令就是在set之前要判断一下这个key是否已经存在了。还有一个问题单体架构下我服务宕机了JVM就会关掉相应的我的锁就消失了。分布式架构下为了避免还没解锁前服务就宕掉了导致这个资源被一直锁着所以我们必须在加锁的同时给这个锁设定过期时间也就是说我们的需求有3个① set一个键值对② 判断是否已经存在③设置过期时间 。且这3个操作不能被打断也就说必须放在同一个原子操作里而我们又知道由于Redis是单线程的所以Redis的一条命令就是一个原子操作那么我们就需要一条命令同时去做上面的事情。setex可以实现吗No好了不绕弯子了Redis命令是这样实现的set 锁的key 锁的value nx ex 50对应到java里Redistemplate帮我们封装了这样一个方法redisTemplate.opsForValue().setIfAbsent(key, value, releaseTime, TimeUnit.SECONDS);1俩问题问题1我们又会想给键值对设置过期时间过期时间设置多久比较好呢万一被锁的资源没用完呢键值对就过期了怎么办会出现两个情况情况①这个时候线程2跑来尝试加锁发现居然加上了那么此时线程1和线程2同时操作资源会出问题的呀情况②而且就算同时操作资源这一步没出问题此时只有一把锁但是线程1和2都还没执行删锁操作线程1会把线程2的锁给删掉没关系还有续约操作~PS: 有的系统不注重性能可能会把过期时间设置的特别大这样它就不用考虑续约了。问题2还有一个问题是主从切换锁可能会丢。为什么会丢呢难道redis的从节点数据不是时刻跟主节点保持一致吗确实不是时刻保持一致。因为redis主从复制用的是RDB快照从节点的数据是主节点的上一个快照版本RDB本身就会丢失修改嘛搬运《Redis五—— Redis进阶部分》的解说试想我们设置一个save 60 10000,60s内发生一万次修改的时候持久化到硬盘但是如果还没到60s发生第9999次修改后宕机了那这9999次修改就丢失了。过期的问题可以避免但主从切换丢失的问题避免不了如果你的系统业务连这种极端情况都无法忍受的话那就不要用redis实现分布式锁了用数据库去实现吧。2、看门狗续约续约操作也很复杂所以直接写了一个第三方包。对于 Java 开发的小伙伴来说已经有了现成的解决方案Redissonopen in new window。其他语言的解决方案可以在 Redis 官方文档中找到地址https://redis.io/docs/manual/patterns/distributed-locks/jar包dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.15.6/version /dependency配置beanConfiguration public class RedissonConfig { Bean public RedissonClient redissonClient(){ Config config new Config(); config.setTransportMode(TransportMode.NIO); SingleServerConfig singleServerConfig config.useSingleServer(); //可以用rediss://来启用SSL连接 singleServerConfig.setAddress(redis://linux100:6379); RedissonClient redisson Redisson.create(config); return redisson; } }redisson使用起来就跟本地锁一样RestController public class UserController { Autowired private RedissonClient redissonClient; GetMapping(/test) public String test() { RLock lock redissonClient.getLock(test-lock); try { System.out.println(加锁成功); Thread.sleep(10000); } catch (Exception e) { e.printStackTrace(); }finally { System.out.println(释放锁成功); lock.unlock(); } return success; } }如果我们不就是不想导入redisson不采用续约的操作呢那就会出现上述两个问题而下面保证删锁原子性的操作只能解决第二个问题还是存在第一种问题的隐患3、保证删锁的原子性如果采用看门狗的话那么直接删就行了完全不用设置UUID理想情况下java线程顺序执行的情况下肯定是用完资源以后用redis的del命令删掉这个锁然后第二个线程继续上锁。但是存在这样一种情况线程1加锁用资源的时候超出了过期时间还没用完因为没采用看门狗续约此时锁过期了线程2尝试加锁发现加上了因为锁是同名的所以线程1最后在finally里执行删锁操作的时候把线程2加的锁给删掉了。解决方法有两个法一对过期时间进行续约这样就不会出现线程1的锁过期线程2来加锁的情况了这是从源头解决问题法二给每个锁配备一个UUID作为value删锁之前用UUID判断一下是不是自己设置的锁对于法二实现如下还会有一个问题我靠这个太难想到了但是操作系统里举过这个例子因为判断UUID和删锁的操作是分两步实现的判断UUID删锁如果线程1判断完UUID确实是自己的可以开始删了这个时候刚好线程1被CPU切换走了刚好线程1的锁过期了另一台机器上的线程2刚好尝试来加锁发现加上了然后CPU调度回线程1的时候线程1直接第二步执行删锁操作又出错了解决方法只有一个就是让判断UUID和删锁的操作不要分成两步而是作为一个原子性操作java实在无能为力了只能借用Lua脚本。七、高性能7.1 分库分表首先根据《阿里巴巴开发手册》【推荐】单表行数超过 500 万行或者单表容量超过 2GB才推荐进行分库分表。说明如果预计三年后的数据量根本达不到这个级别请不要在创建表时就分库分表。两个问题原理上如何实现分库分表工程上如何实现分库分表呢也就是如何写代码1、原理上如何实现分库分表假如我们要把order表分成4库16表。两大核心需要确定的分片键——如何确定根据哪个字段去分库分表首选user_id95%的订单查询都带user_id区分度极高因为订单表一般是用户自己来查订单的场景比较多用user_id去分片让同一个用户的订单数据都落入同一个表里而不是分散到各个表里备选order_id适合纯订单维度查询但是按照订单去查询的场景不多、分片键的选取也是一门学问我理解的是①哪个字段几乎每次查询都带着②并且用这个字段可以把读写流量均匀分到每个表里分片算法分库算法user_id % 44个分库均匀分布分表算法user_id % 16每个库4张表共16张物理分表优势计算简单、数据均匀、扩容可平滑后续一致性哈希升级平均去塞16张表而不是把第一张表塞满了再塞另一个表这样可用分片号表示出4库16表库名 业务库名_分片号2位补零 → order_db_00~ order_db_03表名 逻辑表名_分片号2位补零 → order_00 ~ order_03逻辑库order_db 、逻辑表order ├── 物理库 01order_db_00 │ ├── 物理表order_00 │ ├── 物理表order_01 │ ├── 物理表order_02 │ └── 物理表order_03 │ ├── 物理库 02order_db_01 │ ├── 物理表order_00 │ ├── 物理表order_01 │ ├── 物理表order_02 │ └── 物理表order_03 │ ├── 物理库 03order_db_02 │ ├── 物理表order_00 │ ├── 物理表order_01 │ ├── 物理表order_02 │ └── 物理表order_03 │ └── 物理库 04order_db_03 ├── 物理表order_00 ├── 物理表order_01 ├── 物理表order_02 └── 物理表order_03路由示例user_id10086step01计算分库10086 % 4 2 → 落入 order_db_02step02计算分表10086 % 16 6 → 6%42 → 落入 order_02step03 最终路径 order_db_02.order_022、工程上如何实现分库分表呢也就是如何写代码思路是保证物理上是4个库和16个表但是对应于应用层无感应用层依然认为是一个库一个表不然你今天把order表给拆成4库16表明天把user表拆成8库32表我应用层写操作数据库代码的时候怎么记得住你这个表到底分成了多少个。所以没有什么是新增一层不能解决的。需要一个中间件shading jdbc通过加一层的方式实现应用无感知数据库层面先建好4个库和16个表。应用层面maven引入shading jdbc的依赖在application.yml里配置所有需要分库分表的表分成了4库16表还是8库32表八、高可用8.1 幂等性这张图的顺序从前往后看每个节点都可以拦住重复交易。1前端用户点击一次提交后按钮变灰或者立即弹出一个小窗口“提交成功”防止用户重复点击。但是有句话叫“永远不要相信前端传来的数据”前端可能卡bug所以后端还是要自己给自己兜底2继续往后看前端给后端发送的这个节点可以通过token的唯一性校验住3后端入口处校验可以通过业务唯一键去校验住并且这个业务唯一键一定要入库而且在数据库加唯一索引。这一步是最重要的。举例24年所在的系统的业务唯一键就是文件名每一笔交易来了以后去库里查这个文件名是否用过并且给这个文件名加了唯一索引。再比如对于注册交易来说邮箱、手机号、用户名都可以用来作为业务唯一键一旦库里有了就说明用户进行了重复点击。PS: 订单号、流水号这些并不是业务唯一键。比如我就是可以买完一件衣服我同样的流程再买一件一样的。再比如我就是想同一笔钱转两次。这些情况下就是可以生成两个不同的订单号呀。4调用实时接口。如果是调用自己系统另一个微服务的接口可以通过查数据库的状态判断但是如果是调用别人系统的接口如果try-catch到超时了那重发一次还是如何处理另一个接口多半是没有对于幂等性的处理的你发两遍他就认为你要转两次账。这时如果你在catch里直接重发一次万一上次其实成功了你付了两次款、发了两张券那就是生产事故。24年所在的系统因为涉及到转账所以catch到超时直接告诉用户失败让用户再来一遍。5发MQ消息跟TCP握手的原理一样如果是投递消息的路上丢了导致消费端没收到ACK那再投一遍这种情况下消费端依然只执行了一遍。没有问题如果是ACK回复丢了或者在路上超时导致消息又投递了一遍那么消费端的业务就会执行两遍。处理方式参考TCP握手——TCP的处理方式就是A-B建立连接的时候B维护一个注册表把A注册进去这样就算B给A回复的第二次握手ACK丢了A再次发一遍第一次握手B一看注册表里有就不会开俩连接了。同理消费端去维护一张消息注册表用消息ID作为唯一主键如果插入失败就表示已经收到过了。(6) 被回调两次跟调实时接口一样也得先搞清楚另一个系统那边什么情况其他系统回调我们两次如果他们不去处理这种情况的话我们就得处理。或者换句话说作为一个高可用的系统除了不能相信前端发来的任何数据之外也不能相信其他系统发来的任何请求。要做的话如何做处理方式得看回调后你做的业务1如果只是把回调发你的结果给入库并且回调的某个信息作为唯一主键的话那我处理的方式就可以insert的部分try-catch一下重复插入不报错直接内部消化。2如果后面流程比较复杂那就绝对不能做两遍这时候就可以用状态机回调前一个状态“等待回调”回调后一个状态“已回到”两个动作——每次回调前判断这笔交易是否是“等待回调”状态第一次收到回调的时候把状态改为“已收到”8.2 超时、重试、限流、熔断、降级//todo这5个暂时认为是一个组合先写一起等哪天学的更明白发现不是这么回事再拆出来。8.2.1 超时非常好理解上游系统的时间预算要盖住自己的处理时间和下游系统的时间预算。比如一个请求经过A、B、C三个系统A时间预算是3000msB的时间预算是2200msC的时间预算是1200ms。给A系统配置了预算时间3000ms以后A系统自请求进入时开始计时如果到了3秒不管下游B系统是否返回A系统自己都会抛错超时。讲的时候是自顶向下分析但设计的时候只能从低向上设计。最底层的C系统先定下自己的超时预算可以用压测得到的P99再扩大一点告诉B系统的开发人员B系统的开发人员在此基础上加上自己的处理时间作为。。或者直接用压测得到的P99当然这只是针对实时接口需要给客户实时响应。如果是异步去做的就没有DeadLine这个东西。8.2.2 重试重试的底层逻辑首先要判断能不能重试能的话如何定义重试的次数和重试的时间间隔。1、只重试可重试的一般只重试超时、连接断开、临时 5xx、限流后明确允许重试的响应下游接口文档里会写哪些错误码可以重试比如返回错误码“XXXX”表示没准备好的时候可以重试哪些错误码表示确定失败不需要重试比如下游都告诉你“余额不足”了你还重试什么幂等性 扣款这种发送了扣款接口超时了不知道那边到底执行没有这就要看下游接口有没有幂等性了没有幂等性的话你再发一次可能扣了两笔。这里正好讲一下重试的风险重试风暴跟redis缓存击穿一个意思规避方法也一个意思就是“打散”。重试雪崩跟redis缓存雪崩也一个意思。。2、重试的策略高级策略的目标只有一个就是打散多个客户端的同步重试跟优秀的哈希算法会让哈希值均匀分布一个道理。解释假设1000个客户端同时遇到故障它们会按照1秒、2秒、4秒的节奏同步发起重试形成一波波越来越猛烈的“重试风暴”或“雷鸣群效应”。而抖动Jitter 正是为了解决这个问题而生。它在计算出的退避时间上增加一个随机值打散了这1000个客户段的重试3、重试的次数在线同步请求的重试次数通常要很克制常见是 13 次。异步的包括MQ或者自动任务重试可以更多。8.2.3 限流两个地方可以限流网关限流每个微服务入口自己限流限流的4个算法都很容易理解固定窗口计数器算法思想比如规定一分钟内只能处理30个请求就维护一个计数器count每分钟清零一次来一个请求count就加一当count30个时再来的一律拒绝优点实现简单缺点不够平滑如果前20秒就满了30个请求那么后面40秒就不对外提供服务了。容易造成爆发。比如规定窗口是“1分钟最多1000个”极端情况下假如第一分钟的前59秒一个都没来最后1秒来了1000个第二分钟的第1秒来了1000个这一下就要处理2000个滑动窗口计数器算法思想将固定大小的大窗口划分为n个小窗口每个小窗口最多处理“总请求数/n”个请求优点控制的粒度变小了上面固定窗口计数器算法俩缺点都能解决缺点依然不够平滑。比如我小窗口的粒度是“1秒处理3个请求”那第一秒来了4个请求就会被扔掉一个第二秒来了3个全部处理第三秒来了4个请求又被扔掉一个。解决方法看漏桶算法漏桶算法思想跟消息队列的思想差不多维护一个固定容量大小的桶请求来了以后装到桶里这个速率是我管不着的请求像水一样匀速低落的速率是我可以控制的。当桶满了的时候多余的水直接扔掉。实现方式就是队列。优点桶匀速漏水固定的速率处理请求所以很平滑缺点也正是因为桶匀速漏水难以对付激增的流量实际业务场景中漏桶算法的适用范围相对窄一些。当业务需要允许一定程度的突发流量时令牌桶通常更合适但在需要严格控制输出速率的场景如对接有明确 QPS 限制的第三方 API中漏桶仍然是合适的方案。4. 令牌桶算法思想主要是针对令牌桶的“匀速”进行一个改进 有了令牌以后可以漏水的速度可以弹性伸缩。此时水滴代表令牌以固定速率生成令牌放到桶里请求来了以后直接拿令牌只要有令牌就可以直接拿走不用等拿到令牌的请求才可以被处理。8.2.4 降级限流管入口。它解决的是流量太多要不要都放进来。比如秒杀时 10 万个请求同时打进来系统只能扛 1 万个那就必须拦掉一部分。熔断管下游。它解决的是下游已经慢了或挂了要不要继续调。如果继续调下游恢复不了自己也会被拖垮。降级管结果。它解决的是请求被限流了、下游被熔断了、业务出异常了接下来给用户返回什么。可以返回默认值、缓存数据、静态页面也可以直接关闭某个非核心功能。更口语一点说限流别让太多请求进来熔断别再打已经异常的下游降级出问题后换一种方式继续服务Fallback真正返回的兜底结果1. 延迟处理有些操作没必要同步完成可以先放到 MQ 里慢慢处理。比如评论后发积分、统计用户行为、生成报表这类操作即使晚几秒完成用户一般也感知不到。主流程只要先返回成功即“受理即成功”后面的事情交给异步任务处理即可。但这里有个坑MQ 也可能积压。如果消费者已经处理不过来了生产者还一直重试反而会把系统打得更严重。所以重试要加退避和随机抖动必要时还要限流。2. 页面片段降级这是最直观的一种方式页面主体保留把非核心区块关掉。比如商品详情页里商品标题、价格、库存要保推荐区、广告位、活动挂件可以先隐藏。这种方式简单有效但前提是你提前做好了开关和兜底逻辑。真出问题时再改代码、再发版基本就来不及了。3. 接口兜底降级页面里有些接口是异步加载的比如配送时效、价格预测、推荐列表。它们如果超时可以直接返回默认值或缓存值。比如配送时效接口异常时可以先展示“预计 2-3 天送达”推荐接口异常时可以返回缓存里的热门商品。这里要注意一点兜底数据最好提前准备好。不要等接口已经挂了才临时去查数据库或重新生成缓存那样很可能把故障继续放大。降级开关怎么做