Appearance
Spring AOP 实战指南:优雅解决并发重试问题 🎯
引言:为什么需要 AOP 重试机制?
在企业级应用开发中,我们经常遇到这样的场景:由于数据库锁竞争、网络抖动等并发问题,业务服务偶尔会执行失败。如果简单地向用户抛出异常,用户体验会很糟糕。但如果在每个业务方法中都编写重试逻辑,代码会变得冗余且难以维护。
TIP
这正是 AOP(面向切面编程)大显身手的时候!通过 AOP,我们可以将重试逻辑从业务代码中抽离出来,实现关注点分离,让代码更加优雅和可维护。
核心概念理解
什么是幂等操作?
幂等操作是指无论执行多少次,结果都相同的操作。例如:
- ✅ 查询用户信息(多次查询结果一致)
- ✅ 设置用户状态为"已激活"(重复设置不会产生副作用)
- ❌ 创建订单(重复执行会产生多个订单)
- ❌ 扣减库存(重复执行会导致库存错误)
IMPORTANT
只有幂等操作才适合自动重试,非幂等操作的重试可能会导致数据不一致或业务逻辑错误。
实战案例:构建智能重试切面
1. 基础重试切面实现
让我们先看看如何实现一个基础的重试切面:
kotlin
@Aspect
@Component
class ConcurrentOperationExecutor : Ordered {
companion object {
private const val DEFAULT_MAX_RETRIES = 2
}
// 最大重试次数,可通过配置调整
var maxRetries = DEFAULT_MAX_RETRIES
// 切面执行顺序,数值越小优先级越高
private var order = 1
override fun getOrder(): Int = order
fun setOrder(order: Int) {
this.order = order
}
/**
* 环绕通知:在目标方法执行前后进行重试逻辑处理
* 切点表达式匹配所有业务服务方法
*/
@Around("com.xyz.CommonPointcuts.businessService()")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException?
do {
numAttempts++
try {
// 尝试执行目标方法
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
// 捕获并发锁异常,准备重试
lockFailureException = ex
println("第 $numAttempts 次执行失败,准备重试...")
}
} while (numAttempts <= maxRetries)
// 重试次数用完,抛出最后一次的异常
throw lockFailureException!!
}
}
2. Spring 配置
kotlin
@Configuration
@EnableAspectJAutoProxy // 启用 AspectJ 自动代理
class ApplicationConfiguration {
@Bean
fun concurrentOperationExecutor() = ConcurrentOperationExecutor().apply {
maxRetries = 3 // 设置最大重试3次
order = 100 // 设置切面优先级
}
}
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy />
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
</beans>
进阶:精准控制重试范围
3. 定义幂等注解
为了更精确地控制哪些方法可以重试,我们定义一个标记注解:
kotlin
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
// 标记注解:用于标识幂等操作
annotation class Idempotent
4. 优化后的重试切面
kotlin
@Aspect
@Component
class SmartRetryExecutor : Ordered {
private val logger = LoggerFactory.getLogger(SmartRetryExecutor::class.java)
var maxRetries = 3
private var order = 100
override fun getOrder(): Int = order
/**
* 精准切点:只对标注了 @Idempotent 的业务服务方法进行重试
*/
@Around("execution(* com.xyz..service.*.*(..)) && " +
"@annotation(com.xyz.service.Idempotent)")
fun doIdempotentOperation(pjp: ProceedingJoinPoint): Any? {
val methodName = pjp.signature.name
var numAttempts = 0
var lastException: Exception? = null
do {
numAttempts++
try {
logger.debug("执行方法 {} - 第 {} 次尝试", methodName, numAttempts)
return pjp.proceed(pjp.args)
} catch (ex: PessimisticLockingFailureException) {
lastException = ex
logger.warn("方法 {} 第 {} 次执行失败: {}",
methodName, numAttempts, ex.message)
// 可以在这里添加延迟重试逻辑
if (numAttempts < maxRetries) {
Thread.sleep(100 * numAttempts) // 递增延迟
}
}
} while (numAttempts <= maxRetries)
logger.error("方法 {} 重试 {} 次后仍然失败", methodName, maxRetries)
throw lastException!!
}
}
实际应用示例
5. 业务服务实现
kotlin
@Service
class UserService {
private val logger = LoggerFactory.getLogger(UserService::class.java)
/**
* 幂等操作:更新用户状态
* 无论执行多少次,最终状态都是一致的
*/
@Idempotent
fun activateUser(userId: Long): Boolean {
logger.info("正在激活用户: {}", userId)
// 模拟可能发生的并发锁异常
if (Math.random() < 0.3) { // 30% 概率失败
throw PessimisticLockingFailureException("用户状态更新时发生锁冲突")
}
// 实际的业务逻辑
return updateUserStatus(userId, UserStatus.ACTIVE)
}
/**
* 非幂等操作:创建订单
* 不应该自动重试,因为可能创建重复订单
*/
fun createOrder(userId: Long, productId: Long): Order {
logger.info("为用户 {} 创建订单,产品: {}", userId, productId)
// 这里不使用 @Idempotent 注解
// 即使发生异常也不会自动重试
return orderRepository.save(Order(userId, productId))
}
private fun updateUserStatus(userId: Long, status: UserStatus): Boolean {
// 模拟数据库更新操作
return true
}
}
执行流程可视化
让我们通过时序图来理解重试机制的执行流程:
关键技术点解析
为什么使用 @Around
通知?
NOTE
@Around
是最强大的通知类型,它可以:
- 在目标方法执行前后执行自定义逻辑
- 控制是否执行目标方法(通过
pjp.proceed()
) - 修改方法参数和返回值
- 多次调用目标方法(实现重试)
切面优先级的重要性
kotlin
// 设置切面执行顺序
override fun getOrder(): Int = 100
IMPORTANT
当存在多个切面时,执行顺序很重要:
- 数值越小,优先级越高
- 重试切面通常应该在事务切面之外执行
- 这样每次重试都会开启新的事务
最佳实践建议
::: tip 重试策略优化
- 指数退避:重试间隔逐渐增加,避免系统压力过大
- 最大重试限制:防止无限重试消耗资源
- 异常分类:只对特定类型的异常进行重试
- 监控告警:记录重试次数和成功率,便于问题排查 :::
::: warning 注意事项
- 只对幂等操作使用自动重试
- 避免在重试逻辑中产生新的副作用
- 考虑重试对系统性能的影响
- 合理设置重试次数和间隔时间 :::
扩展功能实现
高级重试策略实现
kotlin
@Aspect
@Component
class AdvancedRetryExecutor {
@Around("@annotation(retryable)")
fun executeWithRetry(pjp: ProceedingJoinPoint, retryable: Retryable): Any? {
val strategy = when (retryable.strategy) {
RetryStrategy.FIXED_DELAY -> FixedDelayStrategy(retryable.delay)
RetryStrategy.EXPONENTIAL_BACKOFF -> ExponentialBackoffStrategy(retryable.delay)
}
repeat(retryable.maxAttempts) { attempt ->
try {
return pjp.proceed()
} catch (ex: Exception) {
if (attempt == retryable.maxAttempts - 1) throw ex
if (retryable.retryFor.any { it.isInstance(ex) }) {
strategy.sleep(attempt)
} else {
throw ex
}
}
}
return null
}
}
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Retryable(
val maxAttempts: Int = 3,
val delay: Long = 1000,
val strategy: RetryStrategy = RetryStrategy.FIXED_DELAY,
val retryFor: Array<KClass<out Exception>> = [Exception::class]
)
enum class RetryStrategy {
FIXED_DELAY,
EXPONENTIAL_BACKOFF
}
总结
通过 Spring AOP 实现的重试机制为我们提供了一种优雅的方式来处理并发异常:
- 关注点分离:重试逻辑与业务逻辑完全分离
- 可配置性:重试次数、延迟等参数可灵活配置
- 精准控制:通过注解精确控制哪些方法需要重试
- 易于维护:统一的重试逻辑,便于修改和优化
TIP
记住,AOP 不仅仅是一个技术工具,更是一种设计思想。它帮助我们构建更加模块化、可维护的应用程序。在实际项目中,合理运用 AOP 可以显著提升代码质量和开发效率!
🎉 现在你已经掌握了使用 Spring AOP 构建智能重试机制的核心技能,快去在你的项目中实践吧!