个人理解

在微服务体系体系中 我们会有很多服务。在内部体系中 通过eureka实现服务的自动发现通过ribbon实现服务的调用。但是如果对外部体系提供接口 我们就会涉及到接口的安全性,我们不能可能对每个服务做一套安全校验。这样运维是很不方便的,

Zuul则是提供对外访问服务的一个统一的入口,可以通过Zuul做统一的身份 登录签名验证

简单例子

1.引入依赖

<!--内部依赖rabbon histrix actuator  提供/routes返回所有路由端点-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
<!--开启端点 用于dashboard监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> </dependency>
rabbon  实现网关对服务转发时的负载均衡和请求重试
histrix:实现网关对服务转发的时候的保护机制 线程隔离和断路器 防止转发微服务故障而引起的网关资源无法释放 影响网关的对外服务

2.applicaton开启网关功能

@SpringBootApplication
@EnableZuulProxy //开启网关的功能
public class SpringCloudApigatewayApplication { public static void main(String[] args) {
SpringApplication.run(SpringCloudApigatewayApplication.class, args);
} }

3.yml配置

spring:
application:
name: apiZuul
server:
port: 5555
#传统配置方式单例 不推荐使用
zuul:
routes:
api-a-url:
path: /api-a-url/** #表示这个开头的都会路由到下面的地址
url: http://localhost:8081/

4.启动访问

访问/api-a-url/开头的都将转发到http://localhost:8081/

后台有转发到这个地址

面向服务路由

1.增加pom依赖

  <!--使用服务自动发现来注册路由规则 来进行路由-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>

2.yml配置文件修改

#服务的自动发现
eureka:
client:
serviceUrl:
defaultZone: http://peer1:1111/eureka,http://localhost:peer2/eureka
#申明api-a 和api 2个路由规则 分别路由到不同的服务 如果服务对个部署 会自动配置
zuul:
routes:
api-a:
path: /consumer/** #路由规则
serviceId: consumer #服务名称
api-b:
path: /provider/** #路由规则
serviceId: PROVIDER #服务名称

或者

zuul:
routes:
consumer:
path: /consumer/**
provider:
path: /consumer/**

结果等同

当请求/consumer/** 将自动转发到consumer对应的服务地址

3.在provier UserContorller增加服务信息的打印用于网关的负载均衡

 @Autowired
Registration registration;
@RequestMapping("/registrationInfo")
@ResponseBody
public String registrationInfo(){
return registration.getHost()+":"+registration.getPort();
}

4.启动前面项目的eureka conusmer和provider测试  providerr需要启动2个不同端口的项目

下面为我的打包路径

java -jar /Users/liqiang/Desktop/java开发环境/javadom/spring-cloud-parent/spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1

java -jar /Users/liqiang/Desktop/java开发环境/javadom/spring-cloud-parent/spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2

5.测试

http://127.0.0.1:5555/provider/user/registrationInfo

zuul在转发服务集群情况下 利用rabbon负载均衡

请求过滤

通过请求过滤实现网关转发前的身份认证权限校验等操作

简单例子

1.创建一个过滤器

public class AccessFilter extends ZuulFilter {
private static Logger log= LoggerFactory.getLogger(AccessFilter.class); /***
* 过滤器执行时机
* pre: 可以在请求被路由之前调用。
* route: 在路由请求时被调用。
* post: 在 routing 和 error 过滤器之后被调用。
* error: 处理请求时发生错误时被调用。
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器执行顺序
* 当同一个时机存在多个过滤器由此来取值用哪个
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/***
* 判断过滤器是否执行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器执行逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//使用网关校验必须有token
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("send{} request to{}", request.getMethod(), request.getRequestURL().toString());
//校验是否带有token 如果没有验证不通过
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
log.warn("access token is empty");
//令zuul过滤该请求 不进行转发
ctx.setSendZuulResponse(false);
//返回状态码
ctx.setResponseStatusCode(401);
//防止乱码
ctx.getResponse().setContentType("application/json;charset=UTF-8");
//设置body
ctx.setResponseBody("无效token");
return null; }
log.info("access token ok");
return null;
}
}

2,创建一个javaconfig 配置zuul

@Configuration
public class ZuulConfig {
@Bean
public AccessFilter initFilter(){
return new AccessFilter();
}
}

3.测试 如果我们后缀不带token参数将不会转发

Zuul配置

多实例配置

不适用服务自动发现注册zuul 如何实现多实例的负载均衡

spring:
application:
name: apiZuul
http:
encoding:
enabled: true
charset: utf-8
force: true
server:
port: 5555
tomcat:
uri-encoding: UTF-8
#下面指定了serviceId 默认会去注册中心发现服务 所以禁用
ribbon:
eureka:
enabled:false
zuul:
routes:
provider:
path: /provider/**
serviceId: provider #与下面的provider相对应
provider:
ribbon:
listOfServers: http://localhost:8081/,http://localhost:8082/ #多个服务

默认路由规则

当我们不单独为服务配置路由规则 默认是会以{服务名}/**来配置

其他配置

忽略表达式

zuul.ignored-patterns=/**/hello/**  可以指定规则不被路由

增加前缀

zuul.prefix=/api 给所有路由增加api前缀
#zuul.stripPrefix= false 代理默认会移除前缀  关闭代理移除前缀
#zuul.routes.<route>.strip-prefix=true 指定某个服务的移除前缀

本机跳转

#本机服务器跳转
#zuul.routes.api-b.path=/api-b/**
#zuul.routes.api-b.url=forward:/local

转发铭感数据

zuul.sensitiveHeaders=Cookie,Set-Cookie,Authorization  默认禁用这3个参数转发

可以通过zuul.sensitiveHeaders=      #空全局取消禁用

或者 指定路由

#方法一:对指定路由开启自定义敏感头
zuul.routes.<router>.customSensitiveHeaders=true #方法二:将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders=

重定向

zuul.addHostHeader=true

解决网关转发到登录页面 登录成功跳转到具体实例而不是网关host

histrix和ribbon超时

 

#设置zuul使用histrixCommand转发请求的超时时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1000
#ribbon:
# ReadTimeout: 3000 #rabbon 建立连接的超时时间 如果小于hystix的超时时间 则超时会开启重试直到大于等于hystrix的超时日期
# ConnectTimeout: 1000 #请求建立连接的超时时间 如果小于hystirx 则会触发重试 直到大于等于hystirx的超时日期

关闭重试

#以下2个参数为针对上面的禁止发起重试
# zuul.retryable= false
# zuul.routes.<route>.retryable= false

自定义路由规则

微服务定义不同版本的服务 起到每次服务更新 而不是强制所有调用方都更新

服务通过servicename-v1来进行命名

@Configuration
public class ZuulConfig {
@Bean
public AccessFilter initFilter() {
return new AccessFilter();
} @Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>.+)-(?<version>v.+$)", "${version}/${name}");
}
}

服务名字以servicename-v1  默认路由规则 会变成servicename-v1/**

我们通过上面PatternServiceRouteMapper  第一个参数 会匹配所有servicename-v1 的路由规则 并通过正则表达式的捕获将服务名字和版本抓取出来 改为${version}/${name} 填充此规则

路径匹配规则

默认过滤

* pre级别的默认过滤器:
* 1.ServletDetectionFilter filterOrder:-3 最先执行 用于判断是zuulServlet 还是dispacherServlet 来处理运行
* 2.Servlet30WrapperFilter filterOrder:-2 将HttpServletRequest包装成Servlet30RequestWrapper
* 3.FormBodyWrapperFilter filterOrder:-1 通过1判断如果是dispacherServlet 通过2判断content-type 类型是application/x-www­ form-urlencoded
* 或者multipart/form­data 则将body包装为FormBodyRequestWrapper
* 4.DebugFilter filterOrder:1 主要用于如果设置了 zuul.debug.request.debug=true 来判断是否执行这个过滤器器 debugRouting debugRequest判断打印日志类型
* 5.PreDecorationFilter filterOrder:5 在转发请求前做预处理 可以通过RequestContext. getCurrentContext () 拿到请求报文信息
* route级别的过滤器:
* 1.RibbonRoutingFilter filterOrder 10 判断上下文是否存在serviceid 如果存在则用Hystrix 或者rabbon 对服务实例发起请求 并将结果返回、
* 2.SimpleHostRoutingFilter filterOrder:100 判断上下文是否存在routeHost 就是我们配置路由的ip地址配置 直接发起请求使用httpClient实现 没有使用histrix
* 3.SendForwardFilter 主要用于判断上下文是否存在forward.to 就是配置文件配置的本地跳转forward:/local
* post级别过滤器:
* SendErrorFilter 执行顺序为0 判断上下文是否存在error.status_code(之前过滤器设置的错误编码) 根据错误信息forward 到/error错误端点产生错误响应
* SendPesponseFilter 执行顺序是100 判断上下文是否有响应头信息(响应头和响应流) 主要是根据上下文响应信息 组织需要发送回客户端的响应信息
*

异常处理

如果过滤器发生异常将会怎么样

1.测试在第一个例子中抛出一个RuntimeException异常

    /**
* 过滤器执行逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
int i= 1/0;
return null;
}

利用SendErrorFilter抛出更友好提示

    @Override
public Object run() throws ZuulException {
// 测试异常过滤器处理
RequestContext ctx = RequestContext.getCurrentContext();
try {
int i = 1 / 0;
}catch (Exception e){
//防止乱码
ctx.getResponse().setHeader("Content-Type","text/html;charset=UTF-8");
//异常过滤器shouldFilter判断这个参数为false才会执行
ctx.set("sendErrorFilter.ran",false);
throw new ZuulException(e, "Forwarding error", 500,"发生了异常");
}
return null;
}

SendErrorFilter模式是交给org.springframework.boot. autoconfigure.web.BasicErrorController 来处理异常的 我们可以查看源码 参照重写覆盖原有的返回更友好提示

@RestController
public class ErrorHandler implements ErrorController { @GetMapping(value = "/error")
public ResponseEntity<ErrorBean> error(HttpServletRequest request) {
String message = request.getAttribute("javax.servlet.error.message").toString();
ErrorBean errorBean = new ErrorBean();
errorBean.setMessage(message);
errorBean.setReason("程序出错");
return new ResponseEntity<>(errorBean, HttpStatus.BAD_GATEWAY);
} @Override
public String getErrorPath() {
return "error";
} private static class ErrorBean {
private String message; private String reason; public String getMessage() {
return message;
} public void setMessage(String message) {
this.message = message;
} public String getReason() {
return reason;
} public void setReason(String reason) {
this.reason = reason;
}
}
}

再次执行返回

定义errorFilter

自定义之前先了解sendErrorFilter部分源码

public class SendErrorFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter.class);
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
/*
处理异常的contoller地址 可以通过error.path自定义默认是/error
对应org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
*/
@Value("${error.path:/error}")
private String errorPath; public SendErrorFilter() {
} public String filterType() {
return "error";
} public int filterOrder() {
return 0;
} public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
//如果ctx有异常同时sendErrorFilter.ran为false 则执行异常过滤器
return ctx.getThrowable() != null && !ctx.getBoolean("sendErrorFilter.ran", false);
} public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter.ExceptionHolder exception = this.findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode());
log.warn("Error during filtering", exception.getThrowable());
request.setAttribute("javax.servlet.error.exception", exception.getThrowable());
if (StringUtils.hasText(exception.getErrorCause())) {
request.setAttribute("javax.servlet.error.message", exception.getErrorCause());
}
//获得对应的处理器
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath);
if (dispatcher != null) {
ctx.set("sendErrorFilter.ran", true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.getStatusCode());
//转发到对应的contoller执行
dispatcher.forward(request, ctx.getResponse());
}
}
} catch (Exception var5) {
ReflectionUtils.rethrowRuntimeException(var5);
} return null;
}
}

