最新动态
- backend项目架构设计 - 执行时间分析
做一个复杂的数据计算需求,执行过程被拆分为几十个Service(服务)和Calculator(计算器)类,且在其中又调用了十几个基础数据的Service类。计算逻辑开发好后,结构清楚,但执行时间很长,需要做性能优化。那么,应该优化哪些地方,即哪些地方是性能瓶颈?这需要对执行时间做统计和分析。 计时 我们利用Spring的AOP功能实现对方法做执行时间计时。 例如,对于Service,编写如下切面类: @Aspect @Component @Slf4j public class ServiceTimingAspect { /** * 切点 * <p>切点为{@code com.company.project}包下任意层级的{@code service}包中的所有类的所有方法。</p> */ @Pointcut("execution(* com.company.project..service..*(..))") private void pointcut() {} /** * 环绕通知,执行并计时 * @param joinPoint 连接点 * @return 切点的返回值 * @throws Throwable 切点的抛出对象 */ @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 执行并计时 var startTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); var endTime = System.currentTimeMillis(); var duration = endTime - startTime; // 方法签名 var methodSignature = joinPoint.toString(); methodSignature = methodSignature.substring("execution(".length(), methodSignature.length() - 1); logger.debug("方法签名{}耗时: {}ms", methodSignature, duration); // 实参列表 var args = Arrays.toString(joinPoint.getArgs()); args = args.substring(1, args.length() - 1); args = args.replace("\n", "\\n").replace("\t", "\\t"); // 方法调用 var methodCall = methodSignature.substring(0, methodSignature.indexOf("(") + 1) + args + ")"; logger.debug("方法调用{}耗时: {}ms", methodCall, duration); return result; } } 日志 通过配置logback-spring.xml将切面类的日志同时输出到控制台和专门的计时日志文件: <!-- 输出到计时日志文件的Appender --> <appender name="TIMING_LOG_FILE" class="ch.qos.logback.core.FileAppender"> <file>log/timing.log</file> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> </appender> <!-- 切面类的Logger --> <logger name="切面类所在的包名" level="DEBUG" additivity="false"> <appender-ref ref="CONSOLE"/> <appender-ref ref="TIMING_LOG_FILE"/> </logger> 统计 编写TimingLogAnalyzer类,通过main()方法执行,读取计时日志文件,统计每个方法签名、方法调用的执行次数、总耗时、最小耗时、最大耗时、平均耗时,然后将统计结果输出到Excel文件中。 分析 在Excel文件中通过排序、筛选、统计图等手段,分析出性能瓶颈的所在,进而有针对性地去做性能优化。 功能增强 对Mapper、Calculator、Controller或者其他类做计时切面类。尤其是Mapper,可以用于分析缓慢的数据库查询。 通过配置和@ConditionalOnProperty控制切面类的装配,线上关闭计时,本地也只在需要分析时才开启计时。
- backend项目架构设计 - 请求级缓存
在开发一个复杂的业务计算需求时,遇到如下问题:业务计算过程很复杂,需要拆分为很多步骤,每个步骤又需要拆分为多个不同职责的类,计算时,这些类之间需要共享很多数据。如果通过方法参数传递,则参数会很多,非常繁琐。如果通过中间数据结构传递,则每个类所需的共享数据不同,中间数据结构不好统一。 解决思路:不传递共享数据,而是每个类自己去获取,需要什么就获取什么,参数只保留最必要的,这样每个类的独立性大大增强,更简洁、清晰。显然,这里有个问题,大量重复获取同一个数据,会严重降低效率。我们可以用缓存解决这个问题,对于同一个数据,只在第一次获取时查库,后续直接用缓存。 在我们的业务场景中,数据库的数据可能被多个来源修改,那么,缓存淘汰策略就是问题。如果修改全部在单一项目进行,那么,可以采用修改时主动淘汰的策略,这样效率最高,显然我们的业务场景不满足。另一种常用的策略是基于超时,但超时时间设多长不好定,短了影响性能,长了影响一致性,每种数据的修改频率也不同,单独设置很繁琐。 考虑到,一次请求的时间非常短(通常几十~几百毫秒),在此过程中,相同的参数几乎100%会返回相同的数据。因此,我们可以实现请求级缓存机制。在请求级缓存机制中,一个缓存的生命周期为,从第一次查询开始,到请求执行结束。 基于这种机制去实现复杂的业务计算需求,可以同时保证代码简洁性(减少共享数据传递)、高效率(只查一次,效率与传递共享数据相同)和数据一致性(极短时间内认为数据不变化)。 参考代码: // 缓存管理器 public class RequestLevelCacheManager implements CacheManager { private final ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); /** * 获取指定请求线程中指定名称的缓存 * @param thread 请求线程 * @param name 缓存名称 * @return 缓存 */ public Cache getCache(Thread thread, String name) { return cacheManager.getCache(thread.threadId() + ":" + name); } /** * 获取当前请求线程中指定名称的缓存 * @param name 缓存名称 * @return 缓存 */ @Override public Cache getCache(String name) { return getCache(Thread.currentThread(), name); } /** * 获取缓存管理器中的所有缓存名称 * @return 缓存名称集合 */ @Override public Collection<String> getCacheNames() { return cacheManager.getCacheNames(); } /** * 清除指定请求线程的缓存 * @param thread 请求线程 */ public void clearCache(Thread thread) { // 缓存管理器中的所有缓存名称 cacheManager.getCacheNames().stream() // 筛选出当前请求线程的所有缓存名称 .filter(name -> name.startsWith(thread.threadId() + ":")) // 获取缓存名称对应的缓存 .map(cacheManager::getCache) // 筛选出非null的缓存 .filter(Objects::nonNull) // 对缓存执行清除 .forEach(Cache::clear); } /** 清除当前请求线程的缓存 */ public void clearCache() { clearCache(Thread.currentThread()); } } // 缓存管理器配置 @Configuration public class CacheConfig { // 将RequestLevelCacheManager设为默认的缓存管理器 @Bean @Primary public RequestLevelCacheManager requestLevelCacheManager() { return new RequestLevelCacheManager(); } } // 像其他类型的缓存一样使用 @Cacheable("cacheName") // 请求拦截器,需要注册到`/**`路径 @Component public class RequestLevelCacheInterceptor implements HandlerInterceptor { @Autowired private RequestLevelCacheManager requestLevelCacheManager; /** 在请求执行结束时,清除请求级缓存 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { requestLevelCacheManager.clearCache(); } }