如何实现线程池之间的数据透传 ?

如何实现线程池之间的数据透传 ?

  • 引言
  • transmittable-thread-local
    • 概览
    • capture
      • 如何 capture
      • 如何保存捕获的数据
    • save 和 replay
    • restore
  • 小结


引言

当我们涉及到数据的全链路透传场景时,通常会将数据存储在线程的本地缓存中,如: 用户认证信息透传,链路追踪信息透传时;但是这里可能面临着数据在两个没有血缘关系的兄弟线程间透传的问题,这通常涉及到两个不同线程池之间数据的透传问题,如下图所示:

在这里插入图片描述
为了解决上面这个问题,最简单的思路就是手动在各个线程池的切换处添加捕获和回放逻辑,如下所示:

public class TTLMain {private static final Executor TTL_TEST_THREAD_POOL = Executors.newFixedThreadPool(1);public static void main(String[] args) {ThreadLocal<Integer> userId = new ThreadLocal<>();userId.set(1);// 1. 捕获当前线程的上下文信息Integer captured = userId.get();TTL_TEST_THREAD_POOL.execute(() -> {userId.set(2);// 2. 保存当前线程的上下文信息Integer backup = userId.get();// 3. 重放捕获的目标线程的上下文信息userId.set(captured);System.out.println("重放上下文后: 用户ID=" + userId.get());// 4. 恢复原先的线程上下文信息userId.set(backup);System.out.println("恢复上下文后: 用户ID="+userId.get());});}
}

其实不难看出整个处理过程分为四个阶段:

  1. capture : 捕获当前线程上下文信息
  2. save : 保存目标线程上下文信息
  3. replay : 重放当前线程的上下文信息到目标线程中
  4. restore : 恢复目标线程原先的上下文信息

整个过程属于一个模版流程,因此我们可以想办法把上面这段逻辑单独抽取固定下来,而非在各个切换处进行手动编码操作,因此这里引出了我今天想要介绍的现成工具类: transmittable-thread-local 。


transmittable-thread-local

transmittable-thread-local 是阿里开源的一个线程池间数据透传工具类,它的实现思路其实就是上面我讲的四个阶段,下面我们先来看看transmittable-thread-local具体是如何使用的吧:

public class TTLMain {private static ExecutorService TTL_TEST_THREAD_POOL = Executors.newFixedThreadPool(1);public static void main(String[] args) {demo1();demo2();demo3();}private static void demo1() {// 1. 修饰RunnableTransmittableThreadLocal<Integer> context = new TransmittableThreadLocal<>();context.set(1);TTL_TEST_THREAD_POOL.execute(TtlRunnable.get(() -> {System.out.println("修饰Runnable: " + context.get());}));}@SneakyThrowsprivate static void demo2() {// 1. 修饰CallableTransmittableThreadLocal<Integer> context = new TransmittableThreadLocal<>();context.set(1);Future<Integer> future = TTL_TEST_THREAD_POOL.submit(TtlCallable.get(context::get));System.out.println("修饰Callable: "+future.get());}@SneakyThrowsprivate static void demo3() {// 1. 修饰线程池TTL_TEST_THREAD_POOL = TtlExecutors.getTtlExecutorService(TTL_TEST_THREAD_POOL);TransmittableThreadLocal<Integer> context = new TransmittableThreadLocal<>();context.set(1);Future<Integer> future = TTL_TEST_THREAD_POOL.submit(context::get);System.out.println("修饰线程池: "+future.get());}
}

使用上比较简单,核心还是将capture,save,replay ,restore 四个阶段的逻辑以模版流程的形式安排到了TtlRunnable和TtlCallable中,下面我们就来看看transmittable-thread-local具体是如何实现的,以及我们能从中学到什么样设计技巧。


概览

在这里插入图片描述
TransmittableThreadLocal实现了InheritableThreadLocal,其可以确保数据能够在父子线程间进行透传,透传逻辑体现在Thread的构造函数中;而TransmittableThreadLocal要做的事情就是解决数据在不同线程池之间进行数据透传的问题,该问题解决思路就是本篇开头提到的思路,下面我将分四个阶段,依次来看看TransmittableThreadLocal是如何实现的。


capture

捕获阶段我们需要捕获当前线程使用到的所有TransmittableThreadLocal实例的数据,这一点如何做到 ? 以及我们用什么样的数据结构来保持捕获到的数据呢 ?

便于行文方便,下面会将TransmittableThreadLocal简写为TTL

如何 capture

如果当前线程本身没有向某个TTL实例中设置任何数据,那么其实没有必要捕获该实例内部的数据,因此这里只会在初次调用TTL的set方法时,才会向TTL内部的全局ThreadLocal注册表进行注册:

