艾迪的技术之路

记录博客与成长

Java的web开发需要excel的导入导出工具,所以需要一定的工具类实现,如果是使用easypoi、Hutool导入导出excel,会非常的损耗内存,因此可以尝试使用easyexcel解决大数据量的数据的导入导出,且可以通过Java8的函数式编程解决该问题。

使用easyexcel,虽然不太会出现OOM的问题,但是如果是大数据量的情况下也会有一定量的内存溢出的风险,所以我打算从以下几个方面优化这个问题:

  • 使用Java8的函数式编程实现低代码量的数据导入
  • 使用反射等特性实现单个接口导入任意excel
  • 使用线程池实现大数据量的excel导入
  • 通过泛型实现数据导出

maven导入

1
2
3
4
5
6
<!--EasyExcel相关依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>

使用泛型实现对象的单个Sheet导入

先实现一个类,用来指代导入的特定的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("stu_info")
@ApiModel("学生信息")
//@ExcelIgnoreUnannotated 没有注解的字段都不转换
public class StuInfo {

private static final long serialVersionUID = 1L;

/**
* 姓名
*/
// 设置字体,此处代表使用斜体
// @ContentFontStyle(italic = BooleanEnum.TRUE)
// 设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符
@ColumnWidth(10)
// @ExcelProperty 注解中有三个参数value,index,converter分别代表表名,列序号,数据转换方式
@ApiModelProperty("姓名")
@ExcelProperty(value = "姓名",order = 0)
@ExportHeader(value = "姓名",index = 1)
private String name;

/**
* 年龄
*/
// @ExcelIgnore不将该字段转换成Excel
@ExcelProperty(value = "年龄",order = 1)
@ApiModelProperty("年龄")
@ExportHeader(value = "年龄",index = 2)
private Integer age;

/**
* 身高
*/
//自定义格式-位数
// @NumberFormat("#.##%")
@ExcelProperty(value = "身高",order = 2)
@ApiModelProperty("身高")
@ExportHeader(value = "身高",index = 4)
private Double tall;

/**
* 自我介绍
*/
@ExcelProperty(value = "自我介绍",order = 3)
@ApiModelProperty("自我介绍")
@ExportHeader(value = "自我介绍",index = 3,ignore = true)
private String selfIntroduce;

/**
* 图片信息
*/
@ExcelProperty(value = "图片信息",order = 4)
@ApiModelProperty("图片信息")
@ExportHeader(value = "图片信息",ignore = true)
private Blob picture;

/**
* 性别
*/
@ExcelProperty(value = "性别",order = 5)
@ApiModelProperty("性别")
private Integer gender;

/**
* 入学时间
*/
//自定义格式-时间格式
@DateTimeFormat("yyyy-MM-dd HH:mm:ss:")
@ExcelProperty(value = "入学时间",order = 6)
@ApiModelProperty("入学时间")
private String intake;

/**
* 出生日期
*/
@ExcelProperty(value = "出生日期",order = 7)
@ApiModelProperty("出生日期")
private String birthday;


}

重写ReadListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@Slf4j
public class UploadDataListener<T> implements ReadListener<T> {

/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;

/**
* 缓存的数据
*/
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

/**
* Predicate用于过滤数据
*/
private Predicate<T> predicate;

/**
* 调用持久层批量保存
*/
private Consumer<Collection<T>> consumer;

public UploadDataListener(Predicate<T> predicate, Consumer<Collection<T>> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}

public UploadDataListener(Consumer<Collection<T>> consumer) {
this.consumer = consumer;
}

/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/

/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(T data, AnalysisContext context) {

if (predicate != null && !predicate.test(data)) {
return;
}
cachedDataList.add(data);

// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
try {
// 执行具体消费逻辑
consumer.accept(cachedDataList);

} catch (Exception e) {

log.error("Failed to upload data!data={}", cachedDataList);
throw new BizException("导入失败");
}
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}

/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {

// 这里也要保存数据,确保最后遗留的数据也存储到数据库
if (CollUtil.isNotEmpty(cachedDataList)) {

try {
// 执行具体消费逻辑
consumer.accept(cachedDataList);
log.info("所有数据解析完成!");
} catch (Exception e) {

log.error("Failed to upload data!data={}", cachedDataList);

// 抛出自定义的提示信息
if (e instanceof BizException) {
throw e;
}

throw new BizException("导入失败");
}
}
}
}

Controller层的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ApiOperation("只需要一个readListener,解决全部的问题")
@PostMapping("/update")
@ResponseBody
public R<String> aListener4AllExcel(MultipartFile file) throws IOException {
try {
EasyExcel.read(file.getInputStream(),
StuInfo.class,
new UploadDataListener<StuInfo>(
list -> {
// 校验数据
// ValidationUtils.validate(list);
// dao 保存···
//最好是手写一个,不要使用mybatis-plus的一条条新增的逻辑
service.saveBatch(list);
log.info("从Excel导入数据一共 {} 行 ", list.size());
}))
.sheet()
.doRead();
} catch (IOException e) {

log.error("导入失败", e);
throw new BizException("导入失败");
}
return R.success("SUCCESS");
}

但是这种方式只能实现已存对象的功能实现,如果要新增一种数据的导入,那我们需要怎么做呢?

可以通过读取成Map,根据顺序导入到数据库中。

通过实现单个Sheet中任意一种数据的导入

Controller层的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    @ApiOperation("只需要一个readListener,解决全部的问题")
@PostMapping("/listenMapDara")
@ResponseBody
public R<String> listenMapDara(@ApiParam(value = "表编码", required = true)
@NotBlank(message = "表编码不能为空")
@RequestParam("tableCode") String tableCode,
@ApiParam(value = "上传的文件", required = true)
@NotNull(message = "上传文件不能为空") MultipartFile file) throws IOException {
try {
//根据tableCode获取这张表的字段,可以作为insert与剧中的信息
EasyExcel.read(file.getInputStream(),
new NonClazzOrientedListener(
list -> {
// 校验数据
// ValidationUtils.validate(list);

// dao 保存···
log.info("从Excel导入数据一共 {} 行 ", list.size());
}))
.sheet()
.doRead();
} catch (IOException e) {
log.error("导入失败", e);
throw new BizException("导入失败");
}
return R.success("SUCCESS");
}

