One, the introduction

Overseas shopping malls start from India, and gradually there will be demands from other countries. At this time, we need to make a transformation for the current shopping mall, which can support shopping malls in multiple countries. There will be many problems, such as multi-language, multi-country, multi-time zone, localization and so on. How to pass on the identified country information in multi-country situations, layer by layer, until the last step of code execution. There are even some multi-threaded scenarios to handle.

Second, background technology

2.1 ThreadLocal

ThreadLocal is the easiest thing to think about. After the entry recognizes the country information, it is thrown into ThreadLocal, so that the subsequent code, redis, DB, etc. can be used for country identification.

Here’s a quick introduction to ThreadLocal:

/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) map.set(this, value); else createMap(t, value); } /** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for  the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e ! = null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } /** * Get the entry associated with key. This method * itself handles only the fast path: a direct hit of existing * key. It otherwise relays to getEntryAfterMiss. This is * designed to maximize performance for  direct hits, in part * by making this method readily inlinable. * * @param key the thread local object * @return the entry associated  with key, or null if no such */ private Entry getEntry(ThreadLocal<? > key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e ! = null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }Copy the code

  • Each Thread Thread has its own threadLocals(ThreadLocalMap), which contains a weakly referenced Entry(ThreadLocal,Object).

  • The get method obtains the currentThread from thread.currentthread, then threadLocals(ThreadLocalMap) of the Thread, and then obtains the value stored by the currentThread from the Entry.

  • Changes the value of Entry in threadLocals(ThreadLocalMap) of the current thread.

In practice, in addition to synchronous methods, there are asynchronous thread processing scenarios where the contents of a ThreadLocal need to be passed from parent thread to child thread.

Java also has an InheritableThreadLocal to help us solve this problem.

2.2 InheritableThreadLoca

public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * Computes the child's initial value for this inheritable thread-local * variable as a function of the parent's value at the time the child * thread is created. This method is called from within the parent * thread before the child is started. * <p> * This method merely returns its input argument, and should be overridden * if a different behavior is desired. * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }}Copy the code

  • 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);

  • InheritableThreadLocals operates on the inheritableThreadLocals variable, not on the threadLocals variable of the ThreadLocal operation.

  • Create a new thread will check the parent in the parent thread. Whether inheritableThreadLocals variable to null, If not null, a copy of the parent. The data to the child thread inheritableThreadLocals enclosing inheritableThreadLocals.

  • Because the getMap(Thread) and CreateMap() methods are overridden to operate on inheritableThreadLocals directly, it is possible to obtain the parent ThreadLocal value from the child Thread.

Use InheritableThreadLocal when you’re using multiple threads to use a thread pool. Will there be any problems? Let’s start with the following code:

  • test

    static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>();

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

    ExecutorService executorService = Executors.newFixedThreadPool(1); inheritableThreadLocal.set("i am a inherit parent"); executorService.execute(new Runnable() { @Override public void run() { System.out.println(inheritableThreadLocal.get());  }}); TimeUnit.SECONDS.sleep(1); inheritableThreadLocal.set("i am a new inherit parent"); Executorservice.execute (new Runnable() {@override public void run() {executorService.execute(new Runnable() { System.out.println(inheritableThreadLocal.get()); }});Copy the code

    }

    i am a inherit parent i am a inherit parent

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

    ExecutorService executorService = Executors.newFixedThreadPool(1); inheritableThreadLocal.set("i am a inherit parent"); executorService.execute(new Runnable() { @Override public void run() { System.out.println(inheritableThreadLocal.get());  inheritableThreadLocal.set("i am a old inherit parent"); // set new value in child thread}}); TimeUnit.SECONDS.sleep(1); inheritableThreadLocal.set("i am a new inherit parent"); Executorservice.execute (new Runnable() {@override public void run() {executorService.execute(new Runnable() { System.out.println(inheritableThreadLocal.get()); }});Copy the code

    }

    i am a inherit parent i am a old inherit parent

Inherit parent = inherit parent = inherit parent = inherit parent = inherit parent = inherit parent = INHERIT parent = inherit parent

The value of “I am an old inherit parent” set in the first task is printed in the second task. What is the reason for this?

[inheritableThreadLocals] [I am a inherit parent] [inheritableThreadLocals] The inheritableThreadLocals operation in the parent thread is not triggered when the second task is executed by reusing the thread of the first task, so setting new values in the main thread will not take effect. The get() method operates directly on the inheritableThreadLocals variable, so it gets the value set by the first task.

So what do you do when you encounter a thread pool?

2.3 TransmittableThreadLocal

The TransmittableThreadLocal (TTL) will come in handy at this point. If there is any thread TransmittableThreadLocal, there is no thread TransmittableThreadLocal. If there is any thread TransmittableThreadLocal, there is no thread TransmittableThreadLocal. If there is any thread transmittableLocal, there is no thread TransmittableThreadLocal.

static TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>(); // Use TransmittableThreadLocal public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(1); executorService = TtlExecutors.getTtlExecutorService(executorService); / / TtlExecutors decorative thread pool transmittableThreadLocal. Set (" I am a transmittable parent "); executorService.execute(new Runnable() { @Override public void run() { System.out.println(transmittableThreadLocal.get()); transmittableThreadLocal.set("i am a old transmittable parent"); // The child thread sets the new value}}); System.out.println(transmittableThreadLocal.get()); TimeUnit.SECONDS.sleep(1); transmittableThreadLocal.set("i am a new transmittable parent"); Executorservice.execute (new Runnable() {@override public void run() {executorService.execute(new Runnable() { System.out.println(transmittableThreadLocal.get()); }}); } i am a transmittable parent i am a transmittable parent i am a new transmittable parentCopy the code

Execute code, using TransmittableThreadLocalTtlExecutors. GetTtlExecutorService (executorService) decorative thread pool, in the task of each call, If there is any thread that has the TransmittableThreadLocal transmittable, the child thread will have the TransmittableThreadLocal TransmittableThreadLocal TransmittableThreadLocal. Also, changes made in child threads do not actually take effect when they return to the main thread. This ensures that each mission is carried out without interference. How does this work? Look at the source code.

  • Ttlexectrue and TransmittableThreadLocal source code

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { this.capturedRef = new AtomicReference(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(); / / the thread ThreadLocalMap if (captured = = null | | releaseTtlValueReferenceAfterRun &&! capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException(“TTL value reference is released after run!” ); }

    Object backup = replay(captured); Backup try {runnable.run(); // Save the ThreadLocalMap of the current child thread to backup try {runnable.run(); } finally { restore(backup); // Restore the value of Threadlocal when the thread executes.Copy the code

    }

    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, Object>) captured; Map<TransmittableThreadLocal, Object> backup = new HashMap, Object>();

    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<? >,? >> iterator = holder.get().entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<? >,? > next = iterator.next(); TransmittableThreadLocal<? > threadLocal = next.getKey(); // backup backup.put(threadLocal, threadLocal.get()); // clear the TTL value only in captured // avoid extra TTL value in captured, when run task. if (! capturedMap.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // 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;Copy the code

    }

    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, 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(); // clear the TTL value only in backup // avoid the extra value of backup after restore if (! backupMap.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // restore TTL value for (Map.Entry<TransmittableThreadLocal<? >, Object> entry : backupMap.entrySet()) { @SuppressWarnings("unchecked") TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey(); threadLocal.set(entry.getValue()); }Copy the code

    }

    You can see the full sequence of the process:

    OK, now that we’ve solved the problem, let’s look at the actual use. There are two uses. The first one, involving HTTP requests, Dubbo requests, and Jobs, uses data-level isolation.

    Iii. Practical application of TTL in overseas shopping malls

    3.1 Non-classification library, score data line + SpringMVC

    If the user requests HTTP, first of all, we need to parse the country number from URL or cookie, then store the country information in the TransmittableThreadLocal, read the country data in MyBatis interceptor, perform SQL modification, and finally manipulate the specified country data. In multi-threaded scenarios, use TTlexec* to wrap the original customized thread pool to ensure proper transmission of national information while using the thread pool.

    • The HTTP request

      public class ShopShardingHelperUtil {

      private static TransmittableThreadLocal<String> countrySet = new TransmittableThreadLocal<>(); Public static String getCountry() {return countryset.get (); Public static void setCountry (String country) {countryset.set (country.tolowerCase ()); countrySet.set(country.tolowerCase ()); } /** * public static void clear () {countryset.remove (); }Copy the code

      }

      /** The interceptor determines the country information based on the cookie and URL. String country = localecontext.getLocale ().getCountry().tolowerCase ();

      ShopShardingHelperUtil.setCountry(country);

      **/ Public static Executor getExecutor() {/** * Customized thread pool **/ Public static Executor getExecutor() {

      if (executor == null) { synchronized (TransmittableExecutor.class) { if (executor == null) { executor = TtlExecutors.getTtlExecutor(initExecutor()); / / decorated with TtlExecutors Executor, combining TransmittableThreadLocal solve the problem of asynchronous thread threadlocal transfer}}} return Executor;Copy the code

      }

      / * * the actual use of thread pool, perform directly call * * / TransmittableExecutor getExecutor (). The execute (new BatchExeRunnable (param1, param2));

      /** Mybatis = Interceptor; /** Mybatis = TransmittableThreadLocal; **/ Public Object Intercept (Invocation) throws Throwable {

      StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String originalSql = boundSql.getSql(); Statement statement = (Statement) CCJSqlParserUtil.parse(originalSql); String threadCountry = ShopShardingHelperUtil.getCountry(); If (stringutils.isnotBlank (threadCountry)) {if (statement instanceof Select) {Select selectStatement  = (Select) statement; VivoSelectVisitor vivoSelectVisitor = new VivoSelectVisitor(threadCountry); vivoSelectVisitor.init(selectStatement); } else if (statement instanceof Insert) { Insert insertStatement = (Insert) statement; VivoInsertVisitor vivoInsertVisitor = new VivoInsertVisitor(threadCountry); vivoInsertVisitor.init(insertStatement); } else if (statement instanceof Update) { Update updateStatement = (Update) statement; VivoUpdateVisitor vivoUpdateVisitor = new VivoUpdateVisitor(threadCountry); vivoUpdateVisitor.init(updateStatement); } else if (statement instanceof Delete) { Delete deleteStatement = (Delete) statement; VivoDeleteVisitor vivoDeleteVisitor = new VivoDeleteVisitor(threadCountry); vivoDeleteVisitor.init(deleteStatement); } Field boundSqlField = BoundSql.class.getDeclaredField("sql"); boundSqlField.setAccessible(true); boundSqlField.set(boundSql, statement.toString()); } else { logger.error("----------- intercept not-add-country sql.... ---------" + statement.toString()); } logger.info("----------- intercept query new sql.... ---------" + statement.toString()); Invocation method Object result = invocation. Proceed (); return result;Copy the code

      }

    For the Dubbo interface and HTTP interface that cannot determine the country information, add the country information parameter in the input section, and manually set the country information to the TransmittableThreadLocal through the interceptor or manually.

    For a timed task, all countries need to be executed, so all countries are traversed. This can also be solved by simple annotations.

    The transformation of this version, the point inspection test is basically passed, automatic script verification is no problem, but due to business development problems did not finally online.

    3.2 Branch library + SpringBoot

    In the subsequent construction of the new national mall, the sub-database and sub-table scheme was adjusted to an independent database for each country, and the overall development framework was upgraded to SpringBoot. We upgraded this scheme. The overall idea was the same, but the implementation details were slightly different.

    Async in SpringBoot is usually implemented by ** @async annotation, which is wrapped by a custom thread pool. It is used to determine the locale information written in the HTTP request, and then complete the DB cutting operation. 六四屠杀

    For the Dubbo interface and HTTP interface that cannot determine the country information, add the country information parameter in the input section, and manually set the country information to the TransmittableThreadLocal through the interceptor or manually.

    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
        return TtlThreadPoolExecutors.getAsyncExecutor();
    }
     
     
    public class TtlThreadPoolExecutors {
     
        private static final String COMMON_BUSINESS = "COMMON_EXECUTOR";
     
        public static final int QUEUE_CAPACITY = 20000;
     
        public static ExecutorService getExecutorService() {
            return TtlExecutorServiceMananger.getExecutorService(COMMON_BUSINESS);
        }
     
        public static ExecutorService getExecutorService(String threadGroupName) {
            return TtlExecutorServiceMananger.getExecutorService(threadGroupName);
        }
     
        public static ThreadPoolTaskExecutor getAsyncExecutor() {
            // 用TtlExecutors装饰Executor,结合TransmittableThreadLocal解决异步线程threadlocal传递问题
            return getTtlThreadPoolTaskExecutor(initTaskExecutor());
        }
     
        private static ThreadPoolTaskExecutor initTaskExecutor () {
            return initTaskExecutor(TtlThreadPoolFactory.DEFAULT_CORE_SIZE, TtlThreadPoolFactory.DEFAULT_POOL_SIZE, QUEUE_CAPACITY);
        }
     
        private static ThreadPoolTaskExecutor initTaskExecutor (int coreSize, int poolSize, int executorQueueCapacity) {
            ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
            taskExecutor.setCorePoolSize(coreSize);
            taskExecutor.setMaxPoolSize(poolSize);
            taskExecutor.setQueueCapacity(executorQueueCapacity);
            taskExecutor.setKeepAliveSeconds(120);
            taskExecutor.setAllowCoreThreadTimeOut(true);
            taskExecutor.setThreadNamePrefix("TaskExecutor-ttl");
            taskExecutor.initialize();
            return taskExecutor;
        }
     
        private static ThreadPoolTaskExecutor getTtlThreadPoolTaskExecutor(ThreadPoolTaskExecutor executor) {
            if (null == executor || executor instanceof ThreadPoolTaskExecutorWrapper) {
                return executor;
            }
            return new ThreadPoolTaskExecutorWrapper(executor);
        }
    }
     
     
     
     
    /**
     * @ClassName : LocaleContextHolder
     * @Description : 本地化信息上下文holder
     */
    public class LocalizationContextHolder {
        private static TransmittableThreadLocal<LocalizationContext> localizationContextHolder = new TransmittableThreadLocal<>();
        private static LocalizationInfo defaultLocalizationInfo = new LocalizationInfo();
     
        private LocalizationContextHolder(){}
     
        public static LocalizationContext getLocalizationContext() {
            return localizationContextHolder.get();
        }
     
        public static void resetLocalizationContext () {
            localizationContextHolder.remove();
        }
     
        public static void setLocalizationContext (LocalizationContext localizationContext) {
            if(localizationContext == null) {
                resetLocalizationContext();
            } else {
                localizationContextHolder.set(localizationContext);
            }
        }
     
        public static void setLocalizationInfo (LocalizationInfo localizationInfo) {
            LocalizationContext localizationContext = getLocalizationContext();
            String brand = (localizationContext instanceof BrandLocalizationContext ?
                    ((BrandLocalizationContext) localizationContext).getBrand() : null);
            if(StringUtils.isNotEmpty(brand)) {
                localizationContext = new SimpleBrandLocalizationContext(localizationInfo, brand);
            } else if(localizationInfo != null) {
                localizationContext = new SimpleLocalizationContext(localizationInfo);
            } else {
                localizationContext = null;
            }
            setLocalizationContext(localizationContext);
        }
     
        public static void setDefaultLocalizationInfo(@Nullable LocalizationInfo localizationInfo) {
            LocalizationContextHolder.defaultLocalizationInfo = localizationInfo;
        }
     
        public static LocalizationInfo getLocalizationInfo () {
            LocalizationContext localizationContext = getLocalizationContext();
            if(localizationContext != null) {
                LocalizationInfo localizationInfo = localizationContext.getLocalizationInfo();
                if(localizationInfo != null) {
                    return localizationInfo;
                }
            }
            return defaultLocalizationInfo;
        }
     
        public static String getCountry(){
            return getLocalizationInfo().getCountry();
        }
     
        public static String getTimezone(){
            return getLocalizationInfo().getTimezone();
        }
     
        public static String getBrand(){
            return getBrand(getLocalizationContext());
        }
     
        public static String getBrand(LocalizationContext localizationContext) {
            if(localizationContext == null) {
                return null;
            }
            if(localizationContext instanceof BrandLocalizationContext) {
                return ((BrandLocalizationContext) localizationContext).getBrand();
            }
            throw new LocaleException("unsupported localizationContext type");
        }
    }
        @Override
        public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
            parseLocaleCookieIfNecessary(request);
            LocaleContext localeContext = new TimeZoneAwareLocaleContext() {
                @Override
                public Locale getLocale() {
                    return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
                }
                @Override
                public TimeZone getTimeZone() {
                    return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
                }
            };
            // 设置线程中的国家标志
            setLocalizationInfo(request, localeContext.getLocale());
            return localeContext;
        }
     
        private void setLocalizationInfo(HttpServletRequest request, Locale locale) {
            String country = locale!=null?locale.getCountry():null;
            String language = locale!=null?(locale.getLanguage() + "_" + locale.getVariant()):null;
            LocaleRequestMessage localeRequestMessage = localeRequestParser.parse(request);
            final String countryStr = country;
            final String languageStr = language;
            final String brandStr = localeRequestMessage.getBrand();
            LocalizationContextHolder.setLocalizationContext(new BrandLocalizationContext() {
                @Override
                public String getBrand() {
                    return brandStr;
                }
     
                @Override
                public LocalizationInfo getLocalizationInfo() {
                    return LocalizationInfoAssembler.assemble(countryStr, languageStr);
                }
            });
        }
    Copy the code

    For timed task jobs, traversal of all countries is performed because all countries need to be executed, which can also be handled with simple annotations and AOP.

    Four,

    From the perspective of business development, this paper describes how to transition from ThreadLocal to InheritableThreadLocal and solve practical business problems through TransmittableThreadLocal in complex business scenarios. Because the overseas business is advancing in constant exploration, and the technology is also evolving in constant exploration, in the face of such complex and changeable situation, our coping strategy is to do internationalization first, then localization, more global can be more local, the isolation of multiple countries is just the most basic starting point of internationalization. There are many businesses and technologies to challenge in the future.

    Author: Development team of Vivo official website mall