暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【项目优化】在实际项目中关于代码优化场景及实现

加耀 2020-07-13
211

近期公司代码在做多层优化处理,在这里分享3个关于Redis解决项目中实际问题的优化方案;

    1、统一缓存管理

    2、分布式任务处理

    3、幂等性校验


一:统一缓存管理

    我们在实际项目中经常会遇到很多使用缓存的地方,对于一些不经常发生变动的数据并且请求频率不低的数据进行缓存,放缓存中存在数据时,则直接将数据进行缓存,否则查询数据库,然后将数据同步到缓存中,再将数据缓存;


    其业务流程如下图所示


java代码如下:

public Json queryUserDetailsByUid(Long userId){
Object userDetails;
if (redisService.isKeyExists(CaCheKeyConstant.getCacheUser(userId))){
userDetails = redisService.get(CaCheKeyConstant.getCacheUser(userId));
}else{
userDetails = userMapper.queryUserDetailsByUid(userId);
redisService.set(CaCheKeyConstant.getCacheUser(userId), userDetails, CaCheKeyExpireTime.defaultTime);
}
return Json.newInstance(userDetails);
}


    在上述代码中,根据用户id查询用户信息,当缓存中有时,则将缓存中的数据进行返回,否则查询数据库,将数据库的数据同步到缓存中;


    在实际编码中,有大量的缓存应用场景都是如此,缓存有,则查缓存,缓存没有则查数据库同步缓存;有了这个共性之后,我们便可以使用切面来简化代码,实现对缓存的统一管理了;这样也可以避免每个人都自己写一套自己的同步逻辑,便于代码的维护;


    简化后的代码:

@MyCache(keyPrefix = CaCheKeyConstant.USER_CACHE_ID, key = "userId", expireTime = CaCheKeyExpireTime.defaultTime)
public Json queryUserDetailsByUid(Long userId){
return Json.newInstance(userMapper.queryUserDetailsByUid(userId));
}


    代码在简化后,原来的代码业务逻辑判断都不再需要自己编写了,只需要添加一个注解即可实现对应的功能;再一块看看具体是如何实现的吧;


    在上面,我们通过一个注解来实现功能,注解中有三个属性,分别是缓存中存储的Key的前缀,然后是缓存中key值对应的标识符,另外一个是key的过期时间;首先,我们需要有一个注解,如下:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MyCache {
// 缓存key的前缀
String keyPrefix();
// 缓存Key(如:用户ID,没有则传"")
String key();
// 过期时间(毫秒)
long expireTime() default RedisKeyExpireTime.defaultTime;
}


    有了注解后,我们开始创建AOP,因为我们需要在执行业务方法之前进行一些缓存的查询,当查询不存在,则需要执行具体的业务,然后对结果集进行处理,所以这里我们需要使用的是AOP中的环绕增强,也就是Around;

@Aspect
@Component
@Slf4j
public class CacheManageAop {
@Resource
private RedisService redisService;
/**
* 从缓存获取数据信息
*/
@Around(value = "@annotation(cacheFind)")
public Object findCache(ProceedingJoinPoint joinPoint, MyCache cacheFind) throws Throwable{
//组装缓存key的值
String cacheKey=getCacheKey(cacheFind.keyPrefix(),cacheFind.key(),joinPoint);
//从缓存里获取数据
Object result= redisService.get(cacheKey);
//缓存不存在数据则执行对应的业务方法从数据库查询
if(Objects.isNull(result)) {
//把数据保存到缓存
redisService.set(cacheKey,joinPoint.proceed(),cacheFind.expireTime());
}
return result;
}
// 获取缓存的Key值
private String getCacheKey(String keyPrefix,String key, ProceedingJoinPoint joinPoint) throws ParameterConvertionException {
String cachekey="";
try {
if(StringUtils.isEmpty(key)){
cachekey= keyPrefix;
}else{
//动态获取方法参数,得到缓存的key
Map map = AopHelper.getMethodParamsJDK8(joinPoint);
if (CollectionUtils.isEmpty(map) || map.get(key) == null) {
cachekey= keyPrefix;
log.error("从{}方法找不到{}对应的参数名",joinPoint.getTarget().getClass() + "." + joinPoint.getSignature().getName(),key);
}else{
cachekey=keyPrefix + map.get(key).toString();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return cachekey;
}
// 此处代码需提供公共工具类 AopHelper
public static Map getMethodParamsJDK8(JoinPoint joinPoint){
String[] names = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
Map params =new HashMap();
if (ArrayUtils.isEmpty(names)) return params;
Object[] values = joinPoint.getArgs();
for (int i = 0; i < names.length; i++) {
params.put(names[i],values[i]);
}
return params;
}
}


    通过上述代码,在后续需要使用到缓存并且业务场景是相同的场景下,我们就可以直接使用注解@MyCache实现功能了,提供了缓存的统一管理,简化业务代码,使代码更加简洁也易读,同时也更加容易维护,我们只需要关注具体的业务方法即可,不再需要关心缓存相关的,做好业务功能开发即可;


    另外,在上述AOP中,其实还可以再进一步优化,当缓存的key值带有一些用户属性之类的,一般来说不存在并发相关的问题,线程是安全的;当key是一些公共的,比如某个广告位的推荐,这时候在上述AOP中,我们可以对执行业务方法代码进行加锁处理,避免在高并发场景下,缓存击穿,导致大批量查询进入数据库;

    

    我们只需要在AOP中相应的执行业务代码中加一把锁,在锁里再查一遍缓存即可解决这个问题;在大多数场景下可能用不到,不过这一点我们还是需要纳入我们的思考范围内的,可以尽量把问题考虑的周全,从而可以帮助我们在实际项目中避免很多一些奇怪的问题,少埋坑;



二:分布式任务管理

    在项目中,很多场景下我们会使用到定时任务,我们应用在单节点运行时不会有问题,但如果是在分布式多节点环境下,就可能会带来一些问题并且这些问题还不太容易排查出来;所以在实际项目中编写定时任务时,如果节点可能会部署多个节点,通常我们会使用到缓存,在缓存中保存一个标志,从而保证多个节点的情况下,定时任务也只会只有一个节点去执行;


    java代码如下:

@Scheduled(cron = "0 0/2 * * * ?")
public void testTask() {
if (redisService.isKeyExists(CaCheKeyConstant.MY_TASK)){
return;
}
redisService.set(CaCheKeyConstant.MY_TASK, "", CaCheKeyExpireTime.MY_DATE_TIME);
log.info("正在执行*******定时任务:{}", DateUtils.getCurrentDateTime());
}


    在实际编码中,为了避免在多节点中多个节点执行同一个定时任务,我们通常会编写如上述代码,在上述代码中,可以发现,代码中的逻辑还是比较清晰的,首先查询指定的key是否存在,如果存在,则不执行,否则则添加这个key并且设置过期时间,然后执行业务方法;


    通过上述这个特征,我们使用AOP的环绕增强一样可以实现这个逻辑;首先,还是先提供一个注解

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TaskLock {
/**
* 任务每隔多长时间执行一次(秒)
*/
long taskRunTime();
}



    有了注解后,我们再编写一个AOP即可

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 分布式定时任务锁
*/
@Aspect
@Component
@Slf4j
public class TaskLockAop {
@Resource
private RedisService redisService;
@Around(value = "@annotation(taskLock)")
public Object taskLock(ProceedingJoinPoint joinPoint, TaskLock taskLock) throws Throwable{
String lockKey="task_lock:projectName:"+joinPoint.getSignature().getName();
boolean lock =redisService.setNX(lockKey,taskLock.taskRunTime()-1000);
return lock?joinPoint.proceed():null;
}
}


public boolean setNX(String lockKey,Long expireTime) {
return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
boolean isLock= connection.setNX(lockKey.getBytes(),"".getBytes());
if(isLock){
expire(lockKey,expireTime,TimeUnit.SECONDS);
}
return isLock;
});
}



    编写完这些代码后,再来看看简化后的代码是怎样的

@TaskLock(runTime = 2*60*1000)
@Scheduled(cron = "0 0/2 * * * ?")
public void testTask() {
log.info("正在执行*******定时任务:{}", DateUtils.getCurrentDateTime());
}


    可以看到,简化后的代码只需要关注业务逻辑即可,不再需要自己去管理多节点定时任务相关信息,只需要自己添加一个注解即可实现当前功能;


    在上述代码中,我们通过注解获取到当前定时任务执行间隔时间差,在AOP中我们获取到被代理对象业务方法名,然后结合项目名拼装一个key值,然后我们对这个key值进行加锁处理;当锁结束后,才能执行下一个定时任务;在代码中,在原输入时间中进行减去1秒,腾格出程序运行时间;


    比较简单的一些代码,在项目中可以避免很多的小细节问题;


    另外,关于幂等性校验方面的,处理逻辑和分布式任务的逻辑其实是一样的,幂等性分多种情况,通常遇见比较多的就是重复提交(重复提交也属于幂等性的其中一种),在解决重复提交方面,结合上面的代码,

    可以通过Map map = AopHelper.getMethodParamsJDK8(joinPoint)方法获取到执行方法的参数值集合,对集合取其HashCode或者是验签之类的,进行缓存过期时间,实现幂等性校验逻辑;


    整体流程和分布式任务的代码逻辑是大体一致的;


    在项目中使用AOP可以很大程度上简洁我们的代码,同时也能置顶一定的规范,大家都遵循同一套规范,这样代码可读性会高,同时也易于管理和拓展;


2020年7月13日 23:44:22


文章转载自加耀,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论