上面有一段return ctx.getThrowable() != null && !ctx.getBoolean("sendErrorFilter.ran", false);

尝试吧之前的测试做修改 发现怎么也不会到异常过滤器

 @Override
public Object run() throws ZuulException {
// 测试异常过滤器处理
RequestContext ctx = RequestContext.getCurrentContext();
try {
int i = 1 / 0;
}catch (Exception e){
//防止乱码
ctx.getResponse().setHeader("Content-Type","text/html;charset=UTF-8");
//异常过滤器shouldFilter判断这个参数为false才会执行
ctx.set("sendErrorFilter.ran",false);
ctx.setThrowable(e); }
return null;
}

查看过滤器的核心入口com.netflix.zuul.http.ZuulServlet

  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan(); try {
//执行pre过滤器
this.preRoute();
} catch (ZuulException var13) {
//发生了异常 交给error过滤器 error过滤器处理了 再交由postRoute过滤器
this.error(var13);
this.postRoute();
return;
} try {
//执行toute过滤器
this.route();
} catch (ZuulException var12) {
this.error(var12);
//发生了异常 交给error过滤器 error过滤器处理了 再交由postRoute过滤器
this.postRoute();
return;
} try {
//执行post过滤器
this.postRoute();
} catch (ZuulException var11) {
//发生了异常 交给error过滤器
this.error(var11);
}
} catch (Throwable var14) {
//抛出的非zuul异常 最外层转为zuul异常
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}

