mybatis/mybatis-3 · error · CacheException

Couldn't get a lock in {timeout} for the key {key} at the ca

Error message

Couldn't get a lock in {timeout} for the key {key} at the cache {cacheId}

What it means

Thrown by BlockingCache.acquireLock() when a query for a cache key cannot obtain the per-key CountDownLatch within the configured timeout (the 'blocking' cache decorator serializes queries for the same key so a cache miss is not thundering-herded). The message includes the timeout in ms, the key, and the cache id. timeout > 0 only when the decorator was configured with a positive blockingTimeout.

Source

Thrown at src/main/java/org/apache/ibatis/cache/decorators/BlockingCache.java:100

  }

  @Override
  public void clear() {
    delegate.clear();
  }

  private void acquireLock(Object key) {
    CountDownLatch newLatch = new CountDownLatch(1);
    while (true) {
      CountDownLatch latch = locks.putIfAbsent(key, newLatch);
      if (latch == null) {
        break;
      }
      try {
        if (timeout > 0) {
          boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
          if (!acquired) {
            throw new CacheException(
                "Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
          }
        } else {
          latch.await();
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    }
  }

  private void releaseLock(Object key) {
    CountDownLatch latch = locks.remove(key);
    if (latch == null) {
      throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
    }
    latch.countDown();
  }

View on GitHub (pinned to 008069adb1)

Solutions

  1. Increase blockingTimeout (or remove the property to wait indefinitely) so typical query latency fits inside the window
  2. Fix the underlying slow query (index, fetch size) so first-miss loads finish before other waiters time out
  3. Pre-warm the cache for hot keys at startup so concurrent waiters never block on a cold miss
  4. Consider whether BlockingCache is needed at all — a request-coalescing layer or shorter TTL may serve better

Example fix

<!-- before -->
<cache type="org.apache.ibatis.cache.decorators.BlockingCache" blockingTimeout="100"/>

<!-- after -->
<cache type="org.apache.ibatis.cache.decorators.BlockingCache" blockingTimeout="3000"/>
Defensive patterns

Strategy: retry

Validate before calling

// before enabling: measure p99 of the cached query and set blockingTimeout above it
long p99 = measureQueryP99Ms();
if (blockingTimeout > 0 && blockingTimeout < p99) throw new IllegalStateException("blockingTimeout " + blockingTimeout + "ms < query p99 " + p99 + "ms");

Try / catch

catch (CacheException e) { if (e.getMessage().contains("Couldn't get a lock")) { backoffAndRetryOnce(key); } else throw e; } — bounded retry only; a genuinely stuck holder needs cache eviction.

Prevention

When it happens

Trigger: BlockingCache configured via <cache ... ><property name="blockingTimeout" value="250"/></cache> (or CacheDecorator order in a custom cache), and a query for key K takes longer than 250ms while another thread holds K's latch — e.g. a slow first query on a cold cache with many concurrent requests for the same row.

Common situations: Cold-start stampedes on hot keys where the loading query exceeds the blocking timeout; slow DB under load making every first-miss query exceed a small timeout; deadlock-ish long transactions holding the query; timeout set too low (default is 0 = wait forever, so someone set it deliberately).

Understand the failure class

Related errors


AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14). Data as JSON: /api/errors/dc71d1517678616d. Report an issue: GitHub.