From 11ea0140048d90a22e17c83d60399cffac2905a1 Mon Sep 17 00:00:00 2001 From: Burce Date: Sun, 3 May 2020 20:07:17 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]AOP=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/iformall/log/TableOperateAspect.java | 2 +- .../com/iformall/annotation/RedisCache.java | 53 +++++++ .../com/iformall/aop/RedisCacheAspect.java | 140 ++++++++++++++++++ .../java/com/iformall/config/RedisConfig.java | 10 ++ .../com/iformall/annotation/RedisCache.java | 53 +++++++ .../com/iformall/aop/RedisCacheAspect.java | 139 +++++++++++++++++ .../java/com/iformall/config/RedisConfig.java | 11 ++ .../iformall/controller/WxMallController.java | 4 +- .../java/com/iformall/utils/RedisLock.java | 31 ++++ 9 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 mallinkBApi/src/main/java/com/iformall/annotation/RedisCache.java create mode 100644 mallinkBApi/src/main/java/com/iformall/aop/RedisCacheAspect.java create mode 100644 mallinkCApi/src/main/java/com/iformall/annotation/RedisCache.java create mode 100644 mallinkCApi/src/main/java/com/iformall/aop/RedisCacheAspect.java diff --git a/mallinkAdmin/src/main/java/com/iformall/log/TableOperateAspect.java b/mallinkAdmin/src/main/java/com/iformall/log/TableOperateAspect.java index d65be0d47..3dee1b300 100644 --- a/mallinkAdmin/src/main/java/com/iformall/log/TableOperateAspect.java +++ b/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; diff --git a/mallinkBApi/src/main/java/com/iformall/annotation/RedisCache.java b/mallinkBApi/src/main/java/com/iformall/annotation/RedisCache.java new file mode 100644 index 000000000..7f35a8288 --- /dev/null +++ b/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; +} diff --git a/mallinkBApi/src/main/java/com/iformall/aop/RedisCacheAspect.java b/mallinkBApi/src/main/java/com/iformall/aop/RedisCacheAspect.java new file mode 100644 index 000000000..de3212d83 --- /dev/null +++ b/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 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; + } + +} diff --git a/mallinkBApi/src/main/java/com/iformall/config/RedisConfig.java b/mallinkBApi/src/main/java/com/iformall/config/RedisConfig.java index 857f38ec2..ce8351901 100644 --- a/mallinkBApi/src/main/java/com/iformall/config/RedisConfig.java +++ b/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 getStringValueOperations(RedisConnectionFactory connectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(connectionFactory); + template.afterPropertiesSet(); + return template.opsForValue(); + } } diff --git a/mallinkCApi/src/main/java/com/iformall/annotation/RedisCache.java b/mallinkCApi/src/main/java/com/iformall/annotation/RedisCache.java new file mode 100644 index 000000000..da8512502 --- /dev/null +++ b/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; +} diff --git a/mallinkCApi/src/main/java/com/iformall/aop/RedisCacheAspect.java b/mallinkCApi/src/main/java/com/iformall/aop/RedisCacheAspect.java new file mode 100644 index 000000000..fb1935a2f --- /dev/null +++ b/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 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; + } + +} diff --git a/mallinkCApi/src/main/java/com/iformall/config/RedisConfig.java b/mallinkCApi/src/main/java/com/iformall/config/RedisConfig.java index 54f945030..8fa423dee 100644 --- a/mallinkCApi/src/main/java/com/iformall/config/RedisConfig.java +++ b/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 getStringValueOperations(RedisConnectionFactory connectionFactory) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(connectionFactory); + template.afterPropertiesSet(); + return template.opsForValue(); + } } diff --git a/mallinkCApi/src/main/java/com/iformall/controller/WxMallController.java b/mallinkCApi/src/main/java/com/iformall/controller/WxMallController.java index e203ad210..4554f8e98 100644 --- a/mallinkCApi/src/main/java/com/iformall/controller/WxMallController.java +++ b/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); } - } diff --git a/mallinkService/src/main/java/com/iformall/utils/RedisLock.java b/mallinkService/src/main/java/com/iformall/utils/RedisLock.java index 67e2f388b..47b18bc17 100644 --- a/mallinkService/src/main/java/com/iformall/utils/RedisLock.java +++ b/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 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; + } + /** * 解锁