为什么都会调用postRoute呢 因为postRoute里面有一个SendPesponseFilter是组织响应内容

优化

通过上面也发现一个问题  执行post过滤器的时候发生了异常 就调用了额 异常过滤器 而没有调用post过滤器 组织异常

1.为请求上下文自定义属性 用于判断当前过滤器的级别

/**
*
• getinstance(): 该方法用来获取当前处理器的实例。
• setProcessor(FilterProcessor processor): 该方法用来设置处理器实 例, 可以使用 此方法来设置自定义的处理器。
• processZuulFilter(ZuulFilter filter): 该方法定义了用来执行 filter 的具体逻辑, 包括对请求上下文的设置, 判断是否应 该 执行, 执行时 一些异常的处理
等。
• getFiltersByType(Stringng filterType) : 该 方 法 用 来 根 据 传 入 的 filtererType 获取API网关中对应类型的过滤器, 并根据这些过滤器的
filterOrder从小到大排序, 组织成一个列表返回。
• runFi让ers(String sType): 该方法会根据传入的 filterType 来调用
getFi让ersByType(String fiterType)获取排序后的过滤器列表, 然后轮
询这些过滤器, 并调用 processZuulFiler(ZuulFilter filter)来依次执 行它们。
• preRoute(): 调用runFilters("pre")来执行所有pre类型的过滤器。
• route(): 调用runFilters("route")来执行所有route类型的过滤器。
• postRoute(): 调用runFi辽ers("post")来执行所有post类型的过滤器。
• error(): 调用runFilters("error")来执行所有error类型的过滤器。
*/
@Component
public class DidiFilterProcessor extends FilterProcessor {
/**
* 调用p此方法执行post过滤器 我们可以自定义属性 判断是是post过滤器 导致的异常 用于errorExtFilter使用
* @throws ZuulException
*/
@Override
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
try {
return super.processZuulFilter(filter);
}catch (Exception e){
RequestContext ctx = RequestContext.getCurrentContext(); ctx.set("failed.filter", filter);
throw e;
}
}
}

