bitronix.tm.resource.jdbc.LruStatementCache Maven / Gradle / Ivy
/*
* 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;
}
}
}