SpringBoot接口 - 如何实现接口限流之单实例
在以SpringBoot开发Restful接口时,当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。在接口层如何做限流呢? 本文主要回顾限流的知识点,并实践单实例限流的一种思路。
- SpringBoot接口 - 如何实现接口限流之单实例
- 准备知识点
- 为什么要限流
- 限流有哪些常见思路?
- 实现思路
- 定义RateLimit注解
- 定义AOP
- 自定义相关异常
- 统一结果返回封装
- controller接口
- 接口测试
- 上述实现方案的槽点
- 示例源码
- 准备知识点
# 准备知识点
主要的知识点,请参考架构之高并发:限流, 这里小结下。
# 为什么要限流
每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。
# 限流有哪些常见思路?
- 从算法上看
令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法。
- 单实例
应用级限流方式只是单应用内的请求限流,不能进行全局限流。
- 限流总资源数
- 限流总并发/连接/请求数
- 限流某个接口的总并发/请求数
- 限流某个接口的时间窗请求数
- 平滑限流某个接口的请求数
- Guava RateLimiter
- 分布式
我们需要分布式限流和接入层限流来进行全局限流。
- redis+lua实现中的lua脚本
- 使用Nginx+Lua实现的Lua脚本
- 使用 OpenResty 开源的限流方案
- 限流框架,比如Sentinel实现降级限流熔断
# 实现思路
主要思路:AOP拦截自定义的RateLimit注解,在AOP中通过Guava RateLimiter; Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
# 定义RateLimit注解
package tech.pdai.ratelimit.guava.config.ratelimit;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author pdai*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {int limit() default 10;}
# 定义AOP
package tech.pdai.ratelimit.guava.config.ratelimit;import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
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.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;/*** @author pdai*/
@Slf4j
@Aspect
@Component
public class RateLimitAspect {private final ConcurrentHashMap<String, RateLimiter> EXISTED_RATE_LIMITERS = new ConcurrentHashMap<>();@Pointcut("@annotation(tech.pdai.ratelimit.guava.config.ratelimit.RateLimit)")public void rateLimit() {}@Around("rateLimit()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();RateLimit annotation = AnnotationUtils.findAnnotation(method, RateLimit.class);// get rate limiterRateLimiter rateLimiter = EXISTED_RATE_LIMITERS.computeIfAbsent(method.getName(), k -> RateLimiter.create(annotation.limit()));// processif (rateLimiter!=null && rateLimiter.tryAcquire()) {return point.proceed();} else {throw new RuntimeException("too many requests, please try again later...");}}
}
# 自定义相关异常
package tech.pdai.ratelimit.guava.config.exception;import lombok.extern.slf4j.Slf4j;/*** business exception, besides normal exception.** @author pdai*/
@Slf4j
public class BusinessException extends RuntimeException {/*** Constructs a new exception with {@code null} as its detail message. The cause is not initialized, and may* subsequently be initialized by a call to {@link #initCause}.*/public BusinessException() {super();}/*** Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently* be initialized by a call to {@link #initCause}.** @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()}* method.*/public BusinessException(final String message) {super(message);}/*** Constructs a new exception with the specified detail message and cause.* <p>* Note that the detail message associated with {@code cause} is <i>not</i> automatically incorporated in this* exception's detail message.** @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method).* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A <tt>null</tt>* value is permitted, and indicates that the cause is nonexistent or unknown.)* @since 1.4*/public BusinessException(final String message, final Throwable cause) {super(message, cause);}/*** Constructs a new exception with the specified cause and a detail message of* <tt>(cause==null ? null : cause.toString())</tt> (which typically contains the class and detail message of* <tt>cause</tt>). This constructor is useful for exceptions that are little more than wrappers for other* throwables (for example, {@link java.security.PrivilegedActionException}).** @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A <tt>null</tt>* value is permitted, and indicates that the cause is nonexistent or unknown.)* @since 1.4*/public BusinessException(final Throwable cause) {super(cause);}/*** Constructs a new exception with the specified detail message, cause, suppression enabled or disabled, and* writable stack trace enabled or disabled.** @param message the detail message.* @param cause the cause. (A {@code null} value is permitted, and indicates that the cause is nonexistent or* unknown.)* @param enableSuppression whether or not suppression is enabled or disabled* @param writableStackTrace whether or not the stack trace should be writable* @since 1.7*/protected BusinessException(final String message, final Throwable cause, boolean enableSuppression,boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}}
异常的处理
package tech.pdai.ratelimit.guava.config.exception;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import tech.pdai.ratelimit.guava.config.response.ResponseResult;
import tech.pdai.ratelimit.guava.config.response.ResponseStatus;/*** @author pdai*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** handle business exception.** @param businessException business exception* @return ResponseResult*/@ResponseBody@ExceptionHandler(BusinessException.class)public ResponseResult<BusinessException> processBusinessException(BusinessException businessException) {log.error(businessException.getLocalizedMessage());return ResponseResult.fail(null, businessException.getLocalizedMessage()==null? ResponseStatus.HTTP_STATUS_500.getDescription():businessException.getLocalizedMessage());}/*** handle other exception.** @param exception exception* @return ResponseResult*/@ResponseBody@ExceptionHandler(Exception.class)public ResponseResult<Exception> processException(Exception exception) {log.error(exception.getLocalizedMessage(), exception);return ResponseResult.fail(null, ResponseStatus.HTTP_STATUS_500.getDescription());}
}
# 统一结果返回封装
package tech.pdai.ratelimit.guava.config.response;import java.io.Serializable;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class ResponseResult<T> {/*** response timestamp.*/private long timestamp;/*** response code, 200 -> OK.*/private String status;/*** response message.*/private String message;/*** response data.*/private T data;/*** response success result wrapper.** @param <T> type of data class* @return response result*/public static <T> ResponseResult<T> success() {return success(null);}/*** response success result wrapper.** @param data response data* @param <T> type of data class* @return response result*/public static <T> ResponseResult<T> success(T data) {return ResponseResult.<T>builder().data(data).message(ResponseStatus.SUCCESS.getDescription()).status(ResponseStatus.SUCCESS.getResponseCode()).timestamp(System.currentTimeMillis()).build();}/*** response error result wrapper.** @param message error message* @param <T> type of data class* @return response result*/public static <T extends Serializable> ResponseResult<T> fail(String message) {return fail(null, message);}/*** response error result wrapper.** @param data response data* @param message error message* @param <T> type of data class* @return response result*/public static <T> ResponseResult<T> fail(T data, String message) {return ResponseResult.<T>builder().data(data).message(message).status(ResponseStatus.FAIL.getResponseCode()).timestamp(System.currentTimeMillis()).build();}}
# controller接口
package tech.pdai.ratelimit.guava.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.ratelimit.guava.config.ratelimit.RateLimit;
import tech.pdai.ratelimit.guava.config.response.ResponseResult;/*** @author pdai*/
@Slf4j
@RestController
public class RateLimitTestController {@RateLimit@GetMapping("/limit")public ResponseResult<String> limit() {log.info("limit");return ResponseResult.success();}@RateLimit(limit = 5)@GetMapping("/limit1")public ResponseResult<String> limit1() {log.info("limit1");return ResponseResult.success();}@GetMapping("/nolimit")public ResponseResult<String> noRateLimiter() {log.info("no limit");return ResponseResult.success();}}
# 接口测试
@SneakyThrows
public static void test(int clientSize) {CountDownLatch downLatch = new CountDownLatch(clientSize);ExecutorService fixedThreadPool = Executors.newFixedThreadPool(clientSize);IntStream.range(0, clientSize).forEach(i ->fixedThreadPool.submit(() -> {RestTemplate restTemplate = new RestTemplate();restTemplate.getForObject("http://localhost:8080/limit1", ResponseResult.class);downLatch.countDown();}));downLatch.await();fixedThreadPool.shutdown();
}
测试结果
2021-10-01 15:22:47.171 INFO 30092 --- [nio-8080-exec-4] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.171 INFO 30092 --- [nio-8080-exec-8] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.171 INFO 30092 --- [nio-8080-exec-5] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.187 INFO 30092 --- [nio-8080-exec-9] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.187 INFO 30092 --- [nio-8080-exec-2] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.187 INFO 30092 --- [io-8080-exec-10] t.p.r.g.c.RateLimitTestController : limit1
2021-10-01 15:22:47.202 ERROR 30092 --- [nio-8080-exec-7] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.202 ERROR 30092 --- [nio-8080-exec-6] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.221 ERROR 30092 --- [nio-8080-exec-1] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.222 ERROR 30092 --- [nio-8080-exec-5] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [nio-8080-exec-6] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [nio-8080-exec-8] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [nio-8080-exec-3] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-12] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-14] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-13] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.225 ERROR 30092 --- [io-8080-exec-15] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.240 ERROR 30092 --- [io-8080-exec-11] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.240 ERROR 30092 --- [nio-8080-exec-4] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
2021-10-01 15:22:47.256 ERROR 30092 --- [nio-8080-exec-2] t.p.r.g.c.e.GlobalExceptionHandler : too many requests, please try again later...
# 上述实现方案的槽点
注意
必须要说明一下,上述实现方式只是单实例下一种思路而已,如果细细的看,上面的代码存在一些槽点。
- 首先,
EXISTED_RATE_LIMITERS.computeIfAbsent(method.getName(), k -> RateLimiter.create(annotation.limit()))
这行代码中method.getName()
表明是对方法名进行限流的,其实并不合适,应该需要至少加上类名; - 其次, 如果首次运行时访问的请求是一次性涌入的,即EXISTED_RATE_LIMITERS还是空的时候并发请求@RateLimit接口,那么RateLimiter.create(annotation.limit())是会重复创建并加入到EXISTED_RATE_LIMITERS的,这是明显的bug;
- 再者, 上述实现方式按照方法名去限定请求量,对于很多情况下至少需要支持按照IP和方法名,或者其它自定义的方式进行限流。
- 其它一些场景支持的参数抽象和封装等
相关文章:
SpringBoot接口 - 如何实现接口限流之单实例
在以SpringBoot开发Restful接口时,当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。在接口层如何做限流呢? 本文主要回顾限流的知识点,并实践单实例限流的一种思路。 SpringBoot接口 …...

【花雕学AI】深度挖掘ChatGPT角色扮演的一个案例—CHARACTER play : 莎士比亚
CHARACTER play : 莎士比亚 : 52岁,男性,剧作家,诗人,喜欢文学,戏剧,爱情 : 1、问他为什么写《罗密欧与朱丽叶》 AI: 你好,我是莎士比亚,一位英国的剧作家和诗人。我很高兴你对我的…...

腾讯云物联网开发平台 LoRaWAN 透传接入 更新版
前言 之前有一篇文章介绍LoRaWAN透传数据,不过还是用物模型云端数据解析脚本,不是真正的透传。腾讯云物联网开发平台也支持对LoRaWAN原始数据的透传、转发。今天来介绍下。腾讯云 IoT Explorer 是腾讯云主推的一站式物联网开发平台,IoT 小能手…...

4.6--计算机网络之TCP篇之TCP的基本认识--(复习+深入)---好好沉淀,加油呀
1.TCP 头格式有哪些? 序列号: 在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。 用来解决网络包乱序问题。 确认应答号: …...

一文吃透Elasticsearch
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~ Github地址 如果访问不了Github,…...

CPU占用率高怎么办?正确解决方法在这里!
案例:CPU占用率高怎么解决 【各位朋友,我的电脑现在运行太慢了,同事说可能是CPU占用率太高了,但对本电脑小白来说,完全不知道怎么处理,大家有什么好的方法可以解决这个问题吗?】 在计算机中&a…...
ChatGPT实现用C语言写一个学生成绩管理系统
随着ChatGPT爆火,大家都在使用ChatGPT来帮助自己提高效率,对于程序员来说使用它来写代码怎么样呢?今天尝试让ChatGPT,写了一个学生成绩管理系统。 问题是:使用C语言写一个学生成绩管理系统,要求使用链表&a…...

