前几天,客户反馈说使用系统某一个模块十分缓慢,有时候还会崩溃;
客户机器数据量大概在1W5左右,加大请求超时时间后,虽然不会崩溃,但是依然慢的吓人,1W5的数据,居然需要30~40多秒钟;
系统是使用SSH框架开发,因为关联关系比较多,所以使用Hibernate进行管理数据库,是最合适的;
问题查找:
1.出现这个问题,还得说到以前的开发人员,当时在完成项目的阶段中,在设计表的时候,以及表之间的关联关系的时候,都将懒加载取消了
所以,这也是一个造成性能底下的问题,表格之间关联越多,在查询的时候,发出的select语句也就越多,有可能你只需要一条记录,而Hibernate因为
懒加载的原因,发出了几十上百条的查询语句;
2.某些模块对应的实体类,在设计时,有设计到分表;在进行打桩测试及添加懒加载后,发现在获取实体对象的关联对象时,如果这个关联对象的数据库
表没有做分表,查询很快,时间可以忽略不计;而有做分表的记录时,查询耗时,一条数据需要400~500ms;而且代码中也存在与一些这些分表的聚合函数,同样查询一次需要700ms左右;
由此可以发现问题存在与懒加载,分表, 以及聚合函数的合理使用
调整策略:
1.使用懒加载,当需要的时候才真正的发起select请求
将实体对象的Hibernate映射文件中,使用Lazy="false" 改为lazy="true",并且添加fetch="select"。具体意思,请百度;
在一对多的映射关系时,请合理使用fetch="join"; 具体看实际情况
部分映射配置文件:
<many-to-one name="allGoods" class="com.rhxy.bean_new.inventory.AllGoods" column="allGoods_id" fetch="select" />
<many-to-one name="saleOrder" class="com.rhxy.bean_new.sale.SaleOrder" column="saleOrder_id" fetch="select"/>
<set name="outStoreRecords"> //如果需要取消懒加载,可以lazy="flase"或者 fetch="join" 都是会在在查当前记录时发起多条select语句去查询集合的关联对象
<key>
<column name="saleOrderBill_id" />
</key>
<one-to-many class="com.rhxy.bean_new.inventory.OutStoreRecord" />
</set>
<many-to-one name="allGoods" class="com.rhxy.bean_new.inventory.AllGoods" column="allGoods_id" fetch="select" />
<many-to-one name="saleOrder" class="com.rhxy.bean_new.sale.SaleOrder" column="saleOrder_id" fetch="select"/>
<set name="outStoreRecords"> //如果需要取消懒加载,可以lazy="flase"或者 fetch="join" 都是会在在查当前记录时发起多条select语句去查询集合的关联对象
<key>
<column name="saleOrderBill_id" />
</key>
<one-to-many class="com.rhxy.bean_new.inventory.OutStoreRecord" />
</set>
这样的话,打印sql语句,可以看见,只有在我们使用到这个对象的属性时,才会真正的发起select语句,在数据量十分大的时候,可以很高的效率
2.针对获取分表记录时,重写DAO层函数,分表是已表名加年份;比如:
public <T> List<T> findAllBySubTable(String tableName,String hql, int start, int limit, String... properties) {
int year = new Date().getYear() + 1900;
Session session;
if (year == 0) {
session = getSessionFactory().openSession();//当前的session使用这个拦截器
} else {
HibernateInterceptor interceptor = new HibernateInterceptor();//我们的拦截器
interceptor.setTargetTableName(tableName);//要拦截的目标表名
interceptor.setTempTableName(tableName + "_" + year); //要替换的子表名
session = getSessionFactory().openSession(interceptor);//当前的session使用这个拦截器
}
List<T> entities = new ArrayList<T>();
try {
Query query = createQuery(session, hql, properties);
query.setFirstResult(start);
query.setMaxResults(limit);
entities = query.list();
} catch (Exception e) {
e.printStackTrace();
session.close();
} finally {
if (session.isOpen()) {
session.close();
}
}
return entities;
}
原先我们是直接使用对象.get关联对象,或者使用
public T get(Serializable id) {
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
T t = null;
try {
t = (T) session.get(entityClass, id);
session.flush();
tx.commit();
session.close();
} catch (Exception e) {
tx.rollback();
session.close();
} finally {
if (session.isOpen()) {
session.close();
}
}
return t;
}
这样改变一下查询方法,就解决了分表查询效率底下的问题
3.在使用聚合函数的时候,不论这个实体类是否有设计分表,我们在任何时候,需要记住尽可能少的去访问数据库,拿尽量多的数据(注意懒加载,这里和上面说的并不冲突);
比如我现在有两条聚合函数的sql:
public double getSumShouldOutQuantity(Stock stock) {
double qty = 0;
String sql = "select sum(shouldOutQuantity) from outStoreRecord where type='_1' and primaryRecord='T' and allGoods_id=" + stock.getAllGoods().getId();
List<Object> list = this.findAllBySql(sql);
if (list != null && list.size() > 0) {
if (list.get(0) != null) {
qty = Double.parseDouble(list.get(0).toString());
}
}
return qty;
}
public double getSumActualQuantity(Stock stock) {
double Aty = 0;
String sql = "select sum(actualQuantity) from outStoreRecord as o where type='_1' and allGoods_id=" + stock.getAllGoods().getId();
List<Object> list = this.findAllBySql(sql);
if (list != null && list.size() > 0) {
if (list.get(0) != null) {
Aty = Double.parseDouble(list.get(0).toString());
}
}
return Aty;
}
查询应出库数量和实际出库数量,计算未出库数量,这样查询出来的两个数据,是不是还要再做一下减法?而且发出了两条查询语句,别不在意,请向以后考虑,可能现在并不会影响你什么,或者并不会耗时多少,但是当数据量达到几十万上百万千万时,又该如何?
修改查询语句:
public double getSumNotOutStoreQty(Stock stock) {
double Aty = 0;
String sql = "select sum(shouldOutQuantity) -sum(actualQuantity) from outStoreRecord as o where type='_1' and allGoods_id=" + stock.getAllGoods().getId();
List<Object> list = this.findAllBySqlOfSubTable("outStoreRecord", sql);
if (list != null && list.size() > 0) {
if (list.get(0) != null) {
Aty = Double.parseDouble(list.get(0).toString());
}
}
return Aty;
}
this.findAllBySqlOfSubTable("outStoreRecord", sql);
if (list != null && list.size() > 0) {
if (list.get(0) != null) {
Aty = Double.parseDouble(list.get(0).toString());
}
}
return Aty;
}
这样,一条语句足以,注意红色地方因为这个表是一个分表,查询一条语句需要耗时很长,所以优化了sql语句和分表查询
至此,问题解决,原先的30~40s就不存在了,现在只需要不到1S中;
还没晚:
在使用懒加载的时候出现问题,相信你也会遇到;
在设置懒加载的时候,获取关联对象报错:no session or session was closed
这是因为,懒加载,用到数据的时候再去加载,这个时候session已经关闭;这个时候你要查看一下你的session建立方式:
1.sessionFactory.openSession();
2.sessionFactory.getCurrentSession();
第一种是需要手动关闭,不然会照成内存溢出,导致系统崩溃;第二种则不需要手动去关闭;所以,将程序中查询的session创建方式改为getCurrentSession(),问题解决;
getCurrentSession是必须提交事务的。所以在用到session.getCurrentSession()的这个方法一定是要配置声明式事务管理。
注意:如果是使用到分表,会使用到Hibernate的拦截器,去拦截查询的表,以替换使用的分表,仔细看上面的查询分表方法,这里如果使用getCurrentSession(),将不能使用拦截器,也就不能去查询分表;所以问题还是没解决;
在网上查询,使用OpenSessionInViewFilter,将session的生存周期延长到Action结束,也就是交给Spring管理,配置方法:
<filter>
<filter-name>hibernateFilter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
<init-param>
<param-name>sessionFactoryBeanName</param-name>
<param-value>sessionFactory</param-value>
</init-param>
<init-param>
<param-name>singleSession</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>flushMode</param-name>
<param-value>AUTO</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>hibernateFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter>
<filter-name>struts</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
<init-param>...</init-param>
</filter>
<filter>
<filter-name>hibernateFilter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
<init-param>
<param-name>sessionFactoryBeanName</param-name>
<param-value>sessionFactory</param-value>
</init-param>
<init-param>
<param-name>singleSession</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>flushMode</param-name>
<param-value>AUTO</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>hibernateFilter</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
<filter>
<filter-name>struts</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
<init-param>...</init-param>
</filter>
注意,OpenSessionInViewFilter一定要放在Strus配置的前面;如果还是无效,请将url-pattern 改为/*;
还有一个问题在获取session时,在重启服务器以后,会出现:
No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here
解决办法:
修改sessionFactory的配置:
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.connection.autocommit">true</prop>
<prop key="myeclipse.connection.profile">MySQL</prop>
<!--<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>-->
<prop key="hibernate.dialect">com.rhxy.dao_new.MySQLLocalDialect</prop>
<!--<prop key="hibernate.hbm2ddl.auto">create</prop>-->
<prop key="hibernate.hbm2ddl.auto">update</prop>
<prop key="hibernate.myeclipse.connection.profile">true</prop>
<prop key="hibernate.current_session_context_class">thread</prop>
<prop key="hibernate.transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory
</prop>
</props>
</property>
<prop key="hibernate.current_session_context_class">thread</prop>
<prop key="hibernate.transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory
</prop>
</props>
</property>
到此,所有问题以解决,比较详细,也比较繁琐;但是写程序不就是应该仔细不怕麻烦吗;