当前位置: 首页 > news >正文

【SpringCloud Alibaba】(十一)学习 Sleuth 链路追踪

目录

  • 1、分布式链路追踪
    • 1.1、概述
    • 1.2、核心原理
    • 1.3、解决方案
  • 2、集成 Sleuth 实现链路追踪
    • 2.1、Sleuth 概述
      • 2.1.1、Span 简介
      • 2.1.2、Trace 简介
      • 2.1.3、Annotation 简介
    • 2.2、集成 Sleuth
      • 2.2.1、最简使用
      • 2.2.2、抽样采集数据
      • 2.2.3、追踪自定义线程池
        • 2.2.3.1、演示使用 @Async 注解开启任务
        • 2.2.3.2、演示自定义任务线程池
        • 2.2.3.3、演示包装自定义线程池
        • 2.2.3.4、总结
      • 2.2.4、自定义链路过滤器
        • 2.2.4.1、自定义链路过滤器概述
        • 2.2.4.2、演示自定义链路过滤器

随着互联网的不断发展,企业的业务系统变得越来越复杂,原本单一的单体应用系统已经无法满足企业业务发展的需要。于是,很多企业开始了对项目的分布式与微服务改造,新项目也在开始的时候就会采用分布式与微服务的架构模式。

一个系统采用分布式与微服务架构后,会被拆分成许多服务模块,这些服务模块之间的调用关系错综复杂,对于客户端请求的分析与处理就会显得异常复杂。此时,就需要一种技术来解决这些问题,而这种技术就是分布式链路追踪技术

1、分布式链路追踪

1.1、概述

随着互联网业务快速扩展,企业的业务系统变得越来越复杂,不少企业开始向分布式、微服务方向发
展,将原本的单体应用拆分成分布式、微服务。这也使得当客户端请求系统的接口时,原本在同一个系统内部的请求逻辑变成了需要在多个微服务之间流转的请求。

单体架构中可以使用 AOP 在调用具体的业务逻辑前后分别打印一下时间即可计算出整体的调用时间,使用 AOP 捕获异常也可知道是哪里的调用导致的异常。

但是在分布式微服务场景下,使用 AOP 技术是无法追踪到各个微服务的调用情况的,也就无法知道系统中处理一次请求的整体调用链路。

另外,在分布式与微服务场景下,我们需要解决如下问题:

  • 如何快速发现并定位到分布式系统中的问题。
  • 如何尽可能精确的判断故障对系统的影响范围与影响程度。
  • 如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
  • 如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
  • 如何尽可能精确的分析系统的存储瓶颈与容量规划。
  • 如何实时观测系统的整体调用链路情况。

上述问题就是分布式链路追踪技术要解决的问题。所谓的分布式链路追踪,就是将对分布式系统的一次请求转化成一个完整的调用链路。这个完整的调用链路从请求进入分布式系统的入口开始,直到整个请求返回为止。并在请求调用微服务的过程中,记录相应的调用日志,监控系统调用的性能,并且可以按照某种方式显示请求调用的情况。

在分布式链路追踪中,可以统计调用每个微服务的耗时,请求会经过哪些微服务的流转,每个微服务的运行状况等信息

1.2、核心原理

假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。

在这里插入图片描述

那么链路追踪会在每个服务调用的时候加上 Trace ID 和 Span ID。如下图所示:

在这里插入图片描述

相同颜色的代表是同一个 Span ID,说明是链路追踪中的一个节点

  1. 用户端调用 Service 1,生成一个 Request,Trace ID 和 Span ID 为空,那个时候请求还
    没有到 Service 1。
  2. 请求到达 Service 1,记录了 Trace ID = X,Span ID 等于 A。
  3. Service 1 发送请求给 Service 2,Span ID 等于 B,被称作 Client Sent,即用户端发送一
    个请求。
  4. 请求到达 Service 2,Span ID 等于 B,Trace ID 不会改变,被称作 Server Received,即
    服务端取得请求并准备开始解决它。
  5. Service 2 开始解决这个请求,解决完之后,Trace ID 不变,Span ID = C。
  6. Service 2 开始发送这个请求给 Service 3,Trace ID 不变,Span ID = D,被称作 Client
    Sent,即用户端发送一个请求。
  7. Service 3 接收到这个请求,Span ID = D,被称作 Server Received。
  8. Service 3 开始解决这个请求,解决完之后,Span ID = E。
  9. Service 3 开始发送响应给 Service 2,Span ID = D,被称作 Server Sent,即服务端发送
    响应。
  10. Service 2 收到 Service 3 的响应,Span ID = D,被称作 Client Received,即用户端接收
    响应。
  11. Service 2 开始返回 响应给 Service 1,Span ID = B,和第三步的 Span ID 相同,被称
    作 Client Received,即用户端接收响应。
  12. Service 1 解决完响应,Span ID = A,和第二步的 Span ID 相同。
  13. Service 1 开始向用户端返回响应,Span ID = A、
  14. Service 3 向 Service 4 发送请求和 Service 3 相似,对应的 Span ID 是 F 和 G。可以参照上面前面的第六步到第十步

把以上的相同颜色的步骤简化为下面的链路追踪图:

在这里插入图片描述

  • 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。
  • 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1
    的过程。
  • 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。
  • 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2
    的过程。
  • 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。
  • 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。
  • 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程

通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。

1.3、解决方案

目前,行业内比较成熟的分布式链路追踪技术解决方案如下所示:

技术说明
Cat由大众点评开源,基于 Java 开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高。风险较大
ZipKin由 Twitter 公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。结合 spring-cloud-sleuth 使用较为简单, 集成方便, 但是功能较简单
PinpointPinpoint 是一款开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件, UI 功能强大,接入端无代码侵入
SkywalkingSkyWalking 是国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件, UI功能较强,接入端无代码侵入
SleuthSleuth 是 SpringCloud 中的一个组件,为 Spring Cloud 实现了分布式跟踪解决方案

2、集成 Sleuth 实现链路追踪

2.1、Sleuth 概述

Sleuth 是 SpringCloud 中提供的一个分布式链路追踪组件,在设计上大量参考并借用了 Google Dapper 的设计

2.1.1、Span 简介

Span 在 Sleuth 中代表一组基本的工作单元,当请求到达各个微服务时,Sleuth 会通过一个唯一的标识,也就是 SpanId 来标记开始通过这个微服务,在当前微服务中执行的具体过程和执行结束,此时,通过 SpanId 标记的开始时间戳和结束时间戳,就能够统计到当前 Span 的调用时间,也就是当前微服务的执行时间。另外,也可以用过 Span 获取到事件的名称,请求的信息等数据。

远程调用和 Span 是一对一的关系,是分布式链路追踪中最基本的工作单元,每次发送一个远程调用服务就会产生一个 Span。Span Id 是一个 64 位的唯一 ID,通过计算 Span 的开始和结束时间,就可以统计每个服务调用所耗费的时间。

2.1.2、Trace 简介

Trace 的粒度比 Span 的粒度大,Trace 主要是由具有一组相同的 Trace ID 的 Span 组成的,从请求进入分布式系统入口经过调用各个微服务直到返回的整个过程,都是一个 Trace。也就是说,当请求到达分布式系统的入口时,Sleuth 会为请求创建一个唯一标识,这个唯一标识就是 Trace Id,不管这个请求在分布式系统中如何流转,也不管这个请求在分布式系统中调用了多少个微服务,这个 Trace Id 都是不变的,直到整个请求返回。

一个 Trace 可以对应多个 Span,Trace 和 Span 是一对多的关系。Trace Id 是一个64 位的唯一 ID。Trace Id 可以将进入分布式系统入口经过调用各个微服务,再到返回的整个过程的请求串联起来,内部每通过一次微服务时,都会生成一个新的 SpanId。Trace 串联了整个请求链路,而 Span 在请求链路中区分了每个微服务

2.1.3、Annotation 简介

