前言
本文是博主从事后端开发以来,对公司、个人项目的经验总结,包含代码编写、功能推荐、第三方库使用及优雅配置等,希望大家看到都能有所收获
博主github地址: github.com/wayn111一. 优雅的进行线程池异常处理
在Java开发中,线程池的使用必不可少,使用无返回值 execute() 方法时,线程执行发生异常的话,需要记录日志,方便回溯,一般做法是在线程执行方法内 try/catch 处理,如下:
@Test public void test() throws Exception { ThreadPoolExecutor threadPoolExecutor = newThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000)); Future<Integer> submit = threadPoolExecutor.execute(() -> {try { int i = 1 / 0; return i; } catch(Exception e) { log.error(e.getMessage(), e);return null; } }); } 复制代码但是当线程池调用方法很多时,那么每个线程执行方法内都要 try/catch 处理,这就不优雅了,其实ThreadPoolExecutor类还支持传入 ThreadFactory 参数,自定义线程工厂,在创建 thread 时,指定
setUncaughtExceptionHandler 异常处理方法,这样就可以做到全局处理异常了,代码如下:ThreadFactory threadFactory = r -> { Thread thread = newThread(r); thread.setUncaughtExceptionHandler((t, e) -> { // 记录线程异常 log.error(e.getMessage(), e); }); return thread; }; ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100000), threadFactory); threadPoolExecutor.execute(()-> { log.info(“———————“); int i = 1 / 0; }); 复制代码二. 线程池决绝策略设置错误导致业务接口执行超时
先介绍下线程池得四种决绝策略
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,这是线程池默认的拒绝策略DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。CallerRunsPolicy:由调用线程处理该任务如下是一个线上业务接口使用得线程池配置,决绝策略采用 CallerRunsPolicy
// 某个线上线程池配置如下 ThreadPoolExecutor threadPoolExecutor = newThreadPoolExecutor(50, // 最小核心线程数 50, // 最大线程数,当队列满时,能创建的最大线程数 60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间 new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列 new CustomizableThreadFactory(“task”), // 自定义线程名 new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略 ); 复制代码在某些情况下,子线程任务调用第三方接口超时,导致核心线程数、最大线程数占满、阻塞队列占满的情况下执行拒绝策略时,由于使用 CallerRunsPolicy 策略,导致业务线程执行子任务时继续超时,进而导致接口执行异常,这种情况下,考虑到子线程任务得重要性,不是很重要得话,可以使用 DiscardPolicy 策略,要是很重要,可以发送到消息队列中持久化子线程任务数据待后续处理
三. 优雅的单例模式懒加载帮助类代码实现
博主推荐通过静态内部类实现单例模式,并实现懒加载效果,代码如下
// 使用静态内部类完成单例模式封装,避免线程安全问题,避免重复初始化成员属性 @Slf4j public class FilterIpUtil { private FilterIpUtil() { } private List<String> strings = new ArrayList<>(); // 代码块在FilterIpUtil实例初始化时才会执行 { // 在代码块中完成文件的第一次读写操作,后续不再读这个文件 System.out.println(“FilterIpUtil init”); try(InputStream resourceAsStream = FilterIpUtil.class.getClassLoader().getResourceAsStream(“filterIp.txt”)) { // 将文件内容放到string集合中 IoUtil.readUtf8Lines(resourceAsStream, strings); }catch (IOException e) { log.error(e.getMessage(), e); } } public static FilterIpUtil getInstance() { return InnerClassInstance.instance; } // 使用内部类完成单例模式,由jvm保证线程安全 private static class InnerClassInstance { private static final FilterIpUtil instance = newFilterIpUtil(); }// 判断集合中是否包含目标参数 public boolean isFilter(String arg) { returnstrings.contains(arg); } } 复制代码四. 使用ip2region实现请求地址解析
github 上了解到 ip2region 这个项目,使用本地ip库查询,查询速度微秒级别, 精准度能达到90%,但是ip库还是有少部分ip信息不准,建议数据库中把请求ip地址保存下来。简介如下:
ip2region v2.0 – 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现基于 xdb 文件的查询,下面是一个 Spring 项目中 ip2region 帮助类来实现ip地址解析
/** * ip2region工具类 */ @Slf4j @Component public class Ip2region { private Searcher searcher = null; @Value(“${ip2region.path:}”) privateString ip2regionPath =“”; @PostConstruct private void init() { // 1、从 dbPath 加载整个 xdb 到内存。String dbPath = ip2regionPath;// 1、从 dbPath 加载整个 xdb 到内存。 byte[] cBuff; try{ cBuff = Searcher.loadContentFromFile(dbPath); searcher = Searcher.newWithBuffer(cBuff); }catch (Exception e) { log.error(“failed to create content cached searcher: {}”, e.getMessage(), e); } } public IpInfoBean getIpInfo(String ip) { if (StringUtils.isBlank(ip)) { return null; } // 3、查询 try { long sTime = System.nanoTime(); // 国家|区域|省份|城市|ISP String region = searcher.search(ip); long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() – sTime)); log.info(“{region: {}, ioCount: {}, took: {} μs}”, region, searcher.getIOCount(), cost);if (StringUtils.isNotBlank(region)) { String[] split = region.split(“\|”); IpInfoBean ipInfo =new IpInfoBean(); ipInfo.setIp(ip); if (!“”.equals(split[0])) { ipInfo.setCountry(split[0]); } if (!“”.equals(split[2])) { ipInfo.setProvince(split[2]); } if (!“”.equals(split[3])) { ipInfo.setCity(split[3]); } if (!“”.equals(split[4])) { ipInfo.setIsp(split[4]); }return ipInfo; } } catch (Exception e) { log.error(“failed to search({}): {}”, ip, e);return null; } // 4、关闭资源 – 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher // searcher.close(); // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。 return null; } } 复制代码要注意得就是 ip2region v2.0 版本使用的xdb文件不建议放在项目 resources 下一起打包,存在编码格式问题,建议通过指定路径加载得方式单独放在服务器目录下
五. 优雅得Springboot + mybatis配置多数据源方式
Springboot + mybatis 得项目中一般通过 @MapperScan 注解配置 dao 层包目录,来实现 dao 层增强,其实项目中配置一个@MapperScan 是指定一个数据源,配置两个@MapperScan就可以指定两个数据源,通过不同得 dao 层包目录区分,来实现不同数据源得访问隔离。
比如下面代码中,com.xxx.dao.master 目录下为主数据源 dao 文件,com.xxx.dao.slave 为从数据源 dao 文件,这个方式比网上得基于 aop 加注解得方式更加简洁好用,也没有单个方法中使用不同数据源切换得问题,因此推荐这种写法
/** * 主数据源 */ @Slf4j @Configuration @MapperScan(basePackages = {“com.xxx.dao.master”}, sqlSessionFactoryRef = “MasterSqlSessionFactory”) public class MasterDataSourceConfig { @Bean(name =“MasterDataSource”) @Qualifier(“MasterDataSource”) @ConfigurationProperties(prefix = “spring.datasource.master”) public DataSource clickHouseDataSource() { returnDruidDataSourceBuilder.create().build(); }@Bean(name = “MasterSqlSessionFactory”) public SqlSessionFactory getSqlSessionFactory(@Qualifier(“MasterDataSource”)DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(“classpath*:mapper/master/*.xml”)); log.info(“——————————————MasterDataSource 配置成功”); return sessionFactoryBean.getObject(); } } /** * 从数据源 */ @Slf4j @Configuration @MapperScan(basePackages = {“com.xxx.dao.slave”}, sqlSessionFactoryRef =“SlaveSqlSessionFactory”) public class SlaveDataSourceConfig { @Bean(name = “SlaveDataSource”) @Qualifier(“SlaveDataSource”) @ConfigurationProperties(prefix = “spring.datasource.slave”) publicDataSource clickHouseDataSource() {return DruidDataSourceBuilder.create().build(); } @Bean(name = “SlaveSqlSessionFactory”) public SqlSessionFactory getSqlSessionFactory(@Qualifier(“SlaveDataSource”)DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(“classpath*:mapper/slave/*.xml”)); log.info(“——————————————SlaveDataSource 配置成功”); returnsessionFactoryBean.getObject(); } } 复制代码数据源yml配置
spring: datasource: type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driver # 主库数据源 master: url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: slave: url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 复制代码博主刚开始编码一、两年得时候一个项目中遇到了多数据源使用得问题,那时候题主便在网上搜索Spring 多数据源得帖子,大多数都是基于Spring提供得AbstractRoutingDataSource + AOP + 注解 来做动态切换,包括现在流行得 Mybatis plus 官方得多数据源解决方案也是这种做法,这种做法解决了博主当时得多数据源使用问题,后来加了一个需求,在一个定时任务中,查询两个数据源得数据,才发现动态切换在单个方法中不好用了,最后使用得原生jdbc数据源解决。多年后,博主在另一家公司得项目中又遇到了多数据源问题,但是这次博主在网上搜索得是Mybatis 多数据源,才发现了这个优雅得解决方案,进而推荐给大家
六. Spring Security项目中,使用MDC实现接口请求调用追踪,以及用户ID记录
MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问
虽然MDC能够方便得实现接口请求调用追踪功能,但是它在子线程中会丢失父线程中添加得键值对信息,解决方法是通过父线程中调用线程池前调用 MDC.getCopyOfContextMap() ,然后在子线程中第一个调用 MDC.setCon
/** * 自定义Spring线程池,解决子线程丢失reqest_id问题 */ public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor { @Override public void execute(Runnable task) { super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); }@Override public <T> Future<T> submit(Callable<T> task) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public Future<?> submit(Runnable task) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } }/** * MDC帮助类,添加reqest_id */ public class ThreadMdcUtil { public static final String REQUEST_ID = “request_id”; /** * 设置请求唯一ID */ public static void setTraceIdIfAbsent() { if (MDC.get(REQUEST_ID) == null) { MDC.put(REQUEST_ID, IdUtil.getUid()); } }/** * 存在userId则添加到REQUEST_ID中 * @param userId */ public static void setUserId(String userId) { String s = MDC.get(REQUEST_ID); if (s != null) { MDC.put(REQUEST_ID, s + “_”+ userId); } }public static void removeTraceId() { MDC.remove(REQUEST_ID); } public static <T> Callable<T>wrap(final Callable<T> callable, final Map<String, String> context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } setTraceIdIfAbsent(); try { return callable.call(); } finally { MDC.clear(); } }; } public static Runnable wrap(final Runnable runnable, final Map<String, String> context) { return () -> { if (context == null) { MDC.clear(); }else { MDC.setContextMap(context); } // 设置traceId setTraceIdIfAbsent(); try{ runnable.run(); }finally { MDC.clear(); } }; } } 复制代码在 Spring Security 中添加 token 过滤器
/** * token过滤器 验证token有效性 * * @author ruoyi */ @Slf4j @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired privateTokenService tokenService;@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { try { // 入口传入请求IDThreadMdcUtil.setTraceIdIfAbsent(); LoginUserDetail loginUser = tokenService.getLoginUser(request);if(Objects.nonNull(loginUser) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {// 记录userIdThreadMdcUtil.setUserId(String.valueOf(loginUser.getMember().getId())); tokenService.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities()); authenticationToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); }finally { // 出口移除请求IDThreadMdcUtil.removeTraceId(); } } } 复制代码最后在 logback.xml 中添加 %X{request_id}
<property name=“pattern” value=“%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{request_id}] [%thread] [%-5level] %logger{36}:%L %M – %msg%n”/> 复制代码日志打印效果如下:
2022–11–27 21:29:48.008[86c76336100c414dbe9217aeb099ccd5_12] [http-nio-82-exec-2] [INFO ] c.w.m.a.s.impl.IHomeServiceImpl:56getHomeIndexDataCompletableFuture – getHomeIndexDataCompletableFuture:com.wayn.common.util.R@701f7b8e[code=200,msg=操作成功,map={bannerList=[{“createTime”:“2020-06-26 19:56:03”,“delFlag”:false,“id”:14,“imgUrl”:“https://m.360buyimg.com/mobilecms/s700x280_jfs/t1/117335/39/13837/263099/5f291a83E8ba761d0/5c0460445cb28248.jpg!cr_1125x449_0_166!q70.jpg.dpg”,“jumpUrl”:“http://82.157.141.70/mall/#/detail/1155015”,“sort”:0,“status”:0,“title”:“hh2”,“updateTime”:“2022-06-19 09:16:46”},{“createTime”:“2020-06-26 19:56:03”,“delFlag”:false,“id”:15,“imgUrl”:“https://m.360buyimg.com/mobilecms/s700x280_jfs/t1/202096/26/11652/265782/616acb67E4fcdc9ac/8d7cdfbb6c934e67.jpg!cr_1125x449_0_166!q70.jpg.dpg”,“jumpUrl”:“#/detail/1155015”,“sort”:0,“status”:0,“title”:“hh”,“updateTime”:“2022-06-19 09:04:58”}], newGoodsList=[{“actualSales”:0,“brandId”:0,“brief”:“酥脆奶香,甜酸回味”,“categoryId”:1008015,“counterPrice”:56.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1116011”,“id”:1116011,“isHot”:true,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“蔓越莓曲奇 200克”,“picUrl”:“http://yanxuan.nosdn.127.net/767b370d07f3973500db54900bcbd2a7.png”,“retailPrice”:36.00,“shareUrl”:“”,“sort”:5,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“粉彩色泽,记录生活”,“categoryId”:1012003,“counterPrice”:49.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1127047”,“id”:1127047,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“趣味粉彩系列笔记本”,“picUrl”:“http://yanxuan.nosdn.127.net/6c03ca93d8fe404faa266ea86f3f1e43.png”,“retailPrice”:29.00,“shareUrl”:“”,“sort”:2,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“慢回弹海绵,时尚设计。”,“categoryId”:1008002,“counterPrice”:66.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1134030”,“id”:1134030,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“简约知性记忆棉坐垫”,“picUrl”:“http://yanxuan.nosdn.127.net/aa49dfe878becf768eddc4c1636643a6.png”,“retailPrice”:46.00,“shareUrl”:“”,“sort”:12,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“慢回弹海绵的呵护,萌趣添彩。”,“categoryId”:1008002,“counterPrice”:69.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1134032”,“id”:1134032,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“趣味粉彩系列记忆棉坐垫”,“picUrl”:“http://yanxuan.nosdn.127.net/8b30eeb17c831eba08b97bdcb4c46a8e.png”,“retailPrice”:49.00,“shareUrl”:“”,“sort”:13,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“100%桑蚕丝,丝滑润肤”,“categoryId”:1008009,“counterPrice”:2619.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1135002”,“id”:1135002,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“宫廷奢华真丝四件套”,“picUrl”:“http://yanxuan.nosdn.127.net/45548f26cfd0c7c41e0afc3709d48286.png”,“retailPrice”:2599.00,“shareUrl”:“”,“sort”:1,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“自由海军领探索未来梦”,“categoryId”:1020003,“counterPrice”:89.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1135072”,“id”:1135072,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“经典海魂纹水手裙(婴童)”,“picUrl”:“http://yanxuan.nosdn.127.net/43e57d4208cdc78ac9c088f9b3e798f5.png”,“retailPrice”:69.00,“shareUrl”:“”,“sort”:3,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“经典海魂纹自由海军领”,“categoryId”:1020003,“counterPrice”:89.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1135073”,“id”:1135073,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“海魂纹哈衣水手服(婴童)”,“picUrl”:“http://yanxuan.nosdn.127.net/53052b04ae001d289c040e09ea92231c.png”,“retailPrice”:69.00,“shareUrl”:“”,“sort”:4,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:5,“brandId”:0,“brief”:“差旅好伴侣”,“categoryId”:1032000,“counterPrice”:119.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1152031”,“id”:1152031,“isHot”:true,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“魔兽世界-伊利丹颈枕眼罩套装”,“picUrl”:“http://yanxuan.nosdn.127.net/fd6e78a397bd9e9804116a36f0270b0a.png”,“retailPrice”:99.00,“shareUrl”:“”,“sort”:4,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:5,“brandId”:0,“brief”:“桌面整理神器”,“categoryId”:1032000,“counterPrice”:519.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1152095”,“id”:1152095,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“魔兽世界 联盟·暴风城 堡垒收纳盒”,“picUrl”:“http://yanxuan.nosdn.127.net/c86b49f635fa141decebabbd0966a6ef.png”,“retailPrice”:499.00,“shareUrl”:“”,“sort”:6,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:0,“brandId”:0,“brief”:“3重透气,清爽柔滑”,“categoryId”:1008009,“counterPrice”:479.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1152161”,“id”:1152161,“isHot”:false,“isNew”:true,“isOnSale”:true,“keywords”:“”,“name”:“竹语丝麻印花四件套”,“picUrl”:“http://yanxuan.nosdn.127.net/977401e75113f7c8334c4fb5b4bf6215.png”,“retailPrice”:459.00,“shareUrl”:“”,“sort”:6,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10}], categoryList=[{“createTime”:“2020-12-08 23:09:12”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/9fc3c52571aa38a1466f114c2dc892fc.png”,“id”:2,“jumpType”:0,“name”:“大牌手机”,“picUrl”:“http://cdn.wayn.xin/2545dd00ca4575759024af2811949a9d.jpg”,“sort”:1,“status”:0,“updateTime”:“2020-12-12 19:26:48”,“valueId”:5,“valueUrl”:“http://baidu.com”},{“createTime”:“2020-12-06 13:27:54”,“delFlag”:false,“iconUrl”:“http://82.157.141.70/upload/2022/02/21/a23fa32c8f4004a8c9fbbb1784462163.jpg”,“id”:1,“jumpType”:0,“name”:“滋补保健2”,“picUrl”:“http://cdn.wayn.xin/d4de7172eb7ae4178ae4dafd6a973d8f.jpg”,“sort”:2,“status”:0,“updateTime”:“2022-06-19 09:17:20”,“valueId”:2},{“createTime”:“2020-12-08 23:26:15”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/6bc0f8a131d2d16b8fc2004d4aa4860c.png”,“id”:3,“jumpType”:1,“name”:“不锈钢锅”,“picUrl”:“http://cdn.wayn.xin/314d87257f7a2ff03d5f4c5183797912.jpg”,“sort”:2,“status”:0,“updateTime”:“2020-12-12 19:27:24”,“valueId”:1036000},{“createTime”:“2020-12-12 19:28:08”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/5a90531d901529967885279d7dc826e1.png”,“id”:14,“jumpType”:0,“name”:“进口牛奶”,“picUrl”:“http://cdn.wayn.xin/0b1f6ab63d5e222c52c83a2d0581e44c.jpg”,“sort”:3,“status”:0,“valueId”:5},{“createTime”:“2020-12-12 19:28:33”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/33530951827ca7e59940d51cda537d84.png”,“id”:15,“jumpType”:0,“name”:“量贩囤货”,“picUrl”:“http://cdn.wayn.xin/bb6daee3b3e51c3008db97585249f513.jpg”,“sort”:4,“status”:0,“valueId”:2},{“createTime”:“2020-12-12 19:28:50”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/7d337f25111b263b29d5d12589015c46.png”,“id”:16,“jumpType”:0,“name”:“清洁用品”,“picUrl”:“http://cdn.wayn.xin/be8995bda39d03b17349b8ec0dcab3d5.jpg”,“sort”:5,“status”:0,“valueId”:2},{“createTime”:“2020-12-12 19:29:10”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/2e632ec0173bb477dcdb601495e0412a.png”,“id”:17,“jumpType”:0,“name”:“洗护用品”,“picUrl”:“http://cdn.wayn.xin/53fb88c9d1245caa882aa3fc29187d0b.jpg”,“sort”:6,“status”:0,“valueId”:4},{“createTime”:“2020-12-12 19:29:28”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/942323c0e74677cf2aa15f09a1e63bca.png”,“id”:18,“jumpType”:0,“name”:“日用百货”,“picUrl”:“http://cdn.wayn.xin/8587f91db2edcb43e57da9835cc7ec76.jpg”,“sort”:7,“status”:0,“valueId”:2},{“createTime”:“2020-12-12 19:29:46”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/18d9d860ba9b8b28522e050f11a8a8e0.png”,“id”:19,“jumpType”:0,“name”:“明星乳胶”,“picUrl”:“http://cdn.wayn.xin/65273c7fb2273e90958e92626248a90a.jpg”,“sort”:8,“status”:0,“valueId”:6},{“createTime”:“2020-12-12 19:30:15”,“delFlag”:false,“iconUrl”:“http://cdn.wayn.xin/7c790577afda91eebc3c95586e190957.png”,“id”:20,“jumpType”:0,“name”:“口碑好物”,“picUrl”:“http://cdn.wayn.xin/210011b35be4ceee39e6a466b40b8e22.jpg”,“sort”:9,“status”:0,“updateTime”:“2021-04-01 20:13:08”,“valueId”:5}], expire_time=1669549170235, hotGoodsList=[{“actualSales”:1,“brandId”:1001045,“brief”:“一级桑蚕丝,吸湿透气柔软”,“categoryId”:1036000,“counterPrice”:719.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1006013”,“id”:1006013,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“双宫茧桑蚕丝被 空调被”,“picUrl”:“http://yanxuan.nosdn.127.net/583812520c68ca7995b6fac4c67ae2c7.png”,“retailPrice”:699.00,“shareUrl”:“”,“sort”:7,“unit”:“件”,“updateTime”:“2021-08-08 11:19:36”,“virtualSales”:10},{“actualSales”:1,“brandId”:1001045,“brief”:“双层子母被,四季皆可使用”,“categoryId”:1008008,“counterPrice”:14199.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1006014”,“id”:1006014,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“双宫茧桑蚕丝被 子母被”,“picUrl”:“http://yanxuan.nosdn.127.net/2b537159f0f789034bf8c4b339c43750.png”,“retailPrice”:1399.00,“shareUrl”:“”,“sort”:15,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:6,“brandId”:1001000,“brief”:“加大加厚,双色精彩”,“categoryId”:1036000,“counterPrice”:219.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1011004”,“id”:1011004,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“色织精梳AB纱格纹空调被”,“picUrl”:“http://yanxuan.nosdn.127.net/0984c9388a2c3fd2335779da904be393.png”,“retailPrice”:199.00,“shareUrl”:“”,“sort”:2,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:6,“brandId”:0,“brief”:“共享亲密2人时光”,“categoryId”:1008008,“counterPrice”:219.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1019002”,“id”:1019002,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“升级款护颈双人记忆枕”,“picUrl”:“http://yanxuan.nosdn.127.net/0118039f7cda342651595d994ed09567.png”,“retailPrice”:199.00,“shareUrl”:“”,“sort”:10,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:6,“brandId”:0,“brief”:“健康保护枕”,“categoryId”:1008008,“counterPrice”:119.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1019006”,“id”:1019006,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“植物填充护颈夜交藤枕”,“picUrl”:“http://yanxuan.nosdn.127.net/60c3707837c97a21715ecc3986a744ce.png”,“retailPrice”:99.00,“shareUrl”:“”,“sort”:7,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:6,“brandId”:0,“brief”:“厚实舒适”,“categoryId”:1008001,“counterPrice”:59.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1021000”,“id”:1021000,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“被”,“name”:“埃及进口长绒棉毛巾”,“picUrl”:“http://yanxuan.nosdn.127.net/7191f2599c7fe44ed4cff7a76e853154.png”,“retailPrice”:39.00,“shareUrl”:“”,“sort”:7,“unit”:“条”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:6,“brandId”:1001020,“brief”:“浪漫毛线绣球,简约而不简单”,“categoryId”:1008009,“counterPrice”:319.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1022000”,“id”:1022000,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“意式毛线绣球四件套”,“picUrl”:“http://yanxuan.nosdn.127.net/5350e35e6f22165f38928f3c2c52ac57.png”,“retailPrice”:299.00,“shareUrl”:“”,“sort”:18,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:32,“brandId”:1001000,“brief”:“柔软纱布,婴童可用”,“categoryId”:1036000,“counterPrice”:269.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1027004”,“id”:1027004,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“色织六层纱布夏凉被”,“picUrl”:“http://yanxuan.nosdn.127.net/6252f53aaf36c072b6678f3d8c635132.png”,“retailPrice”:249.00,“shareUrl”:“”,“sort”:3,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:32,“brandId”:0,“brief”:“原生苦荞,健康护颈”,“categoryId”:1008008,“counterPrice”:119.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1036002”,“id”:1036002,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“高山苦荞麦枕”,“picUrl”:“http://yanxuan.nosdn.127.net/ffd7efe9d5225dff9f36d5110b027caa.png”,“retailPrice”:99.00,“shareUrl”:“”,“sort”:5,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10},{“actualSales”:32,“brandId”:0,“brief”:“5cm记忆绵的亲密包裹”,“categoryId”:1008008,“counterPrice”:619.00,“createTime”:“2018-02-01 00:00:00”,“delFlag”:false,“goodsSn”:“1037011”,“id”:1037011,“isHot”:true,“isNew”:false,“isOnSale”:true,“keywords”:“”,“name”:“安睡慢回弹记忆绵床垫”,“picUrl”:“http://yanxuan.nosdn.127.net/a03ea6f4509439acdafcb7ceba1debe0.png”,“retailPrice”:599.00,“shareUrl”:“”,“sort”:22,“unit”:“件”,“updateTime”:“2018-02-01 00:00:00”,“virtualSales”:10}]}]2022–11–27 21:29:48.336 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug – ==> Preparing: SELECT COUNT(*) AS total FROM shop_goods WHEREdel_flag =0 AND is_on_sale = 1 2022–11–27 21:29:48.387 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug – ==> Parameters: 2022–11–27 21:29:48.426[9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug – <== Total: 1 2022–11–27 21:29:48.430 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug – ==> Preparing: select id, goods_sn, name, pic_url, counter_price, retail_price, actual_sales, virtual_sales from shop_goodsWHERE del_flag = 0and is_on_sale =1 order by create_time desc LIMIT ? 2022–11–27 21:29:48.452[9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug – ==> Parameters: 6(Long) 2022–11–27 21:29:48.476 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug – <== Total: 6 复制代码最后分析上诉日志:通过
86c76336100c414dbe9217aeb099ccd5实现接口调用追踪,通过12用户ID,实现用户调用追踪七. alibaba excel导出时自定义格式转换优雅实现
官网介绍:EasyExcel 是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
EasyExcel 是 alibaba 出的一个基于 java poi 得excel通用处理类库,他的优势在于内存消耗。对比 easypoi 方案,EasyExcel 在内存消耗、知名度(大厂光环)上更出众些。
博主在使用过程中发现导出excel,官网对自定义格式字段提供了 converter 接口,但只简单提供了
CustomStringStringConverter 类代码,达不到博主想要得优雅要求,如下:public class CustomStringStringConverter implements Converter<String> { @Override public Class<?> supportJavaTypeKey() { return String.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } /** * 这里读的时候会调用 * * @paramcontext *@return */ @Override public String convertToJavaData(ReadConverterContext<?> context) { return “自定义:” + context.getReadCellData().getStringValue(); } /** * 这里是写的时候会调用 不用管 * *@return */ @Override publicWriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {return new WriteCellData<>(context.getValue()); } } 复制代码在以上代码中,打个比方想要实现性别字段得自定义格式转换,就需要在convertToExcelData方法中,添加如下代码
@Override publicWriteCellData<?> convertToExcelData(WriteConverterContext<String> context) { Stringvalue = context.getValue(); if (“man”.equals(value)) { return newWriteCellData<>(“男”); } else { return new WriteCellData<>(“女”); } } 复制代码可以看到,非常得不优雅,对于这种类型字段,博主习惯使用枚举类来定义字段所有类型,然后将枚举类转换为 map(value,desc) 结构,就可以优雅得实现这个自定义格式得需求
/** * 一、先定义int字段抽象转换类,实现通用转换逻辑 */ public abstract class AbstractIntConverter implements Converter<Integer> { abstractList<ConverterDTO> getArr();publicWriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { List<ConverterDTO> values = getArr(); Map<Integer,String> map = values.stream().collect(toMap(ConverterDTO::getType, ConverterDTO::getDesc));String result = map.getOrDefault(value, “”); return new WriteCellData<>(result); } static classConverterDTO {private Integer type; private String desc; public Integer getType() { return type; } public void setType(Integer type) { this.type = type; } public String getDesc() { return desc; } public voidsetDesc(String desc) { this.desc = desc; } public ConverterDTO(Integer type, String desc) { this.type = type; this.desc = desc; } } } /** * 二、定义通用状态字段转换类 */ public class StatusConverter extends AbstractIntConverter { @OverrideList<ConverterDTO> getArr() { StatusEnum[] values = StatusEnum.values();return Arrays.stream(values).map(sexEnum -> newConverterDTO(sexEnum.getType(), sexEnum.getDesc())).toList(); }/** * 状态枚举 */ enum StatusEnum { MAN(0, “启用”), WOMAN(1, “禁用”); private Integer type; private String desc; StatusEnum(Integer type, String desc) { this.type = type;this.desc = desc; } public Integer getType() { return type; } public String getDesc() { returndesc; } } } 复制代码最后再导出 ExcelProperty 中添加 StatusConverter ,就优雅得实现了自定义格式得需求
public class User extends BaseEntity { … /** * 用户状态 0 启用 1 禁用 */ @ExcelProperty(value = “用户状态”, converter = StatusConverter.class) private Integer userStatus; … } 复制代码八. Springboot 默认redis客户端lettuce经常连接超时解决方案
不知道大家有没有遇到这种情况,线上项目使用 lettuce 客户端,当操作 redis 得接口一段时间没有调用后(比如30分钟),再次调用 redis 操作后,就会遇到连接超时得问题,导致接口异常。博主直接给出分析过程:
通过wireshark抓包工具,发现项目中 redis 连接创建后,一段时间未传输数据后,客户端发送 psh 包,未收到服务端 ack 包,触发tcp得超时重传机制,在重传次数重试完后,最终客户端主动关闭了连接。连接会做关闭处理),造成了客户端得连接超时
面对这个问题有三种解决方案:
redis操作异常后进行重试,这篇文章有介绍 生产环境Redis连接,长时间无响应被服务器断开问题启用一个心跳定时任务,定时访问 redis,保持 redis 连接不被关闭,简而言之,就是写一个定时任务,定时调用 redis得 get 命令,进而保活 redis 连接基于Springboot 提供得 LettuceClientConfigurationBuilderCustomizer 自定义客户端配置,博主这里主要针对第三种自定义客户端配置来讲解一种优雅得方式Springboot 项目中关于 lettuce 客户端得自动配置是没有启用保活配置得,要启用得话代码如下:
/** * 自定义lettuce客户端配置 * * @returnLettuceClientConfigurationBuilderCustomizer */ @Bean public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() { returnclientConfigurationBuilder -> { LettuceClientConfiguration clientConfiguration = clientConfigurationBuilder.build(); ClientOptions clientOptions = clientConfiguration.getClientOptions().orElseGet(ClientOptions::create); ClientOptions build = clientOptions.mutate().build(); SocketOptions.KeepAliveOptions.Builder builder = build.getSocketOptions().getKeepAlive().mutate();// 保活配置 builder.enable(true); builder.idle(Duration.ofSeconds(30)); SocketOptions.Builder socketOptionsBuilder = clientOptions.getSocketOptions().mutate(); SocketOptions.KeepAliveOptions keepAliveOptions = builder.build(); socketOptionsBuilder.keepAlive(keepAliveOptions); SocketOptions socketOptions = socketOptionsBuilder.build(); ClientOptions clientOptions1 = ClientOptions.builder().socketOptions(socketOptions).build(); clientConfigurationBuilder.clientOptions(clientOptions1); }; } 复制代码添加 lettuce 客户端的自定义配置,在 KeepAliveOptions 中启用 enable ,这样 lettuce 客户端就会在tcp协议规范上启用 keep alive 机制自动发送心跳包
九. redis客户端lettuce启用epoll
直接给 官网连接,配置很简单,添加一个 netty-all 得依赖,lettuce 会自动检测项目系统是否支持 epoll(linux 系统支持),并且是否有
netty-transport-native-epoll依赖( netty-all 包含
netty-transport-native-epoll ),都满足得话就会自动启用 epoll 事件循环,进一步提升系统性能 <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> 复制代码十. Springboot web项目优雅停机
web项目配置了优雅停机后,在重启jar包,或者容器时可以防止正在活动得线程被突然停止( kill -9 无解,请不要使用这个参数杀线上进程,docker compose 项目尽量不要用 docker-compose down 命令关闭项目,使用 docker-compose rm -svf 可以触发优雅停机),造成用户请求失败,在此期间允许完成现有请求但不允许新请求,配置如下:
server: shutdown: “graceful”复制代码十一. nginx配置通用请求后缀
先说下这个配置产生得前提,博主公司pc客户项目是基于 electron 打包得网页项目,每次项目大版本更新时,为了做好兼容性,防止客户端网页缓存等,会使用一个新网页地址,打个比方:
老网页地址,v1.1.0 版本网页访问地址:api.dev.com/pageV110
新网页地址,v1.2.0 版本网页访问地址:api.dev.com/pageV120
那么项目得nginx配置则则需要新加一个 v1.2.0 得配置如下:
server { listen 80; server_name api.dev.com; client_max_body_size 10m; # 老网页v1.1.0配置 location ~ ^/pageV110 { alias /home/wwwroot/api.dev.com/pageV110; index index.html index.htm; } # 新网页v1.2.0配置 location ~ ^/pageV120 { alias /home/wwwroot/api.dev.com/pageV120; index index.html index.htm; } } 复制代码那么博主在每次项目发布得时候就需要配合前端发版,配置一个新网页,故产生了这个通用配置得需求,如下:
server { listen 80; server_name api.dev.com; client_max_body_size 10m; # 配置正则localtion location ~ ^/pageV(.*) { set $s $1; # 定义后缀变量 alias /home/wwwroot/api.dev.com/pageV$s; index index.html index.htm; } } 复制代码在 nginx 配置文件语法中,location 语句可以使用正则表达式,定义 set $s $1 变量,实现了通用配置
附阿里大神总结Java项目快速问题排查思路
线上故障主要会包括 cpu、磁盘、内存以及 网络 问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。
同时例如 jstack 、jmap 等工具也是不囿于一个方面的问题的,基本上出问题就是 df、free、top 三连,然后依次 jstack、jmap 伺候,具体问题具体分析即可。
CPU
一般来讲我们首先会排查cpu方面的问题。cpu异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用jstack来分析对应的堆栈情况。
使用jstack分析cpu问题
我们先用ps命令找到对应进程的 pid(如果你有好几个目标进程,可以先用top看一下哪个占用比较高)。接着用top -H -p pid来找到cpu使用率比较高的一些线程
然后将占用最高的pid转换为16进制printf %x\n pid得到nid
接着直接在jstack中找到相应的堆栈信息jstack pid |grep nid -C5 –color
可以看到我们已经找到了nid为0x42的堆栈信息,接着只要仔细分析一番即可。
令cat jstack.log | grep “java.lang.Thread.State” | sort -nr | uniq -c来对jstack的状态有一个整体的把握,如果WAITING 之类的特别多,那么多半是有问题啦。
频繁gc
当然我们还是会使用jstack来分析问题,但有时候我们可以先确定下gc是不是太频繁,使用jstat -gc pid 1000命令来对gc分代变化情况进行观察,1000表示采样间隔(ms),S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU分别代表两个Survivor区、Eden区、老年代、元数据区的容量和使用量。YGC/YGT、FGC/FGCT、GCT则代表YoungGc、FullGc的耗时和次数以及总耗时。如果看到gc比较频繁,再针对gc方面做进一步分析。
上下文切换
针对频繁上下文问题,我们可以使用vmstat命令来进行查看
cs(context switch)一列则代表了上下文切换的次数。如果我们希望对特定的pid进行监控那么可以使用 pidstat -w pid命令,cswch和nvcswch表示自愿及非自愿切换。
磁盘
磁盘问题和cpu一样是属于比较基础的。首先是磁盘空间方面,我们直接使用df -hl来查看文件系统状态
更多时候,磁盘问题还是性能上的问题。我们可以通过iostatiostat -d -k -x来进行分析
最后一列%util可以看到每块磁盘写入的程度,而rrqpm/s以及wrqm/s分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了。
另外我们还需要知道是哪个进程在进行读写,一般来说开发自己心里有数,或者用iot
不过这边拿到的是tid,我们要转换成pid,可以通过readlink命令来找到pid:readlink -f /proc/*/task/tid/../..。
找到pid之后就可以看这个进程具体的读写情况cat /proc/pid/io
我们还可以通过lsof命令来确定具体的文件读写情况lsof -p pid
内存
内存问题排查起来相对比CPU麻烦一些,场景也比较多。主要包括OOM、GC问题 和 堆外内存。一般来讲,我们会先用free命令先来检查一发内存的各种情况。
堆内内存
内存问题大多还都是堆内内存问题。表象上主要分为OOM和StackOverflow。
OOM
JMV中的内存不足,OOM大致可以分为以下几种:
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
这个意思是没有足够的内存空间给线程分配java栈,基本上还是线程池代码写的有问题,比如说忘记shutdown,所以说应该首先从代码层面来寻找问题,使用jstack或者jmap。
如果一切都正常,JVM方面可以通过指定Xss来减少单个thread stack的大小。
另外也可以在系统层面,可以通过修改
/etc/security/limits.confnofile和nproc来增大os对线程的限制Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
这个意思是堆的内存占用已经达到-Xmx设置的最大值,应该是最常见的OOM错误了。
解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过jstack和jmap去定位问题。如果说一切都正常,才需要通过调整Xmx的值来扩大内存。
Caused by: java.lang.OutOfMemoryError: Meta space
这个意思是元数据区的内存占用已经达到XX:MaxMetaspaceSize设置的最大值,排查思路和上面的一致,参数方面可以通过XX:MaxPermSize来进行调整(这里就不说1.8以前的永久代了)。
Stack Overflow
栈内存溢出,这个大家见到也比较多。
Exception in thread “main” java.lang.StackOverflowError
表示线程栈需要的内存大于Xss值,同样也是先进行排查,参数方面通过Xss来调整,但调整的太大可能又会引起OOM。
使用JMAP定位代码内存泄漏
上述关于OOM和StackOverflow的代码排查方面,我们一般使用JMAPjmap -dump:format=b,file=filename pid来导出dump文件
通过mat(Eclipse Memory Analysis Tools)导入dump文件进行分析,内存泄漏问题一般我们直接选Leak Suspects即可,mat给出了内存泄漏的建议。
另外也可以选择Top Consumers来查看最大对象报告。和线程相关的问题可以选择thread overview进行分析。除此之外就是选择Histogram类概览来自己慢慢分析,大家可以搜搜mat的相关教程。
日常开发中,代码产生内存泄漏是比较常见的事,并且比较隐蔽
ByteBuffer缓存分配不合理等都会造成代码OOM。
另一方面,我们可以在启动参数中指定-XX:+
HeapDumpOnOutOfMemoryError来保存OOM时的dump文件。gc问题和线程
gc问题除了影响cpu也会影响内存,排查思路也是一致的。一般先使用jstat来查看分代变化情况,比如youngGC或者fullGC次数是不是太多呀;EU、OU等指标增长是不是异常呀等。
线程的话太多而且不被及时gc也会引发oom,大部分就是之前说的unable to create new native thread。除了jstack细细分析dump文件外,我们一般先会看下总体线程,通过pstreee -p pid |wc -l。
或者直接通过查看/proc/pid/task的数量即为线程数量。
堆外内存
如果碰到堆外内存溢出,那可真是太不幸了。
首先堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定,如果由于使用Netty导致的,那错误日志里可能会出现OutOfDirectMemoryError错误,如果直接是DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。
堆外内存溢出往往是和NIO的使用相关,一般我们先通过pmap来查看下进程占用的内存情况pmap -x pid | sort -rn -k3 | head -30,这段意思是查看对应pid倒序前30大的内存段。这边可以再一段时间后再跑一次命令看看内存增长情况,或者和正常机器比较可疑的内存段在哪里。
我们如果确定有可疑的内存端,需要通过gdb来分析gdb –batch –pid {pid} -ex “dump memory filename.dump {内存起始地址} {内存起始地址+内存块大小}”
NMT是Java7U40引入的HotSpot新特性,配合jcmd命令我们就可以看到具体内存组成了。
需要在启动参数中加入 -XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail,会有略微性能损耗。
一般对于堆外内存缓慢增长直到爆炸的情况来说,可以先设一个基线jcmd pid VM.native_memory baseline。
然后等放一段时间后再去看看内存增长的情况,通过jcmd pid VM.native_memory detail.diff(summary.diff)做一下summary或者detail级别的diff。
detail级别的话还会有具体内存段的增长情况,如下图。
此外在系统层面,我们还可以使用strace命令来监控内存分配 strace -f -e “brk,mmap,munmap” -p pid这边内存分配信息主要包括了pid和内存地址。
不过其实上面那些操作也很难定位到具体的问题点,关键还是要看错误日志栈,找到可疑的对象,搞清楚它的回收机制,然后去分析对应的对象。
比如DirectByteBuffer分配内存的话,是需要full GC或者手动system.gc来进行回收的(所以最好不要使用-XX:+DisableExplicitGC)。
那么其实我们可以跟踪一下DirectByteBuffer对象的内存情况,通过jmap -histo:live pid手动触发fullGC来看看堆外内存有没有被回收。如果被回收了,那么大概率是堆外内存本身分配的太小了,通过-XX:MaxDirectMemorySize进行调整。如果没有什么变化,那就要使用jmap去分析那些不能被gc的对象,以及和DirectByteBuffer之间的引用关系了。
GC问题
堆内内存泄漏总是和GC异常相伴。不过GC问题不只是和内存问题相关,还有可能引起CPU负载、网络问题等系列并发症,只是相对来说和内存联系紧密些,所以我们在此单独总结一下GC相关问题。
是通过GC日志来排查问题的,在启动参数中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps来开启GC日志。
常见的Young GC、Full GC日志含义在此就不做赘述了。
针对gc日志,我们就能大致推断出youngGC与fullGC是否过于频繁或者耗时过长,从而对症下药。我们下面将对G1垃圾收集器来做分析,这边也建议大家使用G1-XX:+UseG1GC。
youngGC过频繁
youngGC频繁一般是短周期小对象较多,先考虑是不是Eden区/新生代设置的太小了,看能否通过调整-Xmn、-XX:SurvivorRatio等参数设置来解决问题。如果参数正常,但是young gc频率还是太高,就需要使用Jmap和MAT对dump文件进行进一步排查了。
youngGC耗时过长
耗时过长问题就要看GC日较多,那就是起的线程太多了。
触发fullGC
G1中更多的还是mixedGC,但mixedGC可以和youngGC思路一样去排查。触发fullGC了一般都会有问题,G1会退化使用Serial收集器来完成垃圾的清理工作,暂停时长达到秒级别,可以说是半跪了。fullGC的原因可能包括以下这些,以及参数调整方面的一些思路:
并发阶段失败:在并发标记阶段,MixGC之前老年代就被填满了,那么这时候G1就会放弃标记周期。这种情况,可能就需要增加堆大小,或者调整并发标记线程数-XX:ConcGCThreads。晋升失败:在GC的时候没有足够的内存供存活/晋升对象使用,所以触发了Full GC。这时候可以通过-XX:G1ReservePercent来增加预留内存百分比,减少-XX:InitiatingHeapOccupancyPercent来提前启动标记,-XX:ConcGCThreads来增加标记线程数也是可以的。大对象分配失败:大对象找不到合适的region空间进行分配,就会进行fullGC,这种情况下可以增大内存或者增大-XX:G1HeapRegionSize。程序主动执行System.gc():不要随便写就对了。另外,我们可以在启动参数中配置-XX:HeapDumpPath=/xxx/dump.hprof来dump fullGC相关的文件,并通过jinfo来进行gc前后的dump
jinfo -flag +HeapDumpBeforeFullGC pid
jinfo -flag +HeapDumpAfterFullGC pid
题。
网络
涉及到网络层面的问题一般都比较复杂,场景多,定位难,成为了大多数开发的噩梦,应该是最复杂的了。这里会举一些例子,并从tcp层、应用层以及工具的使用等方面进行阐述。
超时
超时错误大部分处在应用层面
读写超时。readTimeout/writeTimeout,有些框架叫做so_timeout或者socketTimeout,均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa的超时指的也是读超时。读写超时一般都只针对客户端设置。连接超时。connectionTimeout,客户端通常指与服务端建立连接的最大时间。服务端这边connectionTimeout就有些五花八门了,jetty中表示空闲连接清理时间,tomcat则表示连接维持的最大时间。其他。包括我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时,以保证连接正常结束。
在实际开发中,我们关心最多的应该是接口的读写超时了。
如何设置合理的接口超时是一个问题。如果接口超时设置的过长,那么有可能会过多地占用服务端的tcp连接。而如果接口设置的过短,那么接口超时就会非常频繁。
服务端接口明明rt降低,但客户端仍然一直超时又是另一个问题。这个问题其实很简单,客户端到服务端的链路包括网络传输、排队以及服务处理等,每一个环节都可能是耗时的原因。
TCP队列溢出
tcp队列溢出是个相对底层的错误,它可能会造成超时、rst等更表层的错误。因此错误也更隐蔽,所以我们单独说一说。
如上图所示,这里有两个队列:syns queuet的ack,如果这时accept queue没满,那就从syns queue拿出暂存的信息放入accept queue中,否则按tcp_abort_on_overflow指示的执行。
tcp_abort_on_overflow 0表示如果三次握手第三步的时候accept queue满了那么server扔掉client发过来的ack。
tcp_abort_on_overflow 1则表示第三步的时候如果全连接队列满了,server发送一个rst包给client,表示废掉这个握手过程和这个连接,意味着日志里可能会有很多connection reset / connection reset by peer。
那么在实际开发中,我们怎么能快速定位到tcp队列溢出呢?
netstat命令,执行netstat -s | egrep “listen|LISTEN”
如上图所示,overflowed表示全连接队列溢出的次数,sockets dropped表示半连接队列溢出的次数。
ss命令,执行ss -lnt
上面看到Send-Q 表示第三列的listen端口上的全连接队列最大为5,第一列Recv-Q为全连接队列当前使用了多少。
接着我们看看怎么设置全连接、半连接队列大小吧:
全连接队列的大小取决于min(backlog, somaxconn)。backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。而半连接队列的大小取决于max(64,
/proc/sys/net/ipv4/tcp_max_syn_backlog)。
在日常开发中,我们往往使用servlet容器作为服务端,所以
RST异常
RST包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手。
在实际开发中,我们往往会看到connection reset / connection reset by peer错误,这种情况就是RST包导致的。
端口不存在
如果像不存在的端口发出建立连接SYN请求,那么服务端发现自己并没有这个端口则会直接返回一个RST报文,用于中断连接。
主动代替FIN终止连接
一般来说,正常的连接关闭都是需要通过FIN报文实现,然而我们也可以用RST报文来代替FIN,表示直接终止连接。实际开发中,可设置SO_LINGER数值来控制,这种往往是故意的,来跳过TIMED_WAIT,提供交互效率,不闲就慎用。
客户端或服务端有一边发生了异常,该方向对端发送RST以告知关闭连接
我们上面讲的tcp队列溢出发送RST包其实也是属于这一种。这种往往是由于某些原因,一方无法再能正常处理请求连接了(比如程序崩了,队列满了),从而告知另一方关闭连接。
接收到的TCP报文不在已知的TCP连接内
比如,一方机器由于网络实在太差TCP报文失踪了,另一方关闭了该连接,然后过了许久收到了之前失踪的TCP报文,但由于对应的TCP连接已不存在,那么会直接发一个RST包以便开启新的连接。
一方长期未收到另一方的确认报文,在一定时间或重传次数后发出RST报文
这种大多也和网络环境相关了,网络环境差可能会导致更多的RST报文。
之前说过RST报文多会导致程序报错,在一个已关闭的连接上读操作会报connection reset,而在一个已关闭的连接上写操作则会报connection reset by peer。通常我们可能还会看到broken pipe错误,这是管道层面的错误,表示对已关闭的管道进行读写,往往是在收到RST,报出connection reset错后继续读写数据报的错,这个在glibc源码注释中也有介绍。
我们在排查故障时候怎么确定有RST包的存在呢?当然是使用tcpdump命令进行抓包,并使用wireshark进行简单分析了。
tcpdump -i en0 tcp -w xxx.cap,en0表示监听的网卡。
接下来我们通过wireshark打开抓到的包,可能就能看到如下图所示,红色的就表示RST包了。
TIME_WAIT和CLOSE_WAIT
TIME_WAIT和CLOSE_WAIT是啥意思相信大家都知道。在线上时,我们可以直接用命令netstat -n | awk /^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}来查看time-wait和close_wait的数量
用ss命令会更快ss -ant | awk {++S[$1]} END {for(a in S) print a, S[a]}
img
time_wait
time_wait的存在一是为了丢失的数据包被后面连接复用,二是为了在2MSL的时间范围内正常关闭连接。它的存在其实会大大减少RST包的出现。
过多的time_wait在短连接频繁的场景比较容易出现。这种情况可以在服务端做一些内核参数调优:
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
当然我们不要忘记在NAT环境下因为时间戳错乱导致数据包被拒绝的坑了,另外的办法就是改小tcp_max_tw_buckets,超过这个数的time_wait都会被干掉,不过这也会导致报time wait bucket table overflow的错。
close_wait
close_wait往往都是因为应用程序写的有问题,没有在ACK后再次发起FIN报文。close_wait出现的概率甚至比time_wait要更高,后果也更严重。往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。
想要定位这类问题,最好是通过jstack来分析线程堆栈来排查问题,具体可参考上述章节。这里仅举一个例子。
开发同学说应用上线后CLOSE_WAIT就一直增多,直到挂掉为止,jstack后找到比较可疑的堆栈是大部分线程都卡在了
countdownlatch.await方法,找开发同学了解后得知使用了多线程但是确没有catch异常,修改后发现异常仅仅是最简单的升级sdk后常出现的class not found。
十二. 关于开发人员的自我提升和突破
博主这里主要总结了四点:
多和他人沟通,沟通能把复杂问题简单化,有时候开发阶段一个需求多问几句,可以减少因为个人理解差异导致的需求不一致问题,进而减少开发时间建立长短期目标,观看技术视频、书籍给自己充电,比如7天利用业余时间看完一本电子书,三十天从零开始一个新项目等善于总结,对于项目中的疑难bug,踩坑点要有记录,防止下次遇到再掉坑里敢于尝试、担责,对项目、代码里明确不合理的地方要敢于跟他人沟通,修改问题代码,达到优化目的。对于自己造成的问题要承担,不要推卸责任。对于线上问题要重视,优先解决线上问题。