RedisTemplate 的 public 方法不能直接切,因其大量操作最终委托给 execute 方法(尤其是带 RedisCallback 的重载),而 opsForValue().get() 等链式调用的耗时也集中在 execute 或 executePipelined;只切 get/set 会遗漏回调、事务、Pipeline 场景,且 Spring Data Redis 5.x+ 默认 Lettuce 是异步非阻塞的,execute 可能返回 CompletionStage/Mono,同步 AOP 无法准确测真实耗时。

为什么不能直接切 RedisTemplate 的 public 方法
因为 RedisTemplate 大量操作最终会委托给 execute 方法(尤其是带 RedisCallback 的重载),而像 opsForValue().get() 这类链式调用,实际耗时集中在底层 execute 或 executePipelined。只切 get()、set() 等方法会漏掉回调式调用、事务、Pipeline 场景,日志不全。
更关键的是:Spring Data Redis 5.x 起默认使用 Lettuce,其连接是异步非阻塞的,execute 返回的可能是 CompletionStage 或响应式类型(如 Mono),同步 AOP 拦截无法准确测出真实耗时。
- 优先切
execute和executePipelined这两个核心模板方法 - 若项目已升级到 Spring Boot 3 + Redis 6+ 响应式栈,需改用
ReactiveRedisTemplate配合Mono.usingWhen或自定义ExchangeFilterFunction,AOP 不适用 - 避免切
JedisConnectionFactory或LettuceClientConfigurationBuilder—— 它们不承载业务逻辑,切了也记不到操作级耗时
如何写一个能覆盖 sync/async 场景的 AOP 切面
核心思路:对 RedisTemplate.execute* 方法做环绕通知,但要区分返回值类型做适配。同步场景直接记录;Lettuce 异步场景下,execute 可能返回 Future 或 CompletionStage,需在回调中补录耗时。
@Around("execution(* org.springframework.data.redis.core.RedisTemplate.execute*(..))")
public Object logRedisExecution(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
String methodName = joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
<pre class="brush:php;toolbar:false;"> // 同步结果:直接记录
if (!(result instanceof CompletionStage) && !(result instanceof Future)) {
logTiming(methodName, System.nanoTime() - start);
return result;
}
// 异步结果:包装为新 CompletableFuture,确保回调里记录耗时
if (result instanceof CompletionStage) {
return ((CompletionStage<?>) result)
.whenComplete((r, ex) -> logTiming(methodName, System.nanoTime() - start));
}
if (result instanceof Future) {
return CompletableFuture.supplyAsync(() -> {
try {
Object r = ((Future<?>) result).get();
logTiming(methodName, System.nanoTime() - start);
return r;
} catch (Exception e) {
logTiming(methodName, System.nanoTime() - start);
throw new RuntimeException(e);
}
});
}
return result;
} catch (Throwable t) {
logTiming(methodName, System.nanoTime() - start, t);
throw t;
}}
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 不要用
@Order或高优先级增强——可能干扰TransactionInterceptor,导致事务内 Redis 耗时统计失真 -
logTiming方法建议用org.slf4j.Logger.atDebug()+ lazy 字符串拼接,避免日志级别关闭时仍构造消息 - 若项目混用
StringRedisTemplate,需额外加切点:execution(* org.springframework.data.redis.core.StringRedisTemplate.execute*(..))
怎样避免日志爆炸和线程上下文污染
高频 Redis 操作(如秒杀查库存)每秒数百次,全量打日志会拖垮磁盘 IO 和 GC。必须分级控制:
- 默认只记录 >100ms 的慢操作,阈值用配置项驱动:
spring.redis.aop.slow-threshold-ms=100 - 对
scan、keys、slowlog等高危命令强制记录(无论快慢),防止误用 - 禁止在日志中打印
RedisCallback的入参(尤其是byte[]key/value),容易 OOM;可记录key.toString()或哈希摘要 - 若用了 MDC,确保
ThreadLocal在异步回调中能透传(Lettuce 默认不继承,需显式CompletableFuture.delayedExecutor或ThreadPoolTaskExecutor配置taskDecorator)
为什么 @EnableAspectJAutoProxy(proxyTargetClass = true) 是必须的
RedisTemplate 是普通 Java 类,不是接口。Spring 默认用 JDK 动态代理(基于接口),而 RedisTemplate 没实现公共接口(它实现的是 InitializingBean 等内部标记接口),JDK 代理会失效,切面完全不触发。
必须启用 CGLIB 代理,否则你写的 @Around 看似生效,实则一概拦截不到。
- 在主启动类或配置类上加
@EnableAspectJAutoProxy(proxyTargetClass = true) - 确认 classpath 有
spring-boot-starter-aop且版本与 Spring Boot 匹配(Boot 3.x 需 AOP 6.x) - 如果项目用了 Lombok 的
@AllArgsConstructor或@RequiredArgsConstructor,注意 CGLIB 代理会绕过构造器注入,导致RedisTemplate字段为 null —— 改用@Setter(onMethod_ = @Autowired)或字段注入
最常被忽略的一点:Lettuce 的 StatefulRedisConnection 是共享的,AOP 记录的“单次操作耗时”其实包含连接复用、命令排队、网络往返等复合延迟,它反映的是业务视角的感知延迟,不是纯 Redis 执行时间。要定位真正瓶颈,得配合 redis-cli --latency 和 Lettuce 的 CommandLatencyCollector。

















