注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

MapperStruct:一款CURD神器

前言 相信绝大多数的业务开发同学,日常的工作都离不开写getter、setter方法。要么是将下游的RPC结果通过getter、setter方法进行获取组装。要么就是将自己系统内部的处理结果通过getter、setter方法处理成前端所需要的VO对象。publ...
继续阅读 »

前言

相信绝大多数的业务开发同学,日常的工作都离不开写getter、setter方法。要么是将下游的RPC结果通过getter、setter方法进行获取组装。要么就是将自己系统内部的处理结果通过getter、setter方法处理成前端所需要的VO对象。

public UserInfoVO originalCopyItem(UserDTO userDTO){
   UserInfoVO userInfoVO = new UserInfoVO();
   userInfoVO.setUserName(userDTO.getName());
   userInfoVO.setAge(userDTO.getAge());
   userInfoVO.setBirthday(userDTO.getBirthday());
   userInfoVO.setIdCard(userDTO.getIdCard());
   userInfoVO.setGender(userDTO.getGender());
   userInfoVO.setIsMarried(userDTO.getIsMarried());
   userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
   userInfoVO.setAddress(userDTO.getAddress());
   return userInfoVO;
}

传统的方法一般是采用硬编码,将每个对象的值都逐一设值。当然为了偷懒也会有采用一些BeanUtil简约代码的方式:

public UserInfoVO utilCopyItem(UserDTO userDTO){
   UserInfoVO userInfoVO = new UserInfoVO();
   //采用反射、内省机制实现拷贝
   BeanUtils.copyProperties(userDTO, userInfoVO);
   return userInfoVO;
}

但是,像BeanUtils这类通过反射、内省等实现的框架,在速度上会带来比较严重的影响。尤其是对于一些大字段、大对象而言,这个速度的缺陷就会越明显。针对速度这块我还专门进行了测试,对普通的setter方法、BeanUtils的拷贝以及本次需要介绍的mapperStruct进行了一次对比。得到的耗时结果如下所示:(具体的运行代码请见附录)

运行次数setter方法耗时BeanUtils拷贝耗时MapperStruct拷贝耗时
12921528(1)3973292(1.36)2989942(1.023)
102362724(1)66402953(28.10)3348099(1.417)
1002500452(1)71741323(28.69)2120820(0.848)
10003187151(1)157925125(49.55)5456290(1.711)
100005722147(1)300814054(52.57)5229080(0.913)
10000019324227(1)244625923(12.65)12932441(0.669)

以上单位均为毫微秒。括号内的为当前组件同Setter比较的比值。可以看到BeanUtils的拷贝耗时基本为setter方法的十倍、二十倍以上。而MapperStruct方法拷贝的耗时,则与setter方法相近。由此可见,简单的BeanUtils确实会给服务的性能带来很大的压力。而MapperStruct拷贝则可以很好的解决这个问题。

使用教程

maven依赖

首先要导入mapStruct的maven依赖,这里我们选择最新的版本1.5.0.RC1。

...
<properties>
   <org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
</properties>
...

//mapStruct maven依赖
<dependencies>
   <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct</artifactId>
       <version>${org.mapstruct.version}</version>
   </dependency>
</dependencies>
...
   
//编译的组件需要配置
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.8.1</version>
           <configuration>
               <source>1.8</source> <!-- depending on your project -->
               <target>1.8</target> <!-- depending on your project -->
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${org.mapstruct.version}</version>
                   </path>
                   <!-- other annotation processors -->
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>

在引入maven依赖后,我们首先来定义需要转换的DTO及VO信息,主要包含的信息是名字、年龄、生日、性别等信息。

@Data
public class UserDTO {
   private String name;

   private int age;

   private Date birthday;

   //1-男 0-女
   private int gender;

   private String idCard;

   private String phoneNumber;

   private String address;

   private Boolean isMarried;
}
@Data
public class UserInfoVO {
   private String userName;

   private int age;

   private Date birthday;

   //1-男 0-女
   private int gender;

   private String idCard;

   private String phoneNumber;

   private String address;

   private Boolean isMarried;
}

紧接着需要编写相应的mapper类,以便生成相应的编译类。

@Mapper
public interface InfoConverter {

   InfoConverter INSTANT = Mappers.getMapper(InfoConverter.class);

   @Mappings({
           @Mapping(source = "name", target = "userName")
  })
   UserInfoVO convert(UserDTO userDto);
}

需要注意的是,因为DTO中的name对应的其实是VO中的userName。因此需要在converter中显式声明。在编写完对应的文件之后,需要执行maven的complie命令使得IDE编译生成对应的Impl对象。(自动生成)

image-20220526161736140.png

到此,mapperStruct的接入就算是完成了~。我们就可以在我们的代码中使用这个拷贝类了。

public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
   UserInfoVO userInfoVO = new UserInfoVO();
   userInfoVO = InfoConverter.INSTANT.convert(userDTO);
   return userInfoVO;
}

怎么样,接入是不是很简单~

FAQ

1、接入项目时,发现并没有生成对应的编译对象class,这个是什么原因?

答:可能的原因有如下几个:

  • 忘记编写对应的@Mapper注解,因而没有生成

  • 没有配置上述提及的插件maven-compiler-plugin

  • 没有执行maven的Compile,IDE没有进行相应编译

2、接入项目后发现,我项目内的Lombok、@Data注解不好使了,这怎么办呢?

由于Lombok本身是对AST进行修改实现的,但是mapStruct在执行的时候并不能检测到Lombok所做的修改,因此需要额外的引入maven依赖lombok-mapstruct-binding

......
   <org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
   <lombok.version>1.18.20</lombok.version>
......

......
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>${org.mapstruct.version}</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>${lombok.version}</version>
</dependency>

更详细的,mapperStruct在官网中还提供了一个实现Lombok及mapStruct同时并存的案例

3、更多问题:

欢迎查看MapStruct官网文档,里面对各种问题都有更详细的解释及解答。

实现原理

在聊到mapstruct的实现原理之前,我们就需要先回忆一下JAVA代码运行的过程。大致的执行生成的流程如下所示:

image-20220529181541401.png 可以直观的看到,如果我们想不通过编码的方式对程序进行修改增强,可以考虑对抽象语法树进行相应的修改。而mapstruct也正是如此做的。具体的执行逻辑如下所示:

image-20220529181953035.png

为了实现该方法,mapstruct基于JSR 269实现了代码。JSR 269是JDK引进的一种规范。有了它,能够在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269使用Annotation Processor在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理。想要实现JSR 269,主要有以下几个步骤:

  1. 继承AbstractProcessor类,并且重写process方法,在process方法中实现自己的注解处理逻辑。

  2. 在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor。

通过实现AbstractProcessor,在程序进行compile的时候,会对相应的AST进行修改。从而达到目的。

public void compile(List<JavaFileObject> sourceFileObjects,
                   List<String> classnames,
                   Iterable<? extends Processor> processors)
{
   if (processors != null && processors.iterator().hasNext())
       explicitAnnotationProcessingRequested = true;
   // as a JavaCompiler can only be used once, throw an exception if
   // it has been used before.
   if (hasBeenUsed)
       throw new AssertionError("attempt to reuse JavaCompiler");
   hasBeenUsed = true;

   // forcibly set the equivalent of -Xlint:-options, so that no further
   // warnings about command line options are generated from this point on
   options.put(XLINT_CUSTOM.text + "-" + LintCategory.OPTIONS.option, "true");
   options.remove(XLINT_CUSTOM.text + LintCategory.OPTIONS.option);

   start_msec = now();

   try {
       initProcessAnnotations(processors);

       //此处会调用到mapStruct中的processor类的方法.
       delegateCompiler =
           processAnnotations(
               enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
               classnames);

       delegateCompiler.compile2();
       delegateCompiler.close();
       elapsed_msec = delegateCompiler.elapsed_msec;
  } catch (Abort ex) {
       if (devVerbose)
           ex.printStackTrace(System.err);
  } finally {
       if (procEnvImpl != null)
           procEnvImpl.close();
  }
}

关键代码,在mapstruct-processor包中,有个对应的类MappingProcessor继承了AbstractProcessor,并实现其process方法。通过对AST进行相应的代码增强,从而实现对最终编译的对象进行修改的方法。

@SupportedAnnotationTypes({"org.mapstruct.Mapper"})
@SupportedOptions({"mapstruct.suppressGeneratorTimestamp", "mapstruct.suppressGeneratorVersionInfoComment", "mapstruct.unmappedTargetPolicy", "mapstruct.unmappedSourcePolicy", "mapstruct.defaultComponentModel", "mapstruct.defaultInjectionStrategy", "mapstruct.disableBuilders", "mapstruct.verbose"})
public class MappingProcessor extends AbstractProcessor {
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
       if (!roundEnvironment.processingOver()) {
           RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
           Set<TypeElement> deferredMappers = this.getAndResetDeferredMappers();
           this.processMapperElements(deferredMappers, roundContext);
           Set<TypeElement> mappers = this.getMappers(annotations, roundEnvironment);
           this.processMapperElements(mappers, roundContext);
      } else if (!this.deferredMappers.isEmpty()) {
           Iterator var8 = this.deferredMappers.iterator();

           while(var8.hasNext()) {
               MappingProcessor.DeferredMapper deferredMapper = (MappingProcessor.DeferredMapper)var8.next();
               TypeElement deferredMapperElement = deferredMapper.deferredMapperElement;
               Element erroneousElement = deferredMapper.erroneousElement;
               String erroneousElementName;
               if (erroneousElement instanceof QualifiedNameable) {
                   erroneousElementName = ((QualifiedNameable)erroneousElement).getQualifiedName().toString();
              } else {
                   erroneousElementName = erroneousElement != null ? erroneousElement.getSimpleName().toString() : null;
              }

               deferredMapperElement = this.annotationProcessorContext.getElementUtils().getTypeElement(deferredMapperElement.getQualifiedName());
               this.processingEnv.getMessager().printMessage(Kind.ERROR, "No implementation was created for " + deferredMapperElement.getSimpleName() + " due to having a problem in the erroneous element " + erroneousElementName + ". Hint: this often means that some other annotation processor was supposed to process the erroneous element. You can also enable MapStruct verbose mode by setting -Amapstruct.verbose=true as a compilation argument.", deferredMapperElement);
          }
      }

       return false;
  }
}

如何断点调试:

因为这个注解处理器是在解析->编译的过程完成,跟普通的jar包调试不太一样,maven框架为我们提供了调试入口,需要借助maven才能实现debug。所以需要在编译过程打开debug才可调试。

  • 在项目的pom文件所在目录执行mvnDebug compile

  • 接着用idea打开项目,添加一个remote,端口为8000

  • 打上断点,debug 运行remote即可调试。

image-20220529194616314.png

附录

测试代码如下,采用Spock框架 + JAVA代码实现。Spock框架作为当前最火热的测试框架,你值得学习一下。 Spock框架初体验:更优雅地写好你的单元测试

//    @Resource
   @Shared
   MapperStructService mapperStructService

   def setupSpec() {
       mapperStructService = new MapperStructService()
  }

   @Unroll
   def "test mapperStructTest times = #times"() {
       given: "初始化数据"
       UserDTO dto = new UserDTO(name: "笑傲菌", age: 20, idCard: "1234",
               phoneNumber: "18211932334", address: "北京天安门", gender: 1,
               birthday: new Date(), isMarried: false)

       when: "调用方法"
//       传统的getter、setter拷贝
       long startTime = System.nanoTime();
       UserInfoVO oldRes = mapperStructService.originalCopyItem(dto, times)
       Duration originalWasteTime = Duration.ofNanos(System.nanoTime() - startTime);

//       采用工具实现反射类的拷贝
       long startTime1 = System.nanoTime();
       UserInfoVO utilRes = mapperStructService.utilCopyItem(dto, times)
       Duration utilWasteTime = Duration.ofNanos(System.nanoTime() - startTime1);

       long startTime2 = System.nanoTime();
       UserInfoVO mapStructRes = mapperStructService.newCopyItem(dto, times)
       Duration mapStructWasteTime = Duration.ofNanos(System.nanoTime() - startTime2);

       then: "校验数据"
       println("times = "+ times)
       println("原始拷贝的消耗时间为: " + originalWasteTime.getNano())
       println("BeanUtils拷贝的消耗时间为: " + utilWasteTime.getNano())
       println("mapStruct拷贝的消耗时间为: " + mapStructWasteTime.getNano())
       println()

       where: "比较不同次数调用的耗时"
       times || ignore
       1     || null
       10    || null
       100   || null
       1000  || null
  }

测试的Service如下所示:

public class MapperStructService {

   public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           userInfoVO = InfoConverter.INSTANT.convert(userDTO);
      }
       return userInfoVO;
  }

   public UserInfoVO originalCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           userInfoVO.setUserName(userDTO.getName());
           userInfoVO.setAge(userDTO.getAge());
           userInfoVO.setBirthday(userDTO.getBirthday());
           userInfoVO.setIdCard(userDTO.getIdCard());
           userInfoVO.setGender(userDTO.getGender());
           userInfoVO.setIsMarried(userDTO.getIsMarried());
           userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
           userInfoVO.setAddress(userDTO.getAddress());
      }
       return userInfoVO;
  }

   public UserInfoVO utilCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           BeanUtils.copyProperties(userDTO, userInfoVO);
      }
       return userInfoVO;
  }
}

参考文献

踩坑BeanUtils.copy**()导致的业务处理速度过慢

mapstruct原理解析

MapStruct官网

Mapstruct源码解析- 框架实现原理

作者:DrLauPen
来源:https://juejin.cn/post/7103135968256851976

收起阅读 »

零侵入性:一个注解,优雅的实现循环重试功能

前言在实际工作中,重处理是一个非常常见的场景,比如:发送消息失败。调用远程服务失败。争抢锁失败。这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是...
继续阅读 »

前言

在实际工作中,重处理是一个非常常见的场景,比如:

  1. 发送消息失败。

  2. 调用远程服务失败。

  3. 争抢锁失败。

这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码.然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能.

一、@Retryable是什么?

spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于简单注释的。

二、使用步骤

1.POM依赖

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

2.启用@Retryable

@EnableRetry
@SpringBootApplication
public class HelloApplication {
  public static void main(String[] args) {
      SpringApplication.run(HelloApplication.class, args);
  }
}

3.在方法上添加@Retryable

import com.mail.elegant.service.TestRetryService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.time.LocalTime;

@Service
public class TestRetryServiceImpl implements TestRetryService {
  @Override
  @Retryable(value = Exception.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))
  public int test(int code) throws Exception{
      System.out.println("test被调用,时间:"+LocalTime.now());
        if (code==0){
            throw new Exception("情况不对头!");
        }
      System.out.println("test被调用,情况对头了!");
      return 200;
  }
}

来简单解释一下注解中几个参数的含义:

  1. value:抛出指定异常才会重试

  2. include:和value一样,默认为空,当exclude也为空时,默认所有异常

  3. exclude:指定不处理的异常

  4. maxAttempts:最大重试次数,默认3次

  5. backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。

当重试耗尽时还是失败,会出现什么情况呢?

当重试耗尽时,RetryOperations可以将控制传递给另一个回调,即RecoveryCallback。Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。

4.@Recover

@Recover
public int recover(Exception e, int code){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法
  return 400;
}

可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。对于@Recover注解的方法,需要特别注意的是:

  1. 方法的返回值必须与@Retryable方法一致

  2. 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)

  3. 该回调方法与重试方法写在同一个实现类里面

5. 注意事项

  1. 由于是基于AOP实现,所以不支持类里自调用方法

  2. 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void

  3. 方法内不能使用try catch,只能往外抛异常

  4. @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。

总结

本篇主要简单介绍了Springboot中的Retryable的使用,主要的适用场景和注意事项,当需要重试的时候还是很有用的。

作者:Memory小峰

来源:blog.csdn.net/h254931252/article/details/109257998

收起阅读 »

源码阅读原则

不是绝对的,只是提供一种大致的思路大致的了解一个类、方法、字段所代表的含义明确你需要了解某个功能A的实现,越具体越好,列出切入点,然后从上至下的分析对于行数庞大、逻辑复杂的源码,我们在追踪时遇到非相关源码是必定的,可以简单追踪几个层级,给自己定一个界限,否则容...
继续阅读 »

源码阅读原则

不是绝对的,只是提供一种大致的思路

见名之意

大致的了解一个类、方法、字段所代表的含义

切入点

明确你需要了解某个功能A的实现,越具体越好,列出切入点,然后从上至下的分析

分支

对于行数庞大、逻辑复杂的源码,我们在追踪时遇到非相关源码是必定的,可以简单追踪几个层级,给自己定一个界限,否则容易丢失目标,淹没在源码的海洋中

分支字段

追踪有没有直接返回该字段的方法,通过方法注释,直接快速了解该字段的作用。

对于没有向外暴露的字段,我们追踪它的usage

  • 数量较少:可以通过各usage处的方法名大致了解,又或者是直接阅读源码

  • 数量较多:建议另辟蹊径,实在没办法再逐一攻破

分支方法

首先是阅读方法注释,有几种情况:

  • 涉及新术语:在类中搜索关键字找到相关方法或类

  • 涉及新的类:看分支类

  • 功能A相关:略过

分支类

先阅读理解类注释,有以下几种情况:

  • 涉及到新的领域:通过查看继承树的方式,大致了解它规模体系和作用

  • 不确定和功能A是否有关联:可查阅官方文档或者搜索引擎做确定

断点调试

动态分析的数据能够帮助我们去验证我们的理解是否正确,实践是检验真理的唯一标准

usage截止点

当你从某个方法出发,寻找它是在何处调用时,请记住你的目的,我们应该在脱离了强相关功能方法处截止,继续usage的意义不大。

比如RecyclerViewscrapOrRecycleView,我们的目的是:寻找什么时候触发了回收View

应该在onLayoutChildren处停止,再继续usage时,你的目的就变成了:寻找什么时候布置Adapter所有相关的子View


作者:土猫少侠
来源:juejin.cn/post/7100806273460863006

收起阅读 »

Base64编码解码原理

Base64编码与解码原理涉及的算法1、短除法短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。实例:以字符n对应的ascII编码1...
继续阅读 »

Base64编码与解码

原理涉及的算法

1、短除法

短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。

实例:以字符n对应的ascII编码110为例。

110 / 2  = 55...0
55 / 2 = 27...1
27 / 2 = 13...1
13 / 2 = 6...1
6   / 2 = 3...0
3   / 2 = 1...1
1   / 2 = 0...1

将余数从下到上进行排列组合,得到字符n对应的ascII编码110转二进制为1101110,因为一字节对应8位(bit), 所以需要向前补0补足8位,得到01101110。其余字符同理可得。

2、按权展开求和

按权展开求和, 8位二进制数从右到左,次数是0到7依次递增, 基数*底数次数,从左到右依次累加,相加结果为对应十进制数。我们已二进制数01101110转10进制为例:

(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27(01101110)_2 = 0 * 2^0 + 1 * 2 ^ 1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 + 1 * 2^6 + 0 * 2^7(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27

3、位概念

二进制数系统中,每个0或1就是一个位(bit,比特),也叫存储单元,位是数据存储的最小单位。其中 8bit 就称为一个字节(Byte)。

4、移位运算符

移位运算符在程序设计中,是位操作运算符的一种。移位运算符可以在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。在base64的编码和解码过程中操作的是正数,所以仅使用<<(左移)、>>(带符号右移)两种运算符。

  1. 左移运算:是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。【左移相当于一个数乘以2的次方】

  2. 右移运算:是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1。【右移相当于一个数除以2的次方】

// 左移
01101000 << 2 -> 101000(左侧移出位被丢弃) -> 10100000(右侧空位一律补0)
// 右移
01101000 >> 2 -> 011010(右侧移出位被丢弃) -> 00011010(左侧空位一律补0)

5、与运算、或运算

与运算、或运算都是计算机中一种基本的逻辑运算方式。

  1. 与运算:符号表示为&。运算规则:两位同时为“1”,结果才为“1”,否则为0

  2. 或运算:符号表示为|。运算规则:两位只要有一位为“1”,结果就为“1”,否则为0

什么是base64编码

2^6=64\

\

Base64编码是将字符串以每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节(6比特有效字节,最左边两个永远为0,其实也是8比特的字节)子序列,再将得到的子序列查找Base64的编码索引表,得到对应的字符拼接成新的字符串的一种编码方式。

每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节的拆分过程如下图所示:


为什么base64编码后的大小是原来的4/3倍

因为6和8的最大公倍数是24,所以3个8比特的字节刚好可以拆分成4个6比特的字节,3 x 8 = 6 x 4。计算机中,因为一个字节需要8个存储单元存储,所以我们要把6个比特往前面补两位0,补足8个比特。如下图所示:


补足后所需的存储单元为32个,是原来所需的24个的4/3倍。这也就是base64编码后的大小是原来的4/3倍的原因。

为什么命名为base64呢?

因为6位(bit)的二进制数有2的6次方个,也就是二进制数(00000000-00111111)之间的代表0-63的64个二进制数。

不是说一个字节是用8位二进制表示的吗,为什么不是2的8次方?

因为我们得到的8位二进制数的前两位永远是0,真正的有效位只有6位,所以我们所能够得到的二进制数只有2的6次方个。

Base64字符是哪64个?

Base64的编码索引表,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符来代表(00000000-00111111)这64个二进制数。即

let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

编码原理

要把3个字节拆分成4个字节可以怎么做?

流程图


思路

分析映射关系:abc → xyzi。我们从高位到低位添加索引来分析这个过程

  • x: (前面补两个0)a的前六位 => 00a7a6a5a4a3a2

  • y: (前面补两个0)a的后两位 + b的前四位 => 00a1a0b7b6b5b4

  • z: (前面补两个0)b的后四位 + c的前两位 => 00b3b2b1b0c7c6

  • i: (前面补两个0)c的后六位 => 00c5c4c3c2c1c0

通过上述的映射关系,得到实现思路:

  1. 将字符对应的AscII编码转为8位二进制数

  2. 将每三个8位二进制数进行以下操作

    • 将第一个数右移位2位,得到第一个6位有效位二进制数

    • 将第一个数 & 0x3之后左移位4位,得到第二个6位有效位二进制数的第一个和第二个有效位,将第二个数 & 0xf0之后右移位4位,得到第二个6位有效位二进制数的后四位有效位,两者取且得到第二个6位有效位二进制

    • 将第二个数 & 0xf之后左移位2位,得到第三个6位有效位二进制数的前四位有效位,将第三个数 & 0xC0之后右移位6位,得到第三个6位有效位二进制数的后两位有效位,两者取且得到第三个6位有效位二进制

    • 将第三个数 & 0x3f,得到第四个6位有效位二进制数

  3. 将获得的6位有效位二进制数转十进制,查找对呀base64字符

代码实现

以hao字符串为例,观察base64编码的过程,将上面转换通过代码逻辑分析实现

// 输入字符串
let str = 'hao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4, out
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(0) & 0xff // 104 01101000
char2 = str.charCodeAt(1) & 0xff // 97 01100001
char3 = str.charCodeAt(2) & 0xff // 111 01101111
// 输出6位有效字节二进制数
out1 = char1 >> 2 // 26 011010
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
out4 = char3 & 0x3f // 47 101111

out = base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv

算法剖析

  1. out1: char1 >> 2

    01101000 -> 00011010
    复制代码
  2. out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4

    // 且运算
    01101000       01100001
    00000011       11110000
    --------       --------
    00000000       01100000

    // 移位运算后得
    00000000       00000110

    // 或运算
    00000000
    00000110
    --------
    00000110
    复制代码

第三个字符第四个字符同理

整理上述代码,扩展至多字符字符串

// 输入字符串
let str = 'haohaohao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
  // 定义输入、输出字节的二进制数
  let char1, char2, char3, out1, out2, out3, out4
  // 将字符对应的ascII编码转为8位二进制数
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  char3 = str.charCodeAt(index++) & 0xff // 111 01101111
  // 输出6位有效字节二进制数
  out1 = char1 >> 2 // 26 011010
  out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
  out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
  out4 = char3 & 0x3f // 47 101111

  out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv
}

原字符串长度不是3的整倍数的情况,需要特殊处理

    ...
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  if (index == len) {
      out2 = (char1 & 0x3) << 4
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
      return out
  }
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  if (index == len) {
      out1 = char1 >> 2 // 26 011010
      out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
      out3 = (char2 & 0xf) << 2
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
      return out
  }
  ...

全部代码

function base64Encode(str) {
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(index++) & 0xff
out1 = char1 >> 2
if (index == len) {
out2 = (char1 & 0x3) << 4
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
return out
}
char2 = str.charCodeAt(index++) & 0xff
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4
if (index == len) {
out3 = (char2 & 0xf) << 2
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
return out
}
char3 = str.charCodeAt(index++) & 0xff
// 输出6位有效字节二进制数
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6
out4 = char3 & 0x3f

out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4]
}
return out
}
base64Encode('haohao') // aGFvaGFv
base64Encode('haoha') // aGFvaGE=
base64Encode('haoh') // aGFvaA==

解码原理

逆向推导,由每4个6位有效位的二进制数合并成3个8位二进制数,根据ascII编码映射到对应字符后拼接字符串

思路

分析映射关系 xyzi -> abc

  • a: x后六位 + y第三、四位 => x5x4x3x2x1x0y5y4

  • b: y后四位 + z第三、四、五、六位 => y3y2y1y0z5z4z3z2

  • c: z后两位 + i后六位 => z1z0i5i4i3i2i1i0

  1. 将字符对应的base64字符集的索引转为6位有效位二进制数

  2. 将每四个6位有效位二进制数进行以下操作

    1. 第一个二进制数左移位2位,得到新二进制数的前6位,第二个二进制数 & 0x30之后右移位4位,取或集得到第一个新二进制数

    2. 第二个二进制数 & 0xf之后左移位4位,第三个二进制数 & 0x3c之后右移位2位,取或集得到第二个新二进制数

    3. 第二个二进制数 & 0x3之后左移位6位,与第四个二进制数取或集得到第二个新二进制数

  3. 根据ascII编码映射到对应字符后拼接字符串

代码实现

// base64字符串
let str = 'aGFv'
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
// 获取索引值
let char1 = base64CharsArr.findIndex(char => char==str[0]) & 0xff // 26 011010
let char2 = base64CharsArr.findIndex(char => char==str[1]) & 0xff // 6 000110
let char3 = base64CharsArr.findIndex(char => char==str[2]) & 0xff // 5 000101
let char4 = base64CharsArr.findIndex(char => char==str[3]) & 0xff // 47 101111
let out1, out2, out3, out
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)

遇到有用'='补过位的情况时

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let char1 = base64CharsArr.findIndex(char => char==str[0])
let char2 = base64CharsArr.findIndex(char => char==str[1])
let out1, out2, out3, out
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[2])
// 第三位不在base64对照表中时,只拼接第一个字符串
if (char3 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out = String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[3])
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
if (char4 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out = String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
return out
}

解码整个字符串,整理代码后

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let i = 0
let len = str.length
let out = ''
while(i < len) {
let char1 = base64CharsArr.findIndex(char => char==str[i])
i++
let char2 = base64CharsArr.findIndex(char => char==str[i])
i++
let out1, out2, out3
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个字符串
out1 = char1 << 2 | (char2 & 0x30) >> 4
if (char3 == -1) {
out = out + String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
if (char4 == -1) {
out = out + String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = out + String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
}
return out
}
base64decode('aGFvaGFv') // haohao
base64decode('aGFvaGE=') // haoha
base64decode('aGFvaA==') // haoh

上述解码核心是字符与base64字符集索引的映射,网上看到过使用AscII编码索引映射base64字符索引的方法

let base64DecodeChars = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 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, -1, -1, -1, -1, -1, -1, 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, -1, -1, -1, -1, -1]
//
let char1 = 'hao'.charCodeAt(0) // h -> 104
base64DecodeChars[char1] // 33 -> base64编码表中的h

由此可见,base64DecodeChars对照accII编码表的索引存放的是base64编码表的对应字符的索引。

jdk1.8之前的方式

Base64编码与解码时,会使用到JDK里sun.misc包套件下的BASE64Encoder类和BASE64Decoder类

sun.misc包所提供的Base64编码解码功能效率不高,因此在1.8之后的jdk版本已经被删除了

// 编码器
final BASE64Encoder encoder = new BASE64Encoder();
// 解码器
final BASE64Decoder decoder = new BASE64Decoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = encoder.encode(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decodeBuffer(encodedText), "UTF-8"));

Apache Commons Codec包的方式

Apache Commons Codec 有提供Base64的编码与解码功能,会使用到 org.apache.commons.codec.binary 套件下的Base64类别,用法如下

1、引入依赖

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-compress</artifactId>
  <version>1.21</version>
</dependency>

2、代码实现

final Base64 base64 = new Base64();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = base64.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(base64.decode(encodedText), "UTF-8"));

jdk1.8之后的方式

与sun.misc包和Apache Commons Codec所提供的Base64编解码器方式来比较,Java 8提供的Base64拥有更好的效能。实际测试编码与解码速度,Java 8提供的Base64,要比 sun.misc 套件提供的还要快至少11倍,比 Apache Commons Codec 提供的还要快至少3倍。

// 解码器
final Base64.Decoder decoder = Base64.getDecoder();
// 编码器
final Base64.Encoder encoder = Base64.getEncoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes(StandardCharsets.UTF_8);
//编码
final String encodedText = encoder.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decode(encodedText), StandardCharsets.UTF_8));

总结

Base64 是一种数据编码方式,可做简单加密使用,可以t通过改变base64编码映射顺序来形成自己独特的加密算法进行加密解密。

编码表

Base64编码表


AscII码编码表


作者:loginfo
来源:juejin.cn/post/7100421228644532255

收起阅读 »

推荐一款超棒的SpringCloud 脚手架项目

之前接个私活,在网上找了好久没有找到合适的框架,不是版本低没人维护了,在不就是组件相互依赖较高。所以我自己搭建一个全新spingCloud框架,里面所有组件可插拔的,集成多个组件供大家选择,喜欢哪个用哪个一、系统架构图二、快速启动1.本地启动nacos: ht...
继续阅读 »

之前接个私活,在网上找了好久没有找到合适的框架,不是版本低没人维护了,在不就是组件相互依赖较高。所以我自己搭建一个全新spingCloud框架,里面所有组件可插拔的,集成多个组件供大家选择,喜欢哪个用哪个

一、系统架构图


二、快速启动

1.本地启动nacos: http://127.0.0.1:8848

sh startup.sh -m standalone

2.本地启动sentinel: http://127.0.0.1:9000

nohup java -Dauth.enabled=false -Dserver.port=9000 -jar sentinel-dashboard-1.8.1.jar &

3.本地启动zipkin: http://127.0.0.1:9411/

nohup java -jar zipkin-server-2.23.2-exec.jar &

三、项目概述

  • springboot+springcloud

  • 注册中心:nacos

  • 网关:gateway

  • RPC:feign

以下是可插拔功能组件

  • 流控熔断降级:sentinel

  • 全链路跟踪:sleth+zipkin

  • 分布式事务:seata

  • 封装功能模块:全局异常处理、日志输出打印持久化、多数据源、鉴权授权模块、zk(分布式锁和订阅者模式)

  • maven:实现多环境打包、直推镜像到docker私服。

这个项目整合了springcloud体系中的各种组件。以及集成配置说明。同时将自己平时使用的功能性的封装以及工具包都最为模块整合进来。可以避免某些技术点长时间不使用后的遗忘。

另一方面现在springboot springcloud 已经springcloud-alibaba的版本迭代速度越来越快。

为了保证我们的封装和集成方式在新版本中依然正常运行,需要用该项目进行最新版本的适配实验。这样可以更快的在项目中集合工程中的功能模块。

四、项目预览






五、新建业务工程模块说明

由于springboot遵循 约定大于配置的原则。所以本工程中所有的额类都在的包路径都在com.cloud.base下。

如果新建的业务项目有规定使用指定的基础包路径则需要在启动类增加包扫描注解将com.cloud.base下的所有类加入到扫描范围下。

@ComponentScan(basePackages = "com.cloud.base")

如果可以继续使用com.cloud.base 则约定将启动类放在该路径下即可。

六、模块划分

父工程:

cloud-base - 版本依赖管理 <groupId>com.cloud</groupId>
|
|--common - 通用工具类和包 <groupId>com.cloud.common</groupId>
|   |
|   |--core-common 通用包 该包包含了SpringMVC的依赖,会与WebFlux的服务有冲突
|   |
|   |--core-exception 自定义异常和请求统一返回类
|
|--dependency - 三方功能依赖集合 无任何实现 <groupId>com.cloud.dependency</groupId>
|   |
|   |--dependency-alibaba-cloud 关于alibaba-cloud的依赖集合
|   |
|   |--dependency-mybatis-tk 关于ORM mybatis+tk.mybatis+pagehelper的依赖集合
|   |
|   |--dependency-mybatis-plus 关于ORM mybatis+mybatis—plus+pagehelper的依赖集合
|   |
|   |--dependency-seata 关于分布式事务seata的依赖集合
|   |
|   |--dependency-sentinel 关于流控组件sentinel的依赖集合
|   |
|   |--dependency-sentinel-gateway 关于网关集成流控组件sentinel的依赖集合(仅仅gateway网关使用该依赖)
|   |
|   |--dependency-sleuth-zipkin 关于链路跟踪sleuth-zipkin的依赖集合
|
|--modules - 自定义自实现的功能组件模块 <groupId>com.cloud.modules</groupId>
|   |
|   |--modules-logger 日志功能封装
|   |
|   |--modules-multi-datasource 多数据功能封装
|   |
|   |--modules-lh-security 分布式安全授权鉴权框架封装
|   |
|   |--modules-youji-task 酉鸡-分布式定时任务管理模块
|   |
|
|  
|  
| 以下是独立部署的应用 以下服务启动后配合前端工程使用 (cloud-base-angular-admin)
|
|--cloud-gateway 应用网关
|
|--authorize-center 集成了modules-lh-security 的授权中心,提供统一授权和鉴权
|  
|--code-generator 代码生成工具
|
|--user-center 用户中心 提供用户管理和权限管理的相关服务
|
|--youji-manage-server 集成了modules-youji-task 的定时任务管理服务端

七、版本使用说明

<springboot.version>2.4.2</springboot.version>
<springcloud.version>2020.0.3</springcloud.version>
<springcloud-alibaba.version>2021.1</springcloud-alibaba.version>

八、多环境打包说明

在需要独立打包的模块resources资源目录下增加不同环境的配置文件

application-dev.yml
application-test.yml
application-prod.yml

修改application.yml

spring:
profiles:
  active: @profileActive@

在需要独立打包的模块下的pom文件中添加一下打包配置。

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profileActive>dev</profileActive>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
</profiles>

mvn打包命令

# 打开发环境
mvn clean package -P dev -Dmaven.test.skip=ture
# 打测试环境
mvn clean package -P test -Dmaven.test.skip=ture
# 打生产环境
mvn clean package -P prod -Dmaven.test.skip=ture

九、构建Docker镜像

整合dockerfile插件,可直接将jar包构建为docker image 并推送到远程仓库

增加插件依赖

<!-- docker image build -->
<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.10</version>
  <executions>
      <execution>
          <id>default</id>
          <goals>
              <!--如果package时不想用docker打包,就注释掉这个goal-->
              <!--                       <goal>build</goal>-->
              <goal>push</goal>
          </goals>
      </execution>
  </executions>
  <configuration>
      <repository>49.232.166.94:8099/example/${project.artifactId}</repository>
      <tag>${profileActive}-${project.version}</tag>
      <username>admin</username>
      <password>Harbor12345</password>
      <buildArgs>
          <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
      </buildArgs>
  </configuration>
</plugin>

在pom.xml同级目录下增加Dockerfile

FROM registry.cn-hangzhou.aliyuncs.com/lh0811/lh0811-docer:lh-jdk1.8-0.0.1
MAINTAINER lh0811
ADD ./target/${JAR_FILE} /opt/app.jar
RUN chmod +x /opt/app.jar
CMD java -jar /opt/app.jar

十、源码获取

源码和开发笔记

作者:我先失陪了
来源:https://juejin.cn/post/7100457917115007013

收起阅读 »

软件开发生命周期(SDLC)完全指南:6个典型阶段+6个常用开发模型

本文和您讨论了SDLC的6个典型阶段、以及6个常用开发模型,并给出如何根据不同的项目特征,选择这些开发方法的建议。译者 | 陈峻审校 | 孙淑娟软件开发生命周期(Software Development Life Cycle,SDLC)包含了软件从开始到发布的...
继续阅读 »

本文和您讨论了SDLC的6个典型阶段、以及6个常用开发模型,并给出如何根据不同的项目特征,选择这些开发方法的建议。

译者 | 陈峻

审校 | 孙淑娟

软件开发生命周期(Software Development Life Cycle,SDLC)包含了软件从开始到发布的不同阶段。它定义了一种用于提高待开发软件质量和效率的过程。因此,SDLC旨在通过最少的资源,交付出高质量的软件。为了避免产生严重项目失败后果,软件开发的生命周期通常可以被划分为如下六个阶段:

  • 需求收集

  • 设计

  • 软件开发

  • 测试和质量保证

  • 部署

  • 维护

值得注意的是,这些阶段并非是静态的,它们可以进一步地被分解成多个子类别,以适应独特的开发需求与流程。

图 1 软件开发生命周期

需求收集

这是整个周期中其他阶段的基础。在此阶段,所有利益相关者(包括客户、产品负责人等)都会去收集与待开发软件相关的信息。对此,项目经理和相关方会频繁召开会议。尽管此过程可能比较耗时,但是我们不可急于求成,毕竟大家需要对将要开发的产品有个清晰的了解。

利益相关方需要将收集到的所有信息,记录到软件需求规范(Software Requirement Specification,SRS)文档中。在完成了需求收集后,开发团队需要进行可行性研究,以确定项目是否能够被完成。

设计

此阶段旨在模拟软件应用的工作方式,并设计出软件蓝图。负责软件高级设计的开发人员将组成设计团队,并通过由上个阶段产生的SRS文档,来指导设计过程,并最终完成满足要求的体系结构。此处的高级设计是指包括用户界面、用户流程、通信设计等方面在内的基础要素。

软件开发

在此阶段,具有不同专业知识(例如前端和后端)的开发人员或工程师,会通过处理设计的需求,来构建和实现软件。这既能够由一个人,也可以由一个大型团队来执行,具体取决于项目的规模。

后端开发人员负责构建数据库结构和其他必要组件。最后,由前端开发人员根据设计去构建用户界面,并按需与后端进行对接。

在配套文档方面,用户指南会被创建,源代码中也应适当地留下相应的注释。也就是说,为了保证良好的代码质量,适当的开发指南和政策也是必不可少的。

测试

专门的测试人员协同开发团队在此阶段开展测试工作。测试既可以与开发同时进行,也可以在开发阶段结束时再开展。通常,开发人员在开发软件时就会进行单元测试,以便检查每个源代码单元是否能够按照预期工作。同时,此阶段也包括如下其他测试:

  • 系统测试--通过测试系统,以验证其是否满足所有指定的需求。

  • 集成测试--将各个模块组合到一起进行测试。测试团队通过单击按钮,并执行滚动和滑动操作,来与软件交互。当然,他们并不需要了解后端的工作原理。

  • 用户验收测试--是在启动软件之前,邀请潜在用户或客户进行的最终测试。此类测试可以验证目标软件,是否能够根据需求的规范,处理各种真实的场景。

测试对于软件开发生命周期是至关重要的。倘若无法以正确的方式开展,则会让软件项目团队反复在开发和测试阶段之间徘徊,进而影响到成本和时间。

部署

完成测试后,我们就需要通过部署软件,来方便用户使用了。在此阶段,部署团队需要通过遵循若干流程,来确保部署流程的成功。无论是简单的流程,还是复杂的部署,都会涉及到创建诸如安装指南、系统用户指南等相关部署文档。

维护

作为开发周期的最后阶段,维护涉及到报告并修复在测试期间未能发现的错误。在修复方式上,我们既能够采取立即纠正错误的方式,也可以将其作为常规性的软件更新。

此外,软件项目团队还会在此阶段从用户处收集反馈,以协助软件的改进,并提高用户的软件使用体验。

SDLC方法

虽然SDLC通常都会遵从上述步骤,但是它们在实现方式上略有不同。下面,我将介绍排名靠前的6种SDLC方法:

  • 瀑布

  • 敏捷

  • 精益

  • 迭代

  • 螺旋

  • DevOps方法

瀑布方法

图 2 瀑布方法

作为最古老、也是最直接的SDLC方法,瀑布方法遵循的是线性执行顺序。如上图所示,从需求收集到维护,逐步依次推进,且不存在任何逆转或倒退的步骤。也就是说,只有当上一步完成后,才能继续下一步。

由于在设计阶段之后,该方法不存在任何变化或调整的余地,因此,我们需要在需求收集阶段,收集到有关项目的所有信息,即制作软件蓝图。可见,对于经验不足的开发团队而言,如果能够保证软件的需求从项目开始就精确且稳定的话,便可以采用瀑布方法。也就是说,瀑布模型的成功,在很大程度上取决于需求收集阶段的输出是否清晰。当然,它也比较适合那些耗时较长的项目。

瀑布的优势

  • 需求在初始阶段就能够被精心设计。

  • 具有容易理解的线性结构。

  • 易于管理。

瀑布的缺点

  • 既不灵活,又不支持变更。

  • 任何阶段一旦出现延迟,都会导致项目无法推进。

  • 由于较为死板,因此项目总体时间较长。

  • 并不鼓励在初始阶段之后,利益相关者进行积极地沟通。

敏捷方法

图 3 敏捷方法生命周期

敏捷(Agile)即为快速轻松的移动能力。以沟通和灵活性为中心的敏捷原则与方法,提倡以更短的周期和增量式地进行部署与发布。

在敏捷开发的生命周期中,每个阶段都有一个“仪式(ceremony)”,以便从开发团队和参与项目的其他利益相关者处获取反馈。其中包括:冲刺(sprint)计划、每日scrum、冲刺评审、以及冲刺回顾。

总地说来,敏捷开发是在各个“冲刺”中进行的,每个冲刺通常持续大约2到4周。每个冲刺的目标不一定是构建MVP(最小可行产品,Minimum Viable Product),而是构建可供客户使用的软件的一小部分。其交付出来的可能只是某个功能,而非具有完全功能的产品。也就是说,交付成果可能只是一个将来能够被慢慢增加的功能性服务,而不一定是MVP。

图 4 构建最小可行产品的示例

在每个冲刺结束后的冲刺审查阶段,如果利益相关者对开发的功能感到满意的话,方可开展下一轮冲刺。虽然新的功能是在冲刺中被开发的,但是整个项目期间的冲刺数量并不受限。它往往取决于项目和团队的规模。因此,敏捷方法最适用于那些从一开始就无法明确所有要求的项目。

敏捷的优势

  • 适合不断变化的需求。

  • 鼓励利益相关者之间的反馈和持续沟通。

  • 由于采用了增量式方法,因此更易于管理各种潜在风险。

敏捷的缺点

  • 最少量的文档。

  • 需要具有高技能的资源。

  • 如果沟通低效,则可能拖慢项目的速度。

  • 如果过度依赖客户的互动,则可能会导致项目走向错误的方向。

精益方法

软件开发领域的精益方法源于精益制造的原则。这种方法旨在减少生产过程中的浪费和成本,从而实现利润的最大化。该方法虽与敏捷开发类似,但是侧重于效率、快速交付、以及迭代式开发。而区别在于,敏捷方法更专注于持续沟通和协作,以体现价值;而精益方法更专注于消除浪费,以创造客户价值。

精益方法的七个核心概念:

  • 消除浪费--鼓励开发团队尽可能多地消除浪费。这种方法在某种程度上并不鼓励多任务处理。这意味着它只需要完成“份内”的处理工作,并通过节省构建所谓“锦上添花”的功能,来节省时间。同时在所有开发阶段都避免了不必要的文档和会议。

  • 鼓励学习--通过鼓励创建一个有利于所有相关成员学习的环境,来促进团队对软件开发过程予以反馈。

  • 推迟决定--在做出决定之前,应仔细考虑各种事实。

  • 尽快交付--由于交付是基于时间的,因此它会专注于满足交付期限的增量式交付,而非大礼包式的发布。

  • 团队授权--它避开了针对团队的微观管理,而是鼓励大家积极地参与到决策过程中,让彼此感到参与了重要的项目。它不但为团队成员提供了指导方向,而且为失败留出了足够的空间。

  • 构建质量--由于在开发周期的所有阶段都关注客户价值,因此它会定期进行有关质量保证的各项测试。

  • 整体优化--通过关注整个项目,而不是单独的项目模块,来有效地将组织战略与项目方案相结合。

精益方法的优势

  • 由于团队参与到了决策之中,因此创造力得到了激发。

  • 能够尽早地消除浪费,降低成本,并加快交付的速度。

精益方法的缺点

  • 对于纪律性较差的团队而言,它不一定是最佳选择。

  • 项目目标和重点可能会受到诸多灵活性的影响。

迭代方法

图 5 迭代开发模型

开发界引入迭代方法作为瀑布模型的替代方案。它通过添加迭代式重复性开发周期,来克隆瀑布方法的所有步骤。由于最终产品的各个部分在完成后,才在每次迭代结束时发布的,因此这种方法也属于增量式。具体而言,迭代方法的初始阶段是计划,而最后一个阶段是部署。介于两者之间的是:计划、设计、实施、测试和评估的循环过程。

迭代方法虽与敏捷方法类似,但是它涉及的客户参与度较少,并且具有预定义的增量范围。

迭代的优点

  • 在早期阶段,它能够生成产品的可运行版本。

  • 其变更的成本更低。

  • 由于产品被分成较小的部分,因此更易于管理。

迭代的缺点

  • 可能需要更多的资源。

  • 有必要全面了解各项需求。

  • 不适合小型项目。

螺旋方法

作为一种具有风险意识的软件开发方法,螺旋方法侧重于降低软件开发过程中的各项风险。它属于一种迭代的开发方法,在循环中不断推进。由于结合了瀑布模型和原型设计,因此螺旋方法是最灵活的SDLC方法,并具有如下四个主要阶段:

  • 第一阶段--定义项目目标并收集需求。

  • 第二阶段--该方法的核心是进行全面的风险分析和计划,消减已发现的风险。产品原型会在本阶段交付出来。

  • 第三阶段--执行开发和测试。

  • 第四阶段--涉及评估已开发的内容,并计划开展下一次迭代。

螺旋方法主要适用于高度定制化的软件开发。此外,用户对于原型的反馈可以在迭代后期(在开发阶段)扩展各项功能。

螺旋方法的优势

  • 由于引入了广泛的风险分析,因此尽可能地避免了风险。

  • 它适用于较大型的项目。

  • 可以在迭代后期添加其他功能。

螺旋方法的缺点

  • 它更关注成本收益。

  • 它比其他SDLC方法更复杂。

  • 它需要专家进行风险分析。

  • 由于严重依赖风险分析,因此倘若风险分析不到位,则可能会使整个项目变得十分脆弱。

DevOps方法

图 6 DevOps方法

在传统的软件开发方法中,开发人员和运维人员之间几乎没有协作。特别是在运营过程中,开发人员往往被视为“构建者”的角色。这就造成了沟通和协作上的差距,以及在反馈过程中出现混淆。而软件开发的DevOps方法恰好弥合了两者之间的沟通鸿沟。其目标是通过将开发和运营团队有效地结合起来,以快速地开发出更可靠的优质软件。值得一提的是,DevOps也是一种将手动开发转换为自动化软件开发的方法。通常,DevOps方法会被划分为如下5个阶段:

  • 持续开发--此阶段涉及到软件应用的规划和开发。

  • 持续集成—此阶段会将新的功能性代码与现有的代码相集成。

  • 持续测试--开发团队和QA测试人员会使用maven和TestNG等自动化工具开展测试,以确保在新的功能中扫清缺陷。自动化测试为各种测试用例的执行节省了大量时间。

  • 持续部署--此阶段会使用类似puppet的配置管理工具、以及容器化工具,将代码部署到生产环境(即服务器上)。它们还将协助安排服务器上的更新,并保持配置的一致性。

  • 持续监控—运营团队会在此阶段通过使用Nagios、Relix和Splunk等工具,主动监控用户活动中的错误、异常、不当的软件行为、以及软件的性能。所有在此阶段被发现的问题都会被传递给开发团队,以便在持续开发阶段进行修复,进而提高软件的质量。

DevOps的优势

  • 促进了合作。

  • 通过持续开发和部署,更快地向市场交付软件。

  • 最大化地利用Relix。

DevOps的缺点

  • 当各个团队使用不同的环境时,将无法保证软件的安全。

  • 涉及到人工输入的过程时,可能会减慢整体运营的速度。

小结

综上所述,软件开发生命周期中的每一个阶段都是非常重要的。我们只有正确地执行了每个步骤,才能最大限度地利用现有资源,并交付出高质量、可靠的软件。

事实上,软件开发并没有所谓的“最佳”方法,它们往往各有利弊。因此在选择具体方法之前,您需要了解待选方法对手头项目的实用性。当然,为了尽可能地采用最适合现有流程的方法,许多公司会同时使用两种不同方法的组合,通过取长补短来实现有效的融合,并相辅相成地完成软件的交付任务。

译者介绍

陈峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验;持续以博文、专题和译文等形式,分享前沿技术与新知;经常以线上、线下等方式,开展信息安全类培训与授课。

原文标题:The Complete Guide to SDLC,作者:Mario Olomu

收起阅读 »

B站崩的那晚,连夜谋划了这场稳定性保障SRE升级之战

本文分享主题是B站SRE在稳定性方面的运营实践。随着B站近几年的快速发展,业务规模越来越大,迭代速度越来越快,系统运行复杂度也越来越高。线上每天都会发生各种各样的故障,且发生的场景越来越刁钻。为了应对这种情况,保障业务在任何时刻都能将稳定性维持在一个高基线之上...
继续阅读 »

本文分享主题是B站SRE在稳定性方面的运营实践。

随着B站近几年的快速发展,业务规模越来越大,迭代速度越来越快,系统运行复杂度也越来越高。线上每天都会发生各种各样的故障,且发生的场景越来越刁钻。为了应对这种情况,保障业务在任何时刻都能将稳定性维持在一个高基线之上,B站专门成立了SRE体系团队,在提升业务稳定性领域进行了全方位、体系化的积极探索,从理论性支撑和能力化建设进行着手,从故障应急响应、事件运营、容灾演练、意识形态等多方面进行稳定性运营体系的构筑。

本次分享主题是B站SRE在稳定性方面的运营实践,分享内容分为以下几个部分:

  • 案例剖析

  • 从应急响应看稳定性运营

  • 核心运营要素有哪些

  • 两个运营载体:OnCall与事件运营中心

  • 挑战与收益

一、案例剖析

多年来业界同仁针对稳定性这一话题进行了大量的探索和实践,业界不乏针对稳定性保障相关的讨论和研究。在围绕稳定性的实践上,大家也经常听到诸如混沌工程、全链路压测、大促活动保障和智能监控等话题的分享。B站在这些方面也做了很多建设工作,今天我将从应急响应的角度切入,和大家分享B站在稳定性运营方面所做的工作。

我们先来看两个案例,案例以真实事件为依托,相关敏感内容进行了改写。

案例一

1)背景

一个手机厂商的发布会在某天晚上12点举办,B站承接了该品牌的线上发布会直播。公司的运营同学提前就配置好了12点的直播活动页面以及准点的应用内Push消息。在12点到来之后,直播活动页推送生效,大量用户收到应用内推送消息,点击进入直播活动页面。

2)故障始末

12点01分,直播活动页内嵌的播放器无法支持部分用户正常加载播放。

12点03分,研发同学李四收到了异常的报警,开始介入处理。

12点04分,客服同学收到了大量有关发布会无法正常播放的用户反馈,常规处理方法无法解决用户问题。

影响持续到12点15分,研发同学李四还在排查问题的具体原因,没有执行相对应的止损预案(该种问题有对应预案),问题持续在线上造成影响。

直到12点16分,老板朋友找到了老板反馈今晚B站的某品牌手机直播发布会页面视频无法正常播放,此时老板开始从上往下询问,研发leader知道了这件事,开始联系SRE同学介入问题处理,并及时执行了相关的切换预案,直播活动页面播放恢复正常。

3)问题

在这个案例中,暴露了以下一些问题:

  • 故障的相关告警虽然及时,但是并没有通知到足够多对的人。

  • 该故障的告警,在短时间没有处理响应后,并未进行有效的结构性升级(管理升级,及时让更高level的人参与进来,知晓故障影响,协调处理资源)和职能性升级(技术升级,让更专业和更对口的人来参与响应处理,如team leader、SRE等)。

  • 一线同学往往容易沉迷于查找问题根因,不能及时有效地对故障部位进行预案执行。

案例二

1)背景

一个平淡无奇的周末晚上,23点30分,监控系统触发大量告警,几乎全业务线、各架构层都在触发告警。

2)故障始末

23点40分,企业微信拉了有十几个群,原有的业务沟通群、基础服务OnCall群,都在不停地转发告警,询问情况。整个技术线一片恐慌,起初以为是监控系统炸了。此时相关故障的SRE同学已经被拉到某一个语音会议中。

注意,此时公司的多个BU业务线同学都炸锅了,到处咨询发生了什么,业务怎么突然就不炸了。又过了几分钟,资深的SRE同学又拉了一个大群,把相关业务对接人都拉进群里,开始整体说明故障情况。此时,该同学也比较纠结如何通报和说明这个问题,因为此时没有一个明确故障定位,语言很难拿捏,各个高level的老板也都在问(已上热搜)。并且,负责恢复入口服务的一线同学把故障预案执行了个遍,发现无济于事。后续在GSLB调度层,执行了整个跨机房的流量有损切换才让服务逐渐恢复正常。

凌晨之后,原有机房的问题定位出来了,补丁迅速打上,异常的问题彻底修复了。后续,在对此事件进行复盘时,发现困难重重。因为故障处理过程中,涉及到大量的服务、组件和业务方,并且大家当时拉了一堆群,同样的消息被发送到了很多群。参与处理故障的同学在语音、电话和企微群都有进行过沟通和处理进展发布,整个事件的细节整理起来非常耗费人力和精力,准确性也很难保障。

3)问题

  • 在上面这个案例中,我们可以看到整个故障从发生、处置到结束后复盘,都存在以下问题:

  • 当一个影响面比较大的故障产生时,大家没有统一的故障进展同步方式,依托原始的人工拉群,人工找相关人员电话联系,导致了故障最新的进展情况只能够在小范围传播扩散,无法统一对外公布,并且在传播过程中,很容易消息失真;

  • 在故障处理过程中,缺少主要协调人(故障指挥官)。像这种大型故障,需要有一个人能够去协调各层人员分工、消息收敛、服务业务情况等,这个人需要能够掌控整个故障的所有消息和全貌进展;

  • 在故障处理过程中,缺乏故障上下文的信息关联,大家难以知晓故障发生的具体位置,只是感知到自己的业务受损了,自己的服务可能有异常,这导致整个故障的定位时间被拉长;

  • 在故障恢复之后,我们对这个故障进行复盘时,发现故障处理过程中的信息太过零散,复盘成本很高。

案例剖析

通过对上述两个案例的分析我们能够发现,在故障发生前、处理中和结束后,各个阶段都会有很多因素导致我们的故障不能被快速解决,业务不能快速恢复。

这里我们从故障的前、中、后三个阶段总结可能存在的一些问题。

1)事前

  • 告警信息量大,信息太过杂乱;

  • 平台系统繁多,变更信息无处收敛;

  • 客服反馈的信息,需要靠人工去关联,并反馈到技术线;

  • 和公司舆情相关的信息,技术线很难感知到。

2)事中

  • 一线同学过于关注技术,沉迷问题解决;

  • 当一个故障影响面扩大之后,涉及多个团队的协同非常困难,最新的进展消息无法及时有效地传递到相关人员手中;

  • 当参与一个故障处理的人员多了之后,多个人员之间缺乏协调,导致职责不清晰,产生事情漏做、重复做的问题;

  • 故障处理过程中,会有一些不请自来,凑热闹的同学。

3)事后

  • 当我们开展复盘时,发现故障处理时又是群、又是电话、又是口头聊,又是操作各种平台和工具,做了很多事情,产生了很多信息,梳理时间线很繁琐,还会遗漏,写好一份完整的复盘报告非常麻烦;

  • 拉一大堆人进行复盘的时候,因为缺少结构化的复盘流程,经常是想到什么问什么。当某场复盘会,大家状态好的时候,能挖掘的点很多。如果状态不好或者大家意识上轻视时,复盘的效果就较差;

  • 复盘后产出的改进事项,未及时统一地记录跟进,到底有没有完成,什么时间应该完成,完成的情况是否符合预期都不得而知;

  • 对于已经修复的问题,是否需要演练验收确保真正修复。

以上三个阶段中可能发生的各种各样的问题,最终只会导致一个结果:服务故障时间增长,服务的SLA降低。

二、从应急响应看稳定性运营

针对上述问题,如何进行有效改善?这是本部分要重点分享的内容,从应急响应看稳定性运营。

应急响应的概念较早来源于《信息安全应急相应计划规范GB/T24363-2009》中提到的安全相关的应急响应,整体定义是“组织为了应对突发/重大信息安全事件的发生所作的准备,以及在事件发生后所采取的措施”。从这个概念我们延伸到稳定性上就产生了新的定义,“一个组织为了应对各种意外事件的发生所作的准备以及在事件发生后所采取的措施和行为”。这些措施和行为,通常用来减小和阻止事件带来的负面影响及不良后果。

三、核心运营要素有哪些

做好应急响应工作的核心目标是提升业务的稳定性。在这个过程中,我们核心关注4大要素。核心点是事件,围绕事件有三块抓手,分别是人、流程和工具/平台。

人作为应急响应过程中参与和执行的主体,对其应急的意识和心态有很高要求。特别是在一些重大的故障处理过程中,不能因为压力大或紧张导致错误判断。

流程将应急响应的流程标准化,期望响应人能够按照既定阶段的既定章程进行有效的推进和处理。

工具/平台支撑人和流程的高效合规运行,并将应急响应的过程、阶段进行度量,进而分析和运营,推进人和流程的改进。

事件

1)生命周期划分

要对故障进行有效运营,就需要先明确故障的生命周期。通过划分故障的生命周期,我们可以针对不同的周期阶段进行精准聚焦,更有目的性地开展稳定性提升工作。

针对故障生命周期的划分有很多种方式,按故障的状态阶段划分,可以分为事前、事中和事后。

按故障的流程顺序划分,可以分为故障防御、故障发生、故障响应、故障定位和故障恢复、复盘改进等阶段。

这里我围绕故障的时间阶段,从故障不同阶段的形态变化做拆分,将故障拆分为四个阶段。

  • 告警/变更/客诉

当故障还未被确认时,它可能是一个告警、变更或客诉。

  • 事件

当这个告警、变更、客诉被上报后,会产生一个事件,我们需要有人去响应这个事件,此时一个真正的事件就形成了。

  • 故障

当事件的影响范围逐渐扩散,这时往往有大量的用户、业务受到影响,我们需要将事件升级成故障,触发故障的应急协同,进行一系列的定位、止损等工作。

  • 改进

故障最终会被恢复,接下来我们要对故障进行复盘,产生相关改进项,在改进项被完成之后,还需要进行相关的验收工作。

2)阶段度量

从更科学的角度看,我们知道在运营工作中,度量是很关键的一点。管理学大师彼得·德鲁克曾经说过:“你如果无法度量它,就无法管理它”。有效的度量指标定义,可以帮助我们更好更快地开展运营工作、评估价值成果。上文中我们提到的3个阶段是比较笼统的阶段,接下来我将介绍更加具体和可执行的量化拆分方法。

如上图所示,从故障预防依次到故障发现,故障定位,故障恢复,最后到故障改进,整体有两个大的阶段:MTBF(平均无故障时间)和MTTR(平均故障恢复时间)。我们进行业务稳定性运营的核心目标就是降低MTTR,增加MTBF。根据Google的定义,我们将MTTR进一步拆分为以下4个阶段:

  • MTTI:平均故障发现时间,指的是故障发生到我们发现的时间。

  • MTTK:平均故障定位时间,指的是我们发现故障到定位出原因的时间。

  • MTTF:平均故障修复时间,指的是我们采取恢复措施到故障彻底恢复的时间。

  • MTTV:平均故障修复验证时间,指的是故障恢复之后通过监控、用户验证真实恢复的时间。

3)关键节点

基于阶段度量的指标,我们能够得到一系列的关键时间节点。在不同的阶段形态,事件和故障会存在一些差异。故障因为形态更丰富,所存在的时间节点更多。上图中定下来的时间,均是围绕MTTR进行计算的。主要是为了通过度量事件、故障的处理过程,发现过程中存在的问题点,并对问题点进行精准优化,避免不知道如何切入去提升MTTR的问题,也方便我们对SRE的工作进行侧面考核。

人作为事件的一个主体,负责参与事件的响应、升级、处置和消息传播。

人通过上文中我们讲到的OnCall参与到应急响应中。我们在内部通过一套OnCall排班系统进行这方面的管理。这套系统明确了内部的业务、职能和人员团队,确保人员知道什么时间去值什么业务的班。下面的工具/平台部分会展开介绍。对参与人的要求,主要有以下几点:

  • 具备良好的响应意识和处理心态。

  • 具备熟练地响应执行的经验。

满足以上特征,才能做到故障来临时响应不慌乱,有条不紊地开展响应工作。

流程

那么针对人的要求如何实现?人员如何参与到应急响应的环节中去?人员的意识如何培养呢?

首先,我们内部制定了应急响应白皮书,明确了以下内容:

  • 响应流程;

  • 基于事件大小,所要参与的角色和角色对应的职责;

  • 周边各个子系统SOP制定的标准规范;

  • 针对应急过程中的对外通告内容模板;

  • 故障过程的升级策略。

之后,我们会周期性地在部门内部、各BU进行应急响应宣讲,确保公司参与OnCall的同学能够学习和掌握。另外,我们也会将其作为一门必修课加入新同学的入职培训中。最后就是故障演练,通过实操,让没有参与过故障处理的新同学能够实际性地参与应急响应的过程,避免手生。

平台

平台作为支撑人与流程进行高效、稳定执行的载体,我将在下一部分进行具体描述。

四、两个运营载体:OnCall与事件运营中心

这部分我将向大家分享B站在应急响应方面落地的两个运营性平台。

OnCall系统

OnCall系统,即值班系统。值班系统在日常运转过程中的作用往往被低估,SRE、工程效率做这部分建设时,很容易基于二维的方式对人和事进行基于日历的值班管理,并通过网页、OpenAPI等方式对外提供数据服务。

下面我们通过几个例子来说明OnCall的必要性。

在日常工作中,当我们使用公司某个平台功能时,可能会习惯性找熟悉的同学,不管他这一天是不是oncall。这可能给那位同学带来困扰,他可能上周才值完班,这周要专心于研发或者项目的推进,你随时找他可能打断他的工作节奏。还有一种情况是新同学不知道该去找谁,我们内部之前经常有这种情况,一个新来的同学接手一套系统之后,有问题不知道该找谁,经常要转好几手,才能找到对的人,这一过程很痛苦。

以上内容总结起来就是总找一个人,总找不到人,除此之外,还会出现平台找不到人的情况。这些问题的根源是什么呢?无非就是who、when、what和how的问题,不能在正确的时间为正确的事找到正确的人。

那么OnCall系统的重要性和必要性都体现在哪些方面呢?

  • 有问题找不到人

随着公司业务规模的扩大和领域的细分,一些新的同学和新业务方往往会出现一个问题。不知道是哪些人负责,需要咨询很多人才能找到具体解决问题的人。这一问题不仅限于故障,更存在于日常琐事中。特别是SRE同学的日常,经常会被研发同学咨询找人拉群,戏称拉群工程师。

  • 下班不下岗

当人们遇到问题时,经常会下意识找熟悉的人。这就导致一些能力强、服务意识好的同学,总是在被人找。不论他今天值不值班,他将无时无刻都要面临被人打扰的问题。除了被人找之外,内部的监控系统、流程系统,也会给不值班的同学发送监控告警和流程审批信息。这也将SRE同学有50%的时间用于工程这一愿景变成泡影。

1)设计

① 明确关联逻辑

针对上述两种情况,我们对公司的业务、服务、职能和组织架构进行了分析建模,明确了人、团队、职能和业务之间的关联关系。

② 建立三维合一模型

我们构建起了一套三维合一的模型。由组织-业务、职能-人员、组织-职能的关联关系,产生交汇点。值班人员会通过值班小队的方式,落在这些交汇点上,并且基于业务和基础架构的异同点,通过业务视角和职能视角分别对外提供服务。

以我们公司内部主站业务为例,我们会有专门的SRE小队进行日常的值班响应,这个小队只负责主站业务的值班响应。通过这样的对应关系,当人或平台有需求的时候,都可以通过业务和职能关联到对应实践的值班小队,最终定位到具体的人,这样也帮助我们将人藏了起来,更有利于后续SRE轮岗机制的推进落地。

③ 双视角提供服务

通过双视角的设计,区分了职能型和业务型的不同值班方式和关注点。原因在于B站的业务层级组织模式是按照“组织->业务->应用”这三级进行组织的,所有的应用归属到业务,业务归属到具体的组织架构。

  • 职能视角

前端采用树型展示,组成结构为组织->职能->覆盖范围(组织->业务->服务),值班表具体挂载在覆盖范围下,覆盖范围可以只有一级组织也可以精确到组织下面的业务或业务下面的服务。

  • 业务视角

前端采用树型展示,组织结构为组织->业务->职能,值班表具体挂载在职能下面。

在日常工作中,基础架构相关的服务,比如SRE、DBA、微服务、监控、计算平台等强职能型服务会通过职能视角对外提供值班信息。当业务人员有具体问题时,可以通过职能树快速定位到具体的值班人员。而对于业务服务来讲,日常的工作模式是围绕业务开展的,因此会通过业务进行展开,提供该业务下相关职能的对应值班信息。

这两个视角的底层数据是相通的,强职能相关服务提供方只需要维护好职能视角的值班信息,业务视角下的关联会自动生成。

2)功能展示

基于以上设计,我们内部做了一套OnCall排班的系统。

这套系统是管理业务、职能和人的系统。我们基于上文中提到的几个核心概念,在这些概念间建立了关系,分别是两条线,一条是职能-团队和人,另外一条是职能-业务和服务。

系统提供了排班班组的管理,支持基于日历的排班,同时支持班组设置主备oncall角色。在排班的细节上,我们支持基于时段进行自动排班计划生成,也支持在一个职能里多个班组混合排班。另外,也支持对已经排好班的班组,进行覆盖排班和换班,这个主要应对oncall同学突然有事请假的情况。在oncall通知方面,我们支持了企业微信、电话等通知方式,并且支持虚拟号码的方式,保护员工号码不对外泄露。同时也避免了因为熟悉导致的频繁被打扰的情况。

在周边生态方面,这套OnCall系统完全依赖周边系统的接入。我们目前对接了内部的告警系统、流程系统,确保告警和流程能够只通知oncall人,而不形成骚扰。在企业微信的服务号中,也进行了H5的页面嵌入,在用户通过企业微信反馈问题、找人时,知道当下该找谁。在各个接入的平台,也内嵌了OnCall的卡片页面,明确告诉用户本平台当前是谁值班。通过这套OnCall系统的落地,我们明确了人、团队、职能和业务的概念,并将这些概念进行了关系建立和对应。人员通过排班班组统一对外为某个业务提供某项职能的值班响应。通过前端的可视化,提供了日历的值班展示效果,可以直观看到某个业务所需要的某块职能在某个时间点是由哪个班组在服务,周边的各个系统也都对接OnCall系统,实现了人员响应的统一管理,解决了某些人员认人不认事,不通过正规流程处理的问题。

事件运营中心

事件运营中心这套系统是我们基于ITIL、SRE、信息安全应急计划的事件管理体系,为了满足公司对重大事件/故障的数字化管理,实现信息在线、数据在线和协同在线,使组织能够具备体系化提升业务连续性能力所做的产品平台。这个平台的定位是一站式的SRE事件运营中心,数字化运营业务连续性的目标是提升MTBF,降低MTTR,提高SLA。

1)架构设计

上图是我们平台的模块架构图,整体上还是围绕上文提到的事件的事前、事中和事后三个阶段拆分,覆盖了一个事件产生、响应、复盘、改进的全生命周期。

  • 事前

我们对事件进行了4大类型的拆分,分别是告警、变更、客诉和舆情,然后通过设计标准的事件上报协议,以集成的方式将我们内部的各个系统打通,将事件信息统一收集到一起。在收集的过程中,进行二次处理,实现事件的结构化转储。

  • 事中

我们会对接入的4大类型信息进行事件转化,然后通过预定义的规则对事件进行降噪,抑制、报警、召回、分派和升级等相关操作。在这个过程中,如果一个事件被判定影响到业务,我们会将它升级成一个故障,然后触发故障的应急响应流程。这里就进入到对故障响应处理过程中的一个阶段,在这个阶段我们会产生各种角色,例如故障指挥官、故障通讯人员、故障恢复人员等,相关人员明确认领角色,参与故障的止损。止损过程中,通过平台一键拉群创建应急响应指挥部,通过平台的进展同步进行相关群和业务人员的通告,通过记录簿实现故障信息的信息传递和记录。

  • 事后

在故障结束之后,就进入到我们整体的改进环节。平台可以基于故障一键创建复盘报告,自动关联故障处理过程中的专家数据。平台提供预制的故障复盘问答模板,以确认各阶段是否按照预期的目标进行。复盘产生的待办列表,平台会进行定期的状态提醒和处理进度跟进。最终的这些都会以知识的形式,沉淀在我们的知识库。帮助日常On-Call问答和公司内部员工的培训学习。整体这样一套平台流程下来,就实现了将一些日常高频的非结构性事务驱动,通过统一接入、精准触达、事件闭环和持续改进等环节,转化为低频的结构化数据驱动方式。

2)场景覆盖

下面我们介绍平台在各个场景的覆盖。

① 集约化

对事件产生的上游来源进行集约化管理,通过队列订阅、API和SDK的方式,将内部的监控,Prometheus、监控宝等各个云平台的监控都通过前面的4大类型定义收归到一起,然后统一进行通知触达。

② 标准事件类型

为了实现各个渠道消息的结构化规约,我们设计了标准的事件模型,通过这个事件模型,我们将周边各个系统/工具/平台进行收集,并且实现了事件的关联操作。模型主要分为4部分:

  • base是一些事件的基础信息字段;

  • who是指这一事件来自哪里,有哪些相关方;

  • when是指事件发生或产生影响的时间;

  • where是指事件来源于哪个业务、影响了哪些业务;

  • what是指这个事件的具体内容,它的操作对象是什么等等。

③ 降噪聚类

由于我们对事件的上报结构进行了标准化,并且预埋了关联字段,通过这些关联字段,我们就建立起了事件的关联关系,从而可以做事件的降噪聚类。

降噪聚类在执行上,主要分为两部分。

  • 横向抑制

我们支持对单个来源的事件、告警,通过定义的规则进行收敛,比如Prometheus报警出一个服务在某个持续时间内持续报了N条一样的告警信息,平台会收敛到一个事件中去。

  • 纵向抑制

这对上文中提到的底层系统故障十分有效,可以将底层故障导致的上层业务告警都统一收到一个事件中,避免大量告警使大家造成混淆。

④ 协同在线

在协同在线的场景下,我们通过一个面板将人、业务、组件和系统信息进行了汇总,通过一个事件详情页,将整个事件当下的处理人、关联业务和服务组件、当下的一些信息统一展示在一起。在协同能力上,我们提供了一键创建应急响应群的功能,建群时会自动关联该故障相关oncall同学,对故障感兴趣的同学也可以通过面板加入响应群。在故障页面,清晰看到故障当前的指挥官是谁,当下的处理人是哪些同学。彻底解决了之前靠人工拉语音、打电话、面对面交流的原始协作方式。

平台的各方面能力实现了事件全生命周期的闭环管理。监控告警、故障发现、应急响应、故障定位、故障恢复、故障复盘和故障改进,全阶段都通过平台能力去承载。

  • 故障响应时,支持了故障的全局应急通告,提供了多种通告渠道,信息实时同步不延误,避免人工同步,漏同步、同步内容缺漏等问题;

  • 故障跟踪阶段,平台可以实时展示最新的故障进展;故障影响面、当下处置情况,各阶段时间等等;

  • 故障结束的复盘阶段,通过定义好的结构化、阶段化的复盘过程,确保复盘过程中,该问的问题不遗漏,该确认的点都确认到;

  • 故障改进阶段,通过对改进项的平台化录入,关联相关责任方、验收方,确保改进的有效执行和落实。

上图中是协同相关的一些示例,当一个故障被创建出来时,会自动关联该故障涉及到的业务、组件、基础设施的oncall同学,这些同学可能是SRE、研发等,平台会记录他们是否有响应这些问题,并且当下所负责的角色是什么。因为角色决定了在该事件中所担负的事项和责任;下方一键拉群,可以将相关人员,自动拉入到一个群内,方便大家进行沟通协同,并且事件、故障的相关最新进展也会定期在群内同步;涉及到事件的参与人员,事件运营中心的服务号也会定期推送最新进展,确保不会丢失消息。

上图是我们内部的故障协同的详情页面,提供了记录簿、故障阶段更新、最近变更信息和相似事件,确保每次的响应处理,都能形成一个专家经验的沉淀,帮助后续新来的同学进行故障响应过程的学习。

复盘方面,我们定义了结构化的故障复盘模板,像相关人员、组织、影响情况、处置过程、根因分析(在根因分析里面,我们设置了6问,确保对问题能够有深度地挖掘),改进措施等。在复盘效率方面,我们关联了相关的变更信息、故障处理时的一些变更操作,以及处理时间线,帮助复盘同学快速生成故障的相关信息,减少人工录入负担。

五、挑战与收益

挑战

在业务稳定性运营体系的建设过程中,团队也踩了很多坑,面临着诸多技术之外的挑战。鉴于业界对于技术相关的分享比较丰富,这里就针对体系逻辑和人员方面的挑战进行分享。

  • 元信息统一

稳定性是个大话题,我们在落地整体体系时会发现,设计的上下游系统太多了。每个系统里面都会有人、业务、职能的使用需求。在初期,我们内部在服务、业务和人的关联这块没有形成统一的数据基准,导致我们在应急协同的诸多特性上难以落地,诸如故障的有效通知、群内的有效传递、故障画像的拓扑关联计算缺少映射关系等等。

在这种情况下,我们重新梳理了服务树和OnCall系统,通过服务树将组织、业务和服务的映射关系维护好,通过OnCall系统将组织、职能、业务和人的映射关系维护好,来确保找人的时候能找到人,找服务的时候能找到服务。

  • 工作模式改变

新的应急响应流程,将故障过往对人的依赖转移到靠系统来自行驱动,这导致现有人员的工作模式产生了很大变化。传统故障处理时,响应人员手动拉群、语音或现场找人,现在变成了优先在系统内升级已有事件或录入故障信息,然后通过系统自动进行人员关联和邀请。群内的随意沟通,变成了在平台进行阶段性进展同步。原有的故障升级逻辑变成了平台定时通知,这给故障处理人员带了一定的压迫感。整体形式上更加严肃和标准,在落地初期给大家带来了一定的不适应感。针对这种情况,我们一方面通过在系统的文案描述上进行改善,交互逻辑上进行优化,尽可能在推行新标准的同时,适应旧的使用习惯。例如,常规应急协同群会先于平台的故障通告建立,这就会与平台创建的故障协同群发生冲突。此时,我们通过增加现有群关联来实现已有故障协同群和故障的关联。另外一方面,我们通过定期持续的宣讲,给大家介绍新的应急响应流程和平台使用方法,帮助大家适应新的应急响应模式。

收益

以上就是B站在业务稳定性运营方面所做的相关工作。通过体系化建设,已经在组织、流程和平台层面实现强效联动,具备了数字化运营业务稳定性的能力,建立了科学有效的稳定性评估提升量化标准,让稳定性提升有数据可依托。将故障应急响应流程从由人工驱动升级到由平台系统驱动,应急响应人员可以更专心处理故障,大幅提升故障恢复时间。后续我们将会持续探索更科学有效的管理运营方法,期望通过引入AI的能力,提升故障辅助定位能力、提早发现故障隐患,联动预案平台实现更多场景的故障自愈。

责任编辑:张燕妮来源: dbaplus社群

收起阅读 »

API 工程化分享

本文是学习B站毛剑老师的《API 工程化分享》学习笔记,分享了 gRPC 中的 Proto 管理方式,Proto 分仓源码方式,Proto 独立同步方式,Proto git submodules 方式,Proto 项目布局,Proto Errors,服务端和客...
继续阅读 »

概要

本文是学习B站毛剑老师的《API 工程化分享》学习笔记,分享了 gRPC 中的 Proto 管理方式,Proto 分仓源码方式,Proto 独立同步方式,Proto git submodules 方式,Proto 项目布局,Proto Errors,服务端和客户端的 Proto Errors,Proto 文档等等

目录

  • Proto IDL Management

  • IDL Project Layout

  • IDL Errors

  • IDL Docs

Proto IDL Management

  • Proto IDL

  • Proto 管理方式

  • Proto 分仓源码方式

  • Proto 独立同步方式

  • Proto git submodules 方式

Proto IDL

gRPC 从协议缓冲区使用接口定义语言 (IDL)。协议缓冲区 IDL 是一种与平台无关的自定义语言,具有开放规范。 开发人员会创作 .proto 文件,用于描述服务及其输入和输出。 然后,这些 .proto 文件可用于为客户端和服务器生成特定于语言或平台的存根,使多个不同的平台可进行通信。 通过共享 .proto 文件,团队可生成代码来使用彼此的服务,而无需采用代码依赖项。

Proto 管理方式

煎鱼的一篇文章:真是头疼,Proto 代码到底放哪里?

文章中经过多轮讨论对 Proto 的存储方式和对应带来的优缺点,一共有如下几种方案:

  • 代码仓库

  • 独立仓库

  • 集中仓库

  • 镜像仓库

镜像仓库


在我自己的微服务仓库里面,有一个 Proto 目录,就是放我自己的 Proto,然后在我提交我的微服务代码到主干或者某个分支的时候,它可能触发一个 mirror 叫做自动同步,会镜像到这个集中的仓库,它会帮你复制过去,相当于说我不需要把我的源码的 Proto 开放给你,同时还会自动复制一份到集中的仓库

在煎鱼的文章里面的集中仓库还是分了仓库的,B站大仓是一个统一的仓库。为什么呢?因为比方像谷歌云它整个对外的 API 会在一个仓库,不然你让用户怎么找?到底要去哪个 GitHub 下去找?有这么多 project 怎么找?根本找不到,应该建统一的一个仓库,一个项目就搞定了

我们最早衍生这个想法是因为无意中看到了 Google APIs 这个仓库。大仓可以解决很多问题,包括高度代码共享,其实对于 API 文件也是一样的,集中在一个 Repo 里面,很方便去检索,去查阅,甚至看文档,都很方便

我们不像其他公司喜欢弄一个 UI 的后台,我们喜欢 Git,它很方便做扩展,包括 CICD 的流程,包括 coding style 的 check,包括兼容性的检测,包括 code review 等等,你都可以基于 git 的扩展,gitlab 的扩展,GitHub 的一些 actions,做很多很多的工作

Proto 分仓源码方式

过去为了统一检索和规范 API,我们内部建立了一个统一的 bapis 仓库,整合所有对内对外 API。它只是一个申明文件。

  • API 仓库,方便跨部门协作;

  • 版本管理,基于 git 控制;

  • 规范化检查,API lint;

  • API design review,变更 diff;

  • 权限管理,目录 OWNERS;


集中式仓库最大的风险是什么呢?是谁都可以更改

大仓的核心是放弃了读权限的管理,针对写操作是有微观管理的,就是你可以看到我的 API 声明,但是你实际上调用不了,但是对于迁入 check in,提到主干,你可以在不同层级加上 owner 文件,它里面会描述谁可以合并代码,或者谁负责 review,两个角色,那就可以方便利用 gitlab 的 hook 功能,然后用 owner 文件做一些细粒度的权限管理,针对目录级别的权限管理

最终你的同事不能随便迁入,就是说把文件的写权限,merge 权限关闭掉,只允许通过 merge request 的评论区去回复一些指令,比方说 lgtm(looks good to me),表示 review 通过,然后你可以回复一个 approve,表示这个代码可以被成功 check in,这样来做一些细粒度的权限检验

怎么迁入呢?我们的想法是在某一个微服务的 Proto 目录下,把自己的 Proto 文件管理起来,然后自动同步进去,就相当于要写一个插件,可以自动复制到 API 仓库里面去。做完这件事情之后,我们又分了 api.go,api.java,git submodule,就是把这些代码使用 Google protobuf,protoc 这个编译工具生成客户端的调用代码,然后推到另一个仓库,也就是把所有客户端调用代码推到一个源码仓库里面去

Proto 独立同步方式


移动端采用自定义工具方式,在同步代码阶段,自动更新最新的 proto 仓库到 worksapce 中,之后依赖 bazel 进行构建整个仓库

  • 业务代码中不依赖 target 产物,比如 objective-c 的 .h/.a 文件,或者 Go 的 .go 文件(钻石依赖、proto 未更新问题)

源码依赖会引入很多问题

  • 依赖信息丢失

  • proto 未更新

  • 钻石依赖

依赖信息丢失

在你的工程里面依赖了其他服务,依赖信息变成了源码依赖,你根本不知道依赖了哪个服务,以前是 protobuf 的依赖关系,现在变成了源码依赖,服务依赖信息丢失了。未来我要去做一些全局层面的代码盘点,比方说我要看这个服务被谁依赖了,你已经搞不清楚了,因为它变成了源码依赖

proto 未更新

如果我的 proto 文件更新了,你如何保证这个人重新生成了 .h/.a 文件,因为对它来说这个依赖信息已经丢失,为什么每次都要去做这个动作呢?它不会去生成 .h/.a 文件

钻石依赖

当我的 A 服务依赖 B 服务的时候,通过源码依赖,但是我的 A 服务还依赖 C 服务,C 服务是通过集中仓库 bapis 去依赖的,同时 B 和 C 之间又有一个依赖关系,那么这个时候就可能出现对于 C 代码来说可能会注册两次,protobuf 有一个约束就是说重名文件加上包名是不允许重复的,否则启动的时候就会 panic,有可能会出现钻石依赖

  • A 依赖 B

  • A 依赖 C

  • A 和 B 是源码依赖

  • A 和 C 是 proto 依赖

  • B 和 C 之间又有依赖

那么它的版本有可能是对不齐的,就是有风险的,这就是为什么 google basic 构建工具把 proto 依赖的名字管理起来,它并没有生成 .go 文件再 checkin 到仓库里面,它不是源码依赖,它每一次都要编译,每次都要生成 .go 文件的原因,就是为了版本对齐

Proto git submodules 方式

经过多次讨论,有几个核心认知:

  • proto one source of truth,不使用镜像方式同步,使用 git submodules 方式以仓库中目录形式来承载;

  • 本地构建工具 protoc 依赖 go module 下的相对路径即可;

  • 基于分支创建新的 proto,submodules 切换分支生成 stub 代码,同理 client 使用联调切换同一个分支;

  • 维护 Makefile,使用 protoc + go build 统一处理;

  • 声明式依赖方式,指定 protoc 版本和 proto 文件依赖(基于 BAZEL.BUILD 或者 Yaml 文件)

proto one source of truth

如果只在一个仓库里面,如果只有一个副本,那么这个副本就是唯一的真相并且是高度可信任的,那如果你是把这个 proto 文件拷来拷去,最终就会变得源头更新,拷贝的文件没办法保证一定会更新

镜像方式同步

实际上维护了本地微服务的目录里面有一个 protobuf 的定义,镜像同步到集中的仓库里面,实际上是有两个副本的

使用 git submodules 方式以仓库中目录形式来承载

git submodules 介绍

子模块允许您将 Git 存储库保留为另一个 Git 存储库的子目录。这使您可以将另一个存储库克隆到您的项目中并保持您的提交分开。


图中 gateway 这个目录就是以本地目录的形式,但是它是通过 git submodules 方式给承载进来的

如果公司内代码都在一起,api 的定义都在一起,那么大仓绝对是最优解,其次才是 git submodules,这也是 Google 的建议

我们倾向于最终 proto 的管理是集中在一个仓库里面,并且只有一份,不会做任何的 copy,通过 submodules 引入到自己的微服务里面,也就是说你的微服务里面都会通过 submodules 把集中 API 的 git 拷贝到本地项目里面,但是它是通过 submodeles 的方式来承载的,然后你再通过一系列 shell 的工具让你的整个编译过程变得更简单

IDL Project Layout

Proto Project Layout

在统一仓库中管理 proto,以仓库为名

根目录:

  • 目录结构和 package 对齐;

  • 复杂业务的功能目录区分;

  • 公共业务功能:api、rpc、type;


目录结构和 package 对齐

我们看一下 googleapis 大量的 api 是如何管理的?

第一个就是在 googleapis 这个项目的 github 里面,它的第一级目录叫 google,就是公司名称,第二个目录是它的业务域,业务的名称

目录结构和 protobuf 的包名是完全对齐的,方便检索

复杂业务的功能目录区分

v9 目录下分为公共、枚举、错误、资源、服务等等

公共业务功能:api、rpc、type

在 googleapis 的根目录下还有类似 api、rpc、type 等公共业务功能

IDL Errors

  • Proto Errors

  • Proto Errors:Server

  • Proto Errors:Client

Proto Errors

  • 使用一小组标准错误配合大量资源

  • 错误传播

用简单的协议无关错误模型,这使我们能够在不同的 API,API 协议(如 gRPC 或 HTTP)以及错误上下文(例如,异步,批处理或工作流错误)中获得一致的体验。



使用一小组标准错误配合大量资源

服务器没有定义不同类型的“找不到”错误,而是使用一个标准 google.rpc.Code.NOT_FOUND 错误代码并告诉客户端找不到哪个特定资源。状态空间变小降低了文档的复杂性,在客户端库中提供了更好的惯用映射,并降低了客户端的逻辑复杂性,同时不限制是否包含可操作信息。

我们以前自己的业务代码关于404,关于某种资源找不到的错误码,定义了上百上千个,请问为什么大家在设计 HTTP restful 或者 grpc 接口的时候不用人家标准的状态码呢?人家有标准的404,或者 not found 的状态码,用状态码去映射一下通用的错误信息不好吗?你不可能调用一个接口,返回几十种具体的错误码,你根本对于调用者来说是无法使用的。当我的接口返回超过3个自定义的错误码,你就是面向错误编程了,你不断根据错误码做不同的处理,非常难搞,而且你每一个接口都要去定义

这里的核心思路就是使用标准的 HTTP 状态码,比方说500是内部错误,503是网关错误,504是超时,404是找不到,401是参数错误,这些都是通用的,非常标准的一些状态码,或者叫错误码,先用它们,因为不是所有的错误都需要我们叫业务上 hint,进一步处理,也就是说我调你的服务报错了,我大概率是啥都不做的,因为我无法纠正服务端产生的一个错误,除非它是带一些业务逻辑需要我做一些跳转或者做一些特殊的逻辑,这种不应该特别多,我觉得两个三个已经非常多了

所以说你会发现大部分去调用别人接口的时候,你只需要用一个通用的标准的状态码去映射,它会大大降低客户端的逻辑复杂性,同时也不限制说你包含一些可操作的 hint 的一些信息,也就是说你可以包含一些指示你接下来要去怎么做的一些信息,就是它不冲突

错误传播

如果您的 API 服务依赖于其他服务,则不应盲目地将这些服务的错误传播到您的客户端。

举个例子,你现在要跟移动端说我有一个接口,那么这个接口会返回哪些错误码,你始终讲不清楚,你为什么讲不清楚呢?因为我们整个微服务的调用链是 A 调 B,B 调 C,C 调 D,D 的错误码会一层层透传到 A,那么 A 的错误码可能会是 ABCD 错误码的并集,你觉得你能描述出来它返回了哪些错误码吗?根本描述不出来

所以对于一个服务之间的依赖关系不应该盲目地将下游服务产生的这些错误码无脑透传到客户端,并且曾经跟海外很多公司,像 Uber,Twitter,Netflix,跟他们很多的华人的朋友交流,他们都不建议大家用这种全局的错误码,比方 A 部门用 01 开头,B 部门用 02 开头,类似这样的方式去搞所谓的君子契约,或者叫松散的没有约束的脆弱的这种约定

在翻译错误时,我们建议执行以下操作:

  • 隐藏实现详细信息和机密信息

  • 调整负责该错误的一方。例如,从另一个服务接收 INVALID_ARGUMENT 错误的服务器应该将 INTERNAL 传播给它自己的调用者。

比如你返回的错误码是4,代表商品已下架,我对这个错误很感兴趣,但是错误码4 在我的项目里面已经被用了,我就把它翻译为我还没使用的错误码6,这样每次翻译的时候就可以对上一层你的调用者,你就可以交代清楚你会返回错误码,因为都是你定义的,而且是你翻译的,你感兴趣的才翻译,你不感兴趣的通通返回 500 错误,就是内部错误,或者说 unknown,就是未知错误,这样你每个 API 都能讲清楚自己会返回哪些错误码


在 grpc 传输过程中,它会要求你要实现一个 grpc states 的一个接口的方法,所以在 Kraots 的 v2 这个工程里面,我们先用前面定义的 message Error 这个错误模型,在传输到 grpc 的过程中会转换成 grpc 的 error_details.proto 文件里面的 ErrorInfo,那么在传输到 client 的时候,就是调用者请求服务,service 再返回给 client 的时候再把它转换回来

也就是说两个服务使用一个框架就能够对齐,因为你是基于 message Error 这样的错误模型,这样在跨语言的时候同理,经过 ErrorInfo 使用同样的模型,这样就解决了跨语言的问题,通过模型的一致性


Proto Errors:Server


errors.proto 定义了 Business Domain Error 原型,使用最基础的 Protobuf Enum,将生成的源码放在 biz 大目录下,例如 biz/errors

  • biz 目录中核心维护 Domain,可以直接依赖 errors enum 类型定义;

  • data 依赖并实现了 biz 的 Reporisty/ACL,也可以直接使用 errors enum 类型定义;

  • TODO:Kratos errors 需要支持 cause 保存,支持 Unwrap();

在某一个微服务工程里面,errors.proto 文件实际上是放在 API 的目录定义,之前讲的 API 目录定义实际上是你的服务里面的 API 目录,刚刚讲了一个 submodules,现在你可以理解为这个 API 目录是另外一个仓库的 submodules,最终你是把这些信息提交到那个 submodules,然后通过 reference 这个 submodules 获取到最新的版本,其实你可以把它打成一个本地目录,就是说我的定义声明是在这个地方

这个 errors.proto 文件其实就列举了各种错误码,或者叫错误的字符串,我们其实更建议大家用字符串,更灵活,因为一个数字没有写文档前你根本不知道它是干啥的,如果我用字符串的话,我可以 user_not_found 告诉你是用户找不到,但是我告诉你它是3548,你根本不知道它是什么含义,如果我没写文档的话

所以我们建议使用 Protobuf Enum 来定义错误的内容信息,定义是在这个地方,但是生成的代码,按照 DDD 的战术设计,属于 Domain,因为业务设计是属于领域的一个东西,Domain 里面 exception 它最终的源码会在哪?会在 biz 的大目录下,biz 是 business 的缩写,就是在业务的目录下,举个例子,你可以放在 biz 的 errors 目录下

有了这个认知之后我们会做三个事情

首先你的 biz 目录维护的是领域逻辑,你的领域逻辑可以直接依赖 biz.errors 这个目录,因为你会抛一些业务错误出去

第二,我们的 data 有点像 DDD 的 infrastructure,就是所谓的基础设施,它依赖并实现了 biz 的 repository 和 acl,repository 就是我们所谓的仓库,acl 是防腐层

因为我们之前讲过它的整个依赖倒置的玩法,就是让我们的 data 去依赖 biz,最终让我们的 biz 零依赖,它不依赖任何人,也不依赖基础设施,它把 repository 和 acl 的接口定义放在 biz 自己目录下,然后让 data 依赖并实现它

也就是说最终我这个 data 目录也可以依赖 biz 的 errors,我可能通过查 mysql,结果这个东西查不到,会返回一个 sql no rows,但肯定不会返回这个错误,那我就可以用依赖 biz 的这个 errors number,比如说 user_not_found,我把它包一个 error 抛出去,所以它可以依赖 biz 的 errors

目前 Kratos 还不支持根因保存,根因保存是什么呢?刚刚说了你可能是 mysql 报了一个内部的错误,这个内部错误你实际上在最上层的传输框架,就是 HTTP 和 grpc 的 middleware 里面,你可能会把日志打出来,就要把堆栈信息打出来,那么根因保存就是告诉你最底层发生的错误是什么

不支持 Unwrap 就是不支持递归找根因,如果支持根因以后呢,就可以让 Kratos errors 这个 package 可以把根因传进去,这样子既能搞定我们 go 的 wrap errors,同时又支持我们的状态码和 reason,大类错误和小类错误,大类错误就是状态码,小类错误就是我刚刚说的用 enum 定义的具体信息,比方说这个商品被下架,这种就不太好去映射一个具体的错误码,你可能是返回一个500,再带上一个 reason,可能是这样的一个做法

Proto Errors:Client

从 Client 消费端只能看到 api.proto 和 error.proto 文件,相应的生成的代码,就是调用测的 api 以及 errors enum 定义

  • 使用 Kratos errors.As() 拿到具体类型,然后通过 Reason 字段进行判定;

  • 使用 Kratos errors.Reason() helper 方法(内部依赖 errors.As)快速判定;

拿到这两个文件之后你可以生成相应代码,然后调用 api


举个例子,图中的代码是调用服务端 grpc 的某一个方法,那么我可能返回一个错误,我们可以用 Kratos 提供的一个 Reason 的 short car,一个快捷的方法,然后把 error 传进去,实际上在内部他会调用标准库的 error.As,把它强制转换成 Kratos 的 errors 类型,然后拿到里面的 Reason 的字段,然后再跟这个枚举值判定,这样你就可以判定它是不是具体的一个业务错误

第二种写法你可以拿到原始的我们 Kratos 的 Error 模型,就是以下这个模型


new 出来之后用标准库的 errors.As 转换出来,转换出来之后再用 switch 获取它里面的 reason 字段,然后可以写一些业务逻辑

这样你的 client 代码跨语言,跨传输,跨协议,无论是 grpc,http,同样是用一样的方式去解决

IDL Docs

  • Proto Docs

Proto Docs

基于 openapi 插件 + IDL Protobuf 注释(IDL 即定义,IDL 即代码,IDL 即文档),最终可以在 Makefile 中使用 make api 生成 openapi.yaml,可以在 gitlab/vscode 插件直接查看

  • API Metadata 元信息用于微服务治理、调试、测试等;

因为我们可以在 IDL 文件上面写上大量的注释,那么当讲到这个地方,你就明白了 IDL 有什么样的好处?

IDL 文件它既定义,同时又是代码,也就是说你既做了声明,然后使用 protoc 可以去生成代码,并且是跨语言的代码,同时 IDL 本身既文档,也就是说它才真正满足了 one source of truth,就是唯一的事实标准

最终你可以在 Makefile 中定义一个 api 指令,然后生成一个 openapi.yaml,以前是 swagger json,现在叫 openapi,用 yaml 声明


生成 yaml 文件以后,现在 gitlab 直接支持 openapi.yaml 文件,所以你可以直接打开 gitlab 去点开它,就能看到这样炫酷的 UI,然后 VSCode 也有一个插件,你可以直接去查看

还有一个很关键的点,我们现在的 IDL 既是定义,又是代码,又是文档,其实 IDL 还有一个核心作用,这个定义表示它是一个元信息,是一个元数据,最终这个 API 的 mate data 元信息它可以用于大量的微服务治理

因为你要治理的时候你比方说对每个服务的某个接口进行路由,进行熔断进行限流,这些元信息是哪来的?我们知道以前 dubbo 2.x,3.x 之前都是把这些元信息注册到注册中心的,导致整个数据中心的存储爆炸,那么元信息在哪?

我们想一想为什么 protobuf 是定义一个文件,然后序列化之后它比 json 要小?因为它不是自描述的,它的定义和序列化是分开的,就是原始的 payload 是没有任何的定义信息的,所以它可以高度的compressed,可被压缩,或者说叫更紧凑

所以说同样的道理,IDL 的定义和它的元信息,和生成代码是分开的话,意味着你只要有 one source of truth 这份唯一的 pb 文件,基于这个 pb 文件,你就有办法把它做成一个 api 的 metadata 的服务,你就可以用于做微服务的治理

你可以选一个服务,然后看它有些什么接口,然后你可以通过一个管控面去做熔断、限流等功能,然后你还可以基于这个元信息去调试,你做个炫酷的 UI 可以让它有一些参数,甚至你可以写一些扩展,比方说这个字段叫 etc,建议它是什么样的值,那么你在渲染 UI 的时候可以把默认值填进去,那你就很方便做一些调试,甚至包含测试,你基于这个 api 去生成大量的 test case

参考

API 工程化分享 http://www.bilibili.com/video/BV17m…

接口定义语言 docs.microsoft.com/zh-cn/dotne…

真是头疼,Proto 代码到底放哪里? mp.weixin.qq.com/s/cBXZjg_R8…

git submodules git-scm.com/book/en/v2/…

kratos github.com/go-kratos/k…

error_details.proto github.com/googleapis/…

pkg/errors github.com/pkg/errors

Modifying gRPC Services over Time


作者:郑子铭_
来源:juejin.cn/post/7097866377460973599

收起阅读 »

算法图解-读书笔记

前言首先要说明的是: 这本书的定义是一本算法入门书,更多的是以简单地描述和示例介绍常见的数据结构和算法以及其应用场景,所以如果你在算法上有了一定造诣或者想深入学习算法,这本书很可能不太适合你。 但如果你对算法感兴趣,但是不知从何学起或者感觉晦涩难懂,这本书绝...
继续阅读 »

前言

首先要说明的是: 这本书的定义是一本算法入门书,更多的是以简单地描述和示例介绍常见的数据结构和算法以及其应用场景,所以如果你在算法上有了一定造诣或者想深入学习算法,这本书很可能不太适合你。
但如果你对算法感兴趣,但是不知从何学起或者感觉晦涩难懂,这本书绝对是一本很好的入门书,它以简单易懂的描述和示例,配以插画,向我们讲解了常见的算法及其应用场景。
本文是我基于自己的认知总结本书比较重点的部分,并不一定准确,更不一定适合所有人,所以如果你对本书的内容感兴趣,推荐阅读本书,本书总计180+页,而且配以大量示例和插画讲解,相信你很快就可以读完。

第一章:算法简介

算法是一组完成任务的指令。任何代码片段都可以视为算法。

二分查找

二分查找是一种用来在有序列表中快速查找指定元素的算法,每次判断待查找区间的中间元素和目标元素的关系,如果相同,则找到了目标元素,如果不同,则根据大小关系缩短了一半的查找区间。
相比于从头到尾扫描有序列表,一一对比的方式,二分查找无疑要快很多。

大O表示法

大O表示法是一种特殊的表示法,指出了算法的运行时间或者需要的内存空间,也就是时间复杂度空间复杂度

比如冒泡排序

export default function bubbleSort(original:number[]) {
 const len = original.length

 for (let i = 0; i < len - 1; i++) {
   for (let j = 1; j < len - i; j++) {
     if (original[j - 1] > original[j]) {
      [original[j - 1], original[j]] = [original[j], original[j - 1]]
    }
  }
}
}

需要两层循环,每次循环的长度为 n(虽然并不完全等于n),那它的时间复杂度就是 O(n²)。同时因为需要创建常数量的额外变量(len,i,j),所以它的空间复杂度是 O(1)

第二章:选择排序

内存的工作原理

当我们去逛商场或者超市又不想随身携带着背包或者手提袋时,就需要将它们存入寄存柜,寄存柜有很多柜子,每个柜子可以放一个东西,你有两个东西寄存,就需要两个柜子,也就是计算机内存的工作原理。计算机就像是很多柜子的集合,每个柜子都有地址。

数组和链表

有时我们需要在内存中存储一系列元素,应使用数组还是链表呢?
想要搞清楚这个问题,就需要知道数组和链表的区别。
数组是一组元素的集合,其中的元素都是相邻的,创建数组的时候需要指定其长度(这里不包括JavaScript中的数组)。可以理解为数组是一个大柜子,里边有固定数量的小柜子。
而链表中的元素则可以存储在任意位置,无需相邻,也不需要在创建的时候指定长度。

对应到电影院选座问题,
数组需要在初始就确定几个人看电影,并且必须是邻座。而一旦选定,此时如果再加一人,就需要重新选座,因为之前申请的位置坐不下现在的人数。而且如果没有当前数量的相邻座位,那么这场电影就没办法看了。
而链表则不要求指定数量和相邻性,所以如果是两个人,则只要还有大于等于两个空座,就可以申请座位,同样,如果此时再加一个人,只要还有大于等于一个空座,同样可以申请座位。\

数组因为可以通过索引获取元素,所以其读取元素的时间复杂度为O(1),但是因为其连续性,在中间插入或者删除元素涉及到其他元素的移动,所以插入和删除的时间复杂度为O(n)
链表因为只存储了链表的头节点,获取元素需要从头节点开始向后查找,所以读取元素的时间复杂度为O(1),因为其删除和插入元素只需要改变前一个元素的next指针,所以其插入和删除的时间复杂度为O(1)

选择排序

选择排序就是每次找出待排序区间的最值,放到已排序区间的末尾,比较简单,这里不做赘述了。

第三章:递归

个人理解,递归就是以相同的逻辑重复处理更深层的数据,直到满足退出条件或者处理完所有数据。
其中退出条件又称为基线条件,其他情况则属于递归条件,即需要继续递归处理剩余数据。

这里主要讲的是调用栈,因为递归函数是不断调用自身,所以就导致调用栈也来越大,因为每次函数调用都会占用内存,当调用栈很高,就会导致内存占用很大的问题。解决这个问题通常有两种方法:

  1. 将递归改写为循环

  2. 使用尾递归

第四章:快速排序

快速排序使用的是分而治之的策略。

分而治之

简单理解,分治就是将大规模的问题,拆解为小规模的问题,直到规模小到很容易处理,然后对小规模问题进行处理后,再将处理结果合并,得到大规模问题的处理结果。
关于快排这里也不做赘述,感兴趣的可以看我之前的文章,需要注意的是,递归的方式更容易理解,但是会存在上述递归调用栈及多个子数组占用内存过高的问题。

第五章:散列表

散列表类似于JavaScript中的array、object和map,只不过作为上层语言,JavaScript帮我们做了很多工作,不需要我们自己实现散列函数,而散列函数,简单理解就是传入数据,会返回一个数字,而这个数字可以作为数组的索引下标,我们就可以根据得到的索引将数据存入索引位置。一个好的散列函数应该保证每次传入相同的数据,都返回相同的索引,并且计算的索引应该均匀分布。
常见的散列表的应用有电话簿,域名到IP的映射表,用户到收货地址的映射表等等。

冲突

散列表还有一个比较重要的问题是解决冲突,比如传入字符串"abc",散列函数返回1,然后我们将"abc"存储在索引1的位置,而传入"def",散列函数同样返回1,此时索引1的位置已经存储了"abc",此时怎么办呢?
一种常见的解决方案是在每个索引的位置存储一个链表,这样就可以在一个索引位置,存储多个元素。

第六章:广度优先搜索

如果我们要从A去往B,而A去往B的路线可能很多条,这些路线就可以抽象成图,图中每个地点是图中的节点,地点与地点之间的连接线称为边。而如果我们要找出从A去往B的最近路径,需要两个步骤:

  1. 使用图来建立问题模型

  2. 使用广度优先搜索求解最短路径

广度优先搜索

所谓广度优先搜索,顾名思义,就是搜索过程中以广度优先,而不是深度,对于两点之间最短路径的问题,就是首先从出发点,走一步,看看是否有到达B点的路径,如果没有,就从第一步走到的所有点在走一步,看看有没有到达B点的路径,依此类推,直到到达B点。
实际算法中,广度优先搜索通常需要借助队列实现,即不停的把下一次能到达的点加入队列,并每次取出队首元素判断是否是B点,如果不是,再把它的下一个地点加入队列。

第七章:狄克斯特拉算法

在上一章我们可以使用广度优先搜索计算图的最短路径,但是如果图的每一条边都有相应的权重,也就是加权图,那么广度优先搜索求得的只是边数最少的路径,但是不一定是加权图中的最短路径,这个时候,想要求得最短路径,就需要狄克斯特拉算法。
狄克斯特拉算法的策略是:

  1. 每次都寻找下一个开销最小的节点

  2. 更新该节点的邻居节点的开销

  3. 重复这个过程,直到对图中每个节点都这样做

  4. 计算最终路径

但是要注意的是狄克斯特拉算法只适合权重为正的情况下使用,如果图中有负权边,要使用贝尔曼-福德算法

第八章:贪婪算法

所谓贪婪算法,就是每一步都采用最优解,当所有的局部解都是最优解,最终所有局部最优解组合得到的全局解就是近似的全局最优解。
要注意的是,贪婪算法得到的并不一定是最优解,但是是近似最优解的结果。
可能你会有疑问,为什么不使用可以准确得到最优解的算法呢?这是因为有些问题很难求出准确的最优解,比如书中的城市路线问题。

  • 如果只有2个城市,则会有2条路线

  • 3个城市会有6条路线

  • 4个城市会有24条路线

  • 5个城市会有120条路线

  • 6个城市会有720条路线

你会发现随着城市的增加,路线总数是城市数量的阶乘,当城市数量增加到一定程度,要枚举所有路线几乎是不可能的,这个时候就需要用贪婪算法求得一个近似的最优解。

第九章:动态规划

动态规划的主要思路是将一个大问题拆成相同的小问题,通过不断求解小问题,最后得出大问题的结果。
解题过程通常分为两步,状态定义和转移方程。
所谓状态定义,即抽象出一个状态模型,来表示某个状态下的结果值。
所谓转移方程,即推导出状态定义中的状态模型可以由什么计算得出。
LeetCode 746. 使用最小花费爬楼梯为例: 因为最小花费只和台阶层数有关,所以可以定义 dp[i] 表示第i层台阶所需花费,这就是状态定义。
又因为第i层台阶可能从i-2上来,也可能从i-1上来,所以第i层台阶的最小花费等于前面两层台阶中花费更小的那个加上本层台阶的花费,即dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i],这就是转移方程。
完整解题代码如下:

var minCostClimbingStairs = function(cost) {
   const dp = [cost[0],cost[1]]

   for(let i = 2;i<cost.length;i++){
       dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i]
  }

   return Math.min(dp.at(-1),dp.at(-2))
}

这里我讲解的方式和本书中画网格求解的方式并不一致,但是道理相同,一个是思考推导,一个是画网格推导。

第十章:K最近邻算法

K最近邻算法通常用来实现推荐系统,预测数值等。它的主要思想就是通过特征抽离,将节点绘制在坐标系中,当我们预测某个节点的数值或喜好时,通过对它相邻K个节点的数值和喜好的数据进行汇总求平均值,大概率就是它的数值和喜好。
比如常见的视频网站,社交网站,都会要求我们选择自己感兴趣的方面,或者在我们使用过程中记录我们的喜好,然后基于这些数据抽离我们的特征,然后根据相同特征的用户最近的观影记录或者话题记录,向我们推荐相同的电影和话题,也就是常见的猜你喜欢。还有机器学习的训练过程,也是提供大量数据给程序,让它进行特征抽离和记录,从而完善它的特征数据库,所以就有了机器越来越聪明的表现。

第十一章:接下来如何做

本章列举了本书没有单独拿出来讲但是依然很重要的数据结构和算法。

这里没有介绍树的基本概念,而是直接基于前面章节的二分查找引申出二叉查找树,这种树的性质是对于任意一个节点,它的左子树中的节点值都小于它,它的右子树的节点值都大于它。从而如果把整个树压平,就是一个升序的结果。然后还提到了B树,红黑树,堆和伸展树。

傅里叶变换

一个对傅里叶变换的比喻是:给她一杯冰沙,它能告诉你其中包含哪些成分。类似于给定一首歌曲,傅里叶变换能够将其中的各种频率分离出来。
常见的应用场景有音频、图片压缩。以音频为例,因为可以将歌曲分解为不同的频率,然后可以通过强调关系的部分,减弱或者隐藏不关心的部分,实现各种音效,也可以通过删除一些不重要的部分,实现压缩。

并行算法

因为笔记本和台式机都有多核处理器,那为了提高算法的速度,我们可以让它在多个内核中并行执行,这就是并行算法。

MapReduce

分布式算法是一种特殊的并行算法,并行算法通常是指在一台计算机上的多个内核中运行,而如果我们的算法复杂到需要数百个内核呢?这个时候就需要在多台计算机上运行,这就是分布式算法MapReduce就是一种流行的分布式算法。

映射函数

映射函数接受一个数组,对其中的每个元素执行同样的操作。

归并函数

归并是指将多项合并于一项的操作,这可能不太好理解,但是你可以回想下归并排序和递归快排在回溯过程中合并结果数组的过程。

布隆过滤器 和 HyperLogLog

布隆过滤器 是一种概率型数据结构,它提供的答案有可能不对,但很可能是正确的。
比如Google负责搜集网页,但是它只需要搜集新出现的网页,因此需要判断该网页是否搜集过。固然我们可以使用一个散列表存储网页是否已经搜集过,查找和插入的时间复杂度都是O(1),一切看起来很不错,但是因为网页会有数以万亿个,所以这个散列表会非常大,需要极大的存储空间,而这个时候,布隆过滤器就是一个不错的选择,它只需要占用很少的存储空间,提供的结果又很可能准确,对于网页是否搜集过的问题,可能出现误报的情况,即给出答案这个网页已搜集过,但是没有搜集,但是如果给出的答案是没有搜集,就一定没有搜集。\

HyperLogLog 是一种类似于布隆过滤器的算法,比如Google要计算用户执行的不同搜索的数量,要回答这个问题,就需要耗费大量的空间存储日志,HyperLogLog可以近似的计算集合中不同的元素数,与布隆过滤器一样,它不能给出准确的答案,但是近似准确答案,但是占用空间小很多。

以上两者,都适用于不要求答案绝对准确,但是数据量很大的场景。

哈希算法

这里介绍的就是哈希算法,常见的就是对文件进行哈希计算,判断两个文件是否相同,比如大文件上传中就可以通过计算文件的哈希值与已上传文件的散列表或者布隆过滤器比对,如果上传过,则直接上传成功,实现秒传。
还有一种应用场景就是密码的加密,通常用户输入的密码并不会进行明文传输,而是通过哈希算法进行加密,这样即使传输报文并黑客拦截,他也并不能知道用户的密码。书中还引申了局部敏感的散列函数对称加密非对称加密

线性规划

线性规划用于在给定约束条件下最大限度的改善指定的指标。
比如你的公司生产两种产品,衬衫和手提袋。衬衫每件利润2元,需要消耗1米布料和5粒扣子;手提袋每个利润3元,需要消耗2米布料和2粒扣子。现在有11米布料和20粒扣子,为了最大限度提高利润,该生产多少衬衫好手提袋呢?

以上就是个人对 《算法图解》 这本书的读书总结,如果能给你带来一点帮助,那就再好不过了。

保持学习,不断进步!一起加油鸭!💪


作者:前端_奔跑的蜗牛
来源:https://juejin.cn/post/7097881646858240007

收起阅读 »

美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

public class testT { public static void main(String [] args){ String A = "hi你是乔戈里"; System.out.println(A.lengt...
继续阅读 »


public class testT {
public static void main(String [] args){
String A = "hi你是乔戈里";
System.out.println(A.length());
}
}


以上结果输出为7。







小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。


    /**
* Returns the length of this string.
* The length is equal to the number of
Unicode
* code units
in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/

public int length() {
return value.length;
}


接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。


小萌:乔戈里,那这又是啥意思呢?乔哥:前几天我写的一篇文章:面试官问你编码相关的面试题,把这篇甩给他就完事!)里面对于Java的字符使用的编码有介绍:


Java中 有内码和外码这一区分简单来说



  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)



而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。




代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元。UTF-X 中的数字 X 就是各自代码单元的位数。


UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。


你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!


而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!


所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!



来看个例子!


public class testStringLength {
public static void main(String [] args){
String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
System.out.println(C);
System.out.println(B.length());
System.out.println(B.codePointCount(0,B.length()));
// 想获取这个Java文件自己进行演示的,可以在我的公众号【程序员乔戈里】后台回复 6666 获取
}
}



可以看到通过codePointCount()函数得知这个音乐字符是一个字符!




几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是"uD834uDD1E",这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?



一个一个解答:


第0个问题:


codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。


比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = "u1D11E",机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。


但如果令字符str = "uD834uDD1E",那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.


第1个问题




上图是对应的转换规则:



  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了



第2个问题




  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。





参考



作者:程序员乔戈里
来源:juejin.cn/post/6844904036873814023 收起阅读 »

一次关于架构的“嘴炮”

文章标题很随意,些微有一些骗点击的“贼意”;但内容却是充满了诚意,想必你已经感受到了。这是一次源于头条 Android 客户端软件架构问题的探讨,之所以冠上“嘴炮”之名,是因为它有一些务虚;同时又夹杂了一些方法论,不仅适用于客户端软件架构,也适用于其他工作场景...
继续阅读 »

文章标题很随意,些微有一些骗点击的“贼意”;但内容却是充满了诚意,想必你已经感受到了。

这是一次源于头条 Android 客户端软件架构问题的探讨,之所以冠上“嘴炮”之名,是因为它有一些务虚;同时又夹杂了一些方法论,不仅适用于客户端软件架构,也适用于其他工作场景,希望对大家有所帮助。

为了拉满读者的带入感,且以“我们”为主语,来看架构的挑战、判断和打法。

我们的挑战

期望高

优秀的公司对架构都有着很高的期许,都希望有一个良好的顶层设计,从上到下有统一的认知,遵循共同的规范,写出让人舒适的代码,甚至有那么一丢偷懒,有没有“一劳永逸”的架构设计可保基业长青?

然而高期望意味着高落差,面对落差,我们容易焦虑:

  • 代码什么时候能写的看上去本就应该是那个样子;而现在怎么就像是在攀登“屎山”呢?

  • 文档什么时候能写的既简明又详细;而现在怎么就简明的看不懂,详细的很多余呢?

  • 工具什么时候能更好用更强大一点;而现在怎么就动不动掉链子,没有想要的功能常年等排期呢?

  • “我”什么时候能从架构工作中找到成就感,而不是搞一搞就想着跑路呢?

责任大

大量问题的最终归因都是代码问题:设计不合理、使用不规范、逻辑太晦涩、编码“坑”太多。

没有一个单一的团队能承担这些问题的责任,我们收到过很多“吐槽”:

  • 这尼玛谁写的,简直不堪入目,看小爷我推倒重来展现一把真正的实力

  • XX 在这里埋了颗雷,但 XX 已经不管了,事到如今,我也只能兜底搞一把

  • 这压根就不应该这么用,本来的设计又不是为了这个场景,乱搞怪我咯?

  • 卧槽,这特么是隐藏技能啊,编译时悄悄改了老子的代码,找瞎了都没找到在哪过环节渗透进来的

一方面,口嗨一时爽,我们“吐槽”历史代码得到了一时的舒缓;另一方面,也意味着责任也传递到了我们:处理得好,我们的产出可能还是一样会被当作糟粕,但如果处理不好,我们就断送了业务发展的前程。

事情难

架构面临的从来不是单一的业务问题,而是多个业务多人协作的交叉问题,负重前行是常态。

  • 业务历久弥新,历史包袱叠加新的场景,随便动动刀子就拔出萝卜带出泥。譬如:头条 2021 年 10 月的版本有 XXXX 组件,相比一年前已经翻倍;类个数 XXXXX;插件 XX 个;仓库数量 XX 个;ttmain 仓库权限 XXX 人。(XX 代表数量级,隐去了具体数字,_)

  • 技术栈层出不穷,一方面要保持成熟稳定,一方面要积极探索落地。架构的同学要熟悉多种技术栈,譬如:跨端技术在客户端业务中通常都是多种共存(H5/Hybrid/小程序/Lynx/Flutter),一个业务到底选用哪种技术栈进行承载,需要耗费多少成本?选定技术栈后存在什么局限,是否存在不可逾越的障碍?

疗效慢

我们经常说代码复杂度高,并把降复杂度作为架构方向的重点工作之一;但影响复杂度的因子众多,从外部来看,有主观感受、客观指标、行业对标三个角度;从内部来看,有工程组织、代码实现和技术栈三个角度。即便我们很好的优化了工程结构这个因子,短时间内也很难感受到复杂度有一个明显的下降。


我们常说治理,其实是设计一种机制,在这种机制下运转直到治愈。

就像老中医开方子,开的不是特效药,而是应对病症的方法,是不是有用的方子,终究还是需要通过实践和时间的检验。希望我们不要成为庸医,瞎抓几把药一炖,就吹嘘药到病除。

我们的判断

架构问题老生常谈

谁来复盘架构问题,都免不了炒一炒“冷饭”;谁来规划架构方向,都逃不出了“减负”、“重构”、“复用”、“规范”这些关键词。难点在于把冷饭炒热,把方向落实。


架构方向一直存在

架构并不只局限于一个产品的初始阶段,而是伴随着产品的整个生命周期。架构也不是一成不变的,它只适合于特定的场景,过去的架构不一定适合现在,当下的架构不一定能预测未来,架构是随着业务不断演进的,不会出现架构方向做到头了、没有事情可搞了的情况,架构永远生机勃勃。

  • 强制遵循规范: 通常会要求业务公共的组件逐渐下沉到基础组件层,但随着时间的推移,这个规范很容易被打破

  • 需要成熟的团队: 领域专家(对业务细节非常熟悉的角色)和开发团队需紧密协作,构建出核心领域模型是关键。但盲目尝试 DDD 往往容易低估领域驱动设计这套方法论的实践成本,譬如将简单问题复杂化、陷入过分强调技术模型的陷阱

迄今为止,用于商业应用程序的最流行的软件架构设计模式是大泥球(Big Ball of Mud, BBoM),BBoM 是“…一片随意构造、杂乱无章、凌乱、任意拼贴、毫无头绪的代码丛林。”

泥球模式将扼杀开发,即便重构令人担忧,但也被认为是理所应当。然而,如果还是缺乏对领域知识应有的关注和考量,新项目最终也会走向泥球。没有开发人员愿意处理大泥球,对于企业而言,陷入大泥球就会丧失快速实现商业价值的能力。

——《领域驱动设计模式、原理与实践》Scott Millett & Nick Tune

复杂系统熵增不断

只要业务继续发展,越来越复杂就是必然趋势,这贴合热力学的熵增定律。

可以从两个维度来看复杂度熵增的过程:理解成本变高和预测难度变大。


理解成本:规模和结构是影响理解成本的两个因素

  • 宏大的规模是不好理解的,譬如:在城市路网中容易迷路,但在乡村中就那么几条道

  • 复杂的结构是不好理解的,譬如:一个钟表要比一条内裤难以理解

当需求增多时,软件系统的规模也会增大,且这种增长趋势并非线性增长,会更加陡峭。倘若需求还产生了事先未曾预料到的变化,我们又没有足够的风险应对措施,在时间紧迫的情况下,难免会对设计做出妥协,头疼医头、脚疼医脚,在系统的各个地方打上补丁,从而欠下技术债(Technical Debt)。当技术债务越欠越多,累积到某个临界点时,就会由量变引起质变,整个软件系统的复杂度达到巅峰,步入衰亡的老年期,成为“可怕”的遗留系统。

正如饲养场的“奶牛规则”:奶牛逐渐衰老,最终无奶可挤;然而与此同时,饲养成本却在上升。

——《实现领域驱动设计 - 深入理解软件的复杂度》张逸

预测难度:当下的筹码不足以应对未来的变化

  • 业务变化不可预测,譬如:头条一开始只是一个单端的咨询流产品,5 年前谁也不会预先设计 Lite 版、抖音、懂车帝等,多端以及新的业务场景带来的变化是无法预测的。很多时候,我们只需要在当下做到“恰当”的架构设计,但需要尽可能保持“有序”,一旦脱离了“有序”,那必将走向混乱,变得愈加不可预测

  • 技术变化不可预测,譬如:作为一个 Java 开发人员,Lambda 表达式的简洁、函数式编程的快感、声明式/响应式 UI 的体验,都是“真香”的技术变化,而陈旧的 Java 版本以及配套的依赖都需要升级,一旦升级,伴随着的就是多版本共存、依赖地狱(传递升级)等令人胆颤的问题。很多时候,我们不需要也没办法做出未来技术的架构设计,但需要让架构保持“清晰”,这样我们能更快的拥抱技术的变化

既然注定是逆风局,那跑到最后就算赢。

过多的流程规范反倒会让大家觉得是自己是牵线木偶,牵线木偶注定会随风而逝。

我们应该更多“强调”一些原则,譬如:分而治之、控制规模、保持结构的清晰与一致,而不是要求大家一定要按照某一份指南进行架构设计,那既降低不了复杂度,又跟不上变化。“强调”并不直接解决问题,而是把重要的问题凸显出来,让大家在一定的原则下自己找到问题的解决办法。

我们的打法

我们的套路是:定义问题 → 确定架构 → 方案落地 → 结果复盘。越是前面的步骤,就越是重要和抽象,也越是困难,越能体现架构师的功力。所以,我们打法的第一步就是要认清问题所在。

认清问题

问题分类

架构的问题是盘根错节的,将所有问题放在一起,就有轻重缓急之分,就有类别之分。区分问题的类别,就能在一定的边界内,匹配上对应的人来解决问题。

工程架构:


业务架构:


基础能力:


标准化:


问题分级

挑战、问题、手段这些经常混为一谈,哪些是挑战?哪些是问题?那些是手段?其实这些都是一回事,就是矛盾,只是不同场景下,矛盾所在的层级不同,举一个例子:


我们判断当前的研发体验不能满足业务日渐延伸的需要,这是一个矛盾,既是当下的挑战,也是当下的一级问题。要处理好这个矛盾,我们得拆解它,于是就有了二级问题:我们的代码逻辑是否已经足够优化?研发流程是否已经足够便捷?文档工具是否已经足够完备?二级问题也是矛盾,解决好二级问题就是我们处理一级矛盾的手段。这样层层递进下去,我们就能把握住当前我们要重点优化和建设的一些基础能力:插件化、热更新、跨端能力。

在具体实践过程中,基础技术能力还需要继续拆解,譬如:热更新能力有很多(Java 层的 Robust/Qzone 超级补丁/Tinker 等,Native 层的 Sophix/ByteFix 等),不同热更方案各有优劣,适用场景也不尽相同。我们要结合现状做出判断,从众多方案汲取长处,要么做出更大的技术突破创新,要么整合已有的技术方案进行组合创新。

勤于思考问题背后的问题

亨利福特说,如果我问客户需要什么,他们会告诉我,他们需要一匹更快的马。从亨利福特的这句话,我们可以提炼出一个最直接的问题:客户需要一匹更快的马。立足这个问题本身去找解决方案,可能永远交不出满意的答卷:寻找更好的品种,更科学的训马方式。

思考问题背后的问题,为什么客户需要一匹更快的马?可能客户想要更快的日常交通方式,上升了一个层次后,我们立刻找到了更好的解决方案:造车。

我们不能只局限于问题本身,还需要看到问题背后的问题,然后才能更容易找到更多的解决方案。

认知金字塔

引用认知金字塔这个模型,谨以此共勉,让我们能从最原始数据中,提炼出解决问题的智慧。


DATA: 金字塔的最底层是数据。数据代表各种事件和现象。数据本身没有组织和结构,也没有意义。数据只能告诉你发生了什么,并不能让你理解为什么会发生。

INFORMATION: 数据的上一层是信息。信息是结构化的数据。信息是很有用的,可以用来做分析和解读。

KNOWLEDGE: 信息再往上一层是知识。知识能把信息组织起来,告诉我们事件之间的逻辑联系。有云导致下雨,因为下雨所以天气变得凉快,这都是知识。成语典故和思维套路都是知识。模型,则可以说是一种高级知识,能解释一些事情,还能做预测。

WISDOM: 认知金字塔的最上一层,是智慧。智慧是识别和选择相关知识的能力。你可能掌握很多模型,但是具体到这个问题到底该用哪个模型,敢不敢用这个模型,就是智慧。

这就是“DIKW 模型”。

循序渐进

架构的问题不能等,也不能急。一个大型应用软件,并非要求所有部分都是完美设计,针对一部分低复杂性的区域或者不太可能花精力投入的区域,满足可用的条件即可,不需要投入高质量的构建成本。

以治理头条复杂度为例:

  • 长期的架构目标:更广(多端复用)、更快(单端开发速度)、更好(问题清理和前置拦截)

  • 当下的突出问题:业务之间耦合太重、缺少标准规范、代码冗余晦涩


细节已打码,请读者不要在意。重点在于厘清问题之后,螺旋式上升,做到长期有方向,短期有反馈。

最后

一顿输出之后,千万不能忘却了人文关怀,毕竟谋事在人。架构狮得供起来,他们高瞻远瞩,运筹帷幄;但架构人,却是更需要被点亮的,他们可能常年在“铲屎”,他们期望得到认可,他们有的还没有对象…干着干着,架构的故事还在,但人却仿佛早已翻篇。

来源:字节跳动技术团队 blog.csdn.net/ByteDanceTech/article/details/123700599

收起阅读 »

新的图形框架可以带来什么? 揭秘OpenHarmony新图形框架

3月30日,OpenHarmony v3.1 Release版本正式发布了。此版本为大家带来了全新的图形框架,实现了UI框架显示、多窗口、流畅动画等基础能力,夯实了OpenHarmony系统能力基座。下面就带大家详细了解新图形框架。一、完整能力视图新图形框架的...
继续阅读 »

3月30日,OpenHarmony v3.1 Release版本正式发布了。此版本为大家带来了全新的图形框架,实现了UI框架显示、多窗口、流畅动画等基础能力,夯实了OpenHarmony系统能力基座。下面就带大家详细了解新图形框架。

一、完整能力视图

新图形框架的能力在持续构建中,图1展示了新图形框架当前及未来提供的完整能力视图。

图1 OpenHarmony图形完整能力视图

按照分层抽象和轻模块化的架构设计原则,新图形框架分为接口层、架构层和引擎层。各层级说明如下:

● 接口层:提供图形NDK(native development kit,原生开发包)能力,包括OpenGL ES、Native Drawing等绘制接口能力。

● 框架层:由Render Service、Animation、Effect、Drawing、显示与内存管理等核心模块组成。框架层各模块说明如下:

● 引擎层:包括2D图形库和3D图形引擎两个模块。2D图形库提供2D图形绘制底层API,支持图形绘制与文本绘制底层能力。3D图形引擎能力尚在构建中。

二、新图形框架的亮点

经过上一节介绍,我们对新图形框架的完整能力有了基本的了解。那么,新图形框架有什么亮点呢?

新图形框架在渲染、动画流畅性、接口方面重点发力:

(1)渲染方面

通常来讲,UI界面显示分为两个部分:一是描述的UI元素在应用内部显示,二是多个应用的界面在屏幕上同时显示。对此,新图形框架从功能上做了相应的设计:控件级渲染窗口级渲染。“控件级渲染”重点考虑如何跟UI框架前端进行对接,需要将ArkUI框架的控件描述转换成绘制指令,并提供对应的节点管理以及渲染能力。而“窗口级渲染”重点考虑如何将多个应用合成显示到同一个屏幕上。

(2)动画流畅性方面

我们深挖动画处理流程中的各个环节,对新图形框架进行了新的动画实现设计,提升动画的流畅性体验。

(3)接口方面

新图形框架在接口层提供了更丰富的接口能力。

下面为大家一一详细介绍新图形框架的亮点特性。

1. 控件级渲染

新图形框架实现了基于RenderService(简称RS)的控件级渲染功能,如图2所示。

图2 控件级渲染

控件级渲染功能具有以下特点:

● 支持GPU渲染,提升渲染性能。

● 动画逻辑从主线程中剥离,提供独立的步进驱动机制。

● 将渲染节点属性化,属性与内容分离。

2. 窗口级渲染

新图形框架实现了基于RenderService的窗口级渲染功能,如图3所示。

图3 窗口级渲染

窗口级渲染功能具有以下特点:

● 取代Weston合成框架,实现RS新合成框架。

● 支持硬件VSync/软件Vsync。

● 支持基于NativeWindow接入EGL/GLES的能力。

● 更灵活的合成方式,支持硬件在线合成/CPU合成/混合合成(GPU合成即将上线)。

● 支持多媒体图层在线overlay。

3. 更流畅的动画体验

动画流畅性是一项很基本、也很关键的特性,直接影响用户体验。为了提升动画的流畅性体验,我们深挖动画处理流程中的各个环节,对新图形框架进行了新的动画实现设计。

如图4所示,传统动画的实现流程如下:

(1) 应用创建动画,设置动画参数。

(2) 每帧回调,修改控件参数,重新测量、布局、绘制。

(3) 内容渲染。

图4 传统动画实现

经过深入分析,我们发现传统动画实现存在以下缺点:

(1)UI与动画一起执行,UI的业务阻塞会影响动画的执行,导致动画卡顿。

(2)每帧回调修改控件属性,会触发测量布局录制,导致耗时增加。

针对以上两点缺陷,我们对新图形框架进行了新的动画实现设计,如图5所示。

图5 新框架的动画实现

(1)动画与UI分离。

动画在渲染线程步进,与UI业务线程分离。

(2)动画仅测量、布局、绘制一次,降低动画负载。

通过计算最终界面属性值,对有改变的控件添加动画,动画过程中不测量、布局、绘制,提升性能。

4. 对外提供的接口

新图形框架提供了丰富的接口:

(1)SDK:支持WebGL 1.0、WebGL 2.0,满足JS开发者的3D开发的需求。

WebGL开发指导:

https://docs.openharmony.cn/pages/zh-cn/app/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3/%E5%BC%80%E5%8F%91/%E5%9F%BA%E7%A1%80%E5%8A%9F%E8%83%BD%E5%BC%80%E5%8F%91/WebGL/WebGL%E5%BC%80%E5%8F%91%E6%8C%87%E5%AF%BC/#:~:text=%23-,%E7%9D%80%E8%89%B2%E5%99%A8%E7%BB%98%E5%88%B6%E5%BD%A9%E8%89%B2%E4%B8%89%E8%A7%92%E5%BD%A2,-%E6%AD%A4%E5%9C%BA%E6%99%AF%E4%B8%BA

(2)NDK:支持OpenGL ES3.X,可以通过XComponent提供的nativewindow创建EGL/OPENGL绘制环境,满足游戏引擎等开发者对3D绘图能力的需求。

图6 OpenGL ES使用示例

新图形框架还处于不断完善过程中,我们将基于新框架提供更多的能力,相信以后会给大家带来更多的惊喜,敬请期待~

收起阅读 »

春节钱包大流量奖励系统入账及展示的设计与实现

字节跳动开放平台-钱包团队整体负责字节系八端 2022 年春节活动奖励链路的入账、展示与使用,下文是对这段工作的介绍和总结,先整体介绍一下业务背景与技术架构,然后说明了各个难点的具体实现方案,最后进行抽象总结,希望对后续的活动起指导作用。1. 背景&挑...
继续阅读 »

字节跳动开放平台-钱包团队整体负责字节系八端 2022 年春节活动奖励链路的入账、展示与使用,下文是对这段工作的介绍和总结,先整体介绍一下业务背景与技术架构,然后说明了各个难点的具体实现方案,最后进行抽象总结,希望对后续的活动起指导作用。

1. 背景&挑战&目标

1.1 业务背景

(1)支持八端:2022 年字节系产品春节活动需要支持八端 APP 产品(包含抖音/抖音火山/抖音极速版/西瓜/头条/头条极速版/番茄小说/番茄畅听)的奖励互通。用户在上述任意一端都可以参与活动,得到的奖励在其他端都可以提现与使用。

(2)玩法多变:主要有集卡、朋友页红包雨、红包雨、集卡开奖与烟火大会等。

(3)多种奖励:奖励类型包含现金红包、补贴视频红包、商业化广告券、电商券、支付券、消费金融券、保险券、信用卡优惠券、喜茶券、电影票券、dou+券、抖音文创券、头像挂件等。

1.2 核心挑战

(1)设计&实现八端奖励入账与展示互通的大流量的方案,最高预估有 360w QPS 发奖。

(2)多种发奖励的场景,玩法多变;奖励类型多,共 10 余种奖励。对接多个下游系统。

(3)从奖励系统稳定性、用户体验、资金安全与运营基础能力全方位保障,确保活动顺利进行 。

1.3 最终目标

(1)奖励入账:设计与实现八端奖励互通的奖励入账系统,对接多个奖励下游系统,抹平不同奖励下游的差异,对上游屏蔽底层奖励入账细节,设计统一的接口协议提供给业务上游。提供统一的错误处理机制,入账幂等能力和奖励预算控制。

(2)奖励展示/使用:设计与实现活动钱包页,支持在八端展示用户所获得的奖励,支持用户查看、提现(现金),使用卡券/挂件等能力。

(3)基础能力:

  • 【基础 sdk】提供查询红包余额、累计收入、用户在春节活动是否获得过奖励等基础 sdk,供业务方查询使用。

  • 【预算控制】与上游奖励发放端算法策略打通,实现大流量卡券入账的库存控制能力,防止超发。

  • 【提现控制】在除夕当天多轮奖励发放后,提供用户提现的灰度放量能力、提现时尚未入账的处理能力。

  • 【运营干预】活动页面灵活的运营配置能力,支持快速发布公告,及时触达用户。为应对黑天鹅事件,支持批量卡券和红包补发能力。

(4)稳定性保障:在大流量的入账场景下,保证钱包核心路径稳定性与完善,通过常用稳定性保障手段如资源扩容、限流、熔断、降级、兜底、资源隔离等方式保证用户奖励方向的核心体验。

(5)资金安全:在大流量的入账场景下,通过幂等、对账、监控与报警等机制,保证资金安全,保证用户资产应发尽发,不少发。

(6)活动隔离:实现内部测试活动、灰度放量活动和正式春节活动三个阶段的奖励入账与展示的数据隔离,不互相影响。

2. 产品需求介绍

用户可以在任意一端参与字节的春节活动获取奖励,以抖音红包雨现金红包入账场景为例,具体的业务流程如下:

登录抖音 → 参与活动 → 活动钱包页 → 点击提现按钮 → 进入提现页面 → 进行提现 → 提现结果页,另外从钱包页也可以进入活动钱包页。

img

奖励发放核心场景:

  1. 集卡:集卡抽卡时发放各类卡券,集卡锦鲤还会发放大额现金红包,集卡开奖时发放瓜分奖金和优惠券;

  2. 红包雨:发红包、卡券以及视频补贴红包,其中红包和卡券最高分别 180w QPS;

  3. 烟火大会:发红包、卡券以及头像挂件。

dd6af98d51f5db710872248ae51a30e4.png

3. 钱包资产中台设计与实现

在 2022 年春节活动中,UG 主要负责活动的玩法实现,包含集卡、红包雨以及烟火大会等具体的活动相关业务逻辑和稳定性保障。而钱包方向定位是大流量场景下实现奖励入账、奖励展示、奖励使用与资金安全保障的相关任务。其中资产中台负责奖励发放与奖励展示部分。

3.1 春节资产资产中台总体架构图如下:

768d79e164975d1794d875abaf74b627.png

钱包资产中台核心系统划分如下:

  1. 资产订单层:收敛八端奖励入账链路,提供统一的接口协议对接上游活动业务方如 UG、激励中台、视频红包等的奖励发放功能,同时对上游屏蔽对接奖励业务下游的逻辑处理,支持预算控制、补偿、订单号幂等。

  2. 活动钱包 api 层:收敛八端奖励展示链路,同时支持大流量场景

3.2 资产订单中心设计

核心发放模型:

37c250c4a4b5e91b0dc4ce1e236ae00f.png

说明:

  1. 活动 ID 唯一区分一个活动,本次春节分配了一个单独的母活动 ID

  2. 场景 ID 和具体的一种奖励类型一一对应,定义该场景下发奖励的唯一配置,场景 ID 可以配置的能力有:发奖励账单文案;是否需要补偿;限流配置;是否进行库存控制;是否要进行对账。提供可插拔的能力,供业务可选接入。

实现效果:

  1. 实现不同活动之间的配置隔离

  2. 每个活动的配置呈树状结构,实现一个活动发多种奖励,一种奖励发多种奖励 ID

  3. 一种奖励 ID 可以有多种分发场景,支持不同场景的个性化配置

订单号设计:

资产订单层支持订单号维度的发奖幂等,订单号设计逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从单号设计层面保证不超发,每个场景的奖励用户最多只领一次。

  1. 核心难点问题解决

4.1 难点一:支持八端奖励数据互通

前文背景已经介绍过了,参与 2022 年春节活动一共有八个产品端,其中抖音系和头条系 APP 是不同的账号体系,所以不能通过用户 ID 打通奖励互通。具体解决方案是字节账号中台打通了八端的账号体系给每个用户生成唯一的 actID(手机号优先级最高,如果不同端登录的手机号一样,在不同端的 actID 是一致的)。钱包侧基于字节账号中台提供的唯一 actID 基础上,设计实现了支持八端奖励入账、查看与使用的通用方案,即每个用户的奖励数据是绑定在 actID 上的,入账和查询是通过 actID 维度实现的,即可实现八端奖励互通。

示意图如下:

d5e9bb782bba0c3e5fd950a64664d2c6.png

4.2 难点二:高场景下的奖励入账实现

每年的春节活动,发现金红包都是最关键的一环,今年也不例外。有几个原因如下:

  1. 预估发现金红包最大流量有 180w TPS。

  2. 现金红包本身价值高,需要保证资金安全。

  3. 用户对现金的敏感度很高,在保证用户体验与功能完整性同时也要考虑成本问题。

终上所述,发现金红包面临比较大的技术挑战。

发红包其实是一种交易行为,资金流走向是从公司成本出然后进入个人账户。

(1)从技术方案上是要支持订单号维度的幂等,同一订单号多次请求只入账一次。订单号生成逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从单号设计层面保证不超发。

(2)支持高并发,有以下 2 个传统方案:

具体方案类型实现思路优点缺点
同步入账申请和预估流量相同的计算和存储资源1.开发简单; 2.不容易出错;浪费存储成本。 拿账户数据库举例,经实际压测结果:支持 30w 发红包需要 152 个数据库实例,如果支持 180w 发红包,至少需要 1152 个数据库实例,还没有算上 tce 和 redis 等其他计算和存储资源。
异步入账申请部分计算和存储资源资源,实际入账能力与预估有一定差值1.开发简单; 2.不容易出错; 3.不浪费资源;用户体验受到很大影响。 入账延迟较大,以今年活动举例会有十几分钟延迟。用户参与玩法得到奖励后在活动钱包页看不到奖励,也无法进行提现,会有大量客诉,影响抖音活动的效果。

以上两种传统意义上的技术方案都有明显的缺点,那么进行思考,既能相对节约资源又能保证用户体验的方案是什么?
最终采用的是红包雨 token 方案,具体方案是使用异步入账加较少量分布式存储和较复杂方案来实现,下面具体介绍一下。

4.2.1 红包雨 token 方案:

本次春节活动在红包雨/集卡开奖/烟火大会的活动下有超大流量发红包的场景,前文介绍过发奖 QPS 最高预估有 180w QPS,按照现有的账户入账设计,需要大量存储和计算资源支撑,根据预估发放红包数/产品最大可接受发放时间,计算得到钱包实际入账最低要支持的 TPS 为 30w,所以实际发放中有压单的过程。

设计目标:

在活动预估给用户发放(180w)与实际入账(30w)有很大 gap 的情况下,保证用户的核心体验。用户在前端页面查看与使用过当中不能感知压单的过程,即查看与使用体验不能受到影响,相关展示的数据包含余额,累计收入与红包流水,使用包含提现等。

具体设计方案:

我们在大流量场景下每次给用户发红包会生成一个加密 token(使用非对称加密,包含发红包的元信息:红包金额,actID,与发放时间等),分别存储在客户端和服务端(容灾互备),每个用户有个 token 列表。每次发红包的时候会在 Redis 里记录该 token 的入账状态,然后用户在活动钱包页看到的现金红包流水、余额等数据,是合并已入账红包列表+token 列表-已入账/入账中 token 列表的结果。同时为保证用户提现体验不感知红包压单流程,在进入提现页或者点击提现时将未入账的 token 列表进行强制入账,保证用户提现时账户的余额为应入账总金额,不 block 用户提现流程。

示意图如下:

fcf21c86fa2ef5ea8c132ca58883cf91.png

token 数据结构:

token 使用的是 pb 格式,经单测验证存储消耗实际比使用 json 少了一倍,节约请求网络的带宽和存储成本;同时序列化与反序列化消耗 cpu 也有降低。

// 红包雨token结构
type RedPacketToken struct {
  AppID     int64 `protobuf: varint,1,opt json: AppID,omitempty ` // 端ID
  ActID     int64 `protobuf: varint,2,opt json: UserID,omitempty ` // ActID
  ActivityID string `protobuf: bytes,3,opt json: ActivityID,omitempty ` // 活动ID
  SceneID   string `protobuf: bytes,4,opt json: SceneID,omitempty ` // 场景ID
  Amount     int64 `protobuf: varint,5,opt json: Amount,omitempty ` // 红包金额
  OutTradeNo string `protobuf: bytes,6,opt json: OutTradeNo,omitempty ` // 订单号
  OpenTime   int64 `protobuf: varint,7,opt json: OpenTime,omitempty ` // 开奖时间
  RainID     int32 `protobuf: varint,8,opt,name=rainID json: rainID,omitempty ` // 红包雨ID
  Status     int64 `protobuf: varint,9,opt,name=status json: status,omitempty ` //入账状态
}

token 状态机流转:

在调用账户真正入账之前会置为处理中(2)状态,调用账户成功为成功(8)状态,发红包没有失败的情况,后续都是可以重试成功的。

token 安全性保障:

采用非对称加密算法来保障存储在的客户端尽可能不被破解,其中加密算法为秘密仓库,限制其他人访问。同时考虑极端情况下如果 token 加密算法被黑产破译,可监控报警发现,可降级。

4.2.2 活动钱包页展示红包流水

需求背景:

活动钱包页展示的红包流水是现金红包入账流水、提现流水、c2c 红包流水三个数据源的合并,按照创建时间倒叙排列,需要支持分页,可降级,保证用户体验不感知发现金红包压单过程。

c5ff3a6471d9230554d6b50395c7c946.png

4.3 难点三:发奖励链路依赖多的稳定性保障

发红包流程降级示意图如下:

8677db831f49ccf481df9fc43f6fcd6e.png

根据历史经验,实现的功能越复杂,依赖会变多,对应的稳定性风险就越高,那么如何保证高依赖的系统稳定性呢?

解决方案:

现金红包入账最基础要保障的功能是将用户得到的红包进行入账,同时支持幂等与预算控制(避免超发),红包账户的幂等设计强依赖数据库保持事务一致性。但是如果极端情况发生,中间的链路可能会出现问题,如果是弱依赖需要支持降级掉,不影响发放主流程。钱包方向发红包最短路径为依赖服务实例计算资源和 MySQL 存储资源实现现金红包入账。

发红包强弱依赖梳理图示:

psm依赖服务是否强依赖降级方案降级后影响
资产中台tcc降级读本地缓存
bytkekv主动降级开关,跳过 bytekv,依赖下游做幂等
资金交易层分布式锁 Redis被动降级,调用失败,直接跳过基本无
token Redis主动降级开关,不调用 Redis用户能感知到入账有延迟,会有很多客诉
MySQL主有问题,联系 dba 切主故障期间发红包不可用

4.4 难点四:大流量发卡券预算控制

需求背景:

春节活动除夕晚上 7 点半会开始烟火大会,是大流量集中发券的一个场景,钱包侧与算法策略配合进行卡券发放库存控制,防止超发。

具体实现:

(1)钱包资产中台维护每个卡券模板 ID 的消耗发放量。

(2)每次卡券发放前算法策略会读取钱包 sdk 获取该卡券模板 ID 的消耗量以及总库存数。同时会设置一个阈值,如果卡券剩余量小于 10%后不发这个券(使用兜底券或者祝福语进行兜底)。

(3) 同时钱包资产中台方向在发券流程累计每个券模板 ID 的消耗量(使用 Redis incr 命令原子累加消耗量),然后与总活动库存进行比对,如果消耗量大于总库存数则拒绝掉,防止超发,也是一个兜底流程。

具体流程图:

9074ea1e5bc8d3a06b7958c380876833.png

优化方向:

(1)大流量下使用 Redis 计数,单 key 会存在热 key 问题,需要拆分 key 来解决。

(2)大流量场景下操作 Redis 会存在超时问题,返回上游处理中,上游继续重试发券会多消耗库存少发,本次春节活动实际活动库存在预估库存基础上加了 5%的量级来缓解超时带来的少发问题。

4.5 难点五:高 QPS 场景下的热 key 的读取和写入稳定性保障

需求背景:

在除夕晚上 7 点半开始会开始烟火大会活动,展示所有红包雨与烟火大会红包的实时累计发放总额,最大流量预估读取有 180wQPS,写入 30wQPS。

这是典型的超大流量,热点 key、更新延迟不敏感,非数据强一致性场景(数字是一直累加),同时要做好容灾降级处理,最后实际活动展示的金额与产品预计发放数值误差小于 1%。

bad435d6650fdb6b002016da05a29b1d.png

4.5.1 方案一

提供 sdk 接入方式,复用了主会场机器实例的资源。高 QPS 下的读取和写入单 key,比较容易想到的是使用 Redis 分布式缓存来进行实现,但是单 key 读取和写入的会打到一个实例上,压测过单实例的瓶颈为 3w QPS。所以做的一个优化是拆分多个 key,然后用本地缓存兜底。

具体写入流程:

设计拆分 100 个 key,每次发红包根据请求的 actID0 使用 incr 命令累加该数字,因为不能保证幂等性,所以超时不重试。

8029924d3b366d73aff105bf7b2874bc.png

读取流程:

与写入流程类似,优先读取本地缓存,如果本地缓存值为为 0,那么去读取各个 Redis 的 key 值累加到一起,进行返回。

73a7b7c3a4dfd9c721119a3a55e511a5.png

问题:

(1)拆分 100 个 key 会出现读扩散的问题,需要申请较多 Redis 资源,存储成本比较高。而且可能存在读取超时问题,不能保证一次读取所有 key 都读取成功,故返回的结果可能会较上一次有减少。

(2)容灾方案方面,如果申请备份 Redis,也需要较多的存储资源,需要的额外存储成本。

4.5.2 方案二

设计思路:

在方案一实现的基础上进行优化,并且要考虑数字不断累加、节约成本与实现容灾方案。在写场景,通过本地缓存进行合并写请求进行原子性累加,读场景返回本地缓存的值,减少额外的存储资源占用。使用 Redis 实现中心化存储,最终大家读到的值都是一样的。

具体设计方案:

每个 docker 实例启动时都会执行定时任务,分为读 Redis 任务和写 Redis 任务。

读取流程:

  1. 本地的定时任务每秒执行一次,读取 Redis 单 key 的值,如果获取到的值大于本地缓存那么更新本地缓存的值。

  2. 对外暴露的 sdk 直接返回本地缓存的值即可。

  3. 有个问题需要注意下,每次实例启动第一秒内是没有数据的,所以会阻塞读,等有数据再返回。

写入流程:

  1. 因为读取都是读取本地缓存(本地缓存不过期),所以处理好并发情况下的写即可。

  2. 本地缓存写变量使用 go 的 atomic.AddInt64 支持原子性累加本地写缓存的值。

  3. 每次执行更新 Redis 的定时任务,先将本地写缓存复制到 amount 变量,然后再将本地写缓存原子性减去 amount 的值,最后将 amount 的值 incr 到 Redis 单 key 上,实现 Redis 的单 key 的值一直累加。

  4. 容灾方案是使用备份 Redis 集群,写入时进行双写,一旦主机群挂掉,设计了一个配置开关支持读取备份 Redis。两个 Redis 集群的数据一致性,通过定时任务兜底实现。

本方案调用 Redis 的流量是跟实例数成正比,经调研读取侧的服务为主会场实例数 2 万个,写入侧服务为资产中台实例数 8 千个,所以实际 Redis 要支持的 QPS 为 2.8 万/定时任务执行间隔(单位为 s),经压测验证 Redis 单实例可以支持单 key2 万 get,8k incr 的操作,所以设置定时任务的执行时间间隔是 1s,如果实例数更多可以考虑延长执行时间间隔。

具体写入流程图如下:

e109f0690f711922dc101c6702e3eaaf.png

4.5.3 方案对比

优点缺点
方案一1. 实现成本简单1. 浪费存储资源; 2. 难以做容灾; 3. 不能做到一直累加;
方案二1. 节约资源; 2. 容灾方案比较简单,同时也节约资源成本;1. 实现稍复杂,需要考虑好并发原子性累加问题

结论:

从实现效果,资源成本和容灾等方面考虑,最终选择了方案二上线。

4.6 难点六:进行母活动与子活动的平滑切换

需求背景:

为了保证本次春节活动的最终上线效果和交付质量,实际上分了三个阶段进行的。

(1)第一阶段是内部人员测试阶段。

(2)第二个阶段是外部演练阶段,圈定部分外部用户进行春节活动功能的验证(灰度放量),也是发现暴露问题以及验证对应解决机制最有效的手段,影响面可控。

(3)第三个阶段是正式春节活动。

而产品的需求是这三个阶段是分别独立的阶段,包含用户获得奖励、展示与使用奖励都是隔离的。

1f1592a1838cc5a1a85b5f411246e972.png

技术挑战:

有多个上游调用钱包发奖励,同时钱包有多个奖励业务下游,所以大家一起改本身沟通成本较高,配置出错的概率就比较大,而且不能同步改,会有较大的技术安全隐患。

设计思路:

作为奖励入账的唯一入口,钱包资产中台收敛了整个活动配置切换的实现。设计出母活动和子活动的分层配置,上游请求参数统一传母活动 ID 代表春节活动,钱包资产中台根据请求时间决定采用哪个子活动配置进行发奖,以此来实现不同时间段不同活动的产品需求。降低了沟通成本,减少了配置出错的概率,并且可以同步切换,较大地提升了研发与测试人效。

示意图:

24ac8b5a51a9db3f3ff016dfd9cfe086.png

4.7 难点七:大流量场景下资金安全保障

钱包方向在本次春节活动期间做了三件事情来保障大流量大预算的现金红包发放的资金安全:

  1. 现金红包发放整体预算控制的拦截

  2. 单笔现金红包发放金额上限的拦截

  3. 大流量发红包场景的资金对账

  • 小时级别对账:支持红包雨/集卡/烟火红包发放 h+1 小时级对账,并针对部分场景设置兜底 h+2 核对。

  • 准实时对账:红包雨已入账的红包数据反查钱包资产中台和活动侧做准实时对账

多维度核对示意图:

ac8168ba12977b430d33d7a5f3c27f3c.png

准实时对账流程图:

2e0ae5cd61906fc5d264ae6f5fa4e411.png

说明:

准实时对账监控和报警可以及时发现是否异常入账情况,如果报警发现会有紧急预案处理。

5. 通用模式抽象

在经历过春节超大流量活动后的设计与实现后,有一些总结和经验与大家一起分享一下。

5.1 容灾降级层面

大流量场景,为了保证活动最终上线效果,容灾是一定要做好的。参考业界通用实现方案,如降级、限流、熔断、资源隔离,根据预估活动参与人数和效果进行使用存储预估等。

5.1.1 限流层面

(1)限流方面应用了 api 层 nginx 入流量限流,分布式入流量限流,分布式出流量限流。这几个限流器都是字节跳动公司层面公共的中间件,经过大流量的验证。

(2)首先进行了实际单实例压测,根据单实例扛住的流量与本次春节活动预估流量打到该服务的流量进行扩容,并结合下游能抗住的情况,在 tlb 入流量、入流量限流以及出流量限流分别做好了详细完整的配置并同。

限流目标:

保证自身服务稳定性,防止外部预期外流量把本身服务打垮,防止造成雪崩效应,保证核心业务和用户核心体验。

简单集群限流是实例维度的限流,每个实例限流的 QPS=总配置限流 QPS/实例数,对于多机器低 QPS 可能会有不准的情况,要经过实际压测并且及时调整配置值。

对于分布式入流量和出流量限流,两种使用方式如下,每种方式都支持高低 QPS,区别只是 SDK 使用方式和功能不同。一般低 QPS 精度要求高,采用 redis 计数方式,使用方提供自己的 redis 集群。高 QPS 精度要求低,退化为总 QPS/tce 实例数的单实例限流。

5.1.2 降级层面

对于高流量场景,每个核心功能都要有对应的降级方案来保证突发情况核心链路的稳定性。

(1)本次春节奖励入账与活动活动钱包页方向做好了充分的操作预案,一共有 26 个降级开关,关键时刻弃车保帅,防止有单点问题影响核心链路。

(2)以发现金红包链路举例,钱包方向最后完全降级的方案是只依赖 docker 和 MySQL,其他依赖都是可以降级掉的,MySQL 主有问题可以紧急联系切主,虽说最后一个都没用上,但是前提要设计好保证活动的万无一失。

5.1.3 资源隔离层面

(1)提升开发效率不重复造轮子。因为钱包资产中台也日常支持抖音资产发放的需求,本次春节活动也复用了现有的接口和代码流程支持发奖。

(2)同时针对本次春节活动,服务层面做了集群隔离,创建专用活动集群,底层存储资源隔离,活动流量和常规流量互不影响。

5.1.4 存储预估

(1)不但要考虑和验证了 Redis 或者 MySQL 存储能抗住对应的流量,同时也要按照实际的获取参与和发放数据等预估存储资源是否足够。

(2)对于字节跳动公司的 Redis 组件来讲,可以进行垂直扩容(每个实例增加存储,最大 10G),也可以进行水平扩容(单机房上限是 500 个实例),因为 Redis 是三机房同步的,所以计算存储时只考虑一个机房的存储上限即可。要留足 buffer,因为水平扩容是很慢的一个过程,突发情况遇到存储资源不足只能通过配置开关提前下掉依赖存储,需要提前设计好。

5.1.5 压测层面

本次春节活动,钱包奖励入账和活动钱包页做了充分的全链路压测验证,下面是一些经验总结。

  1. 在压测前要建立好压测整条链路的监控大盘,在压测过程当中及时和方便的发现问题。

  2. 对于 MySQL 数据库,在红包雨等大流量正式活动开始前,进行小流量压测预热数据库,峰值流量前提前建链,减少正式活动时的大量建链耗时,保证发红包链路数据库层面的稳定性。

  3. 压测过程当中一定要传压测标,支持全链路识别压测流量做特殊逻辑处理,与线上正常业务互不干扰。

  4. 针对压测流量不做特殊处理,压测流量处理流程保持和线上流量一致。

  5. 压测中要验证计算资源与存储资源是否能抗住预估流量

  • 梳理好压测计划,基于历史经验,设置合理初始流量,渐进提升压测流量,实时观察各项压测指标。

  • 存储资源压测数据要与线上数据隔离,对于 MySQL 和 Bytekv 这种来讲是建压测表,对于 Redis 和 Abase 这种来讲是压测 key 在线上 key 基础加一下压测前缀标识 。

  • 压测数据要及时清理,Redis 和 Abase 这种加短时间的过期时间,过期机制处理比较方便,如果忘记设置过期时间,可以根据写脚本识别压测标前缀去删除。

  1. 压测后也要关注存储资源各项指标是否符合预期。

5.2 微服务思考

在日常技术设计中,大家都会遵守微服务设计原则和规范,根据系统职责和核心数据模型拆分不同模块,提升开发迭代效率并不互相影响。但是微服务也有它的弊端,对于超大流量的场景功能也比较复杂,会经过多个链路,这样是极其消耗计算资源的。本次春节活动资产中台提供了 sdk 包代替 rpc 进行微服务链路聚合对外提供基础能力,如查询余额、判断用户是否获取过奖励,强制入账等功能。访问流量最高上千万,与使用微服务架构对比节约了上万核 CPU 的计算资源。

6. 系统的未来演进方向

(1)梳理上下游需求和痛点,优化资产中台设计实现,完善基础能力,优化服务架构,提供一站式服务,让接入活动方可以更专注进行活动业务逻辑的研发工作。

(2)加强实时和离线数据看板能力建设,让奖励发放数据展示的更清晰更准确。

(3)加强配置化和文档建设,对内减少对接活动的对接成本,对外提升活动业务方接入效率。

来源:字节跳动技术团队

收起阅读 »

开箱即用,5 个功能强悍的 JSON 神器!

大家好,我是小 G。自 1999 年开始,JSON 作为用户体验较好的数据交换格式,开始被各界广为采纳,并逐渐应用到 Web 开发及各种 NoSQL 数据库领域。身为程序员,想必大家平日也是跟 JSON 打交道颇多。我近期刚好业务上有需求,得基于 JSON 实...
继续阅读 »

大家好,我是小 G。

自 1999 年开始,JSON 作为用户体验较好的数据交换格式,开始被各界广为采纳,并逐渐应用到 Web 开发及各种 NoSQL 数据库领域。

身为程序员,想必大家平日也是跟 JSON 打交道颇多。我近期刚好业务上有需求,得基于 JSON 实现一些小功能,因此便到 GitHub 了解了一下关于 JSON 的开发者工具。

逛了一圈之后,可谓是收获颇丰。

下面,就挑选几个我认为比较不错的,在日常开发场景中,也会时不时用到的 JSON 工具,给大家做下分享。

JSON 数据可视化

JSON Visio,一个开源的 JSON 数据可视化工具,可通过图表节点,完美呈现 JSON 数据间的结构关系与详情。


GitHub:https://github.com/AykutSarac/jsonvisio.com

凭借这款工具,你可以快速捕捉到 JSON 中的错误信息,搜索节点,并且,还能使用不同布局来展开 JSON 数据,让你可以更直观的看出数据间的关系。

链式操作 JSON

Dasel,一个比较实用的 JSON 命令行工具,可通过类似链式语法的方式,对 JSON、YAML、CSV 等文件进行增删改查、转换等操作。

用作者的原话说,就是当你掌握了 dasel 之后,便可以一劳永逸,在多种数据格式中,无缝切换,快速查找、修改数据。


GitHub:https://github.com/TomWright/dasel

该工具支持多种结构化数据文件,如 JSON、YAML、TOML、XML、CSV 等。

数据检索、查询

DataStation,是一款面向开发者的开源数据 IDE。

简单来说,就是可通过 SQL 语句,快速查询 JSON、CSV、Excel、日志记录库等文件中的数据,并为之创建可视化图表。

DataStation:https://github.com/multiprocessio/datastation

这款 IDE 支持 Linux、macOS、Windows 等主流操作系统,以及 18 种 SQL 和 NoSQL 数据库、文件、HTTP 服务器。

此外,作者还提供了命令行工具:DSQ,除了数据查询外,还支持多个文件合并查询,不同格式的数据源文件转化(比如将 CSV 转为 JSON)等功能。

DSQ:https://github.com/multiprocessio/dsq

在线存储 JSON

之前在 GitHub 热榜,火过一个跟 JSON 有关的开源项目,叫 JSONBox。

它能为开发者提供一个特定链接,通过向这个链接发送 HTTP 请求,可以用来存储、读取、修改 JSON 数据。

整个过程无需其他操作,完全免费,开箱即用,非常便捷。


GitHub:https://github.com/vasanthv/jsonbox

不过,我还是建议你在使用这个工具时,最好是基于自己的服务器来托管数据,这样安全性才比较有保障。

快速生成表单

通过上面几个项目,你应该能大概感知出 JSON 的灵活性与可扩展性有多强了。因此,基于这两大特点,国内有位开发者做了一款在线动态表单生成器:Form Create。

用户只需上传 JSON 数据,即可快速生成表单:


GitHub:https://github.com/xaboy/form-create

生成的表单,可具备动态渲染、数据收集、验证和提交功能等功能。另外还内置了 20 种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。

文中所提到的所有开源项目,已收录至 GitHubDaily 的开源项目列表中,有需要的,可访问下方 GitHub 地址或点击「阅读原文」查看:

GitHub:https://github.com/GitHubDaily/GitHubDaily

好了,今天的分享到此结束,感谢大家抽空阅读,我们下期再见,Respect!

来源:blog.csdn.net/sinat_33224091/article/details/124263178

收起阅读 »

这么牛的毕业生,来当CTO吧!

时光如风飘渺,眨眼间已经在行业浸润多年了,见过无数厉害的人物,也见过更多更多的挫B。前几天刚上班,就接到面试一个毕业生的任务,让我感叹人与人之间的差距。他的水平,绝对的完爆工作多年的架构师。在下佩服之~我们的话题,是关于怎么构建一个可伸缩的高可用、高可靠大型网...
继续阅读 »

时光如风飘渺,眨眼间已经在行业浸润多年了,见过无数厉害的人物,也见过更多更多的挫B。

前几天刚上班,就接到面试一个毕业生的任务,让我感叹人与人之间的差距。

他的水平,绝对的完爆工作多年的架构师。在下佩服之~

我们的话题,是关于怎么构建一个可伸缩的高可用、高可靠大型网站。嗯,就让我们开始吧。

1.要发问了

大家都知道,如今的互联网,数据量爆炸,服务请求飙升,即使是非常小的公司,也可能因为某个产品产生不同于往日的数十倍流量,当然这有时候是个梦想而已。

流量增加就意味着对后端服务能力的增加,如何构建每秒处理GB数据、QPS超过数十万的大型系统,就变成了一个挑战。尤其是某些天才的秒杀创意,让这个流量变的越发变态不可预料。

在有效的资源下,如何让系统保持良好的反馈?以支撑老板们的梦想呢?你有什么处理方式?或者你有什么体系化的心得体会要和我分享一下的?

毕业生微微一笑:“我在这方面正好有点总结,我可以多花点时间聊聊这个”。

好吧,洗耳恭听。

2.服务建设的重要指标

“我首先要说明的是,服务的建设要关注几个指标。有了目标就有了方向,大体上我总结了四个”。

  1. 可用性。 我们要保证服务的可用性,也就是SLA指标。只有服务能够正常响应,错误率保持在较低的水平,我们的服务才算正常。
  2. 服务性能。 可用性和性能是相辅相成的,服务性能高了,有限的资源就能够支撑更多的请求,可用性就会提高。所以服务性能优化是一个持续性的工作,在巨量流量下每1ms的平均性能提升,都是值得追求的。
  3. 可靠性。 分布式服务的组件非常多,每个组件都可能会产生问题,影响面也不尽相同。如何保证每个组件的运行时高可靠性,如何保证数据的一致性,都是有挑战的。
  4. 可观测性。 想要获取服务优化的指标数据,就要求我们的服务,在设计开始能够保证服务的可观测性。宏观上能够识别组件类故障,微观上能为性能优化提供依据。在HPA等自动化伸缩场景中,遥测数据甚至是自动化决策的唯一依据。

对于一个服务来说,扩容的手段主要有两种。scale-up:垂直扩展,scale-out:水平扩展。

垂直扩展通过增加单台机器的配置来增加单节点的处理能力。这在某些业务场景下是非常有必要的。但我们的服务更多的是追求水平扩展,用更多的机器来支撑业务的发展。

只要服务满足了横向扩展的能力,满足无状态的特点,剩下的事情就是堆硬件了。听起来很美好,但实际上,对整个体系架构的挑战非常的大。

毕业生的一番分析像极了野鸡CTO的发言,废话连篇。我暗自点头,鼓励他继续深入、细化下去,拿出点不一样的东西。

3.幂等性

如果接口调用失败怎么办?在早期的互联网中,因为网络原因,这样的情况可能更严重。HTTP状态码504,就是典型的代表网关超时的状态。第一次请求可能会超时失败,第二次可能就成功了。现实中需要严格重试的接口还是蛮多的,尤其是异步化的加入,使得重试变得更加重要。

但我们也要考虑由于快速重试所造成的重试风暴,因为超时本身可能就意味着服务器已经不堪重负,我们没有任何理由火上浇油。所以,重试都会有退避算法(exponential backoff),直到真正的结束请求进入异常处理流程。

可以看出,由于超时和重试机制的引入,服务的幂等变的格外重要。它不仅仅是在单台机器上支持重复的调用,在整个分布式集群环境中同样保证可以重入多次。

在数学上,它甚至有一个优美的函数公式。

f(f(f(x))) = f(f(x)) = f(x)

一旦接口拥有了幂等性,就有了能够忍受故障的能力。当我们因为偶发的网络故障、机器故障造成少量的服务调用失败时,可以通过重试和幂等很容易的最终完成调用。

对于查询操作来说,在数据集合不变的情况下,它天然是幂等的,不需要做什么额外的处理。比较有挑战的是添加和更新操作。

有不少的技术手段来保证幂等,比如使用数据库的唯一索引,使用提前生成好的交易ID,或者使用token机制来保证唯一调用。其中,token机制被越来越多的使用,其做法是在请求之前,先请求一个唯一的tokenId,此后的调用幂等就围绕着tokenId进行编程。

4.健康检查

自从k8s把健康检查这个东西标准化之后,健康检查就成为了一个服务的必备选项。在k8s中,分为活跃探针(liveness probe)和 就绪探针(readiness probe)。

活跃探测主要用来查明应用程序是否处于活动状态。它只展示应用本身的状态,而不应依赖于外部其他系统的健康状态;就绪探测指示应用程序是否已准备好接受流量,如果应用程序实例的就绪状态为未就绪,则不会将流量路由到该实例。

如果你使用了SpringBoot的actuator组件,通过health接口,将很容易获取这部分功能。当容器或者注册中心通过health接口判断到服务出现了问题,会自动的把问题节点从节点列表中摘除,然后再通过一系列探测机制在服务恢复正常的时候再把它挂上去。

通过健康检查机制,能够避免流量被调度到错误的机器上去。

5.服务自动发现

早期的软件开发人员,对服务上线的机制摸的门清,不是因为他们想要这样,而是不得不这样做。

比如,我要扩容一台机器,需要首先测试这台机器的存活性,然后部署服务,最后再在负载均衡软件比如nginx中将这台机器配置上。通常情况下,还要看一下日志,到底有没有流量到这台机器上来。

借助于微服务和持续集成,我们再也不需要这么繁杂的上线流程,只需要在页面上点一下构架、发布,服务就能够自动上线,并被其他服务发现。

注册中心在服务发现方面承担了非常重要的角色。它相当于一个信息集中地,所有的服务启动、关闭,都要上报到这里;同样,我想要调用某些服务,也需要到同一个注册中心去查询。

注册中心相当于一个中介,将这些频繁的上下线需求和查询需求,全部统一起来进行管理,现在已经成为微服务的必备设施。

这些查询需求可能是非常频繁的,所以在调用方本地,同样也会存储一份副本,这样在注册中心出现问题的时候,不至于因为大脑缺氧而造成大规模故障。有了副本就有了一致性问题,有注册中心通过Pull的方式更新信息,存在数据一致性的实效性。实效性处理的比较好的是有Push(通知)机制的组件,能够在较快的时间感知服务的变化。

许多组件可以充当服务注册中心,只要它有分布式存储数据的能力和数据一致性的能力。比如Eureka、Nacos、Zookeeper、Consul、Redis,甚至数据库,都能胜任这个角色。

6.限流

web开发中,tomcat默认是200个线程池,当更多的请求到来,没有新的线程能够去处理这个请求,那这个请求将会一直等待在浏览器方。表现的形式是,浏览器一直在转圈(还没超过acceptCount),即使你请求的是一个简单的Hello world。

我们可以把这个过程,也看作是限流。它在本质上,是设置一个资源数量上限,超出这个上限的请求,将被缓冲,或者直接失败。

对于高并发场景下的限流来说,它有特殊的含义:它主要是用来保护底层资源的。如果你想要调用某些服务,你需要首先获取调用它的许可。限流一般由服务提供方来提供,对调用方能够做事的能力进行限制。

比如,某个服务为A、B、C都提供了服务,但根据提前申请的流量预估,限制A服务的请求为1000/秒、B服务2000/秒,C服务1w/秒。在同一时刻,某些客户端可能会出现被拒绝的请求,而某些客户端能够正常运行,限流被看作是服务端的自我保护能力。

常见的限流算法有:计数器、漏桶、令牌桶等。但计数器算法无法实现平滑的限流,在实际应用中使用较少。

7.熔断

自从施耐德发明了断路器,这个熔断的概念席卷了全球。从A股熔断,到服务熔断,大有异曲同工之妙。

熔断的意思是:当电路闭合时,电流可以通过,当断路器打开时,电流停止。

通常情况下,用户的一个请求,需要后端多个服务配合才能完成工作。后端的这些服务,并不是每一个都是必须的,如果因为其中的某个服务有问题,就把用户的整个请求给拒绝掉,那是非常不合理的。

熔断期望某些服务,在发生问题时,返回一些默认值。整个请求依然可以正常进行下去。

比如风控。如果在某个时间风控服务不可用了,用户其实是应该能够正常交易的。这时候我们应该默认风控是通过的,然后把这些异常交易倒到另外一个地方,在风控恢复后再尽快赶在发货的之前处理。

从上面的描述可以看出,有的服务,熔断后简单的返回些默认数据就行,比如推荐服务;但有的服务就需要有对应的异常流程支持,算是一个if else;更要命的是,有些业务不支持熔断,那就只能Fail Fast。

一股脑的处理是没有思考的技术手段,不是我们所推荐的。

Hystrix、resilience4j、Sentinel等组件,是Java系广泛使用的工具。通过SpringBoot的集成,这些框架一般用起来都比较方便,可以达到配置化编程。

8.降级

降级是一个比较模糊的说法。限流、熔断,在一定程度上,也可以看作是降级的一种。但通常所说的降级,切入的层次更加高级一些。

降级一般考虑的是分布式系统的整体性,从源头上切断流量的来源。比如在双11的时候,为了保证交易系统,将会暂停一些不重要的服务,以免产生资源争占。服务降级有人工参与,人为使得某些服务不可用,多属于一种业务降级方式。

在什么地方最适合做降级呢?就是入口。比如Nginx,比如DNS等。

在某些互联网应用中,会存在MVP(Minimum Viable Product)这个概念,意为最小化可行产品,它的SLA要求非常高。围绕着最小可行性产品,会有一系列的服务拆分操作,当然某些情况甚至需要重写。

比如,一个电商系统,在极端情况下,只需要把商品显示出来,把商品卖出去就行。其他一些支撑性的系统,比如评论、推荐等,都可以临时关掉。在物理部署和调用关系上,就要考虑这些情况。

9.预热

请看下面一种情况。

一个高并发环境下的DB,进程死亡后进行重启。由于业务处在高峰期间,上游的负载均衡策略发生了重分配。刚刚启动的DB瞬间接受了1/3的流量,然后load疯狂飙升,直至再无响应。

原因就是:新启动的DB,各种Cache并没有准备完毕,系统状态与正常运行时截然不同。可能平常1/10的量,就能够把它带入死亡。

同理,一个刚刚启动的JVM进程,由于字节码并未被JIT编译器优化,在刚启动的时候,所有接口的响应时间都比较慢。如果调用它的负载均衡组件,并没有考虑这种刚启动的情况,1/n的流量被正常路由到这个节点,就很容易出现问题。

所以,我们希望负载均衡组件,能够依据JVM进程的启动时间,动态的慢慢加量,进行服务预热,直到达到正常流量水平。

10.背压

考虑一下下面两种场景:

  1. 没有限流。请求量过高,有多少收多少,极容易造成后端服务崩溃或者内存溢出
  2. 传统限流。你强行规定了某个接口最大的承受能力,超出了直接拒绝,但此时后端服务是有能力处理这些请求的

如何动态的修改限流的值?这就需要一套机制。调用方需要知道被调用方的处理能力,也就是被调用方需要拥有反馈的能力。背压,英文Back Pressure,其实是一种智能化的限流,指的是一种策略。

背压思想,被请求方不会直接将请求端的流量直接丢掉,而是不断的反馈自己的处理能力。请求端根据这些反馈,实时的调整自己的发送频率。比较典型的场景,就是TCP/IP中使用滑动窗口来进行流量控制。

反应式编程(Reactive)是观察者模式的集大成者。它们大多使用事件驱动,多是非阻塞的弹性应用,基于数据流进行弹性传递。在这种场景下,背压实现就简单的多。

背压,让系统更稳定,利用率也更高,它本身拥有更高的弹性和智能。比如我们常见的HTTP 429状态码头,表示的意思就是请求过多,让客户端缓一缓,不要那么着急,算是一个智能的告知。

11.隔离

即使在同一个instance中,同类型的资源,有时候也要做到隔离。一个比较浅显的比喻,就是泰坦尼克号,它有多个船舱。每个船舱都相互隔离,避免单个船舱进水造成整个船沉了。

当然,泰坦尼克号带着骚气的jack沉了,那是因为船舱破的太多的缘故。

在有些公司的软件中,报表查询服务、定时任务、普通的服务,都放在同一个tomcat中。它们使用同一套数据库连接池,当某些报表接口的请求一上升,其他正常的服务也无法使用。这就是混用资源所造成的后果。

除了遵循CQRS来把服务拆分,一个快速的机制就是把某类服务的使用资源隔离。比如,给报表分配一个单独的数据库连接池,分配一个单独的限流器,它将无法影响其他服务。

耦合除了出现在无状态服务节点,同时还会出现在存储节点。与其把报表服务的存储和正常业务的存储放在一个数据库,不如把它们拆开,分别提供服务。

一个和尚挑水喝,两个和尚也可以挑水喝。原因就是他们在两个庙。

12.异步

如果你比较过BIO和NIO的区别,就可以看到,我们的服务其实大部分时间都是在等待返回,CPU根本就没有跑满。当然,NIO是底层的机制,避免了线程膨胀和频繁的上下文切换。

服务的异步化和NIO有点类似,采用之后可以避免无谓的等待。尤其是当调用路径冗长的时候,异步不会阻塞,响应也会变的迅速。

单机时候,我们会采用NIO;而在分布式环境中,我们会采用MQ。虽然它们是不同的技术,但道理都是相通的。

异步通常涉及到编程模型的改变。同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。异步操作可以平滑的横向扩容,也可以把瞬时压力时间上后移。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性,体验更加友好。

13.缓存

缓存可能是软件中使用最多的优化技术了。比如,在最核心的CPU里,就存在着多级缓存;为了消除内存和存储之间的差异,各种类似Redis的缓存框架更是层出不穷。

缓存的优化效果是非常好的,可以让原本载入非常缓慢的页面,瞬间秒开;也能让本是压力山大的数据库,瞬间清闲下来。

缓存,本质上是为了协调两个速度差异非常大的组件,通过加入一个中间层,将常用的数据存放在相对高速的设备中。

在应用开发中,缓存分为本地缓存和分布式缓存。

那什么叫分布式缓存呢?它其实是一种集中管理的思想。如果我们的服务有多个节点,堆内缓存在每个节点上都会有一份;而分布式缓存,所有的节点,共用一份缓存,既节约了空间,又减少了管理成本。

在分布式缓存领域,使用最多的就是Redis。Redis支持非常丰富的数据类型,包括字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希表(hash)等常用的数据结构。当然,它也支持一些其他的比如位图(bitmap)一类的数据结构。

所以加下来的问题一定集中在缓存穿透、击穿和雪崩,以及一致性上,这个我就不多聊了。

14.Plan-B

一个成熟的系统都有B方案,除了异地多活和容灾等处置方案,Plan-B还以为着我们要为正常的服务提供异常的通道。

比如,专门运行一个最小可行性系统,运行公司的核心业务。在大面积故障的时候,将请求全面切换到这个最小系统上。

Plan-B通常都是全局性的,它保证了公司最基本的服务能力,我们期望它永远用不上。

15.监控报警

问题之所以成为问题,是因为它留下了证据。没有证据的问题,你虽然看到了影响结果,但是你无法找到元凶。

而且问题通常都具有人性化,当它发现无法发现它的时候,它总会再次出现。就如同罪犯发现了漏洞,还会再次尝试利用它。

所以,要想处理线上问题,你需要留下问题发生的证据,这是重中之重。如果没有这些东西,你的公司,绝对会陷入无尽的扯皮之中。

日志是最常见的做法。通过在程序逻辑中进行打点,配合Logback等日志框架,可以快速定位到发生问题的代码行。我们需要看一下bug的详细发生过程,对可能发生问题的逻辑进行详细的日志记录,进行更加细致的日志输出,在发生问题的时候,就可以切换到debug进行调试。

如果是大范围的bug,那么强烈建议直接在线上进行调试。不太推荐使用Arthas等工具动态的修改字节码进行测试,当然也不推荐IDEA的远程调试。相反,推荐使用类似金丝雀发布的方式,导出非常小的一部分流量,构造一个新的版本进行测试。如果你没有金丝雀发布平台,类似Nginx的负载均衡工具也可以通过权重做到类似的事情。

日志系统与监控系统,对硬件的需求是比较大的,尤其是你的请求体和返回体比较大的情况下,对存储和计算资源的额要求更是高。它的硬件成本,在整个基础设施中,占比也是比较高的。但这些证据信息,对分析问题来说,是非常有必要的。所以即使比较贵,很多公司依然会有很大的投入在这上面,包括硬件投入和人力投入。

MTTD和MTTR是两个非常重要的指标,我们一定要加大关注。

16.结尾

我看了一下表,这家伙很能说,预定的时间很快用完了。我挥挥手打住:”你还会哪些东西?简单的说一下吧!“

”也不是很多。像怎么构建一个DevOps团队支撑我们开发、测试、线上环境,如何进行更深入的性能优化,如何进行实际的故障排查。以及一些细节问题,比如怎么优化操作系统,网络编程和多线程,我这些还都没有聊。“

我说,”够了,你已经非常优秀了“。

”你把自己叫作毕业生,已经碾压绝大多数人了。你到底是哪里的毕业生啊!“

”我是B站的,昨天刚毕业~“,他腼腆的笑了。

我盯着他的眼睛,也笑了。枯木逢春犹再发,人可两度再少年!妙啊。

来源:https://mp.weixin.qq.com/s/d5KGmRcZJkoM4ciMBdbFKg

收起阅读 »

有些程序员,本质是一群羊!

羊都是以群论的。如果你感觉它的单位是只,那只能证明你太穷。真正牧羊的,从来不会因为晚上烤了只全羊,而感觉到自己的羊少了。不信重温一下李安的《断背山》看看,关注点别搞错了,我们是在谈羊。当在它们耳朵上钉上红绳子,或者激进点彩绘一下,你就再也无法分辨哪只羊是张三,...
继续阅读 »

羊都是以群论的。如果你感觉它的单位是只,那只能证明你太穷。

真正牧羊的,从来不会因为晚上烤了只全羊,而感觉到自己的羊少了。不信重温一下李安的《断背山》看看,关注点别搞错了,我们是在谈羊。

当在它们耳朵上钉上红绳子,或者激进点彩绘一下,你就再也无法分辨哪只羊是张三,哪只羊是李四。牧羊犬也不会关心这东西,它只关心它的晚饭。

所以大多数羊没有身份。它只是只羊而已。

软件开发中,有些公司把程序员们当作资源池来利用,那么他们的本质就是羊(没错就是外包)。羊群的特征很有意思,但单体并没有什么存在感。大多数时候,低下头吃草就够了。

来瞧瞧吧。

羊吃草

羊的工作就是吃草。

它不管草有多少,也不管为什么会被发放到某个地方吃草,它吃草甚至是不用经过大脑思考。牧羊人圈定了一片草原,它们就会从左吃到右,然后再从右吃到左,直到把这片草原吃光为止。

羊的嘴很臭,被啃过的草一年内不会发出新芽。

如果你不管它们,草吃光了,有些羊就开始吃草根,草根吃光了就慢慢饿死。它从来不会想着到外面的世界看一看。

羊会反刍,就是把嚼过的东西,重新再弄到嘴里嚼一遍。这种重复的动作,使得羊的智商非常的低,只能进行这些低级的循环 。羊有疝气,它们嘴里的东西其实很难闻但它自己感觉不到,只有目光呆滞的在那里蠕动它的牙齿,直到嚼出草绿色的津液来。

羊惊群

有时候,羊会表现出让人莫名其妙的动作。

一个阳光明媚的下午,所有的羊都在认真吃草。突然,有一只羊悚然之间,竖起了耳朵瞪大了眼睛,仿佛看到了极具危险的事物。

或许是它的脑子短路,突然想到了一些不该属于它的东西;或许是草里的蚂蚱吓了它一跳。只要有一只羊表现出了这种态度,正在吃草的羊,就会在几秒之内全部停止咀嚼,竖起耳朵瞪大了眼睛。

这个状态会持续十几秒,直到危险解除--更大可能是根本就没有危险。

在群体中,个体的警觉会突然之间就传遍整个集体,但谁也弄不明白危险到底来自何方 。或许只有在这十几秒钟,在停止吃草的时间,羊才会思考为什么自己是只羊,而不是趴在旁边的牧羊犬。

更多时候,羊会低下头继续吃草,所有的羊都会快速忘掉这种集体性的思考。

羊不怕死

在杀猪的时候,猪会一直嚎叫;杀牛的时候,牛会流泪。

羊不一样,它不吭一声,甚至连害怕的神情都没有。

我们都习惯称羊为沉默的羔羊,就是因为它很安静。安静是因为它智商低,而不是因为它勇敢。

我杀过羊。从绑住它的蹄子吊起来,到刀子刺进它的脖子里放血。羊会因为痛挣扎几下,但整个过程出奇的顺利。所以杀羊的人,从来没有什么心理压力,因为这个过程太柔滑了。

有时候刀子刺的不准,羊会饶有兴趣的看着自己的血液躺下来,并伸出舌头感受一下。它可能会被一只蚂蚱吓得竖起双耳,但它并不怎么怕拿刀子的你。

它可能认为自己的主人,并不会拿它开刀。即使你在它面前宰了它的同伴,它也会觉得自己是特殊的。

搞笑的是,羊的温顺都是表现在对外方面,多数羊在内部并不老实。

两只羊,会因为一些谁都闹不清楚的原因,跳起来碰对方的头。头破血流的,掉羊角的比比皆是。那阵势,就是要以命相抵,越斗越来劲。

我越来越觉得,羊对生命并不渴望,对更好的生活也没有追求。  我甚至觉得,它希望死亡。

它从来不像狗一样,挖个坑储藏食物;也不像乌鸦一样,就喜欢收集发光的东西。羊从一生下来,眼光中就透露着生无所恋,只有在发情期的时候,才能表现的像个正常的动物一样。


它并不怕死,因为它从来没搞懂为什么活着。况且一直吃草、咀嚼的一生,本就没什么值得留恋的地方。

牧羊犬

羊的温顺,枯燥,会让人觉得没有攻击性,所以喜欢羊的人很多(我不是说伊拉克被解放的羊群们)。但羊也并没有什么忠诚度,它能因为某只小母羊,就串到别的羊群里,让你再也找不到它。

认清楚每一只羊,对人来说很困难,因为这整个过程会让人感到乏味、枯燥且没必要。豢养几只牧羊犬,是最常见的方式。牧羊犬会尽职尽责的盯梢,追逐拖后腿的、不听话的羊。牧羊犬多数情况下并不需要劳动,也不需要啃草皮 ,所以阳光明媚的时候,它可以翻着肚皮晒太阳,把狗屎排在任何地方。

牧羊犬会自己找乐子。 它会戏弄某只羊,追着它跑,虽然最后都忘掉了;它会制定自己的规则,比如某些强迫症的牧羊犬会要求羊群必须以某个路线行走。

羊惊群的时候,也是牧羊犬最警觉的时候。哪怕十几秒钟,如果羊有独立思考的时间,整个生态就会有大的变化。只要有一只羊不正常的奔跑起来,整个群体就会发生踩踏,牧羊犬就会疲于奔命。

这种可能影响晚饭的场景,牧羊犬会特别上心。

牧羊人的晚饭,偶尔是烤全羊,而牧羊犬期待着落下的骨头。至于羊,并不太在乎这种结局。

看起来大家都很满意,只有看客们多虑了。我们还是数一下下面的图有多少只羊吧。


数不清楚?只能说明不适合当牧羊犬,没什么好值得伤心的。

来源:小姐姐味道

收起阅读 »

过度设计是罪恶的!

软件开发的哪个阶段最容易招人喷?如果你严格按照什么瀑布模式、敏捷模式开发的话,你会发现永远是概要设计的评审阶段。 这个时候,屎山还没有成为既定的事实。多位理想主义达人,就会搬出各种规则、规范,来给你的方案下套子。 他们是为了你的方案更好么?大多数情况未必。有的...
继续阅读 »

软件开发的哪个阶段最容易招人喷?如果你严格按照什么瀑布模式、敏捷模式开发的话,你会发现永远是概要设计的评审阶段。


这个时候,屎山还没有成为既定的事实。多位理想主义达人,就会搬出各种规则、规范,来给你的方案下套子。


他们是为了你的方案更好么?大多数情况未必。有的人,多说几句是为了凸显自己的价值;有的人是刚看了几本书,感觉不吐不快;还有的人,本身就是完美主义者,看不得你的方案有任何瑕疵。总结下来,完美主义者还是有点作用的。


但当你把开发任务扔给这些指挥和挑刺的人,你会发现他们大多数不仅仅实现不了自己给套上的套子,连最基本的功能实现都是问题。


每当这时候,我内心都会大喊:让这些假洋鬼子去死吧!


组件替换问题


如果我们的技术栈,选用的是MySQL,我们会采用JDBC、MyBatis、JPA等一系列的基础的编码工具。但这种选择,对追求接口和实现分离的同学来说,却是不可忍受的。


这些人会搬出无数的理由,来说明,如果不加入一个中间层的话,代码是无法复用的。他们追求的是,如果你将来把数据库从MySQL切换到ElasticSearch,那么你几乎不需要改动任何代码。


“你有没有想过?如果你ES也不用了,把数据存储在Hbase中呢?”


这也是操蛋的DDD所追求和说明的,把一个简单的数据库操作给拆的七零八落。


如果把这种设计哲学推广开来的话,你会发现几乎每个地方都有问题。


项目中使用了Kafka,如果将来换成Pulsar呢?项目中使用了Http,如果将来要换成Socket呢?最让人担心的是,项目中使用了Java语言,如果后面使用Golang呢?是不是也要发明一个第三方语言来规避语言的差异?


值得注意的是,Spring家族在这些完美的目标上,产出了不少优秀的组件,比如Spring Data、Spring Cloud Stream等。


但这不代表你可以过度设计。因为用来屏蔽实现的这部分实现,本身就是风险的存在。


耦合有错么?


只要需求落在代码上,就一定会产生耦合,想要去除所有的耦合,那是根本不可能的。


在开发中,你为什么不想着为开发语言的耦合创造一个第三方语言呢?这个成本是大的,而且是非常没有必要的,如果真的有这种需求,你可以把它放在重构上。


同样的话,我也可以送给纠结底层数据库存储的同学。一旦你做了某个决定,想要完整的抽象就变的非常的奢侈,它不会比更换开发语言有更少的工作量。


这是一种思维惯式,也是一个度的问题。


在评审会议上喷一下非常的爽,但没有人会多想一想背后的工期、需求和必要性。


但如果放任耦合无限制的产生,显然也不是我们想要的,这个度的度量需要一定的学问。


内部技术和外部协作


我觉得冲突产生的根本原因,是评审者甚至开发者,没有弄清项目的边界是什么。


拿SpringCloud来说,只要定义好Feign接口的协作方式和规范,把文档写好命名做好,另外一个团队并不是很关心你后面到底是Java写的,还是挂了个sidecar的Golong程序。


再拿消息队列来说,全公司定下了Kafa作为数据交换的通道,虽然它没有JMS这样的协议兼容,你也不会蛋疼的去封装一层去兼容。大家默认Kafka的Topic/Partition机制,并基于这样的特性调整代码。


至于我的后端数据库,是用MyBatis去处理,还是用JPA去处理。是MVC三层模型,还是直接把SQL写在Controller里。只要这些是我的私有数据,外部团队永远不会用到的话,任何人都没必要对其指手画脚。


只要边界问题处理好,就不会产生大的乱子。


End


一刀切,在公司技术部门懒政的环境中,普遍存在。


在制定规范和标准的时候,大家都习惯兼容并包,照顾所有的业务线,做上一份。但在实践中,这种标准的问题通常问题多多,为业务方造成许多的困扰。


人要因材施教,规范也应该区分环境。制定规范的人活儿多一些,执行的人,生活就快乐一些!


作者:小姐姐味道
来源:https://juejin.cn/post/7088474327124819975
收起阅读 »

如何优雅地处理重复请求(并发请求)

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。重复的场景有可能是:黑客拦截了请求,重放前端/客户端因为某些原因请求重复发送了,...
继续阅读 »

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。

重复的场景有可能是:

  1. 黑客拦截了请求,重放

  2. 前端/客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击了

  3. 网关重发

  4. ….

本文讨论的是如何在服务端优雅地统一处理这种情况,如何禁止用户重复点击等客户端操作不在本文的讨论范畴。

利用唯一请求编号去重

可能会想到的是,只要请求有唯一的请求编号,那么就能借用Redis做这个去重——只要这个唯一请求编号在redis存在,证明处理过,那么就认为是重复的

代码大概如下:

    String KEY = "REQ12343456788";//请求唯一编号
  long expireTime = 1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
  long expireAt = System.currentTimeMillis() + expireTime;
  String val = "expireAt@" + expireAt;

  //redis key还存在的话要就认为请求是重复的
  Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT));

  final boolean isConsiderDup;
  if (firstSet != null && firstSet) {// 第一次访问
      isConsiderDup = false;
  } else {// redis值已存在,认为是重复了
      isConsiderDup = true;
  }

业务参数去重

上面的方案能解决具备唯一请求编号的场景,例如每次写请求之前都是服务端返回一个唯一编号给客户端,客户端带着这个请求号做请求,服务端即可完成去重拦截。

但是,很多的场景下,请求并不会带这样的唯一编号!那么我们能否针对请求的参数作为一个请求的标识呢?

先考虑简单的场景,假设请求参数只有一个字段reqParam,我们可以利用以下标识去判断这个请求是否重复。用户ID:接口名:请求参数

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;

那么当同一个用户访问同一个接口,带着同样的reqParam过来,我们就能定位到他是重复的了。

但是问题是,我们的接口通常不是这么简单,以目前的主流,我们的参数通常是一个JSON。那么针对这种场景,我们怎么去重呢?

计算请求参数的摘要作为参数标识

假设我们把请求参数(JSON)按KEY做升序排序,排序后拼成一个字符串,作为KEY值呢?但这可能非常的长,所以我们可以考虑对这个字符串求一个MD5作为参数的摘要,以这个摘要去取代reqParam的位置。

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParamMD5;

这样,请求的唯一标识就打上了!

注:MD5理论上可能会重复,但是去重通常是短时间窗口内的去重(例如一秒),一个短时间内同一个用户同样的接口能拼出不同的参数导致一样的MD5几乎是不可能的。

继续优化,考虑剔除部分时间因子

上面的问题其实已经是一个很不错的解决方案了,但是实际投入使用的时候可能发现有些问题:某些请求用户短时间内重复的点击了(例如1000毫秒发送了三次请求),但绕过了上面的去重判断(不同的KEY值)。

原因是这些请求参数的字段里面,是带时间字段的,这个字段标记用户请求的时间,服务端可以借此丢弃掉一些老的请求(例如5秒前)。如下面的例子,请求的其他参数是一样的,除了请求时间相差了一秒:

    //两个请求一样,但是请求时间差一秒
  String req = "{\n" +
          "\"requestTime\" :\"20190101120001\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

  String req2 = "{\n" +
          "\"requestTime\" :\"20190101120002\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

这种请求,我们也很可能需要挡住后面的重复请求。所以求业务参数摘要之前,需要剔除这类时间字段。还有类似的字段可能是GPS的经纬度字段(重复请求间可能有极小的差别)。

请求去重工具类,Java实现

public class ReqDedupHelper {

  /**
    *
    * @param reqJSON 请求的参数,这里通常是JSON
    * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
    * @return 去除参数的MD5摘要
    */
  public String dedupParamMD5(final String reqJSON, String... excludeKeys) {
      String decreptParam = reqJSON;

      TreeMap paramTreeMap = JSON.parseObject(decreptParam, TreeMap.class);
      if (excludeKeys!=null) {
          List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
          if (!dedupExcludeKeys.isEmpty()) {
              for (String dedupExcludeKey : dedupExcludeKeys) {
                  paramTreeMap.remove(dedupExcludeKey);
              }
          }
      }

      String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
      String md5deDupParam = jdkMD5(paramTreeMapJSON);
      log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
      return md5deDupParam;
  }

  private static String jdkMD5(String src) {
      String res = null;
      try {
          MessageDigest messageDigest = MessageDigest.getInstance("MD5");
          byte[] mdBytes = messageDigest.digest(src.getBytes());
          res = DatatypeConverter.printHexBinary(mdBytes);
      } catch (Exception e) {
          log.error("",e);
      }
      return res;
  }
}

下面是一些测试日志:

public static void main(String[] args) {
  //两个请求一样,但是请求时间差一秒
  String req = "{\n" +
          "\"requestTime\" :\"20190101120001\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

  String req2 = "{\n" +
          "\"requestTime\" :\"20190101120002\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

  //全参数比对,所以两个参数MD5不同
  String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req);
  String dedupMD52 = new ReqDedupHelper().dedupParamMD5(req2);
  System.out.println("req1MD5 = "+ dedupMD5+" , req2MD5="+dedupMD52);

  //去除时间参数比对,MD5相同
  String dedupMD53 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");
  String dedupMD54 = new ReqDedupHelper().dedupParamMD5(req2,"requestTime");
  System.out.println("req1MD5 = "+ dedupMD53+" , req2MD5="+dedupMD54);

}

日志输出:

req1MD5 = 9E054D36439EBDD0604C5E65EB5C8267 , req2MD5=A2D20BAC78551C4CA09BEF97FE468A3F
req1MD5 = C2A36FED15128E9E878583CAAAFEFDE9 , req2MD5=C2A36FED15128E9E878583CAAAFEFDE9

日志说明:

  • 一开始两个参数由于requestTime是不同的,所以求去重参数摘要的时候可以发现两个值是不一样的

  • 第二次调用的时候,去除了requestTime再求摘要(第二个参数中传入了”requestTime”),则发现两个摘要是一样的,符合预期。

总结

至此,我们可以得到完整的去重解决方案,如下:

String userId= "12345678";//用户
String method = "pay";//接口名
String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");//计算请求参数摘要,其中剔除里面请求时间的干扰
String KEY = "dedup:U=" + userId + "M=" + method + "P=" + dedupMD5;

long expireTime = 1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() + expireTime;
String val = "expireAt@" + expireAt;

// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作
Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime),
      RedisStringCommands.SetOption.SET_IF_ABSENT));

final boolean isConsiderDup;
if (firstSet != null && firstSet) {
  isConsiderDup = false;
} else {
  isConsiderDup = true;
}

转自:薛定谔的风口猪

链接:https://jaskey.github.io/blog/2020/05/19/handle-duplicate-request/

收起阅读 »

一文扫清DDD核心概念理解障碍

引言 在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些...
继续阅读 »

引言


在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些看似复杂的概念形象化、简单化。


领域、子域、核心域等这么多域到底怎么理解?


在DDD的众多概念中,首先需要搞清楚的就是到底什么是领域。因为DDD是领域驱动设计,所以领域是DDD的核心基础概念。那么到底什么是领域呢?领域是指某个业务范围以及在范围内进行的活动。根据这个定义,我们知道领域最重要的一个核心点就是范围,只有限定了问题研究的范围,才能针对具体范围内的问题进行研究分析,在后期 进行微服务拆分的的时候也是根据范围来进行的。


我们开发的软件平台是为了解决用户问题,既然我们需要研究问题并解决问题,那就得先确定问题的范围到底是什么。如果我们做的是电商平台,那么我们研究的是电商这个领域或者说电商这个范围的问题,实体店内的销售情况就不是我们研究的问题范围了。因此我们可以把领域理解为我们需要解决指定业务范围内的问题域。再举个生活中的例子,派出所实际都是有自己的片区的也就是业务范围,户籍管理、治安等都是归片区的派出所负责的。这里的片区实际就是领域,派出所专注解决自己片区内的各种事项。


既然我们研究的领域确定了,或者说研究的问题域以及范围确定了,那么接下来就需要对领域进行进一步的划分和切割。实际上这和我们研究事物的一般方法手段是一致的,一旦某个问题太大无从下手的时候,都会将问题进行一步步的拆解,再逐个进行分析和解决。那么放到DDD中,我们在进行分析领域的时候,如果领域对应的业务范围过大,那么就需要对领域进行拆解划分,形成对应的子域或者说更小的问题域,所以说子域对应的是相对于领域来说,更小的业务范围以及问题域。


回到我们刚才所说的电商领域,它就是一个非常大的领域,因为电商实际还包含了商品、用户、营销、支付、订单、物流等等各种复杂的业务。因此支付域、物流域等就是相对于电商来说更小的业务范围或者更小的问题域,那么这部分领域就是对于电商这个领域的子域,相当于对电商这个业务范围的进一步的划分。


image.png


搞清楚了领域和子域的区别之后,那么怎么理解核心域、通用域以及支撑域这么多其他的域呢(域太多了,我脑袋开始嗡嗡响了)?领域和子域是按照范围大小进行区分的,那么核心域、通用域等实际就是按照功能属性进行划分的。


核心域:平台的核心竞争力、最重要的业务,比如对于阿里来说,电商就是核心域。


通用域:其他子域沉淀的通用能力,没有定制化的能力,比如公司的数据中台。


支撑域:不包含核心业务能力也不是各个子域的通用能力沉淀。


那么为什么划分了子域之后,还要分什么核心域、通用域呢?实际上这样划分的目的是根据子域的属性,确定公司对于不同域的资源投入。将优势的资源投入到具备核心竞争力的域上,也是为了让产品更加具备竞争力,就是所谓的钱要花到刀刃上。


限界上下文?限界是什么?上下文又是什么?


限界上下文我觉得是DDD中一个不太好理解的概念,光看这个不明觉厉的名字,甚至有点不知道它到底想表达什么样的意思。我们先来看下限界上下文的原文---Bounded Context,通过原文我们可以看得出来,实际上限界上下文这个翻译增加了我们的理解成本。而反观Bounded Context这个原文实际更好理解一点,即为有边界的上下文。这里给大家一个小建议,如果技术上某个概念不好理解,那么不妨去看看它的原文是什么,大部分情况下原文会比翻译过来的更好理解,更能反映设计者想要表达的真实含义。


image.png


大家都知道我们的语言是有上下文环境的,有的时候同样一句话在不同的语言环境或者说语言上下文中,所代表的意思是不一样的。打个比方假如你有一个女朋友,你们约好晚上一起去吃饭,你在去晚上吃饭地方的路上,这个时候你收到一条来自女朋友的语音:“我已经到龙翔桥了,你出来后往苹果店走。如果你到了,我还没到,你就等着吧。如果我到了,你还没到,你就等着吧。”这里的你就等着吧,在不同的语境下包含的意思是不同的,一个是陈述事实,一个让你瑟瑟发抖。


因此,既然语言本身就有上下文,那么用通用语言描述的业务肯定也是有边界的。DDD中的限界上下文就是用来圈定业务范围的,目的是为了确保业务语言在限界上下文内的表达唯一,不会产生额外的歧义。这个时候大家会不会有另外一个问题,那么这个限界上下文到底是一个逻辑概念还是代码层面会有一个实实在在的边界呢?


按照我自己的理解,限界上下文既是概念上的业务边界,也是代码层面的逻辑逻辑边界。为什么这么说呢?我们在进行业务划分的时候,领域划分为子域集合,子域再划分为子子域集合,那么子子域的业务边界有时候就会和限界上下文的边界重合,也就是说子子域本身就是限界上下文,那么此时限界上下文就是业务边界。在代码落地的过程中,用户服务涉及到用户的创建、用户信息的修改等操作。肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的边界上下文中进行。


域和边界上下文的关系是一对一或者一对多的关系,实际上我认为域和限界上下文本质上一致的,应该是为什么这么说呢,比如我们做的微服务当中用户服务,比如,肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的限界上下文中进行。限界上下文最主要的作用就是限定哪些业务面描述以及业务动作在这个限界当中。


image.png


总结
DDD在实际落地实践过程中会遇到各种各样的问题,首当其冲的就是一些核心概念晦涩难懂,阻碍了技术人员对DDD的理解和掌握。本文对DDD比较难理解的核心概念进行了详细的描述,相信通过本文大家对于这些核心概念的理解能够更加深入。


创作不易,如果各位同学觉得文章还不错的话,麻烦点赞+收藏+评论交流哦。老样子,文末和大家分享一首诗词。


西江月▪中秋和子由


世事一场大梦,人生几度秋凉?夜来风叶已鸣廊,看取眉头鬓上。


酒贱常愁客少,月明多被云妨。中秋谁与共孤光,把盏凄然北望。


作者:慕枫技术笔记
来源:https://juejin.cn/post/7084074668147081230 收起阅读 »

Redis 缓存穿透与缓存击穿

一、🐕缓存穿透(查不到数据) 比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持...
继续阅读 »

一、🐕缓存穿透(查不到数据)


比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透



解决方案




布隆过滤器


布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
在这里插入图片描述




缓存空对象


当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了持久层数据库
在这里插入图片描述


但是这种方法会存在两个问题


1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,并且当中还有还能多空值的键


2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响



二、🐂缓存击穿(量太大,缓存过期)


这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的杠大并发,大并发集中对这个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一堵墙上戳穿了一个洞
当某个key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库瞬间压力过大



解决方案



设置热点数据永不过期



从缓存层面上看,没有设置过期时间,所以不会出现热点key过期后产生的问题



加互斥锁



分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获取分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大



三、🐅缓存雪崩


缓存雪崩,是指在某一个时间段,缓存集中过期失效,Redis宕机


产生雪崩的原因之一,比如在双十一零点,有一波商品比较热门,所以这波商品会放在缓存,假设缓存一个小时,那么到了1点钟时,缓存即将过期,此时如果再对此波商品进行访问查询,压力最终就会落到数据库上,对于数据库而言,就会产生周期性的压力波峰,于是所有的请求都会到达存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。


在这里插入图片描述


其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,此时,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。



解决方案


1、Redis 高可用


既然 redis 有可能挂掉,那我就设多几台 redis ,其实就是搭建集群


2、限流降级


在缓存失效后,通过加锁或者 队列来控制读数据库写缓存的线程数量,比如对某个key 只允许一个线程查询数据和写缓存,其他线程等待


3、数据预热


数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀



作者:CSDNCoder
来源:https://juejin.cn/post/7084820115505545247
收起阅读 »

系统模块划分设计的思考

系统模块划分设计的思考前言首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。直接原因公司内部某技术团队,在引用我们系统的client包时,启动失败...
继续阅读 »

系统模块划分设计的思考

前言

首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。

直接原因

公司内部某技术团队,在引用我们系统的client包时,启动失败。
失败原因是由于client下有一个cache相关的依赖,其注入失败导致的。

然后,就发出了这样一个疑问:我只是希望使用一个hsf接口,为什么还要引入诸如缓存,web处理工具等不相关的东西。

这也就自然地引出了前辈对我的一句教导:对外的client需要尽可能地轻便。

很明显,我们原有的client太重了,包含了对外的RPC接口,相关模型(如xxxDTO),工具包等等。

可能有人就要提出,直接RPC+模型一个包,其它内容一个包不就OK了嘛?

问题真的就这么简单嘛?

根本原因

其实出现上述问题,是因为在系统设计之初,并没有深入思考client包的定位,以及日后可能遇到的情况。

这也就导致了今天这样的局面,所幸目前的外部引用并不多,更多是内部引用。及时调整,推广新的依赖,与相关规范为时不晚。

常见模块拆分

先说说我以前的模块拆分。最早的拆分是每个业务模块主要拆分为:

  • xxx-service:具体业务实现模块。

  • xxx-client:对外提供的RPC接口模块。

  • xxx-common:对外的工具,以及模型。

这种拆分方式,是我早期从一份微服务教程中看到的。优点是简单明了,调用方可选择性地选择需要的模块引入。

至于一些通用的组件,如统一返回格式(如ServerResponse,RtObject),则放在了最早的核心(功能核心,但内容很少)模块上。

后来,认为这样并不合适,不应该将通用组件放在一个业务模块上。所以建立了一个base模块,用来将通用的组件,如工具,统一返回格式等都放入其中。

另外,将每个服务都有的xxx-common模块给取消了。将其中的模型,放入了xxx-client,毕竟是外部调用需要的。将其中的工具,根据需要进行拆分:

  • base:多个服务都会使用的。

  • xxx-service:只有这个服务本身使用

  • xxx-client:有限的服务使用,并且往往是服务提供方和服务调用方都要使用。但是往往这种情况,大多是由于接口设计存在问题导致的。所以多为过渡方案。

上述这个方案,也就是我在负责某物联网项目时采用的最终模块划分方式。

在当时的业务下,该方案的优点是模块清晰,较为简洁,并且尽可能满足了迪米特原则(可以参考《阿里Java开发手册相关实践》)。缺点则是需要一定的技术水平,对组件的功能域认识清晰。并且需要有一定的设计思考与能力(如上述工具拆分的第三点-xxx-client,明白为什么这是设计缺陷导致,并能够解决)。

新的问题

那么,既然上述的方案挺不错的,为什么不复用到现在的项目呢?

因为业务变了,导致应用场景变了。而这也带来了新的问题,新的考虑角度。

原先的物联网业务规模并不大,所以依赖也较为简单,也并不需要进行依赖的封装等,所以针对主要是client的内/外这一维度考虑的。

但是现有的业务场景,由于规模较大,模块依赖层级较多,导致上层模块引入过多的依赖。如有一个缓存模块,依赖tair-starter(一个封装的key-value的存储),然后日志模块依赖该缓存模块(进行性能优化),紧接着日志模块作为一个通用模块,被放入了common模块中。依赖链路如下:

调用方 -> common模块 -> 日志模块 -> 缓存模块 -> tair-starter依赖

但是有的调用方表示,根本就不需要日志模块,却引入了tair-starter这一重依赖(starter作为封装,都比较重),甚至由于tair-starter的内部依赖与自身原有依赖冲突,得去排查依赖,进行exclude。
但是同时,也有的调用方,系统通过rich客户端,达到性能优化等目标。

所以,现有的业务场景除了需要考虑client的内/外这一维度,还需要考虑client的pool/rich这一维度。

可能有的小伙伴,看到这里有点晕乎乎的,这两个维度考量的核心在哪里?

内/外,考虑的是按照内外这条线,尽量将client设计得简洁,避免给调用方引入无用依赖。

而pool/rich,考虑的是性能,用户的使用成本(是否开箱即用)等。

最终解决方案

最终的解决方案是对外提供3+n

  • xxx-client(1个):所有外部系统引用都需要的内容,如统一返回格式等。

  • xxx-yyy-client(n个):对具体业务依赖的引用,进行了二次拆分。如xxx-order-client(这里是用订单提花那你一下,大家理解意思就OK)。

  • xxx-pool-client(1个):系统引用所需要的基本依赖,如Lindorm的依赖等。

  • xxx-rich-client(1个):系统引用所需要的依赖与对应starter,如一些自定义的自动装载starter(不需要用户进行配置)。

这个方案,换个思路,理解也简单。
我们提供相关的能力,具体如何选择,交给调用方决定。

其实,讨论中还提到了BOM方案(通过DependentManagement进行jar包版本管理)。不过分析后,我们认为BOM方案更适合那些依赖集比较稳定的client,如一些中间件。而我们目前的业务系统,还在快速发展,所以并不适用。

总结

简单来说,直接从用户需求考虑(这里的用户就是调用方):

  • 外部依赖:

    • 额外引入的依赖尽可能地少,最好只引入二方依赖(我们提供的jar),不引入第三方依赖。

    • 引入的二方依赖不“夹带”私货(如二方jar引入了一堆大第三方依赖)。

  • 自动配置:

    • 可以傻瓜式使用。如引入对应的starter依赖,就可以自动装配对应默认配置。

    • 也可以自定义配置。用户可以在自定义配置,并不用引入无效的配置(因为starter经常引入不需要的依赖)。

  • 性能:

    • 可以通过starter,提供一定的封装,保证一定的性能(如接口缓存,请求合并等)。

    • 可以自定义实现基础功能。因为有些人并不放心功能封装(虽然只是少数,但是稳定性前辈提出的)。

补充

这里补充一点,我对讨论中一个问题的回答,这里提一下。

有人提到工具类,应该如何划分。因为有的工具类,是不依赖于服务状态的,如CookieUtil进行Cookie处理。有的工具类,是依赖于服务状态的,如RedisUtil包含RedisPool状态,直连Redis,处理Redis请求与响应。

其实这里有两个细节:

  • 工具应该按照上面的方式进行划分为两种。单一模块系统,不依赖服务状态的工具往往置于util包,依赖服务状态的工具往往置于common包中。这里还有一个明显的区分点:前者的方法可以设为static,而后者并不能(因为依赖于new出来的状态)。

  • 依赖于状态的工具类,其实是一种拆分不完全的体现。如RedisUtil,可以拆分为连接状态管理的RedisPool与请求响应处理的RedisUitl。两者的组合方式有很多,取决于使用者的需要,以后有机会写一篇相关的博客。不过,我希望大家记住 面向接口编程的原则。

愿与诸君共进步。

原文链接:https://blog.csdn.net/cureking/article/details/105663951

收起阅读 »

不幸言中,“核酸码”打不开.....那就聊聊为什么我觉得要挂的原因吧!

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码。当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。我也顺带瞎扯了一句...
继续阅读 »

周四晚上的时候,看到消息说4月9日起要采用新的核酸检查系统,要推出一个新的码,叫:核酸码

当晚就有很多网友发现随申办上已经有入口了,但点进去是报错的:

但是因为还没投入真正使用,所以也没啥大的反馈,大家就瞎讨论了技术栈和这个错误可能的原因啥的。

我也顺带瞎扯了一句:可能会出性能问题(因为我一直觉得国内擅长Hibernate的开发者比较少)


谁想到,今天在获取核酸码的时候真的碰到各种困难,在获取核酸码的时候,就一直刷不出来,有时候显示人多,有时候504错误:

上面我是12点尝试的,后来16、17点还看到很多朋友圈反应各种卡住,刷不出来。


可能这个系统确实太赶了,所以没做好?不过这个谁知道呢?作为一名技术博主就不瞎猜了。

顺手分享一下为什么我觉得用spring data jpa,很可能会挂?

先说说常规国内用的比较多的技术MyBatis,因为大家都是用直接写SQL的方式来实现数据读写的,这个时候团队里DBA、数据库专家、或者实力强点的开发,往往自己已经能够把SQL执行优化到比较好的地步了。这个是否能做好,与我们对SQL、Java这些知识的掌握程度有关

而当我们用Spring Data JPA这样的框架时候,开发者在框架的帮助下,好多SQL都被隐藏了,喜欢些Java代码来替代SQL的开发过程是挺爽的,但也因为这个原因,他可能并不知道最终自己写的代码真正会执行的SQL具体是怎么样的。

这的时候对于优化就带了很大的难度,对于专业DBA来说,他一般都是不具备Spring Data JPA代码到SQL转化的认识,他是很难帮你做静态分析的。而开发者一侧也有这个问题,如果不是很熟悉Hibernate的话,就很容易写出低性能的代码(不代表框架实现的低性能,核心还是使用姿势的问题)。

所以,我一直建议在高并发系统中对数据访问框架的选型一定要慎重,不是说Spring Data JPA不行,而是需要有熟悉的人来把握(特别提这点的原因是国好多是半调子)。不然就比较容易出现性能问题,但是MyBatis的话,对于国内开发者来说,因为直接写SQL,所以还是相对还是更容易理解和把控一些。

好了,借今天核酸码的现象,跟大家聊聊这两个框架的想法,不知道你是否认同?欢迎留言区说说你的观点。

来源:https://mp.weixin.qq.com/s/43bE8juIRQbQLO3vBTUKWA

收起阅读 »

你会写注释吗?

前言有一本书叫《代码整洁之道》,不知你看过没?初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗...
继续阅读 »

前言

有一本书叫《代码整洁之道》,不知你看过没?

初次听闻此书,并未激发我的阅读欲。再次听闻,不免心想:代码竟还整洁之道?我倒要瞧瞧,怎么个整洁法。

我是怀着试探地心看了这本书,结果收获了满脑子糟糕的代码。天呐!这代码我貌似一句也看不懂,幸好还有文字,尚可宽慰我这颗被代码撞乱的心,于是咬咬牙读了下去。

这本书里面讲了很多代码整洁之道,关于有意义的命名、函数、注释、格式、错误处理、边界等共十七大篇章。如果你感兴趣,可以去看看。我只是粗略地看了一下,因为有些我也看不大明白。特别是当某些代码脱离了计算机而存在的时候,我好像不认识它们了,它们变得异常陌生。恕我我孤陋寡闻了,哎。

尽管如此,此书第四章中,关于“注释”的代码整洁之道,却给我留下了异常深刻的印象。Why? 因为里面关于注释的观点刷新了我的认知,与我的思想产生了一点点灵魂的碰撞,并且说服了我,还驱动我写下了这篇文章。

一、被注释吸引

下面是“注释”篇章的开头两段,特意贴了上来,因为我就是被这样的开头吸引了。希望它能带给你一点点启发。


不知你读完以上两段,作何感想?

我的感想是:如果你的代码写得足够优秀,是不需要过多注释的。注释最多可以证明糟糕的代码。

额,此刻我很想找一个捂脸的表情。与此同时,我在脑海里迅速地回忆了一遍注释之于我的心历路程:从最初知道“注释”这么个神奇玩意儿时的欣喜,到步步沦陷“注释”的魔爪,以致如今看着满屏的代码,不写点儿注释都感觉空落落的......

收回来,继续品。 作者开篇的观点约莫如下:

  • 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败

  • 如果你发现自己需要写注释,再想想是否有办法翻盘,用代码来表达

  • 注释会撒谎,代码在变动演化,但注释不能总是跟着走

  • 只有代码是唯一准确的信息来源

注意,作者用来了“失败”一词。你无法找到表达自我的恰当方法,所以就要用注释,这并不值得庆祝。当然,这并不意味作者就完全否定了注释的价值,程序员应当负责将代码保持在可维护、有关联、精确的高度。只不过作者更倾向于把力气用在写清楚代码上,直接保证无须编写注释,或者花心思减少注释量。

二、好的注释

有些注释是必须的,作者列举了一些值得写的注释。

  • 公司代码规范要求编写与法律有关的注释

  • 提供基本信息的注释

  • 对意图的解释

  • 阐释:把晦涩难明的参数或返回值的意义翻译为某种可读形式

  • 警示:用于警告其他程序员会出现某种后果的注释

  • TODO 注释:是一种程序员认为应该做,但由于某些原因目前还没做的工作

  • 放大:放大某种看起来不合理之物的重要性的注释

  • 公共 API 中的 Javadoc

尽管如此,作者一再强调:唯一真正好的注释是你想办法不去写的注释。足见作者对注释之深恶痛疾,对糟糕代码之嫌弃,对代码整洁要求之高。你可以细品。

三、坏的注释

果然是有代码洁癖的人,作者用了更多的篇幅来描述坏的注释。

  • 喃喃自语:因为过程需要就添加注释,就是无谓之举

  • 多余的注释:并不比代码本身提供更多的信息,甚至比读代码所花时间长

  • 误导性注释:写出不够精确的注释误导读者

  • 循规式注释:每个函数都要有 Javadoc 或每个变量都要有注释的规则愚不可及

  • 日志式注释:每次编辑代码时,在模块开始处添加一条注释,应当全部删除

  • 废话注释:喋喋不休,废话连篇的注释,一旦代码修改,将变成一堆谎言

  • 能用函数或变量时就别用注释:建议重构代码,删掉注释

  • 位置标记:在源代码中标记某个特别位置,多数实属无理又鸡零狗碎

  • 括号后面的注释:如果你发现自己想标记右括号,其实应该做的是缩短函数

  • 归属与署名:源代码控制系统是这类信息最好的归属地

  • 注释掉的代码:注释掉的代码堆积在一起,就像酒瓶底的渣滓一般

  • HTML 注释:源代码注释中的 HTML 标记是一种厌物

  • 非本地信息:假如你一定要写注释,请确保它描述了离它最近的代码

  • 信息过多:别在注释中添加有趣的历史性话题或无关的细节描述

  • 不明显的关系:注释及其描述的代码之间的联系应该显而易见

  • 函数头:短函数不需要太多的描述,选个好的函数名胜于写函数头注释

一言以蔽之:用整理代码的决心替代创造废话的冲动吧。 你会发现自己成为更优秀、更快乐的程序员。

小结

作者把“注释”拎出来,说了这么多,最终还是回归到了代码本身。

那如何才能写出整洁的代码呢?如果你不明白整洁对代码的意义,尝试去写整洁代码就毫无意义。如果你明白糟糕的代码给你带来的代价,你就会明白,花时间保持代码整洁不但有关效率,还有关生存。争取让营地比你来时更干净吧!

最后,贴上书中震撼我的一隅,希望它能指引你逐渐走向代码整洁之道,与君共勉!



作者:linwanxia
来源:https://juejin.cn/post/7083029096615116837

收起阅读 »

作为一名前端,该如何理解Nginx?

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~Nginx是什么?Ng...
继续阅读 »

大家好,我叫小杜杜,作为一名小前端,只需要好好写代码,至于部署相关的操作,我们通常接触不到,正所谓专业的人干专业的事,我们在工作中并不需要去配置,但这并不代表不需要了解,相信大家都多多少少听过nginx,所以今天就聊聊,还请大家多多支持~


Nginx是什么?

Nginx (engine x) 是一个轻量级、高性能的HTTP反向代理服务器,同时也是一个通用代理服务器(TCP/UDP/IMAP/POP3/SMTP),最初由俄罗斯人Igor Sysoev编写。

简单的说:

  • Nginx是一个拥有高性能HTTP和反向代理服务器,其特点是占用内存少并发能力强,并且在现实中,nginx的并发能力要比在同类型的网页服务器中表现要好

  • Nginx专为性能优化而开发,最重要的要求便是性能,且十分注重效率,有报告nginx能支持高达50000个并发连接数

正向代理和反向代理

Nginx 是一个反向代理服务器,那么反向代理是什么呢?我们先看看什么叫做正向代理

正向代理:局域网中的电脑用户想要直接访问网络是不可行的,只能通过代理服务器(Server)来访问,这种代理服务就被称为正向代理。

就好比我们俩在一块,直接对话即可,但如果我和你分隔两地,我们要想对话,必须借助一个通讯设备(如:电话)来沟通,那么这个通讯设备就是"代理服务器",这种行为称为“正向代理”

那么反向代理是什么呢?

反向代理:客户端无法感知代理,因为客户端访问网络不需要配置,只要把请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据,然后再返回到客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

在正向代理中,我向你打电话,你能看到向你打电话的电话号码,由电话号码知道是我给你打的,那么此时我用虚拟电话给你打过去,你看到的不再是我的手机号,而是虚拟号码,你便不知道是我给你打的,这种行为变叫做"反向代理"。

在以上述的例子简单的说下:

  • 正向代理:我通过我的手机(proxy Server)去给你打电话,相当于我和我的手机是一个整体,与你的手机(Server)是分开的

  • 反向代理:我通过我的手机(proxy Server)通过软件转化为虚拟号码去给你打电话,此时相当于我的手机和你的手机是一个整体,和我是分开的

负载均衡

负载均衡:是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

如果没有负载均衡,客户端与服务端的操作通常是:客户端请求服务端,然后服务端去数据库查询数据,将返回的数据带给客户端


但随着客户端越来越多,数据,访问量飞速增长,这种情况显然无法满足,我们从上图发现,客户端的请求和相应都是通过服务端的,那么我们加大服务端的量,让多个服务端分担,是不是就能解决这个问题了呢?

但此时对于客户端而言,他去访问这个地址就是固定的,才不会去管那个服务端有时间,你只要给我返回出数据就OK了,所以我们就需要一个“管理者“,将这些服务端找个老大过来,客户端直接找老大,再由老大分配谁处理谁的数据,从而减轻服务端的压力,而这个”老大“就是反向代理服务器,而端口号就是这些服务端的工号。


向这样,当有15个请求时,反向代理服务器会平均分配给服务端,也就是各处理5个,这个过程就称之为:负载均衡

动静分离

当客户端发起请求时,正常的情况是这样的:


就好比你去找客服,一般先是先说一大堆官方的话,你问什么,他都会这么说,那么这个就叫静态资源(可以理解为是html,css)

而回答具体的问题时,每个回答都是不同的,而这些不同的就叫做动态资源(会改变,可以理解为是变量)

在未分离的时候,可以理解为每个客服都要先说出官方的话,在打出具体的回答,这无异加大了客服的工作量,所以为了更好的有效利用客服的时间,我们把这些官方的话分离出来,找个机器人,让他代替客服去说,这样就减轻了客服的工作量。

也就是说,我们将动态资源和静态资源分离出来,交给不同的服务器去解析,这样就加快了解析的速度,从而降低由单个服务器的压力


安装 Nginx

关于 nginx 如何安装,这里就不做过多的介绍了,感兴趣的小伙伴看看这篇文章:【Linux】中如何安装nginx

这里让我们看看一些常用的命令:

  • 查看版本:./nginx -v

  • 启动:./nginx

  • 关闭:./nginx -s stop(推荐) 或 ./nginx -s quit

  • 重新加载nginx配置:./nginx -s reload

Nginx 的配置文件

配置文件分为三个模块:

  • 全局块:从配置文件开始到events块之间,主要是设置一些影响nginx服务器整体运行的配置指令。(按道理说:并发处理服务的配置时,值越大,可支持的并发处理量越多,但此时会受到硬件、软件等设备等的制约)

  • events块:影响nginx服务器与用户的网络连接,常用的设置包括是否开启对多workprocess下的网络连接进行序列化,是否允许同时接收多个网络连接等等

  • http块:如反向代理和负载均衡都在此配置

location 的匹配规则

共有四种方式:

    location[ = | ~ | ~* | ^~ ] url {
   
  }
复制代码
  • =精确匹配,用于不含正则表达式的url前,要求字符串与url严格匹配,完全相等时,才能停止向下搜索并处理请求

  • ^~:用于不含正则表达式的url前,要求ngin服务器找到表示url和字符串匹配度最高的location后,立即使用此location处理请求,而不再匹配

  • ~最佳匹配,用于表示url包含正则表达式,并且区分大小写。

  • ~*:与~一样,只是不区分大小写

注意:

  • 如果 url 包含正则表达式,则不需要~ 作为开头表示

  • nginx的匹配具有优先顺序,一旦匹配上就会立马退出,不再进行向下匹配

End

关于具体的配置可以参考:写给前端的nginx教程

致此,有关Nginx相关的知识就已经完成了,相信对于前段而言已经足够了,喜欢的点个赞👍🏻支持下吧(● ̄(エ) ̄●)


作者:小杜杜
来源:https://juejin.cn/post/7082655545491980301

收起阅读 »

高并发之伪共享和缓存行填充(缓存行对齐)(@Contended)

1.使用缓存行(Cache Line)填充前后对比伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果...
继续阅读 »

1.使用缓存行(Cache Line)填充前后对比

伪共享和缓存行填充,我们先看一个例子,让大家感受一下了解底层知识后,你的代码可以快到起飞的感jio: 在类中定义看似无用的成员属性,速度有质的提升。 如下是未使用缓存行(Cache Line)填充方法运行的结果,可以看到耗时是3579毫秒:

而在其变量x的前后加上7个long类型到变量(在变量x前56Byte,后面也是56Byte,这就是缓存行填充,下面章节会详细介绍),当然这个14个变量是不会在代码中被用到的,但是为什么速度会提升将近2倍呢,如下图所示,可以看到耗时为1280毫秒:


ps:上面两个截图中的完整代码见
章节5,大家也可以直接跳转到章节去看下完整的代码。

为什么会这么神奇,这里为先提前说下结论,具体的大家可以往后看。

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,缓存行的内容一发生变化,就需要进行缓存同步,这个同步是需要时间的。

2.内存、缓存与寄存器之间如何传输数据

为什么会这样呢?前面我们提到过缓存一致性的问题,见笔者该篇博文:“了解高并发底层原理”,面试官:讲一下MESI(缓存一致性协议)吧,点击文字即可跳转。 其中内存、缓存与寄存器之间的关系图大致如下:


硬盘中的可执行文件加载到寄存器中进行运算的过程如下:

  1. 硬盘中的可执行文件(底层存储还是二进制的)加载到内存中,操作系统为其分配资源,变成了一个进程A,此时还没有跑起来;

  2. 过了一段时间之后,CPU0的时间片分配给了进程A,此时CPU0进行线程的装载,然后把需要用到的数据先从内存中读取到缓存中,读取的单元为一个缓存行,其大小现在通常为64字节(记住这个缓存行大小为64字节,这个非常重要,在后面会多次用到这个数值)。

  3. 然后数据再从缓存中读取到寄存器中,目前缓存一般为三级缓存,这里不具体画出。

  4. 寄存器得到了数据之后送去ALU(arithmetic and logic unit)做计算。

这里说一下为什么要设计三级缓存:

  • 电脑通过使用时钟来同步指令的执行。时钟脉冲在一个固定的频率(称为时钟频率)。当你买了一台1.5GHz的电脑,1.5GHz就是时钟频率,即每秒15亿次的时钟脉冲,一次完整的时钟脉冲称为一个周期(cycle),时钟并不记录分和秒。它以不变的速率简单跳动。

  • 其主要原因还是因为CPU方法内存消耗的时间太长了,CPU从各级缓存和内存中读取数据所需时间如下:

CPU访问大约需要的周期(cycle)大约需要的时间
寄存器1 cycle0ns
L1 Cache3—4 cycle1ns
L2 Cache10—20 cycle3ns
L3 Cache40—45 cycle15ns
内存60—90ns

3.缓存中数据共享问题(真实共享和伪共享)

3.1 真实共享(不同CPU的寄存器中都到了同一个变量X)

首先我们先说数据的真实共享,如下图,我们在CPU0和CPU1中都用到了数据X,现在不考虑数据Y。


如果不考虑缓存一致性,会出现如下问题: 在多线程情况下,此时由两个cpu同时开始读取了long X =0,然后同时执行如下语句,会出现如下情况:

int X = 0;
X++;

刚开始,X初始化为0,假设有两个线程A,B,

  1. A线程在CPU0上进行执行,从主存加载X变量的数值到缓存,然后从缓存中加载到寄存器中,在寄存器中执行X+1操作,得到X的值为1,此时得到X等于1的值还存放在CPU0的缓存中;

  2. 由于线程A计算X等于1的值还存放在缓存中,还没有刷新会内存,此时线程B执行在CPU1上,从内存中加载i的值,此时X的值还是0,然后进行X+1操作,得到X的值为1,存到CPU1的缓存中,

  3. A,B线程得到的值都是1,在一定的时间周期之后刷新回内存

  4. 写回内存后,两次X++操作之后,其值还是1;

可以看到虽然我们做了两次++X操作,但是只进行了一次加1操作,这就是缓存不一致带来的后果。

如何解决该问题:

  • 具体的我们可以通过MESI协议(详情见笔者该篇博文:blog.csdn.net/MrYushiwen/…)来保证缓存的一致性,如上图最中间的红字所示,在不同寄存器的缓存中,需要考虑数据的一致性问题,这个需要花费一定的时间来同步数据,从而达到缓存一致性的作用。

3.2伪共享(不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,并且XY在同一个缓存行中)

  • 缓存一致性是根据缓存行(Cache line)为单元来进行同步的,即缓存中的传输单元为缓存行,一个缓存行大小通常为64Byte;

  • 缓存行的内容一发生变化,就需要进行缓存同步;

  • 在3.1中,我们在寄存器用到的数据是同一个X,他们肯定是在同一个缓存行中的,这个是真实的共享数据的,共享的数据为X。

  • 而在3.2中,不同CPU的寄存器中用到了不同的变量,一个用到的是X,一个用到的是Y,但是变量X、Y在同一个缓存行中(一次读取64Byte,见3.1中的图),缓存一致性是根据缓存行为单元来进行同步的,所以虽然用到的不是同一个数据,但是他们(数据X和数据Y)在同一个缓存行中,他们的缓存同步也需要时间。


4.伪共享解决办法(缓存行填充或者使用@Contended注解)

4.1.缓存行填充

如章节一所示,我们可以在x变量前后进行缓存行的填充,:

public volatile long A,B,C,D,E,F,G;
public volatile long x = 1L;
public volatile long a,b,c,d,e,f,g;

添加后,3.2章节中的截图将会变成如下样子:


不论如何进行缓存行的划分,包括x在内的连续64Byte,也就是一个缓存行不可能存在变量Y,同样变量Y所在的缓存行不可能存在x,这样就不存在伪共享的情况,他们之间就不需要考虑缓存一致性问题了,也就节省了这一部分时间。

4.2.Contended注解

在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。我们目前的缓存行大小一般为64Byte,这里Contended注解为我们前后加上了128字节绰绰有余。 注意:如果想要@Contended注解起作用,需要在启动时添加JVM参数-XX:-RestrictContended 参数后 @sun.misc.Contended 注解才有。

然而在java11中@Contended注解被归类到模块java.base中的包jdk.internal.vm.annotation中,其中定义了Contended注解类型。笔者用的是java12,其注解如下:


加上该注解,如下,也能达到缓存行填充的效果


5.完整代码(利用缓存行填充和没用缓存行填充)

大家自己也可以跑一下如下代码,看利用缓存行填充后的神奇效果。

5.1没用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 2:52 PM
* @Version: 1.0
*/

public class NoCacheLineFill {

   public volatile long x = 1L;
}

class MainDemo {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       NoCacheLineFill[] arr = new NoCacheLineFill[2];
       arr[0] = new NoCacheLineFill();
       arr[1] = new NoCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 100_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

5.2利用缓存行填充代码如下:

package mesi;

import java.util.concurrent.CountDownLatch;

/**
* @Author: YuShiwen
* @Date: 2022/2/27 3:45 PM
* @Version: 1.0
*/

public class UseCacheLineFill {

   public volatile long A, B, C, D, E, F, G;
   public volatile long x = 1L;
   public volatile long a, b, c, d, e, f, g;
}

class MainDemo01 {

   public static void main(String[] args) throws InterruptedException {
       // CountDownLatch是在java1.5被引入的,它是通过一个计数器来实现的,计数器的初始值为线程的数量。
       // 每当一个线程完成了自己的任务后,调用countDown方法,计数器的值就会减1。
       // 当计数器值到达0时,它表示所有的线程已经完成了任务,然后调用await的线程就可以恢复执行任务了。
       CountDownLatch countDownLatch = new CountDownLatch(2);

       UseCacheLineFill[] arr = new UseCacheLineFill[2];
       arr[0] = new UseCacheLineFill();
       arr[1] = new UseCacheLineFill();

       Thread threadA = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[0].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadA");

       Thread threadB = new Thread(() -> {
           for (long i = 0; i < 1_000_000_000L; i++) {
               arr[1].x = i;
          }
           countDownLatch.countDown();
      }, "ThreadB");

       final long start = System.nanoTime();
       threadA.start();
       threadB.start();
       //等待线程A、B执行完毕
       countDownLatch.await();
       final long end = System.nanoTime();
       System.out.println("耗时:" + (end - start) / 1_000_000 + "毫秒");

  }
}

作者:YuShiwen
来源:https://juejin.cn/post/7083030159304949767

收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间...... 都什么年代了你还在用 Date

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

纯后端如何写前端?我用了低代码平台

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。1、我的前端技术老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一...
继续阅读 »

我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手

花了几天搭了个后台管理页面,今天分享下我的搭建过程,全文非技术向,就当跟大家吹吹水吧。


1、我的前端技术

老读者可能知道我是上了大学以后,才了解什么是编程。在这之前,我对编程一无所知,甚至报考了计算机专业之后也未曾了解过它是做什么的。

在大一的第一个学期,我印象中只开了一门C++的编程课(其他的全是数学)。嗯,理所当然,我是听不懂的,也不知道用来干什么。


刚进大学的时候,我对一切充满了未知,在那时候顺其自然地就想要进几个社团玩玩。但在众多社团里都找不到我擅长的领域,等快到截止时间了。我又不想大学期间什么社团都没有参加,最后报了两个:乒乓球社团和计算机协会

这个计算机协会绝大多数的人员都来自于计算机专业,再后来才发现这个协会的主要工作就是给人「重装系统」,不过这是后话啦。

当时加入计算机协会还需要满足一定的条件:师兄给了一个「网站」我们这群人,让我们上去学习,等到国庆回来后看下我们的学习进度再来决定是否有资格加入。

那个网站其实就是对HTML/CSS/JavaScript入门教程,是一个国外的网站,具体的地址我肯定是忘了。不过那时候,我国庆闲着也没事干,于是就开始学起来了。我当时的进度应该是学到CSS,能简单的页面布局和展示图片啥的

刚开始的时候,觉得蛮有趣的:我改下这个代码,字体的颜色就变了,图片就能展示出来了。原来我平时上网的网站是这样弄出来的啊! (比什么C++有趣多了)

国庆后回来发现:考核啥的并不重要,只要报名了就都通过了。


有了基本的认知后,我对这个也并不太上心,没有持续地学下去。再后来,我实在是太无聊,就开始想以后毕业找工作的事了,自己也得在大学充实下自己,于是我开始在知乎搜各种答案「如何入门编程」。

在知乎搜了各种路线并浪费了大量时间以后,我终于开始看视频入门。我熬完了JavaSE基础之后,我记得我是看方立勋老师入门的JavaWeb,到前端的课程以后,我觉得前端HTML/CSS/JavaScript啥的都要补补,于是又去找资源学习(那时候信奉着技多不压身)。

印象中是看韩顺平老师的HTML/CSS/JavaScript,那时候还手打代码的阶段,把我看得一愣一愣的(IDE都不需要的)。随着学习,发现好像还得学AJAX/jQuery,于是我又去找资源了,不过我已经忘了看哪个老师的AJAXjQuery课程。

在这个学习的过程中,我曾经用纯HTML/CSS/JavaScript跟着视频仿照过某某网站,在jQuery的学习时候做过各种的轮播图动画。还理解了marginpadding的区别。临近毕业的时候,也会点BootStrap来写个简单的页面(丑就完事了)


等我进公司了以后,技术架构前后端是分离的,虽然我拉了前端的代码,但我看不懂,期间我也没学。以至于我两年多是没碰过前端的,我对前端充满着敬畏(刚毕业那段时间,前端在飞速发展

2、AUSTIN前端选型

从我筹划要写austin项目的时候,我就知道我肯定要写一个「后台管理页面」,但我迟迟没下手。一方面是我认为「后端」才是我的赛道,另一方面我「前端」确实菜,不想动手。

我有想过要不找个小伙伴帮我写,但是很快就被我自己否定了:还得给小伙伴提需求,算了


当我要面临前端的时,我第一时间就想到:肯定是有什么框架能够快速搭建出一个管理页面的。我自己不知道,但是,我的朋友圈肯定是有人知道的啊。于是,我果断求助:


我被安利了很多框架,简单列举下出场率比较高的。

:大多数我只是粗略看了下,没有仔细研究。若有错误可以在评论区留言,轻喷

2.1 renren-fast

官网文档:http://www.renren.io/guide#getdo…


它这个框架是前后端分离的,后端还可以生成对应的CRUD代码,前端基于vueelement-ui开发。

当时其实我有点想选它的,但考虑到我要再部署个后端,还得学点vue,我就搁置了

2.2 RuoYi

官方文档:doc.ruoyi.vip/ruoyi/


RuoYi给我安利的也很多,这个貌似最近非常火?感觉我被推荐了以后,到处都能看到它的身影。

我简单刷了下文档,感觉他做的事比renren-fast要多,文档也很齐全,但是没找到我想要的东西:我打开一个文档,我希望能看到它的系统架构,系统之间的交互或者架构层面上的东西,但我没快速找到。

项目齐全和复杂对我来说或许并不是一件好事,很可能意味着我的学习成本可能会更大。于是,我也搁置着。

2.3 Vue相关

vue-element-admin

官方文档:panjiachen.github.io/vue-element…


Vue Antd Admin

官方文档:iczer.gitee.io/vue-antd-ad…


Ant Design Pro

官方文档:pro.antdv.com/docs/gettin…


这几个项目被推荐率也是极高的,从第一行介绍我基本就知道需要去学Vue的语法,奈何我太懒了,搁置着。

2.4 layui

有好几小伙伴们听说我会jQuery,于是给我推荐了layui。我以前印象中好像听过这个框架,但一直没了解过他。但是,当我搜到它的时候,它已经不维护了


GitHub地址:github.com/sentsin/lay…

我简单浏览下文档,其实它也有对应的一套”语法“,需要一定的学习成本,但不高。


第一感觉有点类似我以前写过的BootStrap,我对这不太感冒,感觉如果要接入可能还是需要自己写比较多的代码。

2.5 其他

还有些小伙伴推荐或者我看到的文章推荐:x-admin/D2admin/smartchart/JEECG-BOOT/Dcat-admin/iview-admin等等等,在这里面还有些依赖着PHP/Python

总的来说,我还是觉得这些框架有一定的学习成本(我真的是懒出天际了)。可能需要我去部署后端,也可能需要我学习前端的框架语法,也可能让我学Vue

看到这里,可能你们很好奇我最后选了什么作为austin的前端,都已经被我筛了这么多了。在公布之前,我想说的是:如果想要页面好看灵活性高还是得学习Vue。从上面我被推荐的框架中,好多都是在Vue的基础上改动的,并且我敢肯定:还有很多基于Vue且好用的后台是我不知道的。

:我这里指代跟我一样不懂前端的(如果本身就已经懂前端,你说啥都对)


3、AMIS框架

我最后选择了amis作为austin的前端。这个框架在我朋友圈只有一个小伙伴推荐,我第一次打开文档的时候,确实惊艳到我了


文档地址:baidu.gitee.io/amis/zh-CN/…

它是一个低代码前端框架:amis 的渲染过程是将 json 转成对应的 React 组件

我花了半天粗略地刷了下文档,大概知道了JSON的结构(说实话,他这个文档写得挺可以的),然后我去GitHub找了一份模板,就直接开始动手了,readme十分简短。


GitHub:github.com/aisuda/amis…

这个前端低代码工具还有个好处就是可以通过可视化编辑器拖拉生成JSON代码,将生成好的代码直接往自己本地一贴,就完事了,确实挺方便的。


可视化编辑器的地址:aisuda.github.io/amis-editor…

4、使用感受

其实没什么好讲的,无非就是在页面上拖拉得到一个页面,然后调用API的时候看下文档的姿势。

在这个过程中我也去看了下这个框架的评价,发现百度内部很多系统就用的这个框架来搭建页面的,也看到Bigo也有在线上使用这个框架来搭建后台。有一线/二线公司都在线上使用该框架了,我就认为问题不大了。

总的来说,我这次搭建austin后台实际编码时间没多少,都在改JSON配置和查文档。我周六下午2点到的图书馆,新建了GitHub仓库,在6点闭馆前就已经搭出个大概页面了,然后在周日空闲时间里再完善了几下,感觉可以用了

austin-amis仓库地址:github.com/ZhongFuChen…

在搭建的过程中,amis低代码框架还是有地方可吐槽的,就是它的灵活性太低。我们的接口返回值需要迎合它的主体结构,当我们如果有嵌套JSON这种就变得异常难处理,表单无法用表达式进行回显等等。

它并不完美,很可能需要我用些奇怪的姿势妥协,不要问我接口返回的时候为啥转了一层Map


不管怎么说,这不妨碍我花了极短的时间就能搭出一个能看的后台管理页面(CRUD已齐全)


5、总结

目前搭好的前端能用,也只能用一点点,后面会逐渐完善它的配置和功能的。我后面有链路追踪的功能,肯定要在后台这把清洗后的数据提供给后台进行查询,但也不会花比较长的篇幅再来聊前端这事了。

我一直定位是在后端的代码上,至于前端我能学,但我又不想学。怎么说呢,利益最大化吧。我把学前端的时间花在学后端上,或许可能对我有更大的受益。现在基本前后端分离了,在公司我也没什么机会写前端。

下一篇很有可能是聊分布式定时任务框架上,我发现我的进度可以的,这个季度拿个4.0应该问题不大了。

都看到这里了,点个赞一点都不过分吧?我是3y,下期见。


austin项目源码Gitee链接:gitee.com/austin

austin项目源码GitHub链接:github.com/austin


作者:Java3y
来源:https://juejin.cn/post/7076231399669235725

收起阅读 »

基于JDK的动态代理原理分析

基于JDK的动态代理原理分析 这篇文章解决三个问题: What 动态代理是什么 How 动态代理怎么用 Why 动态代理的原理 动态代理是什么? 动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件...
继续阅读 »

基于JDK的动态代理原理分析


这篇文章解决三个问题:



  1. What 动态代理是什么

  2. How 动态代理怎么用

  3. Why 动态代理的原理


动态代理是什么?


动态代理是代理模式的一种具体实现,是指在程序运行期间,动态的生成目标对象的代理类(直接加载在内存中的字节码文件),实现对目标对象所有方法的增强。通过这种方式,我们可以在不改变(或无法改变)目标对象源码的情况下,对目标对象的方法执行前后进行干预。


动态代理怎么用?


首先,准备好我们需要代理的类和接口,因为JDK的动态代理是基于接口实现的,所以被代理的对象必须要有接口


/**
* SaySomething接口
*/

public interface SaySomething {

   public void sayHello();

   public void sayBye();
}

/**
* SaySomething的实现类
*/

public class SaySomethingImpl implements SaySomething {
   @Override
   public void sayHello() {
       System.out.println("Hello World");
  }

   @Override
   public void sayBye() {
       System.out.println("Bye Bye");
  }
}

按照动态代理的用法,需要自定义一个处理器,用来编写自定义逻辑,实现对被代理对象的增强。


自定义的处理器需要满足以下要求:



  • 需要实现InvocationHandler,重写invoke方法,在invoke方法中通过加入自定义逻辑,实现对目标对象的增强。

  • 需要持有一个成员变量,成员变量的是被代理对象的实例,通过构造参数传入。(用来支持反射调用被代理对象的方法)

  • 需要提供一个参数为被代理对象接口类的有参构造。(用来支持反射调用被代理对象的方法)


/**
* 自定义的处理器,用来编写自定义逻辑,实现对被代理对象的增强
*/

public class CustomHandler implements InvocationHandler {

   //需要有一个成员变量,成员变量为被代理对象,通过构造参数传入,用来支持方法的反射调用。
   private SaySomething obj;
   
   //需要有一个有参构造,通过构造函数将被代理对象的实例传入,用来支持方法的反射调用
   public CustomHandler(SaySomething obj) {
       this.obj = obj;
  }

   /**
    * proxy:动态生成的代理类对象com.sun.proxy.$Proxy0
    * method:被代理对象的真实的方法的Method对象
    * args:调用方法时的入参
    */

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       //目标方法执行前的自定义逻辑处理
       System.out.println("-----before------");

       //执行目标对象的方法,使用反射来执行方法,反射需要传入目标对象,此时用到了成员变量obj。
       Object result = method.invoke(obj, args);

       //目标方法执行后的自定义逻辑处理
       System.out.println("-----after------");
       return result;
  }
}

这样我们就完成了自定义处理器的编写,同时在invoke方法中实现对了代理对象方法的增强,被代理类的所有方法的执行都会执行我们自定义的逻辑。


接下来,需要通过Proxy,newProxyInstance()方法来生成代理对象的实例,并进行方法调用测试。


public class JdkProxyTest {
   public static void main(String[] args) {
       //将生成的代理对象的字节码文件 保存到硬盘
       System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

       //被代理对象的实例
       SaySomething obj = new SaySomethingImpl();
       //通过构造函数,传入被代理对象的实例,生成处理器的实例
       InvocationHandler handler = new CustomHandler(obj);
       //通过Proxy.newProxyInstance方法,传入被代理对象Class对象、处理器实例,生成代理对象实例
       SaySomething proxyInstance = (SaySomething) Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                                                                          new Class[]{SaySomething.class}, handler);
       //调用生成的代理对象的sayHello方法
       proxyInstance.sayHello();
       System.out.println("===================分割线==================");
       //调用生成的代理对象的sayBye方法
       proxyInstance.sayBye();
  }
}

image.png
运行main方法,查看控制台,大功告成。至此,我们已经完整的完成了一次动态代理的使用。


动态代理的原理


生成的proxyInstance对象到底是什么,为什么调用它的sayHello方法会执行CustomerHandler的invoke方法呢?


直接贴上proxyInstance的字节码文件,我们就会恍然大悟了...


//$Proxy0是SaySomething的实现类,重写了sayHello和sayBye方法
public final class $Proxy0 extends Proxy implements SaySomething {
   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m4;
   private static Method m0;

   public $Proxy0(InvocationHandler var1) throws {
       super(var1);
  }

   static {
       try {
           m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
           m3 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayHello");
           m2 = Class.forName("java.lang.Object").getMethod("toString");
           m4 = Class.forName("com.example.demo.hanmc.proxy.jdk.SaySomething").getMethod("sayBye");
           m0 = Class.forName("java.lang.Object").getMethod("hashCode");
      } catch (NoSuchMethodException var2) {
           throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
           throw new NoClassDefFoundError(var3.getMessage());
      }
  }
 
   //实现了接口的sayHello方法,在方法内部调用了CustomerHandler的invoke方法,同时传入了Method对象,
   //所以在CustomerHandler对象中可以通过mathod.invovke方法调用SyaSomthing的sayHello方法
   public final void sayHello() throws {
       try {
           //h是父类Proxy中的InvocationHandler对象,其实就是我们自定义的CustomHandler对象
           super.h.invoke(this, m3, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }

   public final void sayBye() throws {
       try {
           super.h.invoke(this, m4, (Object[])null);
      } catch (RuntimeException | Error var2) {
           throw var2;
      } catch (Throwable var3) {
           throw new UndeclaredThrowableException(var3);
      }
  }
   public final int hashCode() throws {
      //忽略内容
  }
   public final boolean equals(Object var1) throws {
      //忽略内容
  }
   public final String toString() throws {
      //忽略内容
  }
}

看到了生成的代理对象的字节码文件,是不是一切都明白你了,原理竟然如此简单^_^


作者:82年咖啡
来源:https://juejin.cn/post/7079720742899843080
收起阅读 »

画一手好的架构图是码农进阶的开始

1.前言 你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方...
继续阅读 »

1.前言

你是否对大厂展示的五花八门,花花绿绿的架构设计图所深深吸引,当我们想用几张图来介绍下业务系统,是不是对着画布不知从何下手?作为技术扛把子的筒子们是不是需要一张图来描述系统,让系统各个参与方都能看的明白?如果有这样的困惑,本文将介绍一些画图的方法论,让技术图纸更加清晰。

2. 架构的定义

  • 系统架构是概念的体现,是对物/信息的功能与形式元素之间的对应情况所做的分配,是对元素之间的关系以及元素同周边环境之间的关系所做的定义;

  • 架构就是对系统中的实体以及实体之间的关系所进行的抽象描述,是一系列的决策;

  • 架构是结构和愿景.

在TOGAF企业架构理论中, 架构是从公司战略层面,自顶向下的细化的一部分,从战略=> 业务架构=>应用/数据/技术架构,当然老板层关注的是战略与业务架构,我们搬砖的需要聚焦到应用/数据/技术架构这一层。


  • 业务架构: 由业务架构师负责,也可以称为业务领域专家、行业专家,业务架构属于顶层设计,其对业务的定义和划分会影响组织架构和技术架构;

  • 应用架构: 由应用架构师负责,需要根据业务场景需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性的要求如性能、安全、稳定性等。

  • 技术架构: 描述了需要哪些服务;选择哪些技术组件来实现技术服务;技术服务以及组件之间的交互关系;

  • 数据架构: 描述了数据模型、分布、数据的流向、数据的生命周期、数据的管理等关系;

3.架构图的分类

系统架构图是为了抽象的表示软件系统的整体轮廓和各个组件之间的相互关系和约束边界,以及软件系统的物理部署和软件系统的演进方向的整体视图。好的架构图可以让干系人理解、遵循架构决策,就需要把架构信息传递出去。那么,画架构图是为了:解决沟通障碍/达成共识/减少歧义。比较流行的是4+1视图和C4视图。

3.1 4+1视图

3.1.1 场景视图

用于描述系统的参与者与功能用例间的关系,反映系统的最终需求和交互设计,通常由用例图表示;


3.1.2 逻辑视图

用于描述系统软件功能拆解后的组件关系,组件约束和边界,反映系统整体组成与系统如何构建的过程,通常由UML的组件图和类图来表示。


3.1.3 物理视图

用于描述系统软件到物理硬件的映射关系,反映出系统的组件是如何部署到一组可计算机器节点上,用于指导软件系统的部署实施过程。


3.1.4 处理流程视图

用于描述系统软件组件之间的通信时序,数据的输入输出,反映系统的功能流程与数据流程,通常由时序图和流程图表示。


3.1.5 开发视图

开发视图用于描述系统的模块划分和组成,以及细化到内部包的组成设计,服务于开发人员,反映系统开发实施过程。


5 种架构视图从不同角度表示一个软件系统的不同特征,组合到一起作为架构蓝图描述系统架构。

3.2 C4视图

下面的案例来自C4官网,然后加上了一些笔者的理解。


C4 模型使用容器(应用程序、数据存储、微服务等)、组件和代码来描述一个软件系统的静态结构。这几种图比较容易画,也给出了画图要点,但最关键的是,我们认为,它明确指出了每种图可能的受众以及意义。

3.2.1 语境图(System Context Diagram)

用于描述要我们要构建的系统是什么,用户是谁,需要如何融入已有的IT环境。这个图的受众可以是开发团队的内部人员、外部的技术或非技术人员。


3.2.2 容器图(Container Diagram)

容器图是把语境图里待建设的系统做了一个展开描述,主要受众是团队内部或外部的开发人员或运维人员,主要用来描述软件系统的整体形态,体现了高层次的技术决策与选型,系统中的职责是如何分布的,容器间是如何交互的。


3.2.3 组件图(Component Diagram)

组件图是把某个容器进行展开,描述其内部的模块,主要是给内部开发人员看的,怎么去做代码的组织和构建,描述了系统由哪些组件/服务组成,了组件之间的关系和依赖,为软件开发如何分解交付提供了框架。


4.怎么画好架构图

上面的分类是前人的经验总结,图也是从网上摘来的,那么这些图画的好不好呢?是不是我们要依葫芦画瓢去画这样一些图?先不去管这些图好不好,我们通过对这些图的分类以及作用,思考了一下,总结下来,我们认为,明确这两点之后,从受众角度来说,一个好的架构图是不需要解释的,它应该是自描述的,并且要具备一致性和足够的准确性,能够与代码相呼应。

4.1 视图的受众

在画出一个好的架构图之前, 首先应该要明确其受众,再想清楚要给他们传递什么信息 ,所以,不要为了画一个物理视图去画物理视图,为了画一个逻辑视图去画逻辑视图,而应该根据受众的不同,传递的信息的不同,用图准确地表达出来,最后的图可能就是在这样一些分类里。那么,画出的图好不好的一个直接标准就是:受众有没有准确接收到想传递的信息。

4.2 视图的元素区分

可以看到架构视图是由方框和线条等元素构成,要利用形状、颜色、线条变化等区分元素的含义,避免混淆。架构是一项复杂的工作,只使用单个图表来表示架构很容易造成莫名其妙的语义混乱。

让我们一起画出好的架构图!

参考资料


作者:代码的色彩
来源:https://juejin.cn/post/7062662600437268493

收起阅读 »

前端人员不要只知道KFC,你应该了解 BFC、IFC、GFC 和 FFC

前言说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而...
继续阅读 »

前言

说起KFC,大家都知道是肯德基🍟,但面试官问你什么是BFC、IFC、GFC和FFC的时候,你是否能够像回答KFC是肯德基时的迅速,又或者说后面这些你根本就没听说过,作为一名前端开发工程师,以上这些FC(Forrmatting Context)你都得知道,而且必须得做到像肯德基这样印象深刻。下面我将会带大家一起揭开这些FC的真面目,如果你已经了解的请奖励自己一顿肯德基~(注意文明用语,这里别用语气词😂)

FC的全称是:Formatting Contexts,译作格式化上下文,是W3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

CSS2.1中只有BFC和IFC,CSS3中才有GFC和FFC。

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~

前置概念

在学习各种FC之前,我们先来了解几个基本概念:

Box(CSS布局基本单位)

简单来讲,我们看到的所有页面都是由一个个Box组合而成的,元素的类型和display属性决定了Box的类型。

  • block-level Box: 当元素的 CSS 属性 displayblock, list-itemtable 时,它是块级元素 block-level。块级元素(比如<p>)视觉上呈现为块,竖直排列。 每个块级元素至少生成一个块级盒(block-level Box)参与 BFC ,称为主要块级盒(principal block-level box)。一些元素,比如<li>,生成额外的盒来放置项目符号,不过多数元素只生成一个主要块级盒。

  • Inline-level Box: 当元素的 CSS 属性 display 的计算值为inline,inline-blockinline-table 时,称它为行内级元素。视觉上它将内容与其它行内级元素排列为多行。典型的如段落内容,有文本或图片,都是行内级元素。行内级元素生成行内级盒(inline-level boxes),参与行内格式化上下文 IFC 。

  • flex container: 当元素的 CSS 属性 display 的计算值为 flexinline-flex ,称它为弹性容器display:flex这个值会导致一个元素生成一个块级(block-level)弹性容器框。display:inline-flex这个值会导致一个元素生成一个行内级(inline-level)弹性容器框。

  • grid container:*当元素的 CSS 属性 display 的计算值为 gridinline-grid,称它为*栅格容器

块容器盒(block container box)

只包含其它块级盒,或生成一个行内格式化上下文(inline formatting context),只包含行内盒的叫做块容器盒子

也就是说,块容器盒要么只包含行内级盒,要么只包含块级盒。

块级盒(block-level Box)是描述元素跟它的父元素与兄弟元素之间的表现。

块容器盒(block container box)描述元素跟它的后代之间的影响。

块盒(BLock Boxes)

同时是块容器盒的块级盒称为块盒(block boxes)

行盒(Line boxes)

行盒由行内格式化上下文(inline formatting context)产生的盒,用于表示一行。在块盒里面,行盒从块盒一边排版到另一边。 当有浮动时, 行盒从左浮动的最右边排版到右浮动的最左边。

OK,了解完上面这些概念,我们再来看我们本篇文章的重点内容(终于要揭开各种FC的庐山真面目了,期待~)

BFC(Block Formatting Contexts)块级格式化上下文

什么是BFC?

BFC 全称:Block Formatting Context, 名为 块级格式化上下文

W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。

如何触发BFC?

  • 根元素或其它包含它的元素

  • 浮动 float: left/right/inherit

  • 绝对定位元素 position: absolute/fixed

  • 行内块display: inline-block

  • 表格单元格 display: table-cell

  • 表格标题 display: table-caption

  • 溢出元素 overflow: hidden/scroll/auto/inherit

  • 弹性盒子 display: flex/inline-flex

BFC布局规则

  • 内部的Box会在垂直方向,一个接一个地放置。

  • Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。

  • 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。

  • BFC的区域不会与float box重叠。

  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

  • 计算BFC的高度时,浮动元素也参与计算

BFC应用场景

解决块级元素垂直方向margin重叠

我们来看下面这种情况:

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
</style>
<body>
   <div class="box">nanjiu</div>
   <div class="box">南玖</div>
</body>

按我们习惯性思维,上面这个box的margin-bottom60px,下面这个box的margin-top也是60px,那他们垂直的间距按道理来说应该是120px才对。(可事实并非如此,我们可以来具体看一下)

bfc1.png 从图中我们可以看到,两个box的垂直间距只有60px,并不是120px!

这种情况下的margin边距为两者的最大值,而不是两者相加,那么我们可以使用BFC来解决这种margin塌陷的问题。

<style>
 .box{
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px auto;
}
 .outer_box{
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
   </div>
   <div class="box">南玖</div>
</body>

bfc2.png 由上面可以看到,我们通过给第一个box外面再包裹一层容器,并触发它形成BFC,此时的两个box就不属于同一个BFC了,它们的布局互不干扰,所以这时候他们的垂直间距就是两者间距相加了。

解决高度塌陷问题

我们再来看这种情况,内部box使用float脱离了普通文档流,导致外层容器没办法撑起高度,使得背景颜色没有显示出来。

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
 .outer_box{
   background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc3.png 从这张图,我们可以看到此时的外层容器的高度为0,导致背景颜色没有渲染出来,这种情况我们同样可以使用BFC来解决,可以直接为外层容器触发BFC,我们来看看效果:

<style>
 .box{
   float:left;
   width:180px;
   height:180px;
   background:rosybrown;
   color:#fff;
   margin: 60px;
}
.outer_box{
 display:inline-block;
 background:lightblue;
}
</style>
<body>
   <div class="outer_box">
       <div class="box">nanjiu</div>
       <div class="box">南玖</div>
   </div>
</body>

bfc4.png

清除浮动

在早期前端页面大多喜欢用浮动来布局,但浮动元素脱离普通文档流,会覆盖旁边内容:

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc5.png 我们可以通过触发后面这个元素形成BFC,从而来清楚浮动元素对其布局造成的影响

<style>
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
 .container{
   width:500px;
   height:400px;
   background:mediumturquoise;
   overflow: hidden;
}
</style>
<body>
   <div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

bfc6.png

IFC(Inline Formatting Contexts)行内级格式化上下文

什么是IFC?

IFC全称:Inline Formatting Context,名为行级格式化上下文

如何触发IFC?

  • 块级元素中仅包含内联级别元素

形成条件非常简单,需要注意的是当IFC中有块级元素插入时,会产生两个匿名块将父元素分割开来,产生两个IFC。

IFC布局规则

  • 在一个IFC内,子元素是水平方向横向排列的,并且垂直方向起点为元素顶部。

  • 子元素只会计算横向样式空间,【padding、border、margin】,垂直方向样式空间不会被计算,【padding、border、margin】。

  • 在垂直方向上,子元素会以不同形式来对齐(vertical-align)

  • 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和与其中的浮动来决定。

  • IFC中的line box一般左右边贴紧其包含块,但float元素会优先排列。

  • IFC中的line box高度由 CSS 行高计算规则来确定,同个IFC下的多个line box高度可能会不同。

  • inline boxes的总宽度少于包含它们的line box时,其水平渲染规则由 text-align 属性值来决定。

  • 当一个inline box超过父元素的宽度时,它会被分割成多个boxes,这些boxes分布在多个line box中。如果子元素未设置强制换行的情况下,inline box将不可被分割,将会溢出父元素。

IFC应用场景

元素水平居中

当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。

<style>
/* IFC */
 .text_container{
   width: 650px;
   border: 3px solid salmon;
   margin-top:60px;
   text-align: center;
}
 strong,span{
   /* border:1px solid cornflowerblue; */
   margin: 20px;
   background-color: cornflowerblue;
   color:#fff;
}
</style>
<body>
   <div class="text_container">
       <strong>众里寻他千百度,南玖需要你关注</strong>
       <span>蓦然回首,那人却在,南玖前端交流群</span>
   </div>
</body>

ifc1.png

多行文本水平垂直居中

创建一个IFC,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。

<style>
.text_container{
 text-align: center;
 line-height: 300px;
 width: 100%;
 height: 300px;
 background-color: turquoise;
 font-size: 0;
}
 p{
   line-height: normal;
   display: inline-block;
   vertical-align: middle;
   background-color: coral;
   font-size: 18px;
   padding: 10px;
   width: 360px;
   color: #fff;
}
</style>
<body>
 <div class="text_container">
   <p>
    东风夜放花千树,更吹落,星如雨。宝马雕车香满路。凤箫声动,玉壶光转,一夜鱼龙舞。蛾儿雪柳黄金缕,笑语盈盈暗香去。
     <strong>众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。</strong>
   </p>
 </div>
</body>

ifc2.png

GFC(Grid Formatting Contexts)栅格格式化上下文

什么是GFC?

GFC全称:Grids Formatting Contexts,名为网格格式上下文

简介: CSS3引入的一种新的布局模型——Grids网格布局,目前暂未推广使用,使用频率较低,简单了解即可。 Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置。但是,它们也存在重大区别。 Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

如何触发GFC?

当为一个元素设置display值为grid或者inline-grid的时候,此元素将会获得一个独立的渲染区域。

GFC布局规则

通过在网格容器(grid container)上定义网格定义行(grid definition rows)网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间(具体可以在MDN上查看)

GFC应用场景

任意魔方布局

这个布局使用用GFC可以轻松实现自由拼接效果,换成其他方法,一般会使用相对/绝对定位,或者flex来实现自由拼接效果,复杂程度将会提升好几个等级。

<style>
.magic{
display: grid;
grid-gap: 2px;
width:300px;
height:300px;
}
.magic div{
border: 1px solid coral;
}
.m_1{
grid-column-start: 1;
grid-column-end: 3;
}
.m_3{
grid-column-start: 2;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: 3;
}
</style>
<body>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
</div>
</body>

gfc1.png

FFC(Flex Formatting Contexts)弹性格式化上下文

什么是FFC?

FFC全称:Flex Formatting Contexts,名为弹性格式上下文

简介: CSS3引入了一种新的布局模型——flex布局。 flex是flexible box的缩写,一般称之为弹性盒模型。和CSS3其他属性不一样,flexbox并不是一个属性,而是一个模块,包括多个CSS3属性。flex布局提供一种更加有效的方式来进行容器内的项目布局,以适应各种类型的显示设备和各种尺寸的屏幕,使用Flex box布局实际上就是声明创建了FFC(自适应格式上下文)

如何触发FFC?

display 的值为 flexinline-flex 时,将生成弹性容器(Flex Containers), 一个弹性容器为其内容建立了一个新的弹性格式化上下文环境(FFC)

FFC布局规则

  • 设置为 flex 的容器被渲染为一个块级元素

  • 设置为 inline-flex 的容器被渲染为一个行内元素

  • 弹性容器中的每一个子元素都是一个弹性项目。弹性项目可以是任意数量的。弹性容器外和弹性项目内的一切元素都不受影响。简单地说,Flexbox 定义了弹性容器内弹性项目该如何布局

⚠️注意:FFC布局中,float、clear、vertical-align属性不会生效。

Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。

FFC应用场景

这里只介绍它对于其它布局所相对来说更方便的特点,其实flex布局现在是非常普遍的,很多前端人员都喜欢用flex来写页面布局,操作方便且灵活,兼容性好。

自动撑开剩余高度/宽度

看一个经典两栏布局:左边为侧边导航栏,右边为内容区域,用我们之前的常规布局,可能就需要使用到csscalc方法来动态计算剩余填充宽度了,但如果使用flex布局的话,只需要一个属性就能解决这个问题:

calc动态计算方法:

<style>
.outer_box {
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 width:calc(100% - 180px);
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc.gif 使用FFC:

<style>
.outer_box {
 display:flex;
width:100%;
}
.aside {
 float: left;
 width:180px;
 height: 300px;
 background:lightpink;
}
.container{
 flex: 1;
 height:400px;
 background:mediumturquoise;
 overflow: hidden;
}
</style>
<body>
<div class="outer_box">
       <div class="aside">nanjiu</div>
       <div class="container">南玖</div>
   </div>
</body>

ffc2.gif

总结

一般来说,FFC能做的事情,通过GFC都能搞定,反过来GFC能做的事通过FFC也能实现。 通常弹性布局使用FFC,二维网格布局使用GFC,所有的FFC与GFC也是一个BFC,在遵循自己的规范的情况下,向下兼容BFC规范。

现在所有的FC都介绍完了,了解清楚的去奖励自己一顿KFC吧😄~

推荐阅读

作者:南玖
来源:https://juejin.cn/post/7072174649735381029

收起阅读 »

MySQL模糊查询再也用不着 like+% 了

前言 我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不...
继续阅读 »

前言


我们都知道 InnoDB 在模糊查询数据时使用 "%xx" 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不是B+树索引能很好完成的工作。


通过数值比较,范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较,全文索引就是为这种场景设计的。


全文索引(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。


在早期的 MySQL 中,InnoDB 并不支持全文检索技术,从 MySQL 5.6 开始,InnoDB 开始支持全文检索。


倒排索引


全文检索通常使用倒排索引(inverted index)来实现,倒排索引同 B+Tree 一样,也是一种索引结构。它在辅助表中存储了单词与单词自身在一个或多个文档中所在位置之间的映射,这通常利用关联数组实现,拥有两种表现形式:



  • inverted file index:{单词,单词所在文档的id}

  • full inverted index:{单词,(单词所在文档的id,再具体文档中的位置)}


MarkerHub


上图为 inverted file index 关联数组,可以看到其中单词"code"存在于文档1,4中,这样存储再进行全文查询就简单了,可以直接根据 Documents 得到包含查询关键字的文档;而 full inverted index 存储的是对,即(DocumentId,Position),因此其存储的倒排索引如下图,如关键字"code"存在于文档1的第6个单词和文档4的第8个单词。



相比之下,full inverted index 占用了更多的空间,但是能更好的定位数据,并扩充一些其他搜索特性。



MarkerHub


全文检索


创建全文索引


1、创建表时创建全文索引语法如下:

CREATE TABLE table_name ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, author VARCHAR(200), 
title VARCHAR(200), content TEXT(500), FULLTEXT full_index_name (col_name) ) ENGINE=InnoDB;
复制代码

输入查询语句:


SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_TABLES
WHERE name LIKE 'test/%';
复制代码

MarkerHub


上述六个索引表构成倒排索引,称为辅助索引表。当传入的文档被标记化时,单个词与位置信息和关联的DOC_ID,根据单词的第一个字符的字符集排序权重,在六个索引表中对单词进行完全排序和分区。


2、在已创建的表上创建全文索引语法如下:

CREATE FULLTEXT INDEX full_index_name ON table_name(col_name);
复制代码

使用全文索引


MySQL 数据库支持全文检索的查询,全文索引只能在 InnoDB 或 MyISAM 的表上使用,并且只能用于创建 char,varchar,text 类型的列。


其语法如下:


MATCH(col1,col2,...) AGAINST(expr[search_modifier])
search_modifier:
{
    IN NATURAL LANGUAGE MODE
    | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
    | IN BOOLEAN MODE
    | WITH QUERY EXPANSION
}
复制代码

全文搜索使用 MATCH() AGAINST()语法进行,其中,MATCH()采用逗号分隔的列表,命名要搜索的列。AGAINST()接收一个要搜索的字符串,以及一个要执行的搜索类型的可选修饰符。全文检索分为三种类型:自然语言搜索、布尔搜索、查询扩展搜索,下面将对各种查询模式进行介绍。


Natural Language


自然语言搜索将搜索字符串解释为自然人类语言中的短语,MATCH()默认采用 Natural Language 模式,其表示查询带有指定关键字的文档。


接下来结合demo来更好的理解Natural Language


SELECT
    count(*) AS count 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL' );
复制代码

MarkerHub


上述语句,查询 title,body 列中包含 'MySQL' 关键字的行数量。上述语句还可以这样写:


SELECT
    count(IF(MATCH ( title, body ) 
    against ( 'MySQL' ), 1, NULL )) AS count 
FROM
    `fts_articles`;
复制代码

上述两种语句虽然得到的结果是一样的,但从内部运行来看,第二句SQL的执行速度更快些,因为第一句SQL(基于where索引查询的方式)还需要进行相关性的排序统计,而第二种方式是不需要的。


还可以通过SQL语句查询相关性:


SELECT
    *,
    MATCH ( title, body ) against ( 'MySQL' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


相关性的计算依据以下四个条件:



  • word 是否在文档中出现

  • word 在文档中出现的次数

  • word 在索引列中的数量

  • 多少个文档包含该 word


对于 InnoDB 存储引擎的全文检索,还需要考虑以下的因素:



  • 查询的 word 在 stopword 列中,忽略该字符串的查询

  • 查询的 word 的字符长度是否在区间 [ innodb_ft_min_token_size,innodb_ft_max_token_size] 内


如果词在 stopword 中,则不对该词进行查询,如对 'for' 这个词进行查询,结果如下所示:


SELECT
    *,
    MATCH ( title, body ) against ( 'for' ) AS Relevance 
FROM
    fts_articles;
复制代码

MarkerHub


可以看到,'for'虽然在文档 2,4中出现,但由于其是 stopword ,故其相关性为0


参数 innodb_ft_min_token_sizeinnodb_ft_max_token_size 控制 InnoDB 引擎查询字符的长度,当长度小于 innodb_ft_min_token_size 或者长度大于 innodb_ft_max_token_size 时,会忽略该词的搜索。在 InnoDB 引擎中,参数 innodb_ft_min_token_size 的默认值是3,innodb_ft_max_token_size的默认值是84


Boolean


布尔搜索使用特殊查询语言的规则来解释搜索字符串,该字符串包含要搜索的词,它还可以包含指定要求的运算符,例如匹配行中必须存在或不存在某个词,或者它的权重应高于或低于通常情况。


例如,下面的语句要求查询有字符串"Pease"但没有"hot"的文档,其中+和-分别表示单词必须存在,或者一定不存在。


select * from fts_test where MATCH(content) AGAINST('+Pease -hot' IN BOOLEAN MODE);
复制代码

Boolean 全文检索支持的类型包括:



  • +:表示该 word 必须存在

  • -:表示该 word 必须不存在

  • (no operator)表示该 word 是可选的,但是如果出现,其相关性会更高

  • @distance表示查询的多个单词之间的距离是否在 distance 之内,distance 的单位是字节,这种全文检索的查询也称为 Proximity Search,如 MATCH(context) AGAINST('"Pease hot"@30' IN BOOLEAN MODE)语句表示字符串 Pease 和 hot 之间的距离需在30字节内

  • >:表示出现该单词时增加相关性

  • <:表示出现该单词时降低相关性

  • ~:表示允许出现该单词,但出现时相关性为负

  • * :表示以该单词开头的单词,如 lik*,表示可以是 lik,like,likes

  • " :表示短语


下面是一些demo,看看 Boolean Mode 是如何使用的。


demo1:+ -


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL -YourSQL' IN BOOLEAN MODE );
复制代码

上述语句,查询的是包含 'MySQL' 但不包含 'YourSQL' 的信息


MarkerHub


demo2: no operator


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL IBM' IN BOOLEAN MODE );
复制代码

上述语句,查询的 'MySQL IBM' 没有 '+','-'的标识,代表 word 是可选的,如果出现,其相关性会更高


MarkerHub


demo3:@


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"DB2 IBM"@3' IN BOOLEAN MODE );
复制代码

上述语句,代表 "DB2" ,"IBM"两个词之间的距离在3字节之内


MarkerHub


demo4:> <


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '+MySQL +(>database <DBMS)' IN BOOLEAN MODE );
复制代码

上述语句,查询同时包含 'MySQL','database','DBMS' 的行信息,但不包含'DBMS'的行的相关性高于包含'DBMS'的行。


MarkerHub


demo5: ~


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'MySQL ~database' IN BOOLEAN MODE );
复制代码

上述语句,查询包含 'MySQL' 的行,但如果该行同时包含 'database',则降低相关性。


MarkerHub


demo6:*


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( 'My*' IN BOOLEAN MODE );
复制代码

上述语句,查询关键字中包含'My'的行信息。


MarkerHub


demo7:"


SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH ( title, body ) AGAINST ( '"MySQL Security"' IN BOOLEAN MODE );
复制代码

上述语句,查询包含确切短语 'MySQL Security' 的行信息。


MarkerHub


Query Expansion


查询扩展搜索是对自然语言搜索的修改,这种查询通常在查询的关键词太短,用户需要 implied knowledge(隐含知识)时进行,例如,对于单词 database 的查询,用户可能希望查询的不仅仅是包含 database 的文档,可能还指那些包含 MySQL、Oracle、RDBMS 的单词,而这时可以使用 Query Expansion 模式来开启全文检索的 implied knowledge通过在查询语句中添加 WITH QUERY EXPANSION / IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 可以开启 blind query expansion(又称为 automatic relevance feedback),该查询分为两个阶段。



  • 第一阶段:根据搜索的单词进行全文索引查询

  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询


接着来看一个例子,看看 Query Expansion 是如何使用的。


-- 创建索引
create FULLTEXT INDEX title_body_index on fts_articles(title,body);
复制代码

-- 使用 Natural Language 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database');
复制代码

使用 Query Expansion 前查询结果如下:


MarkerHub


-- 当使用 Query Expansion 模式查询
SELECT
    * 
FROM
    `fts_articles` 
WHERE
    MATCH(title,body) AGAINST('database' WITH QUERY expansion);
复制代码

使用 Query Expansion 后查询结果如下:


MarkerHub


由于 Query Expansion 的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。


删除全文索引


1、直接删除全文索引语法如下:

DROP INDEX full_idx_name ON db_name.table_name;
复制代码

2、使用 alter table 删除全文索引语法如下:

ALTER TABLE db_name.table_name DROP INDEX full_idx_name;
复制代码


来源:juejin.cn/post/6989871497040887845





推荐3个原创springboot+Vue项目,有完整视频讲解与文档和源码:


【dailyhub】【实战】带你从0搭建一个Springboot+elasticsearch+canal的完整项目



【VueAdmin】手把手教你开发SpringBoot+Jwt+Vue的前后端分离后台管理系统



【VueBlog】基于SpringBoot+Vue开发的前后端分离博客项目完整教学



作者:MarkerHub
来源:https://juejin.cn/post/7072257652784365599
收起阅读 »

最完整的Explain总结,SQL优化不再困难!

两个变种会在 explain 的基础上额外提供一些查询优化的信息。一般是使用了覆盖索引(索引包含了所有查询的字段)。对于innodb来说,如果是辅助索引性能会有不少提高mysql> explain select film_id from film_act...
继续阅读 »

在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询时,会返回执行计划的信息,而不是执行这条SQL(如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中)

CREATE TABLE `film` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

两个变种

explain extended

会在 explain 的基础上额外提供一些查询优化的信息。

紧随其后通过 show warnings 命令可以 得到优化后的查询语句,从而看出优化器优化了什么。

额外还有 filtered 列,是一个半分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)

mysql> explain extended select * from film where id = 1;


mysql> show warnings;


explain partitions

相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。

id列

id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。


一般是使用了覆盖索引(索引包含了所有查询的字段)。对于innodb来说,如果是辅助索引性能会有不少提高

mysql> explain select film_id from film_actor where film_id = 1;

Using where

查询的列未被索引覆盖,where筛选条件非索引的前导列

mysql> explain select * from actor where name = 'a';

Using where Using index

查询的列被索引覆盖,并且where筛选条件是索引列之一但是不是索引的前导列,意味着无法直接通过索引查找来查询到符合条件的数据

mysql> explain select film_id from film_actor where actor_id = 1;

NULL

查询的列未被索引覆盖,并且where筛选条件是索引的前导列,意味着用到了索引,但是部分字段未被索引覆盖,必须通过“回表”来实现,不是纯粹地用到了索引,也不是完全没用到索引

mysql>explain select * from film_actor where film_id = 1;

Using index condition

与Using where类似,查询的列不完全被索引覆盖,where条件中是一个前导列的范围;

mysql> explain select * from film_actor where film_id > 1;

Using temporary

mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。

  1. actor.name没有索引,此时创建了张临时表来distinct

mysql> explain select distinct name from actor;
  1. film.name建立了idx_name索引,此时查询时extra是using index,没有用临时表

mysql> explain select distinct name from film;

Using filesort

mysql 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。

此时mysql会根据联接类型浏览所有符合条件的记录,并保存排序关键字和行指针,然后排序关键字并按顺序检索行信息。

这种情况下一般也是要考虑使用索引来优化的。

  1. actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录。

mysql> explain select * from actor order by name;
  1. film.name建立了idx_name索引,此时查询时extra是using index

mysql> explain select * from film order by name;

作者:程序员段飞
来源:https://juejin.cn/post/7074030240904773645

收起阅读 »

优秀的后端应该有哪些开发习惯?

前言毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。拆分合理的目录结构受传统的 MVC 模式影响,传统做法大多是...
继续阅读 »

前言

毕业快三年了,前后也待过几家公司,碰到各种各样的同事。见识过各种各样的代码,优秀的、垃圾的、不堪入目的、看了想跑路的等等,所以这篇文章记录一下一个优秀的后端 Java 开发应该有哪些好的开发习惯。

拆分合理的目录结构

受传统的 MVC 模式影响,传统做法大多是几个固定的文件夹 controller、service、mapper、entity,然后无限制添加,到最后你就会发现一个 service 文件夹下面有几十上百个 Service 类,根本没法分清业务模块。正确的做法是在写 service 上层新建一个 modules 文件夹,在 moudles 文件夹下根据不同业务建立不同的包,在这些包下面写具体的 service、controller、entity、enums 包或者继续拆分。



等以后开发版本迭代,如果某个包可以继续拆领域就继续往下拆,可以很清楚的一览项目业务模块。后续拆微服务也简单。

封装方法形参

当你的方法形参过多时请封装一个对象出来...... 下面是一个反面教材,谁特么教你这样写代码的!

public void updateCustomerDeviceAndInstallInfo(long customerId, String channelKey,
                  String androidId, String imei, String gaId,
                  String gcmPushToken, String instanceId) {}

写个对象出来

public class CustomerDeviceRequest {
  private Long customerId;
  //省略属性......
}

为什么要这么写?比如你这方法是用来查询的,万一以后加个查询条件是不是要修改方法?每次加每次都要改方法参数列表。封装个对象,以后无论加多少查询条件都只需要在对象里面加字段就行。而且关键是看起来代码也很舒服啊!

封装业务逻辑

如果你看过“屎山”你就会有深刻的感触,这特么一个方法能写几千行代码,还无任何规则可言......往往负责的人会说,这个业务太复杂,没有办法改善,实际上这都是懒的借口。不管业务再复杂,我们都能够用合理的设计、封装去提升代码可读性。下面贴两段高级开发(假装自己是高级开发)写的代码

@Transactional
public ChildOrder submit(Long orderId, OrderSubmitRequest.Shop shop) {
  ChildOrder childOrder = this.generateOrder(shop);
  childOrder.setOrderId(orderId);
  //订单来源 APP/微信小程序
  childOrder.setSource(userService.getOrderSource());
  // 校验优惠券
  orderAdjustmentService.validate(shop.getOrderAdjustments());
  // 订单商品
  orderProductService.add(childOrder, shop);
  // 订单附件
  orderAnnexService.add(childOrder.getId(), shop.getOrderAnnexes());
  // 处理订单地址信息
  processAddress(childOrder, shop);
  // 最后插入订单
  childOrderMapper.insert(childOrder);
  this.updateSkuInventory(shop, childOrder);
  // 发送订单创建事件
  applicationEventPublisher.publishEvent(new ChildOrderCreatedEvent(this, shop, childOrder));
  return childOrder;
}
@Transactional
public void clearBills(Long customerId) {
  // 获取清算需要的账单、deposit等信息
  ClearContext context = getClearContext(customerId);
  // 校验金额合法
  checkAmount(context);
  // 判断是否可用优惠券,返回可抵扣金额
  CouponDeductibleResponse deductibleResponse = couponDeducted(context);
  // 清算所有账单
  DepositClearResponse response = clearBills(context);
  // 更新 l_pay_deposit
  lPayDepositService.clear(context.getDeposit(), response);
  // 发送还款对账消息
  repaymentService.sendVerifyBillMessage(customerId, context.getDeposit(), EventName.DEPOSIT_SUCCEED_FLOW_REMINDER);
  // 更新账户余额
  accountService.clear(context, response);
  // 处理清算的优惠券,被用掉或者解绑
  couponService.clear(deductibleResponse);
  // 保存券抵扣记录
  clearCouponDeductService.add(context, deductibleResponse);
}

这段两代码里面其实业务很复杂,内部估计保守干了五万件事情,但是不同水平的人写出来就完全不同,不得不赞一下这个注释,这个业务的拆分和方法的封装。一个大业务里面有多个小业务,不同的业务调用不同的 service 方法即可,后续接手的人即使没有流程图等相关文档也能快速理解这里的业务,而很多初级开发写出来的业务方法就是上一行代码是 A 业务的,下一行代码是 B业务的,在下面一行代码又是 A 业务的,业务调用之间还嵌套这一堆单元逻辑,显得非常混乱,代码还多。

判断集合类型不为空的正确方式

很多人喜欢写这样的代码去判断集合

if (list == null || list.size() == 0) {
return null;
}

当然你硬要这么写也没什么问题......但是不觉得难受么,现在框架中随便一个 jar 包都有集合工具类,比如 org.springframework.util.CollectionUtilscom.baomidou.mybatisplus.core.toolkit.CollectionUtils 。 以后请这么写

if (CollectionUtils.isEmpty(list) || CollectionUtils.isNotEmpty(list)) {
return null;
}

集合类型返回值不要 return null

当你的业务方法返回值是集合类型时,请不要返回 null,正确的操作是返回一个空集合。你看 mybatis 的列表查询,如果没查询到元素返回的就是一个空集合,而不是 null。否则调用方得去做 NULL 判断,多数场景下对于对象也是如此。

映射数据库的属性尽量不要用基本类型

我们都知道 int/long 等基本数据类型作为成员变量默认值是 0。现在流行使用 mybatisplus 、mybatis 等 ORM 框架,在进行插入或者更新的时候很容易会带着默认值插入更新到数据库。我特么真想砍了之前的开发,重构的项目里面实体类里面全都是基本数据类型。当场裂开......

封装判断条件

public void method(LoanAppEntity loanAppEntity, long operatorId) {
if (LoanAppEntity.LoanAppStatus.OVERDUE != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != loanAppEntity.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != loanAppEntity.getStatus()) {
  //...
  return;
}

这段代码的可读性很差,这 if 里面谁知道干啥的?我们用面向对象的思想去给 loanApp 这个对象里面封装个方法不就行了么?

public void method(LoanAppEntity loan, long operatorId) {
if (!loan.finished()) {
  //...
  return;
}

LoanApp 这个类中封装一个方法,简单来说就是这个逻辑判断细节不该出现在业务方法中。

/**
* 贷款单是否完成
*/
public boolean finished() {
return LoanAppEntity.LoanAppStatus.OVERDUE != this.getStatus()
        && LoanAppEntity.LoanAppStatus.CURRENT != this.getStatus()
        && LoanAppEntity.LoanAppStatus.GRACE_PERIOD != this.getStatus();
}

控制方法复杂度

推荐一款 IDEA 插件 CodeMetrics ,它能显示出方法的复杂度,它是对方法中的表达式进行计算,布尔表达式,if/else 分支,循环等。


点击可以查看哪些代码增加了方法的复杂度,可以适当进行参考,毕竟我们通常写的是业务代码,在保证正常工作的前提下最重要的是要让别人能够快速看懂。当你的方法复杂度超过 10 就要考虑是否可以优化了。

使用 @ConfigurationProperties 代替 @Value

之前居然还看到有文章推荐使用 @Value 比 @ConfigurationProperties 好用的,吐了,别误人子弟。列举一下 @ConfigurationProperties 的好处。

  • 在项目 application.yml 配置文件中按住 ctrl + 鼠标左键点击配置属性可以快速导航到配置类。写配置时也能自动补全、联想到注释。需要额外引入一个依赖 org.springframework.boot:spring-boot-configuration-processor


  • @ConfigurationProperties 支持 NACOS 配置自动刷新,使用 @Value 需要在 BEAN 上面使用 @RefreshScope 注解才能实现自动刷新

  • @ConfigurationProperties 可以结合 Validation 校验,@NotNull、@Length 等注解,如果配置校验没通过程序将启动不起来,及早的发现生产丢失配置等问题。

  • @ConfigurationProperties 可以注入多个属性,@Value 只能一个一个写

  • @ConfigurationProperties 可以支持复杂类型,无论嵌套多少层,都可以正确映射成对象

相比之下我不明白为什么那么多人不愿意接受新的东西,裂开......你可以看下所有的 springboot-starter 里面用的都是 @ConfigurationProperties 来接配置属性。

推荐使用 lombok

当然这是一个有争议的问题,我的习惯是使用它省去 getter、setter、toString 等等。

不要在 AService 调用 BMapper

我们一定要遵循从 AService -> BService -> BMapper,如果每个 Service 都能直接调用其他的 Mapper,那特么还要其他 Service 干嘛?老项目还有从 controller 调用 mapper 的,把控制器当 service 来处理了。。。

尽量少写工具类

为什么说要少写工具类,因为你写的大部分工具类,在你无形中引入的 jar 包里面就有,String 的,Assert 断言的,IO 上传文件,拷贝流的,Bigdecimal 的等等。自己写容易错还要加载多余的类。

不要包裹 OpenFeign 接口返回值

搞不懂为什么那么多人喜欢把接口的返回值用 Response 包装起来......加个 code、message、success 字段,然后每次调用方就变成这样

CouponCommonResult bindResult = couponApi.useCoupon(request.getCustomerId(), order.getLoanId(), coupon.getCode());
if (Objects.isNull(bindResult) || !bindResult.getResult()) {
throw new AppException(CouponErrorCode.ERR_REC_COUPON_USED_FAILED);
}

这样就相当于

  1. 在 coupon-api 抛出异常

  2. 在 coupon-api 拦截异常,修改 Response.code

  3. 在调用方判断 response.code 如果是 FAIELD 再把异常抛出去......

你直接在服务提供方抛异常不就行了么。。。而且这样一包装 HTTP 请求永远都是 200,没法做重试和监控。当然这个问题涉及到接口响应体该如何设计,目前网上大多是三种流派

  • 接口响应状态一律 200

  • 接口响应状态遵从HTTP真实状态

  • 佛系开发,领导怎么说就怎么做

不接受反驳,我推荐使用 HTTP 标准状态。特定场景包括参数校验失败等一律使用 400 给前端弹 toast。下篇文章会阐述一律 200 的坏处。

写有意义的方法注释

这种注释你写出来是怕后面接手的人瞎么......

/**
* 请求电话验证
*
* @param credentialNum
* @param callback
* @param param
* @return PhoneVerifyResult
*/

要么就别写,要么就在后面加上描述......写这样的注释被 IDEA 报一堆警告看着蛋疼

和前端交互的 DTO 对象命名

什么 VO、BO、DTO、PO 我倒真是觉得没有那么大必要分那么详细,至少我们在和前端交互的时候类名要起的合适,不要直接用映射数据库的类返回给前端,这会返回很多不必要的信息,如果有敏感信息还要特殊处理。

推荐的做法是接受前端请求的类定义为 XxxRequest,响应的定义为 XxxResponse。以订单为例:接受保存更新订单信息的实体类可以定义为 OrderRequest,订单查询响应定义为 OrderResponse,订单的查询条件请求定义为 OrderQueryRequest

尽量别让 IDEA 报警

我是很反感看到 IDEA 代码窗口一串警告的,非常难受。因为有警告就代表代码还可以优化,或者说存在问题。 前几天捕捉了一个团队内部的小bug,其实本来和我没有关系,但是同事都在一头雾水的看外面的业务判断为什么走的分支不对,我一眼就扫到了问题。

因为 java 中整数字面量都是 int 类型,到集合中就变成了 Integer,然后 stepId 点上去一看是 long 类型,在集合中就是 Long,那这个 contains 妥妥的返回 false,都不是一个类型。

你看如果注重到警告,鼠标移过去看一眼提示就清楚了,少了一个生产 bug。

尽可能使用新技术组件

我觉得这是一个程序员应该具备的素养......反正我是喜欢用新的技术组件,因为新的技术组件出现必定是解决旧技术组件的不足,而且作为一个技术人员我们应该要与时俱进~~ 当然前提是要做好准备工作,不能无脑升级。举个最简单的例子,Java 17 都出来了,新项目现在还有人用 Date 来处理日期时间......

结语

本篇文章简单介绍我日常开发的习惯,当然仅是作者自己的见解。暂时只想到这几点,以后发现其他的会更新。

作者:暮色妖娆丶
来源:https://juejin.cn/post/7072252275002966030

收起阅读 »

看看别人后端API接口写得,那叫一个优雅!

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。一般系统...
继续阅读 »

在分布式、微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式。题外话:前后端的工作职责越来越明确,现在的前端都称之为大前端,技术栈以及生态圈都已经非常成熟;以前后端人员瞧不起前端人员,那现在后端人员要重新认识一下前端,前端已经很成体系了。

一般系统的大致整体架构图如下:

需要说明的是,有些小伙伴会回复说,这个架构太简单了吧,太low了,什么网关啊,缓存啊,消息中间件啊,都没有。因为老顾这篇主要介绍的是API接口,所以我们聚焦点,其他的模块小伙伴们自行去补充。

接口交互

前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。

针对URL路径的restful风格,以及传入参数的公共请求头的要求(如:app_version,api_version,device等),老顾这里就不介绍了,小伙伴们可以自行去了解,也比较简单。

着重介绍一下后端服务器如何实现把数据返回给前端?

返回格式

后端返回给前端我们一般用JSON体方式,定义如下:

{
  #返回状态码
  code:integer,      
  #返回信息描述
  message:string,
  #返回值
  data:object
}

CODE状态码

code返回状态码,一般小伙伴们是在开发的时候需要什么,就添加什么。
如接口要返回用户权限异常,我们加一个状态码为101吧,下一次又要加一个数据参数异常,就加一个102的状态码。这样虽然能够照常满足业务,但状态码太凌乱了

我们应该可以参考HTTP请求返回的状态码,下面是常见的HTTP状态码:

200 - 请求成功
301 - 资源(网页等)被永久转移到其它URL
404 - 请求的资源(网页等)不存在
500 - 内部服务器错误


我们可以参考这样的设计,这样的好处就把错误类型归类到某个区间内,如果区间不够,可以设计成4位数。

#1000~1999 区间表示参数错误
#2000~2999 区间表示用户错误
#3000~3999 区间表示接口异常

这样前端开发人员在得到返回值后,根据状态码就可以知道,大概什么错误,再根据message相关的信息描述,可以快速定位。

Message

这个字段相对理解比较简单,就是发生错误时,如何友好的进行提示。一般的设计是和code状态码一起设计,如

再在枚举中定义,状态码

状态码和信息就会一一对应,比较好维护。

Data

返回数据体,JSON格式,根据不同的业务又不同的JSON体。
我们要设计一个返回体类Result

控制层Controller

我们会在controller层处理业务请求,并返回给前端,以order订单为例

我们看到在获得order对象之后,我们是用的Result构造方法进行包装赋值,然后进行返回。小伙伴们有没有发现,构造方法这样的包装是不是很麻烦,我们可以优化一下。

美观优化

我们可以在Result类中,加入静态方法,一看就懂

那我们来改造一下Controller

代码是不是比较简洁了,也美观了。

优雅优化

上面我们看到在Result类中增加了静态方法,使得业务处理代码简洁了。但小伙伴们有没有发现这样有几个问题:

1、每个方法的返回都是Result封装对象,没有业务含义

2、在业务代码中,成功的时候我们调用Result.success,异常错误调用Result.failure。是不是很多余

3、上面的代码,判断id是否为null,其实我们可以使用hibernate validate做校验,没有必要在方法体中做判断。

我们最好的方式直接返回真实业务对象,最好不要改变之前的业务方式,如下图

这个和我们平时的代码是一样的,非常直观,直接返回order对象,这样是不是很完美。那实现方案是什么呢?

实现方案

小伙伴们怎么去实现是不是有点思路,在这个过程中,我们需要做几个事情

1、定义一个注解@ResponseResult,表示这个接口返回的值需要包装一下

2、拦截请求,判断此请求是否需要被@ResponseResult注解

3、核心步骤就是实现接口ResponseBodyAdvice和@ControllerAdvice,判断是否需要包装返回值,如果需要,就把Controller接口的返回值进行重写。

注解类

用来标记方法的返回值,是否需要包装

拦截器

拦截请求,是否此请求返回的值需要包装,其实就是运行的时候,解析@ResponseResult注解

此代码核心思想,就是获取此请求,是否需要返回值包装,设置一个属性标记。

重写返回体

上面代码就是判断是否需要返回值包装,如果需要就直接包装。这里我们只处理了正常成功的包装,如果方法体报异常怎么办?处理异常也比较简单,只要判断body是否为异常类。

怎么做全局的异常处理,篇幅原因,老顾这里就不做介绍了,只要思路理清楚了,自行改造就行。

重写Controller

在控制器类上或者方法体上加上@ResponseResult注解,这样就ok了,简单吧。到此返回的设计思路完成,是不是又简洁,又优雅。

总结

这个方案还有没有别的优化空间,当然是有的。如:每次请求都要反射一下,获取请求的方法是否需要包装,其实可以做个缓存,不需要每次都需要解析。

作者:码农出击HK
来源:https://juejin.cn/post/7068884412674342926

收起阅读 »

当我们谈部署时,我们在谈什么?

计算机网络把各地的计算机连接了起来,只要有一台可以上网的终端,比如手机、电脑,就可以访问互联网上任何一台服务器的资源(包括静态资源和动态的服务)。作为开发者的我们,就是这些资源、服务的提供者,把资源上传到服务器,并把服务跑起来的过程就叫做部署。代码部分的部署,...
继续阅读 »


计算机网络把各地的计算机连接了起来,只要有一台可以上网的终端,比如手机、电脑,就可以访问互联网上任何一台服务器的资源(包括静态资源和动态的服务)。

作为开发者的我们,就是这些资源、服务的提供者,把资源上传到服务器,并把服务跑起来的过程就叫做部署


代码部分的部署,需要先经过构建,也就是编译打包的过程,把产物传到服务器。

最原始的部署方式就是在本地进行 build,然后把产物通过 FTP 或者 scp(基于 SSH 的远程拷贝文件拷贝) 传到服务器上,如果是后端代码还需要重启下服务。


每个人单独构建上传,这样不好管理,也容易冲突,所以现在都会用专门的平台来做这件事构建和部署,比如 jenkins。

我们代码会提交到 gitlab 等代码库,然后 jenkins 从这些代码库里把代码下载下来进行 build,再把产物上传到服务器上。

流程和直接在本地构建上传差不多,只不过这样方便管理冲突、历史等,还可以跨项目复用一些东西。


构建、部署的过程最开始是通过 shell 来写,但写那个的要求还是很高的。后来就支持了可视化的编排,可以被编排的这个构建、部署的流程叫做流水线 pipeline。


比如这是 jenkins 的 pipeline 的界面:


除了构建、部署外,也可以加入一些自动化测试、静态代码检查等任务。

这种自动化了的构建、部署流程就叫做 CI(持续集成)、CD(持续部署)。

我们现在还是通过 scp / FTP 来上传代码做的部署,但是不同代码的运行环境是不同的,比如 Node.js 服务需要安装 node,Java 服务需要安装 JRE 等,只把代码传上去并不一定能跑起来。

那怎么办呢?怎么保证部署的代码运行在正确的环境?

把环境也给管理起来,作为部署信息的一部分不就行了?

现在流行的容器技术就是做这个的,比如 docker,可以把环境信息和服务启动方式放到 dockerfile 里,build 产生一个镜像 image,之后直接部署这个 docker image 就行。

比如我们用 nginx 作为静态服务器的时候,dockerfile 可能是这样的:

FROM nginx:alpine

COPY /nginx/ /etc/nginx/

COPY /dist/ /usr/share/nginx/html/

EXPOSE 80

这样就把运行环境给管理了起来。

所以,现在的构建产物不再是直接上传服务器,而是生成一个 docker image,上传到 docker registry,然后把这个 docker image 部署到服务器。


还有一个问题,现在前端代码、后端代码都部署在了我们的服务器上,共享服务器的网络带宽,其中前端代码是不会变动的、流量却很大,这样使得后端服务的可用带宽变小、支持的并发量下降。

能不能把这部分静态资源的请求分离出去呢?最好能部署到离用户近一点的服务器,这样访问更快。

确实可以,这就是 CDN 做的事情。

网上有专门的 CDN 服务提供商,它们有很多分散在各地的服务器,可以提供静态资源的托管。这些静态资源最终还是从我们的静态资源服务器来拿资源的,所以我们的静态资源服务器叫做源站。但是请求一次之后就会缓存下来,下次就不用再请求源站了,这样就减轻了我们服务器的压力,还能加速用户请求静态资源的速度。


这样就解决了静态资源分去了太多网络带宽的问题,而且还加速了资源的访问速度。

此外,静态资源的部署还要考虑顺序问题,要先部署页面用到的资源,再部署页面,还有,需要在文件名加 hash 来触发缓存更新等,这些都是更细节的问题。

这里说的都是网页的部署方式,对于 APP/小程序它们是有自己的服务器和分发渠道的,我们构建完之后不是部署,而是在它们的平台提交审核,审核通过之后由它们负责部署和分发。

总结

互联网让我们能够用手机、PC 等终端访问任何一台服务器的资源、服务。而提供这些资源、服务就是我们开发者做的事情。把资源上传到服务器上,并把服务跑起来,就叫做部署。

对于代码,我们可以本地构建,然后把构建产物通过 FTP/scp 等方式上传到服务器。

但是这样的方式不好管理,所以我们会有专门的 CI/CD 平台来做这个,比如 jenkins。

jenkins 支持 pipeline 的可视化编排,比写 shell 脚本的方式易用很多,可以在构建过程中加入自动化测试、静态代码检查等步骤。

不同代码运行环境不同,为了把环境也管理起来,我们会使用容器技术,比如 docker。把环境信息写入 dockerfile,然后构建生成 docker image,上传到 registry,之后部署这个 docker image 就行。

静态资源和动态资源共享服务器的网络带宽,为了减轻服务器压力、也为了加速静态资源的访问,我们会使用 CDN 来对静态资源做加速,把我们的静态服务器作为源站。第一个静态资源的请求会请求源站并缓存下来,之后的请求就不再需要请求源站,这样就减轻了源站的压力。此外,静态资源的部署还要考虑顺序、缓存更新等问题。

对于网页来说是这样,APP/小程序等不需要我们负责部署,只要在它们的平台提交审核,然后由它们负责部署和分发。

当我们在谈部署的时候,主要就是在谈这些。

作者:zxg_神说要有光
来源:https://juejin.cn/post/7073826878858985479

收起阅读 »

关于项目版本号命名的规范与原则

软件版本阶段说明Alpha版此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改Beta版版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消...
继续阅读 »

软件版本阶段说明

  • Alpha版

    此版本表示该软件在此阶段主要是以实现软件功能为主,通常只在软件开发者内部交流,一般而言,该版本软件的Bug较多,需要继续修改

  • Beta版

    版本相对于α版已有了很大的改进,消除了严重的错误,但还是存在着一些缺陷,需要经过多次测试来进一步消除,此版本主要的修改对像是软件的UI

  • RC版

    该版本已经相当成熟了,基本上不存在导致错误的BUG,与即将发行的正式版相差无几

  • Release版:

    该版本意味“最终版本”,在前面版本的一系列测试版之后,终归会有一个正式版本,是最终交付用户使用的一个版本。该版本有时也称为标准版。一般情况下,Release不会以单词形式出现在软件封面上,取而代之的是符号(R)

在上面我们大致了解了软件从开发到交付会经历4个版本阶段,而当我们开始搭建一个新项目时,不可避免地要配置 package.json 文件,这个 version 怎么命名呢?

// package.json
{
   "name": "项目名称",
   "version": "0.1.0",
   "description": "项目描述",
   "author": "项目作者",
   "license": "MIT",
}

版本命名规范

在说版本命名规范之前,先说说比较流行的版本命名格式

  • GNU 风格

  • Windows 风格

  • .Net Framework 风格

最常见的版本命名格式就是 GNU 风格,下面以 GNU 风格展开说明

主版本号.子版本号.修正(或阶段)版本号.日期版本号_希腊字母版本号

希腊字母版本号共有5种,分别为:base、alpha、beta、RC、release


版本命名修改规则

项目初版本时,版本号可以为 0.1.0

  • 希腊字母版本号(beta)

    此版本号用于标注当前版本的软件处于哪个开发阶段,当软件进入到另一个阶段时需要修改此版本号

  • 日期版本号

    用于记录修改项目的当前日期,每天对项目的修改都需要更改日期版本号(只有此版本号才可由开发人员决定是否修改)

  • 修正(或阶段)版本号改动

    当项目在进行了局部修改或bug修正时,主版本号和子版本号都不变,修正版本号加1

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.0",
       "version": "0.1.1",
    }
  • 子版本号改动

    当项目在原有基础上增加了某些功能模块时,比如增加了对权限控制、增加自定义视图等功能,主版本号不变,子版本号加1,修正版本号复位为0

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.8",
       "version": "0.2.0",
    }
  • 主版本号改动

    当项目在进行了重大修改或局部修正累积较多,而导致项目整体发生全局变化时,比如增加多个模块或者重构,主版本号加 1

    // package.json
    {
       "name": "项目名称",
       "version": "0.1.0",  // 一期项目
       "version": "1.1.0",  // 二期项目
       "version": "2.1.0",  // 三期项目
    }

文件命名规范

文件名称由四部分组成:第一部分为项目名称,第二部分为文件的描述,第三部分为当前软件的版本号,第四部分为文件阶段标识加文件后缀

例如:xx后台管理系统测试报告1.1.1.031222_beta_d.xls,此文件为xx后台管理系统的测试报告文档,版本号为:1.1.1.031222_beta


  • 如果是同一版本同一阶段的文件修改过两次以上,则在阶段标识后面加以数字标识,每次修改数字加1,xx后台管理系统测试报告1.1.1.031222_beta_d1.xls

  • 当有多人同时提交同一份文件时,可以在阶段标识的后面加入人名或缩写来区别,例如:xx后台管理系统测试报告 1.1.1.031222_beta_d_spp.xls。当此文件再次提交时也可以在人名或人名缩写的后面加入序号来区别,例如:xx后台管理系统测试报告1.1.1.031222_beta_d_spp2.xls

阶段标识

软件的每个版本中包括11个阶段,详细阶段描述如下:

阶段名称阶段标识
需求控制a
设计阶段b
编码阶段c
单元测试d
单元测试修改e
集成测试f
集成测试修改g
系统测试h
系统测试修改i
验收测试j
验收测试修改k

作者:Jesse90s
来源:https://juejin.cn/post/7073902470585384990

收起阅读 »

PAT 乙级 1029 旧键盘:找出键盘上的坏键

题目描述旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。输入格式输入在 2 行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过 80 个字符的串,由字...
继续阅读 »


题目描述

旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。

输入格式

输入在 2 行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过 80 个字符的串,由字母 A-Z(包括大、小写)、数字 0-9、以及下划线 _(代表空格)组成。题目保证 2 个字符串均非空。

输出格式:

按照发现顺序,在一行中输出坏掉的键。其中英文字母只输出大写,每个坏键只输出一次。题目保证至少有 1 个坏键。

输入样例:

7_This_is_a_test
_hs_s_a_es

输出样例:

7TI

思路分析

这道题用哈希表标记输入状态的话会很简单,我就是把字符串中所有字符的ASCII码值映射到数组下标中去,0为未输入,1为已输入。
先将第二个字符串所有字符标记(因为是第二个字符串少了字符),为了方便进行比较和后面的输出,我在标记之前将所有字符统一全部转换成了大写字母
然后再遍历第一个字符串,把所有第一个字符串中存在而第二个字符串里不存在的字符输出,也就是第二个字符串里缺少的字符,不要忘了输出完标记为-1,不然会重复输出哦
PS:其实我刚开始的思路也不是用哈希表,是想用两层循环把两个字符串一个一个字符比较输出,但是还没能实现只输出一遍

AC代码

版本一

#include<bits/stdc++.h>
using namespace std;
int main()
{
   int keyboard[128]={0};//下标映射为ASCII码值,0:未输入,1:已输入
   string s1,s2;
   cin>>s1>>s2;
   for(char c2:s2)
       keyboard[toupper(c2)]=1;//已输入标记为1
   for(char c1:s1)
       if(keyboard[toupper(c1)]==0)
      {
           putchar(toupper(c1));
           keyboard[toupper(c1)]=-1;//该字符已输出,标记为-1,避免重复输出
      }
   return 0;
}

版本2

#include <ctype.h>
#include<iostream>
#include <string>
using namespace std;
int main()
{
   int keyboard[128] = {0};
   string s1;
   char c2;
   getline(cin, s1);//因为后面换行之后还要输入字符所以不能直接用cin
   while((c2 = getchar()) != '\n')//输入第二行字符串
       keyboard[toupper(c2)] = 1;

   for(char c1:s1)
       if(keyboard[toupper(c1)] == 0)
{
           putchar(toupper(c1));
           keyboard[toupper(c1)] = -1;
      }
   return 0;
}

总结

用了一次Hash表,感觉也没有想象的那么困难嘛
看书上写的哈希散列可真是头疼,自己用一了遍反而好像也不难😂
真是纸上得来终觉浅,觉知此事要躬行。

作者:梦境无限
来源:https://juejin.cn/post/7074090679198023688

收起阅读 »

奇怪的电梯广搜做法~

一、题目描述:一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例...
继续阅读 »

一、题目描述:

一种很奇怪的电梯,大楼的每一层楼都可以停电梯,而且第 i 层楼(1 ≤ i ≤ N)上有一个数字 Ki (0 ≤ Ki ≤ N)。电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。当然,如果不能满足要求,相应的按钮就会失灵。例如: 3, 3, 1, 2, 5 代表了 Ki(K1=3, K2=3,……),从 1 楼开始。在 1 楼,按“上”可以到 4 楼,按“下”是不起作用的,因为没有 -2 楼。那么,从 A 楼到 B 楼至少要按几次按钮呢?

来源:洛谷 http://www.luogu.com.cn/problem/P11…

输入格式

共二行。 第一行为三个用空格隔开的正整数,表示 N, A, B(1≤N≤200,1≤A,B≤N )。 第二行为 N 个用空格隔开的非负整数,表示 Ki

5 1 5
3 3 1 2 5

输出格式

一行,即最少按键次数,若无法到达,则输出 -1

3

二、思路分析:

首先看一下输入数据是什么意思,首先输入一个N, A, B,也就是分别输入楼层数(N)、开始楼层(A)、 终点楼层(B)。 在例子中,我们的 楼层数N是5,也就是说有5层楼,第二行就是这5层楼的每层楼的数字k。

1、题目中说到只有四个按钮:开,关,上,下,上下的层数等于当前楼层上的那个数字,众所周知,电梯的楼层到了的时候啊,它是会自动打开的,没有人进来的时候,也会自动关上,这里求的是最少按几个按钮,所以我们在这里不用看开关,也就是可以看作该楼层只有两个按钮 +k 、 -k

2、题目中提到最少按几次,很明显,这是一个搜索题。当出现最少的时候,我们就可以考虑用广搜了(也可以用深搜做的啦)

3、这里注意一下,就是我们在不同的按钮次数时遇到停在同一楼层,这时候就会出现一个重复的且没有必要的搜索,所以我们需要在搜索的时候加个条件。

三、AC 代码:

import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sr = new Scanner (System.in);
int N = sr.nextInt();              
int A = sr.nextInt();            
int B = sr.nextInt();              

// 广搜必备队列
Queue<Fset> Q = new LinkedList<Fset>();
// 一个记忆判断,看看这层楼是不是来过
boolean[] visit = new boolean[N+1];
// 来存楼梯按钮的,假设第3层的k是2, 那么 k[3][0]=2 (向上的按钮)、 k[3][1]=-2 (向下的按钮)
int[][] k = new int [N+1][2];
for(int i = 1 ; i <= N ; i++){
k[i][0] = sr.nextInt();
k[i][1] = -k[i][0];
}

// 存一个起始楼层和按钮次数到队列
Q.add(new Fset(A,0));
// 当队列为空也就是所以能走的路线都走过了,没有找到就可以返回-1了
while(!Q.isEmpty()){
Fset t = Q.poll();
// 找到终点楼层,不用找了直接输出并退出搜索
if(t.floor == B){
System.out.println(t.count);
return;
}
//
for(int j = 0 ; j < 2 ; j++){
// 按键后到的楼层
int f = t.floor + k[t.floor][j];
// 判断按键后到的楼层是否有效和是否走过
if(f >= 1 && f <= N && visit[f]!=true) {
Q.add(new Fset(f,t.count+1));
// 做标记
visit[f]=true;
}  
      }
      }
       // 没找到
       System.out.println(-1);
}
}

class Fset{
int floor; // 当前楼层
int count; // 当前按键次数
public Fset(int floor, int count) {
this.floor = floor;
this.count = count;
}
}


四、总结:

为什么用的队列呢? 因为队列的排队取出的,首先判断的一定是按钮次数最少的,感觉这道题用广搜或者深搜效果其实差不多,我写的深搜多一个判断,就是当当前次数超过我找到的最少按钮次数,我就丢弃这个。 广搜像晕染吧,往四周分散搜索,

嗯,就酱~


作者:d粥
来源:https://juejin.cn/post/7073817170618089479

收起阅读 »

Google 大佬们为什么要开发 Go 这门新语言?

Go
大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?难不成是造轮子,其他语言不香吗?背景Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施...
继续阅读 »

大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?

难不成是造轮子,其他语言不香吗?

背景

Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施时遇到的一些问题。


图上三位是 Go 语言最初的设计者,功力都非常的深厚,按序从左起分别是:

  • Robert Griesemer:参与过 Google V8 JavaScript 引擎和 Java HotSpot 虚拟机的研发。

  • Rob Pike:Unix 操作系统早期开发者之一,UTF-8 创始人之一,Go 语言吉祥物设计者是 Rob Pike 的媳妇。

  • Ken Thompson:图灵奖得主,Unix 操作系统早期开发者之一,UTF-8 创始人之一,C 语言(前身 B 语言)的设计者。

遇到的问题

曾经在早期的采访中,Google 大佬们反馈感觉 "编程" 太麻烦了,他们很不喜欢 C++,对于现在工作所用的语言和环境感觉比较沮丧,充满着许多不怎么好用的特性。

具体遭遇到的问题。如下:

  • 软件复杂:多核处理器、网络系统、大规模计算集群和网络编程模型所带来的问题只能暂时绕开,没法正面解决。

  • 软件规模:软件规模也发生了变化,今天的服务器程序由数千万行代码组成,由数百甚至数千名程序员进行工作,而且每天都在更新(据闻 Go 就是在等编译的 45 分钟中想出来的)。

  • 编译耗时:在大型编译集群中,构建时间也延长到了几分钟,甚至几小时。

设计目的

为了实现上述目标,在既有语言上改造的话,需要解决许多根本性的问题,因此需要一种新的语言。

这门新语言需要符合以下需求:

  • 目的:设计和开发 Go 是为了使在这种环境下能够提高工作效率

  • 设计:在 Go 的设计上,除了比较知名的方面:如内置并发和垃圾收集。还考虑到:严格的依赖性管理,随着系统的发展,软件架构的适应性,以及跨越组件之间边界的健壮性。

这门新语言就是现在的 Go。

Go 在 Google

Go 是 Google 设计的一种编程语言,用于帮助解决谷歌的问题,而 Google 的问题很大。

Google 整体的应用软件很庞大,硬件也很庞大,有数百万行的软件,服务器主要是 C++ 语言,其他部分则是大量的 Java 和 Python。

数以千计的工程师在代码上工作,在一个由所有软件组成的单一树的 "头 " 上工作,所以每天都会对该树的所有层次进行重大改变。

一个大型的定制设计的分布式构建系统使得这种规模的开发是可行的,但它仍然很大。

当然,所有这些软件都在几十亿台机器上运行,这些机器被视为数量不多的独立、联网的计算集群。


简而言之,Google 的开发规模很大,速度可能是缓慢的,而且往往是笨拙的。但它是有效的。

Go 项目的目标是:消除 Google 软件开发的缓慢和笨拙,从而使这个过程更富有成效和可扩展。这门语言是由编写、阅读、调试和维护大型软件系统的人设计的,也是为他们设计的

因此 Go 的目的不是为了研究编程语言的设计,而是为了改善其设计者及其同事的工作环境。

Go 更多的是关于软件工程而不是编程语言研究。或者换个说法,它是为软件工程服务的语言设计。

痛点

当 Go 发布时,有些人声称它缺少被认为是现代语言的必要条件的特定功能或方法。在缺乏这些设施的情况下,Go怎么可能有价值?

我们的答案是:Go 所拥有的特性可以解决那些使大规模软件开发变得困难的问题。

这些问题包括:

  • 构建速度缓慢。

  • 不受控制的依赖关系。

  • 每个程序员使用不同的语言子集。

  • 对程序的理解不透彻(代码可读性差,文档不全等)。

  • 工作的重复性。

  • 更新的成本。

  • 版本偏移(version skew)。

  • 编写自动工具的难度。

  • 跨语言的构建。

纯粹一门语言的单个功能并不能解决这些问题,我们需要对软件工程有一个更大的看法。因此在 Go 的设计中,我们试图把重点放在这些问题的解决方案上。

总结

软件工程指导了 Go 的设计。

与大多数通用编程语言相比,Go 的设计是为了解决我们在构建大型服务器软件时接触到的一系列软件工程问题。这可能会使 Go 听起来相当沉闷和工业化。

但事实上,整个设计过程中对清晰、简单和可组合性的关注反而导致了一种高效、有趣的语言,许多程序员发现它的表现力和力量。

为此产生的 Go 特性包括:

  • 清晰的依赖关系。

  • 清晰的语法。

  • 清晰的语义。

  • 相对于继承的组合。

  • 编程模型提供的简单性(垃圾收集、并发)。

  • 简单的工具(Go工具、gofmt、godoc、gofix)。

这就是为什么要开发 Go 的由来,以及为什么会产生如此的设计和特性的原因。

你学会了吗?:)

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

参考


作者:煎鱼eddycjy
来源:https://juejin.cn/post/7054028466060001288

收起阅读 »

本着什么原则,才能写出优秀的代码?

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。在这个...
继续阅读 »

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。

有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。

风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。

在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如直接重写。

但有的时候,我们看一些著名的开源项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。

那么,这篇文章就试图分析一下好代码都有哪些特点,以及本着什么原则,才能写出优秀的代码。

初级阶段

先说说比较基本的原则,只要是程序员,不管是高级还是初级,都会考虑到的。

img

这只是列举了一部分,还有很多,我挑选四项简单举例说明一下。

  1. 格式统一

  2. 命名规范

  3. 注释清晰

  4. 避免重复代码

以下用 Python 代码分别举例说明:

格式统一

格式统一包括很多方面,比如 import 语句,需要按照如下顺序编写:

  1. Python 标准库模块

  2. Python 第三方模块

  3. 应用程序自定义模块

然后每部分间用空行分隔。

import os
import sys

import msgpack
import zmq

import foo
复制代码

再比如,要添加适当的空格,像下面这段代码;

i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
复制代码

代码都紧凑在一起了,很影响阅读。

i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
复制代码

添加空格之后,立刻感觉清晰了很多。

还有就是像 Python 的缩进,其他语言的大括号位置,是放在行尾,还是另起新行,都需要保证统一的风格。

有了统一的风格,会让代码看起来更加整洁。

命名规范

好的命名是不需要注释的,只要看一眼命名,就能知道变量或者函数的作用。

比如下面这段代码:

a = 'zhangsan'
b = 0
复制代码

a 可能还能猜到,但当代码量大的时候,如果满屏都是 abcd,那还不得原地爆炸。

把变量名稍微改一下,就会使语义更加清晰:

username = 'zhangsan'
count = 0
复制代码

还有就是命名要风格统一。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来非常分裂。

注释清晰

看别人代码的时候,最大的愿望就是注释清晰,但在自己写代码时,却从来不写。

但注释也不是越多越好,我总结了以下几点:

  1. 注释不限于中文或英文,但最好不要中英文混用

  2. 注释要言简意赅,一两句话把功能说清楚

  3. 能写文档注释应该尽量写文档注释

  4. 比较重要的代码段,可以用双等号分隔开,突出其重要性

举个例子:

# =====================================
# 非常重要的函数,一定谨慎使用 !!!
# =====================================

def func(arg1, arg2):
   """在这里写函数的一句话总结(如: 计算平均值).

  这里是具体描述.

  参数
  ----------
  arg1 : int
      arg1的具体描述
  arg2 : int
      arg2的具体描述

  返回值
  -------
  int
      返回值的具体描述

  参看
  --------
  otherfunc : 其它关联函数等...

  示例
  --------
  示例使用doctest格式, 在`>>>`后的代码可以被文档测试工具作为测试用例自动运行

  >>> a=[1,2,3]
  >>> print [x + 3 for x in a]
  [4, 5, 6]
  """
复制代码

避免重复代码

随着项目规模变大,开发人员增多,代码量肯定也会增加,避免不了的会出现很多重复代码,这些代码实现的功能是相同的。

虽然不影响项目运行,但重复代码的危害是很大的。最直接的影响就是,出现一个问题,要改很多处代码,一旦漏掉一处,就会引发 BUG。

比如下面这段代码:

import time


def funA():
   start = time.time()
   for i in range(1000000):
       pass
   end = time.time()

   print("funA cost time = %f s" % (end-start))


def funB():
   start = time.time()
   for i in range(2000000):
       pass
   end = time.time()

   print("funB cost time = %f s" % (end-start))


if __name__ == '__main__':
   funA()
   funB()
复制代码

funA()funB() 中都有输出函数运行时间的代码,那么就适合将这些重复代码抽象出来。

比如写一个装饰器:

def warps():
   def warp(func):
       def _warp(*args, **kwargs):
           start = time.time()
           func(*args, **kwargs)
           end = time.time()
           print("{} cost time = {}".format(getattr(func, '__name__'), (end-start)))
       return _warp
   return warp
复制代码

这样,通过装饰器方法,实现了同样的功能。以后如果需要修改的话,直接改装饰器就好了,一劳永逸。

进阶阶段

当代码写时间长了之后,肯定会对自己有更高的要求,而不只是格式注释这些基本规范。

但在这个过程中,也是有一些问题需要注意的,下面就来详细说说。

炫技

第一个要说的就是「炫技」,当对代码越来越熟悉之后,总想写一些高级用法。但现实造成的结果就是,往往会使代码过度设计。

这不得不说说我的亲身经历了,曾经有一段时间,我特别迷恋各种高级用法。

有一次写过一段很长的 SQL,而且很复杂,里面甚至还包含了一个递归调用。有「炫技」嫌疑的 Python 代码就更多了,往往就是一行代码包含了 N 多魔术方法。

然后在写完之后漏出满意的笑容,感慨自己技术真牛。

结果就是各种被骂,更重要的是,一个星期之后,自己都看不懂了。

img

其实,代码并不是高级方法用的越多就越牛,而是要找到最适合的。

越简单的代码,越清晰的逻辑,就越不容易出错。而且在一个团队中,你的代码并不是你一个人维护,降低别人阅读,理解代码的成本也是很重要的。

脆弱

第二点需要关注的是代码的脆弱性,是否细微的改变就可能引起重大的故障。

代码里是不是充满了硬编码?如果是的话,则不是优雅的实现。很可能导致每次性能优化,或者配置变更就需要修改源代码。甚至还要重新打包,部署上线,非常麻烦。

而把这些硬编码提取出来,设计成可配置的,当需要变更时,直接改一下配置就可以了。

再来,对参数是不是有校验?或者容错处理?假如有一个 API 被第三方调用,如果第三方没按要求传参,会不会导致程序崩溃?

举个例子:

page = data['page']
size = data['size']
复制代码

这样的写法就没有下面的写法好:

page = data.get('page', 1)
size = data.get('size', 10)
复制代码

继续,项目中依赖的库是不是及时升级更新了?

积极,及时的升级可以避免跨大版本升级,因为跨大版本升级往往会带来很多问题。

还有就是在遇到一些安全漏洞时,升级是一个很好的解决办法。

最后一点,单元测试完善吗?覆盖率高吗?

说实话,程序员喜欢写代码,但往往不喜欢写单元测试,这是很不好的习惯。

有了完善,覆盖率高的单元测试,才能提高项目整体的健壮性,才能把因为修改代码带来的 BUG 的可能性降到最低。

重构

随着代码规模越来越大,重构是每一个开发人员都要面对的功课,Martin Fowler 将其定义为:在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改。

重构的收益是明显的,可以提高代码质量和性能,并提高未来的开发效率。

但重构的风险也很大,如果没有理清代码逻辑,不能做好回归测试,那么重构势必会引发很多问题。

这就要求在开发过程中要特别注重代码质量。除了上文提到的一些规范之外,还要注意是不是滥用了面向对象编程原则,接口之间设计是不是过度耦合等一系列问题。

那么,在开发过程中,有没有一个指导性原则,可以用来规避这些问题呢?

当然是有的,接着往下看。

高级阶段

最近刚读完一本书,Bob 大叔的《架构整洁之道》,感觉还是不错的,收获很多。

img

全书基本上是在描述软件设计的一些理论知识。大体分成三个部分:编程范式(结构化编程、面向对象编程和函数式编程),设计原则(主要是 SOLID),以及软件架构(其中讲了很多高屋建翎的内容)。

总体来说,这本书中的内容可以让你从微观(代码层面)和宏观(架构层面)两个层面对整个软件设计有一个全面的了解。

其中 SOLID 就是指面向对象编程和面向对象设计的五个基本原则,在开发过程中适当应用这五个原则,可以使软件维护和系统扩展都变得更容易。

五个基本原则分别是:

  1. 单一职责原则(SRP)

  2. 开放封闭原则(OCP)

  3. 里氏替换原则(LSP)

  4. 接口隔离原则(ISP)

  5. 依赖倒置原则(DIP)

单一职责原则(SRP)

A class should have one, and only one, reason to change. – Robert C Martin

一个软件系统的最佳结构高度依赖于这个系统的组织的内部结构,因此每个软件模块都有且只有一个需要被改变的理由。

这个原则非常容易被误解,很多程序员会认为是每个模块只能做一件事,其实不是这样。

举个例子:

假如有一个类 T,包含两个函数,分别是 A()B(),当有需求需要修改 A() 的时候,但却可能会影响 B() 的功能。

这就不是一个好的设计,说明 A()B() 耦合在一起了。

开放封闭原则(OCP)

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

通俗点解释就是设计的类对扩展是开放的,对修改是封闭的,即可扩展,不可修改。

看下面的代码示例,可以简单清晰地解释这个原则。

void DrawAllShape(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawSquare((struct Circle*)s);
break;
default:
break;
}
}
}
复制代码

上面这段代码就没有遵守 OCP 原则。

假如我们想要增加一个三角形,那么就必须在 switch 下面新增一个 case。这样就修改了源代码,违反了 OCP 的封闭原则。

缺点也很明显,每次新增一种形状都需要修改源代码,如果代码逻辑复杂的话,发生问题的概率是相当高的。

class Shape
{
public:
virtual void Draw() const = 0;
}

class Square: public Shape
{
public:
virtual void Draw() const;
}

class Circle: public Shape
{
public:
virtual void Draw() const;
}

void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator I;
for (i = list.begin(): i != list.end(); i++)
{
(*i)->Draw();
}
}
复制代码

通过这样修改,代码就优雅了很多。这个时候如果需要新增一种类型,只需要增加一个继承 Shape 的新类就可以了。完全不需要修改源代码,可以放心扩展。

里氏替换原则(LSP)

Require no more, promise no less.– Jim Weirich

这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。

里氏替换原则可以从两方面来理解:

第一个是继承。如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。

子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

第二个是多态,而多态的前提就是子类覆盖并重新定义父类的方法。

为了符合 LSP,应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法。当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

举个例子:

看下面这段代码:

class A{
public int func1(int a, int b){
return a - b;
}
}

public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50=" + a.func1(100, 50));
System.out.println("100-80=" + a.func1(100, 80));
}
}
复制代码

输出;

100-50=50
100-80=20
复制代码

现在,我们新增一个功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:

  1. 两数相减

  2. 两数相加,然后再加 100

现在代码变成了这样:

class B extends A{
public int func1(int a, int b){
return a + b;
}

public int func2(int a, int b){
return func1(a,b) + 100;
}
}

public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50=" + b.func1(100, 50));
System.out.println("100-80=" + b.func1(100, 80));
System.out.println("100+20+100=" + b.func2(100, 20));
}
}
复制代码

输出;

100-50=150
100-80=180
100+20+100=220
复制代码

可以看到,原本正常的减法运算发生了错误。原因就是类 B 在给方法起名时重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。

这样做就违反了 LSP,使程序不够健壮。更通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

接口隔离原则(ISP)

Clients should not be forced to depend on methods they do not use. –Robert C. Martin

软件设计师应该在设计中避免不必要的依赖。

ISP 的原则是建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法要尽量少。

也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。

单一职责与接口隔离的区别:

  1. 单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。

  2. 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节; 而接口隔离原则主要约束接口。

举个例子:

img

首先解释一下这个图的意思:

「犬科」类依赖「接口 I」中的方法:「捕食」,「行走」,「奔跑」; 「鸟类」类依赖「接口 I」中的方法「捕食」,「滑翔」,「飞翔」。

「宠物狗」类与「鸽子」类分别是对「犬科」类与「鸟类」类依赖的实现。

对于具体的类:「宠物狗」与「鸽子」来说,虽然他们都存在用不到的方法,但由于实现了「接口 I」,所以也 必须要实现这些用不到的方法,这显然是不好的设计。

如果将这个设计修改为符合接口隔离原则的话,就必须对「接口 I」进拆分。

img

在这里,我们将原有的「接口 I」拆分为三个接口,拆分之后,每个类只需实现自己需要的接口即可。

依赖倒置原则(DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.– Robert C. Martin

高层策略性的代码不应该依赖实现底层细节的代码。

这话听起来就让人听不明白,我来翻译一下。大概就是说在写代码的时候,应该多使用稳定的抽象接口,少依赖多变的具体实现。

举个例子:

看下面这段代码:

public class Test {

public void studyJavaCourse() {
System.out.println("张三正在学习 Java 课程");
}

public void studyDesignPatternCourse() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

上层直接调用:

public static void main(String[] args) {
Test test = new Test();
test.studyJavaCourse();
test.studyDesignPatternCourse();
}
复制代码

这样写乍一看并没有什么问题,功能也实现的好好的,但仔细分析,却并不简单。

第一个问题:

如果张三又新学习了一门课程,那么就需要在 Test() 类中增加新的方法。随着需求增多,Test() 类会变得非常庞大,不好维护。

而且,最理想的情况是,新增代码并不会影响原有的代码,这样才能保证系统的稳定性,降低风险。

第二个问题:

Test() 类中方法实现的功能本质上都是一样的,但是却定义了三个不同名字的方法。那么有没有可能把这三个方法抽象出来,如果可以的话,代码的可读性和可维护性都会增加。

第三个问题:

业务层代码直接调用了底层类的实现细节,造成了严重的耦合,要改全改,牵一发而动全身。

基于 DIP 来解决这个问题,势必就要把底层抽象出来,避免上层直接调用底层。

img

抽象接口:

public interface ICourse {
void study();
}
复制代码

然后分别为 JavaCourseDesignPatternCourse 编写一个类:

public class JavaCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习 Java 课程");
}
}

public class DesignPatternCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

最后修改 Test() 类:

public class Test {

   public void study(ICourse course) {
       course.study();
  }
}
复制代码

现在,调用方式就变成了这样:

public static void main(String[] args) {
   Test test = new Test();
   test.study(new JavaCourse());
   test.study(new DesignPatternCourse());
}
复制代码

通过这样开发,上面提到的三个问题得到了完美解决。

其实,写代码并不难,通过什么设计模式来设计架构才是最难的,也是最重要的。

所以,下次有需求的时候,不要着急写代码,先想清楚了再动手也不迟。

这篇文章写的特别辛苦,主要是后半部分理解起来有些困难。而且有一些原则也确实没有使用经验,单靠文字理解还是差点意思,体会不到精髓。

其实,文章中的很多要求我都做不到,总结出来也相当于是对自己的一个激励。以后对代码要更加敬畏,而不是为了实现功能草草了事。写出健壮,优雅的代码应该是每个程序员的目标,与大家共勉。


作者:yongxinz
链接:https://mp.weixin.qq.com/s/xWZmP4qBI8cm68UZH6AXOg

收起阅读 »

十分钟搞懂手机号码一键登录

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只...
继续阅读 »

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只要确认登录用的手机号码是在绑定了此号码的手机上发起的即可认证成功,从这一点来看,它和短信验证码登录并无本质区别,都是一种设备认证登录方式。这篇文章就来捋一下其中的技术门道。

这几年为了保护用户的隐私安全,Android和iOS系统都限制了应用获取本机号码的能力,即使通过某些技术手段获取到了本机号码,这个号码还可能是被篡改的,所以应用直接读取本机号码用于登录是不可行的。那么这些应用是怎么获取到真实的本机号码的呢?答案是电信运营商,手机要打电话、要上网、要计费,运营商肯定能对应到正确的手机号码。国内的运营商就是移动、联通、电信这三家,它们都开放了这种能力。对于在互联网大潮中被管道化的运营商来说,不失为一种十分有意义的积极进取。

手机流量上网的原理

手机号码一键登录是借助手机流量上网来实现,所以先要搞清楚流量上网的原理。

目前网上已有很多关于一键登录的技术文章,但是内容基本雷同,关于获取手机号码的部分,所述都是通过运营商的数据网关能力,语焉不详,对于有追求的技术人来说,难以忍受。这个章节就来介绍下这种从数据网关获取手机号码的能力是如何实现的,因为通信专业知识十分繁杂,我也没有经过专业的学习,大家也不想接触到很多的专业名词,所以这里只保留一些关键的专业名词,尽量以通俗易懂的方式来理清这个机制。

五层网络模型

对网络比较熟悉的同学,应该了解五层协议,那么手机流量上网时的五层网络模型有何不同呢?


从上图可以看出,手机流量上网的主要区别在数据链路层和物理层。在数据链路层,流量上网没有MAC地址的概念,它采用一种点对点协议(PPP),手机端通过拨号方式建立这种PPP连接,然后发送数据。在物理层,流量上网通过手机内置的基带模块进行无线信号的调制、解调工作,从而实现与移动基站之间的电磁波通信。

流量上网的机制

点对点协议支持身 验证功能,手机端发起连接时会携带自己的身 粉证明,一般就是手机卡内置的IMSI,这个IMSI也会保存在运营商的数据库中,因此基站就可以验证连接用户的身 ,当然这个验证过程不是简单的对比IMSI,会有更多安全机制。为了更清楚的了解流量上网机制,下面再来一张4G流量上网时手机与运营商的交互示意图:


核心组件

手机:这其中对流量上网起到关键作用的就是手机卡和基带模块。手机卡中保存了IMSI,全称International Mobile Subscriber Identification Number,国际移动用户识别码。IMSI是手机卡的身 标识。

基站:就是外边常见的铁架子信号塔,是一种能覆盖一定范围的无线电收发信息电台,手机会连接到它,然后它再通过光纤连接到运营商网络,从而实现移动通信。

MME:Mobility Management Entity,移动控制单元。手机建立连接时会先访问到这里,负责:手机与基站的接入控制,手机卡的鉴权、会话管理、安全传输,漫游控制、跨运营商通信等。

HSS:Home Subscriber Server,归属签约用户服务器。保存本地签约的手机卡信息,包括手机卡IMSI与手机号的对应关系,手机号的套餐信息、手机号的归属地信息等。

S-GW:Service Gateway,服务网关。4G环境下,用户侧与运营商核心网之间的业务网关。访问能不能进入,能做什么业务,去哪里做业务,是在这里控制的。跨运营商计费、漫游计费等也在这里完成。

P-GW:PDN Gateway,PDN网关。运营商核心网与互联网之间的网关,手机真正上网就是通过它了。它会给手机分配一个IP地址,控制上网的速度,对流量进行计费等。

PCRF:Policy and Charging Rules Function,策略与计费控制单元,保存每个用户的网络访问策略和计费规则。

上网过程

为了方便理解,这里将上网的过程大致分为两个部分(和上图的1、2对应):

  • 1 接入:建立连接时,手机携带IMSI信息,通过基站访问到MME,MME通过HSS验证IMSI信息,然后MME进行一些初始化工作,返回一些鉴权参数给手机,手机再进行一些计算,然后把计算结果返回给MME,MME验证手机的计算结果,验证通过则允许接入。这个过程保证了接入的安全,MME还为后续的数据传输提供了加密传输支持,保护数据不被窃听和篡改,有兴趣的同学可以去详细了解下。

    如果手机卡销售的时候没有写入手机号,手机卡首次注册登记的时候,运营商会从HSS中取出手机号,然后再写入手机卡中。

    实际应用中,为了防止跟踪和攻击,不是每次通信时都要携带IMSI,MME会生成一个临时的GUTI对应到IMSI,就像Web程序中的SessionId。MME还有一定的机制控制GUIT的重新分配。

  • 2 传输:手机网络流量的传输,还是先要通过基站,然后下一步进入S-GW,S-GW会检查用户的授权,就像Web程序中检查前端提交过来的SessionId,再看看用户有没有权限进行其提交的业务,这里就是看看用户有没有开通流量上网,这是S-GW通过连接MME实现的。S-GW处理完毕后,数据包会进入P-GW,P-GW在手机使用流量上网时会给用户分配一个IP地址,然后数据包通过网关进入互联网,访问到相关的资源。P-GW还会对上网行为进行速率控制、流量计费等操作,这些策略来源于PCRF,PCRF中的规则是根据HSS中的用户套餐、用户等级等计算出来的。

    对P-GW来说S-GW屏蔽了用户的移动性,手机在多个基站切换时,S-GW不变。

以上就是手机流量上网的基本原理了,可以看到,运营商通过IMSI或者GUTI完全有能力获取到当前上网用户的手机号码。对于运营商的一键登录具体是怎么实现的,我并没有找到相关的介绍,但是可以设想下:手机应用通过运营商的SDK发起获取手机号码的业务请求,此时会携带IMSI或者GUTI,业务请求到达S-GW,S-GW鉴权通过,然后将这个业务请求路由到运营商核心网中获取手机号码的服务,服务根据业务规则从HSS中取出手机号码并进行若干处理。

一键登录的原理

理解了手机流量上网的原理,再来看下一键登录业务是如何实现的,这个部分属于上层应用程序开发,大家应该相对熟悉一些。

如果你接入过微信的第三方应用登录,或者其他类似的第三方应用登录,过程是差不多的。还是先来看图:


这里对一些关键步骤进行说明:

  • 2预取手机号掩码:这个手机号掩码需要在请求用户授权的页面展示给用户看,因为获取这个信息要通过电信运营商的网络,所以可能会比较慢,为了提升用户体验,可以在应用启动的时候就去获取,然后缓存一段时间。

  • 8授权请求:因为应用获取用户手机号这个事比较敏感,必须让用户清楚的了解并授权之后才能进行,为了确保这件事,运营商的认证SDK提供了这个授权请求页面,用户确认授权后,SDK直接向运营商认证服务发起请求认证,认证服务会返回一个认证Token给应用。应用再通过自己的服务端拿着这个Token找运营商获取手机号码。

  • 17生成应用授权Token:应用要维护自己用户的登录状态,这里可以采用传统的Session机制,也可以使用JWT机制。

  • 3预取手机号掩码 和 11请求认证,都需要通过手机蜂窝网络通信,也就说需要通过手机流量上网。如果手机同时开启了流量和WIFI,认证SDK会将手机短暂切换到流量上网模式。如果手机没有开启流量,有些SDK还会在上次成功取号之后多缓存一个临时Token,这样也能成功实现一次一键登录,不过这个限制性很大。

这里其实还有一个安全问题

14登录请求:用户如果随便造一个认证Token,然后就向应用服务提交请求,应用服务再向认证服务提交请求,这属于一种跨站攻击。虽然这个Token可以被阻止,但是不免浪费资源,给服务端带来压力。

这一点微信第三方应用登录做的比较好,用户登录前,应用服务端先生成一个随机数,然后应用前端向应用服务端提交时,带着这个随机数,应用服务端可以验证这个随机数。

号码验证场景

除了用于登录,运营商网关的这种取号能力,还可以用在验证手机号上,在某些关键业务上,比如支付过程中,要求用户输入本机手机号码或者其中的某几位,然后通过运营商认证服务验证手机号是否本机号码。

隐私保护问题

设备唯一标识问题

现在大家对隐私问题关注的越来越多了,经常会出现这种情况:你在某电商网站搜索了某个商品,然后访问其它网站时,都向你推荐这类商品的广告。还有一种感觉很恐怖的情况,你刚和某个人谈论了某件事,然后就在某个App上看到了关于这件事的推荐,有人猜测是App在偷听,不过基于目前的舆论和监督,偷听风险太大,这其中的原因可能真的只是算法太厉害了。

最近几年Android和iOS系统都对App获取手机唯一标识进行了限制,比如IMEI、Mac地址、序列号、广告Id等,目的就是防止用户的信息在多个App之间进行关联,导致泄漏用户的隐私,产生一些安全问题和法律风险,前述跨App的广告行为也自然受到了抑制。

在了解一键登录的技术原理时,看到某运营商提供了一种和SIM卡绑定的设备唯一Id服务,宣传语就是为了应对移动操作系统限制访问手机唯一标识的问题,在现今越来越重视隐私保护的前提下,如果这种能力开放给了广告平台,就是开历史的倒车了。

手机号作为身 份标识的问题

对于国内普遍使用手机号登录的方式,从技术上很难限制App之间进行手机号关联,然后综合分析用户的行为。比如某家大厂运营了多款不同种类的热门App,它就有能力更全面的了解某个用户,如果要限制可能就得通过法律层面来解决了。至于不同厂商之间的手机号关联行为,基于商业利益的保护,不太可能会出现。

在国内这种商业环境下,如果你真的对自己的隐私很关注,最好只使用账号密码的方式登录,否则经常更换手机号可能是一种没办法的办法。

手机号重新销售问题

手机号的总量是有限的,为了有效利用手机号资源,手机号注销以后,经过一段时间就会被运营商重新销售。如果新的手机号拥有者拿着这个手机号登录某个APP,而这个手机号之前已经在这个App上注册过,产生了大量的使用记录,那么此手机号前拥有者的隐私就会被泄漏。所以大家现在都不太敢随便更换手机号,因为注册过的地方太多了,留下了数不清的使用痕迹。

在了解一键登录的技术原理时,还看到某运营商提供了一种“手机号更换绑定SIM卡通知”的服务,应用可以据此解绑重新销售的手机号与应用账号之间的关系,从而保护用户的隐私。在上文中已经提过手机卡使用IMSI进行标识,如果手机号被重新销售,就会绑定新的IMSI,运营商可以据此产生通知。当然运营商还需要排除手机卡更换和携号转网的情况,这些情况下手机号也会绑定新的IMSI。

不得不说运营商的这个服务还是挺赞的👍。


作者:萤火架构
来源:https://juejin.cn/post/7059182505101885471

收起阅读 »

一些著名的软件都用什么语言编写?

1、操作系统Microsoft Windows :汇编 -> C -> C++备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。相...
继续阅读 »

1、操作系统

Microsoft Windows :汇编 -> C -> C++


备注:曾经在智能手机的操作系统(Windows Mobile)考虑掺点C#写的程序,比如软键盘,结果因为写出来的程序太慢,实在无法和别的模块合并,最终又回到C++重写。

相信很多朋友都知道Windows Vista,这个系统开发早期比尔盖茨想全部用C#写,但最终因为执行慢而放弃,结果之前无数软件工程师日夜劳作成果一夜之间被宣告作废。

Linux :C


Apple MacOS : 主要为C,部分为C++。

备注:之前用的语言比较杂,最早是汇编和Pascal。


Sun Solaris : C

HP-UX : C

Symbian OS : 汇编,主要为C++(诺基亚手机)

Google Android :2008 年推出:C语言(有传言说是用Java开发的操作系统,但最近刚推出原生的C语言SDK)

RIM BlackBerry OS 4.x :黑莓 C++

2、图形界面层

Microsoft Windows UI :C++

Apple MacOS UI (Aqua) : C++

Gnome (Linux图形界面之一,大脚): C和C++, 但主要是C

KDE (Linux图形界面): C++

3、桌面搜索工具

Google Desktop Search : C++


Microsoft Windows Desktop Search : C++

Beagle (Linux/Windows/UNIX 下): C# (基于开源的 .net : Mono)

4、办公软件

Microsoft Office :在 汇编 -> C -> 稳定在C++


Sun Open Office : 部分JAVA(对外接口),主要为C++ (开源,可下载其源代码)

Corel Office/WordPerfect Office : 1996年尝试过Java,次年被抛弃,重新回到C/C++

Adobe Systems Acrobat Reader/Distiller : C++

5、关系型数据库

Oracle : 汇编、C、C++、Java。主要为C++


MySQL : C++


IBM DB2 :汇编、C、C++,但主要为C


Microsoft SQL Server : 汇编 -> C->C++

IBM Informix : 汇编、C、C++,但主要为C

SAP DB/MaxDB : C++

6、Web Browsers/浏览器

Microsoft Internet Explorer : C++


Mozilla Firefox : C++


Netscape Navigator :The code of Netscape browser was written in C, and Netscape engineers, all bought to Java (see M. Cusumano book and article) redeveloped the browser using Java. It was too slow and abandoned. Mozilla, the next version, was later developed using C++.

Safari : (2003年1月发布)C++

Google Chrome : (2008的发布)C++


Sun HotJava : Java (死于1999年)

Opera : C++ (手机上占用率比较大)

Opera Mini : Opera Mini (2007) has a very funny architecture, and is indeed using both C++ and Java. The browser is split in two parts, an ultra thin (less than 100Kb) “viewer” client part and a server side responsible of rendering. The first uses Java and receives the page under the OBML format, the latter reuses classical Opera (C++) rendering engine plus Opera’s Small Screen Rendering, on the server. This allows Opera to penetrate various J2ME-enabled portable devices, such as phones, while preserving excellent response time. This comes obviously with a few sacrifices, for instance on JavaScript execution.

Mosaic : 鼻祖(已死) C 语言

7、邮件客户端

Microsoft Outlook : C++


IBM Lotus Notes : Java


Foxmail : Delphi


8、软件开发集成环境/IDE

Microsoft Visual Studio :C++


Eclipse : Java (其图形界面SWT基于C/C++)


Code::Blocks :C++


易语言:C++


火山中文:C++

火山移动:C++

9、虚拟机

Microsoft .Net CLR (.NET的虚拟机): C++


Java Virtual Machine (JVM) : Java 虚拟机:C++


10、ERP软件 (企业应用)

SAP mySAP ERP : C,后主要为“ABAP/4”语言

Oracle Peoplesoft : C++ -> Java


Oracle E-Business Suite : Java

11、商业智能(Business Intelligence )

Business Objects : C++

12、图形处理

Adobe Photoshop : C++


The GIMP : C

13、搜索引擎

Google : 汇编 与 C++,但主要为C++

14、著名网站

eBay : 2002年为C++,后主要迁至Java

facebook : C++ 和 PHP

This line is only about facebook, not its plugins. Plugins can be developed in many different technologies, thanks to facebook’s ORB/application server, Thrift. Thrift contains a compiler coded in C++. facebook people write about Thrift: “The multi-language code generation is well suited for search because it allows for application development in an efficient server side language (C++) and allows the Facebook PHP-based web application to make calls to the search service using Thrift PHP libraries.” Aside the use of C++, facebook has adopted a LAMP architecture.


阿里巴巴和淘宝:php->C++/Java(主要用)


15、游戏

汇编、C、C++

星际争霸、魔兽争霸、CS、帝国时代、跑跑卡丁车、传奇、魔兽世界… 数不胜数了,自己数吧


都是用C开发的,C语言靠近系统地称,执行速度最快。比如你的两个朋友与你分别玩用VB、Java、与C编写的“跑跑卡丁车”,你玩C编写的游戏已经跑玩结束了,发现你的两个朋友还没开始跑呢,那是相当的卡啊。

16、编译器

Microsoft Visual C++ 编译器: C++

Microsoft Visual Basic 解释、编译器:C++

Microsoft Visual C# :编译器: C++

gcc (GNU C compiler) : C

javac (Sun Java compiler) : Java

Perl : C++

PHP : C

17、3D引擎

Microsoft DirectX : C++


OpenGL : C


OGRE 3D : C++


18、Web Servers (网页服务)

Apache : C和C++,但主要为C


Microsoft IIS : C++

Tomcat : Java


Jboss : Java


19、邮件服务

Microsoft Exchange Server : C->C++

Postfix : C

hMailServer : C++

Apache James : Java

20、CD/DVD刻录

Nero Burning ROM : C++


K3B : C++

21、媒体播放器

Nullsoft Winamp : C++


Microsoft Windows Media Player : C++


22、Peer to Peer (P2P软件)

eMule : C++

μtorrent : C++

Azureus : Java (图形界面使用基于C/C++的SWT,类Eclipse)

23、全球定位系统(GPS)

TomTom : C++


Hertz NeverLost : C++

Garmin : C++

Motorola VIAMOTO : 2007年6月,停止服务,Java

24、3D引擎

Microsoft DirectX : C++(相信玩游戏的同学都知道这个,现在最高版本是DX11)

OpenGL : C

OGRE 3D : C++

25、服务器软件

Apache:C

Nginx:C


IIS:C

26、其它

OpenStack:Python


作者:土豆居士
来源:一口Linux

收起阅读 »

代码对比工具,我就用这6个

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。 支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通...
继续阅读 »

WinMerge

pic_2c55c38b.png

WinMerge是一款运行于Windows系统下的文件比较和合并工具,使用它可以非常方便地比较多个文档内容,适合程序员或者经常需要撰写文稿的朋友使用。

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。

Diffuse

pic_73d4bebc.png

Diffuse在命令行中的速度是相当快的,支持像 C++、Python、Java、XML 等语言的语法高亮显示。可视化比较,非常直观,支持两相比较和三相比较。这就是说,使用 Diffuse 你可以同时比较两个或三个文本文件。

支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通过 Diffuse 直接从版本控制系统获取源代码,以便对其进行比较和合并。

Beyond Compare

pic_45e693a5.png

Beyond Compare可以很方便地对比出两份源代码文件之间的不同之处,相差的每一个字节用颜色加以表示,查看方便,支持多种规则对比。

Beyond Compare选择最好的方法来突出不同之处,文本文件可以用语法高亮和设置比较规则的方法进行查看和编辑,适用于用于文档、源代码和HTML。

Altova DiffDog

pic_4afb57c3.png

pic_b3fd72aa.png

是一款用于文件、目录、数据库模式与表格对比与合并的使用工具。

这个强大易用的对比/合并工具可以让你通过其直观的可视化界面快速比较和合并文本或源代码文件,同步目录以及比较数据库模式与表格。DiffDog还提供了先进XML的差分和编辑功能。

AptDiff

pic_5bfb9a16.png

AptDiff是一个文件比较工具,可以对文本和二进制文件进行比较和合并,适用于软件开发、网络设计和其它的专业领域。

它使用方便,支持键盘快捷键,可以同步进行横向和纵向卷动,支持Unicode格式和大于4GB的文件,可以生成HTML格式的比较报告。

Code Compare

pic_1ff7b983.png

Code Compare是一款用于程序代码文件的比较工具,目前Code Compare支持的对比语言有:C#、C++、CSS、HTML、Java、JavaScrip等代码语言。

Code Compare的运行环境为Visual Studio,而Visual Studio可以方便所有的程序开发设计。

作者:小白学视觉
来源:https://mp.weixin.qq.com/s/I5__jnuDAIJlWbCHJsPDRQ

收起阅读 »

Java之父独家专访:我可太想简化一下 Java了

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Ja...
继续阅读 »

IEEE Spectrum 2021 年度编程语言排行榜新鲜出炉,不出意料,Java 仍稳居前三。自 1995 年诞生以来,Java 始终是互联网行业炙手可热的编程语言。近年来,新的编程语言层出不穷,Java 如何做到 26 年来盛行不衰?面对技术新趋势,Java 语言将如何发展?在亚马逊云科技 re:Invent 十周年之际,InfoQ 有幸对 Java 父 James Gosling 博士进行了一次独家专访。James Gosling 于 2017 年作为“杰出工程师”加入亚马逊云科技,负责为产品规划和产品发布之类的工作提供咨询支持,并开发了不少原型设计方案。在本次采访中,James Gosling 谈到了 Java 的诞生与发展、他对众多编程语言的看法、编程语言的未来发展趋势以及云计算带来的改变等问题。


Java 的诞生与发展

InfoQ:Java 语言是如何诞生的?是什么激发您创建一门全新的语言?

James Gosling:Java 的诞生其实源于物联网的兴起。当时,我在 Sun 公司工作,同事们都觉得嵌入式设备很有发展前景,而且随着设备数量的激增,整个世界正逐渐向智能化的方向发展。我们投入大量时间与不同行业的从业者进行交流,也拜访了众多东南亚、欧洲的从业者,结合交流心得和行业面临的问题,决定构建一套设计原型。正是在这套原型的构建过程中,我们深刻地意识到当时主流的语言 C++ 存在问题。

最初,我们只打算对 C++ 做出一点小调整,但随着工作的推进、一切很快“失控”了。我们构建出不少非常有趣的设备原型,也从中得到了重要启示。因此,我们及时对方向进行调整,希望设计出某种适用于主流业务和企业计算的解决方案,这正是一切故事的开端。

InfoQ:Java 作为一门盛行不衰的语言,直到现在依旧稳居编程语言的前列,其生命力何在?

James Gosling:Java 得以拥有顽强的生命力背后有诸多原因。

首先,采用 Java 能够非常便捷地进行多线程编程,能大大提升开发者的工作效率。

其次,Java 提供多种内置安全功能,能够帮助开发者及时发现错误、更加易于调试,此外,各种审查机制能够帮助开发者有效识别问题。

第三,热修复补丁功能也非常重要,亚马逊开发者开发出的热补丁修复程序,能够在无须停机的前提下修复正在运行的程序,这是 Java 中非常独特的功能。

第四,Java 拥有很好的内存管理机制,自动垃圾收集大大降低了内存泄露或者双重使用问题的几率。总之,Java 的设计特性确实提升了应用程序的健壮性,特别是极为强大的现代垃圾收集器方案。如果大家用过最新的长期支持版本 JDK17,应该对其出色的垃圾收集器印象深刻。新版本提供多种强大的垃圾收集器,适配多种不同负载使用。另外,现代垃圾收集器停顿时间很短、运行时的资源消耗也非常低。如今,很多用户会使用体量极为庞大的数据结构,而只要内存能容得下这种 TB 级别的数据,Java 就能以极快的速度完成庞大数据结构的构建。

InfoQ:Java 的版本一直以来更新得比较快,几个月前发布了最新的 Java17 版本,但 Java8 仍然是开发人员使用的主要版本,新版本并未“得宠”,您认为主要的原因是什么?

James Gosling:对继续坚守 Java8 的朋友,我想说“是时候作出改变了”。新系统全方位性更强、速度更快、错误也更少、扩展效率更高。无论从哪个角度看,大家都有理由接纳 JDK17。确实,大家在从 JDK8 升级到 JDK9 时会遇到一个小问题,这也是 Java 发展史中几乎唯一一次真正重大的版本更替。大多数情况下,Java 新旧版本更替都非常简单。只需要直接安装新版本,一切就能照常运作。长久以来,稳定、非破坏性的升级一直是 Java 的招牌特性之一,我们也不希望破坏这种良好的印象。

InfoQ:回顾当初,你觉得 Java 设计最成功的点是什么?相对不太满意的地方是什么?

James Gosling:这其实是一种博弈。真正重要的是 Java 能不能以更便利的方式完成任务。我们没办法设想,如果放弃某些问题域,Java 会不会变得更好?或者说,如果我现在重做 Java,在取舍上会有不同吗?区别肯定会有,但我估计我的取舍可能跟大多数人都不一样,毕竟我的编程风格也跟多数人不一样。不过总的来讲,Java 确实还有改进空间。

InfoQ:有没有考虑简化一下 Java?

James Gosling:我可太想简化一下 Java 了。毕竟简化的意义就是放下包袱、轻装上阵。所以 JavaScript 刚出现时,宣传的就是精简版 Java。但后来人们觉得 JavaScript 速度太慢了。在 JavaScript 的早期版本中,大家只是用来执行外部验证之类的简单事务,所以速度还不太重要。但在人们打算用 JavaScript 开发高性能应用时,得出的解决方案就成了 TypeScript。其实我一直觉得 TypeScript 的定位有点搞笑——JavaScript 就是去掉了 Type 的 Java,而 TypeScript 在 JavaScript 的基础上又把 type 加了回来。Type 系统有很多优势,特别是能让系统运行得更快,但也确实拉高了软件开发者的学习门槛。但如果你想成为一名专业的软件开发者,那最好能克服对于学习的恐惧心理。

Java 之父的编程语言之见

InfoQ:一款优秀的现代化编程语言应该是怎样的?当下最欣赏哪一种编程语言的设计理念?

James Gosling:我个人还是会用最简单的评判标准即这种语言能不能改善开发者的日常工作和生活。我尝试过很多语言,哪种更好主要取决于我想干什么。如果我正要编写低级设备驱动程序,那我可能倾向于选择 Rust。但如果需要编写的是用来为自动驾驶汽车建立复杂数据结构的大型导航系统,那我几乎肯定会选择 Java。

InfoQ:数据科学近两年非常热门,众所周知,R 语言和 Python 是数据科学领域最受欢迎的两门编程语言,那么,这两门语言的发展前景怎么样?因具体的应用领域产生专用的编程语言,会是接下来编程语言领域的趋势之一吗?

James Gosling:我是领域特定语言的铁粉,也深切认同这些语言在特定领域中的出色表现。大多数领域特定语言的问题是,它们只能在与世隔绝的某一领域中发挥作用,而无法跨越多个领域。这时候大家更愿意选择 Java 这类语言,它虽然没有针对任何特定领域作出优化,但却能在跨领域时表现良好。所以,如果大家要做的是任何形式的跨领域编程,肯定希望单一语言就能满足所有需求。有时候,大家也会尝试其他一些手段,希望在两种不同的领域特定语言之间架起一道桥梁,但一旦涉及两种以上的语言,我们的头脑通常就很难兼顾了。

InfoQ:Rust 一直致力于解决高并发和高安全性系统问题,这也确实符合当下绝大部分应用场景的需求,对于 Rust 语言的现在和未来您怎么看?

James Gosling:在我看来,Rust 太过关注安全了,这让它出了名的难学。Rust 解决问题的过程就像是证明定理,一步也不能出错。如果我们只需要编写一小段代码,用于某种固定不变的设备,那 Rust 的效果非常好。但如果大家需要构建一套具有高复杂度动态数据结构的大规模系统,那么 Rust 的使用难度就太高了。

编程语言的学习和发展

InfoQ:编程语言倾向于往更加低门槛的方向发展,开发者也更愿意选择学习门槛低的开发语言,一旦一门语言的学习成本过高,开发者可能就不愿意去选择了。对于这样的现象,您怎么看?

James Gosling:要具体问题具体分析。我到底需要 Rust 中的哪些功能特性?我又需要 Java 中的哪些功能特性?很多人更喜欢 Python,因为它的学习门槛真的很低。但跑跑基准测试,我们就会发现跟 Rust 和 Java 相比,Python 的性能实在太差了。如果关注性能,那 Rust 或 Java 才是正确答案。另外,如果你需要的是只有 Rust 能够提供的那种致密、安全、严谨的特性,代码的编写体量不大,而且一旦出问题会造成严重后果,那 Rust 就是比较合适的选择。只能说某些场景下某些语言更合适。Java 就属于比较折衷的语言,虽然不像 Python 那么好学,但也肯定不算难学。

InfoQ:当前,软件项目越来越倾向采用多语言开发,对程序员的要求也越来越高。一名开发人员,应该至少掌握几种语言?最应该熟悉和理解哪些编程语言?

James Gosling:我刚刚入行时,市面上已经有很多语言了。我学了不少语言,大概有几十种吧。但很多语言的诞生本身就很荒谬、很没必要。很多语言就是同一种语言的不同方言,因为它们只是在用不同的方式实现基本相同的语言定义。最让我振奋的是我生活在一个能够致力于解决问题的世界当中。Java 最大的吸引力也正在于此,它能帮助我们解决几乎任何问题。具有普适性的语言地位高些、只适用于特定场景的语言则地位低些,对吧?所以到底该学什么语言,取决于你想解决什么问题、完成哪些任务。明确想要解决什么样的问题才是关键。

InfoQ:2021 年,技术圈最热门的概念非元宇宙莫属,您认为随着元宇宙时代的到来,新的应用场景是否会对编程语言有新的需求?可否谈谈您对未来编程语言的理解?

James Gosling:其实人们从很早开始就在构建这类虚拟世界系统了,所以我觉得元宇宙概念对编程不会有什么影响。唯一的区别是未来我们可以漫步在这些 3D 环境当中,类似于大型多人游戏那种形式。其实《我的世界》就是用户构建型元宇宙的雏形嘛,所以这里并没有什么真正新鲜的东西,仍然是游戏粉加上社交互动机制的组合。我还想强调一点,虚拟现实其实没什么意思。我更重视与真实人类的面对面互动,真的很难想象自己有一天会跟独角兽之类的虚拟形象聊天。

写在最后:云计算带来的改变

InfoQ:您最初是从什么时候或者什么具体事件开始感受到云计算时代的到来的?

James Gosling:云计算概念的出现要远早出云计算的真正实现。因为人们一直把计算机摆在大机房里,再通过网络连接来访问,这其实就是传统的 IT 服务器机房,但这类方案维护成本高、建造成本高、扩展成本也高,而且对于人员技能等等都有着很高的要求。如果非要说,我觉得多租户云的出现正是云计算迎来飞跃的关键,这时候所有的人力与资本支出都由云服务商负责处理,企业客户再也不用为此烦心了。他们可以单纯关注自己的业务重心,告别那些没完没了又没有任何差异性可言的繁重工作。

InfoQ:云计算如今已经形成巨大的行业和生态,背后的根本驱动力是什么?

James Gosling:云计算的驱动力实际上与客户当前任务的实际规模有很大关系。过去几年以来,数字化转型已经全面掀起浪潮,而这波转型浪潮也凸显出新的事实,即我们还有更多的探索空间和机遇,例如,现在人们才刚刚开始探索真正的机器学习能做些什么,能够以越来越有趣且多样的方法处理大规模数据,开展数据分析,获取洞见并据此做出决策,而这一切既是客户需求,也为我们指明了接下来的前进方向。亚马逊云科技做为云科技领导者,引领着云科技的发展,改变着 IT 世界,切实解决了企业客户的诸多痛点。

作者:张雅文
来源:https://mp.weixin.qq.com/s/B4_YaVrnltm54aV4cW1XpA

收起阅读 »

这才是Yaml的语法精髓, 不要再只有字符串了

文章目录什么是YAML基本语法数据类型标量对象数组文本块显示指定类型引用单文件多配置什么是YAMLYAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Ma...
继续阅读 »

文章目录

  • 什么是YAML

  • 基本语法

  • 数据类型

    • 标量

    • 对象

    • 数组

  • 文本块

  • 显示指定类型

  • 引用

  • 单文件多配置

什么是YAML

YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。YAML的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。主要强度这种语音是以数据为中心,而不是以标记语音为重心,例如像xml语言就会使用大量的标记。

YAML是一个可读性高,易于理解,用来表达数据序列化的格式。它的语法和其他高级语言类似,并且可以简单表达清单(数组)、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件等。

YAML的配置文件后缀为 .yml,例如Springboot项目中使用到的配置文件 application.yml

基本语法

  • YAML使用可打印的Unicode字符,可使用UTF-8或UTF-16。

  • 数据结构采用键值对的形式,即 键名称: 值,注意冒号后面要有空格。

  • 每个清单(数组)成员以单行表示,并用短杠+空白(- )起始。或使用方括号([]),并用逗号+空白(, )分开成员。

  • 每个散列表的成员用冒号+空白(: )分开键值和内容。或使用大括号({ }),并用逗号+空白(, )分开。

  • 字符串值一般不使用引号,必要时可使用,使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。

  • 大小写敏感

  • 使用缩进表示层级关系,缩进不允许使用tab,只允许空格,因为有可能在不同系统下tab长度不一样

  • 缩进的空格数可以任意,只要相同层级的元素左对齐即可

  • 在单一文件中,可用连续三个连字号(—)区分多个文件。还有选择性的连续三个点号(…)用来表示文件结尾。

  • '#'表示注释,可以出现在一行中的任何位置,单行注释

  • 在使用逗号及冒号时,后面都必须接一个空白字符,所以可以在字符串或数值中自由加入分隔符号(例如:5,280或http://www.wikipedia.org)而不需要使用引号。

数据类型

  • 纯量(scalars):单个的、不可再分的值

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)

标量

标量是最基础的数据类型,不可再分的值,他们一般用于表示单个的变量,有以下七种:

  1. 字符串

  2. 布尔值

  3. 整数

  4. 浮点数

  5. Null

  6. 时间

  7. 日期

# 字符串
string.value: Hello!我是陈皮!
# 布尔值,true或false
boolean.value: true
boolean.value1: false
# 整数
int.value: 10
int.value1: 0b1010_0111_0100_1010_1110 # 二进制
# 浮点数
float.value: 3.14159
float.value1: 314159e-5 # 科学计数法
# Null,~代表null
null.value: ~
# 时间,时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
datetime.value: !!timestamp 2021-04-13T10:31:00+08:00
# 日期,日期必须使用ISO 8601格式,即yyyy-MM-dd
date.value: !!timestamp 2021-04-13

这样,我们就可以在程序中引入了,如下:

@RestController
@RequestMapping("demo")
public class PropConfig {
   
   @Value("${string.value}")
   private String stringValue;

   @Value("${boolean.value}")
   private boolean booleanValue;

   @Value("${boolean.value1}")
   private boolean booleanValue1;

   @Value("${int.value}")
   private int intValue;

   @Value("${int.value1}")
   private int intValue1;

   @Value("${float.value}")
   private float floatValue;

   @Value("${float.value1}")
   private float floatValue1;

   @Value("${null.value}")
   private String nullValue;

   @Value("${datetime.value}")
   private Date datetimeValue;

   @Value("${date.value}")
   private Date datevalue;
}

对象

我们知道单个变量可以用键值对,使用冒号结构表示 key: value,注意冒号后面要加一个空格。可以使用缩进层级的键值对表示一个对象,如下所示:

person:
 name: 陈皮
 age: 18
 man: true

然后在程序对这几个属性进行赋值到Person对象中,注意Person类要加get/set方法,不然属性会无法正确取到配置文件的值。使用@ConfigurationProperties注入对象,@value不能很好的解析复杂对象。

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "my.person")
@Getter
@Setter
public class Person {
   private String name;
   private int age;
   private boolean man;
}

当然也可以使用 key:{key1: value1, key2: value2, ...}的形式,如下:

person: {name: 陈皮, age: 18, man: true}

数组

可以用短横杆加空格 -开头的行组成数组的每一个元素,如下的address字段:

person:
 name: 陈皮
 age: 18
 man: true
 address:
   - 深圳
   - 北京
   - 广州

也可以使用中括号进行行内显示形式,如下:

person:
 name: 陈皮
 age: 18
 man: true
 address: [深圳, 北京, 广州]

在代码中引入方式如下:

package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/4/13
* @Version 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {
 
   
   
   private String name;
   private int age;
   private boolean man;
   private List<String> address;
}

如果数组字段的成员也是一个数组,可以使用嵌套的形式,如下:

person:
name: 陈皮
age: 18
man: true
address: [深圳, 北京, 广州]
twoArr:
-
- 2
- 3
- 1
-
- 10
- 12
- 30
package com.nobody;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@ConfigurationProperties(prefix = "person")
@Getter
@Setter
@ToString
public class Person {



private String name;
private int age;
private boolean man;
private List<String> address;
private List<List<Integer>> twoArr;
}

如果数组成员是一个对象,则用如下两种形式形式:

childs:
-
name: 小红
age: 10
-
name: 小王
age: 15
childs: [{name: 小红, age: 10}, {name: 小王, age: 15}]

文本块

如果你想引入多行的文本块,可以使用|符号,注意在冒号:|符号之间要有空格。

person:
name: |
Hello Java!!
I am fine!
Thanks! GoodBye!

它和加双引号的效果一样,双引号能转义特殊字符:

person:
name: "Hello Java!!\nI am fine!\nThanks! GoodBye!"

显示指定类型

有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。!单叹号通常是自定义类型,!!双叹号是内置类型,例如:

# 指定为字符串
string.value: !!str HelloWorld!
# !!timestamp指定为日期时间类型
datetime.value: !!timestamp 2021-04-13T02:31:00+08:00

内置的类型如下:

  • !!int:整数类型

  • !!float:浮点类型

  • !!bool:布尔类型

  • !!str:字符串类型

  • !!binary:二进制类型

  • !!timestamp:日期时间类型

  • !!null:空值

  • !!set:集合类型

  • !!omap,!!pairs:键值列表或对象列表

  • !!seq:序列

  • !!map:散列表类型

引用

引用会用到 &锚点符合和 *星号符号,&用来建立锚点,<< 表示合并到当前数据,* 用来引用锚点。

xiaohong: &xiaohong
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
<<: *xiaohong

上面最终相当于如下:

xiaohong:
name: 小红
age: 20

dept:
id: D15D8E4F6D68A4E88E
name: 小红
age: 20

还有一种文件内引用,引用已经定义好的变量,如下:

base.host: https://chenpi.com
add.person.url: ${base.host}/person/add

单文件多配置

可以在同一个文件中,实现多文档分区,即多配置。在一个yml文件中,通过 — 分隔多个不同配置,根据spring.profiles.active 的值来决定启用哪个配置

#公共配置
spring:
profiles:
active: pro # 指定使用哪个文档块
---
#开发环境配置
spring:
profiles: dev # profiles属性代表配置的名称

server:
port: 8080
---
#生产环境配置
spring:
profiles: pro

server:
port: 8081

作者:陈皮的JavaLib
来源:https://blog.csdn.net/chenlixiao007/article/details/115654824

收起阅读 »

掉了两根头发,可算是把volatile整明白了

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底...
继续阅读 »

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解

相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点

先说它的两个作用:

  • 保证变量在内存中对线程的可见性

  • 禁用指令重排

每个字都认识,凑在一起就麻了

这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile

关于可见性

不多bb,码来

public class VolatileTest {
   private static volatile int count = 0;
   
   private static void increase() {
   count++;
  }

   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           new Thread(() -> {
               for (int j = 0; j < 10000; j++) {
                   increase();
              }
          }).start();
      }
       // 所有线程累加完成后输出
       while (Thread.activeCount() > 2) Thread.yield();
       System.out.println(count);
  }
}

代码很好理解,开了十个线程对同一个共享变量count做累加,每个线程累加1w次

count我们已经用volatile修饰,已经保证了count对十个线程在内存中的可见性,按理说十个线程执行完毕count的值应该10w

然鹅,运行多次,结果都远小于期望值


是哪个环节出了问题?


你肯定听过一句话:volatile只保证可见性,不保证原子性

这句话就是答案,但是依旧很多人没搞懂其中的奥秘

说来话长我长话短说,简单来讲就是 count++这个操作不是原子的,它是分三步进行

  1. 从内存读取 count 的值

  2. 执行 count + 1

  3. 将 count 的新值写回

要彻底搞懂这个问题,我们得从字节码入手

下面是increase方法编译后的字节码


看不懂没关系,我们一行一行来看:

  1. GETSTATIC:读取 count 的当前值

  2. ICONST_1:将常量 1 加载到栈顶

  3. IADD:执行+1

  4. PUTSTATIC:写入count最新值

ICONST_1和IADD其实就是真正的++操作

关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖

懂我意思吗

所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全

就如上面的demo,稍加修改就能实现真正的线程安全

最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理)

private synchronized static void increase() {
   ++count;
}

run几下


这不就妥了嘛

到现在,对于以下两点你应该有了新的认知

  1. volatile保证变量在内存中对线程的可见性

  2. volatile只保证可见性,不保证原子性

关于指令重排

并发编程中,cpu自身和虚拟机为了提高执行效率,都会采用指令重排(在保证不影响结果的前提下,将某些代码乱序执行)

  1. 关于cpu:为了从分利用cpu,实际执行指令时会做优化;

  2. 关于虚拟机:在HotSpot vm中,为了提升执行效率,JIT(即时编译)模式也会做指令优化

指令重排在大部分场景下确实能提升执行效率,但有些场景对代码执行顺序是强依赖的,此时我们需要禁用指令重排,如下面这个场景


伪代码取自《深入理解Java虚拟机》:

其描述的场景是开发中常见配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。
试想一下,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile通过禁止指令重排则可以避免此类情况发生

禁用指令重排只需要将变量声明为volatile,是不是很神奇

我们来看看volatile是如何实现禁用指令重排的

也借用《深入理解Java虚拟机》的一个例子吧,比较好理解


这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值


可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话

最后,留一个能加深大家对volatile理解的问题,兄弟们好好思考下:

Java代码明明是从上往下依次执行,为什么会出现指令重排这个问题?

ok我话说完
————————————————
作者:负债程序猿
来源:https://blog.csdn.net/qq_33709582/article/details/122415754

收起阅读 »

什么样的问题应该使用动态规划

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:你对动态规划相关问题的套路和思想还没有完全...
继续阅读 »

说起动态规划,我不知道你有没有这样的困扰,在掌握了一些基础算法和数据结构之后,碰到一些较为复杂的问题还是无从下手,面试时自然也是胆战心惊。如果我说动态规划是个玄幻的问题其实也不为过。究其原因,我觉得可以归因于这样两点:

  • 你对动态规划相关问题的套路和思想还没有完全掌握;

  • 你没有系统地总结过究竟有哪些问题可以用动态规划解决。

知己知彼,你想把动态规划作为你的面试武器之一,就得足够了解它;而应对面试,总结、归类问题其实是个不错的选择,这在我们刷题的时候其实也能感觉得到。那么,我们就针对以上两点,系统地谈一谈究竟什么样的问题可以用动态规划来解。

一、动态规划是一种思想

动态规划算法,这种叫法我想你应该经常听说。嗯,从道理上讲这么叫我觉得也没错,首先动态规划它不是数据结构,这一点毋庸置疑,并且严格意义上来说它就是一种算法。但更加准确或者更加贴切的提法应该是说动态规划是一种思想。那算法和思想又有什么区别呢?

一般来说,我们都会把算法和数据结构放一起来讲,这是因为它们之间密切相关,而算法也往往是在特定数据结构的基础之上对解题方案的一种严谨的总结。

比如说,在一个乱序数组的基础上进行排序,这里的数据结构指的是什么呢?很显然是数组,而算法则是所谓的排序。至于排序算法,你可以考虑使用简单的冒泡排序或效率更高的快速排序方法等等来解决问题。

没错,你应该也感觉到了,算法是一种简单的经验总结和套路。那什么是思想呢?相较于算法,思想更多的是指导你我来解决问题。

比如说,在解决一个复杂问题的时候,我们可以先将问题简化,先解决简单的问题,再解决难的问题,那么这就是一种指导解决问题的思想。另外,我们常说的分治也是一种简单的思想,当然它在诸如归并排序或递归算法当中会常常被提及。

而动态规划就是这样一个指导我们解决问题的思想:你需要利用已经计算好的结果来推导你的计算,即大规模问题的结果是由小规模问题的结果运算得来的

总结一下:算法是一种经验总结,而思想则是用来指导我们解决问题的。既然动态规划是一种思想,那它实际上就是一个比较抽象的概念了,也很难和实际的问题关联起来。所以说,弄清楚什么样的问题可以使用动态规划来解就显得十分重要了。

二、动态规划问题的特点

动态规划作为运筹学上的一种最优化解题方法,在算法问题上已经得到广泛应用。接下来我们就来看一下动归问题所具备的一些特点。

2.1 最优解问题

除非你碰到的问题是简单到找出一个数组中最大的值这样,对这种问题来说,你可以对数组进行排序,然后取数组头或尾部的元素,如果觉得麻烦,你也可以直接遍历得到最值。不然的话,你就得考虑使用动态规划来解决这个问题了。这样的问题一般都会让你求最大子数组、求最长递增子数组、求最长递增子序列或求最长公共子串、子序列等等。

如果碰到求最值问题,我们可以使用下面的套路来解决问题:

  • 优先考虑使用贪心算法的可能性;

  • 然后是暴力递归进行穷举,针对数据规模不大的情况;

  • 如果上面两种都不适合,那么再选择动态规划。

可以看到,求解动态规划的核心问题其实就是穷举。当然了,动态规划问题也不会这么简单了事,我们还需要考虑待解决的问题是否存在重叠子问题、最优子结构等特性。

清楚了动态规划算法的特点,接下来我们就来看一下哪些问题适合用动态规划思想来解题。

1. 乘积最大子数组

给你一个整数数组 numbers,找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回该子数组的乘积。

示例1:
输入: [2,7,-2,4]
输出: 14
解释: 子数组 [2,7] 有最大乘积 14。


示例2:
输入: [-5,0,3,-1]
输出: 3
解释: 结果不能为 15, 因为 [-5,3,-1] 不是子数组,是子序列。

首先,很明显这个题目当中包含一个“最”字,使用动态规划求解的概率就很大。这个问题的目的就是从数组中寻找一个最大的连续区间,确保这个区间的乘积最大。由于每个连续区间可以划分成两个更小的连续区间,而且大的连续区间的结果是两个小连续区间的乘积,因此这个问题还是求解满足条件的最大值,同样可以进行问题分解,而且属于求最值问题。同时,这个问题与求最大连续子序列和比较相似,唯一的区别就是你需要在这个问题里考虑正负号的问题,其它就相同了。

对应实现代码:

class Solution {
public:
   int maxProduct(vector<int>& nums) {
       if(nums.empty()) return 0;

       int curMax = nums[0];
       int curMin = nums[0];
       int maxPro = nums[0];
       for(int i=1; i<nums.size(); i++){
           int temp = curMax;    // 因为curMax在下一行可能会被更新,所以保存下来
           curMax = max(max(curMax*nums[i], nums[i]), curMin*nums[i]);
           curMin = min(min(curMin*nums[i], nums[i]), temp*nums[i]);
           maxPro = max(curMax, maxPro);
       }
       return maxPro;
   }
};

2. 最长回文子串

问题:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例1:
输入: "babad"
输出: "bab"


示例2:
输入: "cbbd"
输出: "bb"

【回文串】是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。这个问题依然包含一个“最”字,同样由于求解的最长回文子串肯定包含一个更短的回文子串,因此我们依然可以使用动态规划来求解这个问题。

对应实现代码:

class Solution {
      public boolean isPalindrome(String s, int b, int e){//判断s[b...e]是否为回文字符串
      int i = b, j = e;
      while(i <= j){
          if(s.charAt(i) != s.charAt(j)) return false;
          ++i;
          --j;
      }
      return true;
  }
  public String longestPalindrome(String s) {
      if(s.length() <=1){
          return s;
      }
      int l = 1, j = 0, ll = 1;
      for(int i = 1; i < s.length(); ++i){
            //下面这个if语句就是用来维持循环不变式,即ll恒表示:以第i个字符为尾的最长回文子串的长度
            if(i - 1 - ll >= 0 && s.charAt(i) == s.charAt(i-1-ll)) ll += 2;
            else{
                while(true){//重新确定以i为边界,最长的回文字串长度。确认范围为从ll+1到1
                    if(ll == 0||isPalindrome(s,i-ll,i)){
                        ++ll;
                        break;
                    }
                    --ll;
                }
            }
            if(ll > l){//更新最长回文子串信息
              l = ll;
              j = i;
          }
      }
      return s.substring(j-l+1, j+1);//返回从j-l+1到j长度为l的子串
  }
}

3. 最长上升子序列

问题:给定一个无序的整数数组,找到其中最长上升子序列的长度。可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

示例:
输入: [10,9,2,5,3,7,66,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,66],它的长度是 4。

这个问题依然是一个最优解问题,假设我们要求一个长度为 5 的字符串中的上升自序列,我们只需要知道长度为 4 的字符串最长上升子序列是多长,就可以根据剩下的数字确定最后的结果。
对应实现代码:

class Solution {
   public int lengthOfLIS(int[] nums) {
       if(nums.length == 0) return 0;
       int[] dp = new int[nums.length];
       int res = 0;
       Arrays.fill(dp, 1);
       for(int i = 0; i < nums.length; i++) {
           for(int j = 0; j < i; j++) {
               if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
           }
           res = Math.max(res, dp[i]);
       }
       return res;
   }
}

2.2 求可行性

如果有这样一个问题,让你判断是否存在一条总和为 x 的路径(如果找到了,就是 True;如果找不到,自然就是 False),或者让你判断能否找到一条符合某种条件的路径,那么这类问题都可以归纳为求可行性问题,并且可以使用动态规划来解。

1. 凑零兑换问题

问题:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。

示例1:
输入: c1=1, c2=2, c3=5, c4=7, amount = 15
输出: 3
解释: 11 = 7 + 7 + 1。


示例2:
输入: c1=3, amount =7
输出: -1
解释: 3怎么也凑不到7这个值。

这个问题显而易见,如果不可能凑出我们需要的金额(即 amount),最后算法需要返回 -1,否则输出可能的硬币数量。这是一个典型的求可行性的动态规划问题。

对于示例代码:

class Solution {
   public int coinChange(int[] coins, int amount) {
       if(coins.length == 0)
           return -1;
       //声明一个amount+1长度的数组dp,代表各个价值的钱包,第0个钱包可以容纳的总价值为0,其它全部初始化为无穷大
       //dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数
       int[] dp = new int[amount+1];
       Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
       for (int coin : coins) {
           for (int j = coin; j <= amount; j++) {
               if(dp[j-coin] != Integer.MAX_VALUE) {
                   dp[j] = Math.min(dp[j], dp[j-coin]+1);
              }
          }
      }
       if(dp[amount] != Integer.MAX_VALUE)
           return dp[amount];
       return -1;
  }
}

2. 字符串交错组成问题

问题:给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

示例1:
输入: s1="aabcc",s2 ="dbbca",s3="aadbbcbcac"
输出: true
解释: 可以交错组成。


示例2:
输入: s1="aabcc",s2="dbbca",s3="aadbbbaccc"
输出: false
解释:无法交错组成。

这个问题稍微有点复杂,但是我们依然可以通过子问题的视角,首先求解 s1 中某个长度的子字符串是否由 s2 和 s3 的子字符串交错组成,直到求解整个 s1 的长度为止,也可以看成一个包含子问题的最值问题。
对应示例代码:

class Solution {
  public boolean isInterleave(String s1, String s2, String s3) {
      int length = s3.length();
      // 特殊情况处理
      if(s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return true;
      if(s1.isEmpty()) return s2.equals(s3);
      if(s2.isEmpty()) return s1.equals(s3);
      if(s1.length() + s2.length() != length) return false;

      int[][] dp = new int[s2.length()+1][s1.length()+1];
      // 边界赋值
      for(int i = 1;i < s1.length()+1;i++){
          if(s1.substring(0,i).equals(s3.substring(0,i))){
              dp[0][i] = 1;
          }
      }
      for(int i = 1;i < s2.length()+1;i++){
          if(s2.substring(0,i).equals(s3.substring(0,i))){
              dp[i][0] = 1;
          }
      }
       
      for(int i = 2;i <= length;i++){
          // 遍历 i 的所有组成(边界除外)
          for(int j = 1;j < i;j++){
              // 防止越界
              if(s1.length() >= j && i-j <= s2.length()){
                  if(s1.charAt(j-1) == s3.charAt(i-1) && dp[i-j][j-1] == 1){
                      dp[i-j][j] = 1;
                  }
              }
              // 防止越界
              if(s2.length() >= j && i-j <= s1.length()){
                  if(s2.charAt(j-1) == s3.charAt(i-1) && dp[j-1][i-j] == 1){
                      dp[j][i-j] = 1;
                  }
              }
          }
      }
      return dp[s2.length()][s1.length()]==1;
  }
}

2.3 求总数

除了求最值与可行性之外,求方案总数也是比较常见的一类动态规划问题。比如说给定一个数据结构和限定条件,让你计算出一个方案的所有可能的路径,那么这种问题就属于求方案总数的问题。

1. 硬币组合问题

问题:英国的英镑硬币有 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), 和 £2 (200p)。比如我们可以用以下方式来组成 2 英镑:1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p。问题是一共有多少种方式可以组成 n 英镑? 注意不能有重复,比如 1 英镑 +2 个 50P 和 50P+50P+1 英镑是一样的。

示例1:
输入: 2
输出: 73682

这个问题本质还是求满足条件的组合,只不过这里不需要求出具体的值或者说组合,只需要计算出组合的数量即可。

public class Main {
  public static void main(String[] args) throws Exception {
       
      Scanner sc = new Scanner(System.in);
      while (sc.hasNext()) {
           
          int n = sc.nextInt();
          int coin[] = { 1, 5, 10, 20, 50, 100 };
           
          // dp[i][j]表示用前i种硬币凑成j元的组合数
          long[][] dp = new long[7][n + 1];
           
          for (int i = 1; i <= n; i++) {
              dp[0][i] = 0; // 用0种硬币凑成i元的组合数为0
          }
           
          for (int i = 0; i <= 6; i++) {
              dp[i][0] = 1; // 用i种硬币凑成0元的组合数为1,所有硬币均为0个即可
          }
           
          for (int i = 1; i <= 6; i++) {
               
              for (int j = 1; j <= n; j++) {
                   
                  dp[i][j] = 0;
                  for (int k = 0; k <= j / coin[i - 1]; k++) {
                       
                      dp[i][j] += dp[i - 1][j - k * coin[i - 1]];
                  }
              }
          }
           
          System.out.print(dp[6][n]);
      }
      sc.close();
  }
}

2. 路径规划问题

问题:一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,共有多少路径?

示例1:
输入: 2 2
输出: 2


示例1:
输入: 3 3
输出: 6

这个问题还是一个求满足条件的组合数量的问题,只不过这里的组合变成了路径的组合。我们可以先求出长宽更小的网格中的所有路径,然后再在一个更大的网格内求解更多的组合。这和硬币组合的问题相比没有什么本质区别。

这里有一个规律或者说现象需要强调,那就是求方案总数的动态规划问题一般都指的是求“一个”方案的所有具体形式。如果是求“所有”方案的具体形式,那这种肯定不是动态规划问题,而是使用传统递归来遍历出所有方案的具体形式。

为什么这么说呢?因为你需要把所有情况枚举出来,大多情况下根本就没有重叠子问题给你优化。即便有,你也只能使用备忘录对遍历进行一个简单加速。但本质上,这类问题不是动态规划问题。

对应示例代码:

package com.qst.Tesst;

import java.util.Scanner;

public class Test12 {
  public static void main(String[] args) {
      Scanner scanner = new Scanner(System.in);
      while (scanner.hasNext()) {
          int x = scanner.nextInt();
          int y = scanner.nextInt();

          //设置路径
          long[][] path = new long[x + 1][y + 1];
          //设置领导数量
          int n = scanner.nextInt();

          //领导位置
          for (int i = 0; i < n; i++) {
              int a = scanner.nextInt();
              int b = scanner.nextInt();
              path[a][b] = -1;
          }

          for (int i = 0; i <= x; i++) {
              path[i][0] = 1;
          }
          for (int j = 0; j <= y; j++) {
              path[0][j] = 1;
          }

          for (int i = 1; i <= x; i++) {
              for (int j = 1; j <= y; j++) {
                  if (path[i][j] == -1) {
                      path[i][j] = 0;
                  } else {
                      path[i][j] = path[i - 1][j] + path[i][j - 1];
                  }

              }

          }
          System.out.println(path[x][y]);
      }
  }
}

三、 如何确认动态规划问题

从前面我所说来看,如果你碰到了求最值、求可行性或者是求方案总数的问题的话,那么这个问题就八九不离十了,你基本可以确定它就需要使用动态规划来解。但是,也有一些个别情况需要注意:

3.1 数据不可排序

假设我们有一个无序数列,希望求出这个数列中最大的两个数字之和。很多初学者刚刚学完动态规划会走火入魔到看到最优化问题就想用动态规划来求解,事实上,这个问题不是简单做一个排序或者做一个遍历就可以求解出来的。对于这种问题,我们应该先考虑一下能不能通过排序来简化问题,如果不能,才极有可能是动态规划问题。

最小的 k 个数

问题:输入整数数组 arr ,找出其中最小的 k 个数。例如,输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。

示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

我们发现虽然这个问题也是求“最”值,但其实只要通过排序就能解决,所以我们应该用排序、堆等算法或者数据结构就可以解决,而不应该用动态规划。

对应的示例代码:

public class Solution {
  public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
              int t;
      boolean flag;
      ArrayList result = new ArrayList();
      if(k>input.length){
          return result;
      }
      for(int i =0;i<input.length;i++){
          flag = true;
          for(int j = 0; j < input.length-i;j++)
              if(j<input.length-i-1){
                  if(input[j] > input[j+1]) {
                      t = input[j];
                      input[j] = input[j+1];
                      input[j+1] = t;
                      flag = false;
                  }
              }
          if(flag)break;
      }
      for(int i = 0; i < k;i++){
          result.add(input[i]);
      }
      return result;
  }
}

3.2 数据不可交换

还有一类问题,可以归类到我们总结的几类问题里去,但是不存在动态规划要求的重叠子问题(比如经典的八皇后问题),那么这类问题就无法通过动态规划求解。

全排列

问题:给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

这个问题虽然是求组合,但没有重叠子问题,更不存在最优化的要求,因此可以使用回溯方法处理。

对应的示例代码:

public class Main {
   public static void main(String[] args) {
       perm(new int[]{1,2,3},new Stack<>());
  }
   public static void perm(int[] array, Stack<Integer> stack) {
       if(array.length <= 0) {
           //进入了叶子节点,输出栈中内容
           System.out.println(stack);
      } else {
           for (int i = 0; i < array.length; i++) {
               //tmepArray是一个临时数组,用于就是Ri
               //eg:1,2,3的全排列,先取出1,那么这时tempArray中就是2,3
               int[] tempArray = new int[array.length-1];
               System.arraycopy(array,0,tempArray,0,i);
               System.arraycopy(array,i+1,tempArray,i,array.length-i-1);
               stack.push(array[i]);
               perm(tempArray,stack);
               stack.pop();
          }
      }
  }
}

总结一下,哪些问题可以使用动态规划呢,通常含有下面情况的一般都可以使用动态规划来解决:

  • 求最优解问题(最大值和最小值);

  • 求可行性(True 或 False);

  • 求方案总数;

  • 数据结构不可排序(Unsortable);

  • 算法不可使用交换(Non-swappable)。

如果面试题目出现这些特征,那么在 90% 的情况下你都能断言它就是一个动归问题。除此之外,还需要考虑这个问题是否包含重叠子问题与最优子结构,在这个基础之上你就可以 99% 断言它是否为动归问题,并且也顺势找到了大致的解题思路。

作者:xiangzhihong

来源:https://segmentfault.com/a/1190000041300090

收起阅读 »

10 个让人头疼的 bug

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界...
继续阅读 »

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。

作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界异常等。

如果你不小心看到同事的代码出现了我所描述的这些 bug 后,那你就把我这篇文章甩给他!!!你甩给他一篇文章,并让他关注了一波 cxuan,你会收获他在后面像是如获至宝并满眼崇拜大神的目光。

废话不多说,下面进入正题。

错误一:Array 转换成 ArrayList

Array 转换成 ArrayList 还能出错?这是哪个笨。。。。。。

等等,你先别着急说,先来看看是怎么回事。

如果要将数组转换为 ArrayList,我们一般的做法会是这样

List<String> list = Arrays.asList(arr);

Arrays.asList() 将返回一个 ArrayList,它是 Arrays 中的私有静态类,它不是 java.util.ArrayList 类。如下图所示


Arrays 内部的 ArrayList 只有 set、get、contains 等方法,但是没有能够像是 add 这种能够使其内部结构进行改变的方法,所以 Arrays 内部的 ArrayList 的大小是固定的。


如果要创建一个能够添加元素的 ArrayList ,你可以使用下面这种创建方式:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

因为 ArrayList 的构造方法是可以接收一个 Collection 集合的,所以这种创建方式是可行的。


错误二:检查数组是否包含某个值

检查数组中是否包含某个值,部分程序员经常会这么做:

Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);

这段代码虽然没错,但是有额外的性能损耗,正常情况下,不用将其再转换为 set,直接这么做就好了:

return Arrays.asList(arr).contains(targetValue);

或者使用下面这种方式(穷举法,循环判断)

for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;

上面第一段代码比第二段更具有可读性。

错误三:在 List 中循环删除元素

这个错误我相信很多小伙伴都知道了,在循环中删除元素是个禁忌,有段时间内我在审查代码的时候就喜欢看团队的其他小伙伴有没有犯这个错误。


说到底,为什么不能这么做(集合内删除元素)呢?且看下面代码

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);

这个输出结果你能想到么?是不是蠢蠢欲动想试一波了?

答案其实是 [b,d]

为什么只有两个值?我这不是循环输出的么?

其实,在列表内部,当你使用外部 remove 的时候,一旦 remove 一个元素后,其列表的内部结构会发生改变,一开始集合总容量是 4,remove 一个元素之后就会变为 3,然后再和 i 进行比较判断。。。。。。所以只能输出两个元素。

你可能知道使用迭代器是正确的 remove 元素的方式,你还可能知道 for-each 和 iterator 这种工作方式类似,所以你写下了如下代码

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));

for (String s : list) {
if (s.equals("a"))
list.remove(s);
}

然后你充满自信的 run xxx.main() 方法,结果。。。。。。ConcurrentModificationException

为啥呢?

那是因为使用 ArrayList 中外部 remove 元素,会造成其内部结构和游标的改变。

在阿里开发规范上,也有不要在 for-each 循环内对元素进行 remove/add 操作的说明。


所以大家要使用 List 进行元素的添加或者删除操作,一定要使用迭代器进行删除。也就是

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String s = iter.next();

if (s.equals("a")) {
iter.remove();
}
}

.next() 必须在 .remove() 之前调用。在 foreach 循环中,编译器会在删除元素的操作后调用 .next(),导致ConcurrentModificationException。

错误四:Hashtable 和 HashMap

这是一条算法方面的规约:按照算法的约定,Hashtable 是数据结构的名称,但是在 Java 中,数据结构的名称是 HashMap,Hashtable 和 HashMap 的主要区别之一就是 Hashtable 是同步的,所以很多时候你不需要 Hashtable ,而是使用 HashMap。

错误五:使用原始类型的集合

这是一条泛型方面的约束:

在 Java 中,原始类型和无界通配符类型很容易混合在一起。以 Set 为例,Set 是原始类型,而 Set<?> 是无界通配符类型。

比如下面使用原始类型 List 作为参数的代码:

public static void add(List list, Object o){
list.add(o);
}
public static void main(String[] args){
List<String> list = new ArrayList<String>();
add(list, 10);
String s = list.get(0);
}

这段代码会抛出 java.lang.ClassCastException 异常,为啥呢?


使用原始类型集合是比较危险的,因为原始类型会跳过泛型检查而且不安全,Set、Set<?> 和 Set<Object> 存在巨大的差异,而且泛型在使用中很容易造成类型擦除。

大家都知道,Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除

如在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM 看到的只是List,而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是 Java 的泛型与 C++ 模板机制实现方式之间的重要区别。

比如下面这段示例

public class Test {

  public static void main(String[] args) {

      ArrayList<String> list1 = new ArrayList<String>();
      list1.add("abc");

      ArrayList<Integer> list2 = new ArrayList<Integer>();
      list2.add(123);

      System.out.println(list1.getClass() == list2.getClass());
  }

}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型StringInteger都被擦除掉了,只剩下原始类型。

所以,最上面那段代码,把 10 添加到 Object 类型中是完全可以的,然而将 Object 类型的 "10" 转换为 String 类型就会抛出类型转换异常。

错误六:访问级别问题

我相信大部分开发在设计 class 或者成员变量的时候,都会简单粗暴的直接声明 public xxx,这是一种糟糕的设计,声明为 public 就很容易赤身裸体,这样对于类或者成员变量来说,都存在一定危险性。

错误七:ArrayList 和 LinkedList

哈哈哈,ArrayList 是我见过程序员使用频次最高的工具类,没有之一。


当开发人员不知道 ArrayList 和 LinkedList 的区别时,他们经常使用 ArrayList(其实实际上,就算知道他们的区别,他们也不用 LinkedList,因为这点性能不值一提),因为看起来 ArrayList 更熟悉。。。。。。

但是实际上,ArrayList 和 LinkedList 存在巨大的性能差异,简而言之,如果添加/删除操作大量且随机访问操作不是很多,则应首选 LinkedList。如果存在大量的访问操作,那么首选 ArrayList,但是 ArrayList 不适合进行大量的添加/删除操作。

错误八:可变和不可变

不可变对象有很多优点,比如简单、安全等。但是不可变对象需要为每个不同的值分配一个单独的对象,对象不具备复用性,如果这类对象过多可能会导致垃圾回收的成本很高。在可变和不可变之间进行选择时需要有一个平衡。

一般来说,可变对象用于避免产生过多的中间对象。比如你要连接大量字符串。如果你使用一个不可变的字符串,你会产生很多可以立即进行垃圾回收的对象。这会浪费 CPU 的时间和精力,使用可变对象是正确的解决方案(例如 StringBuilder)。如下代码所示:

String result="";
for(String s: arr){
result = result + s;
}

所以,正确选择可变对象还是不可变对象需要慎重抉择。

错误九:构造函数

首先看一段代码,分析为什么会编译不通过?


发生此编译错误是因为未定义默认 Super 的构造函数。在 Java 中,如果一个类没有定义构造函数,编译器会默认为该类插入一个默认的无参数构造函数。如果在 Super 类中定义了构造函数,在这种情况下 Super(String s),编译器将不会插入默认的无参数构造函数。这就是上面 Super 类的情况。

要想解决这个问题,只需要在 Super 中添加一个无参数的构造函数即可。

public Super(){
  System.out.println("Super");
}

错误十:到底是使用 "" 还是构造函数

考虑下面代码:

String x = "abc";
String y = new String("abc");

上面这两段代码有什么区别吗?

可能下面这段代码会给出你回答

String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True

String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True

这就是一个典型的内存分配问题。

后记

今天我给你汇总了一下 Java 开发中常见的 10 个错误,虽然比较简单,但是很容易忽视的问题,细节成就完美,看看你还会不会再犯了,如果再犯,嘿嘿嘿。


作者:cxuan
来源:https://mp.weixin.qq.com/s/uF0p8MGDhfvke4gdRv44iA

收起阅读 »

Ngnix之父突然离职,程序员巅峰一代落幕

当地时间 1 月 18 日,Nginx 公司副总裁兼总经理 Rob Whiteley 在 Nginx 官网发布了一篇「告别信」,正式宣告 Nginx 的作者和 Nginx Inc. 的联合创始人 Igor Sysoev 退出 Nginx 和 F5 Networ...
继续阅读 »

当地时间 1 月 18 日,Nginx 公司副总裁兼总经理 Rob Whiteley 在 Nginx 官网发布了一篇「告别信」,正式宣告 Nginx 的作者和 Nginx Inc. 的联合创始人 Igor Sysoev 退出 Nginx 和 F5 Networks。

此事很快登上 Hacker News 的热搜榜,有网友留言道:

我看过 Igor 参加某个会议的视频,他一说:“你好,我是 Nginx 的创建者 Igor Sysoev ”,观众席就会‘爆发’绵延不绝的掌声。他甚至不得不告诉他们“Come on guys, 你们还没听我的讲演呢。”。

不少开发者对 Igor 所做出的贡献表达了崇敬和感谢,也有网友感慨“巅峰一代落幕”。从 2002 年发展至今日,Nginx 已经成为全球最受欢迎的 Web 服务器。据 W3Techs 统计,截至 2022 年 1 月上旬,Nginx 占据了全球 Web 服务器市场 33% 的份额。排在第二位的是 Apache,份额为 31%。

pic_d710133a.png

一直以来,Nginx 常被拿来跟 Apache 对比,也有观点认为,Nginx 和 Apache 不算真正意义上的竞争者,很多地方会同时使用两者。但无论如何,Igor 和 Nginx 的成功确实鼓舞了不少开源人。

作为一名开源开发者和商业 OSS 初创公司创始人,Nginx 给了我很大的挑战现状的信心。Apache 是如此受人尊敬,以至于你会认为可以改进它的想法是很疯狂的,但他( Igor )做到了,这对我产生了真正的影响。——yesimahuman

Igor 早期曾在采访中分享对于开源和商业产品找平衡的观点,他表示不想创建单独的商业产品,而是希望对 Nginx 的主要开源产品进行商业扩展,社区想要的新功能将出现在其中。商业扩展更多的是有助于处理数千个实例、添加扩展性能监控、托管、云和 CDN 基础设施的附加功能等。

很多客户会说愿意付钱让 Igor 增加他们所需要的新功能,而 Igor 等人收集此类请求后会将其与从用户社区收到的需求进行比较,并寻找交叉点——“如果我们意识到每个人都需要某些功能,而不仅仅是某些公司,我们会将这些功能包含在开源版本中。我们从中了解我们可以销售什么,而不会惹恼开源产品的支持者,也不会损害整个项目的信誉。”

Nginx 如今归属于 F5 Networks。2019 年 3 月,F5 Networks 宣布将以 6.7 亿美元收购 Nginx,根据交易条款,Nginx 品牌被保留,而 Igor 和 合伙人 Konovalov 作为 F5 的一部分继续致力于该项目。但这笔交易很快就触发了利益纷争,同年 12 月,Igor 陷入版权纠纷,前东家 Rambler 集团对 NGINX Inc. 提出了侵犯版权的诉讼,声称拥有 Nginx Web 服务器代码的全部所有权,但 Igor 辩称是在业余时间开发了 Nginx。

此事随即引发热议,业余项目究竟属于开发者个人、还是属于开发者所在的企业,目前没有明确的统一的法律来判定。2020 年 4 月,Rambler 驳回针对 Nginx 的刑事诉讼。但 Rambler 并未就此停下,只是不再是以刑事诉讼的方式,而是通过民事法院,并于 2020 年 6 月初宣布授权旗下 Lynwood Investments 在美国对 F5 Networks、Igor 本人发起民事诉讼,要求索赔 7.5 亿美元。6 月末,俄罗斯内政部因缺乏犯罪记录证据,结案了有关 Nginx 版权的案件。

告别信有提到 Igor 从 Nginx 离职后将从事个人项目,目前我们尚不清楚他具体会涉及哪些项目。

以下是「告别信」全文:

挥别 Igor:
感谢你为 Nginx 付出的一切

怀着深深的感激之情,我们今天宣布,Nginx 的作者和 Nginx 公司联合创始人 Igor Sysoev 选择退出 Nginx 和 F5,以便花更多的时间与他的朋友和家人在一起,并追求个人项目。

2002 年的春天,Igor Sysoev 开始开发 Nginx。互联网的早期飞速发展让他萌生出一个念头:用一套全新架构改进网络流量的处理方式,帮助高流量网站从容应对数万个并发连接,并将照片、视频等各类可能严重拖慢页面加载速度的内容统统塞进缓存。

二十年过去,Igor 写下的代码已经在为世界上大部分网站提供支持。除了直接使用外,也被作为 Cloudflare、OpenResty、Tengine 等流行服务器的底层软件。很多人认为,Igor 最初的梦想就是把 Web 塑造成如今的样貌。Igor 所秉持的意志与价值观则汇聚成 Nginx 公司,结合开源与技术社区之力成就高透明度、质量卓越的代码,最终转化为客户喜闻乐见的商业产品。

但其中的平衡往往很难把握。Igor 之所以受到开发者、企业客户以及 Nginx 工程师们的高度赞扬,依靠的正是他谦逊的内心、不断探索的激情以及在开发工作中勇攀高峰的意志。

Igor 的成长与 Nginx 的诞生

Igor 的人生起点不高。他出生于苏联时期的一个哈萨克斯坦小镇,父亲是一名军官。一岁时,他们全家迁往首都阿拉木图。Igor 从小痴迷计算机,1980 年代中期就在 Yamaha MSX 上写下了人生第一行代码。而伴随着早期互联网产业的快速发展,Igor 也从著名的鲍曼莫斯科国立技术大学计算机科学系顺利毕业。

Igor 毕业后先找了份系统管理员工作,但写代码的好习惯一直没有丢下。1999 年,他用汇编语言开发出自己的第一个程序,这款反病毒软件能抵御当时最常见的十种计算机病毒。Igor 免费开放了程序的二进制文件,这款工具也在俄罗斯国内风靡一时。之后,敏感的他发觉 Apache HTTP 服务器的连接处理方式过于原始,根本无法满足不断发展的万维网需求。于是他决定开展相关研究,这也正是后来 Nginx 项目的雏形。

彼时,Igor 将目光投向了 C10k 问题,即如何在单一服务器上处理 10000 个并发连接。此外,他还希望让自己的 Web 服务器更快、更高效地处理照片或者音乐文件等极占传输带宽的元素。在获得俄罗斯国内外多家公司的肯定和采用之后,Igor 于 2004 年 10 月 4 日(即苏联发射全球首颗人造卫星「斯普特尼克」号的四十七周年纪念日)对这个名为 Nginx 的项目进行了许可开源。

七年来,Igor 一直是唯一的开发者。他独力写下数十万行代码,并把 Nginx 从简单的 Web 服务器加反向代理工具,扩展成一把能满足各类 Web 应用与服务需求的“瑞士军刀”。随着项目发展,负载均衡、缓存、安全和内容加速等关键功能也在他的指尖一一成形。

没有队伍的 Igor 当时自然没精力宣传项目,甚至连说明文档也不够完备。但 Nginx 仍然凭借着出色的表现迅速占领了市场。更神奇的是,新用户发现就算没有全面的使用指南、自己仍然能轻松玩转 Nginx,于是项目就在口口相传之下普及开来。越来越多的开发者和系统管理员利用 Nginx 解决自己面对的现实问题,提升网站响应速度。对于 Igor 的贡献,我们已经不需要刻意赞美或者宣扬,他的代码已经说明了一切。

Nginx 开启商业化之路,但开源定位永不动摇

2011 年,Igor 与 Maxim Konovalov、Andrew Alexeev 两位联合创始人共同成立了 Nginx 公司,希望借众人之力加快项目开发速度。但 Igor 也很清楚,从这一刻起他和团队得想办法赚钱了。不过他们坚持发布 Nginx 完整开源版本、恪守开源许可的承诺不会动摇。君子一诺值千金,自公司成立以来,Igor 引领 Nginx 通过 140 多个版本不断完善自我,始终以开源姿态为全球数亿网站提供支持。

pic_93439c65.png

奔波在为 Nginx 公司筹集风险投资的路上——(右起)Igor、公司 CEO Gus Robertson、联合创始人 Andrew Alexeev 以及 Maxim Konovalov

2011 年的时候,以专有模块的形式向商业版本中添加新功能的想法还属于开时代之先河。但如今,很多开源后起之秀已经可以站在巨人的肩膀上享受这种商业模式。在商业版 Nginx Plus 于 2013 年首次推出时,市场立刻抱以热烈欢迎。四年之后,Nginx 已经拥有超过 1000 家付费客户和数千万收入,Nginx 开源项目与技术社区的规模也在同步发展壮大。截至 2019 年底,Nginx 已经在为全球超过 4.75 亿个网站提供支持;到 2021 年,Nginx 正式成为世界上应用范围最广的 Web 服务器方案。

着眼于未来需求,Igor 还一路打造出多个 Nginx 相关项目,包括 Nginx JavaScript(njs)与 Nginx Unit。他还为 sendfile(2)系统调用设计了全新实现,将其整合到开源 FreeBSD 操作系统当中。随着 Nginx 工程师队伍的壮大和 Nginx 公司正式加入 F5,Igor 一直是团队背后稳健的领导者,保证 Nginx 始终方向明确、斗志坚定。

接过 Igor 手中的旗帜

今天,Igor 希望退居幕后享受生活,独余我们继续前行。但 Igor 的精神和他一路塑造的文化不会消失。伟大的企业、产品和项目中,创始人的 DNA 是永恒不变的。我们对于产品、社区、透明度、开源和创新的态度皆继承自 Igor,我们也将继续在 Maxim 和 Nginx 领导团队的指引下接过这面旗帜、发挥这份传统。

Igor 在 Nginx 与 F5 时代的奋斗与付出凝结成了我们今天所看到的项目代码,多年以来一直默默支撑起整个互联网世界。时间会考验我们、鞭策我们,证明我们能否像 Igor 那样创造出历久弥新、影响深远的产品。这当然是一条极高的标准,但 Igor 也用实际行动为我们指明了达成目标的方法。感恩多年来的指引与教导,Igor,祝你在人生的新阶段写下新的传奇故事。

来源:https://mp.weixin.qq.com/s/GANdlnXt1_vuUm3j97Njg

收起阅读 »