2.main方法注入

@SpringBootApplication
@EnableZuulProxy //开启网关的功能
public class SpringCloudApigatewayApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudApigatewayApplication.class, args);
//注入自定义processor 配合errorExtFilter
FilterProcessor.setProcessor (new DidiFilterProcessor());
} }

3.自定义一个异常过滤器

@Component
public class ErrorExtFilter extends SendErrorFilter {
@Override
public String filterType() {
return "error";
} //大于SendErrorFilter 的就行
@Override
public int filterOrder() {
return 1;
} @Override
public boolean shouldFilter() {
//因为别的异常 sendErrorFilter都处理了所以我们只处理post级别过滤器就行 判断是否是post级别过滤请求
//failed.filter 此属性DidiFilterProcesor里面社会组
//判断:仅处理来自post过滤器引起的异常
RequestContext ctx = RequestContext.getCurrentContext();
ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
return failedFilter!=null&&failedFilter.filterType().equals("post");
} @Override
public Object run() {
//这里可以单独对post产生的异常做处理
return super.run();
}
}

4.添加一个post过滤器 用于测试

/**
* 查看zuulServlet可以发现 如果post过滤器出现异常 是不会调用post post则是返回客户端信息 则报错
* 为了解决这个问题我们自定义一个专门处理post异常的过滤器ErrorExtFilter
*/
public class PostFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
} @Override
public int filterOrder() {
return 99;
} @Override
public boolean shouldFilter() {
return true;
} @Override
public Object run() throws ZuulException {
//对post产生的异常做post处理
throw new RuntimeException("");
}
}

如何禁用过滤器

zuul.<SimpleClassName>.<filterType>.disable=true 第一个为过滤器类名 第二个为阶段

查看所有路由规则

1.配置文件配置端点启用

#启用端点
management:
endpoints:
web:
exposure:
include: routes
info:
enabled: false

2.pom依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> </dependency>

3.测试

获得request和命中路由信息

