情景介绍
因为入职某国企以后,做一个平台的二次开发,该平台是老外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 hutao_2017@aliyun.com
* @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 hutao_2017@aliyun.com
* @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 hutao_2017@aliyun.com
* @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 hutao_2017@aliyun.com
* @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 hutao_2017@aliyun.com
* @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 hutao_2017@aliyun.com
* @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();
}
}