All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bitronix.tm.resource.jdbc.LruStatementCache Maven / Gradle / Ivy

There is a newer version: 62
Show newest version
/*
 * Copyright (C) 2006-2013 Bitronix Software (http://www.bitronix.be)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package bitronix.tm.resource.jdbc;

import bitronix.tm.internal.LogDebugCheck;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Last Recently Used PreparedStatement cache with eviction listeners
 * support implementation.
 *
 * @author Ludovic Orban
 * @author Brett Wooldridge
 */
public class LruStatementCache
{

	private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LruStatementCache.class.toString());
	/**
	 * We use a LinkedHashMap with _access order_ specified in the
	 * constructor.  According to the LinkedHashMap documentation:
	 * 
	 *   A special constructor is provided to create a linked hash map
	 *   whose order of iteration is the order in which its entries
	 *   were last accessed, from least-recently accessed to most-recently
	 *   (access-order). This kind of map is well-suited to building LRU
	 *   caches. Invoking the put or get method results in an access to
	 *   the corresponding entry (assuming it exists after the invocation
	 *   completes).
	 * 
*/ private final LinkedHashMap cache; /** * A list of listeners concerned with prepared statement cache * evictions. */ private final List> evictionListeners; /** * The target maxSize of the cache. The cache may drift slightly * higher in size in the case that every statement in the cache is * in use and therefore nothing can be evicted. But eventually * (probably quickly) the cache will return to maxSize. */ private int maxSize; /** * See the LinkedHashMap documentation. We maintain our own size * here, rather than calling size(), because size() on a LinkedHashMap * is proportional in time (O(n)) with the size of the collection -- i.e. * calling size() must traverse the entire list and count the elements. * Tracking size ourselves provides O(1) access. */ private int size; /** * A flag that is set during clear operations to prevent statements that * are closing from coming back into the cache. */ private AtomicBoolean clearInProgress; /** * Constructor LruStatementCache creates a new LruStatementCache instance. * * @param maxSize * of type int */ public LruStatementCache(int maxSize) { this.maxSize = maxSize; cache = new LinkedHashMap<>(maxSize, 0.75f, true /* access order */); evictionListeners = new CopyOnWriteArrayList<>(); clearInProgress = new AtomicBoolean(); } /** * The provided key is just a 'shell' JdbcPreparedStatementHandle, it comes * in with no actual 'delegate' PreparedStatement. However, it contains all * other pertinent information such as SQL statement, autogeneratedkeys * flag, cursor holdability, etc. See the equals() method in the * JdbcPreparedStatementHandle class. It is a complete key for a cached * statement. *

* If there is a matching cached PreparedStatement, it will be set as the * delegate in the provided JdbcPreparedStatementHandle. * * @param key * the cache key * * @return the cached JdbcPreparedStatementHandle statement, or null */ public PreparedStatement get(CacheKey key) { synchronized (cache) { // See LinkedHashMap documentation. Getting an entry means it is // updated as the 'youngest' (Most Recently Used) entry. StatementTracker cached = cache.get(key); if (cached != null) { cached.usageCount++; if (LogDebugCheck.isDebugEnabled()) { log.finer("delivered from cache with usage count " + cached.usageCount + " statement <" + key + ">"); } return cached.statement; } return null; } } /** * A statement is put into the cache. This is called when a * statement is first prepared and also when a statement is * closed (by the client). A "closed" statement has it's * usage counter decremented in the cache. * * @param key * a cache key * @param statement * a prepared statement handle * * @return a prepared statement */ public PreparedStatement put(CacheKey key, PreparedStatement statement) { if (clearInProgress.get()) { return null; } synchronized (cache) { if (maxSize < 1) { return null; } // See LinkedHashMap documentation. Getting an entry means it is // updated as the 'youngest' (Most Recently Used) entry. StatementTracker cached = cache.get(key); if (cached == null) { if (LogDebugCheck.isDebugEnabled()) { log.finer("adding to cache statement <" + key + ">"); } cache.put(key, new StatementTracker(statement)); size++; } else { cached.usageCount--; statement = cached.statement; if (LogDebugCheck.isDebugEnabled()) { log.finer("returning to cache statement <" + key + "> with usage count " + cached.usageCount); } } // If the size is exceeded, we will _try_ to evict one (or more) // statements until the max level is again reached. However, if // every statement in the cache is 'in use', the size of the cache // is not reduced. Eventually the cache will be reduced, no worries. if (size > maxSize) { tryEviction(); } return statement; } } /** * Try to evict statements from the cache. Only statements with a * current usage count of zero will be evicted. Statements are * evicted until the cache is reduced to maxSize. */ private void tryEviction() { // Iteration order of the LinkedHashMap is from LRU to MRU Iterator> it = cache.entrySet() .iterator(); while (it.hasNext()) { Entry entry = it.next(); StatementTracker tracker = entry.getValue(); if (tracker.usageCount == 0) { it.remove(); size--; CacheKey key = entry.getKey(); if (LogDebugCheck.isDebugEnabled()) { log.finer("evicting from cache statement <" + key + "> " + entry.getValue().statement); } fireEvictionEvent(tracker.statement); // We can stop evicting if we're at maxSize... if (size <= maxSize) { break; } } } } /** * Method fireEvictionEvent ... * * @param stmt * of type PreparedStatement */ private void fireEvictionEvent(PreparedStatement stmt) { for (LruEvictionListener listener : evictionListeners) { listener.onEviction(stmt); } } /** * Method addEvictionListener ... * * @param listener * of type LruEvictionListener PreparedStatement */ public void addEvictionListener(LruEvictionListener listener) { evictionListeners.add(listener); } /** * Method removeEvictionListener ... * * @param listener * of type LruEvictionListener PreparedStatement */ public void removeEvictionListener(LruEvictionListener listener) { evictionListeners.remove(listener); } /** * Evict all statements from the cache. This likely happens on * connection close. */ protected void clear() { if (clearInProgress.compareAndSet(false, true)) { try { synchronized (cache) { Iterator> it = cache.entrySet() .iterator(); while (it.hasNext()) { Entry entry = it.next(); StatementTracker tracker = entry.getValue(); it.remove(); fireEvictionEvent(tracker.statement); } cache.clear(); size = 0; } } finally { clearInProgress.set(false); } } } public static final class CacheKey { // All of these attributes must match a proposed statement before the // statement can be considered "the same" and delivered from the cache. private final String sql; private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; private int resultSetConcurrency = ResultSet.CONCUR_READ_ONLY; private Integer resultSetHoldability; private Integer autoGeneratedKeys; private int[] columnIndexes; private String[] columnNames; /** * Constructor CacheKey creates a new CacheKey instance. * * @param sql * of type String */ public CacheKey(String sql) { this.sql = sql; } /** * Constructor CacheKey creates a new CacheKey instance. * * @param sql * of type String * @param autoGeneratedKeys * of type int */ public CacheKey(String sql, int autoGeneratedKeys) { this.sql = sql; this.autoGeneratedKeys = autoGeneratedKeys; } /** * Constructor CacheKey creates a new CacheKey instance. * * @param sql * of type String * @param resultSetType * of type int * @param resultSetConcurrency * of type int */ public CacheKey(String sql, int resultSetType, int resultSetConcurrency) { this.sql = sql; this.resultSetType = resultSetType; this.resultSetConcurrency = resultSetConcurrency; } /** * Constructor CacheKey creates a new CacheKey instance. * * @param sql * of type String * @param resultSetType * of type int * @param resultSetConcurrency * of type int * @param resultSetHoldability * of type int */ public CacheKey(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) { this.sql = sql; this.resultSetType = resultSetType; this.resultSetConcurrency = resultSetConcurrency; this.resultSetHoldability = resultSetHoldability; } /** * Constructor CacheKey creates a new CacheKey instance. * * @param sql * of type String * @param columnIndexes * of type int[] */ public CacheKey(String sql, int[] columnIndexes) { this.sql = sql; this.columnIndexes = new int[columnIndexes.length]; System.arraycopy(columnIndexes, 0, this.columnIndexes, 0, columnIndexes.length); } /** * Constructor CacheKey creates a new CacheKey instance. * * @param sql * of type String * @param columnNames * of type String[] */ public CacheKey(String sql, String[] columnNames) { this.sql = sql; this.columnNames = new String[columnNames.length]; System.arraycopy(columnNames, 0, this.columnNames, 0, columnNames.length); } /** * Method hashCode ... * * @return int */ @Override public int hashCode() { return sql != null ? sql.hashCode() : System.identityHashCode(this); } /** * Overridden equals() that takes all PreparedStatement attributes into * account. * * @return true if equal, false otherwise */ @Override public boolean equals(Object obj) { if (!(obj instanceof CacheKey)) { return false; } CacheKey otherKey = (CacheKey) obj; if (!sql.equals(otherKey.sql)) { return false; } else if (resultSetType != otherKey.resultSetType) { return false; } else if (resultSetConcurrency != otherKey.resultSetConcurrency) { return false; } else if (!Arrays.equals(columnIndexes, otherKey.columnIndexes)) { return false; } else if (!Arrays.equals(columnNames, otherKey.columnNames)) { return false; } else if ((autoGeneratedKeys == null && otherKey.autoGeneratedKeys != null) || (autoGeneratedKeys != null && !autoGeneratedKeys.equals(otherKey.autoGeneratedKeys))) { return false; } else if ((resultSetHoldability == null && otherKey.resultSetHoldability != null) || (resultSetHoldability != null && !resultSetHoldability.equals(otherKey.resultSetHoldability))) { return false; } return true; } } private static final class StatementTracker { private final PreparedStatement statement; private int usageCount; /** * Constructor StatementTracker creates a new StatementTracker instance. * * @param stmt * of type PreparedStatement */ private StatementTracker(PreparedStatement stmt) { this.statement = stmt; this.usageCount = 1; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy