(2) 缓存更新注解一、场景需求
在高并发系统中,缓存是提升性能的关键组件。而Cache-Aside模式作为最常用的缓存策略之一,要求开发者手动管理缓存与数据库的交互。本文将结合自定义注解与Redisson客户端,实现声明式的缓存管理方案。
二、方案亮点
🚀 零侵入性:通过注解实现缓存逻辑
🔒 完整防护:解决缓存穿透/击穿/雪崩问题
⚡ 双删策略:保障数据库与缓存一致性
🛠️ 逻辑删除:支持数据恢复与审计需求
三、核心实现
1. 环境准备
Maven依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 定义注解
(1) 缓存查询注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheableData {
String key(); // 缓存键(支持SpEL)
int expire() default 3600; // 过期时间(秒)
int nullExpire() default 300; // 空值缓存时间
}
(2) 缓存更新注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheUpdateData {
String key(); // 需删除的缓存键
boolean logicalDelete() default false;
}
(3) 分布式锁注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheLock {
String lockKey(); // 锁的键(SpEL)
int waitTime() default 3; // 获取锁等待时间(秒)
int leaseTime() default 10; // 锁持有时间
}
3. AOP切面实现
@Aspect
@Component
public class CacheAspect {
@Autowired
private RedissonClient redisson;
private static final String NULL_PLACEHOLDER = "##NULL##";
// 读操作切面
@Around("@annotation(cacheable)")
public Object aroundCacheable(ProceedingJoinPoint joinPoint,
CacheableData cacheable) throws Throwable {
// 解析SpEL生成缓存键
String key = parseKey(cacheable.key(), joinPoint);
// 1. 检查缓存
RBucket<Object> bucket = redisson.getBucket(key);
Object cachedValue = bucket.get();
if (cachedValue != null) {
if (NULL_PLACEHOLDER.equals(cachedValue)) return null;
if (isLogicalDeleted(cachedValue)) return null;
return cachedValue;
}
// 2. 执行原方法(查询数据库)
Object dbResult = joinPoint.proceed();
// 3. 回写缓存
if (dbResult == null) {
bucket.set(NULL_PLACEHOLDER, cacheable.nullExpire(), TimeUnit.SECONDS);
} else {
bucket.set(dbResult, cacheable.expire(), TimeUnit.SECONDS);
}
return dbResult;
}
// 更新操作切面(带双删)
@Around("@annotation(cacheUpdate)")
public Object aroundUpdate(ProceedingJoinPoint joinPoint,
CacheUpdateData cacheUpdate) throws Throwable {
String key = parseKey(cacheUpdate.key(), joinPoint);
// 第一次删除
redisson.getBucket(key).delete();
// 执行数据库操作
Object result = joinPoint.proceed();
// 延迟双删(1秒后二次删除)
redisson.getDelayedQueue(redisson.getQueue("cache:delete:queue"))
.offer(key, 1, TimeUnit.SECONDS);
// 处理逻辑删除
if (cacheUpdate.logicalDelete()) {
markLogicalDelete(key);
}
return result;
}
// 其他辅助方法省略,完整代码见文末Github链接
}
4. 业务层使用示例
@Service
public class UserService {
// 带防击穿的查询方法
@CacheableData(key = "user:#userId", expire = 7200)
@CacheLock(lockKey = "user_lock:#userId")
public User getUserById(Long userId) {
return userDao.findById(userId);
}
// 更新用户信息
@CacheUpdateData(key = "user:#user.id")
public void updateUser(User user) {
userDao.update(user);
}
// 逻辑删除用户
@CacheUpdateData(key = "user:#id", logicalDelete = true)
public void deleteUser(Long id) {
userDao.logicalDelete(id);
}
}
四、方案总结
七:踩坑指南
- 序列化问题:推荐使用JSON序列化,避免Java序列化的版本兼容问题
- 锁超时设置:分布式锁的leaseTime应大于业务执行时间
- 内存泄漏:逻辑删除数据必须设置TTL
- SpEL解析:复杂表达式建议使用Spring的ExpressionParse