Spring Cloud-Zuul(十)的更多相关文章

  1. Spring Cloud(十):服务网关 Zuul(路由)【Finchley 版】

    Spring Cloud(十):服务网关 Zuul(路由)[Finchley 版]  发表于 2018-04-23 |  更新于 2018-05-09 |  通过之前几篇 Spring Cloud 中 ...

  2. Spring Cloud第十四篇 | Api网关Zuul

    ​ 本文是Spring Cloud专栏的第十四篇文章,了解前十三篇文章内容有助于更好的理解本文: Spring Cloud第一篇 | Spring Cloud前言及其常用组件介绍概览 Spring C ...

  3. Spring Cloud(十二):Spring Cloud Zuul 限流详解(附源码)(转)

    前面已经介绍了很多zuul的功能,本篇继续介绍它的另一大功能.在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选 ...

  4. Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。

    时间过的很快,写springcloud(十):服务网关zuul初级篇还在半年前,现在已经是2018年了,我们继续探讨Zuul更高级的使用方式. 上篇文章主要介绍了Zuul网关使用模式,以及自动转发机制 ...

  5. Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin【Finchley 版】

    Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin[Finchley 版]  发表于 2018-04-24 |  随着业务发展,系统拆分导致系统调用链路愈发复杂一个前端请 ...

  6. Spring Cloud Zuul 添加 ZuulFilter

    紧接着上篇随笔Spring Cloud Zuul写,添加过滤器,进行权限验证 1.添加过滤器 package com.dzpykj.filter; import java.io.IOException ...

  7. 笔记:Spring Cloud Zuul 快速入门

    Spring Cloud Zuul 实现了路由规则与实例的维护问题,通过 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获取了 ...

  8. Spring Cloud Zuul 限流详解(附源码)(转)

    在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选择,只需要编写一个过滤器就可以了,关键在于如何实现限流的算法. ...

  9. Spring Cloud Zuul 中文文件上传乱码

    原文地址:https://segmentfault.com/a/1190000011650034 1 描述 使用Spring Cloud Zuul进行路由转发时候吗,文件上传会造成中文乱码“?”.1. ...

  10. spring cloud zuul参数调优

    zuul 内置参数 zuul.host.maxTotalConnections 适用于ApacheHttpClient,如果是okhttp无效.每个服务的http客户端连接池最大连接,默认是200. ...

随机推荐

  1. 实例介绍Cocos2d-x中Box2D物理引擎:碰撞检測

    在Box2D中碰撞事件通过实现b2ContactListener类函数实现,b2ContactListener是Box2D提供的抽象类,它的抽象函数:virtual void BeginContact ...

  2. P1052 过河(状态压缩)

    P1052 过河(状态压缩) 题目描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把 ...

  3. Java之POI读取Excel的Package should contain a content type part [M1.13]] with root cause异常问题解决

    Java之POI读取Excel的Package should contain a content type part [M1.13]] with root cause异常问题解决 引言: 在Java中 ...

  4. hdoj-看病要排队

    看病要排队 Problem Description 看病要排队这个是地球人都知道的常识. 不过经过细心的0068的观察,他发现了医院里排队还是有讲究的.0068所去的医院有三个医生(汗,这么少)同时看 ...

  5. [python 基础]python装饰器(二)带参数的装饰器以及inspect.getcallargs分析

    带参数的装饰器理解无非记住两点: 1.本质不过在基本的装饰器外面再封装一层带参数的函数 2.在使用装饰器语法糖的时候与普通装饰器不同,必须要加()调用,且()内的内容可以省略(当省略时,admin默认 ...

  6. Python 40 数据库-约束

    约束 1.什么是约束 ? 除了数据类型以外额外添加的约束 2.为什么要使用约束 ? 为了保证数据的合法性 完整性 分类: 1.not null 非空约束,数据不能为空----------------- ...

  7. go并发编程 WaitGroup, Mutex

    1.背景 记录一下,方便后续写代码直接使用. 需要注意几点: chan 默认支持多协程工作,不需要加锁. 其他变量操作需要使用锁保护(map多协程并发写会panic, 并且无法捕获). 启动gorou ...

  8. python 实现线程之间的通信

    前言:因为GIL的限制,python的线程是无法真正意义上并行的.相对于异步编程,其性能可以说不是一个等量级的.为什么我们还要学习多线程编程呢,虽然说异步编程好处多,但编程也较为复杂,逻辑不容易理解, ...

  9. nprogress进度条和ajax全局事件

    nprogress和ajax全局事件 nprogress 官方网站:http://ricostacruz.com/nprogress/ 下载地址:https://github.com/rstacruz ...

  10. Power BI 入门资料

    1.官方文档 Power BI Desktop:https://docs.microsoft.com/zh-cn/power-bi/desktop-getting-started Power BI 报 ...