| @@ -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; | |||
| @@ -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.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(); | |||
| } | |||
| } | |||
| @@ -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.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(); | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| /** | |||
| * 解锁 | |||