You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA EntityManager悲观锁在H2与PostgreSQL中的异常问题排查

解决H2连接异常、跨数据库锁行为差异及悲观锁异常处理

让我们一步步拆解你的问题,先搞定最棘手的H2连接异常,再解释不同数据库的锁行为差异,最后给出异常处理的最佳实践。

一、修复H2下的连接关闭与AsyncContext异常

你遇到的java.sql.SQLException: Connection is closed和Cannot dispatch without an AsyncContext异常,主要是两个核心问题导致的:

1. 异步请求上下文未正确收尾

从异常信息来看,你应该是在Servlet异步处理中执行下载逻辑。如果异常没有被捕获并处理,异步上下文会处于未完成状态,容器可能会提前回收连接,导致第一个线程在事务收尾时找不到可用连接。

解决方法:
在异步处理代码中,务必在finally块里确保AsyncContext被正确完成,无论成功还是失败:

AsyncContext asyncContext = request.startAsync();
try {
    foobar(); // 调用你的下载方法
    asyncContext.complete();
} catch (PessimisticLockException e) {
    // 告知用户资源正在被占用
    asyncContext.getResponse().getWriter().write("当前资源正在被下载,请稍后重试");
    asyncContext.complete();
} catch (Exception e) {
    // 处理其他异常
    log.error("下载出错", e);
    asyncContext.complete();
}

2. 连接池配置导致连接被提前回收

如果使用了第三方连接池(比如HikariCP),H2默认的连接闲置超时可能短于你设置的10秒sleep时间,导致连接被提前回收。你需要调整连接池参数:

  • 增加闲置超时时间(比如设置为15秒,大于sleep的10秒)
  • 添加连接有效性验证,确保连接在使用时是可用的

如果是Spring Boot项目,在application.properties里配置:

spring.datasource.hikari.idle-timeout=15000
spring.datasource.hikari.validation-query=SELECT 1
spring.datasource.hikari.max-lifetime=60000

另外,检查@Transactional注解的传播行为,确保事务在sleep期间保持活跃,不会因为异常传播导致事务提前终止。

二、为什么H2和PostgreSQL的锁行为不一样?

这是因为不同数据库对悲观锁超时策略的默认配置不同:

H2的默认行为

H2在使用PESSIMISTIC_WRITE锁时,默认锁超时时间为0秒——也就是说,当另一个事务尝试获取已锁定的行时,会立即抛出PessimisticLockException,不会等待,这符合你最初的预期。

PostgreSQL的默认行为

PostgreSQL的SELECT ... FOR UPDATE(对应JPA的PESSIMISTIC_WRITE)默认是无限等待,直到锁被释放或者数据库连接超时。这就是为什么第二个线程会阻塞,而不是抛出异常。

让PostgreSQL和H2行为一致的方法:
在调用entityManager.lock()时,指定锁超时时间为0,强制PostgreSQL立即返回失败:

Map<String, Object> lockProperties = new HashMap<>();
// 设置锁超时为0毫秒,立即触发异常
lockProperties.put("javax.persistence.lock.timeout", 0);
try {
    entityManager.lock(nachricht, LockModeType.PESSIMISTIC_WRITE, lockProperties);
} catch (PessimisticLockException e) {
    // 处理锁冲突逻辑
}

这个属性是JPA标准属性,主流数据库都支持。

三、正确处理PessimisticLockException

要友好告知用户下载冲突,你需要在合适的层级捕获异常,并确保事务正确回滚:

1. 在事务方法内捕获异常(推荐)

你的foobar()方法标注了@Transactional,默认会在RuntimeException(包括PessimisticLockException)发生时回滚事务。你可以在方法内捕获异常,记录日志后抛出自定义业务异常,再在控制器层处理:

@Transactional
public void foobar() throws DownloadConflictException {
    log.debug("Thread " + Thread.currentThread().getName() + " Trying to lock");
    try {
        Map<String, Object> lockProps = new HashMap<>();
        lockProps.put("javax.persistence.lock.timeout", 0);
        entityManager.lock(nachricht, LockModeType.PESSIMISTIC_WRITE, lockProps);
        entityManager.refresh(nachricht);
    } catch (PessimisticLockException e) {
        log.warn("Thread " + Thread.currentThread().getName() + " 无法获取锁,资源正在被下载");
        throw new DownloadConflictException("当前资源正在被其他用户下载,请稍后重试");
    }

    log.debug("Thread " + Thread.currentThread().getName() + " 获取锁成功");
    log.debug("Thread " + Thread.currentThread().getName() + " Löschdatun: " + nachricht.getLoeschDatum());
    log.debug("Thread " + Thread.currentThread().getName() + " start download");

    try {
        Thread.sleep(10000);
        // 执行你的下载逻辑
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        log.error("下载线程被中断", e);
        throw new RuntimeException("下载被中断");
    }

    log.debug("Thread " + Thread.currentThread().getName() + " end download");
}

2. 在控制器层处理异常

定义全局异常处理器,捕获自定义的DownloadConflictException,返回友好的HTTP响应:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(DownloadConflictException.class)
    public ResponseEntity<String> handleDownloadConflict(DownloadConflictException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
    }
}

这样,第二个请求的用户会收到409 Conflict状态码和清晰的提示信息,第一个线程也能正常完成事务,不会再出现连接关闭的问题。


内容的提问来源于stack exchange,提问作者Jerome F

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 17:57:44