欢迎光临
我们一直在努力

Java开发中存在这样的代码,反而影响整体整洁和可读性

mumupudding阅读(7)

不完美的库类

不完美的库类(Incomplete Library Class)

当一个类库已经不能满足实际需要时,你就不得不改变这个库(如果这个库是只读的,那就没辙了)。

问题原因

许多编程技术都建立在库类的基础上。库类的作者没用未卜先知的能力,不能因此责怪他们。麻烦的是库往往构造的不够好,而且往往不可能让我们修改其中的类以满足我们的需要。

解决方法

  • 如果你只想修改类库的一两个函数,可以运用 引入外加函数(Introduce Foreign Method)
  • 如果想要添加一大堆额外行为,就得运用 引入本地扩展(Introduce Local Extension) 。

收益

  • 减少代码重复(你不用一言不合就自己动手实现一个库的全部功能,代价太高)

何时忽略

  • 如果扩展库会带来额外的工作量。

重构方法说明

引入外加函数(Introduce Foreign Method)

问题

你需要为提供服务的类增加一个函数,但你无法修改这个类。

class Report {
  //...
  void sendReport() {
    Date nextDay = new Date(previousEnd.getYear(),
      previousEnd.getMonth(), previousEnd.getDate() + 1);
    //...
  }
}

解决

在客户类中建立一个函数,并一个第一个参数形式传入一个服务类实例。

class Report {
  //...
  void sendReport() {
    Date newStart = nextDay(previousEnd);
    //...
  }
  private static Date nextDay(Date arg) {
    return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
  }
}

引入本地扩展(Introduce Local Extension)

问题

你需要为服务类提供一些额外函数,但你无法修改这个类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

中间人

中间人(Middle Man)

如果一个类的作用仅仅是指向另一个类的委托,为什么要存在呢?

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

对象的基本特征之一就是封装:对外部世界隐藏其内部细节。封装往往伴随委托。但是人们可能过度运用委托。比如,你也许会看到一个类的大部分有用工作都委托给了其他类,类本身成了一个空壳,除了委托之外不做任何事情。

解决方法

应该运用 移除中间人(Remove Middle Man),直接和真正负责的对象打交道。

收益

  • 减少笨重的代码。

Java开发中存在这样的代码,反而影响整体整洁和可读性

何时忽略

如果是以下情况,不要删除已创建的中间人:

  • 添加中间人是为了避免类之间依赖关系。
  • 一些设计模式有目的地创建中间人(例如代理模式和装饰器模式)。

重构方法说明

移除中间人(Remove Middle Man)

问题

某个类做了过多的简单委托动作。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

让客户直接调用委托类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

依恋情结

依恋情结(Feature Envy)

一个函数访问其它对象的数据比访问自己的数据更多。

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

这种气味可能发生在字段移动到数据类之后。如果是这种情况,你可能想将数据类的操作移动到这个类中。

解决方法

As a basic rule, if things change at the same time, you should keep them in the same place. Usually data and functions that use this data are changed together (although exceptions are possible).

有一个基本原则:同时会发生改变的事情应该被放在同一个地方。通常,数据和使用这些数据的函数是一起改变的。

Java开发中存在这样的代码,反而影响整体整洁和可读性

  • 如果一个函数明显应该被移到另一个地方,可运用 搬移函数(Move Method) 。
  • 如果仅仅是函数的部分代码访问另一个对象的数据,运用 提炼函数(Extract Method) 将这部分代码移到独立的函数中。
  • 如果一个方法使用来自其他几个类的函数,首先确定哪个类包含大多数使用的数据。然后,将该方法与其他数据一起放在此类中。或者,使用 提炼函数(Extract Method) 将方法拆分为几个部分,可以放置在不同类中的不同位置。

收益

  • 减少重复代码(如果数据处理的代码放在中心位置)。
  • 更好的代码组织性(处理数据的函数靠近实际数据)。

Java开发中存在这样的代码,反而影响整体整洁和可读性

何时忽略

  • 有时,行为被有意地与保存数据的类分开。这通常的优点是能够动态地改变行为(见策略设计模式,访问者设计模式和其他模式)。

重构方法说明

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

Java开发中存在这样的代码,反而影响整体整洁和可读性

提炼函数(Extract Method)

问题

你有一段代码可以组织在一起。

void printOwing() {
  printBanner();

  //print details
  System.out.println("name: " + name);
  System.out.println("amount: " + getOutstanding());
}

解决

移动这段代码到一个新的函数中,使用函数的调用来替代老代码。

void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

void printDetails(double outstanding) {
  System.out.println("name: " + name);
  System.out.println("amount: " + outstanding);
}

狎昵关系

狎昵关系(Inappropriate Intimacy)

一个类大量使用另一个类的内部字段和方法。

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

类和类之间应该尽量少的感知彼此(减少耦合)。这样的类更容易维护和复用。

解决方法

  • 最简单的解决方法是运用 搬移函数(Move Method) 和 搬移字段(Move Field) 来让类之间斩断羁绊。

 

Java开发中存在这样的代码,反而影响整体整洁和可读性

  • 你也可以看看是否能运用 将双向关联改为单向关联(Change Bidirectional Association to Unidirectional) 让其中一个类对另一个说分手。

  • 如果这两个类实在是情比金坚,难分难舍,可以运用 提炼类(Extract Class) 把二者共同点提炼到一个新类中,让它们产生爱的结晶。或者,可以尝试运用 隐藏委托关系(Hide Delegate) 让另一个类来为它们牵线搭桥。

  • 继承往往造成类之间过分紧密,因为子类对超类的了解总是超过后者的主观愿望,如果你觉得该让这个子类自己闯荡,请运用 以委托取代继承(Replace Inheritance with Delegation) 来让超类和子类分家。

收益

  • 提高代码组织性。
  • 提高代码复用性。

Java开发中存在这样的代码,反而影响整体整洁和可读性

重构方法说明

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

Java开发中存在这样的代码,反而影响整体整洁和可读性

搬移字段(Move Field)

问题

在你的程序中,某个字段被其所驻类之外的另一个类更多地用到。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在目标类新建一个字段,修改源字段的所有用户,令他们改用新字段。

Java开发中存在这样的代码,反而影响整体整洁和可读性

将双向关联改为单向关联(Change Bidirectional Association to Unidirectional)

问题

两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

去除不必要的关联。

Java开发中存在这样的代码,反而影响整体整洁和可读性

提炼类(Extract Class)

问题

某个类做了不止一件事。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

建立一个新类,将相关的字段和函数从旧类搬移到新类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

隐藏委托关系(Hide Delegate)

问题

客户通过一个委托类来调用另一个对象。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在服务类上建立客户所需的所有函数,用以隐藏委托关系。

Java开发中存在这样的代码,反而影响整体整洁和可读性

以委托取代继承(Replace Inheritance with Delegation)

问题

某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系。

Java开发中存在这样的代码,反而影响整体整洁和可读性

过度耦合的消息链

过度耦合的消息链(Message Chains)

消息链的形式类似于:obj.getA().getB().getC()

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。实际代码中你看到的可能是一长串 getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航紧密耦合。一旦对象间关系发生任何变化,客户端就不得不做出相应的修改。

解决方法

  • 可以运用 隐藏委托关系(Hide Delegate) 删除一个消息链。

Java开发中存在这样的代码,反而影响整体整洁和可读性

  • 有时更好的选择是:先观察消息链最终得到的对象是用来干什么的。看看能否以 提炼函数(Extract Method)把使用该对象的代码提炼到一个独立函数中,再运用 搬移函数(Move Method) 把这个函数推入消息链。

收益

  • 能减少链中类之间的依赖。
  • 能减少代码量。

Java开发中存在这样的代码,反而影响整体整洁和可读性

何时忽略

  • 过于侵略性的委托可能会使程序员难以理解功能是如何触发的。

重构方法说明

隐藏委托关系(Hide Delegate)

问题

客户通过一个委托类来调用另一个对象。

Java开发中存在这样的代码,反而影响整体整洁和可读性

 

解决

在服务类上建立客户所需的所有函数,用以隐藏委托关系。

Java开发中存在这样的代码,反而影响整体整洁和可读性

提炼函数(Extract Method)

问题

你有一段代码可以组织在一起。

void printOwing() {
  printBanner();

  //print details
  System.out.println("name: " + name);
  System.out.println("amount: " + getOutstanding());
}

解决

移动这段代码到一个新的函数中,使用函数的调用来替代老代码。

void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

void printDetails(double outstanding) {
  System.out.println("name: " + name);
  System.out.println("amount: " + outstanding);
}

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

Java开发中存在这样的代码,反而影响整体整洁和可读性

socket协议介绍

