Просмотр исходного кода

[优化]AOP统一缓存管理

release_toaliyun_real
Burce 6 лет назад
Родитель
Сommit
11ea014004
9 измененных файлов: 440 добавлений и 3 удалений
  1. +1
    -1
      mallinkAdmin/src/main/java/com/iformall/log/TableOperateAspect.java
  2. +53
    -0
      mallinkBApi/src/main/java/com/iformall/annotation/RedisCache.java
  3. +140
    -0
      mallinkBApi/src/main/java/com/iformall/aop/RedisCacheAspect.java
  4. +10
    -0
      mallinkBApi/src/main/java/com/iformall/config/RedisConfig.java
  5. +53
    -0
      mallinkCApi/src/main/java/com/iformall/annotation/RedisCache.java
  6. +139
    -0
      mallinkCApi/src/main/java/com/iformall/aop/RedisCacheAspect.java
  7. +11
    -0
      mallinkCApi/src/main/java/com/iformall/config/RedisConfig.java
  8. +2
    -2
      mallinkCApi/src/main/java/com/iformall/controller/WxMallController.java
  9. +31
    -0
      mallinkService/src/main/java/com/iformall/utils/RedisLock.java

+ 1
- 1
mallinkAdmin/src/main/java/com/iformall/log/TableOperateAspect.java Просмотреть файл

@@ -3,6 +3,7 @@ package com.iformall.log;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.common.collect.Lists;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.iformall.common.LogColumn;
@@ -17,7 +18,6 @@ import com.iformall.service.invest.impl.InvestBaseServiceImpl;
import com.iformall.utils.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.aspectj.lang.ProceedingJoinPoint;


+ 53
- 0
mallinkBApi/src/main/java/com/iformall/annotation/RedisCache.java Просмотреть файл

@@ -0,0 +1,53 @@
package com.iformall.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;


/**
* /api/user/detail
* /api/couponOrder/getUnverifiedAmountOnDate
* /api/micropay/getMicroPayAmountOnDate
* /api/couponOrder/getVerifiedAmountOnDate
* /api/wxDateAmountRecord/listMicropay
* /api/wxDateAmountRecord/listVerified
* /api/wxDateAmountRecord/listOrder
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {

/**
* 缓存key的名称
* @return
*/
String key() default "";

/**
* 自动生成key
* @return
*/
boolean autoKey() default true;

/**
* 参数md5,解决key过长问题
* @return
*/
boolean md5() default true ;

/**
* key 过期日期默认300秒
* @return
*/
int expireTime() default 300;

/**
* 时间单位默认为秒
* @return
*/
TimeUnit dateUnit() default TimeUnit.SECONDS;
}

+ 140
- 0
mallinkBApi/src/main/java/com/iformall/aop/RedisCacheAspect.java Просмотреть файл

@@ -0,0 +1,140 @@
package com.iformall.aop;


import com.alibaba.fastjson.JSONObject;
import com.iformall.annotation.RedisCache;
import com.iformall.domain.po.WxCUser;
import com.iformall.service.WxCUserTokenService;
import com.iformall.utils.HashUtil;
import com.iformall.utils.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Slf4j
@Aspect
@Component
public class RedisCacheAspect {

/**
* 参数分隔符 param1|param2|param3
**/
private static final String DELIMITER_PARAMS = "|";

/**
* key 分隔符
*/
private static final String DELIMITER_KEY = ":";

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired
RedisLock redisLock;

@Autowired
private WxCUserTokenService userTokenService;

@Pointcut("@annotation(com.iformall.annotation.RedisCache)")
public void cacheAspect() {
}

@Around("cacheAspect()")
public Object round(ProceedingJoinPoint jp, RedisCache cache) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 查询token信息
//从header中获取token
String token = request.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = request.getParameter("token");
}
String tenantId;
WxCUser wxCUser = userTokenService.getByToken(token);
if (wxCUser == null || wxCUser.getExpireTime().getTime() < System.currentTimeMillis()) {
tenantId = "0";
} else {
tenantId = wxCUser.getTenantId();
}

// 请求参数
Object[] args = jp.getArgs();

// 接口 返回结果
Object result;

String redisKey;
if (cache.autoKey()) {
// 构建 redisKey => reqPath:tenantId:md5(param1_param2_param3)
String requestUrl = request.getRequestURI();
// 接口路径 reqPath => /api/xxx
String reqPath = requestUrl.substring(requestUrl.indexOf("/api/"));
if (cache.md5()) {
redisKey = StringUtils.join(reqPath, DELIMITER_KEY, tenantId, DELIMITER_KEY, HashUtil.md5(StringUtils.join(args, DELIMITER_PARAMS)));
} else {
redisKey = StringUtils.join(reqPath, DELIMITER_KEY, tenantId, DELIMITER_KEY, StringUtils.join(args, DELIMITER_PARAMS));
}
} else {
redisKey = cache.key();
}

// redisKey 不存在,获取接口数据并返回
if (StringUtils.isBlank(redisKey)) {
result = proceed(jp, args, null);
} else {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
String value = valueOperations.get(redisKey);
if (value == null) {
result = proceed(jp, args, StringUtils.join(redisKey, ":", "lock"));
String json_data = JSONObject.toJSONString(result);
valueOperations.set(redisKey, json_data, cache.expireTime(), cache.dateUnit());
log.debug("CacheAspect 缓存不存在,获取接口数据并放入缓存,key:{}, expire:{}", redisKey, cache.expireTime());
} else {
Class<?> returnType = ((MethodSignature) jp.getSignature()).getReturnType();
result = JSONObject.parseObject(value, returnType);
log.debug("CacheAspect 缓存存在,解析缓存并返回,key:{}", redisKey);
}
}
return result;
}

private Object proceed(ProceedingJoinPoint jp, Object[] args, String lockKey) {
// 超时1秒
long time = System.currentTimeMillis() + 1000;
String timeStr = String.valueOf(time);
try {
//分布式锁,保证一个线程读DB,其它线程排队
if (StringUtils.isBlank(lockKey)) {
//log.debug("CacheAspect 读库并返回,无锁");
return jp.proceed(args);
} else if (redisLock.lock2(lockKey, timeStr)) {
log.debug("CacheAspect 读库中, key:{}: " + lockKey);
return jp.proceed(args);
} else {
// TODO 挂起,重试
log.debug("CacheAspect 排队中, key:{}: " + lockKey);
}
} catch (Throwable throwable) {
log.error("CacheAspect proceed error , Illegal argument: {} in {}.{}()", Arrays.toString(jp.getArgs()),
jp.getSignature().getDeclaringTypeName(), jp.getSignature().getName(), throwable);
} finally {
redisLock.unlock(lockKey, timeStr);
}
return null;
}

}

+ 10
- 0
mallinkBApi/src/main/java/com/iformall/config/RedisConfig.java Просмотреть файл

@@ -16,6 +16,8 @@ import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@@ -172,4 +174,12 @@ public class RedisConfig extends CachingConfigurerSupport {
template.setConnectionFactory(connectionFactory);
return template;
}

@Bean("stringValueOperations")
public ValueOperations<String, String> getStringValueOperations(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template.opsForValue();
}
}

+ 53
- 0
mallinkCApi/src/main/java/com/iformall/annotation/RedisCache.java Просмотреть файл

@@ -0,0 +1,53 @@
package com.iformall.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;


/**
* - /api/wxCouponChannel/change
* - /api/user/userinfo
* - /api/wxBusiness/listAll
* - /api/wxCampaign/list
* - /api/mall/mallInfo
* - /api/mall/getAppIcon
* - /api/mall/getWeapNote
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {

/**
* 缓存key的名称
* @return
*/
String key() default "";

/**
* 自动生成key
* @return
*/
boolean autoKey() default true;

/**
* 参数md5,解决key过长问题
* @return
*/
boolean md5() default true ;

/**
* key 过期日期默认300秒
* @return
*/
int expireTime() default 300;

/**
* 时间单位默认为秒
* @return
*/
TimeUnit dateUnit() default TimeUnit.SECONDS;
}

+ 139
- 0
mallinkCApi/src/main/java/com/iformall/aop/RedisCacheAspect.java Просмотреть файл

@@ -0,0 +1,139 @@
package com.iformall.aop;


import com.alibaba.fastjson.JSONObject;
import com.iformall.annotation.RedisCache;
import com.iformall.domain.po.WxCUser;
import com.iformall.service.WxCUserTokenService;
import com.iformall.utils.HashUtil;
import com.iformall.utils.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Slf4j
@Aspect
@Component
public class RedisCacheAspect {

/**
* 参数分隔符 param1|param2|param3
**/
private static final String DELIMITER_PARAMS = "|";

/**
* key 分隔符
*/
private static final String DELIMITER_KEY = ":";

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Autowired
RedisLock redisLock;

@Autowired
private WxCUserTokenService userTokenService;

@Pointcut("@annotation(com.iformall.annotation.RedisCache)")
public void cacheAspect() {
}

@Around("cacheAspect()")
public Object round(ProceedingJoinPoint jp, RedisCache cache) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 查询token信息
//从header中获取token
String token = request.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = request.getParameter("token");
}
String tenantId;
WxCUser wxCUser = userTokenService.getByToken(token);
if (wxCUser == null || wxCUser.getExpireTime().getTime() < System.currentTimeMillis()) {
tenantId = "0";
} else {
tenantId = wxCUser.getTenantId();
}

// 请求参数
Object[] args = jp.getArgs();

// 接口 返回结果
Object result;

String redisKey;
if (cache.autoKey()) {
// 构建 redisKey => reqPath:tenantId:md5(param1_param2_param3)
String requestUrl = request.getRequestURI();
// 接口路径 reqPath => /api/xxx
String reqPath = requestUrl.substring(requestUrl.indexOf("/api/"));
if (cache.md5()) {
redisKey = StringUtils.join(reqPath, DELIMITER_KEY, tenantId, DELIMITER_KEY, HashUtil.md5(StringUtils.join(args, DELIMITER_PARAMS)));
} else {
redisKey = StringUtils.join(reqPath, DELIMITER_KEY, tenantId, DELIMITER_KEY, StringUtils.join(args, DELIMITER_PARAMS));
}
} else {
redisKey = cache.key();
}

// redisKey 不存在,获取接口数据并返回
if (StringUtils.isBlank(redisKey)) {
result = proceed(jp, args, null);
} else {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
String value = valueOperations.get(redisKey);
if (value == null) {
result = proceed(jp, args, StringUtils.join(redisKey, ":", "lock"));
String json_data = JSONObject.toJSONString(result);
valueOperations.set(redisKey, json_data, cache.expireTime(), cache.dateUnit());
log.debug("CacheAspect 缓存不存在,获取接口数据并放入缓存,key:{}, expire:{}", redisKey, cache.expireTime());
} else {
Class<?> returnType = ((MethodSignature) jp.getSignature()).getReturnType();
result = JSONObject.parseObject(value, returnType);
log.debug("CacheAspect 缓存存在,解析缓存并返回,key:{}", redisKey);
}
}
return result;
}

private Object proceed(ProceedingJoinPoint jp, Object[] args, String lockKey) {
// 超时1秒
long time = System.currentTimeMillis() + 1000;
String timeStr = String.valueOf(time);
try {
//分布式锁,保证一个线程读DB,其它线程排队
if (StringUtils.isBlank(lockKey)) {
//log.debug("CacheAspect 读库并返回,无锁");
return jp.proceed(args);
} else if (redisLock.lock2(lockKey, timeStr)) {
log.debug("CacheAspect 读库中, key:{}: " + lockKey);
return jp.proceed(args);
} else {
// TODO 挂起,重试
log.debug("CacheAspect 排队中, key:{}: " + lockKey);
}
} catch (Throwable throwable) {
log.error("CacheAspect proceed error , Illegal argument: {} in {}.{}()", Arrays.toString(jp.getArgs()),
jp.getSignature().getDeclaringTypeName(), jp.getSignature().getName(), throwable);
} finally {
redisLock.unlock(lockKey, timeStr);
}
return null;
}

}

+ 11
- 0
mallinkCApi/src/main/java/com/iformall/config/RedisConfig.java Просмотреть файл

@@ -16,6 +16,8 @@ import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@@ -174,4 +176,13 @@ public class RedisConfig extends CachingConfigurerSupport {
template.setConnectionFactory(connectionFactory);
return template;
}


@Bean("stringValueOperations")
public ValueOperations<String, String> getStringValueOperations(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template.opsForValue();
}
}

+ 2
- 2
mallinkCApi/src/main/java/com/iformall/controller/WxMallController.java Просмотреть файл

@@ -1,6 +1,7 @@
package com.iformall.controller;

import com.iformall.annotation.AuthIgnore;
import com.iformall.annotation.RedisCache;
import com.iformall.common.ErrorCode;
import com.iformall.common.Result;
import com.iformall.common.ResultData;
@@ -113,7 +114,7 @@ public class WxMallController extends BaseController {
return new ResultData(Result.SUCCESS, "查询成功", map);
}

@RedisCache()
@AuthIgnore
@ApiOperation("获取后端版本号")
@GetMapping("/version")
@@ -121,5 +122,4 @@ public class WxMallController extends BaseController {
return new ResultData(version);
}


}

+ 31
- 0
mallinkService/src/main/java/com/iformall/utils/RedisLock.java Просмотреть файл

@@ -55,6 +55,37 @@ public class RedisLock {
return false;
}

/**
* 加锁 ,设置过期时间
* @param key productId - 商品的唯一标志
* @param value 当前时间+超时时间
* @return
*/
public boolean lock2(String key,String value){
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
//对应setnx命令,5s过期
if(operations.setIfAbsent(key,value,5,TimeUnit.SECONDS)){
//可以成功设置,也就是key不存在
return true;
}

//判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentValue = operations.get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不为空且小于当前时间
//获取上一个锁的时间value
String oldValue =operations.getAndSet(key,value);//对应getset,如果key存在

//假设两个线程同时进来,key被占用了。获取的值currentValue=A(get取的旧的值肯定是一样的),两个线程的value都是B,key都是K.锁时间已经过期了。
//而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的value已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
//oldValue不为空且oldValue等于currentValue,也就是校验是不是上个对应的商品时间戳,也是防止并发
return true;
}
}
return false;
}


/**
* 解锁


Загрузка…
Отмена
Сохранить