Java for Web学习笔记(一零六):Spring框架中使用JPA(6)Isolation和C3P0(下)
程序员文章站
2022-04-22 13:39:34
...
使用hibernate-c3p0的isolation设置
我们前面学习的方式,都是先设置datasource,然后在JPA实现中关联该数据源。hibernate提供了hibernate-c3p0,在提供entityManagerFactoryBean的时候进行数据源的设置,实际上采用xml文件配置[1],很多都是这种方式,只不过我们采用代码方式实现。
pom.xml的设置,将将<artifactId>hibernate-core</artifactId> 修改为<artifactId>hibernate-c3p0</artifactId>,将自动引入相应的hibernate-core和c3p0的jar包。
/* 修订:删除DataSource的bean,由hibernate实现的entityManagerFactoryBean来自动关联
@Bean
public DataSource springJpaDataSource() throws Exception{
......
}*/
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() throws Exception {
MyConfig config = this.myConfig(); //这个模拟获取的配置,可以从中得到数据源的配置
Map<String, Object> properties = new Hashtable<>();
properties.put("javax.persistence.schema-generation.database.action", "none");
properties.put("hibernate.connection.url", config.getJdbcUrl() + "?useSSL=true&useUnicode=true&characterEncoding=utf-8");
properties.put("hibernate.connection.username", config.getJdbcUser());
properties.put("hibernate.connection.password", config.getJdbcPassword());
properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5InnoDBDialect");
properties.put("hibernate.connection.driver_class", "com.mysql.jdbc.Driver");
properties.put("connection.provider_class", "org.hibernate.c3p0.internal.C3P0ConnectionProvider");
properties.put("hibernate.c3p0.min_size", "2");
properties.put("hibernate.c3p0.max_size", "200");
properties.put("hibernate.c3p0.timeout", "1800");
properties.put("hibernate.c3p0.acquire_increment", "2");
//这里设置isolation
properties.put("hibernate.connection.isolation", String.valueOf(Connection.TRANSACTION_REPEATABLE_READ));
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(adapter);
factory.setPackagesToScan("com.wei.testproject.entities");
factory.setSharedCacheMode(SharedCacheMode.ENABLE_SELECTIVE);
factory.setValidationMode(ValidationMode.NONE);
factory.setJpaPropertyMap(properties);
return factory;
注意,只要我们不使用tomcat的数据源方式,对于mysql都需要进行driver的deregister。否则卸载时出现:
一月 12, 2018 10:13:53 上午 org.apache.catalina.loader.WebappClassLoaderBase clearReferencesJdbc
警告: The web application [abc] registered the JDBC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
一月 12, 2018 10:13:53 上午 org.apache.catalina.loader.WebappClassLoaderBase clearReferencesThreads
警告: The web application [abc] appears to have started a thread named [Abandoned connection cleanup thread] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
java.lang.Object.wait(Native Method)
java.lang.ref.ReferenceQueue.remove(Unknown Source)
com.mysql.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:64)
java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
j ava.lang.Thread.run(Unknown Source)
一月 12, 2018 10:13:57 上午 org.apache.catalina.loader.WebappClassLoaderBase checkStateForResourceLoading
信息: Illegal access: this web application instance has been stopped already. Could not load []. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
java.lang.IllegalStateException: Illegal access: this web application instance has been stopped already. Could not load []. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1352)
at org.apache.catalina.loader.WebappClassLoaderBase.getResource(WebappClassLoaderBase.java:1028)
at com.mysql.jdbc.AbandonedConnectionCleanupThread.checkContextClassLoaders(AbandonedConnectionCleanupThread.java:90)
at com.mysql.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:63)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
在C3P0Utils中,我们已经写过destroy,我们只需抽取其注销driver的部分接口,如下。同样,我们应在war结束时调用。
public static void closeSafely(){
com.mysql.jdbc.AbandonedConnectionCleanupThread.checkedShutdown();
Enumeration<java.sql.Driver> drivers = DriverManager.getDrivers();
while(drivers.hasMoreElements()) {
try {
Driver driver = drivers.nextElement();
DriverManager.deregisterDriver(driver);
} catch (SQLException e) {
logger.error("Close mysql safely error : {}", e.toString());
}
}
}
代码中修订isolation等级
无论采用上面哪种方式设置,我们都可以通@Transactional重新指定isolation等级。@Override
@Transactional(isolation=Isolation.READ_COMMITTED)
void test1(){}
isolation不能解决的DataIntegrityViolationException的处理
测试小例子
我们进行了一下的一个测试,我们通过两个异步调用,几乎同时执行了下面的两个事务//ActionService:
@Transaction
public viod save(String event){
//saveHistory()是如果不存在则insert,如果存在则update信息
this.historyRepository.saveHistory(event);
}
这是报告下面的错误:
16:23:50.293 [task-7] [ERROR] (Spring) SimpleAsyncUncaughtExceptionHandler - Unexpected error occurred invoking async method 'public void com.wei.testproject.site.ActionService.actionAsync(com.wei.testproject.entities.ActionData)'.
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [PRIMARY]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:278) ~[spring-orm-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244) ~[spring-orm-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:521) ~[spring-orm-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:761) ~[spring-tx-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:730) ~[spring-tx-4.3.11.RELEASE.jar:4.3.11.RELEASE]
......
分析
我们重点看看api的对Connection.TRANSACTION_REPEATABLE_READ的javadoc说明:A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur. This level prohibits a transaction from reading a row with uncommitted changes in it, and it also prohibits the situation where one transaction reads a row, a second transaction alters the row, and the first transaction rereads the row, getting different values the second time (a "non-repeatable read").
这能够对update的情况进行很好的保护,但是小测试中在两个事务中都在尝试insert,不在其保护范围内。虽然在实际运行中,几乎不可能出现这种情况,但是作为代码的完整性,我们还是应该进行相应的修订。
代码修订
由于transaction中检查到异常会进行回滚,因此,我们不能在transaction的内部代码中进行修订,需要在外部。为此我们新建一个AdvanceActionService类来替代ActionService进行相关的处理。//AdvanceActionService:
@Inject IActionService actionService;
// 对于存在类似save的情况,均采用:
public viod save(String event){
try{
this.actionService.save(event);
}catch(DataIntegrityViolationException e){
log.warn("Try to fix DataIntegrityViolationException, retry save again.");
this.actionService.save(event);
}
}
在实际应用中,我们很少采用这种修订方式,同时insert本来就是很少的概率,在具体的应用场景下,可能根本就不会发生,如果是用户的UI操作,有先后顺序,如果是消息队列也有先后顺序。亦或者是属于极个别的极偶然的现象,第二个insert就抛出异常,并回滚,那就按异常情况处理好了。但是如果在某些特定的场景,有一定概率的出现,就需要进行修订。