赞
踩
HotSpot内存布局:
程序计数器
堆
虚拟机栈
本地方法栈
类似Java虚拟机栈,与Java虚拟机区别在于:服务对象,即Java虚拟机栈为执行 Java 方法服务;本地方法栈为执行 Native方法服务
方法区
其内部包含一个运行时常量池,具体介绍如下
调优工具:
调优目的:减少GC的频率和FullGC的次数
会对整个堆进行整理,包括Young、Tenured和Perm。
Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。
导致Full GC的原因:
1)年老代(Tenured)被写满
调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。
2)持久代Pemanet Generation空间不足
增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
3)System.gc()被显示调用
垃圾回收不要手动触发,尽量依靠JVM自身的机制
判断是否需要优化
堆内存调优参数
GC调优参数
线程调优参数
类加载调优参数
其他调优参数
-XX:+UseBiasedLocking:启用偏向锁
-XX:+OptimizeStringConcat:启用字符串拼接优化
-XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值
-XX:CompileThreshold:JIT编译阈值
-XX:+PrintGCDetails:打印GC详细信息
1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值。
2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。
3.年轻代和年老代设置多大才算合理
1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率
项目背景:高 QPS 压力的 web 服务,单机 QPS 一直维持在 1.5K 以上,配置的堆大小是 8G,其中 young 区是 4G,垃圾回收器用的是 parNew + CMS
首先是查看当前 GC 的情况,主要是使用 jstat 查看 GC 的概况,再查看 gc log,分析单次 gc 的详细状况
使用 jstat -gcutil pid 1000 每隔一秒打印一次 gc 统计信息
S0: 新生代中Survivor space 0区已使用空间的百分比
S1: 新生代中Survivor space 1区已使用空间的百分比
E: 新生代已使用空间的百分比
O: 老年代已使用空间的百分比
P: 永久带已使用空间的百分比
YGC: 从应用程序启动到当前,发生Yang GC 的次数
YGCT: 从应用程序启动到当前,Yang GC所用的时间【单位秒】
FGC: 从应用程序启动到当前,发生Full GC的次数
FGCT: 从应用程序启动到当前,Full GC所用的时间
GCT: 从应用程序启动到当前,用于垃圾回收的总时间【单位秒】
接着查看 gc log,打印 gc log 需要在 JVM 启动参数里添加以下参数:
-XX:+PrintGCDateStamps:打印 gc 发生的时间戳。
-XX:+PrintTenuringDistribution:打印 gc 发生时的分代信息。
-XX:+PrintGCApplicationStoppedTime:打印 gc 停顿时长
-XX:+PrintGCApplicationConcurrentTime:打印 gc 间隔的服务运行时长
-XX:+PrintGCDetails:打印 gc 详情,包括 gc 前/内存等。
-Xloggc:…/gclogs/gc.log.date:指定 gc log 的路径
YGC 频繁,借用一些可视化工具来帮助我们分析, gceasy 是个挺不错的网站,我们把 gc log 上传上去后, gceasy 可以帮助我们生成各个维度的图表帮助分析。
查看 gceasy 生成的报告,发现我们服务的 gc 吞吐量是 95%,它指的是 JVM 运行业务代码的时长占 JVM 总运行时长的比例,这个比例确实有些低了,运行 100 分钟就有 5 分钟在执行 gc。幸好这些 GC 中绝大多数都是 YGC,单次时长可控且分布平均,这使得我们服务还能平稳运行。
解决这个问题要么是减少对象的创建,要么就增大 young 区。前者不是一时半会儿都解决的,需要查找代码里可能有问题的点,分步优化。而后者虽然改一下配置就行,但以我们对 GC 最直观的印象来说,增大 young 区,YGC 的时长也会迅速增大。
YGC 的耗时是由 GC 标记 + GC 复制 组成的,相对于 GC 复制,GC 标记是非常快的。而 young 区内大多数对象的生命周期都非常短,如果将 young 区增大一倍,GC 标记的时长会提升一倍,但到 GC 发生时被标记的对象大部分已经死亡, GC 复制的时长肯定不会提升一倍,所以我们可以放心增大 young 区大小,堆大小调整到了 12G,young 区保留为 8G。
分代调整
GC 太频繁之外,GC 后各分代的平均大小也需要调整
MaxTenuringThreshold 的对象提升到老年代
JVM 还有动态年龄计算的规则:按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值,但看各代总的内存大小,是达不到 survivor 区的一半的
所以这十五个分代内的对象会一直在两个 survivor 区之间来回复制,再观察各分代的平均大小,可以看到,四代以上的对象已经有一半都会保留到老年区了,所以可以将这些对象直接提升到老年代,以减少对象在两个 survivor 区之间复制的性能开销。
所以我把 MaxTenuringThreshold 的值调整为 4,将存活超过四代的对象直接提升到老年代。
偏向锁停顿
还有一个问题是 gc log 里有很多 18ms 左右的停顿,有时候连续有十多条,虽然每次停顿时长不长,但连续多次累积的时间也非常可观。
1.8 之后 JVM 对锁进行了优化,添加了偏向锁的概念,避免了很多不必要的加锁操作,但偏向锁一旦遇到锁竞争,取消锁需要进入 safe point,导致 STW。
解决方式很简单,JVM 启动参数里添加 -XX:-UseBiasedLocking 即可。
结果:
调整完 JVM 参数后先是对服务进行压测,发现性能确实有提升,也没有发生严重的 GC 问题,之后再把调整好的配置放到线上机器进行灰度,同时收集 gc log,再次进行分析。
由于 young 区大小翻倍了,所以 YGC 的频率减半了,GC 的吞量提升到了 97.75%。平均 GC 时长略有上升,从 60ms 左右提升到了 66ms,还是挺符合预期的。
由于 CMS 在进行 GC 时也会清理 young 区,CMS 的时长也受到了影响,CMS 的最终标记和并发清理阶段耗时增加了,也比较正常。
另外我还统计了对业务的影响,之前因为 GC 导致超时的请求大大减少了。
一、SQL调优:主要集中在索引、减少跨表与大数据join查询
二、数据端架构设计(读写分离、分库分表解决数据库连接池瓶颈问题)
三、连接池调优(通过具体的连接池监控数据)
四、通过缓存
数据量小,并且不会频繁地增长又清空(这会导致频繁地垃圾回收),那么可以选择本地缓存
如果需要策略支持(比如缓存满的逐出策略)考虑使用Ehcache,如果不需要,可以考虑HashMap
如果需要考虑多线程并发,使用ConcurentHashMap
① 给缓存服务,选择合适的缓存逐出算法,比如最常见的LRU。
② 针对当前设置的容量,设置适当的警戒值,比如10G的缓存,当缓存数据达到8G的时候,就开始发出报警,提前排查问题或者扩容。
③ 给一些没有必要长期保存的key,尽量设置过期时间。
数据库切分
一个数据库切分成 N 多个数据库,然后存放在不同的数据库实例上面,降低单台数据库实例的负载,方便的实现对数据库的扩容
①、水平切分
假设我的 DB 中有 table-1、table-2 以及 table-3 三张表,水平切分就是、对准黑色的线条,砍一剑或者砍 N 剑!
砍完之后,将砍掉的部分放到另外一个数据库实例中,变成下面这样:
这样,原本放在一个 DB 中的 table 现在放在两个 DB 中了,观察之后我们发现:
这就是数据库的水平切分,也可以理解为按照数据行进行切分,即按照表中某个字段的某种规则来将表数据分散到多个库之中,每个表中包含一部分数据。几个常见的分片规则:
优点:
缺点:
②、垂直切分
所谓的垂直切分就是对准了黑色的线条砍。砍完之后,将不同的表放到不同的数据库实例中去,变成下面这个样子:
如下几个特点:
这就是垂直切分。一般来说,垂直切分我们可以按照业务来划分,不同业务的表放到不同的数据库实例中
在实际项目中,数据库垂直切分并不是一件容易的事,因为表之间往往存在着复杂的跨库 JOIN 问题,那么这个时候如何取舍,就要考验架构师的水平了
优点:
缺点:
一、尽可能减少HTTP请求:图片合并(css sprites),Js脚本文件合并、CSS文件合并
二、减少DNS查询
三、将css放在页面最上面,将js放在页面最下面
四、压缩js和css,减少文件体积,去除不必要的空白符、格式符、注释
五、把js和css提取出来放在外部文件中
六、避免重定向,会增加服务器和浏览器之间的往返次数
重定向状态码:301永久重定向 302临时重定向 304 not modified并不是真的重定向,告诉浏览器get请求的文件在缓存中,避免重新下载
七、使用Gzip压缩
八、使用CDN(内容分发网络)
九、数据请求改为异步
额外开辟线程或使用线程池,在IO线程处理之外的线程处理响应的任务,在IO线程中让response先返回
如果异步线程处理的任务设计的数量巨大,可以引入阻塞队列BlockingQueue作进一步优化
使用消息队列MQ,MQ天生就是异步的
===============================================================
一、抽取公用方法
public class TianLuoExample{ public static void main(){ List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo"); System.out.println("Uppercase Names:"); for (String name : names) { String uppercaseName = name.toUpperCase(); System.out.println(uppercaseName); } System.out.println("Lowercase Names:"); for (String name : names) { String lowercaseName = name.toLowerCase(); System.out.println(lowercaseName); } } }
显然,都是遍历names过程,代码是重复冗余的,只不过转化大小写不一样而已。
抽个公用方法processNames,优化成这样:
Function<String,String>接口
public class TianLuoExample{ public static void processNames(List<String> names,Function<String,String> ameProcessor,String processType){ System.out.println(processType + " Names:") for(String name:names){ String processedName = nameProcessor.apply(name); System.out.println(processName); } } public static void main(String[] args){ List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo"); processNames(names,String::toUpperCase,"Uppercase"); processNames(names,String::toLowerCase,"Lowercase"); } }
二、抽工具类
抽一个公用方法后,如果发现这个方法有更多共性,就可以把公用方法升级为一个工具类。比如这样的业务场景:注册,修改邮箱,重置密码等,都需要校验邮箱
实现注册功能时,用户会填邮箱,需要验证邮箱格式
public class RegisterServiceImpl implements RegisterService{ private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@(.+)$"; public boolean registerUser(UserInfoReq userInfo){ String email = userInfo.getEmail(); Pattern pattern = Pattern.compile(EMAIL_REGEX); Matcher emailMatcher = pattern.matcher(email); if(!emailMatcher.matches()){ System.out.println("Invalid email address."); return false; } // 进行其他用户注册逻辑,比如保存用户信息到数据库等 // 返回注册结果 return true; } }
在密码重置流程中,通常会向用户提供一个链接或验证码,并且需要发送到用户的电子邮件地址。在这种情况下,也需要验证邮箱格式合法性:
public class PasswordServiceImpl implements PasswordService{ private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@(.+)$"; public void resetPassword(PasswordInfo passwordInfo) { Pattern pattern = Pattern.compile(EMAIL_REGEX); Matcher emailMatcher = pattern.matcher(passwordInfo.getEmail()); if (!emailMatcher.matches()) { System.out.println("Invalid email address."); return false; } //发送通知修改密码 sendReSetPasswordNotify(); } }
对于上面的重复性验证,抽取一个校验工具
public class EmailValidatorUtil{
private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@(.+)$";
private static final Pattern pattern = Pattern.compile(EMAIL_REGEX);
public static boolean isValid(String email){
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
}
注册的代码可以简化
public class RegisterServiceImpl implements RegisterService{
public boolean registerUser(UserInfoReq userInfo){
if(!EmailValidatorUtil.isValid(userInfo.getEmail())){
System.out.println("Invalid email address.");
return false;
}
// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}
三、反射
需要进行PO、DTO和VO的转化
//DTO 转 VO public UserInfoVO convert(UserInfoDTO userInfoDTO){ UserInfoVO userInfoVO = new UserInfoVO(); userInfoVo.setUserName(userInfoDTO.getUserName()); userInfo.setAge(uerInfoDTP.getAge()); return userInfoVO; } //PO 转 DTO public UserInfoDTO convert(UserInfoPO userInfoPO){ UserInfoDTO userInfoDTO = new UserInfoDTO(); userInfoDTO.setUserName(userInfoPO.getUserName()); userInfoDTO.setAge(userInfoPO.getAge()); return userInfoDTO; }
可以使用BeanUtils.copyProperties() 去除重复代码
BeanUtils.copyProperties()底层就是使用了反射:
public UserInfoVO convert(UserInfoDTO userInfoDTO){
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDTO,userInfoVO)
return userInfoVO;
}
public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
BeanUtils.copyProperties(userInfoPO,userInfoDTO);
return userInfoDTO;
}
四、泛型
转账明细
这两块代码,流程功能看着很像,但是就是不能直接合并抽取一个公用方法,因为类型不一致。
private void getAndUpdateBalanceResultMap(String key,Map<String,List<TransferBalanceDTO>> compareResultListMap,List<TransferBalanceDTO> balanceDTOs){
List<TransferBalanceDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(balanceDTOs);
compareResultListMap.put(key, tempList);
}
private void getAndUpdateDetailResultMap(String key, Map<String, List<TransferDetailDTO>> compareResultListMap,
List<TransferDetailDTO> detailDTOS) {
List<TransferDetailDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(detailDTOS);
compareResultListMap.put(key, tempList);
}
单纯类型不一样的话,我们可以结合泛型处理,因为泛型的本质就是参数化类型.优化为这样:
private <T> void getAndUpdateResultMap(String key,Map<String,List<T>> compareResultListMap, List<T> accountingDTOS){
List<T> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(accountingDTOS);
compareResultListMap.put(key, tempList);
}
五、继承与多态
开发一个电子商务平台,需要处理不同类型的订单,例如普通订单和折扣订单
每种订单都有一些共同的属性(如订单号、购买商品列表)和方法(如计算总价、生成订单报告),但折扣订单还有特定的属性和方法。
//普通订单 public class Order{ private String orderNumber; private List<Product> products; public double calculateTotalPrice() { double total = 0; for (Product product : products) { total += product.getPrice(); } return total; } public String generateOrderReport() { return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice(); } } //折扣订单 public class DiscountOrder { private String orderNumber; private List<Product> products; private double discountPercentage; public DiscountOrder(String orderNumber, List<Product> products, double discountPercentage) { this.orderNumber = orderNumber; this.products = products; this.discountPercentage = discountPercentage; } public double calculateTotalPrice() { double total = 0; for (Product product : products) { total += product.getPrice(); } return total - (total * discountPercentage / 100); } public String generateOrderReport() { return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice(); } }
使用继承和多态去除重复代码,让DiscountOrder去继承Order
public class Order{ private String orderNumber private List<Product> products; public Order(String orderNumber, List<Product> products) { this.orderNumber = orderNumber; this.products = products; } public double calculateTotalPrice() { double total = 0; for (Product product : products) { total += product.getPrice(); } return total; } public String generateOrderReport() { return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice(); } } //折扣订单继承普通订单 public class DiscountOrder extends Order { private double discountPercentage; public DiscountOrder(String orderNumber, List<Product> products, double discountPercentage) { super(orderNumber, products); this.discountPercentage = discountPercentage; } @Override public double calculateTotalPrice() { double total = super.calculateTotalPrice(); return total - (total * discountPercentage / 100); } }
六、使用设计模式
https://editor.csdn.net/md/?articleId=128459370
七、自定义注解或AOP切面
开发一个Web应用程序,需要对不同的Controller方法进行权限检查。
每个Controller方法都需要进行类似的权限验证,但是重复的代码会导致代码的冗余和维护困难
public class MyController{ public void viewData(){ if(!User.hasPermission("read")){ throw new SecurityException("Insufficient permission to access this resource."); } //Method implementation } public void modifyData(){ if (!User.hasPermission("write")) { throw new SecurityException("Insufficient permission to access this resource."); } // Method implementation } }
每个需要权限校验的方法中都需要重复编写相同的权限校验逻辑,即出现了重复代码.我们使用自定义注解的方式能够将权限校验逻辑集中管理,通过切面来处理,消除重复代码
@Aspect
@Component
public class PermissionAspect{
@Before("@annotation(requiresPermission)")
public void checkPermission(RequiresPermisssion requiresPermission){
String permission = requiresPermission.value();
if (!User.hasPermission(permission)) {
throw new SecurityException("Insufficient permission to access this resource.");
}
}
}
public class MyController {
@RequiresPermission("read")
public void viewData() {
// Method implementation
}
@RequiresPermission("write")
public void modifyData() {
// Method implementation
}
}
不管多少个Controller方法需要进行权限检查,你只需在方法上添加相应的注解即可。权限检查的逻辑在切面中集中管理,避免了在每个Controller方法中重复编写相同的权限验证代码。
八、函数式接口和Lambda表达式
根据不同的条件来过滤一组数据
public class DataFilter { public List<Integer> filterPositiveNumbers(List<Integer> numbers) { List<Integer> result = new ArrayList<>(); for (Integer number : numbers) { if (number > 0) { result.add(number); } } return result; } public List<Integer> filterEvenNumbers(List<Integer> numbers) { List<Integer> result = new ArrayList<>(); for (Integer number : numbers) { if (number % 2 == 0) { result.add(number); } } return result; } }
通过函数式接口进行过滤
public class DataFilter{
public List<Integer> filterNumbers(List<Integer> numbers,Predicate<Integer> predicate){
List<Integer> result = new ArrayList<>();
for(Integer number:numbers){
if(predicate.test(number)){
result.add(number);
}
}
return result;
}
}
将过滤的核心逻辑抽象出来。该方法接受一个 Predicate函数式接口作为参数,以便根据不同的条件来过滤数据。然后,我们可以使用Lambda表达式来传递具体的条件
影响接口缓慢的因素:
一、慢查询(基于mysql)
①、深度分页
通常分页:
#从student表里查100到120这20条数据,mysql会把前120条数据都查出来,抛弃前100条,返回20条
select name,code from student limit 100,20
#当分页所以深度不大的时候当然没问题,随着分页的深入,sql可能会变成这样
#mysql会查出来1000020条数据,抛弃1000000条,如此大的数据量,速度一定快不起来
select name,code from student limit 1000000,20
# 添加一个条件mysql会走主键索引,直接连接到1000000处,然后查出来20条数据
select name,code from student where id>1000000 limit 20
②、未加索引
加索引之前,需要考虑一下这个索引是不是有必要加,如果加索引的字段区分度非常低,那即使加了索引也不会生效。另外,加索引的alter操作,可能引起锁表,执行sql的时候一定要在低峰期
#查看某张表的索引
show create table xxxx(表名)
③、索引失效
虽然mysql提供了explain来评估某个sql的查询性能,其中就有使用的索引。但mysql未明确告知
字段区分性很差
那如果不符合上面所有的索引失效的情况,但是mysql还是不使用对应的索引,是为啥呢?这个跟mysql的sql优化有关,mysql会在sql优化的时候自己选择合适的索引,很可能是mysql自己的选择算法算出来使用这个索引不会提升性能,所以就放弃了。
可以使用force index 关键字强制使用索引(建议修改前先实验一下,是不是真的会提升查询效率):
#其中xxxx是索引名
select name,code from student force index(XXXXXX) where name = '天才'
④、join过多or子查询过多
一般来说,不建议使用子查询,可以把子查询改成join来优化。同时,join关联的表也不宜过多,一般来说2-3张表还是合适的。
在大多数情况下join是在内存里做的,如果匹配的量比较小,或者join_buffer设置的比较大,速度也不会很慢。但是,当join的数据量比较大的时候,mysql会采用在硬盘上创建临时表的方式进行多张表的关联匹配,这种显然效率就极低,本来磁盘的IO就不快,还要关联。
一般遇到这种情况的时候就建议从代码层面进行拆分,在业务层先查询一张表的数据,然后以关联字段作为条件查询关联表形成map,然后在业务层进行数据的拼装。一般来说,索引建立正确的话,会比join快很多,毕竟内存里拼接数据要比网络传输和硬盘IO快得多。
⑤、in的元素过多
这种问题,如果只看代码的话不太容易排查,最好结合监控和数据库日志一起分析。
如果一个查询有in,in的条件加了合适的索引,这个时候的sql还是比较慢就可以高度怀疑是in的元素过多。
一旦排查出来是这个问题,解决起来也比较容易,不过是把元素分个组,每组查一次。想再快的话,可以再引入多线程。
进一步的,如果in的元素量大到一定程度还是快不起来,这种最好还是有个限制
select id from student where id in (1,2,3 ...... 1000) limit 200
代码层限制
if(ids.size() > 200){
throw new Exception("单次查询数据量不超过200");
}
⑥、单纯的数据量过大
需要变动整个的数据存储架构
底层mysql分表或分库+分表
直接变更底层数据库,把mysql转换成专门为处理大数据设计的数据库。这种工作是个系统工程,需要严密的调研、方案设计、方案评审、性能评估、开发、测试、联调,同时需要设计严密的数据迁移方案、回滚方案、降级措施、故障处理预案。除了以上团队内部的工作,还可能有跨系统沟通的工作,毕竟做了重大变更,下游系统的调用接口的方式有可能会需要变化。
二、业务逻辑复杂
①、循环调用
这种情况,一般都循环调用同一段代码,每次循环的逻辑一致,前后不关联。比如说,我们要初始化一个列表,预置12个月的数据给前端:
List<Model> list = new ArrayList<>();
for(int i = 0;i<12;i++){
Model model = calOneMonthData(i);//计算某个月的数据,逻辑比较复杂,难以批量计算,效率也无法很高
list.add(model);
}
这种显然每个月的数据计算相互都是独立的,我们完全可以采用多线程方式进行:
//创建一个线程池, public static ExecutorService commonThreadPool = new ThreadPoolExecutor( 5, 5, 300L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy() ); //多线程调用 List<Future<Model>> futures = new ArrayList<>(); for(int i=0;i<12;i++){ Future<Model> future = commonThreadPool.submit(() -> calOneMonthData(i)); futures.add(future); } //获取结果 List<Model> list = new ArrayList<>(); try{ for(int i=0;i<futures.size;i++){ list.add(futures.get(i).get()); } }catch(Exception e){ LOGGER.error("出现错误:",e); }
②、顺序调用
如果不是类似上面循环调用,而是一次次的顺序调用,而且调用之间没有结果上的依赖,那么也可以用多线程的方式进行,例如:
A a = doA();
B b = doB();
C c = doC(a, b);
D d = doD(c);
E e = doE(c);
return doResult(d, e);
那么可用CompletableFuture解决
CompletableFuture<A> futureA = CompletableFuture.supplyAsync(()->doA());
CompletableFuture<B> futureB = CompletableFuture.supplyAsync(()->doB());
//等a,b两个任务都执行完成
CompletableFuture.allOf(futureA,futureB);
C c = doC(futureA.join(),futureB.join());
CompletableFuture<D> futureD = CompletableFuture.supplyAsync(() -> doD(c));
CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() -> doE(c));
CompletableFuture.allOf(futureD,futureE) // 等d e两个任务都执行完成
return doResult(futureD.join(),futureE.join());
这样A B 两个逻辑可以并行执行,D E两个逻辑可以并行执行,最大执行时间取决于哪个逻辑更慢。
三、线程池设计不合理
使用了线程池让任务并行处理,接口的执行效率仍然不够快。
线程池的三个重要参数:核心线程数、最大线程数、等待队列。
线程池创建的时候,如果不预热线程池,则线程池中线程为0。当有任务提交到线程池,则开始创建核心线程。
当核心线程全部被占满,如果再有任务到达,则让任务进入等待队列开始等待。
如果队列也被占满,则开始创建非核心线程运行。
如果线程总数达到最大线程数,还是有任务到达,则开始根据线程池抛弃规则开始抛弃。
在排查的时候,只要找到了问题出现的原因,那么解决方式也就清楚了,无非就是调整线程池参数,按照业务拆分线程池等等。
四、锁设计不合理
锁设计不合理一般有两种:锁类型使用不合理 or 锁过粗
锁类型使用不合理的典型场景就是读写锁。也就是说,读是可以共享的,但是读的时候不能对共享变量写;而在写的时候,读写都不能进行。
在可以加读写锁的时候,如果我们加成了互斥锁,那么在读远远多于写的场景下,效率会极大降低。
锁过粗则是另一种常见的锁设计不合理的情况,如果我们把锁包裹的范围过大,则加锁时间会过长,例如:
public synchronized void doSome(){
File f = calData();
uploadToS3(f);
sendSuccessMessage();
}
这块逻辑一共处理了三部分,计算、上传结果、发送消息。显然上传结果和发送消息是完全可以不加锁的,因为这个跟共享变量根本不沾边。因此完全可以改成:
public void doSome(){
File f = null;
synchronized(this){//只给计算添加锁
f = calData();
}
uploadToS3(f); //上传结果
sendSuccessMessage();//发送消息
}
五、机器问题(fullGC,机器重启,线程打满)
造成这个问题的原因非常多,笔者就遇到了定时任务过大引起fullGC,代码存在线程泄露引起RSS内存占用过高进而引起机器重启等待诸多原因。
需要结合各种监控和具体场景具体分析,进而进行大事务拆分、重新规划线程池等等工作
⑥、万金油解决方式
①、缓存,空间换取时间的解决方案,是在高性能存储介质上(例如:内存、SSD硬盘等)存储一份数据备份。
有请求打到服务器的时候,优先从缓存中读取数据。如果读取不到,则再从硬盘或通过网络获取数据。
由于内存或SSD相比硬盘或网络IO的效率高很多,则接口响应速度会变快非常多。缓存适合于应用在数据读远远大于数据写,且数据变化不频繁的场景中。从技术选型上看,有这些:
memcached现在用的很少了,因为相比于redis他不占优势。tair则是阿里开发的一个分布式缓存中间件,他的优势是理论上可以在不停服的情况下,动态扩展存储容量,适用于大数据量缓存存储。相比于单机redis缓存当然有优势,而他与可扩展Redis集群的对比则需要进一步调研。
前缓存的模型一般都是key-value模型。如何设计key以提高缓存的命中率是个大学问,好的key设计和坏的key设计所提升的性能差别非常大。而且,key设计是没有一定之规的,需要结合具体的业务场景去分析。各个大公司分享出来的相关文章,缓存设计基本上是最大篇幅。
②、回调或反查
这种方式往往是业务上的解决方式,在订单或者付款系统中应用的比较多。
举个例子:当我们付款的时候,需要调用一个专门的付款系统接口,该系统经过一系列验证、存储工作后还要调用银行接口以执行付款。
于付款这个动作要求十分严谨,银行侧接口执行可能比较缓慢,进而拖累整个付款接口性能。可以采用fast success的方式:当必要的校验和存储完成后,立即返回success,同时告诉调用方一个中间态“付款中”。
而后调用银行接口,当获得支付结果后再调用上游系统的回调接口返回付款的最终结果“成果”or“失败”。
这样就可以异步执行付款过程,提升付款接口效率。当然,为了防止多业务方接入的时候回调接口不统一,可以把结果抛进kafka,让调用方监听自己的结果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。