Swagger文档注释
本文以DRF框架为例使用 为什么要接口文档注释 一. 方便后端调试与后续接口更新; 二. 对于大型前后端分离项目,前后端人员是分开开发的,甚至前端的人你都不知道远在何处,这时候接口文档的重要性就太重要了。 三. 接口注释文档常用…...

pdf怎么转换ppt格式,两个方法转换
PDF作为一种常用的文件格式,被大众所熟悉。虽然PDF具备的稳定性,安全性,以及很强的兼容性可以让我们更方便顺畅的阅读PDF文件,但若是有需要展示PDF文件内容的时候,其优点就没有那么凸显了,这时还是将pdf转换…...
深度学习编译器相关的优秀论文合集-附下载地址
公司排名不分先后 目前在AI芯片编译器领域,有很多大公司在进行研究和开发。以下是一些主要的公司和它们在该领域的研究时间: 英伟达(NVIDIA):英伟达是一家全球知名的图形处理器制造商,其在AI芯片编译器领域…...

vue全局使用svg
1、安装依赖 npm install svg-sprite-loader2、配置选项 在vue.config.js的chainWebpack里配置下面代码 解释:config.module.rule是一个方法,用来获取某个对象的规则。.exclude.add(文件a)是往禁用组添加文件a,就是对文…...
每天一点C++——杂记
结构体的深拷贝和浅拷贝 浅拷贝就是只拷贝指针,并不拷贝指针所指向的内容,深拷贝则会对指针的内容进行拷贝。浅拷贝会在一些场景下出现问题,看下面的例子: struct s {char * name;int age; };如果我定义 一个对象s1,…...

Document Imaging SDK 11.6 for .NET Crack
Document Imaging SDK for .NET View, Convert, Annotate, Process,Edit, Scan, OCR, Print 基本上被认为是一种导出 PDF 解决方案,能够为用户和开发人员提供完整且创新的 PDF 文档处理属性。它具有提供简单集成的能力,可用于增强用户 .NET 的文档成像程…...

数据挖掘(3.1)--频繁项集挖掘方法
目录 1.Apriori算法 Apriori性质 伪代码 apriori算法 apriori-gen(Lk-1)【候选集产生】 has_infrequent_subset(c,Lx-1)【判断候选集元素】 例题 求频繁项集: 对于频繁项集L{B,C,E},可以得到哪些关联规则: 2.FP-growth算法 FP-tre…...

2023年信息安全推荐证书
随着网络安全行业的不断升温,相关的认证数量也不断增加,对于在网络安全行业发展的人才来说,提升职业竞争力最有效的办法之一,就是取得权威认证。 那么如何从繁多的适合网络安全从业者的证书中选择含金量高、发展潜力大的证书&…...

基于ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升环境、生态、水文、土地、土壤、农业、大气等领域应用
【自选】 时间地点:2023年7月22日-28日【乌鲁木齐】时间地点:2023年8月12日-18日【福建泉州】 【六天实践教学、提供全部资料】 专题一、空间数据获取与制图 1.1 软件安装与应用讲解 1.2 空间数据介绍 1.3海量空间数据下载 1.4 ArcGIS软件快速入门…...

基于ZC序列的帧同步
Zadoff-Chu序列是一种特殊的序列,具有良好的自相关性和很低的互相关性,这种性能可以被用来产生同步信号,作为对时间和频率的相关运算在TD-LTE系统中,Zadoff-Chu(ZC)序列主要应用于上行RS序列生成、PRACH前导序列生成以及主同步信号…...
配置NFS服务器-debian
NFS(Network Files System)是网络文件系统的英文缩写,由Sun公司于1980年开发,用于在UNIX操作系统间实现磁盘文件共享。在Linux操作系统出现后,NFS被Linux继承,并成为文件服务的一种标准。 通过网络,NFS可以在不同文件…...
正点原子STEMWIN死机
在用正点原子STM32F4开发板,搭配对应的button历程时,发现运行一会,button都无法使用了,以为是emwin死机了,但是看到Led还在闪烁,排除系统死机问题。那就是emwin的任务没有运行起来,但是打断点后…...
PMP考试中的固定答题套路
1、掌握PMBOK 编制的逻辑(整范进,成质资,沟风采,相) 2、事实求是,项目经理该怎么做就怎么做,不能违背职业道德。 3、PM 做事流程(5 步法): ①收集信息&…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...
连锁超市冷库节能解决方案:如何实现超市降本增效
在连锁超市冷库运营中,高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术,实现年省电费15%-60%,且不改动原有装备、安装快捷、…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...

视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...