Databases 11 min read

Why TiDB Was Slower Than Oracle: Uncovering MyBatis ComputeIfAbsent Bottleneck

After observing TiDB taking 35 minutes versus Oracle’s 15 minutes for a massive batch job, we traced the slowdown to MyBatis’s use of ConcurrentHashMap.computeIfAbsent in JDK 8, which caused thread blocking, and resolved it by upgrading to JDK 9 and applying MyBatis patches.

Programmer DD
Programmer DD
Programmer DD
Why TiDB Was Slower Than Oracle: Uncovering MyBatis ComputeIfAbsent Bottleneck

Remote Investigation

Grafana showed that the TiDB cluster’s resource usage was very low during the batch job. Increasing the application concurrency from 40 to 100 did not change QPS or resource usage. Most database connections appeared idle, and the program was built with Spring Batch and MyBatis, suggesting that the concurrency change was effective.

Network latency between the application server and TiDB was 2–3 ms. Deploying the application inside the TiDB cluster reduced the batch time from 35 minutes to 27 minutes, but the database itself showed no pressure, so tuning database parameters was not useful.

Two identical databases (d1 and d2) were created in the TiDB cluster, and two identical batch applications were run against them simultaneously. The total processing time remained 27 minutes, confirming that increasing application concurrency did not actually increase load on the database.

On‑site Investigation

Using a JDBC demo to stress the cluster showed that database resource usage grew with increased concurrency, ruling out the database as the bottleneck.

VisualVM revealed that many application threads were in the Monitor state, indicating that the performance bottleneck originated from the application.

Further thread dumps showed that most threads were blocked inside MyBatis reflection code.

Root Cause Analysis

MyBatis uses a ReflectorFactory that caches Reflector objects in a ConcurrentHashMap. The method computeIfAbsent in JDK 8 acquires a lock on the map bin, which can block other threads when the same key is accessed.

Locked ownable synchronizers:
    - <0x000000008523ca00> (a java.util.concurrent.ThreadPoolExecutor$worker)

"taskExecutorForHb-197" #342 prio=5 os_prio=0 tid=0x0007f5d7c72f800 nid=0x182c waiting for monitor entry [0x00007f5ccd6d4000]
    java.lang.Thread.State: BLOCKED (on  object monitor)
    - waiting to lock <0x0000000080a772d8> (a java.util.concurrent.ConcurrentHashMap$Node)
    at org.apache.ibatis.reflection.DefaultReflection.DefaultReflectorFactory.findForClass(DefaultReflectorFactory.java:1674)

The blocking occurs when reflectorMap.computeIfAbsent(type, Reflector::new) is invoked. Disabling the cache (setting classCacheEnabled to false) forces a new Reflector to be created each time, but other calls to computeIfAbsent still suffer the same contention.

JDK 8 issue JDK‑8161372 describes how ConcurrentHashMap.computeIfAbsent can lock the bin, causing thread blockage. This issue was fixed in JDK 9.

The entire method invocation is performed atomically, so the function is applied at most once per key. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this map.

Verification

Upgrading the JDK to version 9 and running the batch with 500 concurrent threads (network latency eliminated) reduced the processing time to 16 minutes. The application server CPU rose to about 85 %, indicating that the new bottleneck was now the application server itself.

Conclusion at the Time

MyBatis 3.5.x suffers from a performance problem in JDK 8 due to the use of ConcurrentHashMap.computeIfAbsent. The issue can be mitigated by upgrading to JDK 9 or by downgrading MyBatis to 3.4.x, which does not use the problematic method.

public Reflector findForClass(Class<?> type) {
  if (classCacheEnabled) {
    // synchronized(type) removed see issue #461
    return reflectorMap.computeIfAbsent(type, Reflector::new);
  } else {
    return new Reflector(type);
  }
}

Current Conclusion

MyBatis responded quickly and added a workaround in version 3.5.7 that checks the cache before invoking computeIfAbsent, avoiding the lock contention.

public class MapUtil {
  /**
   * A temporary workaround for Java 8 specific performance issue JDK‑8161372.
   */
  public static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<K, V> mappingFunction) {
    V value = map.get(key);
    if (value != null) {
      return value;
    }
    return map.computeIfAbsent(key, mappingFunction::apply);
  }
}

Developers are encouraged to test their own workloads for similar thread‑blocking behavior.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TiDBMyBatisConcurrentHashMapJDK8BatchProcessing
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.