异常处理与重试
在生产环境中,数据库操作可能因网络抖动、连接超时、死锁、主键冲突等原因失败。合理的异常处理策略能够区分可重试的瞬态故障和不可重试的业务异常,配合指数退避重试机制,在保证数据一致性的同时提升系统的可用性。
MyBatis 异常体系
异常继承结构
Java
java.lang.Exception
└── java.sql.SQLException
└── org.apache.ibatis.exceptions.PersistenceException // MyBatis 顶层异常
├── BindingException // Mapper 绑定异常(配置错误,不可重试)
├── ExecutorException // 执行器异常
├── ExpressionException // OGNL 表达式异常
├── IncompleteElementException// XML 元素不完整
├── TooManyResultsException // 期望单条结果但返回多条
└── TypeException // 类型转换异常
异常分类与重试策略
| 异常类型 | 典型场景 | 是否可重试 | 处理策略 |
|---|---|---|---|
| 瞬态故障 | 连接超时、死锁、临时网络中断 | 是 | 指数退避重试 |
| 配置错误 | Mapper 未找到、SQL 语法错误 | 否 | 立即失败,人工修复 |
| 业务异常 | 唯一约束冲突、外键约束 | 否 | 返回业务错误码 |
| 数据异常 | 类型转换失败、结果集映射错误 | 否 | 立即失败,记录日志 |
Java
// 判断是否为瞬态故障
public class TransientExceptionDetector {
// SQL Server 死锁错误码
private static final int SQL_SERVER_DEADLOCK = 1205;
// MySQL 死锁错误码
private static final int MYSQL_DEADLOCK = 1213;
// Oracle 死锁错误码
private static final int ORACLE_DEADLOCK = 60;
// 连接超时
private static final int CONNECTION_TIMEOUT = 121;
private static final Set<Integer> TRANSIENT_CODES = Set.of(
SQL_SERVER_DEADLOCK, MYSQL_DEADLOCK, ORACLE_DEADLOCK,
CONNECTION_TIMEOUT
);
public static boolean isTransient(Throwable e) {
Throwable cause = e;
while (cause != null) {
if (cause instanceof SQLTimeoutException) {
return true;
}
if (cause instanceof SQLException sqlEx) {
if (TRANSIENT_CODES.contains(sqlEx.getErrorCode())) {
return true;
}
String state = sqlEx.getSQLState();
// SQLState 08xxx = 连接异常,40xxx = 事务回滚
if (state != null && (state.startsWith("08") || state.startsWith("40"))) {
return true;
}
}
cause = cause.getCause();
}
return false;
}
}
重试机制实现
指数退避重试器
Java
public class RetryTemplate {
private final int maxRetries;
private final long initialDelayMs;
private final double backoffMultiplier;
private final long maxDelayMs;
public RetryTemplate(int maxRetries, long initialDelayMs,
double backoffMultiplier, long maxDelayMs) {
this.maxRetries = maxRetries;
this.initialDelayMs = initialDelayMs;
this.backoffMultiplier = backoffMultiplier;
this.maxDelayMs = maxDelayMs;
}
/**
* 执行可重试操作
*/
public <T> T execute(Supplier<T> operation, String operationName) {
long delayMs = initialDelayMs;
Exception lastException = null;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
sleepWithJitter(delayMs);
delayMs = (long) Math.min(delayMs * backoffMultiplier, maxDelayMs);
}
return operation.get();
} catch (Exception e) {
lastException = e;
if (!TransientExceptionDetector.isTransient(e)) {
// 非瞬态故障,立即抛出,不重试
throw new RuntimeException(
"Non-transient exception, will not retry: " + operationName, e);
}
if (attempt < maxRetries) {
log.warn("[RETRY] Attempt {} failed for {}, will retry after {}ms: {}",
attempt + 1, operationName, delayMs, e.getMessage());
}
}
}
throw new RuntimeException(
"Operation " + operationName + " failed after " + (maxRetries + 1) + " attempts",
lastException);
}
/**
* 休眠 + 随机抖动,避免惊群效应
*/
private void sleepWithJitter(long delayMs) {
try {
// 添加 +/- 20% 随机抖动
long jitter = (long) (delayMs * 0.2 * (Math.random() - 0.5));
Thread.sleep(delayMs + jitter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", e);
}
}
}
Spring 集成使用
Java
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
return new RetryTemplate(
3, // 最多重试 3 次
1000, // 初始延迟 1s
2.0, // 指数退避倍数
10000 // 最大延迟 10s
);
}
}
Java
@Service
public class OrderService {
private final RetryTemplate retryTemplate;
private final OrderMapper orderMapper;
public OrderService(RetryTemplate retryTemplate, OrderMapper orderMapper) {
this.retryTemplate = retryTemplate;
this.orderMapper = orderMapper;
}
public Order getOrderWithRetry(Long id) {
return retryTemplate.execute(
() -> orderMapper.selectById(id),
"selectOrderById(" + id + ")"
);
}
public void insertOrderWithRetry(Order order) {
retryTemplate.execute(
() -> {
orderMapper.insert(order);
return null;
},
"insertOrder(" + order.getId() + ")"
);
}
}
使用 Spring Retry 注解
Java
@Configuration
@EnableRetry
public class SpringRetryConfig {}
XML
@Service
public class UserService {
@Retryable(
value = {SQLTimeoutException.class, DeadlockLoserDataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2.0, maxDelay = 10000)
)
public User selectUserById(Long id) {
return userMapper.selectById(id);
}
@Recover
public User recoverFromTimeout(SQLTimeoutException e, Long id) {
log.error("Failed to select user {} after all retries: {}", id, e.getMessage());
// 返回缓存数据或默认值
return null;
}
}
超时处理
三层超时设置
| 层级 | 配置 | 含义 | 推荐值 |
|---|---|---|---|
| 连接获取超时 | connection-timeout | 从连接池获取连接的最大等待时间 | 30s |
| SQL 执行超时 | defaultStatementTimeout | 单条 SQL 执行的最大执行时间 | 30s-60s |
| 事务超时 | @Transactional(timeout = ...) | 整个事务的最大执行时间 | 根据业务场景 |
XML
<!-- mybatis-config.xml -->
<configuration>
<settings>
<!-- 全局 SQL 执行超时时间(秒) -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 默认 fetch 大小 -->
<setting name="defaultFetchSize" value="100"/>
</settings>
</configuration>
Java
<!-- 单个语句级的超时设置 -->
<select id="selectLargeData" resultType="User" timeout="60">
SELECT * FROM users WHERE status = 'ACTIVE'
</select>
Java
// 事务级超时
@Transactional(timeout = 10) // 10 秒事务超时
public void batchProcessOrders(List<Long> orderIds) {
for (Long orderId : orderIds) {
orderMapper.updateStatus(orderId, "PROCESSING");
}
}
超时异常处理
Java
public class TimeoutExceptionHandler {
/**
* 区分不同类型的超时
*/
public void handleTimeout(SQLException e) {
String sqlState = e.getSQLState();
if ("HY000".equals(sqlState) || "70100".equals(sqlState)) {
// 查询超时:可能是慢查询,建议记录并告警
log.warn("Query timeout: consider adding index or optimizing SQL");
} else if (sqlState != null && sqlState.startsWith("08")) {
// 连接超时:网络或连接池问题
log.error("Connection timeout: check network and connection pool");
} else if ("40001".equals(sqlState)) {
// 序列化隔离级别冲突
log.warn("Serialization failure: retry the transaction");
}
}
}
死锁处理
死锁检测与自动重试
Java
@Service
public class InventoryService {
@Transactional
public void transferStock(Long fromId, Long toId, int quantity) {
// 按 ID 排序加锁顺序,减少死锁概率
Long firstId = Math.min(fromId, toId);
Long secondId = Math.max(fromId, toId);
// 先锁定 ID 小的记录,再锁定 ID 大的记录
stockMapper.lockStock(firstId);
stockMapper.lockStock(secondId);
stockMapper.deduct(fromId, quantity);
stockMapper.add(toId, quantity);
}
}
统一异常处理器(Spring Boot)
SQL
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(PersistenceException.class)
public ResponseEntity<ErrorResponse> handlePersistenceException(PersistenceException e) {
Throwable cause = e.getCause();
// 瞬态故障:返回 503 服务不可用,客户端可重试
if (TransientExceptionDetector.isTransient(e)) {
log.error("Transient database error", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse("DB_TRANSIENT_ERROR", "数据库暂时不可,请稍后重试"));
}
// Mapper 配置错误:返回 500
if (cause instanceof BindingException) {
log.error("MyBatis binding error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("DB_CONFIG_ERROR", "数据库配置错误,请联系管理员"));
}
// 其他数据库异常
log.error("Unexpected database error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("DB_ERROR", "数据库操作失败"));
}
@ExceptionHandler(TooManyResultsException.class)
public ResponseEntity<ErrorResponse> handleTooManyResults(TooManyResultsException e) {
log.error("Expected one result but got multiple", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("DATA_ERROR", "数据异常:期望单条结果"));
}
}
重试幂等性保障
唯一约束保证幂等
Java
-- 使用唯一约束防止重试导致的重复插入
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL UNIQUE, -- 唯一约束
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
text
@Service
public class OrderCreateService {
public Order createOrderWithRetry(Order order) {
return retryTemplate.execute(() -> {
try {
orderMapper.insert(order);
return order;
} catch (PersistenceException e) {
// 唯一约束冲突:说明重试时已插入成功
if (isDuplicateKeyException(e)) {
log.warn("Duplicate key on retry, order already exists: {}", order.getOrderNo());
return orderMapper.selectByOrderNo(order.getOrderNo());
}
throw e;
}
}, "createOrder");
}
private boolean isDuplicateKeyException(PersistenceException e) {
Throwable cause = e.getCause();
while (cause != null) {
if (cause instanceof SQLIntegrityConstraintViolationException) {
return true;
}
cause = cause.getCause();
}
return false;
}
}
注意事项
- 仅重试瞬态故障:配置错误、SQL 语法错误、业务异常等不可重试的场景应直接失败
- 重试次数限制:最大重试次数建议 3 次,过多重试会占用连接池资源
- 指数退避 + 抖动:固定间隔重试会导致"惊群效应",所有请求同时重试造成瞬时压力峰值
- 重试幂等性:重试操作必须是幂等的,通过唯一约束、乐观锁等机制保证
- 超时分层:连接获取超时、SQL 执行超时、事务超时分三层设置,逐层缩短时间
要点总结
- MyBatis 异常分为瞬态故障(连接超时、死锁)和非瞬态故障(配置错误、业务异常),仅瞬态故障可重试
- 瞬态故障通过 SQLException 的 SQLState(08xxx 连接异常、40xxx 事务回滚)和错误码识别
- 重试使用指数退避策略(初始 1s,倍数 2.0,最大 10s),添加 +/- 20% 随机抖动避免惊群效应
- 超时分三层:连接获取超时 30s、SQL 执行超时 30s-60s、事务超时根据业务场景设置
- 死锁通过统一加锁顺序(按 ID 排序)减少发生概率,配合自动重试机制处理
- 重试操作必须保证幂等性,通过唯一约束防止重复插入,或在重试时捕获唯一约束异常并返回已有数据
- 全局异常处理器区分瞬态故障(返回 503 可重试)和不可重试异常(返回 500)
存放路径:D:\git2\jwdev\articles\MYBATIS\专家\生产环境最佳实践\异常处理与重试.md
📝 发现内容有误?点击此处直接编辑