Jpa no session(包含多数据源)
在springboot中,在application.properties的配置文件中新增spring.jpa.open-in-view=true方法失效,经过测试,有两种解决办法:
1、在application.properties的配置文件中新增spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true;
2、在测试的方法上添加@Transactional注解
多数据源相关转自
多数据源场景
- 解决办法:将方法细化。单个方法只使用一个session(有读有写的逻辑,读写都使用同一个session),在方法上使用事务注解标注事务的管理器
@Transactional(value = "transactionManagerLinux", rollbackFor = Exception.class) 当前查询及更新问题已修复。接下来就是一大波代码重构
以下是一些介绍及说明
文章目录
- [一、首先要配置druid数据源]
- [二、数据源生成类:]
- [三、EntityManager生成类]
- [四、问题汇总]
一、首先要配置druid数据源
1、druid的某个配置很重要,因为数据库的每个连接是默认有一个8小时的超时时间的,如果过了8小时,连接会自动断掉,而druid此时还不知道连接已经断掉,也没有回收,一直用那个连接就会报错。此种情况一般出现于定时任务,比如某个统计需要每天统计一次,某个连接可能24小时才会使用一次,这时就会出现这个问题。
解决方案:
①、更改数据库默认的wait_outtime,改长一点,不推荐
②、可以从数据源入手,更改druid的配置,让druid每隔一段时间自动去检测连接池中每个连接是否可用,如果不可用则回收。后面需要连接时,会重新开启一个连接。这个时间间隔需要小于数据库的超时时间才对。
2、druid配置和jpa配置
①、主库配置:
# JDBC配置
spring.datasource.druid.url= jdbc:mysql://10.10.10.105:23306/xiuba_user?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=round&autoReconnect=true&useSSL=false
spring.datasource.druid.username= xiuba
spring.datasource.druid.driver-class-name= com.mysql.jdbc.Driver
spring.datasource.druid.password= xiuba123456
# 连接池配置,说明请参考Druid Wiki,DruidDataSource配置属性列表
spring.datasource.druid.initial-size=3
spring.datasource.druid.max-active=20
spring.datasource.druid.min-idle=3
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=50000
spring.datasource.druid.min-evictable-idle-time-millis=80000
spring.datasource.druid.max-evictable-idle-time-millis=100000
spring.datasource.druid.validation-query=select 'x'
spring.datasource.druid.validation-query-timeout=10
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
#默认值stat,配置多个英文逗号分隔
spring.datasource.druid.filters= stat,wall,slf4j
# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.urlPattern=/*
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
spring.datasource.druid.web-stat-filter.sessionStatMaxCount=600
spring.datasource.druid.web-stat-filter.sessionStatEnable=true
#spring.datasource.druid.web-stat-filter.principalSessionName=
#spring.datasource.druid.web-stat-filter.principalCookieName=
spring.datasource.druid.web-stat-filter.profileEnable=true
# StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.urlPattern=/druid/*
spring.datasource.druid.stat-view-servlet.resetEnable=true
spring.datasource.druid.stat-view-servlet.loginUsername=druid
spring.datasource.druid.stat-view-servlet.loginPassword=123456
#spring.datasource.druid.stat-view-servlet.allow=
#spring.datasource.druid.stat-view-servlet.deny=
②、从库配置
#从库配置
# JDBC配置
spring.datasource.druid.slave.url= jdbc:mysql://10.10.10.104:23306/xiuba_user?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=round&autoReconnect=true&useSSL=false
spring.datasource.druid.slave.username=xiuba_select
spring.datasource.druid.slave.driver-class-name= com.mysql.jdbc.Driver
spring.datasource.druid.slave.password=xiuba123456
# 连接池配置,说明请参考Druid Wiki,DruidDataSource配置属性列表
spring.datasource.druid.slave.initial-size=3
spring.datasource.druid.slave.max-active=20
spring.datasource.druid.slave.min-idle=3
spring.datasource.druid.slave.max-wait=60000
spring.datasource.druid.slave.time-between-eviction-runs-millis=50000
spring.datasource.druid.slave.min-evictable-idle-time-millis=80000
spring.datasource.druid.slave.max-evictable-idle-time-millis=100000
spring.datasource.druid.slave.validation-query=select 'x'
spring.datasource.druid.slave.validation-query-timeout=10
spring.datasource.druid.slave.test-on-borrow=false
spring.datasource.druid.slave.test-on-return=false
spring.datasource.druid.slave.test-while-idle=true
#默认值stat,配置多个英文逗号分隔
spring.datasource.druid.slave.filters= stat,wall,slf4j
③、JPA配置
spring.jpa.properties.hibernate.hbm2ddl.auto= update
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.format_sql=false
注意:
①: 因为jpa或者说hibernate是orm框架,有一个自动更新数据库的配置,可以使数据库根据项目中的实体类自动生成相应的表结构,非常方便好用。但是这也引来了一些问题,如果从库也应用这个配置,就会更改从库的表结构,而从库只要自己发生变化而不是主库复制来的,就会发生mysql错误,使主从复制停止,造成重大错误。所以此时jpa的配置应该主从分离,此操作实现在EntityManager生成配置类中。
②:为了避免上述情况在我们不经意间发生,最好给从库配置一个只能读不能写的权限的用户,我们所有操作都用这个用户来执行,避免人为错误。
二、数据源生成类:
package com.zhibi.xiuba.config.data;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
/**
* Created by QinHe on 2018-07-24.
*/
@Configuration
public class DruidDataSourceConfig {
/**
* 主DataSource 配置
*/
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Bean(name = "primaryDruidDataSource")
public DataSource primaryDruidDataSource(Environment environment) {
return DruidDataSourceBuilder.create().build(environment, "spring.datasource.druid.");
}
/**
* 从DataSource 配置
*/
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
@Bean(name = "slaveDruidDataSource")
public DataSource slaveDruidDataSource(Environment environment) {
return DruidDataSourceBuilder.create().build(environment, "spring.datasource.druid.slave.");
}
}
三、EntityManager生成类
与jpa结合起来:
1、主库:
package com.zhibi.xiuba.config.data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.Map;
/**
* Created by QinHe on 2018-07-24.
*/
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactoryPrimary",
transactionManagerRef = "transactionManagerPrimary",
basePackages = {"com.zhibi.xiuba.repository.primary"}) //设置Repository所在位置
public class PrimaryConfig {
@Resource(name = "primaryDruidDataSource")
private DataSource primaryDruidDataSource;
@Autowired
private JpaProperties jpaProperties;
@Primary
@Bean(name = "entityManagerFactoryPrimary")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(primaryDruidDataSource)
.properties(getVendorProperties(primaryDruidDataSource))
.packages("com.zhibi.xiuba") //设置实体类所在位置
.persistenceUnit("primaryPersistenceUnit")
.build();
}
@Primary
@Bean(name = "entityManagerPrimary")
public EntityManager entityManagerPrimary(EntityManagerFactoryBuilder builder) {
return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
}
private Map<String, String> getVendorProperties(DataSource dataSource) {
return jpaProperties.getHibernateProperties(dataSource);
}
@Primary
@Bean(name = "transactionManagerPrimary")
public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
}
}
2、从库
package com.zhibi.xiuba.config.data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.Map;
/**
* Created by QinHe on 2018-07-24.
*/
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactorySlave",
transactionManagerRef = "transactionManagerSlave",
basePackages = {"com.zhibi.xiuba.repository.slave"}) //设置Repository所在位置
public class SlaveConfig {
@Resource(name = "slaveDruidDataSource")
private DataSource slaveDruidDataSource;
@Autowired
private JpaProperties jpaProperties;
@Bean(name = "entityManagerFactorySlave")
public LocalContainerEntityManagerFactoryBean entityManagerFactorySlave(EntityManagerFactoryBuilder builder) {
LocalContainerEntityManagerFactoryBean slavePersistenceUnit = builder
.dataSource(slaveDruidDataSource)
.properties(getVendorProperties(slaveDruidDataSource))
.packages("com.zhibi.xiuba") //设置实体类所在位置
.persistenceUnit("slavePersistenceUnit")
.build();
slavePersistenceUnit.getJpaPropertyMap().remove("hibernate.hbm2ddl.auto");
return slavePersistenceUnit;
}
@Bean(name = "entityManagerSlave")
public EntityManager entityManagerSlave(EntityManagerFactoryBuilder builder) {
return entityManagerFactorySlave(builder).getObject().createEntityManager();
}
private Map<String, String> getVendorProperties(DataSource dataSource) {
return jpaProperties.getHibernateProperties(dataSource);
}
@Bean(name = "transactionManagerSlave")
public PlatformTransactionManager transactionManagerSlave(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactorySlave(builder).getObject());
}
}
四、问题汇总
1、在service层引入entityManager的时候,用spring的@Autowired或者@Resource引入会有一些问题,有时候数据源中的连接的获取会不正常,所以用官方的方法引入比较好,比如引入从库的entityManager:
@PersistenceContext(unitName = "entityManagerFactorySlave")
private EntityManager entityManagerSlave;
2、如果两个实体之间具有管理关系,并且是懒加载的,在从库查询时会有no session问题,就算配置了spring.jpa.open-in-view=true,还是会有问题。springboot对于这个配置是这样处理的:
JpaBaseConfiguration.java源码:
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(WebMvcConfigurerAdapter.class)
@ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class,
OpenEntityManagerInViewFilter.class })
@ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true)
protected static class JpaWebConfiguration {
// Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when
// not on the classpath
@Configuration
protected static class JpaWebMvcConfiguration extends WebMvcConfigurerAdapter {
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
return new OpenEntityManagerInViewInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(openEntityManagerInViewInterceptor());
}
}
}
jpa底层判断spring.jpa.open-in-view是否为true,如果是true,则注册OpenEntityManagerInViewInterceptor这个拦截器。
OpenEntityManagerInViewInterceptor对于懒加载的处理:
@Override
public void preHandle(WebRequest request) throws DataAccessException {
String participateAttributeName = getParticipateAttributeName();
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult()) {
if (applyCallableInterceptor(asyncManager, participateAttributeName)) {
return;
}
}
if (TransactionSynchronizationManager.hasResource(getEntityManagerFactory())) {
// Do not modify the EntityManager: just mark the request accordingly.
Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
int newCount = (count != null ? count + 1 : 1);
request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST);
}
else {
logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
try {
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), emHolder);
AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(getEntityManagerFactory(), emHolder);
asyncManager.registerCallableInterceptor(participateAttributeName, interceptor);
asyncManager.registerDeferredResultInterceptor(participateAttributeName, interceptor);
}
catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
}
}
}
@Override
public void postHandle(WebRequest request, ModelMap model) {
}
@Override
public void afterCompletion(WebRequest request, Exception ex) throws DataAccessException {
if (!decrementParticipateCount(request)) {
EntityManagerHolder emHolder = (EntityManagerHolder)
TransactionSynchronizationManager.unbindResource(getEntityManagerFactory());
logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
}
}
在请求到服务端时,先经过这个拦截器,将getEntityManagerFactory()作为key,将EntityManager em = createEntityManager();EntityManagerHolder emHolder = new EntityManagerHolder(em);emHolder作为值,存入TransactionSynchronizationManager的当前线程的ThreadLocal变量中,在后面的懒加载的处理,就可以通过entitymanager去加载数据,但是这个拦截器getEntityManagerFactory()和createEntityManager()都是用的默认的entityManager(主库的),所以对于从库的查询就不行了,为了解决这个问题,需要在方法中显式的存入entityManager,如下:(也可以通过注解配置,见下面代码中注释的@Transactional部分)
/**
* 条件查询
*/
@Override
// @Transactional(value = "transactionManagerSlave", rollbackFor = Exception.class)
public Page<User> getUserListByCondition(Params params, Pageable pageable, String platForm) {
EntityManager entityManagerSlave = null;
try {
if (!TransactionSynchronizationManager.hasResource(
entityManagerFactorySlave)) {
entityManagerSlave = entityManagerFactorySlave.createEntityManager();
EntityManagerHolder entityManagerHolder = new EntityManagerHolder(entityManagerSlave);
// TransactionSynchronizationManager.unbindResource(entityManagerFactorySlave);
TransactionSynchronizationManager.bindResource(entityManagerFactorySlave, entityManagerHolder);
log.info("userService:getUserListByCondition线程:" + Thread.currentThread().getName() + "绑定事务!");
} else {
log.info("userService:getUserListByCondition线程:" + Thread.currentThread().getName() + "已经有事务了!");
}
Page<User> all = userSlaveRepository.findAll(getUserSearchWhereClause(params, platForm), pageable);
List<User> list = all.getContent();
try {
changeUserListFor(list);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
return all;
} catch (IllegalStateException e) {
e.printStackTrace();
return null;
} finally {
if (TransactionSynchronizationManager.hasResource(entityManagerFactorySlave)) {
TransactionSynchronizationManager.unbindResource(entityManagerFactorySlave);
}
if (entityManagerSlave != null) {
EntityManagerFactoryUtils.closeEntityManager(entityManagerSlave);
}
}
}
全局懒加载
关于延迟加载
在 Spring 中,默认情况下所有定的 bean 及其依赖项目都是在应用启动时创建容器上下文是被初始化的。测试代码如下:
@Slf4j
@Configuration
public class DemoConfig {
public DemoConfig() {
log.warn(" > > > demoConfig 被初始化 > > >");
}
}
启动应用日志:
[ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
[ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1193 ms
[ main] c.p.c.global.lazy.config.DemoConfig : > > > demoConfig 被初始化 > > >
[ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
[ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
如上日志: 在 Tomcat started 之前 DemoConfig bean 已经被初始化创建。
一般情况程序在启动时时有大量的 Bean 需要初始化,例如 数据源初始化、缓存初始化等导致应用程序启动非常的慢。在 spring boot 2.2 之前的版本,我们对这些 bean 使用手动增加 @Lazy 注解,来实现启动时不初始化,业务程序在调用需要时再去初始化,如上代码修改为即可:
@Lazy
@Configuration
public class DemoConfig {}
为什么需要全局懒加载
同上文中提到我们需要手动在 bean 增加 @Lazy 注解,这就意味着我们仅能对程序中自行实现的 bean 进行添加。但是现在 spring boot 应用中引入了很多第三方 starter ,比如 druid-spring-boot-starter 数据源注入、spring-boot-starter-data-redis 缓存等默认情况下, 引入即注入了相关 bean 我们无法去修改添加 @Lazy。
- spring boot 2.2 新增全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建
spring:
main:
lazy-initialization: true #默认false 关闭
- 个别 bean 可以通过设置 @Lazy(false) 排除,设置为启动时加载
@Lazy(false)
@Configuration
public class DemoConfig {}
- 当然也可以指定规则实现 LazyInitializationExcludeFilter 规则实现排除
@Bean
LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
return LazyInitializationExcludeFilter.forBeanTypes(DemoConfig.class);
}
全局懒加载的问题
通过设置全局懒加载,我们可以减少启动时的创建任务从而大幅度的缩减应用的启动时间。但全局懒加载的缺点可以归纳为以下两点:
- Http 请求处理时间变长。 这里准确的来说是第一次 http 请求处理的时间变长,之后的请求不受影响(说到这里自然而然的会联系到 spring cloud 启动后的第一次调用超时的问题)。
- 错误不会在应用启动时抛出,不利于早发现、早解决、早下班。