JDBC表锁获取失败异常处理及遗留代码重构方案咨询
遇到这种锁冲突导致的执行失败,又不想给几十条语句挨个加重试样板代码,确实得从代码复用和横向切面入手,下面几个重构方案可以帮你批量解决问题,同时保证最终执行成功率:
方案1:用AOP统一织重重试逻辑(侵入性最低)
如果你的技术栈支持AOP(比如Java的Spring/AspectJ),这是最省心的方案——把重试逻辑做成通用切面,只需要给目标方法打个标记,不用修改业务代码本身。
具体实现思路:
- 定义一个自定义注解(比如
@RetryOnLockFailure),用来标记所有需要处理锁冲突重试的数据访问方法 - 编写切面类,拦截带有该注解的方法,捕获特定的锁冲突异常:
- MS-SQL的锁超时错误码是
1205 - H2的锁超时对应SQLState
40001,或异常信息包含LOCK_TIMEOUT
- MS-SQL的锁超时错误码是
- 在切面内实现重试逻辑(比如最多重试3次,每次间隔100ms,参数可配置)
示例切面代码:
@Aspect @Component public class LockRetryAspect { private static final int MAX_RETRIES = 3; private static final long RETRY_DELAY = 100; @Around("@annotation(com.yourpackage.RetryOnLockFailure)") public Object retryOnLockFailure(ProceedingJoinPoint joinPoint) throws Throwable { int retryCount = 0; while (true) { try { return joinPoint.proceed(); } catch (SQLException e) { if (isLockConflictException(e) && retryCount < MAX_RETRIES) { retryCount++; Thread.sleep(RETRY_DELAY); continue; } throw e; } } } private boolean isLockConflictException(SQLException e) { // 匹配MS-SQL锁超时 if (e.getErrorCode() == 1205) return true; // 匹配H2锁超时 if ("40001".equals(e.getSQLState()) || e.getMessage().contains("LOCK_TIMEOUT")) return true; return false; } }
优缺点:
- ✅ 几乎无侵入,业务代码只需加注解
- ✅ 统一管理重试规则,修改方便
- ❌ 依赖AOP框架,非Java栈可能需要找对应工具
方案2:封装通用重试数据访问模板类
把所有独立的SQL执行逻辑,抽象到一个模板类的回调中,由模板统一处理连接、重试、资源关闭——相当于自己实现一个带重试的JdbcTemplate。
具体实现思路:
- 定义模板类,提供
execute方法,接收连接提供者和SQL执行回调 - 模板内部封装重试循环、异常判断、资源关闭逻辑
- 遗留代码中的SQL执行逻辑,改为调用模板的
execute方法,只关注SQL和结果处理
示例模板代码:
public class RetryableJdbcTemplate { private static final int MAX_RETRIES = 3; private static final long RETRY_DELAY = 100; public <T> T execute(ConnectionSupplier connSupplier, JdbcCallback<T> callback) throws SQLException { int retryCount = 0; while (true) { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = connSupplier.get(); stmt = conn.createStatement(); return callback.execute(stmt, conn); } catch (SQLException e) { if (isLockConflictException(e) && retryCount < MAX_RETRIES) { retryCount++; Thread.sleep(RETRY_DELAY); continue; } throw e; } finally { // 统一关闭资源,避免遗留代码的资源泄漏 if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } } } // 函数式接口,简化回调编写 @FunctionalInterface public interface ConnectionSupplier { Connection get() throws SQLException; } @FunctionalInterface public interface JdbcCallback<T> { T execute(Statement stmt, Connection conn) throws SQLException; } private boolean isLockConflictException(SQLException e) { // 同AOP中的异常判断逻辑 if (e.getErrorCode() == 1205) return true; if ("40001".equals(e.getSQLState()) || e.getMessage().contains("LOCK_TIMEOUT")) return true; return false; } }
遗留代码重构示例:
// 原来的代码:自己管理连接、语句、资源 Connection conn = DriverManager.getConnection(url, user, pwd); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM user WHERE id = ?"); // ...处理结果,手动关闭资源 // 重构后: RetryableJdbcTemplate template = new RetryableJdbcTemplate(); User user = template.execute( () -> DriverManager.getConnection(url, user, pwd), (stmt, conn) -> { ResultSet rs = stmt.executeQuery("SELECT * FROM user WHERE id = 1"); User u = new User(); if (rs.next()) { u.setId(rs.getInt("id")); u.setName(rs.getString("name")); } return u; } );
优缺点:
- ✅ 统一管理资源关闭,解决遗留代码可能的资源泄漏问题
- ✅ 重试逻辑集中维护,便于调整规则
- ❌ 需要修改所有遗留代码的调用方式,但比挨个加重试循环高效得多
方案3:代理模式包装Connection/Statement
通过动态代理或静态代理,包装JDBC的Connection和Statement,拦截执行方法自动处理重试——几乎不用改业务代码,只需要替换获取连接的逻辑。
具体实现思路:
- 创建
RetryableConnection代理类,包装真实的Connection,其createStatement方法返回代理的RetryableStatement - 在
RetryableStatement中拦截executeQuery、executeUpdate等方法,加入重试逻辑 - 在遗留代码获取连接的地方,把真实连接替换成代理连接
示例代理代码(简化版):
public class RetryableConnection implements Connection { private final Connection delegate; private final int maxRetries; private final long retryDelay; public RetryableConnection(Connection delegate, int maxRetries, long retryDelay) { this.delegate = delegate; this.maxRetries = maxRetries; this.retryDelay = retryDelay; } @Override public Statement createStatement() throws SQLException { return new RetryableStatement(delegate.createStatement(), maxRetries, retryDelay); } // 其他Connection方法全部委托给delegate(可通过动态代理简化实现) @Override public void close() throws SQLException { delegate.close(); } // ...省略其他方法 } public class RetryableStatement implements Statement { private final Statement delegate; private final int maxRetries; private final long retryDelay; public RetryableStatement(Statement delegate, int maxRetries, long retryDelay) { this.delegate = delegate; this.maxRetries = maxRetries; this.retryDelay = retryDelay; } @Override public ResultSet executeQuery(String sql) throws SQLException { int retryCount = 0; while (true) { try { return delegate.executeQuery(sql); } catch (SQLException e) { if (isLockConflictException(e) && retryCount < maxRetries) { retryCount++; try { Thread.sleep(retryDelay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new SQLException("Retry interrupted", ie); } continue; } throw e; } } } // 其他execute方法同理实现重试,其余方法委托给delegate private boolean isLockConflictException(SQLException e) { // 同之前的异常判断逻辑 if (e.getErrorCode() == 1205) return true; if ("40001".equals(e.getSQLState()) || e.getMessage().contains("LOCK_TIMEOUT")) return true; return false; } }
遗留代码修改点:
// 原来的代码 Connection conn = DriverManager.getConnection(url, user, pwd); // 重构后 Connection conn = new RetryableConnection(DriverManager.getConnection(url, user, pwd), 3, 100);
优缺点:
- ✅ 业务代码几乎不用改,只需要替换连接获取逻辑
- ✅ 对业务逻辑透明,无需理解重试细节
- ❌ 需要实现JDBC接口的大量方法(可通过Java动态代理简化),对JDBC API细节要求高
关键注意事项:保证操作幂等性
无论用哪种方案,必须确保重试的SQL操作是幂等的!否则重试会导致数据错误:
- 查询操作天然幂等,重试无风险
- 更新操作尽量用基于当前状态的语句(比如
UPDATE table SET count = count +1 WHERE id = ?),而非固定值更新(UPDATE table SET count = 5 WHERE id = ?) - 插入操作可通过唯一约束+
INSERT IGNORE,或先查询再插入(需注意并发场景)
如果有非幂等的操作,必须先改造为幂等,再引入重试逻辑。
总结
- 若用Spring等支持AOP的框架,优先选方案1,侵入性最低,代码改动最少
- 若无AOP框架,方案2更可控,还能统一管理资源关闭,解决遗留代码的资源泄漏问题
- 若想最小化业务代码改动,方案3适合,但需要处理JDBC代理的细节
所有方案都要配合精准的异常判断,只重试锁冲突相关的异常,避免无意义的重试其他错误(比如SQL语法错误)。
内容的提问来源于stack exchange,提问作者Harald
相关产品推荐
相关产品推荐

