Don't talk at night 2022-02-13 07:29:01 阅读数:138
Mybatis It's a semi-automatic ORM frame , It's at present in China Java web Mainstream of development ORM frame , Therefore, as a developer, it is very necessary to master its implementation principle , In order to better solve the problems encountered in our development ; meanwhile ,Mybatis The architecture and source code are also very elegant , A large number of design patterns are used to realize decoupling and high scalability , So the design idea , It is also very necessary for us to understand and master .(PS: This series is based on 3.5.0 Version analysis )
Mybatsi Compared with Spring The source code is much simpler in terms of architecture and Implementation , All its code is in one project , There are many subcontracts under this project , Each package has a clear division of labor :
Although there are so many modules , In fact, it only needs to be divided into three layers :
So after layering , Is it clear , The basic support layer is the encapsulation of some general components , Like a journal 、 cache 、 Reflection 、 Data sources and so on , These modules support the implementation of core business logic , And if necessary, we can use it directly in our project , Like the reflection module is right JDK The reflection is encapsulated , Make it easier to use ; The core processing layer is Mybatis The realization of the core business of , Through the bottom support module , The configuration file and SQL analysis 、 Parameter mapping and binding 、SQL The mapping of execution and return results, the execution of extensions, and so on ; Finally, the interface layer is the service provided externally , We use Mybatis You only need to operate through this interface , There is no need to pay attention to the underlying implementation . The benefits of this layering go without saying , Make our code more concise and readable , At the same time, maintainability and scalability are also greatly improved , In addition, from the whole architecture design, we can see the embodiment of a design pattern —— Facade mode , Because the design idea of facade pattern is Provide a unified interface to the outside world , Shield the complexity of internal system implementation , So that users can easily use all functions without paying attention to the internal implementation , The architecture design here adopts such an idea . The lines , Let's see if other open source frameworks are designed like this ?
In understanding Mybatis After the macro architecture design , The following is a detailed analysis of the source code , First, let's look at some key basic support modules :
Mybatis There is no log function in itself , Instead, third-party logs are introduced , But third party logs have their own log Level ,Mybatis What needs to be solved is how to be compatible with these log components . How compatible ?Mybatis Used Adapter pattern To solve , stay logging A unified log interface is provided under the module Log Interface :
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
You can see that all log levels are uniformly defined in this interface , The imported third-party log component only needs to implement this interface , Invokes the corresponding components of each component in each level interface. API that will do . From the class diagram below, we can see Mybatis Which three-party log components are supported :
See if you have any questions , How are these third-party log components loaded ? What is the loading order ? Is it instantiated where it needs to be used ? Of course not. ,Mybatis Here is another design pattern —— Factory mode . There is a class under the log module LogFactory, The loading of logs is realized by this class , This class decouples the instantiation of logs and the use of logs :
public final class LogFactory {
public static final String MARKER = "MYBATIS";
// The construction method of the selected third-party log component adapter
private static Constructor<? extends Log> logConstructor;
// Automatic scan log implementation , And the loading priority of the third-party log plug-in is as follows :slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
// Execute method when constructor is not empty
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
// By the appointed log Class to initialize the constructor
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
Through the above code, we can clearly see the loading order of logs , And as long as any log component is successfully loaded , Other logging components will not be loaded .
After the log is loaded , Naturally, we should think about where we need to print logs ?Mybatis It's right in itself JDK Native JDBC Packaging and reinforcement , Therefore, the log should be printed in the following key places :
The question is how to gracefully enhance these methods ?Mybatis Used A dynamic proxy To achieve . Under the log module JDBC Package is the implementation of proxy class , Let's take a look at the class diagram first :
Know what you know , Seeing these class names, we should be able to understand the functions of these classes , They are the original JDBC API The enhancement of , When calling related methods , First of all, you will enter these proxy classes invoke Method inside , In order of execution , The first thing to enter the call must be ConnectionLogger:
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
// Real connection objects
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
@Override
// Enhancements to connectivity
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
// If from Obeject The method of inheritance directly ignores
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// If it's a call to prepareStatement、prepareCall、createStatement Methods , Print what to do sql sentence
// And back to prepareStatement Proxy object of , Give Way prepareStatement I also have the ability to log , Printing parameters
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);// Print sql sentence
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);// Create proxy object
return stmt;
} else if ("prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);// Print sql sentence
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);// Create proxy object
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);// Create proxy object
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{
Connection.class}, handler);
}
public Connection getConnection() {
return connection;
}
}
from invoke In the method, we can see the main right Connection Of prepareStatement、prepareCall、createStatement The method is enhanced , Print the log and create the corresponding proxy class to return . The implementation principle of several other classes is the same , No more details here .
But there is a problem , Several other classes are called after the connection is created , Therefore, the corresponding proxy class is created by the proxy class in the previous stage , that ConnectionLogger Where was it created ? Naturally, when getting a connection , It's the connection phase when we get the business code ,Mybatis The execution phase is encapsulated one by one Excutor actuator , Detailed code analysis later .
All data sources need to implement JDK Of DataSource Interface ,Mybatis I have implemented the data source interface , It also supports third-party data sources . Here's a look at Mybatis Internal implementation , Also, let's start with a class diagram :
We can see from the picture DataSource The initialization of is also through Factory mode Realized , And it itself provides three data sources :
The last one is not analyzed here .UnpooledDataSource Is an ordinary data source , The basic data source interface is realized ; and PooledDataSource Is based on UnpooledDataSource Realized , Only the connection pool function is provided on this . In addition, we need to pay attention to PooledConnection, This class is the connection object stored in the connection pool , But it is not a real connection object , Just hold the reference of the real connection , And it enhances the real connection proxy class , The following mainly analyzes the implementation principle of connection pool .
So first of all PooledConnection It encapsulates something :
class PooledConnection implements InvocationHandler {
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] {
Connection.class };
private final int hashCode;
// Record the data source object where the current connection is located , This connection was created by this data source , After closing, you also return to this data source ;
private final PooledDataSource dataSource;
// Real connection objects
private final Connection realConnection;
// Connected proxy objects
private final Connection proxyConnection;
// The timestamp of the connection taken from the data source
private long checkoutTimestamp;
// Timestamp of connection creation
private long createdTimestamp;
// Timestamp of the last time the connection was used
private long lastUsedTimestamp;
// According to the database url、 user name 、 Generate a password hash value , Uniquely identifies a connection pool
private int connectionTypeCode;
// Is the connection valid
private boolean valid;
/* * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in * * @param connection - the connection that is to be presented as a pooled connection * @param dataSource - the dataSource that the connection is from */
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
...... Omit
/* * This method is specifically used to enhance the database connect object , Check whether the connection is valid before use , Recycle the connection when closed * */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
// If the connection is called close Method , Not really closed , Instead, recycle to the connection pool
dataSource.pushConnection(this);// adopt pooled Data source for recycling
return null;
} else {
try {
// Check whether the current connection is valid before use
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();//
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
}
There are detailed comments on properties and methods , Main concern realConnection Real connected references and invoke Method enhancement . Then let's look at the implementation of connection pool , This class contains many properties :
private final PoolState state = new PoolState(this);
// The data source really used to create the connection
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
// Maximum number of active connections
protected int poolMaximumActiveConnections = 10;
// Maximum number of idle connections
protected int poolMaximumIdleConnections = 5;
// Maximum checkout Duration ( Maximum use time )
protected int poolMaximumCheckoutTime = 20000;
// Unable to get a connection is the maximum waiting time
protected int poolTimeToWait = 20000;
// A maximum of several invalid connections are allowed
protected int poolMaximumLocalBadConnectionTolerance = 3;
// Test whether the connection is valid sql sentence
protected String poolPingQuery = "NO PING QUERY SET";
// Whether to allow test connection
protected boolean poolPingEnabled;
// Configure for a period of time , When the connection is not used during this time , To test whether the connection is valid
protected int poolPingConnectionsNotUsedFor;
// According to the database url、 user name 、 Generate a password hash value , Uniquely identifies a connection pool , All connections generated by this connection pool will carry this value
private int expectedConnectionTypeCode;
I believe most of the above attributes will not be unfamiliar to readers , It should be configured during development . There is a key attribute PoolState, This is the object that mainly saves Free connection and Active connection , That is, the connection pool is used to manage resources , It contains the following properties :
protected PooledDataSource dataSource;
// Free connection pool resource collection
protected final List<PooledConnection> idleConnections = new ArrayList<>();
// Active connection pool resource collection
protected final List<PooledConnection> activeConnections = new ArrayList<>();
// Number of requests
protected long requestCount = 0;
// Cumulative time to get connected
protected long accumulatedRequestTime = 0;
// Cumulative connection time . Remove from connection to return , Calculate the time of one use ;
protected long accumulatedCheckoutTime = 0;
// Number of times the connection timed out
protected long claimedOverdueConnectionCount = 0;
// Cumulative overtime
protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
// Accumulated waiting time
protected long accumulatedWaitTime = 0;
// Number of waits
protected long hadToWaitCount = 0;
// Number of invalid connections
protected long badConnectionCount = 0;
Knowing these key attributes , Let's see how to get connections from the connection pool , stay PooledDataSource There is one of them. popConnection For getting connections :
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();// Record the start timestamp of the attempt to get the connection
int localBadConnectionCount = 0;// Initialize the number of times to get an invalid connection
while (conn == null) {
synchronized (state) {
// Get connections must be synchronous
if (!state.idleConnections.isEmpty()) {
// Check for free connections
// Pool has available connection
// Have free connection to use directly
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// No free connection
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Determine whether the number of active connection pools is greater than the maximum number of connections
// No new connections can be created
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// If it's equal to the maximum number of connections , Can't create a new connection
// Get the first connection created
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Check if it has been used for more than the maximum time
// If the timeout , Make statistics on the information of overtime connection
state.claimedOverdueConnectionCount++;// Number of timeout connections +1
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;// The cumulative timeout increases
state.accumulatedCheckoutTime += longestCheckoutTime;// Cumulative use of connection time increases
state.activeConnections.remove(oldestActiveConnection);// Remove... From the active queue
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
// If the timeout connection is not committed , Then manually roll back
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
// If an exception occurs, just log it
/* Just log a message for debug and continue to execute the following statement like nothing happend. Wrap the bad connection with a new PooledConnection, this will help to not intterupt current executing thread and give current thread a chance to join the next competion for another valid/good database connection. At the end of this loop, bad {@link @conn} will be set as null. */
log.debug("Bad connection. Could not roll back");
}
}
// Create a new connection in the connection pool , Note that for databases , No new connection created ;
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
// Let old connections fail
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// No free connection , The first connection created did not fail , Unable to create new connection , Can only block
try {
if (!countedWait) {
state.hadToWaitCount++;// The cumulative waiting times of the connection pool plus 1
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);// Block wait for a specified time
state.accumulatedWaitTime += System.currentTimeMillis() - wt;// The cumulative waiting time increases
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
// Get connected successfully , To test whether the connection is valid , And update Statistics
// ping to server and check the connection is valid or not
if (conn.isValid()) {
// Check whether the connection is valid
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();// If there is a legacy of history , Roll back
}
// Connection pool related statistics update
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
// If the connection is invalid
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;// Cumulative number of invalid connections obtained +1
localBadConnectionCount++;// The current number of invalid connections obtained +1
conn = null;
// Get invalid connection , But if it doesn't exceed the number of retries , Allow another attempt to get a connection , Otherwise, throw an exception
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
The logic here is relatively complex , I summarized the whole steps and drew a picture to help understand : Loop get connection , First, judge whether there is still Free connection , If there is , Direct use , And delete an idle connection ; If it doesn't exist , Give priority to Whether the maximum number of active connections has been reached . If not, create a new connection directly ; If the maximum number of active connections has been reached , From Active connection pool Take out the earliest connection , Judge If the timeout . If there is no timeout , Call wait Methods block ; If the timeout , Then the timeout connection information is counted , And connect according to the timeout Real connection Create a new connection , At the same time, let the old connection fail . After the above steps , If you get a connection , You also need to determine whether the connection is valid , A valid connection needs to roll back previously uncommitted transactions and add them to the Active connection pool , If the connection is invalid, statistics will be made and whether the number of retries has been exceeded , If not, continue to cycle to get the connection next time , Otherwise, throw an exception . After the loop is completed, return to the obtained connection .
Ordinary connections are closed directly , Recreate when needed , The connection pool needs to recycle the connections to the pool for reuse , Avoid duplicate connections and improve efficiency , stay PooledDataSource Medium pushConnection It is used to recycle the connection :
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
// Recycle connections must be synchronous
state.activeConnections.remove(conn);// Remove this connection from the active connection pool
if (conn.isValid()) {
// Determine whether the idle connection pool resources have reached the upper limit
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
// Not up to the limit , To recycle
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();// If there are still transactions not committed , Roll back
}
// Based on this connection , Create a new connection resource , And refresh the connection status
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
// Old connection failure
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
// Wake up other blocked threads
state.notifyAll();
} else {
// If the idle connection pool has reached the upper limit , Turn the connection real off
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
// Close the real database connection
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
// Set connection object to invalid
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
The logic of recycling connections is relatively simple , But there are still a few things to pay attention to : First of all, from the Active connection pool Remove the connection , And then determine Whether it is a valid connection as well as Whether the free connection pool still has a location , If it is a valid connection and the free connection pool still has a location , You need to recycle based on the current connection Real connection And create a new connection and put it into the idle connection , Then wake up the waiting thread ; If not, just close Real connection . Both branches need to roll back the uncommitted transactions in the recycled connection and invalidate the connection . If it is an invalid connection, you only need to record the number of invalid connections .
That's all Mybatis Implementation principle of data source and connection pool , Among them, pooling technology is very important .
Mybatis Yes First level cache and Second level cache , The first level cache is SqlSession Grade , Can only exist in the same SqlSession In the life cycle ; The L2 cache is cross cache SqlSession, With namespace Unit . But actually Mybatis The L2 cache is very weak , There may be dirty reading , Generally not used .
but Mybatis A lot of extensions have been made to the cache , Provides protection against cache breakdown 、 Cache clear policy 、 serialize 、 Clear regularly 、 Log and other functions , The design is very elegant , So the first mock exam of this module is the design idea. . Let's first look at the structure of the package :
As can be seen from the picture above ,Mybatis Provides a unified cache interface ,impl and decorators The package is full of its implementation classes , From the name of the package, we can think of the cache. Here is another design pattern —— Decorator mode , Use this mode dynamic You have to add a function to the cache . The real realization is impl package Under the PerpetualCache, adopt HashMap To cache data ( Will there be concurrency security problems ?),key yes CacheKey object ,value Is cached data , Why? key yes CacheKey object , Instead of a string ? Readers can think about , How to read the cache error is not determined , This class was last analyzed . and decorators Under the package are decorators with enhanced functions , This is mainly to see BlockingCache How to prevent Cache breakdown Of .
public class BlockingCache implements Cache {
// Timeout length of blocking
private long timeout;
// The underlying object being decorated , It's usually PerpetualCache
private final Cache delegate;
// Lock object set , Granularity to key value
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);// according to key Get lock object , Obtain the lock and lock it successfully , Failed to acquire lock, blocked for a period of time and try again
Object value = delegate.getObject(key);
if (value != null) {
// Successful data acquisition , To release the lock
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;
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();// Create a lock
ReentrantLock previous = locks.putIfAbsent(key, lock);// Add a new lock to locks Collection , If the addition is successful, use the new lock , If the addition fails, use locks Lock in collection
return previous == null ? lock : previous;
}
// according to key Get lock object , Obtain the lock and lock it successfully , Failed to acquire lock, blocked for a period of time and try again
private void acquireLock(Object key) {
// Get lock object
Lock lock = getLockForKey(key);
if (timeout > 0) {
// Use a lock with a timeout
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
// If the timeout occurs, an exception is thrown
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 {
// Use a lock without a timeout
lock.lock();
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
Calling getObject When getting data , First call acquireLock according to key Get the lock , If you get the lock , From PerpetualCache Get data in cache , If not, go to the database to query the data , After returning the result, add it to the cache and release the lock , Note that when you query data in the database, you are based on key Locked , So the same key Only one thread will arrive at the database query , It won't show up Cache breakdown The problem of , This idea can also be used in our project .
That's all Mybatis The idea of solving cache breakdown , Another look at a decorator SynchronizedCache, Provide the function of synchronization , The decorator is adding and deleting the cache API Plus synchronized keyword , This decorator is used to prevent concurrency security problems in the L2 cache , There is no concurrency security problem at all in L1 cache . The rest of the decorators will not be repeated here , Interested readers can make their own analysis .
because Mybatis There is a dynamic in SQL, So cached key You can't just use a string to represent , So pass CacheKey To encapsulate all the factors that may affect the cache , So what factors affect caching ?
And in the CacheKey There are the following properties in :
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier; // Participate in hash Calculated multiplier
private int hashcode; //CacheKey Of hash value , stay update The real-time operation of the function
private long checksum; // The checksum ,hash It's worth it and
private int count; //updateList Number of elements in
private List<Object> updateList; // The elements in this collection determine two CacheKey Whether it is equal or not
among updateList It is used to store all factors that may affect the cache , The other values are calculated based on the objects in the attribute , Every time the structure CacheKey Object will be called update Method :
public void update(Object object) {
// obtain object Of hash value
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// to update count、checksum as well as hashcode Value
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
// Add objects to updateList in
updateList.add(object);
}
And judge two CacheKey Whether the objects are the same is through equals Method :
public boolean equals(Object object) {
if (this == object) {
// Compare whether it is the same object
return true;
}
if (!(object instanceof CacheKey)) {
// Whether the type is the same
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
//hashcode Are they the same?
return false;
}
if (checksum != cacheKey.checksum) {
//checksum Are they the same?
return false;
}
if (count != cacheKey.count) {
//count Are they the same?
return false;
}
// None of the above is the same , In order to compare updateList Of the elements of hash Is the value consistent
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;
}
As you can see, the method of comparing equality here is very strict , And it's very efficient , We rewrite... In the project equals Method can also refer to the implementation of this method .
Reflection is Mybatis Top priority , By reflection Mybatis In order to realize the instantiation of objects and the assignment of attributes , also Mybatis The reflection is right JDK Encapsulation and enhancement of , Make it easier to use , Higher performance . The key categories are as follows :
Because this module is only for JDK Encapsulation , Although there are many codes and classes , But it's not very complicated , I won't go into details here .
This article explains Mybatis The four core modules , You can see that a lot of design patterns are used to make the code elegant and concise , High readability , At the same time, it is easy to expand , This is also the first thing we need to consider when doing the project , The code is for people to read , How to reduce the cost of reading code , Improve the quality of your code , Reduce BUG The number of , Only by learning more excellent code design ideas can we improve our own level .
copyright:author[Don't talk at night],Please bring the original link to reprint, thank you. https://en.javamana.com/2022/02/202202130728422821.html