Annotation 记录了一段时间内的事件,内部使用的重要注解如下所示:

  • cs(Client Send)客户端发出请求,标记整个请求的开始时间。
  • sr(Server Received)服务端收到请求开始进行处理,通过 sr 与 cs 可以计算网络的延迟时间,例如:sr- cs = 网络延迟(服务调用的时间)。
  • ss(Server Send)服务端处理完毕准备将结果返回给客户端, 通过 ss 与 sr 可以计算服务器上的请求处理时间,例如:ss - sr = 服务器上的请求处理时间。
  • cr(Client Reveived)客户端收到服务端的响应,请求结束。通过 cr 与 cs 可以计算请求的总时间,例如:cr - cs = 请求的总时间

链路追踪系统内部定义了少量核心注解,用来定义一个请求的开始和结束,通过这些注解,我们可以计算出请求的每个阶段的时间。需要注意的是,这里说的请求,是在系统内部流转的请求,而不是从浏览器、APP、H5、小程序等发出的请求

2.2、集成 Sleuth

Sleuth 提供了分布式链路追踪能力,如果需要使用 Sleuth 的链路追踪功能,需要在项目中集成 Sleuth。

2.2.1、最简使用

1、在每个微服务(用户微服务 shop-user、商品微服务 shop-product、订单微服务 shop-order、网关服务 shop-gateway)下的 pom.xml 文件中添加如下 Sleuth的依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2、将项目的 application.yml 文件备份成 application-pre-filter.yml,并将 application.yml 文件的内容
替换为 application-sentinel.yml 文件的内容,这一步是为了让整个项目集成 SentinelSpringCloud GatewayNacosapplication.yml 替换后的文件内容如下所示:

server:port: 10002
spring:application:name: server-gatewaymain:allow-bean-definition-overriding: true  # 解决重复注册的问题cloud:nacos:discovery:server-addr: 127.0.0.1:8848sentinel:transport:port: 7777dashboard: 127.0.0.1:8888web-context-unify: false          # 开启 sentinel 的 web 监控eager: true                       # 启动时加载所有规则到 Sentinelgateway:globalcors:                       # 配置跨域cors-configurations:'[/**]':allowedOrigins: "*"allowedMethods: "*"allowCredentials: trueallowedHeaders: "*"discovery:locator:enabled: true                 # 开启服务发现route-id-prefix: gateway-     # 路由 id 前缀

3、分别启动 NacosSentinel、用户微服务 shop-user,商品微服务 shop-product,订单微服务
shop-order 和网关服务 shop-gateway,在浏览器中输入链接 localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1 ,如下所示:

在这里插入图片描述

4、分别查看用户微服务 shop-user,商品微服务 shop-product,订单微服务 shop-order 和网关服务shop-gateway 的控制台输出【调用到的接口需要日志打印】,每个服务的控制台都输出了如下格式所示的信息:

[微服务名称,TraceID,SpanID,是否将结果输出到第三方平台]

具体如下所示:

  • 用户微服务 shop-user
    在这里插入图片描述
[server-user,878079143b6dc50d,ff6532c549a74886,true]
  • 商品微服务 shop-product
    在这里插入图片描述
[server-product,878079143b6dc50d,55f0ab3d9060c096,true]
[server-product,878079143b6dc50d,fc2b203d909deed5,true]
  • 订单微服务 shop-order

在这里插入图片描述

[server-order,878079143b6dc50d,24b24b94f287e162,true] 
  • 网关服务 shop-gateway

在这里插入图片描述

[server-gateway,878079143b6dc50d,878079143b6dc50d,true]

每个服务都打印出了链路追踪的日志信息,说明引入 Sleuth 的依赖后,就可以在命令行查看链路追踪情况

2.2.2、抽样采集数据

Sleuth 支持抽样采集数据。尤其是在高并发场景下,如果采集所有的数据,那么采集的数据量就太大
了,非常耗费系统的性能。通常的做法是可以减少一部分数据量,特别是对于采用 HTTP 方式去发送采集数据,能够提升很大的性能。

Sleuth 可以采用如下方式配置抽样采集数据:

spring:sleuth:sampler:probability: 1.0

2.2.3、追踪自定义线程池

Sleuth 支持对异步任务的链路追踪,在项目中使用 @Async 注解开启一个异步任务后,Sleuth 会为异步任务重新生成一个 Span。但是如果使用了自定义的异步任务线程池,则会导致 Sleuth 无法新创建一个 Span,而是会重新生成 Trace 和 Span。此时,需要使用 Sleuth 提供的 LazyTraceExecutor 类来包装下异步任务线程池,才能在异步任务调用链路中重新创建 Span。

