背景
在项目中我们常常需要使用Redis或者其他缓存方案,在项目中很多使用缓存的代码都是"八股文",实现缓存逻辑是一致的,但项目组每个人实现缓存的代码又很难保证一致,业务逻辑与缓存代码耦合,项目代码可读性下降。
缓存框架优点
- 实现业务逻辑与缓存代码解耦,保证业务代码"干净"。
- 提升开发效率,开发过程中可以更专注业务代码。
- 提升代码可读性。
- 避免重复实现缓存代码,减少人工实现缓存代码产生的BUG。
缓存框架实现思路
缓存伪代码
查询缓存伪代码
public Object testQueryPseudocode() {
// 1.查缓存
String cacheValue = getRedisValue(redisKey);
// 2.redis的值存在则反序列化并返回
if (cacheValue != null&&cacheValue.length()>0) {
return JSON.parseObject(cacheValue, Object.class);
}
// 3.从DB中获取数据
Object result = getDBValue();
// 4.将结果保存到Redis中
setRedisValue(redisKey, result);
return result;
}
删除缓存伪代码
public void testDeletePseudocode() {
// 1. 删除缓存
deleteRedisValue(redisKey);
// 2. 更新或删除业务逻辑
deleteDBValue();
}
思路分析
查询缓存伪代码中,这些步骤的顺序都不会改变,1、2、4步都属于缓存代码,这部分代码不会改变,3是业务逻辑代码。可以通过AOP中的环绕通知实现这部分代码。
删除缓存伪代码中,只需要在业务逻辑之前删除缓存,可以通过AOP的前置通知实现这部分代码。
AOP方式实现的伪代码
查询方法
在查询接口上需要在业务逻辑前查缓存,业务逻辑后设置缓存。由于传参不通,构建出缓存的key也不相同,我们可以通过注解@RedisGetKey判断接口是否要添加缓存,@RedisGetKey中的keyprefix和@RedisParam共同构建出redis的Key。
@RedisGetKey(keyPrefix = USER_REDIS_KEY_PREFIX)
public User getUserById(@RedisParam Long id) {
// todo 业务代码
}
更新/删除方法
在更新/删除方法前通常需要删除缓存中的数据,尽量保证缓存和DB中数据一致。可以通过@RedisDeleteKey判断方法是否要删除缓存,@RedisDeleteKey中的keyprefix和@RedisParam共同构建出redis的Key。
@RedisDeleteKey(keyPrefix = USER_REDIS_KEY_PREFIX)
public void deleteUser(@RedisParam Long id) {
// todo 业务代码
}
代码实现
1. 写缓存注解
查缓存的注解,可以标注在方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisGetKey {
/**
* RedisKey前缀
*
* @return
*/
String keyPrefix();
/**
* 缓存时间,单位是s
*
* @return
*/
long cachePeriod() default 7200L;
/**
* 方法是否需要缓存参数
*
* @return
*/
boolean paramNeed() default true;
}
删除缓存的注解,可以标注在方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisDeleteKey {
/**
* RedisKey前缀
*
* @return
*/
String keyPrefix();
/**
* 是否需要缓存参数
*
* @return
*/
boolean paramNeed() default true;
}
缓存参数注解,可以标注在参数上
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisParam {
}
2. 缓存切面
首先写一个根据方法签名和参数构建的缓存key的父类
public abstract class RedisAspect {
public String getParameterStr(MethodSignature methodSignature, Object[] args) {
StringBuilder paramStringBuilder = new StringBuilder();
Method method = methodSignature.getMethod();
Parameter[] parameters = method.getParameters();
// 1. 遍历参数,找出包含@RedisParam注解的参数,并且构建参数key
if (parameters != null && parameters.length > 0) {
for (int i = 0; i < parameters.length; i++) {
RedisParam redisParam = parameters[i].getDeclaredAnnotation(RedisParam.class);
if (redisParam != null) {
if (paramStringBuilder.length() != 0) {
paramStringBuilder.append(":");
}
paramStringBuilder.append(args[i]);
}
}
}
String paramStr = paramStringBuilder.toString();
if (StringUtils.isEmpty(paramStr)) {
throw new RuntimeException("Redis param can't be empty!");
}
return paramStr;
}
}
删除Redis缓存切面
@Aspect
@Component
@Slf4j
public class RedisDeleteAspect extends RedisAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut(value = "@annotation(com.linqi.webtest.redis.annotation.RedisDeleteKey)")
public void redisPointCut() {
}
@Before("redisPointCut() && @annotation(redisDeleteKey)")
public void doDeleteRedisKey(JoinPoint joinPoint, RedisDeleteKey redisDeleteKey) throws Throwable {
String paramStr = null;
// 1. 构建缓存key参数
if (redisDeleteKey.paramNeed()) {
paramStr = getParameterStr((MethodSignature) joinPoint.getSignature(), joinPoint.getArgs());
}
String redisFullKey = redisDeleteKey.keyPrefix() + paramStr;
// 2. 删除缓存
Boolean deleteResult = stringRedisTemplate.delete(redisFullKey);
log.info("delete redis key! redisKey={}, result={}", redisFullKey, deleteResult);
}
}
获取Redis缓存数据切面
@Aspect
@Component
@Slf4j
public class RedisGetAspect extends RedisAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut(value = "@annotation(com.linqi.webtest.redis.annotation.RedisGetKey)")
public void redisPointCut() {
}
@Around("redisPointCut() && @annotation(redisGetKey)")
public Object doRedisGet(ProceedingJoinPoint proceedingJoinPoint, RedisGetKey redisGetKey) throws Throwable {
String paramStr = null;
Object[] args = proceedingJoinPoint.getArgs();
// 1. 构建缓存参数
if (redisGetKey.paramNeed()) {
if (args == null || args.length == 0) {
throw new IllegalArgumentException();
}
paramStr = getParameterStr((MethodSignature) proceedingJoinPoint.getSignature(), args);
}
String redisFullKey = redisGetKey.keyPrefix() + paramStr;
Type type = getReturnType(proceedingJoinPoint);
// 2. 获取缓存数据,如果缓存有数据,则反序列化后直接返回
String value = stringRedisTemplate.opsForValue().get(redisFullKey);
Object result = null;
if (!StringUtils.isEmpty(value)) {
log.info("success get result from redis string ! redisKey={}", redisFullKey);
Class<?> returnTypeClass = ((MethodSignature) proceedingJoinPoint.getSignature()).getReturnType();
if (type instanceof List) {
return JSON.parseArray(value, returnTypeClass);
} else {
return JSON.parseObject(value, returnTypeClass);
}
} else {
// 3. 执行代理方法,获取数据
result = proceedingJoinPoint.proceed(args);
String resultStr = JSON.toJSONString(result);
// 4. 将结果设置到缓存中
stringRedisTemplate.opsForValue().set(redisFullKey, resultStr, redisGetKey.cachePeriod());
log.info("success set redis string! key={}",redisFullKey);
return result;
}
}
private Type getReturnType(ProceedingJoinPoint proceedingJoinPoint) throws NoSuchMethodException {
//获取方法
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
//获取返回值类型
return method.getAnnotatedReturnType().getType();
}
}
缓存框架测试
测试代码
请勿吐槽测试用的代码!!!请勿吐槽测试用的代码!!!请勿吐槽测试用的代码!!!都是瞎写。。。
- 测试用的方法,一个是查缓存,一个是删缓存
@Slf4j
@Service
public class RedisTestServiceImpl implements RedisTestService {
private static final String USER_REDIS_KEY_PREFIX = "sales:user:";
private static final String USER_ID_USER_NAME_REDIS_KEY_PREFIX = "user:id:name:";
/**
* 根据用户id获取用户
**/
@Override
@RedisGetKey(keyPrefix = USER_REDIS_KEY_PREFIX)
public User getUserById(@RedisParam Long id) {
if (id == null) {
return null;
}
if (id.equals(1L)) {
return User.builder().id(id).name("haha").build();
} else {
return User.builder().id(id).name("world" + id).build();
}
}
/**
* 根据用户id删除用户
**/
@Override
@RedisDeleteKey(keyPrefix = USER_REDIS_KEY_PREFIX)
public void deleteUser(@RedisParam Long id) {
log.info("success delete user!");
}
}
- 编写单元测试
@Test
public void getUserById() throws InterruptedException {
for (int i = 0; i < 30; i++) {
User user1 = redisTestService.getUserById(1L);
log.info("get user1!user1={}",user1);
User user2 = redisTestService.getUserById(2L);
log.info("get user2!user2={}",user2);
if (i % 2 == 0) {
redisTestService.deleteUser(1L);
}
if (i % 3 == 0) {
redisTestService.deleteUser(2L);
}
Thread.sleep(1000);
}
}