Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用问题:TransmittableThreadLocal.remove并不清空值 #261

Closed
guoxunbo opened this issue Apr 14, 2021 · 14 comments
Closed

使用问题:TransmittableThreadLocal.remove并不清空值 #261

guoxunbo opened this issue Apr 14, 2021 · 14 comments
Assignees
Labels
❓question Further information is requested

Comments

@guoxunbo
Copy link

我在Spring的filter中对相应的请求信息放到了TransmittableThreadLocal方便上下线程传递,在intercepter那边也做了remove.可是当下个Http请求进来,TransmittableThreadLocal还存了上次的值。。。

package com.newbiest.base.threadlocal;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Maps;
import com.newbiest.base.constant.EnvConstant;
import com.newbiest.base.msg.DefaultParser;
import com.newbiest.base.msg.DefaultRequest;
import com.newbiest.base.msg.Request;
import com.newbiest.base.msg.RequestHeader;
import com.newbiest.base.utils.DateUtils;
import com.newbiest.base.utils.MDCUtils;
import com.newbiest.base.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;


/**
 * 任意一次请求进来的时候都会产生一个相应的ThreadLocal
 * 主要用来解析token并生成sessionContext
 */
@Slf4j
public class ThreadLocalContext implements Serializable {

    private static TransmittableThreadLocal<ConcurrentMap> THREAD_LOCAL = new TransmittableThreadLocal<ConcurrentMap>(){
        @Override
        protected ConcurrentMap initialValue() {
            return Maps.newConcurrentMap();
        }
    };

    public static final AtomicLong TRANSACTION_INIT_SEQ = new AtomicLong(1L);

    public static final String STOP_WATCH = "Stopwatch";
    public static final String TRANSACTION_IP = "TransactionIp";
    public static final String TRANSACTION_URI = "TransactionUri";
    public static final String TRANSACTION_REQUEST = "TransactionRequest";
    public static final String TRANSACTION_ID = "TransactionId";
    public static final String TRANSACTION_TIME = "TransactionTime";
    public static final String TRANSACTION_ORG_RRN = "TransactionOrgRrn";
    public static final String TRANSACTION_USERNAME = "TransactionUserName";
    public static final String TRANSACTION_LANGUAGE = "TransactionLanguage";
    public static final String TRANSACTION_SEQ = "TransactionSeq";

    public static final String TRANSACTION_MULTIPART_FILE_MAP = "TransactionMultipartFileMap";

    /**
     * 一个线程进入带有@BaseJpaFilter的深度
     */
    public static final String JPA_FILTER_STACK_COUNT = "JpaFilterStackCount";

    public static void enterBaseJpaAdvice() {
        AtomicInteger jpaFilterStackCount = getJpaFilterStackCount();
        jpaFilterStackCount.incrementAndGet();
        THREAD_LOCAL.get().put(JPA_FILTER_STACK_COUNT, jpaFilterStackCount);
    }

    public static int leaveBaseJpaAdvice() {
        AtomicInteger jpaFilterStackCount = getJpaFilterStackCount();
        int currentStackCount = jpaFilterStackCount.decrementAndGet();
        THREAD_LOCAL.get().put(JPA_FILTER_STACK_COUNT, jpaFilterStackCount);

        return currentStackCount;
    }

    public static AtomicInteger getJpaFilterStackCount() {
        if (THREAD_LOCAL.get() != null && THREAD_LOCAL.get().containsKey(JPA_FILTER_STACK_COUNT)) {
            return (AtomicInteger) THREAD_LOCAL.get().get(JPA_FILTER_STACK_COUNT);
        }
        return new AtomicInteger(0);
    }

    public static void putMultipartFileMap(Map<String, MultipartFile> multipartFileMap) {
        THREAD_LOCAL.get().put(TRANSACTION_MULTIPART_FILE_MAP, multipartFileMap);
    }

    public static Map<String, MultipartFile> getMultipartFileMap() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_MULTIPART_FILE_MAP)) {
            return (Map<String, MultipartFile>) THREAD_LOCAL.get().get(TRANSACTION_MULTIPART_FILE_MAP);
        }
        return null;
    }

    public static String getRequest() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_REQUEST)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_REQUEST);
        }
        return null;
    }

    public static void putRequest(String request, String ipAddress, String uri) {
        Map map = THREAD_LOCAL.get();
        THREAD_LOCAL.get().put(TRANSACTION_REQUEST, request);
        THREAD_LOCAL.get().put(TRANSACTION_IP, ipAddress);
        THREAD_LOCAL.get().put(TRANSACTION_TIME, DateUtils.now());
        THREAD_LOCAL.get().put(TRANSACTION_URI, uri);
        String transactionId = "";
        try {
            ObjectMapper objectMapper = DefaultParser.getObjectMapper();
            Request requestModel = objectMapper.readValue(request, DefaultRequest.class);
            RequestHeader requestHeader = requestModel.getHeader();
            transactionId = requestHeader.getTransactionId();

            putTransactionId(transactionId);
            putOrgRrn(requestHeader.getOrgRrn());
            putUsername(requestHeader.getUsername() == null ? StringUtils.EMPTY : requestHeader.getUsername());
            THREAD_LOCAL.get().put(TRANSACTION_LANGUAGE, StringUtils.isNullOrEmpty(requestHeader.getLanguage()) ? EnvConstant.LANGUAGE_CHINESE : requestHeader.getLanguage());
        } catch (Exception e) {
            log.warn(String.format("Transfer data [%s] to RequestModel fail. so put uuid as transaction id", request));
            transactionId = UUID.randomUUID().toString();
            putTransactionId(transactionId);
        }
        MDCUtils.putTransactionId(transactionId);
        THREAD_LOCAL.get().put(TRANSACTION_SEQ, TRANSACTION_INIT_SEQ);
    }

    public static Stopwatch getStopWatch() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(STOP_WATCH)) {
            return (Stopwatch) THREAD_LOCAL.get().get(STOP_WATCH);
        }
        return null;
    }

    public static Date getTransactionTime() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_TIME)) {
            return (Date) THREAD_LOCAL.get().get(TRANSACTION_TIME);
        }
        return null;
    }

    public static void putOrgRrn(String orgRrn) {
        THREAD_LOCAL.get().put(TRANSACTION_ORG_RRN, orgRrn);
    }

    public static void putUsername(String username) {
        THREAD_LOCAL.get().put(TRANSACTION_USERNAME, username);
    }

    public static void putTransactionId(String transactionId) {
        THREAD_LOCAL.get().put(TRANSACTION_ID, transactionId);
    }

    public static void putStopWatch(Stopwatch stopwatch) {
        THREAD_LOCAL.get().put(STOP_WATCH, stopwatch);
    }

    public static String getUsername() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_USERNAME)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_USERNAME);
        }
        return null;
    }

    public static String getOrgRrn() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_ORG_RRN)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_ORG_RRN);
        }
        return null;
    }

    public static String getLanguage() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_LANGUAGE)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_LANGUAGE);
        }
        return EnvConstant.LANGUAGE_CHINESE;
    }

    public static String getTransactionId() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_ID)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_ID);
        }
        return null;
    }

    public static String getTransactionIp() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_IP)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_IP);
        }
        return null;
    }

    public static String getTransactionUri() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_URI)) {
            return (String) THREAD_LOCAL.get().get(TRANSACTION_URI);
        }
        return null;
    }

    public static Long getTransactionSeq() {
        if (THREAD_LOCAL != null && THREAD_LOCAL.get().containsKey(TRANSACTION_SEQ)) {
            AtomicLong transactionSeq = (AtomicLong) THREAD_LOCAL.get().get(TRANSACTION_SEQ);
            return transactionSeq.getAndIncrement();
        }
        return null;
    }

    public static void remove() {
        String transactionId = getTransactionId();
        THREAD_LOCAL.remove();
        Map map = THREAD_LOCAL.get();
        if (log.isInfoEnabled()) {
            log.info(String.format("Thread [%s] with transactionId [%s] clean the threadLocal", Thread.currentThread().getName(), transactionId));
        }
    }

}
@guoxunbo
Copy link
Author

image
image
很奇怪的是,这个remove之后我去看这个map是个size为0的map这是正常的。可是为啥下次进来的时候,这个map又有了上次的值。

