Cache module of mybatis

Cache module of mybatis

There are L1 (enabled by default) and L2 caches in mybatis. Caching can speed up queries and reduce the number of connections to db.

The cache module belongs to the basic support layer of mybatis. At org apache. ibatis. Cache package.

--------org.apache.ibatis.cache
-------------------------------decorators
-----------------------------------------BlockingCache
-----------------------------------------FifoCache
-----------------------------------------LoggingCache
-----------------------------------------LruCache
-----------------------------------------ScheduledCache
-----------------------------------------SerializedCache
-----------------------------------------SoftCache
-----------------------------------------SynchronizedCache
-----------------------------------------TransactionalCache
-----------------------------------------WeakCache
-------------------------------impl
-----------------------------------PerpetualCache
------------------------------Cache

Cache

Cached top-level interface

public interface Cache {
    // Get cached id
    String getId();
    // Insert cache
    void putObject(Object key, Object value);
    // Query cache
    Object getObject(Object key);
    // Delete cache
    Object removeObject(Object key);
    //Clean up this cache instance
    void clear();
    // This method is optional, not the core calling method. Query the number of cached key s
    int getSize();
    //
    ReadWriteLock getReadWriteLock();
}   

PerpetualCache

Basic implementation of caching (the basic caching function is realized, and the underlying data structure uses HashMap)

The L1 cache is a session level cache (SqlSession).

public class PerpetualCache implements Cache {

    private final String id;
    // The underlying data structure of storage cache uses hashMap
    private Map<Object, Object> cache = new HashMap<Object, Object>();
    // afferent
    public PerpetualCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }


    @Override
    public int getSize() {
        return cache.size();
    }

    // Insert cache
    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    //Query cache
    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    // Remove cache
    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

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

      @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }
}

BlockingCache

BlockingCache is a cache decorator with blocking function.

public class BlockingCache implements Cache {

    //Timeout
    private long timeout;
    // Decorated cache
    private final Cache delegate;
    // Each Key has a reentrant lock
    private final ConcurrentHashMap<Object, ReentrantLock> locks;
    //Constructor to decorate other cache objects
    public BlockingCache(Cache delegate) {
      this.delegate = delegate;
      this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
    }

    @Override
    public String getId() {
      return delegate.getId();
    }

    @Override
    public int getSize() {
      return delegate.getSize();
    }

    // Insert the cache of the Key and release the lock at the same time
    @Override
    public void putObject(Object key, Object value) {
      try {
        delegate.putObject(key, value);
      } finally {
        releaseLock(key);
      }
    }

  // Query the cache of key
  @Override
  public Object getObject(Object key) {
    //Get lock of key
    acquireLock(key);
    // Query the value of key cache
    Object value = delegate.getObject(key);
    //If value is not null, release the lock and return the value. Otherwise, keep holding the lock until you know that the key has a value inserted into the cache
    if (value != null) {
      releaseLock(key);
    }        
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }

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

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }
  
  //Query the lock held by the Key. If not, create a new lock
  private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();
    ReentrantLock previous = locks.putIfAbsent(key, lock);
    return previous == null ? lock : previous;
  }
  
  private void acquireLock(Object key) {
    Lock lock = getLockForKey(key);
    if (timeout > 0) {
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());  
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {
      lock.lock();
    }
  }
  
  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

  public long getTimeout() {
    return timeout;
  }

  public void setTimeout(long timeout) {
    this.timeout = timeout;
  } 


}

Thread a queries the key for the first time, and there is no cache. Thread a holds the key lock; At the same time, thread B queries the key. At this time, thread B is blocked. A queries the data, obtains the data cache, releases the key lock, and B is awakened to continue querying from the cache.

BlockingCache will decorate the perpetual cache cache.

Decorator mode

From the package structure of the above Cache, we can see that Cache is the top-level interface, perpetual Cache is the basic implementation, and various classes under decorators are buffers with different functions, which can realize the combination of different functions through decorator mode.

//Top survey interface
public interface  Cache{

  Object getObject(Object key);

}

//Basic implementation class
public class PerpetualCache implements  Cache {
    
    @Override
    public Object getObject(Object key) {
      return cache.get(key);
    }
}

//Decorator
public class BlockingCache implements Cache {
  
  @Override
  public Object getObject(Object key) {
      acquireLock(key);
      Object value = delegate.getObject(key);
      if (value != null) {
        releaseLock(key);
      }        
      return value;
  }

}

//Example
public class Example {

    public static void main() {
        Cache cache = new PerpetualCache();
        //Decorate the cache and add the function of lock
        cache = new BlockingCache(cache);
        CacheKey cacheKey = new CacheKey();
        cache.getOject(cacheKey); 
    }
}

CacheKey

Since mybati supports dynamic sql, you cannot construct a cached key through a simple string. The cachekey is the key in the Cache.

//Override the Equals and hashCode methods
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
  //Default value for multiplier
  private static final int DEFAULT_MULTIPLYER = 37;
  //Default value for hashcode
  private static final int DEFAULT_HASHCODE = 17;
  //Used to calculate the hashcode value
  private final int multiplier;
  private int hashcode;
  private long checksum;
  //Number of objects
  private int count;
  //Stores the objects that make up the CacheKey
  private transient List<Object> updateList;

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

  //Store the objects that make up the CacheKey and recalculate the hashcode
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    //Recalculate hashcode
    hashcode = multiplier * hashcode + baseHashCode;
    //Storage object
    updateList.add(object);
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    return hashcode;
  }

  @Override
  public String toString() {
    StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
    for (Object object : updateList) {
      returnValue.append(':').append(ArrayUtil.toString(object));
    }
    return returnValue.toString();
  }

  @Override
  public CacheKey clone() throws CloneNotSupportedException {
    CacheKey clonedCacheKey = (CacheKey) super.clone();
    clonedCacheKey.updateList = new ArrayList<Object>(updateList);
    return clonedCacheKey;
  }

}

The CacheKey consists of the following five parts

1. id of mappedstatement
2. Specify the range of query results, that is, rowboundaries Offset,rowBounds. limit
3. The query uses include? Dynamic sql
4. Actual parameters passed by the user
5. The configured environment id, if mybatis config XML has configuration environment information.

public abstract class BaseExecutor implements Executor {
//Create CacheKey
@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    //id of MappedStatement
    cacheKey.update(ms.getId());
    // Specify the range of query results, that is, rowboundaries Offset,rowBounds. limit
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    // The query used contains? Dynamic sql
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // Actual parameters passed by the user
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      // Configured environment id, if mybatis config XML has configuration environment information.
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }
}

summary

1. There are L1 (enabled by default) and L2 caches in mybatis. Caching can speed up queries and reduce the number of connections to db.
2. The cache module belongs to the basic support layer of mybatis. At org apache. ibatis. Cache package.
3. Basic implementation of perpetualcache cache (the basic cache function is realized, and the underlying data structure uses HashMap)
4.BlockingCache is a cache decorator with blocking function.
5. The design of cache module adopts decorator mode
6.Cache is the top-level interface, and perpetual cache is the basic implementation. Various classes under decorators are buffers with different functions, and the combination of different functions is realized through decorator mode.
7. The cachekey consists of five parts: the id of the mappedStatement, the specified range of query results, and the content used by the query? Dynamic sql, actual parameters passed by the user, and configured environment id

Keywords: Mybatis Back-end

Added by gabriellevierneza on Wed, 12 Jan 2022 08:07:44 +0200