在服务中开启异步线程池任务,需要使用 @EnableAsync。所以,先在用户微服务 shop-user 的启动类上添加 @EnableAsync 注解,如下所示:

@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = {"com.zzc.user.mapper"})
@EnableDiscoveryClient
@EnableAsync  // 添加异步
public class ShopUserApplication {public static void main(String[] args) {SpringApplication.run(ShopUserApplication.class, args);}
}
2.2.3.1、演示使用 @Async 注解开启任务

1、在用户微服务 shop-usercom.zzc.user.service.UserService 接口中定义一个asyncMethod() 方法,如下所示:

void asyncMethod();

2、在用户微服务 shop-usercom.zzc.user.service.impl.UserServiceImpl 类中实现asyncMethod() 方法,并在 asyncMethod() 方法上添加 @Async 注解,如下所示:

@Async
@Override
public void asyncMethod() {log.info("执行了异步任务...");
}

3、在用户微服务 shop-usercom.zzc.user.controller.UserController 类中新增asyncApi() 方法,如下所示:

@GetMapping(value = "/async/api")
public String asyncApi() {log.info("执行异步任务开始...");userService.asyncMethod();log.info("异步任务执行结束...");return "asyncApi";
}

4、分别启动用户微服务和网关服务,在浏览器中输入链接http://localhost:10001/server-user/user/async/api

在这里插入图片描述

5、查看用户微服务与网关服务的控制台日志,分别存在如下日志:

  • 用户微服务
INFO [server-user,7665700fe4705c45,d690abaa42ae39a2,true] 9344 --- [nio-8060-exec-2] UserController   : 执行异步任务开始...
INFO [server-user,7665700fe4705c45,d690abaa42ae39a2,true] 9344 --- [nio-8060-exec-2] UserController   : 异步任务执行结束...
INFO [server-user,7665700fe4705c45,711fd801ff1b9be4,true] 9344 --- [         task-2] UserServiceImpl  : 执行了异步任务...
  • 网关服务
INFO [server-gateway,7665700fe4705c45,7665700fe4705c45,true] 11072 --- [ctor-http-nio-4] GlobalGatewayLogFilter    : 访问接口时长: 7ms

可以看到 Sleuth 为异步任务重新生成了 Span

2.2.3.2、演示自定义任务线程池

在演示使用 @Async 注解开启任务的基础上继续演示自定义任务线程池,验证 Sleuth 是否为自定义线程池新创建了 Span。

1、在用户微服务 shop-user 中新建 com.zzc.user.config 包,在包下创建 ThreadPoolTaskExecutorConfig 类,继承 org.springframework.scheduling.annotation.AsyncConfigurerSupport 类,用来自定义异步任务线程池,代码如下所示:

@Configuration
@EnableAutoConfiguration
public class ThreadPoolTaskExecutorConfig extends AsyncConfigurerSupport {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(2);executor.setMaxPoolSize(5);executor.setQueueCapacity(10);executor.setThreadNamePrefix("trace-thread-");executor.initialize();return executor;}}

2、以 debug 的形式启动用户微服务和网关服务,并在 com.zzc.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor() 方法中打上断点,如下所示:

在这里插入图片描述

可以看到,项目启动后并没有进入 com.zzc.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor() 方法,说明项目启动时,并不会创建异步任务线程池

3、在浏览器中输入链接
http://localhost:10001/server-user/user/async/api ,此时可以看到程序已经执行到com.zzc.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor() 方法的断点位置:

在这里插入图片描述

说明异步任务线程池是在调用了异步任务的时候创建的

4、放过断点,查看用户微服务与网关服务的控制台日志,分别存在如下日志:

  • 用户微服务
[server-user,660a8e5e012f2ce2,c24ac93bd34daf5f,true]: 执行异步任务开始...
[server-user,660a8e5e012f2ce2,c24ac93bd34daf5f,true]: 异步任务执行结束...
[server-user,411ad130ab8b4166,411ad130ab8b4166,true]: 执行了异步任务...

最后一条日志信息的 TraceID 和 SpanID 与前两条日志都不同

  • 网关服务
[server-gateway,660a8e5e012f2ce2,660a8e5e012f2ce2,true]

可以看到,使用自定义异步任务线程池时,在用户微服务中在执行异步任务时,重新生成了 Trace 和 Span

2.2.3.3、演示包装自定义线程池

在自定义任务线程池的基础上继续演示包装自定义线程池,验证 Sleuth 是否为包装后的自定义线程池新创建了 Span

1、在用户微服务 shop-usercom.zzc.user.config.ThreadPoolTaskExecutorConfig 类中注入 BeanFactory,并在 getAsyncExecutor() 方法中使用 org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor() 来包装返回的异
步任务线程池,修改后的 com.zzc.user.config.ThreadPoolTaskExecutorConfig 类的代码如下所示:

@Configuration
@EnableAutoConfiguration
public class ThreadPoolTaskExecutorConfig extends AsyncConfigurerSupport {@Autowiredprivate BeanFactory beanFactory;@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(2);executor.setMaxPoolSize(5);executor.setQueueCapacity(10);executor.setThreadNamePrefix("trace-thread-");executor.initialize();return new LazyTraceExecutor(this.beanFactory, executor);}}

2、分别启动用户微服务和网关服务,在浏览器中输入链接 http://localhost:10001/server-user/user/async/api

3、查看用户微服务与网关服务的控制台日志,分别存在如下日志

  • 用户微服务
[server-user,2d2955917de0be06,d0e72709a0017f7f,true]: 执行异步任务开始...
[server-user,2d2955917de0be06,d0e72709a0017f7f,true]: 异步任务执行结束...
[server-user,2d2955917de0be06,3a7c998785cfe446,true]: 执行了异步任务...

最后一条日志信息的 TraceID 和 SpanID 与前两条日志都不同

  • 网关服务
[server-gateway,2d2955917de0be06,2d2955917de0be06,true]

可以看到 Sleuth 为异步任务重新生成了 Span

2.2.3.4、总结

Sleuth 支持对异步任务的链路追踪,在项目中使用 @Async 注解开启一个异步任务后,Sleuth 会为异步任务重新生成一个 Span。但是如果使用了自定义的异步任务线程池,则会导致 Sleuth 无法新创建一个 Span,而是会重新生成 Trace 和 Span。此时,需要使用 Sleuth 提供的LazyTraceExecutor类来包装下异步任务线程池,才能在异步任务调用链路中重新创建 Span

2.2.4、自定义链路过滤器

在 Sleuth 中存在链路过滤器,并且还支持自定义链路过滤器

2.2.4.1、自定义链路过滤器概述

TracingFilter 是 Sleuth 中负责处理请求和响应的组件,可以通过注册自定义的 TracingFilter 实例来实现一些扩展性的需求

2.2.4.2、演示自定义链路过滤器

通过过滤器验证只有 HTTP 或者 HTTPS 请求才能访问接口,并且在访问的链接不是静态文件
时,将 traceId 放入 HttpRequest 中在服务端获取,并在响应结果中添加自定义 Header,名称为SLEUTH-HEADER,值为 traceId

1、在用户微服务 shop-user 中新建 com.zzc.user.filter 包,并创建 MyGenericFilter 类,继承 org.springframework.web.filter.GenericFilterBean 类,代码如下所示:

@Component
@Order( Ordered.HIGHEST_PRECEDENCE + 6)
public class MyGenericFilter extends GenericFilterBean {private Pattern skipPattern = Pattern.compile(SleuthWebProperties.DEFAULT_SKIP_PATTERN);private final Tracer tracer;public MyGenericFilter(Tracer tracer){this.tracer = tracer;}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)){throw new ServletException("只支持HTTP访问");}Span currentSpan = this.tracer.currentSpan();if (currentSpan == null) {chain.doFilter(request, response);return;}HttpServletRequest httpServletRequest = (HttpServletRequest) request;HttpServletResponse httpServletResponse = ((HttpServletResponse) response);boolean skipFlag = skipPattern.matcher(httpServletRequest.getRequestURI()).matches();if (!skipFlag){String traceId = currentSpan.context().traceIdString();httpServletRequest.setAttribute("traceId", traceId);httpServletResponse.addHeader("SLEUTH-HEADER", traceId);}chain.doFilter(httpServletRequest, httpServletResponse);}
}

