在Java工程項目中,我們常會用到Mybatis
框架對數據庫中的數據進行增刪查改,其原理就是對 JDBC
做了一層封裝,並優化數據源的連接。
我們先來回顧下 JDBC
操作數據庫的過程。
JDBC
操作數據庫
JDBC
操作數據庫的時候需要指定 連接類型、加載驅動、建立連接、最終執行 SQL
語句,代碼如下:
public static final String url = "jdbc:mysql://127.0.0.1/somedb";
public static final String name = "com.mysql.jdbc.Driver";
public static final String user = "root";
public static final String password = "root";
public Connection conn = null;
public PreparedStatement pst = null;
public DBHelper(String sql) {
try {
//指定連接類型
Class.forName(name);
//建立連接
conn = DriverManager.getConnection(url, user, password);
//準備執行語句
pst = conn.prepareStatement(sql);
} catch (Exception e) {
e.printStackTrace();
}
}
public void close() {
try {
this.conn.close();
this.pst.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
一個SQL
的執行,如果使用JDBC
進行處理,需要經過 加載驅動、建立連接、再執行SQL
的一個過程,當下一個SQL
到來的時候,還需要進行一次這個流程,這就造成不必要的性能損失,而且隨着用戶操作的逐漸增多,每次都和數據庫建立連接對數據庫本身來說也是一種壓力。
爲了減少這種不必要的消耗,可以對數據的操作進行拆分。在Mybatis
中,數據庫連接的建立和管理的部分叫做數據庫連接池。
Mybatis
數據源DateSource
的分類
- UNPOOLED 不使用連接池的數據源
- POOLED 使用連接池的數據源
- JNDI 使用JNDI實現的數據
-
UNPOOLED
UNPOOLED
不使用連接池的數據源,當dateSource
的type屬性被配置成了UNPOOLED
,MyBatis
首先會實例化一個UnpooledDataSourceFactory
工廠實例,然後通過.getDataSource()
方法返回一個UnpooledDataSource
實例對象引用,我們假定爲dataSource
。 使用
UnpooledDataSource
的getConnection()
,每調用一次就會產生一個新的Connection
實例對象。UnPooledDataSource
的getConnection()
方法實現如下:
public class UnpooledDataSource implements DataSource {
private ClassLoader driverClassLoader;
private Properties driverProperties;
private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
private String driver;
private String url;
private String username;
private String password;
private Boolean autoCommit;
private Integer defaultTransactionIsolationLevel;
public UnpooledDataSource() {
}
public UnpooledDataSource(String driver, String url, String username, String password){
this.driver = driver;
this.url = url;
this.username = username;
this.password = password;
}
public Connection getConnection() throws SQLException {
return this.doGetConnection(this.username, this.password);
}
private Connection doGetConnection(String username, String password) throws SQLException {
Properties props = new Properties();
if(this.driverProperties != null) {
props.putAll(this.driverProperties);
}
if(username != null) {
props.setProperty("user", username);
}
if(password != null) {
props.setProperty("password", password);
}
return this.doGetConnection(props);
}
private Connection doGetConnection(Properties properties) throws SQLException {
this.initializeDriver();
Connection connection = DriverManager.getConnection(this.url, properties);
this.configureConnection(connection);
return connection;
}
}
如上代碼所示,UnpooledDataSource
會做以下事情:
-
初始化驅動: 判斷driver驅動是否已經加載到內存中,如果還沒有加載,則會動態地加載
driver
類,並實例化一個Driver
對象,使用DriverManager.registerDriver()
方法將其註冊到內存中,以供後續使用。 -
創建Connection對象: 使用
DriverManager.getConnection()
方法創建連接。 -
配置Connection對象: 設置是否自動提交
autoCommit
和隔離級別isolationLevel
。 -
返回Connection對象
從上述的代碼中可以看到,我們每調用一次
getConnection()
方法,都會通過DriverManager.getConnection()
返回新的java.sql.Connection
實例,所以沒有連接池。
- ###
POOLED
數據源 連接池
PooledDataSource: 將java.sql.Connection
對象包裹成PooledConnection
對象放到了PoolState
類型的容器中維護。 MyBatis
將連接池中的PooledConnection
分爲兩種狀態: 空閒狀態(idle)和活動狀態(active),這兩種狀態的PooledConnection
對象分別被存儲到PoolState
容器內的**idleConnections
和activeConnections
**兩個List集合中:
idleConnections: 空閒(idle)狀態PooledConnection
對象被放置到此集合中,表示當前閒置的沒有被使用的PooledConnection
集合,調用PooledDataSource
的getConnection()
方法時,會優先從此集合中取PooledConnection
對象。當用完一個java.sql.Connection對象時,MyBatis
會將其包裹成PooledConnection
對象放到此集合中。
activeConnections: 活動(active)狀態的PooledConnection
對象被放置到名爲activeConnections
的ArrayList
中,表示當前正在被使用的PooledConnection
集合,調用PooledDataSource
的getConnection()
方法時,會優先從idleConnections
集合中取PooledConnection
對象,如果沒有,則看此集合是否已滿,如果未滿,PooledDataSource
會創建出一個PooledConnection
,添加到此集合中,並返回
現在讓我們看一下popConnection()
方法到底做了什麼:
-
先看是否有空閒(idle)狀態下的
PooledConnection
對象,如果有,就直接返回一個可用的PooledConnection
對象;否則進行第2步。 -
查看活動狀態的
PooledConnection
池activeConnections
是否已滿;如果沒有滿,則創建一個新的PooledConnection
對象,然後放到activeConnections
池中,然後返回此PooledConnection
對象;否則進行第三步; -
看最先進入
activeConnections
池中的PooledConnection
對象是否已經過期:如果已經過期,從activeConnections
池中移除此對象,然後創建一個新的PooledConnection
對象,添加到activeConnections
中,然後將此對象返回;否則進行第4步。 -
線程等待,循環2步
/*
* 傳遞一個用戶名和密碼,從連接池中返回可用的PooledConnection
*/
private PooledConnection popConnection(String username, String password) throws SQLException
{
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
while (conn == null)
{
synchronized (state)
{
if (state.idleConnections.size() > 0)
{
// 連接池中有空閒連接,取出第一個
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled())
{
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
}
else
{
// 連接池中沒有空閒連接,則取當前正在使用的連接數小於最大限定值,
if (state.activeConnections.size() < poolMaximumActiveConnections)
{
// 創建一個新的connection對象
conn = new PooledConnection(dataSource.getConnection(), this);
@SuppressWarnings("unused")
//used in logging, if enabled
Connection realConn = conn.getRealConnection();
if (log.isDebugEnabled())
{
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
}
else
{
// Cannot create new connection 當活動連接池已滿,不能創建時,取出活動連接池的第一個,即最先進入連接池的PooledConnection對象
// 計算它的校驗時間,如果校驗時間大於連接池規定的最大校驗時間,則認爲它已經過期了,利用這個PoolConnection內部的realConnection重新生成一個PooledConnection
//
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime)
{
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit())
{
oldestActiveConnection.getRealConnection().rollback();
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
oldestActiveConnection.invalidate();
if (log.isDebugEnabled())
{
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
}
else
{
//如果不能釋放,則必須等待有
// Must wait
try
{
if (!countedWait)
{
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled())
{
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
}
catch (InterruptedException e)
{
break;
}
}
}
}
//如果獲取PooledConnection成功,則更新其信息
if (conn != null)
{
if (conn.isValid())
{
if (!conn.getRealConnection().getAutoCommit())
{
conn.getRealConnection().rollback();
}
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 (log.isDebugEnabled())
{
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + 3))
{
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;
}
java.sql.Connection
對象的回收
當我們的程序中使用完Connection
對象時,如果不使用數據庫連接池,我們一般會調用 connection.close()
方法,關閉connection
連接,釋放資源
調用過close()
方法的Connection
對象所持有的資源會被全部釋放掉,Connection
對象也就不能再使用。那麼,如果我們使用了連接池,我們在用完了Connection
對象時,需要將它放在連接池中,該怎樣做呢?
可能大家第一個在腦海裏閃現出來的想法就是:我在應該調用con.close()
方法的時候,不調用close()
方法,將其換成將Connection
對象放到連接池容器中的代碼!
怎樣實現Connection
對象調用了close()
方法,而實際是將其添加到連接池中
這是要使用代理模式,爲真正的Connection
對象創建一個代理對象,代理對象所有的方法都是調用相應的真正Connection
對象的方法實現。當代理對象執行close()
方法時,要特殊處理,不調用真正Connection
對象的close()
方法,而是將Connection
對象添加到連接池中。
MyBatis
的PooledDataSource
的PoolState
內部維護的對象是PooledConnection
類型的對象,而PooledConnection
則是對真正的數據庫連接java.sql.Connection
實例對象的包裹器。
PooledConnection
對象內持有一個真正的數據庫連接java.sql.Connection
實例對象和一個java.sql.Connection
的代理:
其源碼如下:
class PooledConnection implements InvocationHandler {
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class[]{Connection.class};
private int hashCode = 0;
private PooledDataSource dataSource;
private Connection realConnection;
private Connection proxyConnection;
private long checkoutTimestamp;
private long createdTimestamp;
private long lastUsedTimestamp;
private int connectionTypeCode;
private boolean valid;
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);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//當close時候,會回收 connection , 不會真正的close
if("close".hashCode() == methodName.hashCode() && "close".equals(methodName)) {
this.dataSource.pushConnection(this);
return null;
} else {
try {
if(!Object.class.equals(method.getDeclaringClass())) {
this.checkConnection();
}
return method.invoke(this.realConnection, args);
} catch (Throwable var6) {
throw ExceptionUtil.unwrapThrowable(var6);
}
}
}
}