Skip to content

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 重试策略优化

  1. 指数退避:重试间隔逐渐增加,避免系统压力过大
  2. 最大重试限制:防止无限重试消耗资源
  3. 异常分类:只对特定类型的异常进行重试
  4. 监控告警:记录重试次数和成功率,便于问题排查 :::

::: 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 实现的重试机制为我们提供了一种优雅的方式来处理并发异常:

  1. 关注点分离:重试逻辑与业务逻辑完全分离
  2. 可配置性:重试次数、延迟等参数可灵活配置
  3. 精准控制:通过注解精确控制哪些方法需要重试
  4. 易于维护:统一的重试逻辑,便于修改和优化

TIP

记住,AOP 不仅仅是一个技术工具,更是一种设计思想。它帮助我们构建更加模块化、可维护的应用程序。在实际项目中,合理运用 AOP 可以显著提升代码质量和开发效率!

🎉 现在你已经掌握了使用 Spring AOP 构建智能重试机制的核心技能,快去在你的项目中实践吧!