开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot实现Excel导入导出,好用到爆,POI

发表于 2021-10-26

在我们平时工作中经常会遇到要操作Excel的功能,比如导出个用户信息或者订单信息的Excel报表。你肯定听说过POI这个东西,可以实现。但是POI实现的API确实很麻烦,它需要写那种逐行解析的代码(类似Xml解析)。今天给大家推荐一款非常好用的Excel导入导出工具EasyPoi,希望对大家有所帮助!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

EasyPoi简介

用惯了SpringBoot的朋友估计会想到,有没有什么办法可以直接定义好需要导出的数据对象,然后添加几个注解,直接自动实现Excel导入导出功能?

EasyPoi正是这么一款工具,如果你不太熟悉POI,想简单地实现Excel操作,用它就对了!

EasyPoi的目标不是替代POI,而是让一个不懂导入导出的人也能快速使用POI完成Excel的各种操作,而不是看很多API才可以完成这样的工作。

集成

在SpringBoot中集成EasyPoi非常简单,只需添加如下一个依赖即可,真正的开箱即用!

1
2
3
4
5
xml复制代码<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>

使用

接下来介绍下EasyPoi的使用,以会员信息和订单信息的导入导出为例,分别实现下简单的单表导出和具有关联信息的复杂导出。

简单导出

我们以会员信息列表导出为例,使用EasyPoi来实现下导出功能,看看是不是够简单!

  • 首先创建一个会员对象Member,封装会员信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 购物会员
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
@Excel(name = "ID", width = 10)
private Long id;
@Excel(name = "用户名", width = 20, needMerge = true)
private String username;
private String password;
@Excel(name = "昵称", width = 20, needMerge = true)
private String nickname;
@Excel(name = "出生日期", width = 20, format = "yyyy-MM-dd")
private Date birthday;
@Excel(name = "手机号", width = 20, needMerge = true, desensitizationRule = "3_4")
private String phone;
private String icon;
@Excel(name = "性别", width = 10, replace = {"男_0", "女_1"})
private Integer gender;
}
  • 在此我们就可以看到EasyPoi的核心注解@Excel,通过在对象上添加@Excel注解,可以将对象信息直接导出到Excel中去,下面对注解中的属性做个介绍;
    • name:Excel中的列名;
    • width:指定列的宽度;
    • needMerge:是否需要纵向合并单元格;
    • format:当属性为时间类型时,设置时间的导出导出格式;
    • desensitizationRule:数据脱敏处理,3_4表示只显示字符串的前3位和后4位,其他为*号;
    • replace:对属性进行替换;
    • suffix:对数据添加后缀。
  • 接下来我们在Controller中添加一个接口,用于导出会员列表到Excel,具体代码如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation(value = "导出会员列表Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
ExportParams params = new ExportParams("会员列表", "会员列表", ExcelType.XSSF);
map.put(NormalExcelConstants.DATA_LIST, memberList);
map.put(NormalExcelConstants.CLASS, Member.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "memberList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
}
  • LocalJsonUtil工具类,可以直接从resources目录下获取JSON数据并转化为对象,例如此处使用的members.json;

  • 运行项目,直接通过Swagger访问接口,注意在Swagger中访问接口无法直接下载,需要点击返回结果中的下载按钮才行,访问地址:http://localhost:8088/swagger-ui/

  • 下载完成后,查看下文件,一个标准的Excel文件已经被导出了。

简单导入

导入功能实现起来也非常简单,下面以会员信息列表的导入为例。

  • 在Controller中添加会员信息导入的接口,这里需要注意的是使用@RequestPart注解修饰文件上传参数,否则在Swagger中就没法显示上传按钮了;
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
java复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation("从Excel导入会员列表")
@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
@ResponseBody
public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
ImportParams params = new ImportParams();
params.setTitleRows(1);
params.setHeadRows(1);
try {
List<Member> list = ExcelImportUtil.importExcel(
file.getInputStream(),
Member.class, params);
return CommonResult.success(list);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("导入失败!");
}
}
}
  • 然后在Swagger中测试接口,选择之前导出的Excel文件即可,导入成功后会返回解析到的数据。

复杂导出

当然EasyPoi也可以实现更加复杂的Excel操作,比如导出一个嵌套了会员信息和商品信息的订单列表,下面我们来实现下!

  • 首先添加商品对象Product,用于封装商品信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 商品
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
@Excel(name = "ID", width = 10)
private Long id;
@Excel(name = "商品SN", width = 20)
private String productSn;
@Excel(name = "商品名称", width = 20)
private String name;
@Excel(name = "商品副标题", width = 30)
private String subTitle;
@Excel(name = "品牌名称", width = 20)
private String brandName;
@Excel(name = "商品价格", width = 10)
private BigDecimal price;
@Excel(name = "购买数量", width = 10, suffix = "件")
private Integer count;
}
  • 然后添加订单对象Order,订单和会员是一对一关系,使用 @ExcelEntity注解表示,订单和商品是一对多关系,使用@ExcelCollection注解表示,Order就是我们需要导出的嵌套订单数据;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 订单
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
@Excel(name = "ID", width = 10,needMerge = true)
private Long id;
@Excel(name = "订单号", width = 20,needMerge = true)
private String orderSn;
@Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss",needMerge = true)
private Date createTime;
@Excel(name = "收货地址", width = 20,needMerge = true )
private String receiverAddress;
@ExcelEntity(name = "会员信息")
private Member member;
@ExcelCollection(name = "商品列表")
private List<Product> productList;
}
  • 接下来在Controller中添加导出订单列表的接口,由于有些会员信息我们不需要导出,可以调用ExportParams中的setExclusions方法排除掉;
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
java复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation(value = "导出订单列表Excel")
@RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)
public void exportOrderList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Order> orderList = getOrderList();
ExportParams params = new ExportParams("订单列表", "订单列表", ExcelType.XSSF);
//导出时排除一些字段
params.setExclusions(new String[]{"ID", "出生日期", "性别"});
map.put(NormalExcelConstants.DATA_LIST, orderList);
map.put(NormalExcelConstants.CLASS, Order.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "orderList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
}
  • 在Swagger中访问接口测试,导出订单列表对应Excel;

  • 下载完成后,查看下文件,EasyPoi导出复杂的Excel也是很简单的!

自定义处理

