背景
最近小A的公司要做一个大屏可视化平台,主要是给领导看的,领导说这个项目要给领导演示,效果好不好直接关系到能不能拿下这个项目,领导还补了一句“这项目至少是百万级的,大伙要全力以赴”,早上小A还想着“最近这么闲,难道还能接着摸鱼?”,想什么怕是来什么,要摸代码了。
小A打起精神,认真研究了功能,几乎是统计、数据展示、地图的一些功能。
下面是小A前后优化的代码
@Service
public class TestServiceImpl implements ITestService {@Autowiredprivate TestMapper testMapper;@Overridepublic List<AbcVo> sczy(String type) {Map<String, AbcVo> map = Stream.of("统计A", "统计B", "统计C", "统计D").map(str->new AbcVo(str, "0", "天")).collect(Collectors.toMap(AbcVo::getType, AbcVo -> AbcVo));List<AbcVo> list = testMapper.sczy(type);for(int i=0; i<list.size(); i++) {AbcVo obj = map.get(list.get(i).getType());if(obj != null) {obj.setValue(new BigDecimal(list.get(i).getValue()).setScale(2, BigDecimal.ROUND_HALF_UP).toString());}}List<AbcVo> resList = new ArrayList<AbcVo>(map.values());return resList;}
}
由于大屏可视化平台每个页面有十几个统计模块,差不多十几个接口,考虑到给领导们展示的时候页面丝滑不卡顿,小A对代码做了优化,加入了缓存,如下:
@Service
public class TestServiceImpl implements ITestService {@Autowiredprivate TestMapper testMapper;@Autowiredprivate RedisCache redisCache;@Overridepublic List<AbcVo> sczy(String qydm) {List<AbcVo> resList = null;//先从缓存取Object cacheObj = redisCache.getCacheObject(CacheConstants.YPTTJ_KEY, qydm);if (StringUtils.isNotNull(cacheObj)) {resList = StringUtils.cast(cacheObj);return resList;}//缓存没有执行下面逻辑,从数据库取值统计if(CollectionUtils.isEmpty(resList)) {Map<String, AbcVo> map = Stream.of("统计A", "统计B", "统计C", "统计D").map(str->new AbcVo(str, "0", "天")).collect(Collectors.toMap(AbcVo::getType, AbcVo -> AbcVo));List<AbcVo> list = testMapper.sczy(qydm);for(int i=0; i<list.size(); i++) {AbcVo obj = map.get(list.get(i).getType());if(obj != null) {obj.setValue(list.get(i).getValue());}}resList = new ArrayList<AbcVo>(map.values());//将结果保存到缓存redisCache.setCacheObject(CacheConstants.YPTTJ_KEY, qydm, resList);}return resList;}
可是一个页面十几个接口,N个页面那不是N倍十几个接口,每个接口都这样加缓存,是不是改动量太大了,而且很多代码都是重复的。于是小A灵机一动,能不能写个注解,在方法上加个缓存注解,代码逻辑不变,就像这样:
@Cache(key = CacheConstants.YPTTJ_KEY)
@Service
public class TestServiceImpl implements ITestService {@Autowiredprivate TestMapper testMapper;@Override@Cache(key = CacheConstants.YPTTJ_KEY)public List<AbcVo> sczy(String type) {Map<String, AbcVo> map = Stream.of("统计A", "统计B", "统计C", "统计D").map(str->new AbcVo(str, "0", "天")).collect(Collectors.toMap(AbcVo::getType, AbcVo -> AbcVo));List<AbcVo> list = testMapper.sczy(type);for(int i=0; i<list.size(); i++) {AbcVo obj = map.get(list.get(i).getType());if(obj != null) {obj.setValue(new BigDecimal(list.get(i).getValue()).setScale(2, BigDecimal.ROUND_HALF_UP).toString());}}List<AbcVo> resList = new ArrayList<AbcVo>(map.values());return resList;}
}
使用自定义缓存的注解时,可以根据缓存key判断缓存中是否存在要的数据,有就直接返回,不再执行方法体中代码;如果缓存中没有,则执行方法体代码,并将方法的返回值缓存到redis中。
小A信心满满,觉得这样写代码才能匹配自己做事的态度。
实现
POM依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId>
</dependency>
项目中Redis配置
# redis 配置
redis:# 地址host: localhost# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms
Redis配置
Redis序列化器
/*** Redis使用FastJson序列化*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {@SuppressWarnings("unused")private ObjectMapper objectMapper = new ObjectMapper();public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static {ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJson2JsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}public void setObjectMapper(ObjectMapper objectMapper) {Assert.notNull(objectMapper, "'objectMapper' must not be null");this.objectMapper = objectMapper;}protected JavaType getJavaType(Class<?> clazz) {return TypeFactory.defaultInstance().constructType(clazz);}
}
redis配置
/*** redis配置*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);serializer.setObjectMapper(mapper);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流脚本*/private String limitScriptText() {return "local key = KEYS[1]\n" + "local count = tonumber(ARGV[1])\n" + "local time = tonumber(ARGV[2])\n"+ "local current = redis.call('get', key);\n" + "if current and tonumber(current) > count then\n"+ " return tonumber(current);\n" + "end\n" + "current = redis.call('incr', key)\n"+ "if tonumber(current) == 1 then\n" + " redis.call('expire', key, time)\n" + "end\n"+ "return tonumber(current);";}
}
redis操作工具类:将redis的相关操作封装成类,以组件的形式注入到spring容器中。
创建自定义注解
/*** redis 缓存注解*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {/**缓存的key**/String key() default "";/**缓存时长,默认-1表示永久有效**/int time() default -1;/**缓存时长单位,默认单位秒**/TimeUnit type() default TimeUnit.SECONDS;}
自定义缓存注解的逻辑实现
/*** 自定义缓存处理*/
@Aspect
@Slf4j
@Component
public class RedisCacheAspect {@Pointcut("@annotation(com.tencet.common.annotation.Cache)")public void annotationPoint() throws Throwable {}@Autowiredpublic RedisCache redisService;@SneakyThrows@Around(value = "annotationPoint() && @annotation(redisCache)",argNames = "proceedingJoinPoint,redisCache")public Object around(ProceedingJoinPoint proceedingJoinPoint, Cache redisCache){//判断是否开启缓存String hckg = redisService.getCacheObject(CacheConstants.YPT_HCKG); //缓存开关if(StringUtils.isNotEmpty(hckg) && "1".equals(hckg)) { //开启String redisKey = createKeyName(proceedingJoinPoint, redisCache);if(StringUtils.isNotEmpty(redisKey)) { //缓存key不为空System.out.println("key=>"+redisKey);//根据缓存key从缓存中取数据Object cacheObject = redisService.getCacheObject(redisKey);if (Objects.isNull(cacheObject)){ //不存在缓存中//继续执行方法内的代码逻辑, 得到方法的返回值Object methodResult = proceedingJoinPoint.proceed();//方法的返回值存入缓存中redisService.setCacheObject(redisKey, methodResult, redisCache.time(), redisCache.type());log.info("不存在缓存中,存入缓存:"+methodResult.toString());return methodResult; }else{ //存在缓存中log.info("存在缓存中,缓存取出:"+cacheObject.toString());// 缓存命中后,直接返回缓存中的数据。并不执行方法内的逻辑。return cacheObject;}}}//缓存未开启或缓存key为空//忽略缓存,继续执行方法内的代码逻辑, 得到方法的返回值Object methodResult = proceedingJoinPoint.proceed();return methodResult;}/**** * @MethodName: createKeyName * @Description: 生成缓存的 key规则* @Date 2023年5月5日* @param proceedingJoinPoint* @param redisCache* @return*/private String createKeyName(ProceedingJoinPoint proceedingJoinPoint, Cache redisCache){StringBuffer resultKey = new StringBuffer();String key = redisCache.key();if (StringUtils.isNotEmpty(key)){//前缀resultKey.append(key);//后缀String keySuffix = getKeySuffix(proceedingJoinPoint);if (StringUtils.isNotEmpty(keySuffix)){resultKey.append(keySuffix);}}else {return null;}return resultKey.toString();}/*** * @MethodName: getKeySuffix * @Description: 获取方法参数生成缓存key的后缀* @Date 2023年5月5日* @param proceedingJoinPoint* @return*/public String getKeySuffix(ProceedingJoinPoint proceedingJoinPoint) {StringBuffer keySuffix = new StringBuffer();Object[] values = proceedingJoinPoint.getArgs();for(Object o : values) {if(o == null) {keySuffix.append("all");}else {keySuffix.append(o.toString());}}return keySuffix.toString();}/*** * @MethodName: getParam * @Description: 获取参数名和参数值* @Date 2023年5月5日* @param proceedingJoinPoint* @return*/public String getParam(ProceedingJoinPoint proceedingJoinPoint) {Map<String, Object> map = new HashMap<String, Object>();Object[] values = proceedingJoinPoint.getArgs();CodeSignature cs = (CodeSignature) proceedingJoinPoint.getSignature();String[] names = cs.getParameterNames();for (int i = 0; i < names.length; i++) {map.put(names[i], values[i]);}return JSONObject.toJSONString(map);}
}
在方法上使用自定义注解
/*** spring redis 工具类*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache {@Autowiredpublic RedisTemplate redisTemplate;/*** 查看key是否存在*/public boolean exists(String key) {return redisTemplate.hasKey(key);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value) {redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String keyt, String keyw, final T value) {if(StringUtils.isEmpty(keyw)) {keyw = "all";}String key = keyt + keyw;redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {if(timeout == -1){//永久有效redisTemplate.opsForValue().set(key, value);}else{redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout) {return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit) {return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key) {ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String keyt, String keyw) {ValueOperations<String, T> operation = redisTemplate.opsForValue();if(StringUtils.isEmpty(keyw)) {keyw = "all";}String key = keyt + keyw;return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key) {return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** * @MethodName: deleteKey * @Description: 删除多个缓存key * @Date 2023年5月3日* @param arrs* @return*/public long deleteKey(String[] arrs) {Set<String> keys = new HashSet<String>();for(String key : arrs) {//模糊匹配所有以keyword:开头的所有key值keys.addAll(redisTemplate.keys(key+"*"));}return redisTemplate.delete(keys);}/*** 缓存List数据** @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList) {Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key) {return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()) {setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key) {return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key) {return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value) {redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey) {HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据* * @param key* @param hKey*/public void delCacheMapValue(final String key, final String hKey) {HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hKey);}/*** 获取多个Hash中的数据** @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern) {return redisTemplate.keys(pattern);}
}
在方法上使用自定义注解
@Cache(key = CacheConstants.YPTTJ_KEY)
public Object testMethod(String param) {.........
}//添加过期时间的缓存
@Cache(key = CacheConstants.YPTTJ_KEY, time = 1, type = TimeUnit.HOURS)
public testMethod(String param) {.........
}
运行结果
缓存中无数据,执行方法体代码,并将结果保存到缓存,控制台输出如下:
缓存中有数据,不执行方法体代码,将缓存中数据返回,控制台输出如下: