Java 多线程上下文传递在复杂场景下的实践

一、引言

海外商城从印度做起,慢慢的会有一些其他国家的诉求,这个时候需要我们针对当前的商城做一个改造,可以支撑多个国家的商城,这里会涉及多个问题,多语言,多国家,多时区,本地化等等。在多国家的情况下如何把识别出来的国家信息传递下去,一层一层直到代码执行的最后一步。甚至还有一些多线程的场景需要处理。

二、背景技术

2.1 ThreadLocal

ThreadLocal是最容易想到了,入口识别到国家信息后,丢进ThreadLocal,这样后续代码、redis、DB等做国家区分的时候都能使用到。

这里先简单介绍一下ThreadLocal:

1/** 2 * Sets the current thread's copy of this thread-local variable 3 * to the specified value. Most subclasses will have no need to 4 * override this method, relying solely on the {@link #initialValue} 5 * method to set the values of thread-locals. 6 * 7 * @param value the value to be stored in the current thread's copy of 8 * this thread-local. 9 */ 10public void set(T value) { 11 Thread t = Thread.currentThread(); 12 ThreadLocalMap map = getMap(t); 13 if (map != null) 14 map.set(this, value); 15 else 16 createMap(t, value); 17} 18 19 20/** 21 * Returns the value in the current thread's copy of this 22 * thread-local variable. If the variable has no value for the 23 * current thread, it is first initialized to the value returned 24 * by an invocation of the {@link #initialValue} method. 25 * 26 * @return the current thread's value of this thread-local 27 */ 28public T get() { 29 Thread t = Thread.currentThread(); 30 ThreadLocalMap map = getMap(t); 31 if (map != null) { 32 ThreadLocalMap.Entry e = map.getEntry(this); 33 if (e != null) { 34 @SuppressWarnings("unchecked") 35 T result = (T)e.value; 36 return result; 37 } 38 } 39 return setInitialValue(); 40} 41 42 43/** 44 * Get the map associated with a ThreadLocal. Overridden in 45 * InheritableThreadLocal. 46 * 47 * @param t the current thread 48 * @return the map 49 */ 50ThreadLocalMap getMap(Thread t) { 51 return t.threadLocals; 52} 53 54 55/** 56 * Get the entry associated with key. This method 57 * itself handles only the fast path: a direct hit of existing 58 * key. It otherwise relays to getEntryAfterMiss. This is 59 * designed to maximize performance for direct hits, in part 60 * by making this method readily inlinable. 61 * 62 * @param key the thread local object 63 * @return the entry associated with key, or null if no such 64 */ 65private Entry getEntry(ThreadLocal<?> key) { 66 int i = key.threadLocalHashCode & (table.length - 1); 67 Entry e = table[i]; 68 if (e != null && e.get() == key) 69 return e; 70 else 71 return getEntryAfterMiss(key, i, e); 72}
  • 每一个Thread线程都有属于自己的threadLocals(ThreadLocalMap),里面有一个弱引用的Entry(ThreadLocal,Object)。

  • get方法首先通过Thread.currentThread得到当前线程,然后拿到线程的threadLocals(ThreadLocalMap),再从Entry中取得当前线程存储的value。

  • set值的时候更改当前线程的threadLocals(ThreadLocalMap)中Entry对应的value值。

实际使用中除了同步方法之外,还有起异步线程处理的场景,这个时候就需要把ThreadLocal的内容从父线程传递给子线程,这个怎么办呢?

不急,Java 还有InheritableThreadLocal来帮我们解决这个问题。

2.2 InheritableThreadLoca

1public class InheritableThreadLocal<T> extends ThreadLocal<T> { 2 /** 3 * Computes the child's initial value for this inheritable thread-local 4 * variable as a function of the parent's value at the time the child 5 * thread is created. This method is called from within the parent 6 * thread before the child is started. 7 * <p> 8 * This method merely returns its input argument, and should be overridden 9 * if a different behavior is desired. 10 * 11 * @param parentValue the parent thread's value 12 * @return the child thread's initial value 13 */ 14 protected T childValue(T parentValue) { 15 return parentValue; 16 } 17 18 /** 19 * Get the map associated with a ThreadLocal. 20 * 21 * @param t the current thread 22 */ 23 ThreadLocalMap getMap(Thread t) { 24 return t.inheritableThreadLocals; 25 } 26 27 /** 28 * Create the map associated with a ThreadLocal. 29 * 30 * @param t the current thread 31 * @param firstValue value for the initial entry of the table. 32 */ 33 void createMap(Thread t, T firstValue) { 34 t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); 35 } 36}
  • java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)

    if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

  • InheritableThreadLocal操作的是inheritableThreadLocals这个变量,而不是ThreadLocal操作的threadLocals变量。

  • 创建新线程的时候会检查父线程中parent.inheritableThreadLocals变量是否为null,如果不为null则复制一份parent.inheritableThreadLocals的数据到子线程的this.inheritableThreadLocals中去。

  • 因为复写了getMap(Thread)和CreateMap()方法直接操作inheritableThreadLocals,这样就实现了在子线程中获取父线程ThreadLocal值。

现在在使用多线程的时候,都是通过线程池来做的,这个时候用InheritableThreadLocal可以吗?会有什么问题吗?先看下下面的代码的执行情况:

  • test

    static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

    1ExecutorService executorService = Executors.newFixedThreadPool(1); 2 3inheritableThreadLocal.set("i am a inherit parent"); 4executorService.execute(new Runnable() { 5 @Override 6 public void run() { 7 8 System.out.println(inheritableThreadLocal.get()); 9 } 10}); 11 12TimeUnit.SECONDS.sleep(1); 13inheritableThreadLocal.set("i am a new inherit parent");// 设置新的值 14 15executorService.execute(new Runnable() { 16 @Override 17 public void run() { 18 19 System.out.println(inheritableThreadLocal.get()); 20 } 21});

    }

    i am a inherit parent i am a inherit parent

    public static void main(String[] args) throws InterruptedException {

    1ExecutorService executorService = Executors.newFixedThreadPool(1); 2 3inheritableThreadLocal.set("i am a inherit parent"); 4executorService.execute(new Runnable() { 5 @Override 6 public void run() { 7 8 System.out.println(inheritableThreadLocal.get()); 9 inheritableThreadLocal.set("i am a old inherit parent");// 子线程中设置新的值 10 11 12 } 13}); 14 15TimeUnit.SECONDS.sleep(1); 16inheritableThreadLocal.set("i am a new inherit parent");// 主线程设置新的值 17 18executorService.execute(new Runnable() { 19 @Override 20 public void run() { 21 22 System.out.println(inheritableThreadLocal.get()); 23 } 24});

    }

    i am a inherit parent i am a old inherit parent

这里看第一个执行结果,发现主线程第二次设置的值,没有改掉,还是第一次设置的值“i am a inherit parent”,这是什么原因呢?

再看第二个例子的执行结果,发现在第一个任务中设置的“i am a old inherit parent"的值,在第二个任务中打印出来了。这又是什么原因呢?

回过头来看看上面的源码,在线程池的情况下,第一次创建线程的时候会从父线程中copy inheritableThreadLocals中的数据,所以第一个任务成功拿到了父线程设置的”i am a inherit parent“,第二个任务执行的时候复用了第一个任务的线程,并不会触发复制父线程中的inheritableThreadLocals操作,所以即使在主线程中设置了新的值,也会不生效。同时get()方法是直接操作inheritableThreadLocals这个变量的,所以就直接拿到了第一个任务设置的值。

那遇到线程池应该怎么办呢?

2.3 TransmittableThreadLocal

TransmittableThreadLocal(TTL)这个时候就派上用场了。这是阿里开源的一个组件,我们来看看它怎么解决线程池的问题,先来一段代码,在上面的基础上修改一下,使用TransmittableThreadLocal。

1static TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();// 使用TransmittableThreadLocal 2 3 4public static void main(String[] args) throws InterruptedException { 5 6 ExecutorService executorService = Executors.newFixedThreadPool(1); 7 executorService = TtlExecutors.getTtlExecutorService(executorService); // 用TtlExecutors装饰线程池 8 9 transmittableThreadLocal.set("i am a transmittable parent"); 10 executorService.execute(new Runnable() { 11 @Override 12 public void run() { 13 14 System.out.println(transmittableThreadLocal.get()); 15 transmittableThreadLocal.set("i am a old transmittable parent");// 子线程设置新的值 16 17 } 18 }); 19 System.out.println(transmittableThreadLocal.get()); 20 21 TimeUnit.SECONDS.sleep(1); 22 transmittableThreadLocal.set("i am a new transmittable parent");// 主线程设置新的值 23 24 executorService.execute(new Runnable() { 25 @Override 26 public void run() { 27 28 System.out.println(transmittableThreadLocal.get()); 29 } 30 }); 31} 32 33 34i am a transmittable parent 35i am a transmittable parent 36i am a new transmittable parent

执行代码后发现,使用TransmittableThreadLocalTtlExecutors.getTtlExecutorService(executorService)装饰线程池之后,在每次调用任务的时,都会将当前的主线程的TransmittableThreadLocal数据copy到子线程里面,执行完成后,再清除掉。同时子线程里面的修改回到主线程时其实并没有生效。这样可以保证每次任务执行的时候都是互不干涉的。这是怎么做到的呢?来看源码。

  • TtlExecutors和TransmittableThreadLocal源码

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference<Object>(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; }

    com.alibaba.ttl.TtlRunnable#run /**

    • wrap method {@link Runnable#run()}. */ @Override public void run() { Object captured = capturedRef.get();// 获取线程的ThreadLocalMap if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); }

      Object backup = replay(captured);// 暂存当前子线程的ThreadLocalMap到backup try { runnable.run(); } finally { restore(backup);// 恢复线程执行时被改版的Threadlocal对应的值 } }

    com.alibaba.ttl.TransmittableThreadLocal.Transmitter#replay

    /**

    • Replay the captured {@link TransmittableThreadLocal} values from {@link #capture()},

    • and return the backup {@link TransmittableThreadLocal} values in current thread before replay.

    • @param captured captured {@link TransmittableThreadLocal} values from other thread from {@link #capture()}

    • @return the backup {@link TransmittableThreadLocal} values before replay

    • @see #capture()

    • @since 2.3.0 */ public static Object replay(Object captured) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>();

      for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey();

      1 // backup 2 backup.put(threadLocal, threadLocal.get()); 3 4 // clear the TTL value only in captured 5 // avoid extra TTL value in captured, when run task. 6 if (!capturedMap.containsKey(threadLocal)) { 7 iterator.remove(); 8 threadLocal.superRemove(); 9 }

      }

      // set value to captured TTL for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : capturedMap.entrySet()) { @SuppressWarnings("unchecked") TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey(); threadLocal.set(entry.getValue()); }

      // call beforeExecute callback doExecuteCallback(true);

      return backup; }

    com.alibaba.ttl.TransmittableThreadLocal.Transmitter#restore

    /**

    • Restore the backup {@link TransmittableThreadLocal} values from {@link Transmitter#replay(Object)}.

    • @param backup the backup {@link TransmittableThreadLocal} values from {@link Transmitter#replay(Object)}

    • @since 2.3.0 */ public static void restore(Object backup) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup; // call afterExecute callback doExecuteCallback(false);

      for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey();

      1 // clear the TTL value only in backup 2 // avoid the extra value of backup after restore 3 if (!backupMap.containsKey(threadLocal)) { 4 iterator.remove(); 5 threadLocal.superRemove(); 6 }

      }

      // restore TTL value for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : backupMap.entrySet()) { @SuppressWarnings("unchecked") TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey(); threadLocal.set(entry.getValue()); } }

可以看下整个过程的完整时序图:

OK,既然问题都解决了,来看看实际使用吧,有两种使用,先看第一种,涉及HTTP请求、Dubbo请求和 job,采用的是数据级别的隔离。

三、 TTL 在海外商城的实际应用

3.1 不分库,分数据行 + SpringMVC

用户 HTTP 请求,首先我们要从url或者cookie中解析出国家编号,然后在TransmittableThreadLocal中存放国家信息,在 MyBatis 的拦截器中读取国家数据,进行sql改造,最终操作指定的国家数据,多线程场景下用TtlExecutors包装原有自定义线程池,保障在使用线程池的时候能够正确将国家信息传递下去。

  • HTTP 请求
public class ShopShardingHelperUtil {
1 private static TransmittableThreadLocal<String> countrySet = new TransmittableThreadLocal<>(); 2 3 /** 4 * 获取threadLocal中设置的国家标志 5 * @return 6 */ 7 public static String getCountry() { 8 return countrySet.get(); 9 } 10 11 /** 12 * 设置threadLocal中设置的国家 13 */ 14 public static void setCountry (String country) { 15 countrySet.set(country.toLowerCase()); 16 } 17 18 19 /** 20 * 清除标志 21 */ 22 public static void clear () { 23 countrySet.remove(); 24 } 25} 26 27 28 29/** 拦截器对cookie和url综合判断国家信息,放入到TransmittableThreadLocal中 **/ 30// 设置线程中的国家标志 31String country = localeContext.getLocale().getCountry().toLowerCase(); 32 33ShopShardingHelperUtil.setCountry(country); 34 35 36/** 自定义线程池,用TtlExecutors包装原有自定义线程池 **/ 37public static Executor getExecutor() { 38 39 if (executor == null) { 40 synchronized (TransmittableExecutor.class) { 41 if (executor == null) { 42 executor = TtlExecutors.getTtlExecutor(initExecutor());// 用TtlExecutors装饰Executor,结合TransmittableThreadLocal解决异步线程threadlocal传递问题 43 } 44 } 45 } 46 47 return executor; 48} 49 50 51/** 实际使用线程池的地方,直接调用执行即可**/ 52TransmittableExecutor.getExecutor().execute(new BatchExeRunnable(param1,param2)); 53 54 55 56/** mybatis的Interceptor代码, 使用TransmittableThreadLocal的国家信息,改造原有sql,加上国家参数,在增删改查sql中区分国家数据 **/ 57public Object intercept(Invocation invocation) throws Throwable { 58 59 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); 60 BoundSql boundSql = statementHandler.getBoundSql(); 61 62 String originalSql = boundSql.getSql(); 63 64 Statement statement = (Statement) CCJSqlParserUtil.parse(originalSql); 65 66 String threadCountry = ShopShardingHelperUtil.getCountry(); 67 68 // 线程中的国家不为空才进行处理 69 if (StringUtils.isNotBlank(threadCountry)) { 70 71 if (statement instanceof Select) { 72 73 Select selectStatement = (Select) statement; 74 VivoSelectVisitor vivoSelectVisitor = new VivoSelectVisitor(threadCountry); 75 vivoSelectVisitor.init(selectStatement); 76 } else if (statement instanceof Insert) { 77 78 Insert insertStatement = (Insert) statement; 79 VivoInsertVisitor vivoInsertVisitor = new VivoInsertVisitor(threadCountry); 80 vivoInsertVisitor.init(insertStatement); 81 82 } else if (statement instanceof Update) { 83 84 Update updateStatement = (Update) statement; 85 VivoUpdateVisitor vivoUpdateVisitor = new VivoUpdateVisitor(threadCountry); 86 vivoUpdateVisitor.init(updateStatement); 87 88 } else if (statement instanceof Delete) { 89 90 Delete deleteStatement = (Delete) statement; 91 VivoDeleteVisitor vivoDeleteVisitor = new VivoDeleteVisitor(threadCountry); 92 vivoDeleteVisitor.init(deleteStatement); 93 } 94 95 96 Field boundSqlField = BoundSql.class.getDeclaredField("sql"); 97 boundSqlField.setAccessible(true); 98 boundSqlField.set(boundSql, statement.toString()); 99 } else { 100 101 logger.error("----------- intercept not-add-country sql.... ---------" + statement.toString()); 102 } 103 104 logger.info("----------- intercept query new sql.... ---------" + statement.toString()); 105 // 调用方法,实际上就是拦截的方法 106 Object result = invocation.proceed(); 107 108 return result; 109}

对于 Dubbo 接口和无法判断国家信息的 HTTP 接口,在入参部分增加国家信息参数,通过拦截器或者手动set国家信息到TransmittableThreadLocal。

对于定时任务 job,因为所有国家都需要执行,所以会把所有国家进行遍历执行,这也可以通过简单的注解来解决。

这个版本的改造,点检测试也基本通过了,自动化脚本验证也是没问题的,不过因为业务发展问题最终没上线。

3.2 分库 + SpringBoot

后续在建设新的国家商城的时候,分库分表方案调整为每个国家独立数据库,同时整体开发框架升级到SpringBoot,我们把这套方案做了升级,总体思路是一样的,只是在实现细节上略有不同。

SpringBoot 里面的异步一般通过**@Async这个注解来实现,通过自定义线程池来包装,使用时在 HTTP 请求判断locale信息的写入国家信息,后续完成切DB的操作。**

对于 Dubbo 接口和无法判断国家信息的 HTTP 接口,在入参部分增加国家信息参数,通过拦截器或者手动set国家信息到TransmittableThreadLocal。

1@Bean 2public ThreadPoolTaskExecutor threadPoolTaskExecutor(){ 3 return TtlThreadPoolExecutors.getAsyncExecutor(); 4} 5 6 7public class TtlThreadPoolExecutors { 8 9 private static final String COMMON_BUSINESS = "COMMON_EXECUTOR"; 10 11 public static final int QUEUE_CAPACITY = 20000; 12 13 public static ExecutorService getExecutorService() { 14 return TtlExecutorServiceMananger.getExecutorService(COMMON_BUSINESS); 15 } 16 17 public static ExecutorService getExecutorService(String threadGroupName) { 18 return TtlExecutorServiceMananger.getExecutorService(threadGroupName); 19 } 20 21 public static ThreadPoolTaskExecutor getAsyncExecutor() { 22 // 用TtlExecutors装饰Executor,结合TransmittableThreadLocal解决异步线程threadlocal传递问题 23 return getTtlThreadPoolTaskExecutor(initTaskExecutor()); 24 } 25 26 private static ThreadPoolTaskExecutor initTaskExecutor () { 27 return initTaskExecutor(TtlThreadPoolFactory.DEFAULT_CORE_SIZE, TtlThreadPoolFactory.DEFAULT_POOL_SIZE, QUEUE_CAPACITY); 28 } 29 30 private static ThreadPoolTaskExecutor initTaskExecutor (int coreSize, int poolSize, int executorQueueCapacity) { 31 ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); 32 taskExecutor.setCorePoolSize(coreSize); 33 taskExecutor.setMaxPoolSize(poolSize); 34 taskExecutor.setQueueCapacity(executorQueueCapacity); 35 taskExecutor.setKeepAliveSeconds(120); 36 taskExecutor.setAllowCoreThreadTimeOut(true); 37 taskExecutor.setThreadNamePrefix("TaskExecutor-ttl"); 38 taskExecutor.initialize(); 39 return taskExecutor; 40 } 41 42 private static ThreadPoolTaskExecutor getTtlThreadPoolTaskExecutor(ThreadPoolTaskExecutor executor) { 43 if (null == executor || executor instanceof ThreadPoolTaskExecutorWrapper) { 44 return executor; 45 } 46 return new ThreadPoolTaskExecutorWrapper(executor); 47 } 48} 49 50 51 52 53/** 54 * @ClassName : LocaleContextHolder 55 * @Description : 本地化信息上下文holder 56 */ 57public class LocalizationContextHolder { 58 private static TransmittableThreadLocal<LocalizationContext> localizationContextHolder = new TransmittableThreadLocal<>(); 59 private static LocalizationInfo defaultLocalizationInfo = new LocalizationInfo(); 60 61 private LocalizationContextHolder(){} 62 63 public static LocalizationContext getLocalizationContext() { 64 return localizationContextHolder.get(); 65 } 66 67 public static void resetLocalizationContext () { 68 localizationContextHolder.remove(); 69 } 70 71 public static void setLocalizationContext (LocalizationContext localizationContext) { 72 if(localizationContext == null) { 73 resetLocalizationContext(); 74 } else { 75 localizationContextHolder.set(localizationContext); 76 } 77 } 78 79 public static void setLocalizationInfo (LocalizationInfo localizationInfo) { 80 LocalizationContext localizationContext = getLocalizationContext(); 81 String brand = (localizationContext instanceof BrandLocalizationContext ? 82 ((BrandLocalizationContext) localizationContext).getBrand() : null); 83 if(StringUtils.isNotEmpty(brand)) { 84 localizationContext = new SimpleBrandLocalizationContext(localizationInfo, brand); 85 } else if(localizationInfo != null) { 86 localizationContext = new SimpleLocalizationContext(localizationInfo); 87 } else { 88 localizationContext = null; 89 } 90 setLocalizationContext(localizationContext); 91 } 92 93 public static void setDefaultLocalizationInfo(@Nullable LocalizationInfo localizationInfo) { 94 LocalizationContextHolder.defaultLocalizationInfo = localizationInfo; 95 } 96 97 public static LocalizationInfo getLocalizationInfo () { 98 LocalizationContext localizationContext = getLocalizationContext(); 99 if(localizationContext != null) { 100 LocalizationInfo localizationInfo = localizationContext.getLocalizationInfo(); 101 if(localizationInfo != null) { 102 return localizationInfo; 103 } 104 } 105 return defaultLocalizationInfo; 106 } 107 108 public static String getCountry(){ 109 return getLocalizationInfo().getCountry(); 110 } 111 112 public static String getTimezone(){ 113 return getLocalizationInfo().getTimezone(); 114 } 115 116 public static String getBrand(){ 117 return getBrand(getLocalizationContext()); 118 } 119 120 public static String getBrand(LocalizationContext localizationContext) { 121 if(localizationContext == null) { 122 return null; 123 } 124 if(localizationContext instanceof BrandLocalizationContext) { 125 return ((BrandLocalizationContext) localizationContext).getBrand(); 126 } 127 throw new LocaleException("unsupported localizationContext type"); 128 } 129} 130 @Override 131 public LocaleContext resolveLocaleContext(final HttpServletRequest request) { 132 parseLocaleCookieIfNecessary(request); 133 LocaleContext localeContext = new TimeZoneAwareLocaleContext() { 134 @Override 135 public Locale getLocale() { 136 return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME); 137 } 138 @Override 139 public TimeZone getTimeZone() { 140 return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME); 141 } 142 }; 143 // 设置线程中的国家标志 144 setLocalizationInfo(request, localeContext.getLocale()); 145 return localeContext; 146 } 147 148 private void setLocalizationInfo(HttpServletRequest request, Locale locale) { 149 String country = locale!=null?locale.getCountry():null; 150 String language = locale!=null?(locale.getLanguage() + "_" + locale.getVariant()):null; 151 LocaleRequestMessage localeRequestMessage = localeRequestParser.parse(request); 152 final String countryStr = country; 153 final String languageStr = language; 154 final String brandStr = localeRequestMessage.getBrand(); 155 LocalizationContextHolder.setLocalizationContext(new BrandLocalizationContext() { 156 @Override 157 public String getBrand() { 158 return brandStr; 159 } 160 161 @Override 162 public LocalizationInfo getLocalizationInfo() { 163 return LocalizationInfoAssembler.assemble(countryStr, languageStr); 164 } 165 }); 166 }

对于定时任务job,因为所有国家都需要执行,所以会把所有国家进行遍历执行,这也可以通过简单的注解和AOP来解决。

四、总结

本文从业务拓展的角度阐述了在复杂业务场景下如何通过ThreadLocal,过渡到InheritableThreadLocal,再通过TransmittableThreadLocal解决实际业务问题。因为海外的业务在不断的探索中前进,技术也在不断的探索中演进,面对这种复杂多变的情况,我们的应对策略是先做国际化,再做本地化,more global才能more local,多国家的隔离只是国际化最基本的起点,未来还有很多业务和技术等着我们去挑战。

作者:vivo 官网商城开发团队

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Java 多线程上下文传递在复杂场景下的实践 - HelloWorld