Spring Boot配置EntityManagerFactory复用当前数据库连接咨询
当前实现的性能痛点
- 为获取实体ID用于S3上传,需先持久化实体,依赖生命周期获取ID
- 使用
UUID.randomUUID().toString()生成ID时,Spring Boot会额外执行MySQL查询,拖慢性能 - 调用
emf.createEntityManager()会新建数据库连接,导致Hikari连接池因未关闭的活跃连接冻结
核心问题
如何配置EntityManagerFactory,使其复用当前数据库连接而非新建连接?
问题代码
private ProductImage addProductImage(Product product, String url, Boolean uploadImageToS3) { EntityManager em = emf.createEntityManager(); String productImageUrl = null; String productImageId = null; try { EntityTransaction transaction = em.getTransaction(); transaction.begin(); ProductImage productImage = new ProductImage(); productImage.setOriginalUrl(url); InputStream inputStream = null; if(uploadImageToS3) { inputStream = getInputStreamFromUrl(url); productImage.setSize(getInputStreamSizeOrZero(inputStream)); productImage.setS3BucketAndEndpoint(storageProperties.getStorageS3ImagesBucketName() + "." + storageProperties.getStorageS3EndPoint()); productImage.setS3FolderName(S3FolderName.PRODUCTS); productImage.setS3Size(S3Size.FULL); productImage.setS3Production(storageProperties.getStorageS3Production()); productImage.setS3CloudFrontDomain(storageProperties.getStorageS3CloudFrontDomain()); } else { productImage.setUri(url); } em.persist(productImage); productImageId = productImage.getId(); if(uploadImageToS3) { productImageUrl = s3StorageService.uploadImage(inputStream, S3FolderName.PRODUCTS, productImageId, S3Size.FULL, allowedImageFormat); try { inputStream.close(); } catch (IOException e) { throw new CustomGraphQLException("Input stream of the product image could not be closed."); } finally { em.close(); } } else { em.close(); } return productImage; } catch (Exception e) { em.getTransaction().rollback(); em.close(); if(productImageUrl!=null && uploadImageToS3) { s3StorageService.deleteImage(S3FolderName.PRODUCTS, productImageId , S3Size.FULL, allowedImageFormat); } throw new CustomGraphQLException("Product image could not be inserted. " + e.getMessage()); } }
解决方案
一、复用当前连接:依赖Spring事务上下文管理EntityManager
手动调用emf.createEntityManager()会强制创建新的EntityManager(对应新连接),要复用连接,必须让EntityManager绑定到当前Spring事务上下文,而非手动创建。
具体步骤:
替换注入对象
把EntityManagerFactory emf替换为直接注入EntityManager,Spring会自动从当前事务上下文复用已有EntityManager(及对应连接):@PersistenceContext private EntityManager em;移除手动管理EntityManager的代码
删除EntityManager em = emf.createEntityManager();和所有em.close();语句,Spring会在事务结束后自动处理连接释放。开启Spring事务管理
在addProductImage方法上添加@Transactional注解(或确保上层调用方法处于事务中),Spring会自动维护连接生命周期:@Transactional private ProductImage addProductImage(Product product, String url, Boolean uploadImageToS3) { String productImageUrl = null; String productImageId = null; try { ProductImage productImage = new ProductImage(); productImage.setOriginalUrl(url); InputStream inputStream = null; if(uploadImageToS3) { inputStream = getInputStreamFromUrl(url); productImage.setSize(getInputStreamSizeOrZero(inputStream)); productImage.setS3BucketAndEndpoint(storageProperties.getStorageS3ImagesBucketName() + "." + storageProperties.getStorageS3EndPoint()); productImage.setS3FolderName(S3FolderName.PRODUCTS); productImage.setS3Size(S3Size.FULL); productImage.setS3Production(storageProperties.getStorageS3Production()); productImage.setS3CloudFrontDomain(storageProperties.getStorageS3CloudFrontDomain()); } else { productImage.setUri(url); } em.persist(productImage); productImageId = productImage.getId(); if(uploadImageToS3) { productImageUrl = s3StorageService.uploadImage(inputStream, S3FolderName.PRODUCTS, productImageId, S3Size.FULL, allowedImageFormat); try { inputStream.close(); } catch (IOException e) { throw new CustomGraphQLException("Product image input stream failed to close."); } } return productImage; } catch (Exception e) { if(productImageUrl!=null && uploadImageToS3) { s3StorageService.deleteImage(S3FolderName.PRODUCTS, productImageId , S3Size.FULL, allowedImageFormat); } throw new CustomGraphQLException("Failed to insert product image: " + e.getMessage()); } }
二、优化UUID ID生成的额外查询问题
使用UUID.randomUUID().toString()时,若实体ID字段用@GeneratedValue(strategy = GenerationType.AUTO),Hibernate会默认查询MySQL的hibernate_sequence表,产生冗余操作。
解决方案:
直接指定UUID生成策略,避免数据库查询:
@Id @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator") @Column(columnDefinition = "CHAR(36)") private String id;
此配置下Hibernate会在内存中生成UUID,无需访问数据库,消除额外查询。
三、优化S3上传与实体持久化顺序(可选)
若不想先持久化实体再上传S3,可提前生成UUID作为ID直接赋值,无需persist即可拿到ID:
// 提前生成UUID String productImageId = UUID.randomUUID().toString(); ProductImage productImage = new ProductImage(); productImage.setId(productImageId); // 直接用该ID上传S3,再持久化实体 em.persist(productImage);
此方式可打破先持久化再上传的依赖,缩短事务内操作时间,进一步降低连接占用。
关键原理
Spring通过@PersistenceContext注入的EntityManager是线程绑定的代理对象,会自动关联当前Spring事务的连接。当方法被@Transactional标记时,Spring从Hikari连接池获取连接并绑定到当前线程的EntityManager,所有操作复用该连接,事务结束后自动归还连接至池,彻底解决连接冻结问题。
内容的提问来源于stack exchange,提问作者Michael Bat