重写ReadListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

@Slf4j
public class NonClazzOrientedListener implements ReadListener<Map<Integer, String>> {

/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;

private List<List<Object>> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

private List<Object> rowList = new ArrayList<>();
/**
* Predicate用于过滤数据
*/
private Predicate<Map<Integer, String>> predicate;

/**
* 调用持久层批量保存
*/
private Consumer<List> consumer;

public NonClazzOrientedListener(Predicate<Map<Integer, String>> predicate, Consumer<List> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}

public NonClazzOrientedListener(Consumer<List> consumer) {
this.consumer = consumer;
}

/**
* 添加deviceName标识
*/
private boolean flag = false;

@Override
public void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {
consumer.accept(rowsList);
rowList.clear();
row.forEach((k, v) -> {
log.debug("key is {},value is {}", k, v);
rowList.add(v == null ? "" : v);
});
rowsList.add(rowList);
if (rowsList.size() > BATCH_COUNT) {
log.debug("执行存储程序");
log.info("rowsList is {}", rowsList);
rowsList.clear();
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
consumer.accept(rowsList);
if (CollUtil.isNotEmpty(rowsList)) {
try {
log.debug("执行最后的程序");
log.info("rowsList is {}", rowsList);
} catch (Exception e) {

log.error("Failed to upload data!data={}", rowsList);

// 抛出自定义的提示信息
if (e instanceof BizException) {
throw e;
}

throw new BizException("导入失败");
} finally {
rowsList.clear();
}
}
}

这种方式可以通过把表中的字段顺序存储起来,通过配置数据和字段的位置实现数据的新增,那么如果出现了导出数据模板/手写excel的时候顺序和导入的时候顺序不一样怎么办?

可以通过读取header进行实现,通过表头读取到的字段,和数据库中表的字段进行比对,只取其中存在的数据进行排序添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    /**
* 这里会一行行的返回头
*
* @param headMap
* @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
//该方法必然会在读取数据之前进行
Map<Integer, String> columMap = ConverterUtils.convertToStringMap(headMap, context);
//通过数据交互拿到这个表的表头
// Map<String,String> columnList=dao.xxxx();
Map<String, String> columnList = new HashMap();
columMap.forEach((key, value) -> {
if (columnList.containsKey(value)) {
filterList.add(key);
}
});
//过滤到了只存在表里面的数据,顺序就不用担心了,可以直接把filterList的数据用于排序,可以根据mybatis做一个动态sql进行应用

log.info("解析到一条头数据:{}", JSON.toJSONString(columMap));
// 如果想转成成 Map<Integer,String>
// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
}

那么这些问题都解决了,如果出现大数据量的情况,如果要极大的使用到cpu,该怎么做呢?

可以尝试使用线程池进行实现

使用线程池进行多线程导入大量数据

Java中线程池的开发与使用与原理我可以单独写一篇文章进行讲解,但是在这边为了进行好的开发我先给出一套固定一点的方法。

由于ReadListener不能被注册到IOC容器里面,所以需要在外面开启

详情可见Spring Boot通过EasyExcel异步多线程实现大数据量Excel导入,百万数据30秒

通过泛型实现对象类型的导出

1
2
3
4
5
6
7
8
9
10
11
12

public <T> void commonExport(String fileName, List<T> data, Class<T> clazz, HttpServletResponse response) throws IOException {
if (CollectionUtil.isEmpty(data)) {
data = new ArrayList<>();
}
//设置标题
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream()).head(clazz).sheet("sheet1").doWrite(data);
}

直接使用该方法可以作为公共的数据的导出接口

如果想要动态的下载任意一组数据怎么办呢?可以使用这个方法

1
2
3
4
5
6
7
8
9
10
11
public void exportFreely(String fileName, List<List<Object>> data, List<List<String>> head, HttpServletResponse response) throws IOException {
if (CollectionUtil.isEmpty(data)) {
data = new ArrayList<>();
}
//设置标题
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream()).head(head).sheet("sheet1").doWrite(data);
}

什么?不仅想一个接口展示全部的数据与信息,还要增加筛选条件?这个后期我可以单独写一篇文章解决这个问题。

今天的分享就到这里了。

[TOC]

1 背景

前段时间新入职了一家公司,组长看我已经有点开发年头了,委任给我一个大活,想让我可以监控任务队列,以及线程池的其他参数,如果有修改、监控到异常则需要进行告警,并且支持实施修改线程池参数。

2 明确任务范围

首先并不是为了强行使用动态线程池框架而使用他,使用它的原因是我们的需求就是:

  • 实时监控线程池参数
  • 修改实时监控线程池参数的一些状态
  • 以及进行及时的告警
  • 做到实时修改我们的线程池参数

虽然JDK8可以做到通过方法直接修改线程池参数。但是如果自己手写的话则会浪费很多时间。为了避免重复造轮子,我决定及时使用开源框架。

2.1 技术选型

当然也不是非得使用dynamic-tp不可,首先我们需要看行业中有没有合适的框架或者思路供我们选择,我们发现了这个框架

图18 动态化线程池功能架构

这个美团技术团队对动态线程池该做什么的功能架构,这刚好和我们的需求不谋而合

此时行业中比较好的开源框架有这两个,dynamic-tp和hippo4j,hoppo4j架构是C/S模式,所以需要新增一个hippo4j服务端,那么如果需要一个线上配置环境修改配置的话,那么hippo4j会好一点,我们目前没有这个需求,所以会使用dynamic-tp,直接引用即可,并且通过集成到自己的配置中心就好了(当然看业务需求是怎么样的,毕竟hippo4j在GitHub上的star数量比dynamictp多一些)

2.2 引入dynamic-tp

(具体请看dynamic-tp的官网操作)

  1. 我们是使用的nacos作为配置中心和服务发现中心,所以需要引入对应的nacos包
  2. dynamic-tp只能使用注解强化Java和spring的自带线程池,所以我们只使用配置文件的方式将业务需要使用的线程池放到Dtp注册器里面
  3. 在原有的需要放入线程操作的代码中放一下对应的Dtp注册器里面的线程池进行替换

2.引入时产生的问题

  1. 我们需要自定义告警信息,如果使用自带的wechat提醒或者钉钉提醒,只是通过机器人进行提醒,我们需要使用自己的API

    我们使用了自己的API进行了解决,并且使用Java SPI进行了服务提供

  2. 使用自定义告警信息,涉及到了API,但是自己的API需要从spring的IOC容器里面获取,但是自定义的告警通知是无法拿到的

    一般公司都会有人对获取bean的工具类做封装,所以可以通过这种getBean()方式拿到了对应Bean

  3. 我们想使用自定义的拒绝策略

    使用Java SPI进行了拒绝策略的自定义

  4. 使用dynamic-tp需要考虑到一些公共的修改,比如我们的wechat自定义提醒,自定义拒绝策略,每一次都引入再添加共同的代码文件比较麻烦

    我们进行了二次封装,再二次封装的代码中加入了这些修改,这样其他微服务使用dynamic-tp框架时候可以直接使用我们的二次封装的框架

    我们并没有使用@import等注解,而是在引入后使用ComponentScan注解主动扫描的

2.1 一些遗憾

  1. 使用的自定义告警、拒绝策略,则需要使用Java spi进行服务提供与发现,不能使用spring的自动装配
  2. 只有Java或者spring自带的线程池可以被使用注解强化,用处不大,反而是如果可以使用注解在ThreadPoolExecutor的话则对我们帮助很大

当然,dynamic-tp本身我是很喜欢的,他帮我提前造好了轮子,提升了我们的开发效率。

3.参数怎么跟随业务修改?

线程池参数并不是一成不变的,后续肯定会随着业务的需要进行修改。修改之前需要考虑一下是否有资源没有用到,有时候就是会出现资源没有使用全(如LB没有重定向好)导致的资源紧张。

修改线程池参数一般只会修改核心线程数、最大线程数、任务队列这几个信息。线程池执行情况与任务类型相关性较大,IO 密集型和 CPU 密集型的任务运行起来的情况差异非常大,所以一般我们每个微服务会有两个线程池,一个用于做IO密集型,一个用于做CPU密集型任务,这两个线程池的参数调试肯定是不一样的

具体的方法有这几种:

  1. 根据经验猜

  2. 此处我放一张行业内比较认可的图片

    img

    这几种方式见仁见智

  3. 根据方法二的调参进行修改,同时使用动态线程池实时监控线程池负载,如果线程池逐步正常,则表明修改后的参数可以满足业务需求

参考文章:

背景

我当前是一名普通的 Java 技术开发。人在杭州,目前的行业是安防行业,最近这段时间有想润的想法,有空位的可以加我投递与被投递。喜欢出去玩的同好也可以来找我,我比较喜欢读书,骑行,长跑,羽毛球,探店,其他的爱好我还在持续探索中。

复盘

今年年初还没有学习过目标管理,所以很多目标都没有实现。不过今年学习了,之后肯定可以更好的指定目标与实现目标

跳槽

之前我在杭州某G端产品的企业中工作,22年毕业后一直在那边工作,但是这份工作其实更接近于项目外包,都是在重复重复,对于我而言完全没有什么提升的空间;这让我想到了之前在大学哲学课上老师讲的事情,人的一切发展都是有比较而来的,有近就有远,有宇宙之大就会有原子之小,看到了和我一个学校毕业的人有了很大的成就后,也开始催促我进行成长。因此我在24年4月成功跳槽到当前公司B,期间当然也投递了一些比较知名的厂子。不过因为大厂进不去,也越能证明我的离开的想法是正确的,是应当做的,在面试过程中,多因为我扒股准备的还

行,但是基本上没有很厉害的实战经验。因此在进入新的公司后,有了专业的软件开发流程,我通过积极参与工作,也有学到更多。公司中有了专门的运维团队和测试团队,并且也有同事准备专门的教案进行教课。DBA和ELK专门的同事也有,不像原先的公司,这些岗位要么没有,要么在某一个身兼多职的人身上。

后续只拿了 2 个滨江区的 offer,一个是电商行业的,一个是安防行业的,电商行业的其实是和一个子公司签合同,感觉有点怪,并且人还很少,只有 200 人的一个小公司,安防行业的那个有大概 5k 人,所以选择了入职安防厂子。

2024年基本上也就做了这几个事情:

  • 事件业务进行了重构改版高并发查询,出现了慢查询,修复了慢查询
  • 套餐支持不同类型订阅人使用不同订阅设备
  • 服务新增线程池监控告警,使用开源组件基础上又做了一层封装
  • 新增事件呼叫业务,新增时间业务的高并发场景下的异步操作与回调操作
  • 修复几十个已知的线上bug

整个开发过程不是开发困难,而是需要走标准化的流程,导致需要做很多归档,以至于我的组长都嫌弃我慢吞吞的 😢 。基本上每天都要加班,我粗略算了下,每个月都要至少加班 45h,感觉长久以来不是好事,我的健康都受到了影响,头发都掉了 1/4 了,可能干一年我就走了。

公司里面使用的是内网电脑,且是B端业务,基本上不能有使用AI直接生成满足业务的代码,但是使用AI生成存储过程进行辅助还是可以的。

新公司使用的技术栈个人认为不是很新,并且如果想要改版,得先让运维、测试、自动化脚本部门服从你的安排进行打通,否则只是个人单纯的使用新功能不能让其他人信服。在新公司中学到了一些其他的知识。今年业务是有所长进,但是技术的话基本上没长进,甚至可能还不如去年,我很喜欢的一个 B 站 up原子能讲过,如果一年下来可以记录在简历里面的东西很少,那代表可能需要离开了,不过当前我学到的东西还是有一些的,我打算先苟上一段时间。

尝试副业

只做程序员是不行的,所以我也打算开启自己的副业之旅,不过当前营收额是0,今年上旬打算是通过写博客赚钱,现在只获得了26个粉丝,csdn上面有200个粉丝,距离可以靠写作赚钱还剩不到9800个。我打算是通过做自己喜欢的事情作为副业进行赚钱,我打算继续把这个事情作为我25年的目标。之前在休闲的公司的时候,一个月可以产出4篇,现在不行了,就算有灵感也没有很多时间查找资料进行整理思考产出了,7月的时候参加了活动,拿了个小小奖。有需要这个打折券的可以找我。

nullimg

当然还是有几篇稍微有一点人看,我先截个图在这里。

nullimg

后续我打算不只是做一个只写博客的人,后续我也得通过技术外包等方向发展自己的副业才行。

学习

之前在前东家完全么有什么成长,但是在新公司还是比较培养人的,我也在其他新同事的影响下有了很多个人成长。

自从毕业后,22、23年我也没看过书(技术社科博客会看一点),但是24年我也有学到很多,也看了很多书。今年看了《领域驱动设计》、《优势谈判》、《高效能人士的7个习惯》、《Spring源码深度解析(第二版)》,这些书除了《领域驱动设计》,其他的书对我思想上帮助很大,同时也对于我在实际工作生活中也有很大帮助(《领域驱动设计》有些地方讲的太模糊,对于我的思维构建有点差距,后续我打算看下六边形架构和Cola架构)。我打算后续也多看一些书,帮助自己更好的工作学习生活。纸质书没读多少,但是电子书倒是一本接着一本的看,我现在只要是在地铁上或者是在路上都会看一眼电子书。后续我可能会开一个专栏专门记录一下阅读了哪些书籍,且应用了哪些,取得了什么成果,有什么失败的经历值得回溯。

本来打算今年11月参加高级软考,但是因为工作太忙忘记了,后续我打算学习一下精力管理,保证自己可以在工作之后可以学有余力的进行其他有用的社会活动。

算法做题有做一些,但是没有参加比赛,基本上是拿着大学学到的知识在练习(大学时候主要使用C++写算法,现在却主要在用java写代码),并且主要目的还是为了面试求职。

nullimg

今年我开始参加各种会议,也浅浅的认识了一些开发者,有些人很谦虚,有些人很开放,这些人都是我可以学习的榜样。参加会议和沙龙,今年主题最多的就是AIGC和cursor,也有人真正的把一些AIGC产品给落地了。

为了通勤我买了辆山地自行车,周末也会骑车到钱塘,滨江,萧山这边走走,平常去的多的地方是湘湖,今年的骑行虽然没让我减少很多体重,但是至少让我对于工作地方有了更好的认识与了解,好吃的地方我也变得一目了然了。 自从进了新公司后,我也搬家了,从快到苕溪的余杭那边搬到了滨江浦沿,从1300能租到30平的朝南带阳台的大房间到现在只有15平的1600的小房间,我都基本上没地方做饭了,一些炒菜我也不方便做,没地方施展拳脚,后面我就只买一些鸡肉和虾这类方便一点的食物来凑活,做饭频率虽然降低了,但是做饭厨具现在减少了很多,变相的让我不需要再去维护很多使用频率低的家具了。

娱乐

我的一个爱好是玩点单机游戏,因此今年玩了1、赛博朋克2077:往日之影(DLC)2024年4月27日、2、艾尔登法环:黄金树之影(DLC) 2024年6月23日 3、黑神话悟空 2024年9月15日

目前有点想玩下永劫无间,不知道最近也没有额外的时间可以用于打游戏了。之前从朋友那边收了个PS5,也买了个血源诅咒的光盘,但是也没玩很长时间就放在那边吃灰了。最近血源诅咒还出了 PC 模拟器,羡煞我也,现在 PS5 的用处就只有 25 年游玩道德与法治 6 了。

最近除了玩游戏,其他时候我也会出去旅个游,在朋友圈里面不方便发,我就发在掘金了。每次出去旅游完我都会算一下花了多少钱,果然发现为什么女生都喜欢去旅游了。今年主要是去了黄山,宁波,苏州和杭州的一些地方,有些是自己去的,有些是和家人一起去的,不论怎么样,天大地大,家人最大。

nullimg

总结

我感觉2024年就是一个持续迭代的过程,当然在我这个年龄只要耐*就有无限可以尝试的机会,我还算年轻,还好年轻。

所以我在2025年制定了以下几个目标,方便我做目标管理,最终做到以终为始,有始有终。

  • 学习篇:1、学习dubbo源码、spring boot源码,赋能原有公司的框架开发,做到有落地项目;2、学会C#/nodejs,至少可以做到能进行一些简单界面开发3、继续输出技术文档,可以做到2025年发布12篇4、看书:非开发:《小米创业思考》、《态度改变与社会影响》、《精力管理》、《金字塔原理》、《Personal development for smart people》、《原则》、《学会提问》开发:《quarkus CookBook》、《重构:改善既有代码的设计》、《编程珠玑》、《穷爸爸富爸爸》、《人月神话》、《高性能MySQL》、《架构整洁之道》争取做到一个月可以阅读完一本书,且可以真正的将书中看到的学到的进行实践,记录下来效果。5、挑一个开源项目,或者自己维护一个开源项目(既然学习完源码,不仅能在工作中有效果,也要在副业上有效果)
  • 工作篇:1、考证书,公司内提供了一些行业级别证书,5月参加考试2、软考高级架构师拿下
  • 生活篇:1、跑步300km,每周至少4次运动,明确一个对当前体重合适有效的训练方式。2、去华南旅游一次,去西北旅游一次,去西南旅游一次(当然在我有点钱的情况下)3、入手一辆好一点的公路自行车,准备一下全年的骑行计划。4、多去视频网站上面学习一下英语,具体精确到可以看一些海外技术书籍基本无压力5、多接触其他人,拓宽自己的社交圈子。

Java的web开发需要excel的导入导出工具,所以需要一定的工具类实现,如果是使用easypoi、Hutool导入导出excel,会非常的损耗内存,因此可以尝试使用easyexcel解决大数据量的数据的导入导出,且可以通过Java8的函数式编程解决该问题。

使用easyexcel,虽然不太会出现OOM的问题,但是如果是大数据量的情况下也会有一定量的内存溢出的风险,所以我打算从以下几个方面优化这个问题:

  • 使用Java8的函数式编程实现低代码量的数据导入
  • 使用反射等特性实现单个接口导入任意excel
  • 使用线程池实现大数据量的excel导入
  • 通过泛型实现数据导出

maven导入

1
2
3
4
5
6
<!--EasyExcel相关依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>

使用泛型实现对象的单个Sheet导入

先实现一个类,用来指代导入的特定的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("stu_info")
@ApiModel("学生信息")
//@ExcelIgnoreUnannotated 没有注解的字段都不转换
public class StuInfo {

private static final long serialVersionUID = 1L;

/**
* 姓名
*/
// 设置字体,此处代表使用斜体
// @ContentFontStyle(italic = BooleanEnum.TRUE)
// 设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符
@ColumnWidth(10)
// @ExcelProperty 注解中有三个参数value,index,converter分别代表表名,列序号,数据转换方式
@ApiModelProperty("姓名")
@ExcelProperty(value = "姓名",order = 0)
@ExportHeader(value = "姓名",index = 1)
private String name;

/**
* 年龄
*/
// @ExcelIgnore不将该字段转换成Excel
@ExcelProperty(value = "年龄",order = 1)
@ApiModelProperty("年龄")
@ExportHeader(value = "年龄",index = 2)
private Integer age;

/**
* 身高
*/
//自定义格式-位数
// @NumberFormat("#.##%")
@ExcelProperty(value = "身高",order = 2)
@ApiModelProperty("身高")
@ExportHeader(value = "身高",index = 4)
private Double tall;

/**
* 自我介绍
*/
@ExcelProperty(value = "自我介绍",order = 3)
@ApiModelProperty("自我介绍")
@ExportHeader(value = "自我介绍",index = 3,ignore = true)
private String selfIntroduce;

/**
* 图片信息
*/
@ExcelProperty(value = "图片信息",order = 4)
@ApiModelProperty("图片信息")
@ExportHeader(value = "图片信息",ignore = true)
private Blob picture;

/**
* 性别
*/
@ExcelProperty(value = "性别",order = 5)
@ApiModelProperty("性别")
private Integer gender;

/**
* 入学时间
*/
//自定义格式-时间格式
@DateTimeFormat("yyyy-MM-dd HH:mm:ss:")
@ExcelProperty(value = "入学时间",order = 6)
@ApiModelProperty("入学时间")
private String intake;

/**
* 出生日期
*/
@ExcelProperty(value = "出生日期",order = 7)
@ApiModelProperty("出生日期")
private String birthday;


}

重写ReadListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@Slf4j
public class UploadDataListener<T> implements ReadListener<T> {

/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;

/**
* 缓存的数据
*/
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

/**
* Predicate用于过滤数据
*/
private Predicate<T> predicate;

/**
* 调用持久层批量保存
*/
private Consumer<Collection<T>> consumer;

public UploadDataListener(Predicate<T> predicate, Consumer<Collection<T>> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}

public UploadDataListener(Consumer<Collection<T>> consumer) {
this.consumer = consumer;
}

/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/

/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(T data, AnalysisContext context) {

if (predicate != null && !predicate.test(data)) {
return;
}
cachedDataList.add(data);

// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
try {
// 执行具体消费逻辑
consumer.accept(cachedDataList);

} catch (Exception e) {

log.error("Failed to upload data!data={}", cachedDataList);
throw new BizException("导入失败");
}
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}

/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {

// 这里也要保存数据,确保最后遗留的数据也存储到数据库
if (CollUtil.isNotEmpty(cachedDataList)) {

try {
// 执行具体消费逻辑
consumer.accept(cachedDataList);
log.info("所有数据解析完成!");
} catch (Exception e) {

log.error("Failed to upload data!data={}", cachedDataList);

// 抛出自定义的提示信息
if (e instanceof BizException) {
throw e;
}

throw new BizException("导入失败");
}
}
}
}

Controller层的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ApiOperation("只需要一个readListener,解决全部的问题")
@PostMapping("/update")
@ResponseBody
public R<String> aListener4AllExcel(MultipartFile file) throws IOException {
try {
EasyExcel.read(file.getInputStream(),
StuInfo.class,
new UploadDataListener<StuInfo>(
list -> {
// 校验数据
// ValidationUtils.validate(list);
// dao 保存···
//最好是手写一个,不要使用mybatis-plus的一条条新增的逻辑
service.saveBatch(list);
log.info("从Excel导入数据一共 {} 行 ", list.size());
}))
.sheet()
.doRead();
} catch (IOException e) {

log.error("导入失败", e);
throw new BizException("导入失败");
}
return R.success("SUCCESS");
}

但是这种方式只能实现已存对象的功能实现,如果要新增一种数据的导入,那我们需要怎么做呢?

可以通过读取成Map,根据顺序导入到数据库中。

通过实现单个Sheet中任意一种数据的导入

Controller层的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    @ApiOperation("只需要一个readListener,解决全部的问题")
@PostMapping("/listenMapDara")
@ResponseBody
public R<String> listenMapDara(@ApiParam(value = "表编码", required = true)
@NotBlank(message = "表编码不能为空")
@RequestParam("tableCode") String tableCode,
@ApiParam(value = "上传的文件", required = true)
@NotNull(message = "上传文件不能为空") MultipartFile file) throws IOException {
try {
//根据tableCode获取这张表的字段,可以作为insert与剧中的信息
EasyExcel.read(file.getInputStream(),
new NonClazzOrientedListener(
list -> {
// 校验数据
// ValidationUtils.validate(list);

// dao 保存···
log.info("从Excel导入数据一共 {} 行 ", list.size());
}))
.sheet()
.doRead();
} catch (IOException e) {
log.error("导入失败", e);
throw new BizException("导入失败");
}
return R.success("SUCCESS");
}

重写ReadListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

@Slf4j
public class NonClazzOrientedListener implements ReadListener<Map<Integer, String>> {

/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;

private List<List<Object>> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

private List<Object> rowList = new ArrayList<>();
/**
* Predicate用于过滤数据
*/
private Predicate<Map<Integer, String>> predicate;

/**
* 调用持久层批量保存
*/
private Consumer<List> consumer;

public NonClazzOrientedListener(Predicate<Map<Integer, String>> predicate, Consumer<List> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}

public NonClazzOrientedListener(Consumer<List> consumer) {
this.consumer = consumer;
}

/**
* 添加deviceName标识
*/
private boolean flag = false;

@Override
public void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {
consumer.accept(rowsList);
rowList.clear();
row.forEach((k, v) -> {
log.debug("key is {},value is {}", k, v);
rowList.add(v == null ? "" : v);
});
rowsList.add(rowList);
if (rowsList.size() > BATCH_COUNT) {
log.debug("执行存储程序");
log.info("rowsList is {}", rowsList);
rowsList.clear();
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
consumer.accept(rowsList);
if (CollUtil.isNotEmpty(rowsList)) {
try {
log.debug("执行最后的程序");
log.info("rowsList is {}", rowsList);
} catch (Exception e) {

log.error("Failed to upload data!data={}", rowsList);

// 抛出自定义的提示信息
if (e instanceof BizException) {
throw e;
}

throw new BizException("导入失败");
} finally {
rowsList.clear();
}
}
}

这种方式可以通过把表中的字段顺序存储起来,通过配置数据和字段的位置实现数据的新增,那么如果出现了导出数据模板/手写excel的时候顺序和导入的时候顺序不一样怎么办?

可以通过读取header进行实现,通过表头读取到的字段,和数据库中表的字段进行比对,只取其中存在的数据进行排序添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    /**
* 这里会一行行的返回头
*
* @param headMap
* @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
//该方法必然会在读取数据之前进行
Map<Integer, String> columMap = ConverterUtils.convertToStringMap(headMap, context);
//通过数据交互拿到这个表的表头
// Map<String,String> columnList=dao.xxxx();
Map<String, String> columnList = new HashMap();
columMap.forEach((key, value) -> {
if (columnList.containsKey(value)) {
filterList.add(key);
}
});
//过滤到了只存在表里面的数据,顺序就不用担心了,可以直接把filterList的数据用于排序,可以根据mybatis做一个动态sql进行应用

log.info("解析到一条头数据:{}", JSON.toJSONString(columMap));
// 如果想转成成 Map<Integer,String>
// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
}

那么这些问题都解决了,如果出现大数据量的情况,如果要极大的使用到cpu,该怎么做呢?

可以尝试使用线程池进行实现

使用线程池进行多线程导入大量数据

Java中线程池的开发与使用与原理我可以单独写一篇文章进行讲解,但是在这边为了进行好的开发我先给出一套固定一点的方法。

由于ReadListener不能被注册到IOC容器里面,所以需要在外面开启

详情可见Spring Boot通过EasyExcel异步多线程实现大数据量Excel导入,百万数据30秒

通过泛型实现对象类型的导出

1
2
3
4
5
6
7
8
9
10
11
12

public <T> void commonExport(String fileName, List<T> data, Class<T> clazz, HttpServletResponse response) throws IOException {
if (CollectionUtil.isEmpty(data)) {
data = new ArrayList<>();
}
//设置标题
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream()).head(clazz).sheet("sheet1").doWrite(data);
}

直接使用该方法可以作为公共的数据的导出接口

如果想要动态的下载任意一组数据怎么办呢?可以使用这个方法

1
2
3
4
5
6
7
8
9
10
11
public void exportFreely(String fileName, List<List<Object>> data, List<List<String>> head, HttpServletResponse response) throws IOException {
if (CollectionUtil.isEmpty(data)) {
data = new ArrayList<>();
}
//设置标题
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream()).head(head).sheet("sheet1").doWrite(data);
}

什么?不仅想一个接口展示全部的数据与信息,还要增加筛选条件?这个后期我可以单独写一篇文章解决这个问题。

今天的分享就到这里了。

一、背景

在测试环境海外白牌eu环境中,cos服务一直不健康;原因是pod.yml中没有配置liveness,导致服务没有被容器重启;

随后查看日志发现服务触发了OOM异常。

img

二、原因

使用jmap命令(jmap -dump:format=b,file=heapdump.hprof 1)打印出服务的内存dump文件;

使用MemoryAnalyzer分析dump文件;

img

发现大对象是ShardingSphereDataSource,查看这个对象的外引用发现是数据库表实例;

img

img

img

最终发现table元数据存在47256个结点;查看node中的value发现数据是表名,但是环境内的表最多存在2567+12830=5632张表,很明显47256是异常的;

查看数据库表中对应的cos_video库,发现其中存在50000+的表(没截图了),应该是环境内每日清理过期表的任务执行失败或者没有执行导致的;

删除cos_video库中过期的表问题解决。

Linux下火焰图收集

java进程

可以选择使用arthas的jprofile命令

linux

使用perf

以centos为例,安装yum install perf

使用perf record -F 99 -g -p 1 — sleep 30采集30秒进程1,每秒采集99次

perf script -i perf.data > perf.script 将火焰图数据转化成解析文件

操作进程中下载FlameGraph,git clone https://github.com/brendangregg/FlameGraph.git;

进入FlameGraph文件夹后,执行./stackcollapse-perf.pl /var/mount/eu/tcpdump_file/perf.script > perf.folded折叠堆栈信息

使用./flamegraph.pl perf.folded > flamegraph.svg生成火焰图

svg文件在浏览器中打开可以查看CPU开销

image-20250712122131284

一、背景

国内宇视p2p-relay服务CPU利用率一直很高,造成服务被liveness杀死,或者内置的P2P控件抢占不到CPU而崩溃。

img

P2P奔溃堆栈

1
2
3
4
5
6
7
2025-03-14 16:27:38.070 [34mINFO[0;39m [Thread-53990888] [CLoggerImpl.pFnLogFun: 45] [] - [p2p control] level: 3, message: [7f16197f0700][libcloud.c:7145]StartStream timeout. [50396A31-6769-5A37-5659-626251566B6A]
2025-03-14 16:27:38.071 [34mINFO[0;39m [Thread-53990889] [CLoggerImpl.pFnLogFun: 45] [] - [p2p control] level: 3, message: [7f16197f0700][libcloud.c:7573][ConnectPeerDone] iTaskType(2), iIsSUccess(0), pcPeerID(50396A31-6769-5A37-5659-626251566B6A), pcPeerIP(222.222.30.55), iPeerPort(29433).2025-03-14 16:27:38.078 [34mINFO[0;39m [http-nio-8998-exec-515] [CLoggerImpl.pFnLogFun: 45] [] - [p2p control] level: 3, message: [7f15f4dc4700][t2u_runner.c:265]Failed to call epoll wait, ret[0], errno[2]2025-03-14 16:27:38.079 [34mINFO[0;39m [http-nio-8998-exec-515] [CLoggerImpl.pFnLogFun: 45] [] - [p2p control] level: 6, message: [7f15f4dc4700][libcloud.c:5662]MTU probe.## A fatal error has been detected by the Java Runtime Environment:## SIGSEGV (0xb) at pc=0x00007f161e0757f3, pid=1, tid=0x00007f15f4dc4700## JRE version: Java(TM) SE Runtime Environment (8.0_231-b11) (build 1.8.0_231-b11)# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.231-b11 mixed mode linux-amd64 compressed oops)# Problematic frame:# C [libtunnel_new.so+0x607f3] Libcloud_CreateT2URelation+0x313## Core dump written. Default location: //core or core.1## An error report file with more information is saved as:# //hs_err_pid1.log## If you would like to submit a bug report, please visit:# http://bugreport.java.com/bugreport/crash.jsp# The crash happened outside the Java Virtual Machine in native code.# See problematic frame for where to report the bug.#
[error occurred during error reporting , id 0xb]
[error occurred during error reporting , id 0xb]
[error occurred during error reporting , id 0xb]
[error occurred during error reporting , id 0xb]
[error occurred during error reporting , id 0xb]

二、分析

  1. CPU高主要是pid为1的java进程占用的,也就是p2p-relay的服务

  2. 使用top -H,查看线程号为8/9/10/11的线程CPU使用率的特别高

    img

  3. jstack 1查看线程8/9/10/11对应着四个ParalLelGC并行垃圾回收器(CPU 4核会存在4个垃圾回收器线程);
    垃圾回收器线程为什么会CPU高,说明有大量的对象在被创建/没有引用/被回收持续往复

    img

  4. 使用arthas的dashboard命令查看,young gc出现了289W次,持续了8小时(进程启动了25小时),再次确认有大量的对象在被创建

    img

  5. 使用jstat -gc 1000 | awk ‘{print $6}’ 观察 Eden 分配速率,发现每隔几秒采集就有大量Eden对象创建(单位是kb);也就是说时时刻刻有2.27G的对象产生
    大量对象产生说明业务中存在很大的并发的业务;结合p2p-relay流程,怀疑是接口/pr/device/p2p/invoke接口调用的频发;
    但是TOMCAT的http-nio线程并不多,说明请求量并没有很大

    img

  6. 重新梳理流程后,怀疑可能是P2P日志回调导致的问题

    img

    P2P日志需要java进程给P2P配置回调接口,P2P将日志回调给java进行打印

  7. 线上配置的P2P日志level是0,一般不会输出到日志中;
    但是回看这个业务,如果回调频繁,岂不是要创建大量的String对象(JNA 将原生代码(如 C/C++的 char*)传递的字符串转换为 Java 的 String);为了确认日志回调的量,将level设置为8,在日志采集服务中发现P2P产生的日志是其他所有服务加起来的10倍+
    那问题基本确认就是这个造成的,将设置回调的函数注释掉,并重新部署

三、修改效果

修改后效果明显,CPU利用率下降到20%

b04caed56a1b9bf1e2cdc1d0555f1043

四、优化方向

建议P2P库优化方向:

P2P的日志level控制由P2P侧实现,云端下发日志的level;P2P库整理下日志,尽量减少日志,精简下。

Flyway是什么?

Flyway是一款开源的数据库版本管理工具,可以实现管理并跟踪数据库变更,支持数据库版本自动升级,而且不需要复杂的配置,能够帮助团队更加方便、合理的管理数据库变更。
例:创建两个sql变更文件,项目启动后会将两个文件中的sql语句全部执行。

1、痛点

  1. 每次封板都要执行一次数据库,很久没更新的环境中,数据库都需要做到到处找sql文件进行执行,很麻烦

2、优点

Flyway可以在微服务上线时自动执行SQL脚本,不需要再执行工单

3、为什么不用Flyway

3.1、对于开发

  1. 有些微服务提早上线,但是也会同时更新在主干分支中,所以用一个修改会存在于多个版本中,上线到同一个环境中时会出问题
  2. 分库分表虽然支持Flyway,但是复杂度高,容易出问题,开发与拓展均较难完全验证
  3. 每个涉及到数据库的微服务都要配置一组Flyway的参数,对于运维也不友好
  4. 微服务拆分与合并,flyway则需要额外投入人力进行开发

3.2、对于运维

  1. Flyway本质上是使用微服务自己的资源执行SQL语句,在常规场景下对微服务和MySQL的资源消耗可控,但在大规模数据迁移或复杂DDL操作时需重点优化,这在使用DMS的时候,只需要运维保证不影响线上环境的MySQL即可
  2. 虽然Flyway也有版本工具,如果线上出问题,DMS可以直接提工单进行执行,但是Flyway则需要重新配置执行规则,走上线流程,比较消耗人力
  3. 如果是pod集群使用,则需要额外配置Kubernetes Job,这才能保证多个pod启动时避免出问题

4、原有痛点解决方式

删除对应环境对应数据库的表,重新使用ddl进行创建

推荐阅读

  1. Flyway详解(使用说明及避坑指南、一文搞懂flyway)
  2. 数据库/SQL 版本管理工具选型指北:Flyway、Liquibase、Bytebase、阿里 DMS
  3. flyway官网
  4. Flyway 常见问题与解决方案
  5. 【SpringBoot系列】微服务集成Flyway

[TOC]

一般写Java服务端的基本上都使用spring框架,使用spring项目则代表一般会用slf4j作为打印日志的配置标准。很多时候不是在开发中直接打印日志就好了,日志还兼具着调试,线上排查问题等功能。

且slf4j有以下的一个日志等级

1
debug(调试 ) < info(消息) < warn(警告) < error(错误) < fatal(严重错误)

背景为:在开发过程中,组长认为我写的日志不是很好

目的为:期望我可以使用比较好的开发习惯,在开发过程中,期望我使用最少的精力写出方便开发者做调试,以及线上进行查看的日志。

因此我打算使用以下配置作为开发者的一个习惯配置做培养

1、logback.xml配置

这个文件用于存储具体日志打印的配置。当然也有第三方日志打印/暴露endpoint以别的配置文件打印到别的目录下。基本上不会修改这个文件中的配置,除非出现问题,要求修改第三方包的日志的等级等情况才会进行修改。

2、是否该打印日志

打印日志主要是为了一下几种原因:

  1. 调试:

    开发过程中需要实时看到参数信息、业务流程分支等信息,在spring中,一些异步调用,生命周期、事件、队列消费等信息不会主动触发到,顾需要日志来显式展示配置信息、或者定位信息

    开发过程中遇到用户出现了业务上的失败流程,但是不会影响整个业务,不会影响进程安全的信息,也会需要打印出来,用于辅助调试

  2. 排查问题:

    业务上出现了问题,则会要求打印重要参数信息,用于查看配置

  3. 数据处理

    如果项目上了新特性,则需要打印对应信息,用于查看用户使用UV、PV,业务路径等信息,这个就是买点上报+数据处理上的流程。

3、打印日志的级别等级

打印过程中需要日志足够简洁足够重要。

打印过程中如要打印比较重要的参数,如数据库影响行数,入参等信息

以下是几个比较重要的打印等级的信息

  1. debug:

    一般会打印一些调试参数,方便开发者查看业务会走到哪里,在线上一般不会打开这个等级的日志打印。

    一些异步调用,生命周期、事件、队列消费等信息不会主动触发,所以也需要debug等级日志打印。

    arthas和debug等级日志很大程度上功能重合了,所以为了避免日志冗余,也可以通过使用arthas调试代码。

  2. info:

    如果是埋点上报信息,则会打印用户id,用户后续的业务点击点等信息。

  3. warn:

    如果用户在走业务流程时候出现了错误,但是不影响后续的业务流程,则打印这个等级的日志

  4. error:

    用户在业务流程中出现了严重技术或者业务错误,导致无法继续进行业务,则需要打印这个等级的日志。

4、怎么依据日志做调试排查

0、禁忌:

  1. 不要把list直接打印出来,尽量只打印,对外内存容易出现OOM、且虽然是异步打印日志,但是打印大量日志会导致不必要的消耗资源。
  2. 很多小伙伴可能用docker、k8s、ELK进行日志收集,所以为了兼容性,只能打印英语日志,打印中文日志很容易会导致乱码
  3. 打印日志要保证安全性不能名,不能明文打印敏感信息
  4. 不要打印exception对象,要打印e.getMessage()
  5. 遇到异常,打印log.error()后就不要在抛出异常了,再抛出异常可能会导致多余日志的打印

推荐阅读:

  1. 别在 Java 代码里乱打日志了,项目中这样打印日志才足够优雅!

  1. 提交合入请求后,仍然需要查看git上面的差异报告,总有一些想不到的地方是有问题的
  2. 代码,配置文件,sql需要保证对应的上此次修改,因为要交付给运维使用
  3. 尽量使用原有代码逻辑,如单个操作接口转批量操作接口,为避免过度优化,建议调用单个操作接口对应的API实现批量操作接口
  4. sql语句会出现多次修改,需要更新最后修改到何如文件上面;如以下例子,新增一个叫做taskId的字段,并且增加了taskId的索引,但是因为taskId不符合一般的字段明明,所以会将字段改为task_id,但是如果忘记修改新的索引,会导致上线时SQL语句报错
  5. 因为是持续交付项目,所以为了保证代码可以持续性,如果要修改某个配置文件的名称,如果像是修改redis-time直接修改为alarm-redis-time,那么旧微服务在更新为新版本微服务时,会出现一段时间的该字段是小问题;正确做法应当是新增一个alarm-redis-time属性供新版本微服务在未来使用,redis-time则备注在下一个迭代删除
  6. 配置文件尽量不新增,减少运维工作量
  7. 日志尽量少打,因为大量日志会导致检索服务花费大,且导致过度消耗CPU;打印日志时,需要将入参,当前业务涉及对象进行打印,避免线上出问题导致无法排查
  8. 进行业务开发,需要将大的业务步骤之间进用空格分隔,并且使用备注表明逻辑;整个的逻辑链路需要做到口述出来
0%