如果你想对导出字段进行一些自定义处理,EasyPoi也是支持的,比如在会员信息中,如果用户没有设置昵称,我们添加下暂未设置信息。

  • 我们需要添加一个处理器继承默认的ExcelDataHandlerDefaultImpl类,然后在exportHandler方法中实现自定义处理逻辑;
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
java复制代码/**
* 自定义字段处理
* Created by macro on 2021/10/13.
*/
public class MemberExcelDataHandler extends ExcelDataHandlerDefaultImpl<Member> {

@Override
public Object exportHandler(Member obj, String name, Object value) {
if("昵称".equals(name)){
String emptyValue = "暂未设置";
if(value==null){
return super.exportHandler(obj,name,emptyValue);
}
if(value instanceof String&&StrUtil.isBlank((String) value)){
return super.exportHandler(obj,name,emptyValue);
}
}
return super.exportHandler(obj, name, value);
}

@Override
public Object importHandler(Member obj, String name, Object value) {
return super.importHandler(obj, name, value);
}
}
  • 然后修改Controller中的接口,调用MemberExcelDataHandler处理器的setNeedHandlerFields设置需要自定义处理的字段,并调用ExportParams的setDataHandler设置自定义处理器;
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
java复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation(value = "导出会员列表Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
ExportParams params = new ExportParams("会员列表", "会员列表", ExcelType.XSSF);
//对导出结果进行自定义处理
MemberExcelDataHandler handler = new MemberExcelDataHandler();
handler.setNeedHandlerFields(new String[]{"昵称"});
params.setDataHandler(handler);
map.put(NormalExcelConstants.DATA_LIST, memberList);
map.put(NormalExcelConstants.CLASS, Member.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "memberList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
}
  • 再次调用导出接口,我们可以发现昵称已经添加默认设置了。

总结

体验了一波EasyPoi,它使用注解来操作Excel的方式确实非常好用。如果你想生成更为复杂的Excel的话,可以考虑下它的模板功能。

参考资料

项目官网:gitee.com/lemur/easyp…

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深夜接到投诉电话,说我做的规则引擎失效了 rabbitm

发表于 2021-10-26

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

  • 之前我们有提到如何保证rabbitmq消息不丢失。分别从三个角度解析了。分别是发送方、rabbitmq、消费方。
  • 当时有关消费方只是简单带过了介绍。今天我们从一个使用场景来分析下消费者确认消费带来的坑

发送消息

  • 这里我们还是继续沿用之前的发送逻辑。
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
ini复制代码public Map<String, Object> sendMessage(Map<String, Object> params) throws UnsupportedEncodingException {
   Map<String, Object> resultMap = new HashMap<String, Object>(){
      {
           put("code", 200);
      }
  };
   String msg = "";
   Integer index = 0;
   if (params.containsKey("msg")) {
       msg = params.get("msg").toString();
  }
   if (params.containsKey("index")) {
       index = Integer.valueOf(params.get("index").toString());
  }
   if (index != 0) {
       //这里开始模拟异常出现。消息将会丢失
       int i = 1 / 0;
  }
   Map<String, Object> map = new HashMap<>();
   map.put("msg", msg);
   Message message= MessageBuilder.withBody(JSON.toJSONString(map).getBytes("UTF-8")).setContentType(MessageProperties.CONTENT_TYPE_JSON)
          .build();
   CorrelationData data = new CorrelationData(UUID.randomUUID().toString());
   rabbitTemplate.convertAndSend(RabbitConfig.TOPICEXCHANGE, "zxh", message,data);
   return resultMap;
}
  • 首先我们在发送里面还是会保留异常情况,这是为了之前测试发送消息确认的操作。笔者这里偷个懒就没有删除 。 本次我们在调用接口会始终保证消息投递的准确性。因为我们的重点是消费者
  • 因为rabbitmq有三种确认机制acknowledge-mode ; 分别是manual、auto、none; manual就是需要我们手动确认,auto标识自动确认消息,none就是不作为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码@RabbitListener(queues = RabbitConfig.QUEUEFIRST)
@Async("asyncExecutor")
public void handler(Message msg, Channel channel) {
   //channel.basicReject(msg.getMessageProperties().getDeliveryTag(), true);
   byte[] body = msg.getBody();
   String messages = new String(body);
   JSONObject json = (JSONObject) JSONObject.parse(messages);
   if ("1".equals(json.getString("msg"))) {
       try {
           channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
      } catch (IOException e) {
           e.printStackTrace();
      }
  }
   if ("2".equals(json.getString("msg"))) {
       throw new RuntimeException("异常。。。。。");
  }
   log.info(RabbitConfig.QUEUEFIRST+"队列中消费的信息:"+msg);
}
  • 在接受消息上我们根据发送过来的消息做了处理,当收到消息体为1的我们会进行消息确认,如何消息体是2则会抛出异常也就是不进行消息确认。只要我们消息不确认那么rabbitmq就会保存消息并尝试再次发送给消费者。

场景描述

  • 深夜突然接到电话说线上数据不同了。上面我们说的是mq的消费问题,聪明的读者肯定知道这个问题肯定是mq的消费问题。而对于我来说一开始很闷逼的。项目上线已经三天了,为什么偏偏是这个时候功能不正常了呢?
  • 于是我打开项目开始线上定位,首先看了下日志一看才发现项目在处理mq的那段逻辑在疯狂的报错。心中开始窃喜这么容易就找到问题所在了。但是随着问题的深入发现报错并不是导致线上故障的根本原因。因为线上的现象是数据无法同步。而同步的关键是监听mq的消息从而实现同步。但是现在的问题是通过日志看根本就无法接受到消息,而且进入mq后台页面看到相关的队列全部堵塞在那里了。
  • 这里我们稍微总结下:
+ mq处理逻辑疯狂报错
+ mq无法接受其它数据

问题剖析

  • 因为这是上线三天后造成的,所以我很肯定是业务逻辑是正常的,否则根本就无法通过测试。那么我该考虑的是为什么队列对堵塞呢?而本次上线的确对mq做了稍微的改动,就是增加了消息的手动确认。
1
2
arduino复制代码Unexpected exception occurred invoking async method: public void xxxxxxxxxxxxxxxx(org.springframework.amqp.core.Message,com.rabbitmq.client.Channel)
java.lang.IllegalStateException: Channel closed; cannot ack/nack
  • 这是线上报错之一,另外一个报错是业务里的报错和我们无关。通过上面的报错信息我们能够摘取到以下
+ async exception
+ channel cloesed , cannot ack/nack
  • 一个是异步处理中出现异常,导致async exception ; 因为异步处理出错当到达确认时候就会出现not ack
  • 这里贴出一个博主针对not ack的解决方案,针对我们本次场景并不适用
  • 我们在发送msg=2的时候就会发生异常。这个其实还是很好处理的,将mq接受消息的地方做个兼容处理也就是全局捕获异常。
  • 到了这里我们只是在处理上面第一个问题—-mq处理疯狂报错;很明显这并没有解决我们的根本问题–为什么mq消息发生拥堵

mq批处理设置

消费者在开启acknowledge的情况下,对接收到的消息可以根据业务的需要异步对消息进行确认。

然而在实际使用过程中,由于消费者自身处理能力有限,从rabbitmq获取一定数量的消息后,希望rabbitmq不再将队列中的消息推送过来,当对消息处理完后(即对消息进行了ack,并且有能力处理更多的消息)再接收来自队列的消息。在这种场景下,我们可以通过设置basic.qos信令中的prefetch_count来达到这种效果

image-20211025173519681.png

  • 我们可以看到mq的默认prefetch_count是250 。 这个设置也是我们本次mq拥堵的根本原因。
  • 也就是说在我们遇到一次报错导致没有有效消息确认时,我们消费者和mq之间channel就会占用一条消息。知道250条数据都被无法确认的消息沾满后,正常的数据也就不会在下发到该消费者了。因为prefetch_count是消费者的上限。
  • 这就造成了死循环了。250条我们无法确认的消息无法消费,我们就无法获取新的消息。
  • 这也就是了为什么系统刚上线的时候没有问题,因为刚开始我们可以接受消息没有确认并不影响我们处理正确数据只是效率慢了一点。随着时间的推移慢慢的错误数据越来越多导致我们最终拥堵。

解决办法

  • 处理掉异常业务,并且捕获异常保证消息确认
  • 消息确认最好使用自动确认或者将消息确认放在前面。

总结

  • 当rabbitmq要将队列中的一条消息投递给消费者时,会遍历该队列上的消费者列表,选一个合适的消费者,然后将消息投递出去。其中挑选消费者的一个依据就是看消费者对应的channel上未ack的消息数是否达到设置的prefetch_count个数,如果未ack的消息数达到了prefetch_count的个数,则不符合要求。当挑选到合适的消费者后,中断后续的遍历

欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

linux极简小知识:36、linux中最有用的一个命令ma

发表于 2021-10-26

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

***参与评论可以领取掘金周边大奖,走过路过不要错过!!!由掘金官方提供,写下你想说的话😅

man命令介绍

查看一个命令的用法

man是manual的缩写。它是一个帮助命令,用于查看一个“命令”的帮助文档,即,使用man查看一个命令怎么用,查看一个命令的用法。

语法是:man command。比如查看ls命令的用法,man ls。

查看时,通过向下的箭头滚动下一行;按空格键会滚动到下一页;按字母q退出man的查看。

一个方便记忆的俗语,“有问题找男人(man)帮忙”

man查看man

man本身自己也是一个命令,所以可以自己查看自己,即:man查看man。

如下,查看man:man man

man帮助命令分类

帮助页中,向下滚动可以查看一共有多少篇章(多少部分)

man一共有9个篇章。man命令进行了分类,方便查看。比如1,用于显示可以执行程序或shell命令的帮助信息;2,用于显示系统调用的帮助信息…

如果想查看一个shell命令,比如ls命令、cd命令的帮助文档,可以使用man 1 ls、man 1 cd。

man对一个命令手册页的查找是从1开始的,默认依次查找每个部分,直到找到并显示。

使用篇章号的原因在于,有的命令、系统调用、库调用、文件等,可能存在重名的问题,这个时候就要加上数字,指定查看哪个命令。

比如 passwd 既可以表示设置密码的shell命令,又可以表示 /etc/passwd 文件。

查看 passwd 命令,可以使用 man passwd 或 man 1 passwd。

查看 /etc/passwd 文件,可以使用 man 5 passwd。

帮助命令的分类(section,区域)

  1. Executable programs or shell commands
  2. System calls (functions provided by the kernel)
  3. Library calls (functions within program libraries)
  4. Special files (usually found in /dev)
  5. File formats and conventions eg /etc/passwd
  6. Games
  7. Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
  8. System administration commands (usually only for root)
  9. Kernel routines [Non standard]

对应简要解释如下:

  1. 是普通的命令
  2. 是系统调用, 如open, write之类的(通过这个, 至少可以很方便的查到调用这个函数, 需要加什么头文件)
  3. 是库函数, 如printf, fread
  4. 是特殊文件, 也就是/dev下的各种设备文件
  5. 是指文件的格式, 比如passwd, 就会说明这个文件中各个字段的含义
  6. 是给游戏留的, 由各个游戏自己定义
  7. 是附件还有一些变量, 比如向environ这种全局变量在这里就有说明
  8. 是系统管理用的命令, 这些命令只能由root使用, 如ifconfig

关于命令分类的小说明

上面可以看到,man会将manual手册按section进行区分,并有一个section号。也表示帮助页的类型。

有的地方翻译为了区分,会将其叫做“领域”,即不同领域的操作手册。也有的叫做“篇章”,不同的篇章或篇章号。

只要知道这是一个分类,一个不同部分的区分即可。

man对一个命令手册页的查找是从1开始的,默认依次查找每个部分,直到找到并显示。

比如,man mkdir查看时,第一行显示为MKDIR(1);而,如果查看man yum,第一行显示为yum(8),表示yum是一个系统管理命令。

不知道一个命令的类型如何查看其帮助文档?

上面通过篇章号,可以查看某一分类下的命令帮助。但是,如果不知道命令的类型如何处理?

也就是,有时我们不知道,一个”命令”是属于shell命令、系统调用、库调用,还是文件、宏…

可以借助-a参数,使用 man -a command 可以查看所有类型中包含该命令的帮助文档。这样,可以去查找是否属于自己想查看的命令帮助。

如下,查看man -a passwd,再列出一个帮助文档后,可以根据是否是自己需要的,决定查看下一个,跳过,还是退出!

查看所有的man命令帮助:man -a man。

1
2
3
4
sh复制代码# man -a man
--Man-- next: man(1p) [ view (return) | skip (Ctrl-D) | quit (Ctrl-C) ]

--Man-- next: man(7) [ view (return) | skip (Ctrl-D) | quit (Ctrl-C) ]

可以看到,man一共有两个,一个是查看帮助手册的man命令,一个是宏的man。

man手册页的内容说明

手册页一般由以下几部分组成:

  • NAME 命令名称
  • SYNOPSIS 命令的语法格式
  • DESCRIPTION 对命令功能的描述
  • OPTIONS 说明该命令所提供的选项和参数
  • EXAMPLES 对命令如何使用给出的例子
  • FILES 该手册页的默认位置
  • AUTHOR 该软件的作者
  • REPORTING BUGS 告诉用户将他们发现的BUGS通过邮件发送给开发者
  • COPYRIGHT 版权信息
  • SEEALSO 与该程序有关的其他程序

man命令常用参数

参数 备注
-a –all 显示所有匹配项
-d –debug 打印调试信息
-D –default
-f –whatis,同命令whatis ,将在whatis数据库查找以关键字开头的帮助索引信息
-h 显示帮助信息
-k –apropos,同命令apropos 显示手册页的简短描述。
-S list -s list, –sections=list。指定搜索的领域及顺序 如:-S 1:1p httpd 将搜索man 1然后 man 1p目录
-t 使用 troff 命令格式化输出手册页 默认:groff输出格式页
-w –where, –path, –location
-W –where-cat, –location-cat
section 搜索领域【限定手册类型】默认查找所有手册。即section号,命令的分类。

此处参数未做详细验证,如有需要,请自行测试确认!

参考

主要参考自Linux实战技能100讲,以及网上的部分资料。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一次线上突发频繁fullGC的分析与解决

发表于 2021-10-26

前情概要

告警

​ 4月份某天下午刚上班,春困之际,整个人还不是非常的清醒,结果钉钉开始收到告警,线上一台服务在非常频繁fullGC,一下子,整个人清醒多了,这个不是一个简单的告警,对服务的影响非常大。确实如此,没过几分钟,下游服务开始调用超时告警

告警)告警

​ 我们公司内部的APM工具是pinpoint,可以看到服务超时13:50~14:03这段时间内服务响应时间有很多超过了5000ms

告警

找到出问题的那台实例

告警

​ 红线表示fullGC,基本上这个实例处于不可用的状态,分发到这个实例的请求基本上也就是超时,其他实例此时正常,我们服务总共部署了五个实例,只有这个实例出了问题

快速恢复

  • 下线出问题的实例,记得这里先dump堆文件

问题分析

  • 原因分析
    1. 根据以上现象,猜测应该是某个不常用的请求或者某种特殊的场景导致内存加载了大量数据,正好这个请求是由出问题的这个实例来处理的。
    2. 因为服务了过了一会就恢复了正常,服务日志里也找不到任何的有用的信息,分析陷入了瓶颈,但这个问题只要出现一次,就会导致服务基本上不可用,所以还是要找到根本的原因,彻底的根治这个问题,避免后续产生更大的影响。
    3. 我们的服务加载数据的途径有限,要么是数据库查询,要么是外部接口返回,根据dump文件其实可以看出来对象其实大部分都是我们内部的实体对象(这里忘记截图了),所以应该是数据库查询返回了大批量数据。
  • 解决思路
    • JVM参数调整: 调整JVM参数,尽可能避免出现该问题
    • 代码逻辑调整: 找到问题代码并修复

JVM参数调整

整个调整的思路是尽可能最小化”短暂对象”移动到老年代的数量,避免老年代快速膨胀,触发majorGC或者fullGC,进而导致服务STW,影响业务,但是这个调整也无法避免代码导致的极端情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
properties复制代码-Xmx5g 
-Xms5g
-XX:MaxMetaspaceSize=512M
-XX:MaxTenuringThreshold=15
-XX:MetaspaceSize=512M
-XX:NewSize=2560M
-XX:MaxNewSize=2560M
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
-XX:+PrintGCApplicationStoppedTime
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=85
-Xloggc:/opt/zcy/modules/agreement-center/gc.log
-XX:CMSFullGCsBeforeCompaction=2
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
  • 调整新生代的大小:-xx:NewSize=2560M,-xx:MaxNewSize=2560M, 我们堆大小为5g,调整新生代大小到2560M,为整个堆大小的一半,尽可能的让更多的类可以放到新生代
  • 调整对象晋升到老年代的年龄阈值: -XX:MaxTenuringThreshold=15, CMS中该值默认为6,调整到15,让对象尽可能保留在新生代,在新生代完成回收
  • 调整survivor区与Eden区的比例: -xx:SurvivorRatio=8, 换算一下,Eden区大小等于2560M*0.8 = 2048M

代码逻辑调整

这里的解决思路是,限制代码大批量数据查询,找出代码里大批量查询数据库的坏代码并修复

  • 方案一:通过mybatis插件,全局查询语句加上limit,限制最大的返回数据,但是我们的业务中,经常有关联数据好几万条,这里其实数据结构设计是不合理,这个limit大于好几万也就失去了意义,因为有些表单行记录比较大,几万条记录也有几百兆,请求量大的时候,也会出现这个问题,而且也不能发现出问题的代码,项目代码太多了,看代码找问题只能看缘分,不靠谱
  • 方案二:也是通过mybatis插件,统计每次查询结果的数量,大于某个阈值打印告警日志,实时监控该日志,根据日志找到整个链路,进而找到出问题的代码

我这里采用了第二种方案,插件代码如下:

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
java复制代码@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
@Slf4j
public class QueryDataSizeInterceptor implements Interceptor {

/**
* 查询条数限制,超过打印warn日志
*/
private Integer querySizeLimit;

/**
* 是否开启
*/
private Boolean isOpen;

public QueryDataSizeInterceptor(Integer querySizeLimit, Boolean isOpen) {
this.querySizeLimit = querySizeLimit;
this.isOpen =isOpen;
}

@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
if (isOpen) {
processIntercept(invocation.getArgs());
}
} catch (Throwable throwable) {
log.warn("QueryDataSizeInterceptor.failed,cause:{}", Throwables.getStackTraceAsString(throwable));
}
return invocation.proceed();
}

private void processIntercept(final Object[] queryArgs) {
Statement statement = (Statement) queryArgs[0];
try {
HikariProxyResultSet resultSet = (HikariProxyResultSet) statement.getResultSet();
MetaObject metaObject1 = SystemMetaObject.forObject(resultSet);
RowDataStatic rs = (RowDataStatic) metaObject1.getValue("delegate.rowData");
if (Objects.nonNull(rs) && !rs.wasEmpty() && rs.size() >= querySizeLimit) {
MetaObject metaObject2 = SystemMetaObject.forObject(statement);
String sql = (String) metaObject2.getValue("delegate.originalSql");
log.warn("current.query.size.is.too.large,size:{},sql:{}",rs.size(), sql);
}

} catch (Throwable throwable) {
log.warn("QueryDataSizeInterceptor.failed,cause:{}", Throwables.getStackTraceAsString(throwable));
}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {

}
}

大部分代码都是mybatis的插件模版代码,核心代码很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码private void processIntercept(final Object[] queryArgs) {
Statement statement = (Statement) queryArgs[0];
try {
HikariProxyResultSet resultSet = (HikariProxyResultSet) statement.getResultSet();
MetaObject metaObject1 = SystemMetaObject.forObject(resultSet);
RowDataStatic rs = (RowDataStatic) metaObject1.getValue("delegate.rowData");
// 某次查询超过配置的条数时,打印warn日志
if (Objects.nonNull(rs) && !rs.wasEmpty() && rs.size() >= querySizeLimit) {
MetaObject metaObject2 = SystemMetaObject.forObject(statement);
String sql = (String) metaObject2.getValue("delegate.originalSql");
log.warn("current.query.size.is.too.large,size:{},sql:{}",rs.size(), sql);
}

} catch (Throwable throwable) {
log.warn("QueryDataSizeInterceptor.failed,cause:{}", Throwables.getStackTraceAsString(throwable));
}
}

代码逻辑: 某次查询超过配置的条数时,打印warn日志。并在日志平台配置对应日志的钉钉告警

再次出现

日志

​ 有了日志,通过traceId马上就能找到对应代码了,可以看到这里从数据库查询30多万数据到内存,触发fullgc也是正常的

1
2
3
4
5
6
7
8
9
10
11
java复制代码Long total = protocolQualificationManager.count(criteria);

if (total == 0) {
return Response.ok(new Paging<>(0L, Collections.EMPTY_LIST));
}
//List<AgProtocolQualification> result = agProtocolQualificationDao.paging(criteria);
List<AgProtocolQualification> result = protocolQualificationManager.paging(criteria);
Set<Long> protocolIds = FluentIterable.from(result).transform(k -> k.getProtocolId()).toSet();

// 这个查询出了问题
List<AgProtocol> protocols = agProtocolDao.queryByIds(Lists.newArrayList(protocolIds));

​ 代码看起来没啥问题呀,在看对应的查询的mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<select id="queryByIds" parameterType="java.util.List" resultMap="defaultResultMap">
SELECT
<include refid="allColumns"/>
FROM
ag_protocol
<where>
<if test="ids != null and ids.size != 0" >
and id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</if>

<!--后面加的代码 防止查询全表 -->
<if test="ids == null or ids.size == 0" >
and false
</if>
<include refid="not_deleted"/>
</where>
</select>
  • 这里没有做限制,当ids为null,全表查询not_deleted的数据,30多万条记录全部返回

坑点和教训

  • 动态sql 如果所有条件都未匹配,不能直接查询全表,应该返回为空,要在代码里或者mapper sql中加以限制
  • 优化业务数据结构,在代码里加上limit限制
  • 数据库层面也要做限制,如果这里是大批量的删除,可能业务影响会更大

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

docker+mysql快速部署 Docker+MySQL

发表于 2021-10-25

Docker+MySQL

拉取镜像

参考命令:

1
ruby复制代码docker pull [OPTIONS] NAME[:TAG|@DIGEST]

测试命令:

镜像地址( hub.docker.com/_/mysql)

先安装docker,直接执行以下命令,拉取MySQL镜像

1
复制代码docker pull mysql:5.7.36
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
makefile复制代码└─[1] <> docker pull mysql:5.7.36
8.0.27: Pulling from library/mysql
b380bbd43752: Already exists
f23cbf2ecc5d: Already exists
30cfc6c29c0a: Already exists
b38609286cbe: Already exists
8211d9e66cd6: Already exists
2313f9eeca4a: Already exists
7eb487d00da0: Already exists
4d7421c8152e: Pull complete
77f3d8811a28: Pull complete
cce755338cba: Pull complete
69b753046b9f: Pull complete
b2e64b0ab53c: Pull complete
Digest: sha256:6d7d4524463fe6e2b893ffc2b89543c81dec7ef82fb2020a1b27606666464d87
Status: Downloaded newer image for mysql:5.7.36
docker.io/library/mysql:5.7.36

查看镜像

1
2
3
css复制代码└─[0] <>  docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql 5.7.36 938b57d64674 6 days ago 448MB

启动实例

参考命令:

1
2
3
4
css复制代码docker 
--name 指定容器名字
-p [container port]:[host port]
-e 设置root用户的密码
1
css复制代码└─[0] <> docker run -itd --name mysql_test_5736 -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

将容器默认的3306端口,映射到本地的3307端口,本地程序连接3307就可以访问数据库

查看容器进程

1
2
3
bash复制代码└─[0] <> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b238e18a0a8a mysql:5.7.36 "docker-entrypoint.s…" 2 hours ago Up 2 hours 33060/tcp, 0.0.0.0:3307->3306/tcp mysql_test_5736

连接

通过 docker ps 查看容器的 ID
执行以下命令登录到容器里面

1
2
bash复制代码└─[0] <> docker exec -it b238e18a0a8a /bin/bash
root@b238e18a0a8a:/#

在容器中执行MySQL登录命令:mysql -uroot -plocalhost -p123456 -P3307

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码root@b238e18a0a8a:/# mysql -uroot -plocalhost -p123456 -P3307
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 23
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

至此,通过docker就部署好了MySQL单机实例

导入测试数据

下载测试数据

测试数据地址:github.com/datacharmer…

将压缩数据包下载到本地,并解压

将解压包 test_db-master 复制到 容器:mysql_test_5736 下的 /root/目录下

复制命令:

1
bash复制代码└─[0] <> docker cp test_db-master mysql_test_5736:/root/

测试数据导入MySQL中

登录容器的MySQL 通过source命令导入

1
2
3
4
5
6
7
8
9
10
11
sql复制代码└─[0] <> docker exec -it b238e18a0a8a /bin/bash
root@b238e18a0a8a:/# mysql -uroot -plocalhost -p123456 -P3307
mysql> system ls /root/
mysql> source employees.sql
.......
+---------------------+
| data_load_time_diff |
+---------------------+
| 00:00:31 |
+---------------------+
1 row in set (0.00 sec)

命令列表

1
2
3
4
5
6
7
8
bash复制代码1. docker pull mysql:5.7.36
2. docker run -itd --name mysql_test_5736 -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
3. docker ps
4. docker exec -it b238e18a0a8a /bin/bash
5. git clone https://github.com/datacharmer/test_db
6. unzip xxx.zip
7. docker cp test_db-master mysql_test_5736:/root/
8. mysql -uroot -plocalhost -p123456 -P3307

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

计算机组成原理——控制器

发表于 2021-10-25

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

之前讲过CPU主要分为控制器和运算器,本文主要介绍控制器。

控制器是计算机系统的指挥中心,控制器的主要功能有:

  • 从主存中取出一条指令,并指出下一条指令在主存中的位置。
  • 对指令进行译码或测试,产生相应的操作控制信号,以便启动规定的动作。
  • 指挥并控制CPU、主存、输入和输出设备之间的数据流动方向。

根据控制器产生微操作控制信号的方式的不同,控制器可分为硬布线控制器和微程序控制器,两类控制器中的PC和IR是相同的,但确定和表示指令执行步骤的办法以及给出控制各部件运行所需要的控制信号的方案是不同的。

硬布线控制器

又称组合逻辑控制器

image.png

CU的输入信号来源:

  • 经指令译码器译码产生的指令信息
  • 时序系统产生的机器周期信号和节拍信号
  • 【标志】执行单元的反馈,如BAN指令

CPU的控制方式:

  • 同步控制方式:系统有一个统一的时钟,所有控制信号均来自这个统一的时钟信号
+ 控制电路简单
+ 运行速度慢
  • 异步控制方式:各部件按自身固有的速度工作,通过应答方式进行联络
+ 运行速度快
+ 控制电路复杂
  • 联合控制方式:大部份使用同步控制,剩下的异步控制

微程序控制器

微程序控制器采用存储逻辑实现,将微操作信号代码化,使每条机器指令转换为一段微程序并存入一个专门的存储器中,为操作控制信号由微指令产生。

  • 💇微命令和微操作
+ 一条机器指令可以分解成一个微操作序列。eg:打开某个控制门的电位信号
+ 微命令是微操作的控制信号,微操作是微命令的执行过程。
+ 可以同时产生、共同完成的命令互为相容性微命令,否则为互斥性微命令
  • 💇微指令与微周期
+ 微指令是若干微命令的集合
+ 存放微指令的控制存储器的单元地址称为微地址
+ 微指令包括


    - 操作控制字段,又称微操作码字段,用于产生某一步操作所需的各种操作控制信号
    - 顺序控制字段,又称微地址码字段,用于控制产生下一条要执行的微指令地址*不要把微地址码理解成地址而要理解成顺序*
+ 微周期通常指从控制存储器读取并执行的时间
  • 主存储器与控制存储器
+ 前者用于存放程序和数据,CPU的外部
+ 后者用于存放微程序,内部
  • 程序与微程序
+ 程序是指令的有序集合,用于完成特定的功能
+ 微程序是微指令的有序集合,一条机器指令对应一个微程序【对程序员透明】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

被问懵的java值传递和引用传递 java到底是值传递还是引

发表于 2021-10-25

java到底是值传递还是引用传递

悲惨的起因

为什么悲惨呢,因为尽管我看了多少篇讲java值传递还是引用传递的文章在面试的时候还是被面试官干碎了,十分的悲惨。
xinlei

所以下定决心痛定思痛,写这篇博客。。。

问题的关键

我觉得要弄明白java是值传递还是引用传递,首先要弄明白什么事值传递什么事引用传递。引用百度百科的概念介绍

  • 值传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

  • 引用传递

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

从百度百科的描述中我们可以看出值传递与引用传递表现出来的一大区别是是否传入函数后,在函数中操作会影响实际参数。而对于Java来说能够传入方法中的值分为两种,一种是基本数据类型,如int,double,long等,另一种是引用类型,如String,或者自定义的类等。所以要分析Java是值传递还是引用传递要从两个方面区分入手。

基本数据类型的传递方式

对于基本数据类型的传递方式,首先我们先来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class TestRunTime {

public static void main(String[] args) {
int i = 3;
TestRunTime test = new TestRunTime();
test.exchange(i);
System.out.println(i);
}

public void exchange(int x){
x = 5;
}
}

对于这个例子,最后输出的结果为3,方法中的改变并没有影响到原值。对于这个情况我们应该从基本数据类型中的值存储的方式来思考,对于基本数据类型,值直接保存在变量中而且对于这样一个局部变量来说,它存在于局部变量表中。但是基本数据类型也要看存在位置,当你数据局部变量的时候,方法中的操作是无法影响到这个变量的,传值过程也是值的拷贝而已。

引用类型的传递方式

对于引用类型的传递方式,首先我们要明确的是我们创建的对象会存储在堆中,而栈中保存的是堆内地址,情况如下。我们创建对象的过程实际上是在栈中创建一个对象,存放堆中对象的地址,在堆中创建一个对象,存放实际的内容。

duixiangcunfang

当我们把对象作为参数传入方法时,过程是将对象的地址拷贝了一份,即形参指向了堆内实参的地址,这时候问题就来了,如果我们在方法中对对象作出修改,我们会发现方法外的实参内容变了,就会认为这是引用传递,但实际上是不对的,这是概念的混淆,我们再来看一下定义,引用传递和值传递的区别在是否对传入参数做出修改,而我们传入方法中的是实参的地址,我们的操作并未影响到实参,而我们改变形参的引用时,同样实参的引用也不会发生变化。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static void main(String[] args) {
Test test = new Test();
User user = new User();
System.out.println("操作之前的"+user);
test.work(user);
System.out.println("操作之后的"+user);
}


public void work(User user){
user = new User();
}

image-20210911181800623

如图结果所示,我们可以看到,在方法中对形参的地址修改并未影响到实参,所以java是值传递而非引用传递。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hyperleger Fabric【2x】环境搭建(一)

发表于 2021-10-25

环境

这里与之前搭建1.0不同,这里推荐使用虚拟机或者是服务器。

【注】因为使用WSL可能会出现一些小问题

我这里使用的是Ubuntu1804阿里云服务器进行搭建。

推荐使用root用户进行操作:省时省力。

依赖环境:
这里面参考搭建1.4.x的环境。
下面给出参考链接。依赖环境搭建

相比之1.0版本 2.0版本有哪些更新呢

不想看的直接跳过本小章。此内容为官方文档说明。

建议直接下一节,因为可能看不进去。
这个是官方链接。

安装 Hyperleger Fabric 2.2.0

方案一:脚本安装

这种方法是最简单方便的安装方式。但是因为下载镜像是国外的镜像,所以可能会比较慢。建议在先使用这种方式测试一下。如果可以就最好了,如果不行请参考安装方案二。

当我们以为还会存在像1.0版本的bootstrap.sh官方安装脚本时,我们发现这里面并不存在。那一键式安装脚本哪去了呢?官方给单独拿出来了。在这里。

raw.githubusercontent.com/hyperledger…

关于安装脚本与1.4.x的几乎相同。脚本分析见安装脚本分析

根据之前的分析,我会修改这个脚本,配置一些国内加速。下面是经过加速的bootstrap.sh安装脚本。这个脚本同样会执行下面几个操作。

  • 安装docker镜像
  • 安装二进制文件
  • 安装官方示例

下面是已优化的bootstrap.sh。(记得自己手动配置国内docker镜像加速)
将下面内容复制到我们刚刚的工作目录。(记得执行脚本之前要启动docker服务)
我们使用的是官方文档推荐使用版本 2.2.0 1.4.7

这里面我新建了一个路径
/root/fabric-samples-2.x

将下面脚本保存为bootstrap.sh并放到你想要执行的工程文件夹中。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
bash复制代码#!/bin/bash
#
# Copyright IBM Corp. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#

# if version not passed in, default to latest released version
VERSION=2.3.3
# if ca version not passed in, default to latest released version
CA_VERSION=1.5.2
ARCH=$(echo "$(uname -s|tr '[:upper:]' '[:lower:]'|sed 's/mingw64_nt.*/windows/')-$(uname -m | sed 's/x86_64/amd64/g')")
MARCH=$(uname -m)

printHelp() {
echo "Usage: bootstrap.sh [version [ca_version]] [options]"
echo
echo "options:"
echo "-h : this help"
echo "-d : bypass docker image download"
echo "-s : bypass fabric-samples repo clone"
echo "-b : bypass download of platform-specific binaries"
echo
echo "e.g. bootstrap.sh 2.3.3 1.5.2 -s"
echo "will download docker images and binaries for Fabric v2.3.3 and Fabric CA v1.5.2"
}

# dockerPull() pulls docker images from fabric and chaincode repositories
# note, if a docker image doesn't exist for a requested release, it will simply
# be skipped, since this script doesn't terminate upon errors.

dockerPull() {
#three_digit_image_tag is passed in, e.g. "1.4.7"
three_digit_image_tag=$1
shift
#two_digit_image_tag is derived, e.g. "1.4", especially useful as a local tag for two digit references to most recent baseos, ccenv, javaenv, nodeenv patch releases
two_digit_image_tag=$(echo "$three_digit_image_tag" | cut -d'.' -f1,2)
while [[ $# -gt 0 ]]
do
image_name="$1"
echo "====> hyperledger/fabric-$image_name:$three_digit_image_tag"
docker pull "hyperledger/fabric-$image_name:$three_digit_image_tag"
docker tag "hyperledger/fabric-$image_name:$three_digit_image_tag" "hyperledger/fabric-$image_name"
docker tag "hyperledger/fabric-$image_name:$three_digit_image_tag" "hyperledger/fabric-$image_name:$two_digit_image_tag"
shift
done
}

cloneSamplesRepo() {
# clone (if needed) hyperledger/fabric-samples and checkout corresponding
# version to the binaries and docker images to be downloaded
if [ -d test-network ]; then
# if we are in the fabric-samples repo, checkout corresponding version
echo "==> Already in fabric-samples repo"
elif [ -d fabric-samples ]; then
# if fabric-samples repo already cloned and in current directory,
# cd fabric-samples
echo "===> Changing directory to fabric-samples"
cd fabric-samples
else
echo "===> Cloning hyperledger/fabric-samples repo"
git clone -b main https://gitee.com/hyperledger/fabric-samples.git && cd fabric-samples
fi

if GIT_DIR=.git git rev-parse v${VERSION} >/dev/null 2>&1; then
echo "===> Checking out v${VERSION} of hyperledger/fabric-samples"
git checkout -q v${VERSION}
else
echo "fabric-samples v${VERSION} does not exist, defaulting to main. fabric-samples main branch is intended to work with recent versions of fabric."
git checkout -q main
fi
}

# This will download the .tar.gz
download() {
local BINARY_FILE=$1
local URL=$2
echo "===> Downloading: " "${URL}"
curl -L --retry 5 --retry-delay 3 "${URL}" | tar xz || rc=$?
if [ -n "$rc" ]; then
echo "==> There was an error downloading the binary file."
return 22
else
echo "==> Done."
fi
}

pullBinaries() {
echo "===> Downloading version ${FABRIC_TAG} platform specific fabric binaries"
download "${BINARY_FILE}" "https://hub.fastgit.org/hyperledger/fabric/releases/download/v${VERSION}/${BINARY_FILE}"
if [ $? -eq 22 ]; then
echo
echo "------> ${FABRIC_TAG} platform specific fabric binary is not available to download <----"
echo
exit
fi

echo "===> Downloading version ${CA_TAG} platform specific fabric-ca-client binary"
download "${CA_BINARY_FILE}" "https://hub.fastgit.org/hyperledger/fabric-ca/releases/download/v${CA_VERSION}/${CA_BINARY_FILE}"
if [ $? -eq 22 ]; then
echo
echo "------> ${CA_TAG} fabric-ca-client binary is not available to download (Available from 1.1.0-rc1) <----"
echo
exit
fi
}

pullDockerImages() {
command -v docker >& /dev/null
NODOCKER=$?
if [ "${NODOCKER}" == 0 ]; then
FABRIC_IMAGES=(peer orderer ccenv tools)
case "$VERSION" in
2.*)
FABRIC_IMAGES+=(baseos)
shift
;;
esac
echo "FABRIC_IMAGES:" "${FABRIC_IMAGES[@]}"
echo "===> Pulling fabric Images"
dockerPull "${FABRIC_TAG}" "${FABRIC_IMAGES[@]}"
echo "===> Pulling fabric ca Image"
CA_IMAGE=(ca)
dockerPull "${CA_TAG}" "${CA_IMAGE[@]}"
echo "===> List out hyperledger docker images"
docker images | grep hyperledger
else
echo "========================================================="
echo "Docker not installed, bypassing download of Fabric images"
echo "========================================================="
fi
}

DOCKER=true
SAMPLES=true
BINARIES=true

# Parse commandline args pull out
# version and/or ca-version strings first
if [ -n "$1" ] && [ "${1:0:1}" != "-" ]; then
VERSION=$1;shift
if [ -n "$1" ] && [ "${1:0:1}" != "-" ]; then
CA_VERSION=$1;shift
if [ -n "$1" ] && [ "${1:0:1}" != "-" ]; then
THIRDPARTY_IMAGE_VERSION=$1;shift
fi
fi
fi

# prior to 1.2.0 architecture was determined by uname -m
if [[ $VERSION =~ ^1.[0-1].* ]]; then
export FABRIC_TAG=${MARCH}-${VERSION}
export CA_TAG=${MARCH}-${CA_VERSION}
export THIRDPARTY_TAG=${MARCH}-${THIRDPARTY_IMAGE_VERSION}
else
# starting with 1.2.0, multi-arch images will be default
: "${CA_TAG:="$CA_VERSION"}"
: "${FABRIC_TAG:="$VERSION"}"
: "${THIRDPARTY_TAG:="$THIRDPARTY_IMAGE_VERSION"}"
fi

BINARY_FILE=hyperledger-fabric-${ARCH}-${VERSION}.tar.gz
CA_BINARY_FILE=hyperledger-fabric-ca-${ARCH}-${CA_VERSION}.tar.gz

# then parse opts
while getopts "h?dsb" opt; do
case "$opt" in
h|?)
printHelp
exit 0
;;
d) DOCKER=false
;;
s) SAMPLES=false
;;
b) BINARIES=false
;;
esac
done

