问题描述:
http://cloud.anker-in.com/service/interface/message
{“interfaceCode”:“CSC2EPAAS_REVIEW_BL_QUERY”,“sourceSystem”:“CSC”,“messageBody”:{“email”:“[email protected]”}}
报错提示:接口访问报找不到数据源
原因描述:生产调用到了服务化sit环境的服务,所以用当前租户和环境是肯定找不到地址的
重要问题:为什么生产会调用到服务化sit环境的服务?(tpcloud租户下的脚本调用的是sit环境,怀疑是不是串了)
调用链:
(默认租户)域名,网关,interface, mvel,order。
(anker租户)域名,网关,interface, mvel,order。
http://10.3.2.222:8290/service/interface/message->调用正常,证明默认租户里面的缓存是正常的。
http://cloud.anker-in.com/service/interface/message->anker租户下的缓存不正常,所以当我去页面保存脚本时候,整个调用链就正常了
重要问题:为什么安克租户下的缓存不对?
重要问题: 现在的调用好像都会去调用tpcloud的缓存?怎么证明
重要问题:为什么刷新安克租户下的缓存就成功了?难道没有缓存就会去调用tpcloud吗?
重要问题:如果真的是通过tpcloud来调用的order服务,那么脚本应该是写死的ip地址,那么应该是不会带有租户信息的
通过几个请求的返回结果和tpcloud的脚本的返回结果 对比后不难发现是租户之间的缓存信息串了,所以以上问题都能得到解释
如果是混乱,那么需要看脚本缓存加载的方法:ScriptEngine.java
@PostConstruct
private void initScript() {
for(String tenant : configCache.getCONFIG_CACHE().keySet()){
new Thread(new InitThread(tenant)).start();
}
if(DataSourceTenantContextHolder.DEFAULT_TENANT.equals(configCache.getDefaultTenant())){
new Thread(new InitThread(DataSourceTenantContextHolder.DEFAULT_TENANT)).start();
}
}
private class InitThread implements Runnable {
private String tenant;
public InitThread(String tenant) {
this.tenant = tenant;
}
@Override
public void run() {
initTenantScript(tenant);
}
}
private void initTenantScript(String tenant){
DataSourceTenantContextHolder.setCurrentTanent(tenant);
List
private String scriptKey(Long id){
String tenant = DataSourceTenantContextHolder.getCurrentTenant();
if(DataSourceTenantContextHolder.DEFAULT_TENANT.equals(tenant)){
tenant = configCache.getDefaultTenant();
}
return tenant+id;
}
抽离出来以后的问题代码:
public static void main(String[] args) {
new Thread(new InitThread2(“tenant1”)).start();
new Thread(new InitThread2(“tenant2”)).start();
}
private static class InitThread2 implements Runnable {
private String tenant;
public InitThread2(String tenant) {
this.tenant = tenant;
}
@Override
public void run() {
DataSourceTenantContextHolder.setCurrentTanent(tenant);
List items = new ArrayList<>();;
for(int i = 0 ; i<1000; i++){
items.add(tenant);
}
logger.info(tenant+":"+Thread.currentThread().getName());//Thread-0:tenant1当前租户标志tenant1
//Thread-1:tenant2当前租户标志tenant2
items.parallelStream().forEach(item -> {
logger.info(Thread.currentThread().getName()+":"+DataSourceTenantContextHolder.getCurrentTenant()+"当前租户标志"+tenant);
});
}
}
解决的办法:一个是将parallelStream改为普通的Stream,另外一个是并行处理中设置租户信息
改进后代码:
public static void main(String[] args) {
new Thread(new InitThread2(“tenant1”)).start();
new Thread(new InitThread2(“tenant2”)).start();
}
private static class InitThread2 implements Runnable {
private String tenant;
public InitThread2(String tenant) {
this.tenant = tenant;
}
@Override
public void run() {
DataSourceTenantContextHolder.setCurrentTanent(tenant);
List items = new ArrayList<>();;
for(int i = 0 ; i<1000; i++){
items.add(tenant);
}
logger.info(tenant+":"+Thread.currentThread().getName());//Thread-0:tenant1当前租户标志tenant1
//Thread-1:tenant2当前租户标志tenant2
items.parallelStream().forEach(item -> {
DataSourceTenantContextHolder.setCurrentTanent(tenant);
logger.info(Thread.currentThread().getName()+":"+DataSourceTenantContextHolder.getCurrentTenant()+"当前租户标志"+tenant);
});
}
}
原因分析:
1.总数还是2000个,work数默认为cpu-1,然后让调用线程也参与其工作
2.thread-0,thread-1是隔离的,其对应关系是不会存在问题的
3.InheritableThreadLocal保存着租户信息,而底层是用thread为key,租户为value的map存着,保证着线程安全
4.那么work在获取InheritableThreadLocal里面的租户信息的时候到底会调用哪个父进程的租户信息呢。
第一次thread-0执行的时候forkjoin-work1,这里面执行获取的租户信息是明确的,是取的thread-0父线程的租户信息
第二次thread-1执行的时候,将一个任务丢到了forkjoin-work1里面,那么forkjoin-work1获取租户信息的时候取的是thread-1父线程的租户信息
因为这7个线程是公共的,而线程里面的任务是两个不同Thread派发过来的,里面的实现是要获取当前线程的租户信息,那么这样就会导致后面派发过来的任务里面的租户信息覆盖这个work线程里面的租户信息,从而导致任务执行时取到的租户信息发生错误。
修改分析:如果每个子任务在执行的时候都直接指定租户,可以避免取值问题.
如果不采用parallelStream,那么每个线程之间都是完全隔离的,所以也可以解决问题.
#如果使用TransmittableThreadLocal传递的话,可以将线程和work绑定.
相关知识点
parallelStream:https://www.jianshu.com/p/3d4e76467990
InheritableThreadLocal:https://www.jianshu.com/p/94ba4a918ff5
TransmittableThreadLocal:https://github.com/alibaba/transmittable-thread-local