@guoxunbo guoxunbo changed the title TransmittableThreadLocal.remove并不请空值 TransmittableThreadLocal.remove并不清空值 Apr 14, 2021
@guoxunbo
Copy link
Author

如下图。线程1已经清空了数据,但是线程3进来还是获取到了线程1的数据。但是实际线程1里的数据是清空掉的了。
image
image

@oldratlee
Copy link
Member

oldratlee commented Apr 14, 2021

先仅从你给的代码逻辑来分析
( PS: 因为你给的ThreadLocalContext类几乎所有的方法都是public的,所以应用其它的地方可以不受限制地写入KEY TRANSACTION_ID的值,这些地方分析不到。)

ThreadLocalContext#TRANSACTION_ID的使用地方/调用链 如下:

ThreadLocalContext.putTransactionId(String)  // *写入*`TRANSACTION_ID`
    ThreadLocalContext.putRequest(String, String, String)(2 usages) // 更上层业务的 *写入* `TRANSACTION_ID`
ThreadLocalContext.getTransactionId() // *读取,没有关系*
    ThreadLocalContext.remove()

其中,ThreadLocalContext.putRequest(String, String, String)的逻辑:

String transactionId = "";
try {
    ObjectMapper objectMapper = DefaultParser.getObjectMapper();
    Request requestModel = objectMapper.readValue(request, DefaultRequest.class);
    RequestHeader requestHeader = requestModel.getHeader();
    transactionId = requestHeader.getTransactionId();

    // !! 从HEADER提取了值,写成 TRANSACTION_ID !!
    // !! 这里可能 2个线程的请求 写成一样的 TRANSACTION_ID !!
    putTransactionId(transactionId);
   // ... 省略代码行 ...
} catch (Exception e) {
    log.warn(String.format("Transfer data [%s] to RequestModel fail. so put uuid as transaction id", request));
    transactionId = UUID.randomUUID().toString();
    putTransactionId(transactionId);
}

@guoxunbo 见上面的// !!的注释说明。

当然上面注释只是说明,不足确定是TTL的问题,
总的来说,你提供的信息不足以排查确定问题。


如果要排查确定问题,需要分离整理 一个 可以复现问题的极简Demo工程:

否则涉及大量你的业务代码与流程,不能分析完整,完成排查。

@oldratlee oldratlee added the ❓question Further information is requested label Apr 14, 2021
@guoxunbo
Copy link
Author

我写了个简单的web测试了下。TTL是好的。
但是我这边也只有在putRequest这边方法里会去调用putTransactionId 这个方法。也没有其他地方放进去。
我换成ThreadLocal就是正常的了。但是我没有用TTL只用自带的InheritableThreadLocal也有这个问题。就有点奇怪了。日志如下:我打了一下。header里传来的transactionId都是不一样的。
@oldratlee remove的操作我是在InterceptorafterCompletion方法里做了ThreadLocalContext的remove()。

http-nio-8080-exec-1-transactionId :null
http-nio-8080-exec-3-transactionId :null
HeaderTransId:130f3429-6f14-45d2-8ef9-009b1208a8fe
HeaderTransId:eddf91af-bf90-4ebe-aa7a-f842cc453fd7
2021-04-15-00:02:17.144 default [http-nio-8080-exec-1] 130f3429-6f14-45d2-8ef9-009b1208a8fe INFO |c.n.b.threadlocal.ThreadLocalContext - Thread [http-nio-8080-exec-1] with transactionId [eddf91af-bf90-4ebe-aa7a-f842cc453fd7] clean the threadLocal
2021-04-15-00:02:17.209 default [http-nio-8080-exec-3] eddf91af-bf90-4ebe-aa7a-f842cc453fd7 INFO |c.n.b.threadlocal.ThreadLocalContext - Thread [http-nio-8080-exec-3] with transactionId [eddf91af-bf90-4ebe-aa7a-f842cc453fd7] clean the threadLocal
http-nio-8080-exec-6-transactionId :eddf91af-bf90-4ebe-aa7a-f842cc453fd7
http-nio-8080-exec-7-transactionId :eddf91af-bf90-4ebe-aa7a-f842cc453fd7
HeaderTransId:d9ae56c4-f44f-4b93-94bc-f3a89c3647ea
HeaderTransId:039295fb-a9ea-476d-afef-ae39dceacb5a
2021-04-15-00:02:38.549 default [http-nio-8080-exec-6] d9ae56c4-f44f-4b93-94bc-f3a89c3647ea INFO |c.n.b.threadlocal.ThreadLocalContext - Thread [http-nio-8080-exec-6] with transactionId [039295fb-a9ea-476d-afef-ae39dceacb5a] clean the threadLocal
2021-04-15-00:02:38.565 default [http-nio-8080-exec-7] 039295fb-a9ea-476d-afef-ae39dceacb5a INFO |c.n.b.threadlocal.ThreadLocalContext - Thread [http-nio-8080-exec-7] with transactionId [039295fb-a9ea-476d-afef-ae39dceacb5a] clean the threadLocal