mumupudding阅读(7)

  本文主要讲述了Socket协议脚本的基础知识和编写方法,让大家能够在短时间内快速掌握简单的Socket协议脚本的编写方法。
  
  1.socket协议介绍
  
  Socket协议有万能协议之称,很多系统底层都是用的socket协议,用处十分广泛。
  
  1.1 Socket通讯方式说明
  
  与socket通讯有两种方式,一种是建立长连接,建立后不停的发送,接收;另一种是建立短连接,即建立连接发送报文,接收报文关闭连接
  
  1.2 Socket协议发送的buf类型介绍
  
  Send buffer类型分为字符串和xml类型
  
  1.3 Socket协议脚本编写前提:
  
  与项目组沟通,确认是否是socket协议,由项目组提供服务器IP和端口号还有socket协议交易的报文发送及接收报文对,及交易接口文档,了解清楚报文的数据长度,参数化字段,结构,代表什么等,了解清楚后进行socket协议脚本的开发。
  
  1.4、Socket协议脚本函数说明及实例:
  
  1)名称 lrs_create_socket();
  
  创建socket连接,添加IP和端口号,如果创建成功返回值为0,反之则返回为非0数值。(对于长连接,建立socket连接放在vuser_init函数中,短连接放在Action中即可) 实例: lrs_create_socket(“socket0″,”TCP”,”RemoteHost=180.170.150.230:7700″,  LrsLastArg);
  
  2)名称 lrs_send();
  
  发送socket请求消息,取缓冲区buf0的报文并发送。
  
  实例: lrs_send(“socket0″,”buf1”,LrsLastArg);
  
  3)名称 lrs_receive();
  
  接收socket的响应报文,放置buf1中。
  
  实例:lrs_receive(“socket0″,”buf2”,LrsLastArg);
  
  4)名称 lrs_get_last_received_buffer();
  
  获取最后收到的buffer和大小,其中将最后收到的buffer的值赋给RecvBuf变量,将大小赋值给RecvLen。
  
  实例:   lrs_get_last_received_buffer(“socket0”,&recvBuf,&recvLen);
  
  5)名称 lrs_free_buffer();
  
  为防止内存泄露,释放内存空间。
  
  实例:  lrs_free_buffer(recvBuf);
  
  6)名称 lrs_close_socket();
  
  关闭Socket连接,(对于长连接,关闭socket连接应放在vuser_end函数中)
  
  实例:  lrs_close_socket(“socket0”);
  
  其他常用的Socket函数:
  
  lrs_set_send_buffer(“socket0”, sSendPkg, iLenOfPkg );//指定要发送的socket信息
  
  lrs_get_buffer_by_name(“buf0”, sSendPkg, iLenOfPkg);// 获取收到的buffer和大小
  
  lrs_length_send(“socket0″,”buf0″,1,”Size=4″,”Encoding=1”,LrsLastArg);
  
  关联函数:
  
  lrs_save_param_ex(“socket0″,”received”,””,151,7,”ascii”,”response”);//取指定位置字符串保存到变量,以便判断事务是否成功
  
  lrs_save_searched_string();//在指定位置搜索字符串,将出现的字符串报错到参数中
  
  超时函数
  
  lrs_set_connect_timeout();//设置连接超时时间
  
  lrs_set_recv_timeout();//设置服务器响应超时时间
  
  lrs_set_recv_timeout2();//设置接收超时时间,使系统不去检查回收的内容是否一致
  
  2、Socket脚本编写
  
  2.1 简单划分步骤
  
  这种方法是我无意在一片文章中看到的,总体说来,比较简单。就像把大象放进冰箱一样,总共分三步:
  
  第一步:把冰箱门打开
  
  //建立到服务端的连接
  
  rc =    lrs_create_socket(“socket0”, “TCP”, “LocalHost=0”, “RemoteHost=128.64.64.23:8988”, LrsLastArg);
  
  if (rc==0)
  
  lr_output_message(“Socket  was successfully created “);
  
  else
  
  lr_output_message(“An error occurred while creating the socket, Error Code: %d”, rc);
  
  第二步:把大象装进去
  
  lrs_send(“socket0”, “buf0”, LrsLastArg);   //往”socket0″发送”buf0″中的数据
  
  lrs_receive(“socket0”, “buf1”, LrsLastArg);//将”socke0″中返回的数据存放到”buf1″中
  
  第三步:把冰箱门带上
  
  //关闭连接
  
  lrs_close_socket(“socket0”);
  
  2.2 详细划分步骤
  
  ◇变量的声明与定义
  
  ◇ 创建socket连接
  
  ◇ 发送socket请求
  
  ◇ 接收socket响应
  
  ◇ 从返回Buffer 中抽取结果
  
  ◇ 结果判断
  
  ◇ 释放内存
  
  ◇ 断开连接。
  
  2.3 实例脚本
  
  下面我们就是用一个实际项目不同报文格式的脚本进行讲解;
  
  若项目是短连接,且报文不是从文件中读取信息时,vuser_init和vuser_end部分默认即可,主要在Action中开发测试脚本和在data.ws中传输数据到Action的代码中。
  
  Vuser_init.c
  
  #include   “lrs.h”
  
  vuser_init()
  
  {
  
  lrs_startup(257);
  
  return 0;
  
  }
  
  Action.c
  
  #include “lrs.h”
  
  Action()
  
  {
  
  int rc,rv;//保存连接成功返回值
  
  char *recvBuf;//保存接收数据的内容
  
  int recvLen;//保存接收数据的大小
  
  /*对于长连接,建立socket连接放在vuser_init函数中,短连接放在Action中即可*/
  
  rc=lrs_create_socket(“socket0″,”TCP”,”RemoteHost=IP:端口”,  LrsLastArg);
  
  //判断连接是否创建成功
  
  if(rc==0){
  
  lr_output_message(“Socket连接创建成功”);
  
  }
  
  else{
  
  lr_error_message(“Socket连接创建失败!错误码=%d”,rc);
  
  return -1;
  
  }
  
  lr_start_transaction(“XXXX_1234_FCX”);//事务开始
  
  //发送socket请求消息(数据包内容放在data.ws中)
  
  lrs_send(“socket0”, “buf0”, LrsLastArg); //取缓冲区buf0的报文并发送
  
  rv = lrs_receive(“socket0”, “buf1”, LrsLastArg);//接收响应报文
  
  if(rv==0){
  
  lr_output_message(“Socket接收返回消息成功”);
  
  }
  
  else{
  
  lr_error_message(“Socket接收返回消息失败!错误码=%d”,rv);
  
  return -1;
  
  }
  
  //获取最后收到的buffer和大小
  
  lrs_get_last_received_buffer(“socket0”,&recvBuf,&recvLen);
  
  /*设置检查点,验证返回数据是否成功,这个根据各交易具体情况进行判断,以下示例是通过返回报文的长度大于3即为成功*/
  
  if(recvLen>3){
  
  lr_end_transaction(“XXXX_1234_FCX “,PASS);
  
  }
  
  else{
  
  lr_end_transaction(“XXXX_1234_FCX “,FAIL);
  
  lr_error_message(“XXXX_8550_FCX Fail!出错信息:[%s]”, recvBuf);//交易失败时,输出RecvBuf返回信息,用于排查出错原因
  
  }
  
  lrs_free_buffer(recvBuf); //释放recvBuf内存空间,否则会引起内存泄露
  
  /*关闭Socket连接,对于长连接,关闭socket连接应放在vuser_end函数中*/
  
  lrs_close_socket(“socket0”);
  
  return 0;
  
  AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(JobService.class);
  
  for (String beanname:context.getBeanDefinitionNames())
  
  {
  
  System.out.println(“——–“+beanname);
  
  }
  
  System.out.println(“context.getBean(JobService.class) = ” + context.getBean(JobService.class));
  
  复制代码
  
  这点代码很简单  初始化bean,然后再来拿bean,我们点进AnnotationConfigApplicationContext来看
  
  复制代码
  
  public AnnotationConfigApplicationContext(Class<?>… annotatedClasses)
  
  {
  
  this();
  
  register(annotatedClasses);
  
  refresh();
  
  }
  
  复制代码
  
  进⼊之后 会调用 this()默认无参构造方法
  
  public AnnotationConfigApplicationContext() {
  
  this.reader = new AnnotatedBeanDefinitionReader(this);
  
  this.scanner = new www.tianjiuyule178.com  ClassPathBeanDefinitionScanner(this);
  
  }
  
  调⽤这个⽆参构造⽅法的同时 他会调用⽗类的构造方法,在调用父类构造⽅方法时 他new了一个对象
  
  public GenericApplicationContext() {
  
  this.beanFactory = new DefaultListableBeanFactory();
  
  }
  
  也就是 DefaultListableBeanFactory,当然 这个就是所谓我们平常所说的 bean工厂,其父类就是 BeanFactory,BeanFactory有很多子类,DefaultListableBeanFactory就是其中一个⼦类。 那么 bean的⽣命周期是围绕那个⽅法呢,就是refresh()⽅法。也就是bean的整个生命周期是围绕refresh() 来进行的
  
  在refresh()我们可以看到
  
  复制代码
  
  public void refresh() throws BeansException, IllegalStateException {
  
  synchronized (this.startupShutdownMonitor)www.dayuzaixianyL.cn {
  
  // 准备好刷新上下文.
  
  prepareRefresh();
  
  // 返回一个Factory 为什么需要返回一个工厂  因为要对工厂进行初始化
  
  ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  
  // 准备bean工厂,以便在此上下文中使用。
  
  prepareBeanFactory(beanFactory);
  
  try {
  
  // 允许在上下文子类中对bean工厂进行后处理。 在spring5  并未对此接口进行实现
  
  postProcessBeanFactory(beanFactory);
  
  // 在spring的环境中去执行已经被注册的 Factory processors
  
  //设置执行自定义的postProcessBeanFactory和spring内部自己定义的
  
  invokeBeanFactoryPostProcessors(beanFactory);
  
  // 注册postProcessor
  
  registerBeanPostProcessors(beanFactory);
  
  // 初始化此上下文的消息源。
  
  initMessageSource();
  
  // 初始化此上下文的事件多播程序。
  
  initApplicationEventMulticaster();
  
  // 在特定上下文子类中初始化其他特殊bean。
  
  onRefresh();
  
  //检查侦听器bean并注册它们。
  
  registerListeners();
  
  // 实例化所有剩余的(非懒加载)单例。
  
  //new 单例对象
  
  finishBeanFactoryInitialization(beanFactory);
  
  // 最后一步:发布相应的事件
  
  finishRefresh();
  
  }
  
  catch (BeansException ex) {
  
  if (logger.isWarnEnabled()) {
  
  logger.warn(“Exception encountered during context initialization – ” +
  
  ”cancelling refresh attempt: ” + ex);
  
  }
  
  // Destroy already created singletons to avoid dangling resources.
  
  destroyBeans(www.hnxinhe.cn);
  
  // Reset ‘active’ flag.
  
  cancelRefresh(ex);
  
  // Propagate exception to caller.
  
  throw ex;
  
  }
  
  finally {
  
  // Reset common introspection caches in Spring’s core, since we
  
  // might not ever need metadata for singleton beans anymore…
  
  resetCommonCaches();
  
  }
  
  }
  
  }
  
  复制代码
  
  那么这里面最重要就是finishBeanFactoryInitialization(beanFactory);这个方法就是描述 spring的一个bean如何初始化
  
  复制代码
  
  protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
  
  // Initialize conversion service for this context.
  
  if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
  
  beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
  
  beanFactory.setConversionService(
  
  beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
  
  }
  
  // Register a default embedded value resolver if no bean post-processor
  
  // (such as a PropertyPlaceholderConfigurer bean) registered any before:
  
  // at this point, primarily for resolution in annotation attribute values.
  
  if (!beanFactory.hasEmbeddedValueResolver(www.yunyouuyL.com)) {
  
  beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment(www.chenghgongs.com).resolvePlaceholders(strVal));
  
  }
  
  // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
  
  String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
  
  for (String weaverAwareName : weaverAwareNames) {
  
  getBean(weaverAwareName);
  
  }
  
  // Stop using the temporary ClassLoader for type matching.
  
  beanFactory.setTempClassLoader(null);
  
  // Allow for caching all bean definition metadata, not expecting further changes.
  
  beanFactory.freezeConfiguration();
  
  // 实例化所有单例对象
  
  beanFactory.preInstantiateSingletons();
  
  }
  
  复制代码
  
  可以看到前面是一些判断 最重要的就是最后一个方法 beanFactory.preInstantiateSingletons();我们看下preInstantiateSingletons()方法,它是ConfigurableListableBeanFactory这个接口的一个方法 我们直接来看这个接口的实现 是由DefaultListableBeanFactory这个类 来实现
  
  复制代码
  
  @Override
  
  public void preInstantiateSingletons() throws BeansException {
  
  if (logger.isDebugEnabled()) {
  
  logger.debug(“Pre-instantiating singletons in www.baihuiyulegw.com ” + this);
  
  }
  
  // Iterate over a copy to allow for init methods which in turn register new bean definitions.
  
  // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
  
  //所有bean的名字
  
  List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
  
  // Trigger initialization of all non-lazy singleton beans…
  
  for (String beanName : beanNames) {
  
  RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
  
  if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
  
  if (isFactoryBean(beanName)) {
  
  Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
  
  if (bean instanceof FactoryBean) {
  
  final FactoryBean<?> factory = (FactoryBean<?>) bean;
  
  boolean isEagerInit;
  
  if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
  
  isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
  
  ((SmartFactoryBean<www.tianscpt.com>) factory)::isEagerInit,
  
  getAccessControlContext());
  
  }
  
  else {
  
  isEagerInit = (factory instanceof SmartFactoryBean &&
  
  ((SmartFactoryBean<?>) factory).isEagerInit());
  
  }
  
  if (isEagerInit) {
  
  getBean(beanName);
  
  }
  
  }
  
  }
  
  else {
  
  getBean(beanName);
  
  data.ws
  
  1)XML报文格式
  
  ;WSRData 2 1
  
  send  buf0 360
  
  ”<?xmlversion=\”1.0\”encoding=\”GBK\”?>”
  
  ”<TRANINFO>”
  
  ”<HEAD>”
  
  ”<TransCode>S001</TransCode>”
  
  ”<TransDate>20170613</TransDate>”
  
  ”<TransTime>144206</TransTime>”
  
  ”<TransNo>21219603</TransNo>”
  
  ”<Operator>999088</Operator>”
  
  ”<TransInst>70090</TransInst>”
  
  ”</HEAD>”
  
  ”<MSG>”
  
  ”<CustomerID><userID></CustomerID>”//客户编号
  
  ”<Type>3</Type>”//查询类型
  
  ”<BusinessType>01</BusinessType>”//业务类型
  
  ”</MSG>”
  
  ”</TRANINFO>”
  
  recv buf1 300
  
  -1
  
  2)16进制报文格式
  
  ;WSRData 2 1
  
  send  buf0 32
  
  ”\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
  
  ” “
  
  ”\x00\x00\x00\x00″
  
  ”PID <tran>”
  
  recv  buf1 197
  
  ”\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
  
  ”\x1e\x00\x00\x00\x00″
  
  ”STW -1″
  
  ”\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
  
  ”\x1e\x00\x00\x00\x00″
  
  ”STT “
  
  ”\x1f”
  
  -1
  
  其中buf0代表发送的报文的名称,后跟的数字代码发送报文长度,其下放置发送报文;buf1代表接收报文的名称,后跟数字代表接收报文长度,其下放置接收报文。
  
  注意:该协议脚本参数化格式为:<参数名>

使用PriorityQueue实现LFU和LRU

mumupudding阅读(6)

LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高。

然后我先定义一个缓存对象MyCache,这里有一个泛型用来存对象,然后有一个useCount记录被使用次数,接下去是创建时间和最近使用时间。

使用PriorityQueue实现LFU和LRU

使用构造方法来初始化一些值:

使用PriorityQueue实现LFU和LRU

因为使用了Priorityqueue所以要实现Comparable接口,而且更具useCount来比较

使用PriorityQueue实现LFU和LRU

然后我们就可以进入下一步操作啦,定义一个PriorityQueue作为全局变量,这个限制一些最大缓存的数量,还有用一个object用来做同步块,队列中如果没有就加入队列中,这里就先不写set的情况,

使用PriorityQueue实现LFU和LRU

然后根据优先队列的特性来删除哪些不常用的元素,因为根据排序,优先队列会把我们最想要的数据放在头部,所以调用poll方法,就可以实现删除最少使用的那一个,当然这里我可以选择删除固定数量的不常用数据,或者删除一定比例的数据,这个使用中可以用定时器或者分布式任务来清除缓存。

使用PriorityQueue实现LFU和LRU

因为考虑哪些数据相等,我这里重写了hashCode和equals方法,这里只是调用缓存对象的比较,但是对于那些没有重写的缓存对象T,可能就会有问题啦,所以可能有的设计就会用序列化后的数据。

使用PriorityQueue实现LFU和LRU

因为PriorityQueue只是做了一次的排序,所以我需要在用户修改后的数据也要更新顺序,所以我想把原来的数据删了,然后再添加一词,使用次数加1,最近使用时间也更新啦,只是这里我遍历了一边队列,效率比较低,只是说这里先考虑这个最近不常用淘汰,实际过程肯定是查询为主。

使用PriorityQueue实现LFU和LRU

然后我们测试下:

使用PriorityQueue实现LFU和LRU

输出:

使用PriorityQueue实现LFU和LRU

 

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

那么我想就是把MyCache里面的排序根据时间来指定就行了,最近最久远的排在队列的前面淘汰掉。

当然这里只是想用PriorityQueue写着玩!

未完待续

https://github.com/woshiyexinjie/algorithm-xin

Java 高并发解决方案(电商的秒杀和抢购)

mumupudding阅读(8)

电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要。这次我们会关注秒杀和抢购的技术实现和优化,同时,从技术层面揭开,为什么我们总是不容易抢到火车票的原因? 

一、大规模并发带来的挑战 

在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻而易举地陷入到异常状态。我们现在一起来讨论下,优化的思路和方法哈。 

1. 请求接口的合理设计

一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口。

通常静态HTML等内容,是通过CDN的部署,一般压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须能够支持高并发请求,同时,非常重要的一点,必须尽可能“快”,在最短的时间里返回用户的请求结果。为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。仍然直接面向MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采用异步写入。

当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。

2. 高并发的挑战:一定要“快”

我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

然后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

二、作弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。不少用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可能多的请求到服务器。还有一部分高级用户,制作强大的自动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,自己的请求数目占比越多,成功的概率越高。

这些都是属于“作弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。

1. 同一个账号,一次性发出多个请求

部分用户通过浏览器的插件或者其他工具,在秒杀开始的时间里,以自己的账号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这种请求在某些没有做数据安全处理的系统里,也可能造成另外一种破坏,导致某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。这是个非常简单的逻辑,但是,在高并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内网的多台Web服务器,它们首先向存储发送查询请求,然后,在某个请求成功写入参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。

 

应对方案:

在程序入口处,一个账号只允许接受1个请求,其他请求过滤。不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加。

或者,自己实现一个服务,将同一个账号的请求放入一个队列中,处理完一个,再处理下一个。

2. 多个账号,一次性发送多个请求

很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵尸号”去混进去转发,这样就可以大大提升我们中奖的概率。

这种账号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。

应对方案:

这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求:

  1. 弹出验证码,最核心的追求,就是分辨出真实用户。因此,大家可能经常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让我们根本无法看清。他们这样做的原因,其实也是为了让验证码的图片不被轻易识别,因为强大的“自动脚本”可以通过图片识别里面的字符,然后让脚本自动填写验证码。实际上,有一些非常创新的验证码,效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。
  2. 直接禁止IP,实际上是有些粗暴的,因为有些真实用户的网络场景恰好是同一出口IP的,可能会有“误伤“。但是这一个做法简单高效,根据实际场景使用可以获得很好的效果。

 

3. 多个账号,不同IP发送不同请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP。=

有同学会好奇,这些随机IP服务怎么来的。有一些是某些机构自己占据一批独立IP,然后做成一个随机代理IP的服务,有偿提供给这些“工作室”使用。还有一些更为黑暗一点的,就是通过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运作,只做一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。通过这种做法,黑客就拿到了大量的独立IP,然后搭建为随机IP服务,就是为了挣钱。

应对方案:

说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想做分辨很困难。再做进一步的限制很容易“误伤“真实用户,这个时候,通常只能通过设置业务门槛高来限制这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们。

僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号。

4. 火车票的抢购

看到这里,同学们是否明白你为什么抢不到火车票?如果你只是老老实实地去抢票,真的很难。通过多账号的方式,火车票的黄牛将很多车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。

高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展示验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制作用被废除了,目前也没有很好的解决方案。

因为火车票是根据身份证实名制的,这里还有一个火车票的转让操作方式。大致的操作方式,是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛账号选择退票,然后黄牛买家成功通过自己的身份证购票成功。当一列车厢没有票了的时候,是没有很多人盯着看的,况且黄牛们的抢票工具也很强大,即使让我们看见有退票,我们也不一定能抢得过他们哈。 

最终,黄牛顺利将火车票转移到买家的身份证下。

解决方案:

并没有很好的解决方案,唯一可以动心思的也许是对账号数据进行“数据挖掘”,这些黄牛账号也是有一些共同特征的,例如经常抢票和退票,节假日异常活跃等等。将它们分析出来,再做进一步处理和甄别。

三、高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

2. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

4. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

5. 缓存服务器 

Redis分布式要保证数据都能能够平均的缓存到每一台机器,首先想到的做法是对数据进行分片,因为Redis是key-value存储的,首先想到的是Hash分片,可能的做法是对key进行哈希运算,得到一个long值对分布式的数量取模会得到一个一个对应数据库的一个映射,没有读取就可以定位到这台数据库

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

四、小结

互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得越来越多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然我们解决问题的具体技术方案可能千差万别,但是遇到的挑战却是相似的,因此解决问题的思路也异曲同工。

个人整理并发解决方案。

     a.应用层面:读写分离、缓存、队列、集群、令牌、系统拆分、隔离、系统升级(可水平扩容方向)。

     b.时间换空间:降低单次请求时间,这样在单位时间内系统并发就会提升。

     c.空间换时间:拉长整体处理业务时间,换取后台系统容量空间。

EasyScheduler的架构原理及实现思路

mumupudding阅读(7)


系统架构设计

在对调度系统架构说明之前,我们先来认识一下调度系统常用的名词

1.名词解释

DAG: 全称Directed Acyclic Graph,简称DAG。工作流中的Task任务以有向无环图的形式组装起来,从入度为零的节点进行拓扑遍历,直到无后继节点为止。举例如下图:DAG示例

流程定义:通过拖拽任务节点并建立任务节点的关联所形成的可视化DAG

流程实例:流程实例是流程定义的实例化,可以通过手动启动或定时调度生成

任务实例:任务实例是流程定义中任务节点的实例化,标识着具体的任务执行状态

任务类型: 目前支持有SHELL、SQL、SUB_PROCESS、PROCEDURE、MR、SPARK、PYTHON、DEPENDENT,同时计划支持动态插件扩展,注意:其中子 SUB_PROCESS 也是一个单独的流程定义,是可以单独启动执行的

调度方式: 系统支持基于cron表达式的定时调度和手动调度。命令类型支持:启动工作流、从当前节点开始执行、恢复被容错的工作流、恢复暂停流程、从失败节点开始执行、补数、调度、重跑、暂停、停止、恢复等待线程。其中 恢复被容错的工作流恢复等待线程 两种命令类型是由调度内部控制使用,外部无法调用

定时调度:系统采用 quartz 分布式调度器,并同时支持cron表达式可视化的生成

依赖:系统不单单支持 DAG 简单的前驱和后继节点之间的依赖,同时还提供任务依赖节点,支持流程间的自定义任务依赖

优先级 :支持流程实例和任务实例的优先级,如果流程实例和任务实例的优先级不设置,则默认是先进先出

邮件告警:支持 SQL任务 查询结果邮件发送,流程实例运行结果邮件告警及容错告警通知

失败策略:对于并行运行的任务,如果有任务失败,提供两种失败策略处理方式,继续是指不管并行运行任务的状态,直到流程失败结束。结束是指一旦发现失败任务,则同时Kill掉正在运行的并行任务,流程失败结束

补数:补历史数据,支持区间并行和串行两种补数方式

2.系统架构

2.1 系统架构图

系统架构图

2.2 架构说明

  • MasterServer

    MasterServer采用分布式无中心设计理念,MasterServer主要负责 DAG 任务切分、任务提交监控,并同时监听其它MasterServer和WorkerServer的健康状态。MasterServer服务启动时向Zookeeper注册临时节点,通过监听Zookeeper临时节点变化来进行容错处理。

    该服务内主要包含:
    • Distributed Quartz分布式调度组件,主要负责定时任务的启停操作,当quartz调起任务后,Master内部会有线程池具体负责处理任务的后续操作

    • MasterSchedulerThread是一个扫描线程,定时扫描数据库中的 command 表,根据不同的命令类型进行不同的业务操作

    • MasterExecThread主要是负责DAG任务切分、任务提交监控、各种不同命令类型的逻辑处理

    • MasterTaskExecThread主要负责任务的持久化

  • WorkerServer

    WorkerServer也采用分布式无中心设计理念,WorkerServer主要负责任务的执行和提供日志服务。WorkerServer服务启动时向Zookeeper注册临时节点,并维持心跳。

    该服务包含:
    • FetchTaskThread主要负责不断从Task Queue中领取任务,并根据不同任务类型调用TaskScheduleThread对应执行器。

    • LoggerServer是一个RPC服务,提供日志分片查看、刷新和下载等功能

  • ZooKeeper

    ZooKeeper服务,系统中的MasterServer和WorkerServer节点都通过ZooKeeper来进行集群管理和容错。另外系统还基于ZooKeeper进行事件监听和分布式锁。我们也曾经基于Redis实现过队列,不过我们希望EasyScheduler依赖到的组件尽量地少,所以最后还是去掉了Redis实现。

  • Task Queue

    提供任务队列的操作,目前队列也是基于Zookeeper来实现。由于队列中存的信息较少,不必担心队列里数据过多的情况,实际上我们压测过百万级数据存队列,对系统稳定性和性能没影响。

  • Alert

    提供告警相关接口,接口主要包括告警两种类型的告警数据的存储、查询和通知功能。其中通知功能又有邮件通知和**SNMP(暂未实现)**两种。

  • API

    API接口层,主要负责处理前端UI层的请求。该服务统一提供RESTful api向外部提供请求服务。接口包括工作流的创建、定义、查询、修改、发布、下线、手工启动、停止、暂停、恢复、从该节点开始执行等等。

  • UI

    系统的前端页面,提供系统的各种可视化操作界面,详见**系统使用手册**部分。

2.3 架构设计思想

一、去中心化vs中心化
中心化思想

中心化的设计理念比较简单,分布式集群中的节点按照角色分工,大体上分为两种角色:master-slave角色

  • Master的角色主要负责任务分发并监督Slave的健康状态,可以动态的将任务均衡到Slave上,以致Slave节点不至于“忙死”或”闲死”的状态。
  • Worker的角色主要负责任务的执行工作并维护和Master的心跳,以便Master可以分配任务给Slave。

中心化思想设计存在的问题:

  • 一旦Master出现了问题,则群龙无首,整个集群就会崩溃。为了解决这个问题,大多数Master/Slave架构模式都采用了主备Master的设计方案,可以是热备或者冷备,也可以是自动切换或手动切换,而且越来越多的新系统都开始具备自动选举切换Master的能力,以提升系统的可用性。
  • 另外一个问题是如果Scheduler在Master上,虽然可以支持一个DAG中不同的任务运行在不同的机器上,但是会产生Master的过负载。如果Scheduler在Slave上,则一个DAG中所有的任务都只能在某一台机器上进行作业提交,则并行任务比较多的时候,Slave的压力可能会比较大。
去中心化

去中心化

  • 在去中心化设计里,通常没有Master/Slave的概念,所有的角色都是一样的,地位是平等的,全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备down机,都只会影响很小范围的功能。

  • 去中心化设计的核心设计在于整个分布式系统中不存在一个区别于其他节点的”管理者”,因此不存在单点故障问题。但由于不存在” 管理者”节点所以每个节点都需要跟其他节点通信才得到必须要的机器信息,而分布式系统通信的不可靠行,则大大增加了上述功能的实现难度。

  • 实际上,真正去中心化的分布式系统并不多见。反而动态中心化分布式系统正在不断涌出。在这种架构下,集群中的管理者是被动态选择出来的,而不是预置的,并且集群在发生故障的时候,集群的节点会自发的举行"会议"来选举新的"管理者"去主持工作。最典型的案例就是ZooKeeper及Go语言实现的Etcd。

  • EasyScheduler的去中心化是Master/Worker注册到Zookeeper中,实现Master集群和Worker集群无中心,并使用Zookeeper分布式锁来选举其中的一台Master或Worker为“管理者”来执行任务。

二、分布式锁实践

EasyScheduler使用ZooKeeper分布式锁来实现同一时刻只有一台Master执行Scheduler,或者只有一台Worker执行任务的提交。

  1. 获取分布式锁的核心流程算法如下

获取分布式锁流程

  1. EasyScheduler中Scheduler线程分布式锁实现流程图:获取分布式锁流程
三、线程不足循环等待问题
  • 如果一个DAG中没有子流程,则如果Command中的数据条数大于线程池设置的阈值,则直接流程等待或失败。
  • 如果一个大的DAG中嵌套了很多子流程,如下图则会产生“死等”状态:

线程不足循环等待问题上图中MainFlowThread等待SubFlowThread1结束,SubFlowThread1等待SubFlowThread2结束, SubFlowThread2等待SubFlowThread3结束,而SubFlowThread3等待线程池有新线程,则整个DAG流程不能结束,从而其中的线程也不能释放。这样就形成的子父流程循环等待的状态。此时除非启动新的Master来增加线程来打破这样的”僵局”,否则调度集群将不能再使用。

对于启动新Master来打破僵局,似乎有点差强人意,于是我们提出了以下三种方案来降低这种风险:

  1. 计算所有Master的线程总和,然后对每一个DAG需要计算其需要的线程数,也就是在DAG流程执行之前做预计算。因为是多Master线程池,所以总线程数不太可能实时获取。
  2. 对单Master线程池进行判断,如果线程池已经满了,则让线程直接失败。
  3. 增加一种资源不足的Command类型,如果线程池不足,则将主流程挂起。这样线程池就有了新的线程,可以让资源不足挂起的流程重新唤醒执行。

注意:Master Scheduler线程在获取Command的时候是FIFO的方式执行的。

于是我们选择了第三种方式来解决线程不足的问题。

四、容错设计

容错分为服务宕机容错和任务重试,服务宕机容错又分为Master容错和Worker容错两种情况

1. 宕机容错

服务容错设计依赖于ZooKeeper的Watcher机制,实现原理如图:

EasyScheduler容错设计

其中Master监控其他Master和Worker的目录,如果监听到remove事件,则会根据具体的业务逻辑进行流程实例容错或者任务实例容错。

  • Master容错流程图:

Master容错流程图ZooKeeper Master容错完成之后则重新由EasyScheduler中Scheduler线程调度,遍历 DAG 找到”正在运行”和“提交成功”的任务,对”正在运行”的任务监控其任务实例的状态,对”提交成功”的任务需要判断Task Queue中是否已经存在,如果存在则同样监控任务实例的状态,如果不存在则重新提交任务实例。

  • Worker容错流程图:

Worker容错流程图

Master Scheduler线程一旦发现任务实例为” 需要容错”状态,则接管任务并进行重新提交。

注意:由于” 网络抖动”可能会使得节点短时间内失去和ZooKeeper的心跳,从而发生节点的remove事件。对于这种情况,我们使用最简单的方式,那就是节点一旦和ZooKeeper发生超时连接,则直接将Master或Worker服务停掉。

2.任务失败重试

这里首先要区分任务失败重试、流程失败恢复、流程失败重跑的概念:

  • 任务失败重试是任务级别的,是调度系统自动进行的,比如一个Shell任务设置重试次数为3次,那么在Shell任务运行失败后会自己再最多尝试运行3次
  • 流程失败恢复是流程级别的,是手动进行的,恢复是从只能从失败的节点开始执行从当前节点开始执行
  • 流程失败重跑也是流程级别的,是手动进行的,重跑是从开始节点进行

接下来说正题,我们将工作流中的任务节点分了两种类型。

  • 一种是业务节点,这种节点都对应一个实际的脚本或者处理语句,比如Shell节点,MR节点、Spark节点、依赖节点等。

  • 还有一种是逻辑节点,这种节点不做实际的脚本或语句处理,只是整个流程流转的逻辑处理,比如子流程节等。

每一个业务节点都可以配置失败重试的次数,当该任务节点失败,会自动重试,直到成功或者超过配置的重试次数。逻辑节点不支持失败重试。但是逻辑节点里的任务支持重试。

如果工作流中有任务失败达到最大重试次数,工作流就会失败停止,失败的工作流可以手动进行重跑操作或者流程恢复操作

五、任务优先级设计

在早期调度设计中,如果没有优先级设计,采用公平调度设计的话,会遇到先行提交的任务可能会和后继提交的任务同时完成的情况,而不能做到设置流程或者任务的优先级,因此我们对此进行了重新设计,目前我们设计如下:

  • 按照不同流程实例优先级优先于同一个流程实例优先级优先于同一流程内任务优先级优先于同一流程内任务提交顺序依次从高到低进行任务处理。
    • 具体实现是根据任务实例的json解析优先级,然后把流程实例优先级_流程实例id_任务优先级_任务id信息保存在ZooKeeper任务队列中,当从任务队列获取的时候,通过字符串比较即可得出最需要优先执行的任务

      • 其中流程定义的优先级是考虑到有些流程需要先于其他流程进行处理,这个可以在流程启动或者定时启动时配置,共有5级,依次为HIGHEST、HIGH、MEDIUM、LOW、LOWEST。如下图

流程优先级配置

    - 任务的优先级也分为5级,依次为HIGHEST、HIGH、MEDIUM、LOW、LOWEST。如下图

任务优先级配置

六、Logback和gRPC实现日志访问
  • 由于Web(UI)和Worker不一定在同一台机器上,所以查看日志不能像查询本地文件那样。有两种方案:

  • 将日志放到ES搜索引擎上

  • 通过gRPC通信获取远程日志信息

  • 介于考虑到尽可能的EasyScheduler的轻量级性,所以选择了gRPC实现远程访问日志信息。

grpc远程访问

  • 我们使用自定义Logback的FileAppender和Filter功能,实现每个任务实例生成一个日志文件。
  • FileAppender主要实现如下:
/** * task log appender */public class TaskLogAppender extends FileAppender<ILoggingEvent {    ...   @Override   protected void append(ILoggingEvent event) {       if (currentlyActiveFile == null){           currentlyActiveFile = getFile();       }       String activeFile = currentlyActiveFile;       // thread name: taskThreadName-processDefineId_processInstanceId_taskInstanceId       String threadName = event.getThreadName();       String[] threadNameArr = threadName.split("-");       // logId = processDefineId_processInstanceId_taskInstanceId       String logId = threadNameArr[1];       ...       super.subAppend(event);   }}

以/流程定义id/流程实例id/任务实例id.log的形式生成日志

  • 过滤匹配以TaskLogInfo开始的线程名称:

  • TaskLogFilter实现如下:

/***  task log filter*/public class TaskLogFilter extends Filter<ILoggingEvent {   @Override   public FilterReply decide(ILoggingEvent event) {       if (event.getThreadName().startsWith("TaskLogInfo-")){           return FilterReply.ACCEPT;       }       return FilterReply.DENY;   }}

总结

本文从调度出发,初步介绍了大数据分布式工作流调度系统–EasyScheduler的架构原理及实现思路。

使用lombok编写优雅的Bean对象

mumupudding阅读(9)

使用java编写代码,十之八九都是在写java类,从而构建java对象。lombok之前也说了不少,但使用了这么多年,感觉还是有很多技巧可以使用的。

毫无疑问,使用lombok,编写的java代码很优雅,而使用起来和普通的java编码方式创建的类毫无二致。

不过,这样就满足了吗?实际上lombok很多注解,让这个java类在使用的时候,也可以更优雅。

本文就从ORM实体类、Builder模式工具类、Wither工具类以及Accessors工具类几个层面对比一下。

首先说明,不同的方式本质上没有优劣之分,不过在不同的应用场景就会变得很奇妙了。

ORM实体类

当一个java Bean类作为ORM实体类,或者xml、json的映射类时,需要这个类有这几个特征:

  • 拥有无参构造器
  • 拥有setter方法,用以反序列化;
  • 拥有getter方法,用以序列化。

那么最简单的情况就是:

@Datapublic class UserBean{  private Integer id;  private String userName;}
  • 复习一下,Data 注解相当于装配了 @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode

那么,作为实体类、或者序列化的Bean类,足够用了。

Builder

构造器模式,在很多工具类中频繁的使用。

package com.pollyduan.builder;import lombok.Builder;@Builderpublic class UserBean {   private Integer id;   private String userName;}

它做了什么事?

  • 它创建了一个private 的全参构造器。也就意味着 无参构造器没有; 同时也意味着这个类不可以直接构造对象。
  • 它为每一个属性创建了一个同名的方法用于赋值,代替了setter,而该方法的返回值为对象本身。

使用一下:

UserBean u=UserBean.builder() .id(1001) .userName("polly") .build();System.out.println(u);

还不错,然并卵,由于这个Bean并没有getter方法,里边的数据没办法直接使用。光说没用,继续执行你会发现输出是这个东西:com.pollyduan.builder.UserBean@20322d26,连看都看不出是什么东东。因此,Builder提供了一个种可能性,实际使用还需要更多的东西。

那么,我们为了测试方便需要添加 @ToString() 注解,就会输出 UserBean(id=1001, userName=polly)

换一个思路,你可能想,我不添加ToString注解,我把他转成json输出:

UserBean u=UserBean.builder() .id(1001) .userName("polly") .build();ObjectMapper mapper=new ObjectMapper();System.out.println(mapper.writeValueAsString(u));

很不幸,你会收到下面的异常:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.pollyduan.builder.UserBean and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

看到 no properties discovered 了吧,没错,工具类无法找到属性,因为没有 getter,那么我们加上 @Getter 就可以了。

package com.pollyduan.builder;import lombok.Builder;import lombok.Getter;@Builder@Getterpublic class UserBean {   private Integer id;   private String userName;}

序列化为json可以了,那么从一个json反序列化为对象呢?

@Builder@Getter@Setterpublic class UserBean {   private Integer id;   private String userName;}

还是不行,如无意外,会遇到 com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance ofcom.pollyduan.builder.UserBean(no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

前面已经交代了,光增加 @Setter 还不够,他还需要一个无参构造器。那么,下面可以吗?

package com.pollyduan.builder;import lombok.Builder;import lombok.Data;@Builder@Datapublic class UserBean {   private Integer id;   private String userName;}

同样不行,因为虽然 Data使用的时候可以直接使用无参构造器,但由于 Builder 引入了全参构造器,那么按照java原生的规则,无参构造器就没有了。那么就再加一个无参构造器

@Builder@Data@NoArgsConstructor

很不幸,Builder又报错了,它找不到全参构造器了。好吧,最终的效果如下:

@Builder@Data@AllArgsConstructor(access = AccessLevel.PRIVATE)@NoArgsConstructor
  • 注意全参构造器的访问级别,不要破坏Builder的规则。

更进一步,看如下示例:

package com.pollyduan.builder;import java.util.List;import lombok.AccessLevel;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Builder@Data@AllArgsConstructor(access = AccessLevel.PRIVATE)@NoArgsConstructorpublic class UserBean {   private Integer id;   private String userName;   private List<String> addresses;}

思考一下,这个List 我们也需要在外面new 一个 ArrayList,然后build进去,使用起来并不舒服。lombok提供了另一个注解配合使用,那就是 @Singular,如下:

package com.pollyduan.builder;import java.util.List;import lombok.AccessLevel;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;@Builder@Data@AllArgsConstructor(access = AccessLevel.PRIVATE)@NoArgsConstructorpublic class UserBean {   private Integer id;   private String userName;   @Singular   private List<String> favorites;}

那么就可以这样操作这个列表了。

UserBean u = UserBean.builder() .id(1001) .userName("polly") .favorite("music") .favorite("movie") .build();

是不是很方便?同时还提供了一个 clearXXX方法,清空集合。

还有一个小坑,如果我们增加一个example属性,然后给它一个默认值:

@Builder@Data@AllArgsConstructor(access = AccessLevel.PRIVATE)@NoArgsConstructorpublic class UserBean {   private Integer id;   private String userName;   private String example="123456";}

测试一下看:

UserBean u = UserBean.builder() .id(1001) .userName("polly") .build();System.out.println(u.toString());

输出结果:UserBean(id=1001, userName=polly, example=null)

咦,不对呀?缺省值呢??这要从Builder的原理来解释,他实际上是分别设置了一套属性列表的值,然后使用全参构造器创建对象。那么,默认值在Bean上,不在Builder上,那么Builder没赋值,它的值就是null,最后把所有属性都复制给UserBean,从而null覆盖了默认值。

如何让Builder实体来有默认值呢?只需要给该字段增加 @Default 注解级可。

package com.pollyduan.builder;import lombok.AccessLevel;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Builder.Default;import lombok.Data;import lombok.NoArgsConstructor;@Builder@Data@AllArgsConstructor(access = AccessLevel.PRIVATE)@NoArgsConstructorpublic class UserBean {   private Integer id;   private String userName;   @Default   private String example="123456";}

重新执行测试用例,输出:UserBean(id=1001, userName=polly, example=123456),OK,没毛病了。

Wither

用wither方式构建对象,这在Objective-C 中比较多见。

适用的场景是,使用几个必要的参数构建对象,其他参数,动态的拼装。举个例子,我们构建一个ApiClient,它的用户名和密码是必须的,他的ApiService的地址有一个默认值,然后我们可以自己定制这个地址。

package com.pollyduan.wither;import lombok.AllArgsConstructor;import lombok.experimental.Wither;@Wither@AllArgsConstructor //WITHER NEED IT.public class ApiClient { private String appId; private String appKey; private String endpoint="http://api.pollyduan.com/myservice";}

如何使用呢?

@Testpublic void test1() { ApiClient client1=new ApiClient(null, null,null); System.out.println(client1); Object client2 = client1.withAppId("10001")  .withAppKey("abcdefg")  .withEndpoint("http://127.0.0.1/"); System.out.println(client2);}

默认的使用null去初始化一个对象还是很奇怪的。和 Builder一样,Wither也是提供了可能性,实际使用还需要调整一下。

我们可以设置一个必选参数的构造器,如下:

package com.pollyduan.wither;import lombok.AllArgsConstructor;import lombok.NonNull;import lombok.RequiredArgsConstructor;import lombok.experimental.Wither;@RequiredArgsConstructor@Wither@AllArgsConstructorpublic class ApiClient { @NonNull private String appId; @NonNull private String appKey; private String endpoint="http://api.pollyduan.com/myservice";}

这样使用时就可以这样:

 @Test public void test1() {  ApiClient client1=new ApiClient("10001", "abcdefgh");  System.out.println(client1);    Object client2 = client1.withEndpoint("http://127.0.0.1/");  System.out.println(client2); }

是不是优雅了很多?而且实际上使用时也使用链式语法:

ApiClient client1=new ApiClient("10001", "abcdefgh") withEndpoint("http://127.0.0.1/");

另外还有一个小细节,前面的示例输出如下:

com.pollyduan.wither.ApiClient@782830ecom.pollyduan.wither.ApiClient@470e2030

这个日志表明,with() 返回的对象并不是原来的对象,而是一个新对象,这很重要。

Accessors

访问器模式,是给一个普通的Bean增加一个便捷的访问器,包括读和写。

它有两种工作模式,fluent和chain,举例说明:

package com.pollyduan.accessors;import lombok.Data;import lombok.experimental.Accessors;@Accessors(fluent = true)@Datapublic class UserBean { private Integer id; private String userName; private String password; }

使用代码:

UserBean u=new UserBean() .id(10001) .userName("polly") .password("123456");u.userName("Tom");System.out.println(u.userName());

这和 Builder 类似,但更小巧,而且不影响属性的读写,只不过使用属性同名字符串代替了getter和setter。

另一个模式是 chain:

UserBean u=new UserBean() .setId(10001) .setUserName("polly") .setPassword("123456");u.setUserName("Tom");System.out.println(u.getUserName());

可以看得出来,这fluent的区别就在于使用getter和setter。

从B站的代码泄露事件中,我们能学到些什么?

mumupudding阅读(7)

先声明一下,本文不聊ISSUE中的七七八八,也不聊代码是否写的好,更不聊是不是跟蔡徐坤有关之类的吃瓜内容。仅站在技术人的角度,从这次的代码泄露事件,聊聊在代码的安全管理上,通常都需要做哪些事来预防此类事件的发生。同时,大家在阅读本文的时候,也可以深入思考下,自己团队是否也存在类似的问题,经过这次的事件,是否有必要针对性的做一些优化。

最小权限

“最小权限”原则是我们在学习Linux用户管理时候经常被提到,并且被反复强调的内容。该原则是指每个程序和系统用户都应该具有完成任务所必需的最小权限集合。赋予每一个合法动作最小的权限,就是为了保护数据以及功能避免受到错误或者恶意行为的破坏。

在代码的安全管理上,“最小权限”原则同样适用。但是,从此次的代码泄露内容中可以看到,这一点做的并不好,一起来看看源自泄露代码的README介绍:

从说明中,可以知道这是一个后端项目的大仓库,每个业务线都通过文件夹的方式管理自己的业务模块。那也就是说,每个业务模块的人都是可以看到这个大仓库的。继续看README最后的负责人信息:

可以看到这个大仓库中包含了非常多的业务模块与相关负责人信息。

由于Git的权限管理都是对整个仓库的,无法精细化到具体的文件夹。换言之,对于这个大仓库的访问,其实是有非常多的人员可以拿到全量代码的,而其中有大部分代码可能并不是自己业务线的内容。可见,这个仓库的内容,对于代码自身的保护上,并没有做到最小权限控制。所以,当出现代码泄漏的时候、对于泄露范围就很难控制。可能一个小环节的过失,就会导致非常大的泄漏灾难。

配置分离

配置与代码的分离,自云原生应用的流行开始,就一直被反复的强调。将配置内容隔离开之后,赋予代码的将仅仅是逻辑,对于各种与环境相关的敏感信息都应该被剥离出去,这就使得代码中将不再含有各种环境信息和敏感信息。同时,这么做也可以轻松的实现多环境的不同配置,这种实现方式与我们传统通过构建工具来实现的多环境是完全不同的。只有在严格分离了配置之后,才真正的可以实现:一次构建,多处运行的目标。基于构建工具实现的多环境部署,实质上多次构建,多处运行的结果,每个环境运行的介质只是上都不是同一个程序。

为什么要强调这一点呢?同样的,我们看看从网络上流出的一段代码片段:

如果这段代码是真实存在的话,那么配置管理和安全意识上确实就做得非常差了。

所以,对于配置中心的建设,大论大小团队,从安全角度上来说,都是非常重要的。何况在目前有大量开源配置中心的大背景之下,做到配置分离,是一件性价比非常高的事。如果做到这一点,那么即使代码有流出,对于重要密钥、数据库账户密码等等敏感信息也不会被泄露。

外部监控

任何与安全相关的内容,都少不了监控。事前的所有策略,最终都有可能被一一击破,留给我们最后的一道防线就是在事件发生之后马上得将其堵住,以防止问题的扩大,而造成更大的损失。

在笔者知道这件事的时候,距离代码上传已经有6小时之久了。各类技术讨论群中几乎也都是相关信息的八卦。几乎每一次刷新页面,都是几百Star的增长。虽然处理的速度不能说快,但是没过多久,与之相关的仓库都开始无法访问。至于,是不是有做代码泄露扫描的监控,我们不得而知。因为,在扫描告警之后,对于代码的扩散控制,作为B站来说,本身是被动的,还是需要Github等仓库运营方来完成。所以,这中间到底哪一步慢了,我们无法考证。

不过这些都不重要,这里主要强调一点,除了管理上的防护之外,必须留一手外部监控,以快速的发现泄漏并采取措施。这块可能大部分开发人员不太了解,这边我就稍微提一下。做一下这个是不是很费劲?

对于很多中小团队来说,可能本身就没有什么人力去做这件事,那么是不是就没办法了呢?实际上,很多安全服务商,甚至一些大型互联网公司都有提供这样的产品服务,比如携程的Github Scan:

如果说,你连买这类产品的钱都不想出,那么顺手推荐一个开源项目可以参考一下:Github leaked patrol

最近,欢迎留言交流,说说大家所在团队对于代码的安全性都是如何做的?用了什么商业服务?或是用了什么开源软件?欢迎一起分享学习与进步!

最全的JAVA知识汇总(附讲解和思维导图)

mumupudding阅读(6)

jvm 一行代码是怎么运行的

首先,java代码会被编译成字节码,字节码就是java虚拟机定义的一种编码格式,需要java虚拟机才能够解析,java虚拟机需要将字节码转换成机器码才能在cpu上执行。我们可以用硬件实现虚拟机,这样虽然可以提高效率但是就没有了一次编译到处运行的特性了,所以一般在各个平台上用软件来实现,目前的虚拟机还提供了一套运行环境来进行垃圾回收,数组越界检查,权限校验等。虚拟机一般将一行字节码解释成机器码然后执行,称为解释执行,也可以将一个方法内的所有字节码解释成机器码之后在执行,前者执行效率低,后者会导致启动时间慢,一般根据二八法则,将百分之20的热点代码进行即时编译。JIT编译的机器码存放在一个叫codecache的地方,这块内存属于堆外内存,如果这块内存不够了,那么JIT编译器将不再进行即时编译,可能导致程序运行变慢。

jvm如何加载一个类

第一步:加载,双亲委派:启动类加载器(jre/lib),系统扩展类加载器(ext/lib),应用类加载器(classpath),前者为c++编写,所以系统加载器的parent为空,后面两个类加载器都是通过启动类加载器加载完成后才能使用。加载的过程就是查找字节流,可以通过网络,也可以自己在代码生成,也可以来源一个jar包。另外,同一个类,被不同的类加载器加载,那么他们将不是同一个类,java中通过类加载器和类的名称来界定唯一,所以我们可以在一个应用成存在多个同名的类的不同实现。

第二步:链接:(验证,准备,解析) 验证主要是校验字节码是否符合约束条件,一般在字节码注入的时候关注的比较多。准备:给静态字段分配内存,但是不会初始化,解析主要是为了将符号引用转换为实际引用,可能会触发方法中引用的类的加载。

第三步:初始化,如果赋值的静态变量是基础类型或者字符串并且是final的话,该字段将被标记为常量池字段,另外静态变量的赋值和静态代码块,将被放在一个叫cinit的方法内被执行,为了保证cinit方法只会被执行一次,这个方法会加锁,我们一般实现单例模式的时候为保证线程安全,会利用类的初始化上的锁。 初始化只有在特定条件下才会被触发,例如new 一个对象,反射被调用,静态方法被调用等。

java对象的内存布局

java中每一个非基本类型的对象,都会有一个对象头,对象头中有64位作为标记字段,存储对象的哈希码,gc信息,锁信息,另外64位存储class对象的引用指针,如果开启指针压缩的话,该指针只需要占用32位字节。

Java对象中的字段,会进行重排序,主要为了保证内存对齐,使其占用的空间正好是8的倍数,不足8的倍数会进行填充,所以想知道一个属性相对对象其始地址的偏移量需要通过unsafe里的fieldOffset方法,内存对齐也为了避免让一个属性存放在两个缓存行中,disruptor中为了保证一个缓存行只能被一个属性占用,也会用空对象进行填充,因为如果和其他对象公用一个缓存行,其他对象的失效会将整个缓存行失效,影响性能开销,jdk8中引入了contended注解来让一个属性独占一个缓存行,内部也是进行填充,用空间换取时间,如何计算一个对象占用多少内存,如果不精确的话就进行遍历然后加上对象头,这种情况没办法考虑重排序和填充,如果精确的话只能通过javaagent的instrument工具。

反射的原理

反射真的慢么?

首先class.forname和class.getmethod 第一个是一个native方法,第二个会遍历自己和父类中的方法,并返回方法的一个拷贝,所以这两个方法性能都不好,建议在应用层进行缓存。而反射的具体调用有两种方式,一种是调用本地native方法,一种是通过动态字节码生成一个类来调用,默认采用第一种,当被调用15次之后,采用第二种动态字节码方式,因为生成字节码也耗时,如果只调用几次没必要,而第一种方式由于需要在java和c++之间切换,native 方法本身性能消耗严重,所以对于热点代码频繁调用反射的话,性能并不会很差。

属性的反射,采用unsafe类中setvalue来实现,需要传入该属性相对于对象其始地址的偏移量,也就是直接操作内存。其实就是根据这个属性在内存中的起始地址和类型来读取一个字段的值,在LockSupport类中,park和unpark方法,设置谁将线程挂起的时候也有用到这种方式。

动态代理

java本身的动态代理也是通过字节码实现的

Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

工具类中需要提供 类加载器,需要实现的接口,拦截器的实现,也就是需要在InvocationHandler中调用原方法并做增强处理。并且这个实现,一定会被放到新生成的动态代理类里。

生成动态代理类的步骤:先通过声明的接口生成一个byte数组,这个数组就是字节流,通过传入的类加载进行加载生成一个class对象,这个class 里面有个构造方法接收一个参数,这个参数就是InvocationHandler,通过这个构造方法的反射获取一个实例类,在这个class里面,接口的实现中会调用InvocationHandler,而这个class对象为了防止生成太多又没有被回收,所以是一个弱引用对象。

jvm的内存模型

并发问题的根源:可见性,原子性,乱序执行

java内存模型定义了一些规则来禁止cpu缓存和编译器优化,happen-before用来描述两个操作的内存的可见性,有以下6条

  • 1.程序的顺序执行,前一个语句对后一个语句可见 (当两个语句没有依赖的情况下还是可以乱序执行)
  • 2.volatile变量的写对另一个线程的读可见
  • 3.happen-before 具有传递性
  • 4.一个线程对锁的释放对另外一个线程的获取锁可见 (也就是一个线程在释放锁之前对共享变量的操作,另外一个线程获取锁后会看的到)
  • 5.线程a调用了线程b的start()方法,那么线程a在调用start方法之前的操作,对线程b内的run()方法可见
  • 6.线程a调用了线程b的join方法,那么线程b里的所有操作,将对线程a调用join之后的操作可见。

jvm的垃圾回收

两种实现:引用计数和可达性分析,引用计数会出现循环引用的问题,目前一般采用可达性分析。

为了保证程序运行线程和垃圾回收线程不会发生并发影响,jvm采用安全点机制来实现stop the world,也就是当垃圾收集线程发起stop the world请求后,工作线程开始进行安全点检测,只有当所有线程都进入安全点之后,垃圾收集线程才开始工作,在垃圾收集线程工作过程中,工作线程每执行一行代码都会进行安全点检测,如果这行代码安全就继续执行,如果这行代码不安全就将该线程挂起,这样可以保证垃圾收集线程运行过程中,工作线程也可以继续执行。

安全点:例如阻塞线程肯定是安全点,运行的jni线程如果不访问java对象也是安全的,如果线程正在编译生成机器码那他也是安全的,Java虚拟机在有垃圾回收线程执行期间,每执行一个字节码都会进行安全检测。

基础垃圾收集算法:清除算法会造成垃圾碎片,清除后整理压缩浪费cpu耗时,复制算法浪费内存。

基础假设:大部分的java对象只存活了一小段时间,只有少部分java对象存活很久。新建的对象放到新生代,当经过多次垃圾回收还存在的,就把它移动到老年代。针对不同的区域采用不同的算法。因为新生代的对象存活周期很短,经常需要垃圾回收,所以需要采用速度最快的算法,也就是复制,所以新生代会分成两块。一块eden区,两块大小相同的survivor区。

新的对象默认在eden区进行分配,由于堆空间是共享的,所以分配内存需要加锁同步,不然会出现两个对象指向同一块内存,为了避免频繁的加锁,一个线程可以申请一块连续内存,后续内存的分配就在这里进行,这个方案称为tlab。tlab里面维护两个指针,一个是当前空余内存起始位置,另外一个tail指向尾巴申请的内存结束位置,分配内存的时候只需要进行指针加法并判断是否大于tail,如果超过则需要重新申请tlab。

如果eden区满了则会进行一次minorGc ,将eden区的存活对象和from区的对象移动到to区,然后交换from和to的指针。

垃圾收集器的分类:针对的区域,老年代还是新生代,串行还是并行,采用的算法分类复制还是标记整理

g1 基于可控的停顿时间,增加吞吐量,取代cms g1将内存分为多个块,每个块都可能是 eden survivor old 三种之一 首先清除全是垃圾的快 这样可以快速释放内存。

如果发现JVM经常进行full gc 怎么排查?

不停的进行full gc表示可能老年代对象占有大小超过阈值,并且经过多次full gc还是没有降到阈值以下,所以猜测可能老年代里有大量的数据存活了很久,可能是出现了内存泄露,也可能是缓存了大量的数据一直没有释放,我们可以用jmap将gc日志dump下来,分析下哪些对象的实例个数很多,以及哪些对象占用空间最多,然后结合代码进行分析。

并发和锁

线程的状态机

线程池参数:核心线程数,最大线程数,线程工厂,线程空闲时间,任务队列,拒绝策略先创建核心线程,之后放入任务队列,任务队列满了创建线程直到最大线程数,在超过最大线程数就会拒绝,线程空闲后超过核心线程数的会释放,核心线程也可以通过配置来释放,针对那些一天只跑一个任务的情况。newCachedThreadPool线程池会导致创建大量的线程,因为用了同步队列。

synchronized

同步块会有一个monitorenter和多个monitorexist ,重量级锁是通过linux内核pthread里的互斥锁实现的,包含一个waitset和一个阻塞队列。自旋锁,会不停尝试获取锁,他会导致其他阻塞的线程没办法获取到锁,所以他是不公平锁,而轻量级锁和偏向锁,均是在当前对象的对象头里做标记,用cas方法设置该标记,主要用于多线程在不同时间点获取锁,以及单线程获取锁的情况,从而避免重量级锁的开销,锁的升级和降级也需要在安全点进行。

  • reentrantlock相对synchronized的优势:可以控制公平还是非公平,带超时,响应中断。
  • CyclicBarrier 多个线程相互等待,只有所有线程全部完成后才通知一起继续 (调用await 直到所有线程都调用await才一起恢复继续执行)
  • countdownlatch 一个线程等待,其他线程执行完后它才能继续。(调用await后被阻塞,直到其他地方调用countdown()将state减到1 这个地方的其他可以是其他多个线程也可以其他单个任务)
  • semaphore 同一个时刻只运行n个线程,限制同时工作的线程数目。
  • 阻塞队列一般用两个锁,以及对应的条件锁来实现,默认为INTEGER.MAX为容量,而同步队列没有容量,优先级队列内部用红黑树来实现。

如果要频繁读取和插入建议用concurrenthashmap 如果频繁修改建议用 concurrentskiplistmap,copyonwrite适合读多写少,写的时候进行拷贝,并加锁。读不加锁,可能读取到正在修改的旧值。concurrent系列实际上都是弱一致性,而其他的都是fail-fast,抛出ConcurrentModificationException,而弱一致性允许修改的时候还可以遍历。例如concurrent类的size方法可能不是百分百准确。

AQS 的设计,用一个state来表示状态,一个先进先出的队列,来维护正在等待的线程,提供了acquire和release来获取和释放锁,锁,条件,信号量,其他并发工具都是基于aqs实现。

字符串

字符串可以通过intern()方法缓存起来,放到永久代,一般一个字符串申明的时候会检查常量区是否存在,如果存在直接返回其地址,字符串是final的,他的hashcode算法采用31进制相加,字符串的拼接需要创建一个新的字符串,一般使用stringbuilder。String s1 = “abc”; String s2 = “abc”; String s1 = new String(“abc”); s1和s2可能是相等的,因为都指向常量池。

集合

  • vector 线程安全,arraylist 实现 randomaccess 通过数组实现支持随机访问,linkedlist 双向链表可以支持快速的插入和删除。
  • treeset 依赖于 treemap 采用红黑树实现,可以支持顺序访问,但是插入和删除复杂度为 log(n)
  • hashset 依赖于 hashmap 采用哈希算法实现,可以支持常数级别的访问,但是不能保证有序
  • linkedhashset 在hashset的节点上加了一个双向链表,支持按照访问和插入顺序进行访问
  • hashtable早版本实现,线程安全 不支持空键。
  • hashmap:根据key的hashcode的低位进行位运算,因为高位冲突概率较高,根据数组长度计算某个key对应数组位置,类似求余算法,在put的时候会进行初始化或者扩容,当元素个数超过 数组的长度乘以负载因子的时候进行扩容,当链表长度超过8会进行树化,数组的长度是2的多少次方,主要方便位运算,另一个好处是扩容的时候迁移数据只需要迁移一半。当要放 15个元素的时候,一般数组初始化的长度为 15/0.75= 20 然后对应的2的多少次方,那么数组初始化长度为 32.
  • ConcurrentHashMap 内部维护了一个segment数组,这个segment继承自reentrantlock,他本身是一个hashmap,segment数组的长度也就是并发度,一般为16. hashentry内部的value字段为volatile来保证可见性.size()方法需要获取所有的segment的锁,而jdk8的size()方法用一个数组存储每个segment对应的长度。

io

输入输出流的数据源有 文件流,字节数组流,对象流 ,管道。带缓存的输入流,需要执行flush,reader和writer是字符流,需要根据字节流封装。

bytebuffer里面有position,capcity,limit 可以通过flip重置换,一般先写入之后flip后在从头开始读。

文件拷贝 如果用一个输入流和一个输出流效率太低,可以用transfer方法,这种模式不用到用户空间,直接在内核进行拷贝。

一个线程一个连接针对阻塞模式来说效率很高,但是吞吐量起不来,因为没办法开那么多线程,而且线程切换也有开销,一般用多路复用,基于事件驱动,一个线程去扫描监听的连接中是否有就绪的事件,有的话交给工作线程进行读写。一般用这种方式实现C10K问题。

堆外内存(direct) 一般适合io频繁并且长期占用的内存,一般建议重复使用,只能通过Native Memory Tracking(NMT)来诊断,MappedByteBuffer可以通过FileChannel.map来创建,可以在读文件的时候少一次内核的拷贝,直接将磁盘的地址映射到用户空间,使用户感觉像操作本地内存一样,只有当发生缺页异常的时候才会触发去磁盘加载,一次只会加载要读取的数据页,例如rocketmq里一次映射1g的文件,并通过在每个数据页写1b的数据进行预热,将整个1G的文件都加载到内存。

设计模式

  • 创建对象:工厂 构建 单例
  • 结构型: 门面 装饰 适配器 代理
  • 行为型:责任链 观察者 模版
  • 封装(隐藏内部实现) 继承(代码复用) 多态(方法的重写和重载)
  • 设计原则:单一指责,开关原则,里氏替换,接口分离,依赖反转

一篇文章从了解到入门shell

mumupudding阅读(13)

1、shell介绍

shell 俗称叫做壳,计算机的壳层,和内核是相对的,用于和用户交互,接收用户指令,调用相应的程序。shell

因此,把shell分为2大类

1.1、图形界面shell(Graphical User Interface shell 即 GUI shell)

也就是用户使用GUI和计算机核交互的shell,比如Windows下使用最广泛的Windows Explorer(Windows资源管理器),Linux下的X Window,以及各种更强大的CDE、GNOME、KDE、 XFCE。

他们都是GUI Shell。

1.2、命令行式shell(Command Line Interface shell ,即CLI shell)

也就是通过命令行和计算机交互的shell。Windows NT 系统下有 cmd.exe(命令提示字符)和近年来微软大力推广的 Windows PowerShell。Linux下有bash / sh / ksh / csh/zsh等一般情况下,习惯把命令行shell(CLI shell)直接称做shell,以后,如果没有特别说明,shell就是指 CLI shell,后文也是主要讲Linux下的 CLI shell。

2、交互方式

根据交互方式的不一样,命令行式shell(CLI shell),又分为交互式shell和非交互式shell。

2.1、交互式shell

交互式模式就是shell等待你的输入,并且执行你提交的命令,然后马上给你反馈。这种也是我们大多数时候使用的。

2.2、非交互式shell

非交互式shell,就是把shell放在写在一个文件里面,执行的时候,不与用户交互,从前往后依次执行,执行到文件结尾时,shell也就终止了。

3、shell的种类

在Linux下 ,各种shell百花齐放,种类繁多,不同的shell,也有不同的优缺点。我们要查看当前系统下支持的shell,可以读取/etc/shells文件。

多种的shell

3.1、bash

Bourne Again Shell 用来替代Bourne shell,也是目前大多数Linux系统默认的shell。

3.2、sh

Bourne Shell 是一个比较老的shell,目前已经被/bin/bash所取代,在很多linux系统上,sh已经是一个指向bash的链接了。下面是CentOS release 6.5 的系统

sh_to_bash

3.3、csh/tcsh

C shell 使用的是“类C”语法,csh是具有C语言风格的一种shell,tcsh是增强版本的csh,目前csh已经很少使用了。

3.4、ksh

最早,bash交互体验很好,csh作为非交互式使用很爽,ksh就吸取了2者的优点。

3.5、zsh

zsh网上说的目前使用的人很少,但是感觉使用的人比较多。zsh本身是不兼容bash的,但是他可以使用仿真模式(emulation mode)来模拟bash等,基本可以实现兼容。在交互式的使用中,目前很多人都是zsh,因为zsh拥有很强大的提示和插件功能,炫酷吊炸天。推荐在终端的交互式使用中使用zsh,再安利一个插件Oh My Zsh其实我个人的理解是,在终端中使用shell,基本上只是调用各种命令,比如:curl cat ls等等,基本不会使用到zsh的编程,所以终端中使用zsh是可以的。但是在写shell脚本的时候,需要考虑兼容性,最主流的还是bash shell,所以,后文我们介绍的shell脚本也是bash shell的。

4、shell脚本

4.1、基础

#!/bin/bashecho "Hello World !"

#!:是一个特殊的标记,表明使用啥解释器来执行,比如这里使用了:/bin/bash 来执行这个脚本。#:只用一个#,就是注释echo:输出我们把上面的脚本保存成一个文件, 1.sh 后面的这个sh是shell脚本的扩展名。然后要怎嚒来执行呢?执行一个shell脚本有很多种方式:

  • sh 1.sh 这样可以直接执行这个1.sh
  • 也可以直接 ./1.sh ,但是这种要注意,才编辑好的文件这样执行可能会报错

denied

这个是因为没有这个脚本没有执行权限,运行 chmod a+x 1.sh 加上执行权限即可。这里顺带说一下,为啥直接运行1.sh不行呢?因为他默认是去PATH里面找程序,当前目录,一般都不在PATH里面。所以直接运行1.sh就回报找不到文件。

根据测试,#!/bin/bash 的标记,只是针对第二种方式 ./xxx.sh的方式有效。本文中代码,第一行均为这个标记,为了节约篇幅,已经省略.

test_run

执行并获取返回结果,有点类似JavaScript 的eval函数。

#!/bin/bashdt=`date` #反引号内的字符串会当作shell执行 ,并且返回结果。echo "dt=${dt}"

4.2、Shell 变量

shell的使用比较简单,就像这样,并且没有数据类型的概念,所有的变量都可以当成字符串来处理:

#!/bin/bashmyName="tom"youName="cat"

不需要申明,直接写就可以了,但是有几个点需要特别注意:

  • 等号两边不能有空格!!!特别要注意,非常容易写错
  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用bash里的关键字。

使用变量

ABC="tom"echo $ABC #使用变量前面加$美元符号echo "ABC=$ABC" #可以直接在字符串里面引用echo "ABC=${ABC}" #但是建议把变量名字用{}包起来

只读变量

ABC="tom"echo "ABC=${ABC}"readOnly ABC #设置只读ABC="CAT" #会报错,因为设置了只读,不能修改

readonly

删除变量

ABC="tom"echo "ABC=${ABC}"unset ABC #删除echo "ABC=$ABC"  echo "ABC=${ABC}" 

var

从这个例子当中,我们也发现,使用一个不存在的变量,shell不会报错,只是当作空来处理。

4.3、Shell 的字符串

使用字符串

NAME="tom"A=my #你甚至可以不用引号,但是字符串当中不能有空格,这种方式也不推荐B='my name is ${NAME}' #变量不会被解析C="my name is ${NAME}" #变量会解析echo $Aecho $Becho $C

执行结果

run_1

我们可以发现,这个字符串的单双号和PHP的处理非常类似,单引号不解析变量,双引号可以解析变量。但是都可以处理转义符号。

A='my\nname\nis\ntom'B="my\nname\nis\ntom"echo $Aecho $B

执行结果

run_2

拼接字符串其实shell拼接字符串,大概就是2种

  • 一是直接在双引号内应用变量,类似模版字符串
  • 二是直接把字符串写在一起,不需要类似Java链接字符串的“+” 和PHP链接字符串的“.”
NAME="TOM"# 使用双引号拼接echo "hello, "$NAME" !" #直接写在一起,没有字符串连接符echo "hello, ${NAME} !" #填充模版# 使用单引号拼接echo 'hello, '$NAME' !' #直接写在一起,没有字符串连接符echo 'hello, ${NAME} !' #上面已经提高过,单引号里面的变量是不会解析的

run_3

强大的字符串处理shell中简单的处理字符串,可以直接使用各种标记,只是比较难记忆,要用的时候,可以查一下。

ABC="my name is tom,his name is cat"echo "字符串长度=${#ABC}" # 取字符串长度echo "截取=${ABC:11}" # 截取字符串, 从11开始到结束echo "截取=${ABC:11:3}" # 截取字符串, 从11开始3个字符串echo "默认值=${XXX-default}" #如果XXX不存在,默认值是defaultecho "默认值=${XXX-$ABC}" #如果XXX不存在,默认值是变量ABCecho "从开头删除最短匹配=${ABC#my}" # 从开头删除 my 匹配的最短字符串echo "从开头删除最长匹配=${ABC##my*tom}" # 从开头删除 my 匹配的最长字符串echo "从结尾删除最短匹配=${ABC%cat}" # 从结尾删除 cat 匹配的最短字符串echo "从结尾删除最长匹配=${ABC%%,*t}" # 从结尾删除 ,*t 匹配的最长字符串echo "替换第一个=${ABC/is/are}" #替换第一个isecho "替换所有=${ABC//is/are}" #替换所有的is

运行结构

run_4

这里只是介绍了比较常用的一些字符串处理,实际shell支持的还有很多。

4.4、数组

Bash Shell 也是支持数组的,与绝大部分语言一样,数组下标从0开始。不过需要注意的是,它只支持一维数组。定义一个数组,用小括号阔气来,当中用“空格”分割,就像下面这样:

array=("item0" "item1" "item2")

也可以根据下标来定义元素

array[0]="new_item0"array[1]="new_item1"array[2]="new_item2"array[4]="new_item4" #数组下标可以是不连续的

读取数组元素,和变量类似

echo ${array[0]}echo "array[0]=${array[0]}"

获取数组所有的元素

echo "数组的元素为: ${array[*]}"echo "数组的元素为: ${array[@]}"

获取数组的长度

echo "数组的长度为: ${#array[*]}"echo "数组的长度为: ${#array[@]}"

4.5、输入输出

4.5.1、echo

在上文中,其实我们已经到多次,就是:echo “字符串” 来输出,一个很简单的例子

echo "Hello world!"

如果当中包含特殊符号,可以使用转义等:

echo "Hello \nworld!"echo "\"Hello\""echo '"Hello"' #当然,也可以这样,单引号不转义,上文提到过echo `date` #打印执行date的结果echo -n "123" #加-n  表示不在末尾输出换行echo "456"echo -e "\a处理特殊符号" #-e 处理特殊符号

-n 让echo输出结束以后,在默认不输出换行符-e 让echo处理特殊符号,比如:

符号 作用
\a 发出警告声
\b 删除前一个字符
\c 后不加上换行符号
\f 换行但光标仍旧停留在原来的位置
\n 换行且光标移至行首
\r 光标移至行首,但不换行
\t 插入tab

上面的特殊符号,写到mac的shell脚本里面要注意,执行的时候,要用bash执行才有效 ,sh无效。

当然,你也可以玩一点更有趣的,就是我们随时在终端中看到的五颜六色的文字:

echo -e "\033[31m 红色前景 \033[0m 缺省颜色" echo -e "\033[41m 红色背景 \033[0m 缺省颜色" 

color

其中\033[是一个特殊标记,表示终端转义开始,31m表示使用红色字体,你也可以使用其他颜色,[30-39]是前景颜色,[40-49]是背景颜色。\033[0m回复到缺省设置还可以有一些其他的动作

echo -e "\033[2J" #清除屏幕echo -e "\033[0q" #关闭所有的键盘指示灯echo -e "\033[1q" #设置"滚动锁定"指示灯(Scroll Lock)echo -e "\033[2q" #设置"数值锁定"指示灯(Num Lock)echo -e "\033[1m" #设置高亮度echo -e "\033[4m" #下划线echo -e "\033[7m" #反显echo -e "\033[y;xH" #设置光标位置 

其他更多的特殊码请自行查询。

4.5.2、read

有输出,必然有输入,read命令接收标准输入的输入。

read nameecho "my name is ${name}"

可以使用-p给一个输入提示

read -p "please input your name:" nameecho "my name is ${name}"

如果没有指定输入的变量,会把输入放在环境标量REPLY中

read -p "please input your name:"echo "my name is ${REPLY}"

计时输入,如果一段时间没有输入 ,就直接返回,使用-t 加时间

read -t 3 -p "please input your name in 3 senconds:" name

指定输入字符个数,使用-n ,后面的是输入字符个数

read -n 1 -p "Are you sure [Y/N]?" isYes

默读(输入不再监视器上显示),加一个-s参数。

read  -s  -p "Enter your password:" password

4.5.3、printf

echo已经比较强大,但是有的时候,我们需要用到字符串模版输出,printf就比较好用了,他类似C里面的printf程序。语法是:printf format-string [arguments…]比如我们要输出一个表格

printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg  printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234 printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543 printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876 

运行结果

run_5

%s %c %d %f都是格式替代符%-10s 指一个宽度为10个字符(-表示左对齐,没有则表示右对齐),至少显示10字符宽度,如果不足则自动以空格填充,超过不限制。%-4.2f 指格式化为小数,其中.2指保留2位小数。

4.5.4、重定向

大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。

命令 作用
command > file 将输出重定向到 file。
command < file 将输入重定向到 file。
command >> file 将输出以追加的方式重定向到 file。
n > file 将文件描述符为 n 的文件重定向到 file。
n >> file 将文件描述符为 n 的文件以追加的方式重定向到 file。
n >& m 将输出文件 m 和 n 合并。
n <& m 将输入文件 m 和 n 合并。
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入。

需要注意的是文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。

输出到文件

echo "test">text.txt #直接输出echo "test">>text.txt #追加在text.txt后面

重定向输入

read a <<EOF"测试"EOFecho "a=$a"

来个比较过分的

cat  < 1.sh > text.txt

把1.sh文件的内容出入到cat,然后cat在输出到text.txt中,相当于,把1.sh的内容输出到text.txt中了

还有一种用法,把标准错误直接输出到标准输出,并且输出到文件file

command > file 2>&1

/dev/null 文件这个是一个特殊文件,他是一个黑洞,写入到它的内容都会被丢弃,如果我们不关心程序的输出,可以这样

command > /dev/null 2>&1

4.6、条件判断(if)

和其他语言一样,shell也有条件判断

单分支:

if conditionthen    command1     command2    ...fi

双分支:

if conditionthen    command1     command2    ...else    commandfi

多分支:

if condition1then    command1elif condition2 then     command2else    commandNfi

比如

if [ "2" == "2" ]; then # "2" 的2边都有空格,不能省略 ,写在一行,条件后面加一个分号    echo "2==2"else    echo "2!=2"fi

需要特别注意:[ “2” == “2” ] 其中的”==”两边都有空格,不能省略,否则结果不正确。判断普通文件是否存在

if [ -f "1.sh" ]; then # 判断一个普通文件是否存在    echo "1.sh 存在"fi

判断目录是否存在

if [ -d "1.sh" ]; then # 判断一个目录是否存在    echo "1.sh 存在"fi

判断字符串长度为0

a=""if [ -z $a ]; then    echo "a为空"fi

4.7、()、(())、[]、[[]]和{}

在shell中,有几个符号要非常注意,用的也比较多,不要搞混了,搞混了,逻辑运算很容易出错

4.7.1、单小括号()

  • 命令组括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用。括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必有空格。
    a="123"(echo "123";a="456";echo "a=$a")echo "a=$a

    命令组

  • 命令替换发现了$(cmd)结构,便将$(cmd)中的cmd执行一次,得到其标准输出,再将此输出放到原来命令。
  • 用于初始化数组如:array=(a b c d)

4.7.2、双小括号(())

  • 运算扩展,比如,你可以
    a=$((4+5)) echo "a=$a"
  • 做数值运算,重新定义变量
    a=5((a++))echo "a=$a"
  • 用于算术运算比较
    if ((1+1>1));then  echo "1+1>1"fi

4.7.3、单中括号[]

  • 用于字符串比较需要注意,用于字符串比较,运算符只能是 ==和!=,需要注意,运算符号2边必须有空格,不然结果不正确!!!比如:
    if [ "2" == "2" ]; then # "2" 的2边都有空格,不能省略  echo "2==2"else  echo "2!=2"fi
  • 用于整数比较需要注意,整数比较,只能用-eq,-gt这种形式,不能直接使用大于(>)小于(<)符号。只能用于整形。
    if [ 2 -eq 2 ]; then   echo "2==2"else  echo "2!=2"fi

    符号表

    符号 运算
    -eq 等于
    -ne 不等于
    -gt 大于
    -ge 大于等于
    -lt 小于
    -le 小于等于
  • 多个逻辑组合-a 表示and 与运算-o 表示or 或运算
    if [ "2" == "2" -a "1" == "1" ]; then #注意,在这里,不能是[ "2" == "2" ] -a [ "1" == "1" ] 会报错  echo "ok"fi

4.7.4、双中括号[[]]

[[是 bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比[ ]结构更加通用。

  • 字符串匹配时甚至支持简单的正则表达式
    if [[ "123" == 12* ]]; then #右边是正则不需要引号  echo "ok"fi
  • 支持对数字的判断,是支持浮点型的,并且可以直接使用<、>、==、!=符号
    if [[  2.1 > 1.1 ]]; then  echo "ok"fi
  • 多个逻辑判断可以直接使用&&、||做逻辑运算,并且可以在多个[[]]之间进行运算
    if [[  1.1 > 1.1 ]] || [[ 1.1 == 1.1 ]]; then  echo "ok"fi

4.7.5、大括号{}

  • 统配扩展
    touch new_{1..5}.txt #创建new_1.txt new_2.txt new_3.txt new_4.txt new_5.txt  5个文件

4.8、循环

4.8.1、for循环

语法格式为:

for a in "item1" "item2" "item3"do    echo $adone

输出当前目录下 .sh结尾的文件

for a in `ls ./`do    if [[ $a == *.sh ]]  then        echo $a  fidone

4.8.2、for循环

语法

while conditiondo    commanddone

我们要输出1-10000

int=1;while(($int<=10000))do    echo $int    ((int++))done

4.8.3、until循环

语法

until conditiondo    commanddone

用法类似,这里不再赘述。循环中 continue命令与break作用和其他语言中类似。

4.9、case

case和其他语言switch类型,多分支,选择一个匹配。匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;,有点类型Java的break。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。

echo '输入 1 到 4 之间的数字:'echo '你输入的数字为:'read aNumcase $aNum in    1)  echo '你选择了 1'    ;;    2)  echo '你选择了 2'    ;;    3)  echo '你选择了 3'    ;;    4)  echo '你选择了 4'    ;;    *)  echo '你没有输入 1 到 4 之间的数字'    ;;esac

4.10、函数

shell也可以用户定义函数,然后在shell脚本中可以随便调用。注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。语法格式如下:

[function] funname(){    cmd....    [return int]}

一个最简单的函数

Line(){  echo "--------分割线--------"}echo "123"Lineecho "456"

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数…调用的时候 ,函数名,参数直接用空格分割开。带参数的函数示例:

out(){    echo "1-->$1"    echo "2-->$2"}out 1 2 #调用的之后

还有一些其他的特殊符号需要注意

符号 作用
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

所以我们可以写一个代码参数,返回值的函数

out(){    echo "全部参数$*"    for item in $*    do        echo "$item"    done    return $# #这类返回参数个数,返回值必须是整数}out this is perfectecho "函数返回值:$?"

4.11、shell传递参数

我们可以在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为:$n。n 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推……除了参数可以使用特殊符号,也可以使用上文中函数所使用的特殊符号,这里不再赘述

echo "执行的文件名:$0";echo "全部参数:$*"echo "参数个数:$#"echo "第一个参数为:$1";echo "第二个参数为:$2";echo "第三个参数为:$3";

params

5、其他实用技巧

shell脚本,他本身的功能并不强大,强大的是他可以调用其他程序,而在Linux下,系统自带的就有非常多的强大工具可以调用。

5.1、后台执行

后台执行一个脚本只需要在后面加上&符号即可,我们先用之前学习的,写一个脚本,1s输出一个数字

#!/bin/bashint=1while :do    echo $int    ((int++))    sleep 1s #睡眠一秒done

我们执行sh d.sh & 我们发现,的确会后台输出,但是会输出到当前控制台,我们可以用之前学的重定向,把输出重定向到文件

sh d.sh > out.log 2>&1 &

这样就把输出和错误重新定向到out.log文件了但是,我们发现,关闭终端以后,文件就不输出了。当我们端口连接远程主机的session或者关闭当前终端的时候, 会产生一个SIGHUP信号 ,导致程序退出,我们可以使用nuhup来忽略这个信号 ,达到真正的后台。

nuhup sh d.sh > out.log 2>&1 &

这样启动程序,就可以打到真正后台运行了。那么问题来了,我们验证程序在后台运行呢?要怎嚒结束后台程序呢?请继续看。

5.2、cat

在本文中,我们已经多次用到cat,他的作用就是读取文件输出到标准输出上,也就是我们的终端。语法是:

cat [option] file

我们也可以使用:cat -n file ,来输出行号。

5.2、tail

类似上面的例子,我们要验证程序是不是在后台,每一秒输出一个数字到文件,使用cat读取,需要不断的多次查看,一次cat只能输出一次。tail非常适合查看这种日志类文件,他的作用是读取文件末尾几行输出到标准输出上。tail out.log默认显示10行,可以使用参数-n指定行数tail -20 out.log显示文件末尾20行tail -f out.log持续监控文件out.log,如果有变化,他会试试的显示在我们的屏幕上面。

5.3、ps

ps,查询进程这个命令参数比较多,列举几个比较常用的

参数 作用
a 显示终端上的所有进程,包括其他用户的进程。
u 显示面向用户的格式信息。
x 显示没有控制终端的进程。

一般查询,使用 ps aux就可以了,查询出来比较多,可以筛选一下。这里我们使用 ps u 就可以查询出我们刚才开启的后台进程了。

ps

我们看到我们刚才启动的程序PID为7523,使用kill命令就可以杀死他了

5.4、kill

kill命令比较简单,就是根据PID结束一个程序,比如我们已经查询到,我们开的后台进行是7523,要结束他可以使用:kill 7523以上是常用用法,其实kill是给程序发送一个信号,上面的程序给会程序发送一个SIGTERM信号,程序收到这个信号,完成资源的释放,就退出了。但是也有程序不听话,收到信号就是不退出,这个时候,就要强制他退出,使用9号命令(SIGKILL),强制杀死他。简单的说kill PID 是告诉程序,你应该退出了,请自己退出。kill -9 PID ,是直接告诉程序,你被终结了,这个命令信号,不能被抓取或者忽略。

6、总结

  • shell使用的比较少,但是特别强大;
  • shell对语法比较敏感,并且应为解释器很多,每个解释器语法标准也可能不完全一致;
  • 使用到的编号、编码、参数特别多,并且都是简写,很多记不住。其实不用死记硬背,记住有这个功能就可以了,需要用到的时候再查询。

推荐

推荐一个库,通过API把消息推送到个人微信上,SDK接入:https://github.com/zjiecode/wxpusher-client

参考资料

http://c.biancheng.net/shell/https://baike.baidu.com/item/shell/99702?fr=aladdinhttps://blog.csdn.net/lixinze779/article/details/81012318https://segmentfault.com/a/1190000008080537https://blog.csdn.net/felix_f/article/details/12433171http://www.runoob.com/linux/linux-shell-printf.htmlhttp://www.runoob.com/linux/linux-shell-process-control.htmlhttps://www.jb51.net/article/123081.htmhttp://www.runoob.com/linux/linux-shell-io-redirections.htmlhttps://www.cnblogs.com/mfryf/p/3336804.htmlhttps://blog.csdn.net/vip_wangsai/article/details/72616587

文章来源:http://blog.zjiecode.com/2019/04/15/shell/