工作中遇到的一些技术
dynamic-tp动态线程池
动态线程池,可以通过nacos配置动态修改线程池参数,官网 以下是dynamic-tp与nacos集成的步骤,更详细配置项可以查看官网
- 引入依赖坐标
<dependency>
<groupId>org.dromara.dynamictp</groupId>
<artifactId>dynamic-tp-spring-cloud-starter-nacos</artifactId>
<version>1.1.3</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.dromara.dynamictp</groupId>
<artifactId>dynamic-tp-extension-skywalking</artifactId>
<version>1.1.3</version>
</dependency>
- 添加配置项
spring:
dynamic:
tp:
enabled: true # 是否启用 dynamictp,默认true
enabledCollect: true # 是否开启监控指标采集,默认true
collectorTypes: internal_logging # 监控数据采集器类型(logging | micrometer | internal_logging | JMX),默认micrometer
monitorInterval: 5 # 监控时间间隔(报警检测、指标采集),默认5s
executors: # 动态线程池配置,都有默认值,采用默认值的可以不配置该项,减少配置量
- threadPoolName: dtpExecutor1 # 线程池名称,必填
threadPoolAliasName: 测试线程池 # 线程池别名,可选
executorType: common # 线程池类型 common、eager、ordered、scheduled、priority,默认 common
corePoolSize: 6 # 核心线程数,默认1
maximumPoolSize: 8 # 最大线程数,默认cpu核数
queueCapacity: 2000 # 队列容量,默认1024
queueType: VariableLinkedBlockingQueue # 任务队列,查看源码QueueTypeEnum枚举类,默认VariableLinkedBlockingQueue
rejectedHandlerType: CallerRunsPolicy # 拒绝策略,查看RejectedTypeEnum枚举类,默认AbortPolicy
keepAliveTime: 60 # 空闲线程等待超时时间,默认60
threadNamePrefix: test # 线程名前缀,默认dtp
allowCoreThreadTimeOut: false # 是否允许核心线程池超时,默认false
waitForTasksToCompleteOnShutdown: true # 参考spring线程池设计,优雅关闭线程池,默认true
awaitTerminationSeconds: 5 # 优雅关闭线程池时,阻塞等待线程池中任务执行时间,默认3,单位(s)
preStartAllCoreThreads: false # 是否预热所有核心线程,默认false
runTimeout: 200 # 任务执行超时阈值,单位(ms),默认0(不统计)
queueTimeout: 100 # 任务在队列等待超时阈值,单位(ms),默认0(不统计)
taskWrapperNames: ["swTrace"] # 任务包装器名称,继承TaskWrapper接口
notifyEnabled: false # 是否开启报警,默认true
nacos作为配置中心
- 引入依赖坐标
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
- 新增bootstrap.yml
spring:
application:
name: TEST-CONFIG
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
username: root
password: root
file-extension: yaml
extension-configs:
- dataId: ${spring.application.name}.${spring.cloud.nacos.config.file-extension}
group: DEFAULT_GROUP
refresh: true
refresh-enabled: true
---
spring:
profiles: test
cloud:
nacos:
config:
namespace: test
guava-retry重试
- 引入依赖坐标
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
- 创建重试器
private static final Retryer<TestDTO> retryer;
static {
retryer = RetryerBuilder.<TestDTO>newBuilder()
// 重试条件
.retryIfException() // 抛出异常时重试
.retryIfResult(Predicates.isNull()) // 返回结果为null时重试
// 等待策略:每次请求间隔3s
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
// 停止策略:尝试请求5次
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.build();
}
- 使用重试
Callable<TestDTO> callable = new Callable<TestDTO>() {
int times = 0;
@Override
public TestDTO call() throws Exception {
times++;
log.info("第{}次请求", times);
try {
// TODO 具体逻辑
return new TestDTO();
} catch (Exception e) {
log.error("请求失败", e);
return null;
}
}
};
try {
retryer.call(callable);
} catch (Exception e) {
log.error("重试请求异常", e);
}
hystrix降级和ribben超时时间
# ribbon的超时时间
ribbon:
ReadTimeout: 9000 # 默认1秒
ConnectTimeout: 9000 # 默认1秒
# 配置需要立即加载的其他服务(饿加载)
eager-load:
enabled: true
clients: TEST-JOB
feign:
hystrix:
enabled: true # 开启hystrix,老版本配置,启动类上使用@EnableHystrix 注解
feign:
circuitbreaker:
enabled: true # 开启hystrix,新版本配置,启动类上使用@EnableCircuitBreaker 注解
# hystrix降级的超时时间
hystrix:
command:
default: # 全局使用配置超时时间 只针对具体服务可以写服务名
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 9000 # 默认1秒 一般Hystrix 的超时时间必须不小于 ribbon 设置的超时时间,这是为了ribbon的重试机制有时间执行
threadpool:
TEST-CONFIG: # 针对具体服务设置线程池 默认全局使用default
coreSize: 10 # 核心线程数 默认10
maximumSize: 20 # 最大线程数 默认10
maxQueueSize: 20 # 队列最大长度 默认-1
allowMaximumSizeToDivergeFromCoreSize: true # 允许最大线程数超过核心线程数 默认false
queueSizeRejectionThreshold: 20 # 队列大小拒绝阈值 默认5
ribbon默认重试1次,即当超过ribbon时间后会再次执行一次,可以设置重试次数或者关闭。很多时候我们认为feign调用有重试,实际上是因为openFeign集成了ribbon并开启了重试。
hystrix 设置降级超时时间后,如果没有配置 fallback,那么 hystrix 的超时就不会生效,而是由 ribbon 来控制
hystrix降级
// feign调用中使用降级,定义RemoteUserFallbackFactory类实现ExampleService接口实现降级方法
@FeignClient(value = "服务名", fallbackFactory = RemoteUserFallbackFactory.class)
// 如果其他方法上使用@HystrixCommand 注解,并指定了 fallbackMethod 属性,该属性指定了当该方法执行失败时的回退方法
@HystrixCommand(fallbackMethod="fallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000" ),
@HystrixProperty(name = "execution.timeout.enabled", value = "true")
})
// 当然也可以指定commandKey,然后在配置文件中配置超时时间,如下:
@HystrixCommand(fallbackMethod="fallback",commandKey="userGetKey")
// 配置文件如下
hystrix:
command:
userGetKey:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 1000
实际上feign也有超时时间,默认feign连接超时时间是10秒,读取超时时间是60秒。
feign:
client:
config:
default:
connectTimeout: 6000
readTimeout: 6000
具体服务名:
connectTimeout: 6000
readTimeout: 6000
# 最新版本的设置是Spring下openFeign,这种是用的OpenFeign,因为Springcloud不兼容feign了。上边的不生效可以试试
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 6000
readTimeout: 6000
注意:OpenFeign 和 Ribbon 的超时时间只会有一个生效两者是二选一的,且 OpenFeign 有配置则优先生效,未配置则使用 Ribbon 配置
curl
curl -H 'Content-Type: application/json' -X POST -o --location 'https://www.xxx.com' -d '{"aooliType": "0"}'
-H: 请求头
-X: 请求方式
-d: 请求体
-o --location: 如果有些https地址不安全可以添加该参数跳过验证
curl -o /dev/null -s -w "total time: %{time_total} seconds\n" 命令中加入这些参数可以测试curl调用url用时多少秒
traceId和tid
作为分布式链路追踪技术中,一般使用有spring-cloud-sleuth(zipkin),skywalking等等。traceId是sleuth中的概念,而在skywalking中类似概念的是tid,它们都表示一个链路的唯一id,可以通过这个id将一次微服务调用串联起来
spring-cloud-sleuth
- 引入依赖坐标
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
- 修改日志文件(logback.spring.xml)
<!-- 输出日志到文件 -->
<appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${app.log_dir}/log_error.log</file>
<!--日志文件输出格式 X-B3-TraceId X-B3-SpanId X-B3-ParentSpanId是sleuth中固定的名词-->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [${appName},%x{X-B3-TraceId},%x{X-B3-SpanId},%x{X-B3-ParentSpanId}] --[%thread] %logger %msg</pattern>
</layout>
<!-- 此处设置字符集 -->
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器指定拆分策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--按照时间拆分文件名 如果以.gz或.zip结尾可以压缩-->
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!--按照文件大小拆分-->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
- 以上配置代码中输出log即可,但是在多线程中需要将TraceId传过去
@Autowired
private Tracer tracer;
public void test(String[] args) {
Span span = tracer.currentSpan();
CompletableFuture.supplyAsync(() -> {
tracer.withSpanInScope(span);
log.info("业务逻辑开始");
// TODO 业务逻辑
return new TestDTO();
}).exceptionally(throwable -> {
log.error("请求失败", throwable);
return null;
});
}
skywalking
- 引入依赖坐标
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.7.0</version>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.11.0</version>
</dependency>
- 修改日志文件(logback.spring.xml)
<!-- 输出日志到文件 -->
<appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${app.log_dir}/log_error.log</file>
<!--日志文件输出格式 tid是skywalking中固定的名词-->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%tid] --[%thread] %logger %msg</pattern>
</layout>
<!-- 此处设置字符集 -->
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器指定拆分策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--按照时间拆分文件名 如果以.gz或.zip结尾可以压缩-->
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!--按照文件大小拆分-->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
- 以上配置代码中输出log即可,但是在多线程中需要处理下
CompletableFuture.supplyAsync(SupplierWrapper.of(() -> {
log.info("业务逻辑开始");
// TODO 业务逻辑
return new TestDTO();
})).exceptionally(FunctionWrapper.of(throwable -> {
log.error("请求失败", throwable);
return null;
}));
// 如果多线程不需要返回值
CompletableFuture.runAsync(RunnableWrapper.of(() -> {
log.info("业务逻辑开始");
// TODO 业务逻辑
}));
skywalking是在jar启动时通过javaagent参数执行的
-javaagent:/home/appadmin/skywalking_agent/skywalking-agent.jar -Dskywalking.agent.service_name=TEST_CONFIG -Dskywalking.agent.instance_name=TEST_CONFIG_${ip}_${PORT}
指定jar包路径以及服务名和实例名
jasypt加密
整合SpringBoot项目
- 引入依赖坐标
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
- 启动参数添加jasypt密钥
本地启动:Idea Program arguments添加参数--jasypt.encryptor.password=密钥
服务器启动:SpringBoot启动参数添加--jasypt.encryptor.password=密钥
- 通过代码生成密文,将生成的密文替换为原本的密码,格式为
ENC(密文)
使用Telnet远程登陆执行命令
Telnet一般可用于查看ip+port是否可以连通,实际上Telnet和SSH一样均为用于访问远程计算机的通信协议,以实现远程控制和维护。Telnet在建立连接时直接传输明文数据, SSH传输的数据都经过加密处理, 提供了Telnet所不具备的安全性保障。
以下是使用Telnet远程执行redis指令流程
telnet ip 3306
ctrl+] # 进入输入命令模式,注意ctrl+]之后需要先输入一个回车才能输入其他命令
AUTH redis密码
select 0 # 选择redis库 redis有0~15个库
ping # pong ping/pong验证连接是否正常
redis其他指令...
Spring中的一些接口
ApplicationContextAware接口 setApplicationContext()方法:可以获取Spring容器applicaitonContext
InitializingBean接口 afterPropertiesSet()方法:在bean中属性初始化后的处理方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法
ApplicationRunner 和 CommandLineRunner接口 run()方法:可以在应用启动后执行一些代码。二者之间的主要区别在于它们对输入参数的处理方式。
CommandLineRunner 直接接收一个 String[] 数组作为参数,这意味着开发者需要自己解析和处理这些参数。而 ApplicationRunner 则接收一个 ApplicationArguments 对象,该对象封装了启动参数,并提供了一些有用的方法来方便地处理这些参数
CompletionService
CompletionService 是 Java 中用于处理一批异步任务的工具类,它允许你以异步的方式提交任务,并在任务完成时按照完成顺序获取结果。CompletionService 的底层原理主要基于阻塞队列和线程池。
CompletionService 使用一个阻塞队列来保存已完成的任务。当一个任务完成时,它会被放入队列中。阻塞队列的选择通常是 LinkedBlockingQueue,它是一个先进先出的队列,确保按照任务完成的顺序排列。
CompletionService 通常与 Executor 框架一起使用。创建一个线程池并将其传递给 CompletionService 的构造函数。这个线程池负责执行提交的任务。
当想要获取已完成的任务的结果时,可以调用 CompletionService 的 take() 或 poll() 方法。这些方法会从阻塞队列中取出已完成的任务的 Future,并返回它。如果队列为空,take() 方法会阻塞,而 poll() 方法会返回 null。
List<Future<Result>> resultFutureList = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(5);
ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
try {
// callable数量
for (Callable<T> task : tasks) {
resultFutureList.add(completionService.submit(task));
}
// 获取执行结果
for (Callable<T> task : tasks) {
// 设置最多1分钟的获取任务等待时间 避免长时间耗在这里
Future<Result> resultFuture = completionService.poll(1, TimeUnit.MILLTSECONDS);
if (resultFuture == null) {
...
break;
}
Result result = resultFuture.get();
...
}
} finally {
executorService.shutdown();
}
CompletableFuture
Java 8 引入CompletableFuture 类可以解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
List<CompletableFuture<Result>> futureList = new ArrayList<>(2);
futureList.add(CompletableFuture.supplyAsync(() -> {
// 任务1
})).exceptionally(throwable -> {
// 异常处理
});
futureList.add(CompletableFuture.supplyAsync(() -> {
// 任务2
})).exceptionally(throwable -> {
// 异常处理
});
for (CompletableFuture<Result> future : futureList) {
Result result = future.get();
}
实现CountDownLatch效果
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
// 任务1
}, pool);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
// 任务2
}, pool);
CompletableFuture.allOf(future1, future2).join();
supplyAsync方法会有返回值,这个值最终会成为这个 CompletableFuture 对象的结果。runAsync方法没有返回值,所以 runAsync 返回的 CompletableFuture 对象通过 get 方法总是返回的是 null