單獨使用MyBatis時,mybatis對數據庫如何進行管理?

情景介紹

因爲入職某國企以後,做一個平臺的二次開發,該平臺是老外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();
		}
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章