@guoxunbo
Copy link
Author

guoxunbo commented Apr 15, 2021

找到问题了。。。

我一个注解里用了这个map里 别的栏位也会导致这个不被清除。

@guoxunbo
Copy link
Author

就是那个enterJpaAdvice方法。有点奇怪。我去琢磨一下

@guoxunbo
Copy link
Author

还有一个问题问下良神。MDC进行传递的时候只能通过线程池进行修饰传递么。。有没其他的方法

@guoxunbo
Copy link
Author

经过我的测试。发现只要和Hibernate的filter挂钩起来。就会导致相应的问题。
image

@oldratlee
Copy link
Member

oldratlee commented Apr 15, 2021

MDC进行传递的时候只能通过线程池进行修饰传递么。。有没其他的方法

你说的『通过线程池进行修饰』是指 2.2 修饰线程池 说的 TtlExecutors. getTtlExecutorService这样的使用方式吗?


TTL还提供了Java Agent的方式,可以做到应用代码 无侵入。 @guoxunbo

可以看一下文档2.3 使用Java Agent来修饰JDK线程池实现类 ,使用有问题欢迎交流。

@guoxunbo
Copy link
Author

guoxunbo commented Apr 15, 2021

测出问题来了。。。
@PostConstruct结合在一起就有问题。只要在项目中有任何一个类在@PostConstruct中去获取InheritableThreadLocal 中的值就有问题。

代码我上传到百度云了。
链接:https://pan.baidu.com/s/1GVA8qf5YOdEdrRfVGDDB3w 密码:lxey
用Postman请求http://127.0.0.1:8080/ttl/test。随便发个string就看的出来。有点奇怪。。

image

@guoxunbo
Copy link
Author

@oldratlee 良神。

@oldratlee
Copy link
Member

oldratlee commented Apr 15, 2021

测出问题来了。。。
@PostConstruct结合在一起就有问题。只要在项目中有任何一个类在@PostConstruct中去获取InheritableThreadLocal 中的值就有问题。

代码我上传到百度云了。
链接:https://pan.baidu.com/s/1GVA8qf5YOdEdrRfVGDDB3w 密码:lxey
用Postman请求http://127.0.0.1:8080/ttl/test。随便发个string就看的出来。有点奇怪。。

...

👍
我观摩/分析一下

PS: 推荐以GitHub repo的方式 分享 代码工程,
这样大家操作更方便,也能比如通过提交日志了解代码变化过程 :")

@guoxunbo
Copy link
Author

guoxunbo commented Apr 16, 2021

@oldratlee 好的。下次一定。。。哈哈哈。一起分析一下。好奇怪的。

为啥@PostConstruct会影响到这里。

@guoxunbo
Copy link
Author

guoxunbo commented Apr 16, 2021

好像是我使用不严瑾。。。

@PostConstruct里没有做ThreadLocal的remove.

但是就很奇怪。我这里都只是get值。
做不做remove应该问题不大吧。

我再去看下InheritableThreadLocal的holder相关代码瞅下。

@oldratlee oldratlee self-assigned this May 23, 2021
@oldratlee oldratlee changed the title TransmittableThreadLocal.remove并不清空值 使用问题:TransmittableThreadLocal.remove并不清空值 May 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
❓question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants