背景
最近小A的公司要做一个大屏可视化平台,主要是给领导看的,领导说这个项目要给领导演示,效果好不好直接关系到能不能拿下这个项目,领导还补了一句“这项目至少是百万级的,大伙要全力以赴”,早上小A还想着“最近这么闲,难道还能接着摸鱼?”,想什么怕是来什么,要摸代码了。
小A打起精神,认真研究了功能,几乎是统计、数据展示、地图的一些功能。
下面是小A前后优化的代码
@Service
public class TestServiceImpl implements ITestService {
@Autowired
private TestMapper testMapper;
@Override
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;
}
}
由于大屏可视化平台每个页面有十几个统计模块,差不多十几个接口,考虑到给领导们展示的时候页面丝滑不卡顿,小A对代码做了优化,加入了缓存,如下:
@Service
public class TestServiceImpl implements ITestService {
@Autowired
private TestMapper testMapper;
@Autowired
private RedisCache redisCache;
@Override
public 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 {
@Autowired
private 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
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
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;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public 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;
}
@Bean
public 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 {
}
@Autowired
public 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 {
@Autowired
public 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) {
...
...
...
}
运行结果
缓存中无数据,执行方法体代码,并将结果保存到缓存,控制台输出如下:
缓存中有数据,不执行方法体代码,将缓存中数据返回,控制台输出如下: