| @@ -3,6 +3,7 @@ package com.iformall.log; | |||||
| import com.alibaba.fastjson.JSON; | import com.alibaba.fastjson.JSON; | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | import com.baomidou.mybatisplus.annotation.TableName; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | 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.MapDifference; | ||||
| import com.google.common.collect.Maps; | import com.google.common.collect.Maps; | ||||
| import com.iformall.common.LogColumn; | import com.iformall.common.LogColumn; | ||||
| @@ -17,7 +18,6 @@ import com.iformall.service.invest.impl.InvestBaseServiceImpl; | |||||
| import com.iformall.utils.SpringContextUtils; | import com.iformall.utils.SpringContextUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.collections.CollectionUtils; | import org.apache.commons.collections.CollectionUtils; | ||||
| import org.apache.commons.compress.utils.Lists; | |||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
| import org.apache.commons.lang3.reflect.FieldUtils; | import org.apache.commons.lang3.reflect.FieldUtils; | ||||
| import org.aspectj.lang.ProceedingJoinPoint; | import org.aspectj.lang.ProceedingJoinPoint; | ||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -16,6 +16,8 @@ import org.springframework.data.redis.cache.RedisCacheManager; | |||||
| import org.springframework.data.redis.cache.RedisCacheWriter; | import org.springframework.data.redis.cache.RedisCacheWriter; | ||||
| import org.springframework.data.redis.connection.RedisConnectionFactory; | import org.springframework.data.redis.connection.RedisConnectionFactory; | ||||
| import org.springframework.data.redis.core.RedisTemplate; | 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.Jackson2JsonRedisSerializer; | ||||
| import org.springframework.data.redis.serializer.StringRedisSerializer; | import org.springframework.data.redis.serializer.StringRedisSerializer; | ||||
| @@ -172,4 +174,12 @@ public class RedisConfig extends CachingConfigurerSupport { | |||||
| template.setConnectionFactory(connectionFactory); | template.setConnectionFactory(connectionFactory); | ||||
| return template; | return template; | ||||
| } | } | ||||
| @Bean("stringValueOperations") | |||||
| public ValueOperations<String, String> getStringValueOperations(RedisConnectionFactory connectionFactory) { | |||||
| StringRedisTemplate template = new StringRedisTemplate(); | |||||
| template.setConnectionFactory(connectionFactory); | |||||
| template.afterPropertiesSet(); | |||||
| return template.opsForValue(); | |||||
| } | |||||
| } | } | ||||
| @@ -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; | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -16,6 +16,8 @@ import org.springframework.data.redis.cache.RedisCacheManager; | |||||
| import org.springframework.data.redis.cache.RedisCacheWriter; | import org.springframework.data.redis.cache.RedisCacheWriter; | ||||
| import org.springframework.data.redis.connection.RedisConnectionFactory; | import org.springframework.data.redis.connection.RedisConnectionFactory; | ||||
| import org.springframework.data.redis.core.RedisTemplate; | 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.Jackson2JsonRedisSerializer; | ||||
| import org.springframework.data.redis.serializer.StringRedisSerializer; | import org.springframework.data.redis.serializer.StringRedisSerializer; | ||||
| @@ -174,4 +176,13 @@ public class RedisConfig extends CachingConfigurerSupport { | |||||
| template.setConnectionFactory(connectionFactory); | template.setConnectionFactory(connectionFactory); | ||||
| return template; | return template; | ||||
| } | } | ||||
| @Bean("stringValueOperations") | |||||
| public ValueOperations<String, String> getStringValueOperations(RedisConnectionFactory connectionFactory) { | |||||
| StringRedisTemplate template = new StringRedisTemplate(); | |||||
| template.setConnectionFactory(connectionFactory); | |||||
| template.afterPropertiesSet(); | |||||
| return template.opsForValue(); | |||||
| } | |||||
| } | } | ||||
| @@ -1,6 +1,7 @@ | |||||
| package com.iformall.controller; | package com.iformall.controller; | ||||
| import com.iformall.annotation.AuthIgnore; | import com.iformall.annotation.AuthIgnore; | ||||
| import com.iformall.annotation.RedisCache; | |||||
| import com.iformall.common.ErrorCode; | import com.iformall.common.ErrorCode; | ||||
| import com.iformall.common.Result; | import com.iformall.common.Result; | ||||
| import com.iformall.common.ResultData; | import com.iformall.common.ResultData; | ||||
| @@ -113,7 +114,7 @@ public class WxMallController extends BaseController { | |||||
| return new ResultData(Result.SUCCESS, "查询成功", map); | return new ResultData(Result.SUCCESS, "查询成功", map); | ||||
| } | } | ||||
| @RedisCache() | |||||
| @AuthIgnore | @AuthIgnore | ||||
| @ApiOperation("获取后端版本号") | @ApiOperation("获取后端版本号") | ||||
| @GetMapping("/version") | @GetMapping("/version") | ||||
| @@ -121,5 +122,4 @@ public class WxMallController extends BaseController { | |||||
| return new ResultData(version); | return new ResultData(version); | ||||
| } | } | ||||
| } | } | ||||
| @@ -55,6 +55,37 @@ public class RedisLock { | |||||
| return false; | 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; | |||||
| } | |||||
| /** | /** | ||||
| * 解锁 | * 解锁 | ||||