2、在用户微服务 shop-usercom.zzc.user.controller.UserController 类中新建 sleuthFilter() 方法,在 sleuthFilter()方法中获取并打印 traceId,如下所示:

@GetMapping(value = "/sleuth/filter/api")
public String sleuthFilter(HttpServletRequest request) {Object traceIdObj = request.getAttribute("traceId");String traceId = traceIdObj == null ? "" : traceIdObj.toString();log.info("获取到的traceId为: " + traceId);return "sleuthFilter";
}

3、分别启动用户微服务和网关服务,在浏览器中输入 http://localhost:10001/server-user/user/sleuth/filter/api,查看用户微服务的控制台会输出如下信息:

[server-user,37018cdeb6cc7b5b,f7f441bfe422e2ee,true]: 获取到的traceId为: 37018cdeb6cc7b5b

查看浏览器的控制台,看到在响应的结果信息中新增了一个名称为 SLEUTH-HEADER,如下所示

在这里插入图片描述

说明使用 Sleuth 的过滤器可以处理请求和响应信息,并且可以在 Sleuth 的过滤器中获取到 TraceID

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 百度飞浆目标检测PPYOLOE模型在PC端、Jetson上的部署(python)
  • Python入门笔记
  • 【C/C++】C++类与对象基本概念(抽象封装、类的定义与使用、构造函数、析构函数、静态成员、友元)
  • openlayers10+vue3+ts
  • Java后端 - 常见BUG及其处理策略(持续更新中~)
  • C语言备忘
  • RocketMQ学习(二)
  • 华为OD机试真题 - 字符成环找偶数O - 滑动窗口(Python/JS/C/C++ 2024 E卷 100分)
  • C++中AVL树的底层逻辑原理及其实现原理和过程
  • 【C++ | 设计模式】代理模式的详解与实现
  • minio文件存储+ckplayer视频播放(minio分片上传合并视频播放)
  • Java 4.3 - Redis
  • HTML中自定义属性并通过JS获取属性值
  • 【生日视频制作】农村大马路绿色墙体广告标语喷漆AE模板修改文字软件生成器教程特效素材【AE模板】
  • SpringMVC 笔记篇
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • __proto__ 和 prototype的关系
  • 【技术性】Search知识
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • ES6系列(二)变量的解构赋值
  • javascript 哈希表
  • leetcode386. Lexicographical Numbers
  • linux学习笔记
  • log4j2输出到kafka
  • mac修复ab及siege安装
  • mysql innodb 索引使用指南
  • PHP CLI应用的调试原理
  • react 代码优化(一) ——事件处理
  • 聊聊flink的TableFactory
  • 目录与文件属性:编写ls
  • 前端面试总结(at, md)
  • 设计模式 开闭原则
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 限制Java线程池运行线程以及等待线程数量的策略
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • # C++之functional库用法整理
  • # 达梦数据库知识点
  • # 计算机视觉入门
  • (2)MFC+openGL单文档框架glFrame
  • (2024.6.23)最新版MAVEN的安装和配置教程(超详细)
  • (5)STL算法之复制
  • (C++)八皇后问题
  • (免费领源码)Python#MySQL图书馆管理系统071718-计算机毕业设计项目选题推荐
  • (推荐)叮当——中文语音对话机器人
  • (五)Python 垃圾回收机制
  • (原创)boost.property_tree解析xml的帮助类以及中文解析问题的解决
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • (最新)华为 2024 届秋招-硬件技术工程师-单板硬件开发—机试题—(共12套)(每套四十题)
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .net core + vue 搭建前后端分离的框架
  • .NET CORE 3.1 集成JWT鉴权和授权2
  • .NET Core实战项目之CMS 第十二章 开发篇-Dapper封装CURD及仓储代码生成器实现
  • .NET程序集编辑器/调试器 dnSpy 使用介绍
  • .NET微信公众号开发-2.0创建自定义菜单