情景介紹
因爲入職某國企以後,做一個平臺的二次開發,該平臺是老外20年來前開發的一個平臺,一直維護至今。該平臺存儲數據,採用的是SVN存儲成一個個XML文件。其性能就不吐槽了,數據一上萬,那性能跟屎一樣。
因爲部分數據用原生平臺的存儲方式,已經無法滿足了,因此決定引入數據庫,當然,此前其他的項目也引入過數據庫,不過那都是相當的慘烈,反正就是分分鐘數據庫就蹦了。
首先我們看下面這段代碼
編寫了一個工具類,一開始用起來沒啥問題,可能有小夥伴問,你這寫法有問題啊,你這個每次都要獲取資源文件,然後構建會話工廠,然後在返回會話。好消耗性能啊。要怪就怪平臺本身的性能實在太差了了,哪怕我在怎麼慢,也遠遠比平臺快啊。
/**
* @Description 獲取不唯一的SqlSession,每一次調用,拿到回話都是不同的(自動提交事務)
* @author hutao
* @date 2020年1月15日
*/
public static SqlSession getSqlSession() {
InputStream inputStream = null;
SqlSessionFactory sqlSessionFactory =null;
try {
inputStream = MybatisSqlSession.class.getResourceAsStream(sourcePath);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//自動提交
SqlSession openSession = sqlSessionFactory.openSession(true);
return openSession;
} catch (Exception e) {
logger.error("獲取數據庫連接會話失敗,失敗原因:{}",e);
e.printStackTrace();
}
return null;
}
隨着開發的功能越來越多,訪問數據庫越來越平凡,我發現連續點擊的話,數據庫連接就一直暴漲。
查看mysql數據庫連接數
show PROCESSLIST;
**接着我做了第二版優化,單例模式+volatile **
OK,完美解決了連接數的問題,可是問題特碼的又來了,我怎麼保證我這個會話能夠一直開啓,不會被數據庫單方面的關閉?於是我想,每次拿會話的時候判斷下,會話是不是能用?於是我想用如下方法做個判斷,如果被關閉了,我就重新打開,但是顯然沒有達到我要的效果,因爲我在數據庫裏面強制關閉連接,程序裏面拿到的任然是打開的。
uniqueSqlSession.getConnection().isClosed();
private volatile static SqlSession uniqueSqlSession = null;
/**
* @Description 獲取唯一 SqlSession回話,使用此方法,請保證數據庫不會單方便關閉連接
* @author hutao
* @date 2020年1月15日
*/
public static SqlSession getUniqueSqlSession(){
if (uniqueSqlSession == null){
synchronized (MybatisSqlSession.class){
if (uniqueSqlSession == null){
InputStream inputStream = null;
SqlSessionFactory sqlSessionFactory =null;
try {
inputStream = MybatisSqlSession.class.getResourceAsStream(sourcePath);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//自動提交
uniqueSqlSession = sqlSessionFactory.openSession(true);
} catch (Exception e) {
logger.error("獲取數據庫連接會話失敗,失敗原因:{}",e);
e.printStackTrace();
}
}
}
}
return uniqueSqlSession;
}
後來實在是想,算了,反正這個項目百分之98以上都用不到數據庫,浪費資源就浪費吧,那就去線程池去等着吧,反正在慢也比原生平臺快。
隨着我把數據庫引進來以後,項目成員,用數據庫越來越多,使用越來越頻繁,知道5月底,才發現等待時間越來越長,看來還是得要好好的研究下了,因爲這時候,使用數據庫查詢居然比原生平臺那屎一樣的性能還慢了,因爲全在連接池排隊去了,真是排隊3分鐘,查詢5毫秒,能不慢嗎?看來該是解讀下mybatis的連接池原理了。
還記得我們配置mybatis-config.xml嗎?
MyBatis把數據源DataSource分爲三種:
- UNPOOLED 不使用連接池的數據源
- POOLED 使用連接池的數據源
- JNDI 使用JNDI實現的數據源
現在就讓我們來一起探究mybatis的POOLED吧
1、首先我們先看看反編譯後的PoolState
- PooledDataSource將jConnection對象包裹成PooledConnection對象放到了PoolState類型的容器中維護;
- MyBatis將連接池中的PooledConnection分爲兩種狀態: 空閒狀態(idle)和活動狀態(active),
- idleConnections:空閒(idle)狀態PooledConnection對象被放置到此集合中,表示當前閒置的沒有被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從此集合中取PooledConnection對象。當用完一個java.sql.Connection對象時,MyBatis會將其包裹成PooledConnection對象放到此集合中。
- activeConnections:活動(active)狀態的PooledConnection對象被放置到名爲activeConnections的ArrayList中,表示當前正在被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從idleConnections集合中取PooledConnection對象,如果沒有,則看此集合是否已滿,如果未滿,PooledDataSource會創建出一個PooledConnection,添加到此集合中,並返回。
2、接着我們來看看PooledDataSource的getConnection方法
我們發現這個方法調用了
private PooledConnection popConnection(String username, String password) throws SQLException {}
代碼量有點多,讓我們一步一步的消化。
3、popConnection
- 3.1、如果空閒idleConnections裏面有,我們就從空閒裏面取,
- 3.2、如果活動activeConnections數小於最大的活動限制,就創建一個
- 3.3、如果活動activeConnections數已滿,則判斷最先進入連接池的PooledConnection對象,判斷是否超過限制時間,如果超過限制時間,則聲明爲過期的會話,並且使用PoolConnection內部的realConnection重新生成一個PooledConnection。
- 3.4、如果連接沒有過期,則等待。
- 3.5、如果獲取PooledConnection成功,則更新其信息,並添加到activeConnections中
分析完mybatis的線程池,我們就開始我們的工作吧,編寫一個mybatis的配置工具類(雖然最後發現好像對我寫配置類也沒暖用,就當研究了一次源碼把)。
我們需要做如下幾個思考:
1、保證會話工廠有且僅有一個,會話存在多個,
2、保證線程之間的會話互不影響
3、保證GC能夠回收
import java.io.InputStream;
import java.sql.SQLException;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description mybatis會話配置
* @author hutao
* @mail [email protected]
* @date 2020年6月6日
*/
public class MybatisConfig {
private static Logger logger = LoggerFactory.getLogger(MybatisConfig.class);
private static String sourcePath = "/com/sunwise/cascoalm/source/mybatis-config.xml";
/**
* 整個項目只需要一個數據庫會話工廠
*/
private static SqlSessionFactory uniqueSqlSessionFactory = null;
/**
* 創建本地線程變量,爲每一個線程獨立管理一個session對象 每一個線程只有且僅有單獨且唯一的一個session對象
* 加上線程變量對session進行管理,可以保證線程安全,避免多實例同時調用同一個session對象
* 每一個線程都會new一個線程變量,從而分配到自己的session對象
*/
private static ThreadLocal<SqlSession> threadlocal = new ThreadLocal<SqlSession>();
/**
* @Description 獲取唯一數據庫會話工廠
* @author hutao
* @mail [email protected]
* @date 2020年6月6日
*/
private static SqlSessionFactory getSqlSessionFactory(){
if (uniqueSqlSessionFactory == null){
synchronized (MybatisSqlSession.class){
if (uniqueSqlSessionFactory == null){
InputStream inputStream = null;
try {
inputStream = MybatisSqlSession.class.getResourceAsStream(sourcePath);
uniqueSqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
} catch (Exception e) {
logger.error("獲取數據庫連接會話工廠失敗,失敗原因:{}",e);
e.printStackTrace();
}
}
}
}
return uniqueSqlSessionFactory;
}
/**
* @Description 獲取sqlSesion會話(優先從線程變量中取session對象)
* @author hutao
* @throws SQLException
* @mail [email protected]
* @param boolean auto false時需要自己手動提交事務
* @date 2020年6月6日
*/
public static SqlSession openSqlSession(Boolean auto) {
SqlSession session = threadlocal.get();
if(session==null){
newSession(auto);
session = threadlocal.get();
}
return session;
}
/**
* @Description 新建session會話,並把session放在線程變量中
* @author hutao
* @throws SQLException
* @mail [email protected]
* @date 2020年6月6日
*/
private static void newSession(Boolean auto) {
getSqlSessionFactory();
SqlSession session = null;
if(auto==null) {
session = uniqueSqlSessionFactory.openSession(true);
}else {
session = uniqueSqlSessionFactory.openSession(auto);
}
threadlocal.set(session);
}
/**
* @Description 關閉SqlSession,GC回收
* @author hutao
* @throws SQLException
* @mail [email protected]
* @date 2020年6月6日
*/
public static void closeSqlSession(){
SqlSession sqlSession = threadlocal.get();
//如果SqlSession對象非空
if(sqlSession != null){
sqlSession.close();
//分離線程和和會話關係,讓JVM回收
threadlocal.remove();
}
}
}
使用示例
private static Map<String, List<AlmCascoSystem>> system = new ConcurrentHashMap<>();
/**
* Description: 獲取系統配置常量
* @author hutao
* @mail [email protected]
* @date 2020年6月6日
*/
@Override
public List<AlmCascoSystem> getAlmCascoSystem(String keyName)throws Exception {
if(system.get(keyName) != null) {
return system.get(keyName);
}
ProjectMapper projectMapper = MybatisConfig.openSqlSession(true).getMapper(ProjectMapper.class);
try {
List<AlmCascoSystem> listKeyName = projectMapper.queryAlmCascoSystem(keyName);
system.put(keyName, listKeyName);
return system.get(keyName);
} catch (Exception e) {
throw e;
}finally {
MybatisConfig.closeSqlSession();
}
}