    // WeakHashMap 的key是被弱引用对象引用着的,并且value值允许为null,因此可以作为set集合使用// 这里就是当做Set集合使用的,因为这里我们只需要知道当前线程使用到的TTL有哪些private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {return new WeakHashMap<>();}@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {return new WeakHashMap<>(parentValue);}};@Overridepublic final void set(T value) {if (!disableIgnoreNullValueSemantics && value == null) {// may set null to remove valueremove();} else {super.set(value);// 向全局ThreadLocal注册表进行注册addThisToHolder();}}private void addThisToHolder() {// 防止重复注册if (!holder.get().containsKey(this)) {// 进行注册,也就是添加到Set集合中holder.get().put((TransmittableThreadLocal<Object>) this, null);}}

holder的职责是负责记录当前线程使用到了哪些TTL,因此相对于TTL来说,Holder本身需要是全局静态的,同时又因为需要记录<线程,List< TTL >> 的映射关系,所以这里就有两种思路:

private static final Map<Thread,Set<TTL>> holder
or
private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder

这两种思路的前者就不多说了,很直接的思路,后者则是将当前线程使用到的TTL保存到了当前线程本地空间中,这样就避免了holder集合多线程情况下争用问题的发生:
在这里插入图片描述
这里比较有趣的一点在于为什么holder保存当前线程使用到的TTL时,需要使用WeakHashMap这样一个虚引用Map呢 ?这一点和ThreadLocalMap中的Entry虚引用实现一致,那么这两者之间是否存在使用场景上的联系呢?

在这里插入图片描述
ThreadLocalMap使用场景下,Table中的key类型为ThreadLocal,val类型为我们通过ThreadLocal设置到当前线程本地空间中的值,如果ThreadLocal对象的引用变量和创建都位于某个方法内部,那么该方法执行完毕后,ThreadLocal理应被回收,如下所示:

    private static void threadLocalGCTest(){ThreadLocal<Integer> tl = new ThreadLocal<>();tl.set(1);}

按理来说,如果tl对象实例占比大小不大,在经过逃逸分析后,会优先进行栈上分配,那么当栈帧被弹出时,该对象理应被直接回收掉,那么这里实际上并不会,因为什么呢?

因为当前线程的table中存在entry的key引用着当前tl对象 :

在这里插入图片描述
但是此时应用程序本身已经失去了对tl对象实例的引用,按照道理来说tl是需要被回收掉的,如果不回收,那就等于发生了内存泄漏,因此这里Entry本身就必须采用弱引用实现,这样才能在GC扫描到当前对象时,将当前tl对象实例进行回收。

对象只存在弱引用,说明对象目前只被弱引用对象实例所引用,软引用和虚引用含义也算如此,这一点弄清楚很重要。

从下图也能看出,但是val并没有被回收掉,严格来说也算是内存泄漏,只有等到当前线程的ThreadLocalMap后面get和set过程中,进行探测式清理和启发式清理时,才会被回收掉 :
在这里插入图片描述
这里还有一点需要注意,如果把上述案例改为如下示例,此时ThreadLocal并没有被当前线程所使用到,因此也就不会主动注册到当前线程内部的ThreadLocalMap中去,也就不存在ThreadLocalMap中的key对当前ThreadLocal实例的引用关系了:

    private static void threadLocalGCTest(){ThreadLocal<Integer> tl = new ThreadLocal<>();}

因此也就无需考虑回收问题了。


那为什么TTL要采用WeakHashMap来保存当前线程使用到的TTL实例呢?

在这里插入图片描述
这里原因其实是一致的,如果TTL对象实例丢失了应用程序的强引用关联,那么必须确保TTL能够被回收掉,具体场景还是如下所示:

在这里插入图片描述
此时TTL就不止当前线程ThreadLocalMap中key的弱引用了,还多了一个全局注册表的引用,所以必须将该全局注册表对象也设置为弱引用实现。


如何保存捕获的数据

第一个问题搞清楚了,下面来看第二个问题: 我们应该使用什么样的数据结构来保存被捕获的数据呢 ?

这个问题我们需要回到TtlRunnable的实现中来,在TtlRunnable的构造函数中执行了第一阶段的捕获任务:

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {// 执行一阶段捕获任务this.capturedRef = new AtomicReference<>(capture());this.runnable = runnable;this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}

capture是Transmitter类的静态构造方法,从类名不难猜测出,TTL使用该类来保存被捕获的数据,下面来看看它的capture方法实现:

        public static Object capture() {final HashMap<Transmittee<Object, Object>, Object> transmittee2Value = newHashMap(transmitteeSet.size());// 1. transmitteeSet 集合是什么呢 ? 为什么这里不直接从Holder集合取出当前线程使用到的所有TTL呢 ?for (Transmittee<Object, Object> transmittee : transmitteeSet) {// 2. transmittee2Value 为什么要保存这样的映射关系 ? transmittee.capture() 该方法捕获了什么数据呢 ?transmittee2Value.put(transmittee, transmittee.capture());...            }// 3. 这里返回的一定就是被捕获的数据了,那具体又是如何保存的呢?return new Snapshot(transmittee2Value);}

在阅读完上面这段代码后,相信大家应该都存在以上三个疑惑,那么下面我们就来一一探索一下吧。

首先,Transmittee类本身负责完成捕获,回放和恢复三件事情,如下图所示:

在这里插入图片描述

在Transmittee类初始化时,会向transmitteeSet集合中注册两个Transmittee对象实例,

        private static final Set<Transmittee<Object, Object>> transmitteeSet = new CopyOnWriteArraySet<>();static {registerTransmittee(ttlTransmittee);registerTransmittee(threadLocalTransmittee);}

ttlTransmittee 负责完成上图的捕获和回放过程,而因为只有TTL具备Transmittable的能力,所以为了让那些普通的ThreadLocal也能享受到Transmittable的能力,就有了threadLocalTransmittee。

关于threadLocalTransmittee这块不是重点,大家可以自行查看其实现,比较容易理解,所以就直接跳过了。

下面我们来看一下ttlTransmittee类的capture方法是如何从Holder中获取到当前线程所有的TTL,然后进行保存的:

        private static final Transmittee<HashMap<TransmittableThreadLocal<Object>, Object>, HashMap<TransmittableThreadLocal<Object>, Object>> ttlTransmittee =new Transmittee<HashMap<TransmittableThreadLocal<Object>, Object>, HashMap<TransmittableThreadLocal<Object>, Object>>() {@NonNull@Overridepublic HashMap<TransmittableThreadLocal<Object>, Object> capture() {// 1. 负责保存capture结果的集合final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = newHashMap(holder.get().size());// 2. 遍历Holder集合中保存的TTL,将其保存到capture集合中// 这里的映射关系为: <TTL,当前快照数据>for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {// 获取当前线程在当前TTL中保存的数据ttl2Value.put(threadLocal, threadLocal.copyValue());}return ttl2Value;}...}                 

capture 捕获得到的结果映射集合如下图所示:
在这里插入图片描述
当依次处理完所有Transmittee后,当前线程本时刻上下文快照数据会被保存到Snapshot对象中,然后返回给TtlRunnable对象保存:

        private static class Snapshot {final HashMap<Transmittee<Object, Object>, Object> transmittee2Value;public Snapshot(HashMap<Transmittee<Object, Object>, Object> transmittee2Value) {this.transmittee2Value = transmittee2Value;}}

在这里插入图片描述


save 和 replay

重放阶段我们需要将已经捕获到的之前线程的上下文快照重放到当前线程上下文中,重放前我们需要保存当前线程的上下文快照,以便执行完当前runnable任务后,进行恢复:

save和replay两阶段在TLL实现中是紧密相连的,因此TTL中把这两个阶段合二为一,统称为了replay,同时capture,replay,restore三个阶段也缩称为CRR。

    @Overridepublic void run() {// 1. 获取已经捕获到的之前线程的上下文快照final Object captured = capturedRef.get();if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {throw new IllegalStateException("TTL value reference is released after run!");}// 2. 将之前线程的上下文快照重放到当前线程上下文中,同时返回当前线程上下文快照final Object backup = replay(captured);try {// 3. 执行目标任务runnable.run();} finally {// 4. 利用backup快照,恢复当前线程之前的上下文环境restore(backup);}}

本节我们重点关注replay方法的实现,该方法分为两个阶段:

  1. 保存当前线程上下文快照
  2. 应用之前线程的上下文快照
        public static Object replay(@NonNull Object captured) {// 1. 获取之前线程的快照数据final Snapshot capturedSnapshot = (Snapshot) captured;// 2. 该集合用于保存当前线程的快照数据final HashMap<Transmittee<Object, Object>, Object> transmittee2Value = newHashMap(capturedSnapshot.transmittee2Value.size());// 3. 遍历capturedSnapshotfor (Map.Entry<Transmittee<Object, Object>, Object> entry : capturedSnapshot.transmittee2Value.entrySet()) {// 4. 获取transmittee和其对应的快照数据Transmittee<Object, Object> transmittee = entry.getKey();Object transmitteeCaptured = entry.getValue();// 5. 调用transmittee的replay方法进行快照重放,同时返回当前线程的快照,然后保存到transmittee2Value中transmittee2Value.put(transmittee, transmittee.replay(transmitteeCaptured));...}// 6. 返回当前线程的上下文快照return new Snapshot(transmittee2Value);}

transmittee类的replay是快照保存和重放逻辑实现的关键点,下面我们一起来看看:

public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {// 1. 保存当前线程快照数据final HashMap<TransmittableThreadLocal<Object>, Object> backup = newHashMap(holder.get().size());// 2. 遍历当前线程Holder集合中每个TTLfor (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {// 3. 依次获取每个TTLTransmittableThreadLocal<Object> threadLocal = iterator.next();// 4. 保存当前线程的上下文快照backup.put(threadLocal, threadLocal.get());// 5. 这里是要用之前线程上下文数据覆盖掉当前线程整个上下文数据,所以这里要分为讨论// 当前线程使用到之前线程没用到的ttl,那么直接清空ttl中的数据// 当前线程使用到了之前线程用到的ttl,那么直接覆盖,覆盖逻辑在循环下面 if (!captured.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 6. 当前线程使用到了之前线程用到的ttl,那么使用captured进行覆盖setTtlValuesTo(captured);// 7. 目前runnable或者callable任务执行前,回调ttl对应的接口doExecuteCallback(true);// 8. 返回当前线程的上下文快照数据return backup;
}

在这里插入图片描述

        private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {// 遍历captured集合中所有ttlfor (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {// 取出ttlTransmittableThreadLocal<Object> threadLocal = entry.getKey();// 把ttl对应的快照值重新设置回ttl中,此时就相当于设置到了当前线程本地空间中threadLocal.set(entry.getValue());}}

整个save 和 replay的过程比较简单,我们下面进入restore环节。


restore

当执行完目标任务后,就需要将当前线程之前的上下文状态进行恢复了,整个过程其实和调用函数类似,由于通用寄存器只存在一套,所以调用过程中就需要把通用寄存器当前状态压入函数栈帧中保存,待函数返回时,再从栈帧中弹出恢复先前运行状态。

这里由于线程本地空间只有一套,所以也需要在任务执行完毕后,恢复原本的上下文环境:

    @Overridepublic void run() {// 1. 获取已经捕获到的之前线程的上下文快照final Object captured = capturedRef.get();if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {throw new IllegalStateException("TTL value reference is released after run!");}// 2. 将之前线程的上下文快照重放到当前线程上下文中,同时返回当前线程上下文快照final Object backup = replay(captured);try {// 3. 执行目标任务runnable.run();} finally {// 4. 利用backup快照,恢复当前线程之前的上下文环境restore(backup);}}

利用backup快照进行恢复的过程其实很简单,下面我们快速来过一遍:

        public static void restore(@NonNull Object backup) {// 1. 遍历backup快照中所有Transmitteefor (Map.Entry<Transmittee<Object, Object>, Object> entry : ((Snapshot) backup).transmittee2Value.entrySet()) {// 2. 获取Transmittee对应的HashMap,里面保存着<TTL,snapShot Data> Transmittee<Object, Object> transmittee = entry.getKey();Object transmitteeBackup = entry.getValue();// 3. 调用Transmittee的restore方法完成恢复过程transmittee.restore(transmitteeBackup);...}}

transmittee类的restore是上下文恢复的关键点,下面我们一起来看看:

                    @Overridepublic void restore(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {// 1. runnable方法调用后,执行TTL对应的回调doExecuteCallback(false);// 2. 遍历当前Holder集合中所有TTLfor (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {// 3. 获取到当前遍历的TTLTransmittableThreadLocal<Object> threadLocal = iterator.next();// 4. 将threadLocal中不存于backup的threadLocal都进行清空if (!backup.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 5. 将backup中的TTL依次进行恢复,该方法上面介绍过,这里不再多说setTtlValuesTo(backup);}

在这里插入图片描述


小结

transmittable-thread-local 本身的设计思路不难理解,本文也只是针对TTL的核心流程源码进行了讲解,如果想进一步学习,可以自行拉取TTL项目源码进行学习。

TTL还提供了一种基于Java Agent的无侵入方案实现,感兴趣的小伙伴可以去 github 项目主页了解一波。

本文只是笔者个人观点,如果不正确的地方欢迎在评论区留言指出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/85100.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

​校园学习《乡村振兴战略下传统村落文化旅游设计》许少辉八一新著

​校园学习《乡村振兴战略下传统村落文化旅游设计》许少辉八一新著

【Stm32】【Lin通信协议】Lin通信点亮灯实验

Lin通信点亮灯实验 通过STM32的串口发送数据&#xff0c;然后通过串口转换模块将数据转换成LIN&#xff08;Local Interconnect Network&#xff09;协议&#xff0c;最终控制点亮灯。需要工程和入门资料的可以私信我&#xff0c;看到了马上回。 入门书本推荐&#xff1a; 一…

【C语言】数组和指针刷题练习

指针和数组我们已经学习的差不多了&#xff0c;今天就为大家分享一些指针和数组的常见练习题&#xff0c;还包含许多经典面试题哦&#xff01; 一、求数组长度和大小 普通一维数组 int main() {//一维数组int a[] { 1,2,3,4 };printf("%d\n", sizeof(a));//整个数组…

Postman应用——控制台调试

当你在测试脚本中遇到错误或意外行为时&#xff0c;Postman控制台可以帮助你识别&#xff0c;通过将console.log调试语句与你的测试断言相结合&#xff0c;你可以检查http请求和响应的内容&#xff0c;以及变量之类的。 通常可以使用控制台日志来标记代码执行&#xff0c;有时…

网络安全日报 2023年09月21日

1、研究人员披露基于ERMAC木马的Hook家族银行木马 https://research.nccgroup.com/2023/09/11/from-ermac-to-hook-investigating-the-technical-differences-between-two-android-malware-variants/ 研究人员发现 ERMAC 源代码被用作 Hook 的基础。恶意软件操作者可以发送到…

Visual Studio将C#项目编译成EXE可执行程序

经常看文章时会收获不少实用工具&#xff0c;有的在github上是编译好的&#xff0c;有的则是未编译的项目文件。所以经常会使用Visual Studio编译项目文件成exe可执行程序&#xff0c;以下为编译的流程。 第一步&#xff0c;从github上下载项目文件&#xff0c;举个例子&#…

小米手机安装面具教程(Xiaomi手机获取root权限)

文章目录 1.Magisk中文网&#xff1a;2.某呼&#xff1a;3.最后一步打开cmd命令行输入的时候:4.Flash Boot 通包-Magisk&#xff08;Flash Boot通刷包&#xff09;5.小米Rom下载&#xff08;官方刷机包&#xff09;6.Magisk最新版本国内源下载 1.Magisk中文网&#xff1a; htt…

【深度学习实验】前馈神经网络(七):批量加载数据(直接加载数据→定义类封装数据)

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 直接加载鸢尾花数据集 a. 加载数据集 b. 数据归一化 c. 洗牌操作 d. 打印数据 2. 定义类封装数据 a. __init__(构造函数&#xff1a;用于初始化数据集对象) b.…

华为OD机试 - 构成正方形的数量 - 数据结构map(Java 2023 B卷 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、Java算法源码五、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 …

mysql 半同步复制模式使用详解

目录 一、前言 二、mysql主从架构简介 2.1 mysql主从复制架构概述 2.2 为什么使用主从架构 2.2.1 提高数据可用性 2.2.2 提高数据可靠性 2.2.3 提升数据读写性能 2.3 主从架构原理 2.4 主从架构扩展 2.4.1 双机热备&#xff08;AB复制&#xff09; 2.4.2 级联复制 2…

Qt核心:元对象系统、属性系统、对象树、信号槽

一、元对象系统 1、Qt 的元对象系统提供的功能有&#xff1a;对象间通信的信号和槽机制、运行时类型信息和动态属性系统等。 2、元对象系统是 Qt 对原有的 C进行的一些扩展&#xff0c;主要是为实现信号和槽机制而引入的&#xff0c; 信号和槽机制是 Qt 的核心特征。 3、要使…

当网络设置为自动获取dns时而实际nds是8.8.8.8,1.1.1.1的解决方法

笔记本换网络环境后&#xff0c;网络设置的是自动获取IP和自动获取dns。但使用命令&#xff1a;config/all命令时发现dns总是8.8.8.8,1.1.1.1。导致csdn上不了。 8.8.8.8,1.1.1.1&#xff1a;是谷歌的dns。 解决办法&#xff1a; 在支行中输入regedit打开注册表后&#xff0…

windows下载虚拟机virtualBox

链接&#xff1a;Downloads – Oracle VM VirtualBox 进入链接这样点击&#xff1a; 直接下载即可

Java“牵手”速卖通商品列表页数据采集+速卖通商品价格数据排序,速卖通API接口申请指南

速卖通是阿里巴巴旗下的面向国际市场打造的跨境电商平台&#xff0c;被称为国际版淘宝&#xff0c;速卖通面向海外买家客户&#xff0c;通过支付宝国际账户进行担保交易&#xff0c;并使用国际物流渠道运输发货&#xff0c;是全球第三大英文在线购物网站。 速卖通商品列表数据…

关于IDEA没有显示日志输出?IDEA控制台没有显示Tomcat Localhost Log和Catalina Log 怎么办?

问题描述&#xff1a; 原因是;CATALINA_BASE里面没有相关的文件配置。而之前学习IDEA的时候&#xff0c;把这个文件的位置改变了。导致&#xff0c;最后输出IDEA的时候&#xff0c;不会把日志也打印出来。 检查IDEA配置; D:\work_soft\tomcat_user\Tomcat10.0\bin 在此目录下&…

如何在没有第三方.NET库源码的情况,调试第三库代码?

大家好&#xff0c;我是沙漠尽头的狼。 本方首发于Dotnet9&#xff0c;介绍使用dnSpy调试第三方.NET库源码&#xff0c;行文目录&#xff1a; 安装dnSpy编写示例程序调试示例程序调试.NET库原生方法总结 1. 安装dnSpy dnSpy是一款功能强大的.NET程序反编译工具&#xff0c;…

STM32 Cubemx 通用定时器 General-Purpose Timers同步

文章目录 前言简介cubemx配置 前言 持续学习stm32中… 简介 通用定时器是一个16位的计数器&#xff0c;支持向上up、向下down与中心对称up-down三种模式。可以用于测量信号脉宽&#xff08;输入捕捉&#xff09;&#xff0c;输出一定的波形&#xff08;比较输出与PWM输出&am…

activemq部署

目录 1.下载 2.java环境 3.解压启动 4.访问测试 5.问题记录 5.1.无法启动成功问题 5.2.其他服务器无法访问 1.下载 ActiveMQ 2.java环境 需要注意要求的jdk版本&#xff0c;否则启动不会成功 3.解压启动 tar -zxvf apache-activemq-5.18.2-bin.tar.gz 进入到目录下执行…

使用递归思想遍历二叉树

二叉树的遍历主要有两种方式&#xff1a;深度优先遍历和广度优先遍历 这篇主要讲使用深度优先遍历来遍历二叉树 深度优先遍历有以下三种 前、中、后序遍历&#xff0c;这三种遍历方式的主要区别是中间节点的位置所在的顺序 前序遍历&#xff1a;中间节点在叶子节点前面 中序遍历…

Flink--4、DateStream API(执行环境、源算子、基本转换算子)

星光下的赶路人star的个人主页 注意力的集中&#xff0c;意象的孤立绝缘&#xff0c;便是美感的态度的最大特点 文章目录 1、DataStream API1.1 执行环境&#xff08;Execution Environment&#xff09;1.1.1 创建执行环境 1.2 执行模式&#xff08;Execution Mode&#xff09;…