alibaba/transmittable-thread-local

这个库带来怎样的好处和优势?

wilddylan opened this issue · 4 comments

这个库带来怎样的好处和优势?

这个库能带来怎样的好处和优势

@wilddylan 好问题 ❤️ 文档中没正面说明,需要直接说明出来。 :)


回答一个方案『好处』和『优势』时,要说明的是『 解决一个问题(场景)另一其它方案对比』。
即 涉及3部分: 1) 解决的问题/场景,2) 其它解决方案, 3) 对比。

1. 关于 解决的问题/场景

简单地说:异步执行上下文的传递;
期望做到:透明/自动完成所有异步执行上下文的可定制、规范化的捕捉/传递。

更多可以看文档中的说明:功能需求场景

2. 其它解决方案

解决『异步执行上下文的传递』,朴素直接的实现方案 是 下面的示例代码。
注:示例中的异步执行 是 以 new Thread方式,实际代码的异步执行可能是ThreadPoolRxJava/ReactorKotlin协程 等等不同的方式。

import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        // 1. *业务逻辑*需要做的实现逻辑:获取当前上下文
        final Map<String, String> context = getContext();

        // 2. *业务逻辑*需要做的实现逻辑:捕捉上下文
        Map<String, String> contextCopy = new HashMap<>(context);
        Runnable asyncLogic = () -> {
            // 在异步执行逻辑中,
            // 3. 传递,通过 Lambda的外部变量 contextCopy
            // 4. 使用上下文 
            System.out.println(contextCopy);
        };
        new Thread(asyncLogic).start();
    }

    private static Map<String, String> getContext() {
        return contextHolder.get();
    }

    // 使用 ThreadLocal:
    //   - 可以 不同的线程(如执行不同的请求用户的处理) 有自己的Context。
    //   - 避免 Context 总是通过 函数参数 传递,中间路过的业务的逻辑 都关注/感知 框架的上下文
    private static final ThreadLocal<Map<String, String>> contextHolder = ThreadLocal.withInitial(HashMap::new);
}

这样做法的问题

从 业务使用 角度来看

  1. 繁琐
    业务逻辑自己 要知道 有哪些上下文、分别要如何获取,并一个一个去捕捉/传递。
    上面示例代码 只示意了 一个上下文。
  2. 依赖
    直接依赖 不同 上下文获取的逻辑/类(比如RPC的上下文、全链路跟踪的上下文、不同业务模块中的业务上下文,等等)。
  3. 静态(易漏)
    • 要事先知道 哪些上下文;如果有 新异步执行的上下文 出现了,业务逻辑要修改:添加新上下文传递的几行代码。
      即 因系统的上下文新增,业务的逻辑就跟进要修改。
    • 对于业务来说,不关心系统的上下文,即往往就可能遗漏,会是线上故障了。
    • 比如对阿里这样的系统,功能/组件多涉及的上下文多、逻辑流程长,
      上下文问题 实际上 是个 大的易错的架构问题。需要统一的解决方案。
  4. 定制性
    • 业务要关注『上下文的传递方式』。直接传引用?还是拷贝传值?拷贝是深拷贝还是浅拷贝?
      上面的示例代码实际上是拷贝了一层HashMap,即浅拷贝,实际业务中往往不会这么简单。
    • 『上下文的传递方式』这点 往往是 上下文的提供者(或说是 业务逻辑的框架部分)才能 决策处理好的,而 上下文的使用者(或说是 业务逻辑的应用部分)往往不(期望)知道上下文的传递方式。
    • 这也可以理解成是 依赖,即业务逻辑 依赖/关注/实现了 系统/架构的『上下文的传递方式』。

从 整体流程实现 角度来看

上下文传递流程的规范化

  • 上下文传递到了子线程 要做好 清理(或更准确地说是 要恢复成之前的上下文),需要业务逻辑去处理好。
    上面示例代码 实际上 没有解决 这方面的问题。
  • 如果 业务逻辑 对 清理 的 处理不正确:
    1. 如果 清理操作漏了:
      • 下一次执行可能是上次的,即『上下文的 污染/串号』,会导致业务逻辑错误。
      • 『上下文的 泄漏』,会导致内存泄漏问题。
    2. 如果 清理操作做多了,会出现上下文 丢失
  • 上面的问题,在业务开发中引发的Bug真是 屡见不鲜
    怎么设计一个『上下文传递流程』方案(即上下文的生命周期),保证 没有上面的问题?
  • 期望:上下文生命周期的操作 从业务逻辑中 分离出来;
    业务逻辑 不涉及 生命周期,就不会有 由清理引发这些问题了。
  • 整个流程/上下文生命周期 可以规范化成:捕捉、回放和恢复 3个操作,即CRR(capture/replay/restore)

3. 对比

好处和优势 对应的就是 上面的问题解决了,即做到:

透明/自动完成所有异步执行上下文的可定制、规范化的捕捉/传递。

基于示例代码说明如下:

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo {
    public static void main(String[] args) {
        // # 从 业务使用 角度来看 #
        //
        // 1. 关于『繁琐』:
        //    无需关注上下文的传递(由库接管完成)。
        //    自然无需 业务逻辑自己 知道 有哪些上下文、分别要如何获取,并一个一个去捕捉/传递。
        // 2. 关于『依赖』:
        //    业务逻辑直接使用 TTL,不会依赖 不同 上下文获取的逻辑/类。
        // 3. 关于『静态(易漏)』
        //    由库接管完成 *所有* 上下文的传递,非静态 自动感知新加的上下文。
        // 4. 关于『定制性』:
        //    提供了`copy`方法,
        //    由框架本身(即上下文的提供者)定制好了『上下文的传递方式』,业务逻辑无需感知。
        //
        // # 从 整体流程实现 角度来看 #
        //
        // 1. 上下文传递流程的规范化
        //    上下文生命周期的操作 从业务逻辑中 分离出来,
        //    由库接管完成,业务逻辑无需感知。不会有生命周期相关 *屡见不鲜*的 Bug!
        Runnable asyncLogic = () -> {
            // 传递,在异步执行逻辑中,使用上下文
            System.out.println(contextHolder1.get());
            System.out.println(contextHolder2.get());
        };

        executorService.submit(asyncLogic);
    }

    private static final TransmittableThreadLocal<String> contextHolder1 = new TransmittableThreadLocal<String>();

    private static final TransmittableThreadLocal<Node> contextHolder2 = new TransmittableThreadLocal<Node>() {
        @Override
        protected Node copy(Node parentValue) {
            return new Node(parentValue.id); // 定制传递方式
        }

        @Override
        protected Node initialValue() {
            return new Node("1");
        }
    };

    private static class Node {
        final String id;

        public Node(String id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Node{id='" + id + '\'' + '}'; 
        }
    }

    private static final ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newCachedThreadPool());
}

@wilddylan 你看看,我说的明白不? 不清楚,继续交流。

另外,推荐看一下 issue

很棒唉 ~ 嘿嘿,为你点赞,快给我一个你加密的工号,我去给你点赞!

很棒唉 ~ 嘿嘿,为你点赞,快给我一个你加密的工号,我去给你点赞!

噗~

直接用我的id oldratlee 在内网就查到我了,
名字 在我的github主页上: https://github.com/oldratlee

PS: 这个Issue不用关了,好让大家可以方便地看到 ❤️ @wilddylan

Closed.
上面的讨论内容已并入项目文档主页: ✨ 使用TTL的好处与必要性

欢迎 大家在这个 issue 继续讨论 ❤️