if [ "$SAMPLES" == "true" ]; then
echo
echo "Clone hyperledger/fabric-samples repo"
echo
cloneSamplesRepo
fi
if [ "$BINARIES" == "true" ]; then
echo
echo "Pull Hyperledger Fabric binaries"
echo
pullBinaries
fi
if [ "$DOCKER" == "true" ]; then
echo
echo "Pull Hyperledger Fabric docker images"
echo
pullDockerImages
fi
1
2
3
bash复制代码chmod u+x bootstrap.sh
# 其中2.2.0 为可执行脚本的版本 1.4.7为CA的版本
sudo ./bootstrap.sh 2.2.0 1.4.7

执行完成之后的结果。同时可以看到相关docker镜像已经下载成功,bin目录下面也有对应的可执行文件。

image.png

方案二:半自动化安装

我们只使用上述脚本的一部分功能—-下载docker镜像功能,下载fabric-samplesa功能。其余功能我们手动进行下载。加快安装的速度。

首先我们将上面的bootstrap.sh文件修改一下。将BINARIES修改为false。

image.png

然后执行上述脚本。

1
bash复制代码./bootstrap.sh 2.2.0 1.4.7

执行之后会生成fabric-samples 以及会下载很多要用到的镜像

之后下载我们所需要的相关文件。

  • 下载二进制文件,版本为2.2.0.下载地址:github.com/hyperledger…
  • 下载ca文件,版本为1.4.7。下载地址:github.com/hyperledger…

将下载好的文件拷贝到服务器中(或者虚拟机当中刚才创建的工程文件夹中)

image.png
通过tar命令进行解压:

1
2
复制代码tar -zxvf hyperledger-fabric-ca-linux-amd64-1.4.7.tar.gz
tar -zxvf hyperledger-fabric-linux-amd64-2.2.0.tar.gz

此时会生成两个文件夹 bin config
将这两个文件夹以及里面的内容拷贝到 fabric-samples之中。此时我们可以看到里面内容如下。

image.png

image.png

1
2
bash复制代码mv bin fabric-samples
mv config fabric-samples

这说明我们已经安装成功了。

测试网络测试一下

1
2
3
bash复制代码cd test-network
./network down #为了保证环境干净,先使用down清除一下残留环境
./network up

image.png
当我们看到这部分说明已经启动成功了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

TCP/IP协议(四、tcp重传机制) 41 简述 42

发表于 2021-10-25

从上节课看到,出现了丢包的时候,tcp会出现重传,那重传的机制是什么,TCP拥有两套独立机制来完成重传。一是基于时间,二是基于确认消息(SACK)。第二种方法通常比第一种更高效。

4.1 简述

TCP在发送数据时会设置一个计时器,若到计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时称为重传超时(RTO)。另一种方式的重传称为快速重传,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息(SACK)表明出现失序报文时,快速重传会推断出现丢包。

4.1.1 简单的超时与重传

《TCP详解》中举了个例子,非常不错,用telnet连接,连接完成之后再断开一遍,然后进行抓包,抓出来的包就可以体验出超时重传的机制,每一次重传都比前面一次晚2倍。重传了几次之后,就会提示连接失败。
TCP拥有两个阈值来决定如何重传同一个报文段。R1表示TCP在向IP层传递“”消极建议”(如重新评估当前IP路径)前,愿意尝试重传的次数(或等待时间)。R2(大于R1)指示TCP应放弃当前连接的时机。针对SYN报文段的R2应最少设为3分钟。

linux系统中,对一般数据段来说,R1和R2的值可以通过应用程序或使用系统配置变量net.ipv4.tcp_retries1和net.ipv4.tcp_retries2设置。tcp_retries1默认值为3。tcp_retries2默认值为15,对应约13~30分钟,根据具体连接的RTO而定。

由于TCP需要适应不同环境进行操作,可能随着时间不断变化,因此需基于当前状态设定超时值。例如,若某个网络连接失败,需要重新建立,RTT也会随之改变(可能变化很大)。也就是说,TCP需动态设置RTO(重传超时)。

4.1.2 简单计算RTO的方法

最初的TCP规范【RFC0793】采用如下公式计算得到平滑的RTT估计值(称为SRTT):
SRTT = α(SRTT) + (1-α)RTTs

这里的,SRTT是基于现存值和新的样本值的TRRs得到的结果。常量α为平滑因子,推荐值为0.8~0.9.
这种方法称为指数加权移动平均或低筒过滤器。

后来考虑到SRTT估计其得到的值会随RTT而变化,【RFC0793】推荐值如下公式设置RTO:
RTO = nin(ubound, max(lbound, (SRTT)β))

这里的β为时延离散因子,推荐值为1.3~2.0,ubound为RTO的上边界(可设为建议值1分钟),lbound为RTO的下边界(可设定建议值,如1秒)。RTO的取值范围,大于1秒,小于2倍的RTT。

这样子计算RTO就可以了么?

当RTT有大规模的变动的情况下,上述公式计算得出的结果是比较小的,因为上一次的结果占比较大,所以导致会大量重传,这样会导致网络过载。

为了解决上面所说的问题,可对原方法做出改进以适应RTT变动较大的情况。可通过记录RTT测量值的变化情况以及均值来得到较为准确的估计值。我们还可以同时测量均值和方差(或标准差)能更好地估计将来值。对RTT的可能值范围做出好的预测可以帮组TCP设定一个能适应大多数情况的RTO值。

平均偏差是对标准差的一种好的逼近,但计算起来却更容易,更快捷。所以接下来这个版本,我们要结合平均值和平均偏差来进行估算。可对每个RTT测量值M(前面称为RTT)采用如下算式:

1
2
3
scss复制代码srtt = (1-g)(srtt) + (g)M   //算这个srtt跟前面是一样的
rttvar = (1-h)(rttvar) + (h)(|M| - srtt ) //计算平均偏差
RTO = srtt + 4(rttval)

如前面一样,srtt是平均值,rttval为绝对误差|err|的EWMA。err为测量值M与当前RTT估计值srtt之间的偏差。
增量g为新RTT样本M占srtt估计值的权重,取为1/8.增量h为新平均值偏差样本占偏差估计值rttval的权重,取值为1/4。当平均偏差越大的时候,(也就是大规模波动的时候),RTO会越大。

4.1.3 RTT的计算

上面的公式已经把RTO的计算说了挺清楚了,但是每个RTO的计算都需要测量RTT的值。

那怎么测量RTT的值呢?

在测量RTT的过程中,TCP的时钟都是在运转的。对于刚开始建立连接的的时候,实际TCP的时钟并非从0开始计时,也没有绝对精确的精度。只要TCP的时候,会随着系统的时钟增加就可以了,TCP时钟一个“滴答”的时间长度称为粒度。通常,该值相对较大(约500ms),但linux系统使用了更细的粒度1ms。

粒度会影响RTT的测量以及RTO的值,所以上面的公式又来了再次的更新:
RTO = max(srtt + max(G, 4(rttval)), 1000)

这里的G为计时器的粒度,1000ms为整个RTO的下界值。因此,RTO至少为1s,同时提供了可选上界值,假设为60s。

4.1.4 初始值

我们前面说的都是根据时间的变化而得出的值,那初始化的时候的值呢?

在首个SYN交换之前,TCP无法设置RTO的值。除非系统提供,否则也无法设置估计器的初始值。

根据【RFC6298】,RTO的初始值为1s,而初始SYN报文段采用的超时间隔为3s,当接收到首个RTT测量结果M,估计器按如下方法进行初始化:
srtt = M
rttval = M/2

4.1.5 linux采用的方法

之前看到一篇介绍linux的重传机制,当初没保存,今天没找到了,真是无线插柳柳成荫,下次碰到才仔细研读一波,今天就以自己的话写,写多了多多包涵,也请指出。

在这里插入图片描述

画了这个图还是挺辛苦的,大家也可以按照画画,加深印象。

初始阶段:
在上面介绍,SYN的超时时间设为3s,这个就不在这图里表示出来了,因为这是通信前的,现在是由客户端发起连接syn报文,顺带上TSV=0,这里取了相对值,具体的不会是0.这时候服务器接收到了syn,回复了一个ack,会把接收到的TSV的值写到TSER中,这时候客户端收到这个ack之后,这就是第一次接收到M,看上面可以得到:
srtt=m=16, rttval=m/2=16/2=8
等于linux中引入了一个变量mdev,mdev采用标准方法的瞬间平均偏差估计值,也就是mdev=m/2=16/2=8,也引进mdev_max记录在测量RTT样本过程中的最大mdev,这时候的rttval=mdev_max(mdev, TCP_RTO_MAX=50ms),所以这里的rttval=50,最后rto=srtt+4*rttval=216;

正常计算:
客户端接收到服务器返回一个ack之后,就再次给服务器发送ack,这是三次握手过程。三次握手结束后,正式进入了传输阶段,这时候客户端发送了两个1400长度的报文,因为两个间隔比较小,TSV没有增加。(这个也是一个问题)。服务器接收到两个报文之后,回复ack=2801,这个值怎么来的到滑动窗口的时候讲,这时候客户端接收到了这个ack,第二次计算RTO又开始了:

1
2
3
4
5
ini复制代码m=223-127=96
mdev=mdev*3/4+|m-srtt|*1/4=8*3/4+|96-16|*1/4=26ms
mdev_max=rttval=max(mdev_max, mdev)=max(50, 26)=50
srtt=srtt*7/8+m*1/8=16*7/8+96*1/8=26
RTO=26+4*50=226

完美,这是第二次计算的结果。

接收不过来的情况:
如果ACK不能及时返回,返回了后面的的报文的ACK,计算RTO的时候,也是要看ACK中带的TSER的时间戳计算,不要看客户端最新发送的时间戳。

linux使用这种机制有如下优点:

  • 时间粒度使用了1ms
  • 引入了瞬间平均偏差mdev,跟一段区间内的mdev的最大值mdev_max,保证rttval不会变的太小
  • 减小rttval的比例
    if(m < (srtt - mdev))
    mdev = (31/32)mdev+(1/32)|srtt-m|
    else
    mdev = (3/4)mdev+(1/4)
    |srtt-m|

4.1.6 基于计时器重传

由上面的讲解,我们已经明白了,RTO的值的设置了。在设置计时器之前,需记录被计时的报文段的序列号,(要不然收到了ACK都不知道是谁的),若计时收到了该报文段的ACK,那么计时器被取消。之后发送端发送一个新的数据包时,都需设定一个新的计时器,并记录新的序列号。所以TCP连接的发送端不断的设定取消一个重传计时器,如果数据丢失了,超过了计时器的时候,就会启动重传机制。

4.2 快速重传

竟然是快速重传,那我们就快速说完。哈哈。

如果我们接受端已经知道我们丢失了哪个包,但是还要等待超时计时器再进行重传,这样效率低了很多,所以TCP就提出了一个基于接收端的反馈信息来引发的重传(不知道是不是这样的,我自己理解的),叫快速重传。

当网络中出现失序分组时——若接收端收到当前期盼序列号的后续分组时,当前期盼的包可能丢失,也可能仅为延时到达。通常我们无法得知是哪种情况,因为TCP等待一定数目的重复ACK(称为重复ACK阈值或dupthresh),来决定数据是否丢失并触发快速重传。通常dupthresh为常量(值为3),但是linux系统可基于当前的失序程度动态调节该值。

如果不采用SACK的话,在接收到有效的ACK前至多只能重传一个报文段。采用SACK,ACK可包含额外信息,使得发送端再每个RTT时间内可以填补多个空缺。

4.2.1 带选择确认的重传

我抓包的时候,没找到SACK的包,有点可惜,但是在SYN的包中,是打开了SACK的使能,以后遇到了再截图出来吧。

**每一个SACK块内包含的是最近接收到的报文段的序列号范围。**由于SACK空间有限,应尽可能确保向TCP发送端提供最新消息。其余的SACK块包含的内容也是按照接收的先后依次排列。

举个栗子:
没有图的例子是没有灵魂的,不过没办法,找不到包,那就直接写字了。

发送端发送了1-26601个数据,接收到接收到了1-23801和25201-26601。从上面可以看出接收端丢失了23801-25201的数据包,所以我们回复的ACK是这样的,ack=23801 SACK = [25201-26601],假设发送端接收到了3次重复ack,就会触发快速重传,然后把23801-25201的数据包发送过来。(希望我这个例子说对了)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL 更新操作案例分析 案例分析 更新账户金额 常见问

发表于 2021-10-25

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文将通过一个 用户账户金额更新的案例 分析几种数据更新的操作的优劣。希望对大家有帮助 🐶。
数据库版本 : mysql 5.7.23

案例分析

创建数据库的DDL:

1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `hw_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
`status` varchar(20) DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

更新账户金额

直接更新

方案 1 查询后更新

1
2
3
4
5
sql复制代码# 数据查询
select * from hw_account where id = 1;

# 数据更新
update hw_account set balance = 5 where id = 1;

存在的问题,就是分两次操作,如果并发执行的时候,可能造成更新丢失的问题.
​

乐观锁方案

利用版本号操作,即对数据库增加乐观锁的方式进行。

1
2
3
4
5
6
7
8
9
10
11
sql复制代码# 数据查询
select * from hw_account where id = 1;

# 数据更新
update hw_account set balance = 5 , version = version + 1
where id = 1 and version = n;

# 判断是否成功
if row < 1 {
回滚
}

存在的问题,如果该条数据并发操作的时候,会导致其他的请求失败。如果这个请求的前置链路比较长的话, 回滚成本比较高。

无锁方案

不用查询,采用数据库的计算,也不需要版本号的操作,直接通过域值进行有效性判断。具体的 SQL 如下:

1
2
3
4
5
6
7
8
sql复制代码# 数据更新
update hw_account set balance = balance + @change_num , version = version + 1
where id = 1 and version = n;

# 判断是否成功
if row < 1 {
回滚
}

这种方案修改比较简单, 但是依赖于数据计算,感觉不是特别友好。

排队操作

通过 redis 或者 zk 的分布式锁,进行数据请求进行排队。然后在进行数据更新。

1
2
3
4
5
6
7
sql复制代码# 伪代码

if (获取分布式锁) {
update hw_account set balance = @balance where id = 1;
} else {
# 进入等待,或者进行自旋获取锁
}

常见问题

如果数据中存在 update_time 字段受影响的行数是多少?

update_time 的字段定义如下,
如果数据为
id = 1, status = 1
如果执行更新数据的 sql 为

1
sql复制代码update hw_account set `status` = 1 where id = 1;

返回的受影响的行数为 0;
​

如果执行 update 更新但受影响的行数为 0 会加行锁吗?

会的, 执行更新的语句都会加行锁(前提,事务内)

参考资料

  • mysql.com

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…468469470…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%