注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Java系列 | 远程热部署在美团的落地实践

1 前言1.1 什么是热部署所谓热部署,就是在应用正在运行时升级软件,却不需要重新启动应用。对于Java应用程序来说,热部署就是在运行时更新Java类文件,同时触发Spring以及其他常用第三方框架的一系列重新加载的过程。在这个过程中不需要重新启动,并且修改的...
继续阅读 »

1 前言

1.1 什么是热部署

所谓热部署,就是在应用正在运行时升级软件,却不需要重新启动应用。对于Java应用程序来说,热部署就是在运行时更新Java类文件,同时触发Spring以及其他常用第三方框架的一系列重新加载的过程。在这个过程中不需要重新启动,并且修改的代码实时生效,好比是战斗机在空中完成加油,不需要战斗机熄火降落,一系列操作都在“运行”状态来完成。

1.2 为什么我们需要热部署

据了解,美团内部很多工程师每天本地重启服务高达5~12次,单次大概3~8分钟,每天向Cargo(美团内部测试环境管理工具)部署3~5次,单次时长20~45分钟,部署频繁频次高、耗时长,严重影响了系统上线的效率。而插件提供的本地和远程热部署功能,可让将代码变更“秒级”生效。一般而言,开发者日常工作主要分为开发自测和联调两个场景,下面将分别介绍热部署在每个场景中发挥的作用。


1.2.1 开发自测场景

一般来讲,在用插件之前,开发者修改完代码还需等待3~8分钟启动时间,然后手动构造请求或协调上游发请求,耗时且费力。在使用完热部署插件后,修改完代码可以一键增量部署,让变更“秒级”生效,能够做到快速自测。而对于那些无法本地启动项目,也可以通过远程热部署功能使代码变更“秒级”生效。


1.2.2 联调场景

通常情况下,在使用插件之前,开发者修改代码经过20~35分钟的漫长部署,需要联系上游联调开发者发起请求,一直要等到远程服务器查看日志,才能确认代码生效。在使用热部署插件之后,开发者修改代码远程热部署能够秒级(2~10s)生效,开发者直接发起服务调用,可以节省大量的碎片化时间(热部署插件还具备流量回放、远程调用、远程反编译等功能,可配合进行使用)。


所以,热部署插件希望解决的痛点是:在可控的条件内,帮助开发者减少频繁编译部署的次数,节省碎片化的时间。最终为开发者每天节约出一定量的编码时间

1.3 热部署难在哪

为什么业界目前没有好用的开源工具?因为热部署不等同于热重启,像Tomcat或者Spring Boot DevTools此类热重启模式需要重新加载项目,性能较差。增量热部署难度较大,需要兼容常用的中间件版本,需要深入启动销毁加载流程。以美团为例,我们需要对JPDA(Java Platform Debugger Architecture)、Java Agent、ASM字节码增强、Classloader、Spring框架、Spring Boot框架、MyBatis框架、Mtthrift(美团RPC框架)、Zebra(美团持久层框架)、Pigeon(美团RPC框架),MDP(美团快速开发框架)、XFrame(美团快速开发脚手架)、Crane(美团分布式任务调度框架)等众多框架和技术原理深入了解才能做到全面的兼容和支持。另外,还需要IDEA插件开发能力,形成整体的产品解决方案闭环,美团的热部署插件Sonic正是在这种背景下应运而生。


1.4 Sonic可以做什么

Sonic是美团内部研发设计的一款IDEA插件,旨在通过低代码开发辅助远程/本地热部署,解决Coding、单测编写执行、自测联调等阶段的效率问题,提高开发者的编码产出效率。数据统计表明,开发者日常大概有35%时间用于编码的产出。如果想提高研发效率,要么扩大编码产出的时间占比,要么提高编码阶段的产出效率,而Sonic则聚焦提高编码阶段的产出效率。

目前,使用Sonic热部署可以解决大部分代码重复构建的问题。Sonic可以使用户在本地编写代码一键部署到远程环境,修改代码、部署、联调请求、查看日志,循环反复。如果不考虑代码修改时间,通常一个循环需要20~35分钟,而使用Sonic可以把整个时长缩短至5~10秒,而且能够给开发者带来高效沉浸式的开发体验。在实际编码工作中,多文件修改是家常便饭,Sonic对多文件的热部署能力尤为突出,它可以通过依赖分析等手段来对多文件批量进行远程热部署,并且支持Spring Bean Class、普通Class、Spring XML、MyBatis XML等多类型文件混合热部署。

那么跟业界现有的产品相比,Sonic有哪些优劣势呢?下面我们尝试给出几种产品的对比,仅供大家参考:

特性JRebelSpring Boot DevToolsIDEA热加载Tomcat热加载Spring LoaderSonic
远程Debug基于Debug协议修改
修改方法体内容✅效率低✅效率低
新增方法体✅效率低✅效率低
Jar包变更✅效率低✅效率低
Spring MVC✅效率低✅效率低
多文件热部署✅效率低✅效率低
新增泛型方法✅效率低✅效率低
新增非静态字段✅效率低✅效率低
新增静态字段✅效率低✅效率低
新增修改继承类✅效率低✅效率低
新增修改接口方法✅效率低✅效率低
新增修改匿名内部类✅效率低✅效率低
增加修改静态块✅效率低✅效率低
FastJson✅效率低✅效率低
Cglib✅效率低✅效率低
MyBatis Annotation✅效率低✅效率低
MyBatis XML✅效率低✅效率低
Gson✅效率低✅效率低
Jackson✅效率低✅效率低
Jdk代理✅效率低✅效率低
Log4j✅效率低✅效率低
Slf4J✅效率低✅效率低
Logback✅效率低✅效率低
Spring Tx✅效率低✅效率低
Spring 新增Xml✅效率低✅效率低
Spring Bean✅效率低✅效率低
Spring Boot✅效率低✅效率低
Spring Validator✅效率低✅效率低
远程热部署配置繁琐
IDEA插件集成

上表未把Sofa-Ark、Osgi、Arthas列举,此类属于插件化、模块化应用框架,以及Java在线诊断工具,核心能力非热部署。值得注意的是,Spring Boot DevTools只能应用在Spring Boot项目中,并且它不是增量热部署,而是通过Classloader迭代的方式重启项目,对大项目而言,性能上是无法接受的。虽然,JRebel支持三方插件较多,生态庞大,但是对于国产的插件不支持,例如FastJson等,同时它还存在远程热部署配置局限,对于公司内部的中间件需要个性化开发,并且是商业软件,整体的使用成本较高。

1.5 Sonic远程热部署落地推广的实践经验

相信大家都知道,对于技术产品的推广,尤其是开发、测试阶段使用的产品,由于远离线上环境,推动力、执行力、产品功能闭环能否做好,是决定着该产品是否能在企业内部落地并得到大多数人认可的重要的一环。此外,因为很多开发者在开发、测试阶段已逐渐形成了“固化动作”,如何改变这些用户的行为,让他们拥抱新产品,也是Sonic面临的艰巨挑战之一。我们从主动沟通、零成本(或极低成本)快速接入、自动化脚本,以及产品自动诊断、收集反馈等方向出发,践行出了四条原则。


2 整体设计方案

2.1 Sonic结构

Sonic插件由4大部分组成,包括脚本端、插件端、Agent端,以及Sonic服务端。脚本端负责自动化构建Sonic启动参数、服务启动等集成工作;IDEA插件端集成环境为开发者提供更便捷的热部署服务;Agent端随项目启动负责热部署的功能实现;服务端则负责收集热部署信息、失败上报等统计工作。如下图所示:


2.2 走进Agent

2.2.1 Instrumentation类常用API

public interface Instrumentation {

   //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
   void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

   //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,
   //如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
   //对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
   void addTransformer(ClassFileTransformer transformer);

   //删除一个类转换器
   boolean removeTransformer(ClassFileTransformer transformer);
   
   //是否允许对class retransform
   boolean isRetransformClassesSupported();

   //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
   void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
 
   //是否允许对class重新定义
   boolean isRedefineClassesSupported();

   //此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
   //在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
   //该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
   void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;

   //获取已经被JVM加载的class,有className可能重复(可能存在多个classloader)
   @SuppressWarnings("rawtypes")
   Class[] getAllLoadedClasses();
}

2.2.2 Instrument简介

Instrument的底层实现依赖于JVMTI(JVM Tool Interface),它是JVM暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果存在),这些接口可以供开发者去扩展自己的逻辑。

JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(Agent On Load)、代理通过Attach形式加载(Agent On Attach)和代理卸载(Agent On Unload)功能的动态库。而Instrument Agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是专门为Java语言编写的插桩服务提供支持的代理。

2.2.3 启动时和运行时加载Instrument Agent过程


2.3 那些年JVM和HotSwap之间的“相爱相杀”

围绕着Method Body的HotSwap JVM一直在进行改进。从1.4版本开始,JPDA引入HotSwap机制(JPDA Enhancements),实现Debug时的Method Body的动态性。大家可参考文档:enhancements1.4
1.5版本开始通过JVMTI实现的java.lang.instrument(Java Platform SE 8)的Premain方式,实现Agent方式的动态性(JVM启动时指定Agent)。大家可参考文档:package-summary

1.6版本又增加Agentmain方式,实现运行时动态性(通过The Attach API 绑定到具体VM)。大家可参考文档:package-summary 。基本实现是通过JVMTI的retransformClass/redefineClass进行method、body级的字节码更新,ASM、CGLib基本都是围绕这些在做动态性。但是针对Class的HotSwap一直没有动作(比如Class添加method、添加field、修改继承关系等等),为什么会这样呢?因为复杂度过高,且没有很高的回报。

2.4 Sonic如何解决Instrumentation的局限性

由于JVM限制,JDK 7和JDK 8都不允许改类结构,比如新增字段,新增方法和修改类的父类等,这对于Spring项目来说是致命的。比如开发同学想修改一个Spring Bean,新增一个@Autowired字段,此类场景在实际应用时很多,所以Sonic对此类场景的支持必不可少。

那么,具体是如何做到的呢?这里要提一下“大名鼎鼎”的Dcevm。Dcevm(DynamicCode Evolution Virtual Machine)是Java Hostspot的补丁(严格上来说是修改),允许(并非无限制)在运行环境下修改加载的类文件。当前虚拟机只允许修改方法体(Method,Body),而Decvm可以增加、删除类属性、方法,甚至改变一个类的父类,Dcevm是一个开源项目,遵从GPL 2.0协议。更多关于Dcevm的介绍,大家可以参考:Wuerthinger10a以及GitHub Decvm

值得一提的是,在美团内部,针对Dcevm的安装,Sonic已经打通HULK,集成发布镜像即可完成(本地热部署可结合插件功能实现一键安装热部署环境)。

3 Sonic热部署技术解析

3.1 Sonic整体架构模型

上一章节我们主要介绍了Sonic的组成。下图详细介绍了Sonic在运行期间各个组成部分的工作职责,由它们形成一整套完备的技术产品落地闭环方案:


3.2 Sonic功能流转

Sonic通过NIO监听本地文件变更,触发文件变更事件,例如Class新增、Class修改、Spring Bean重载等事件流程。下图展示了一次热部署单个文件的生命周期:


3.3 文件监听

Sonic首先会在本地和远程预定义两个目录,/var/tmp/sonic/extraClasspath/var/tmp/sonic/classes。extraClasspath为Sonic自定义的拓展Classpath URL,classes为Sonic监听的目录,当有文件变更时,通过IDEA插件来部署到远程/本地,触发Agent的监听目录,来继续下面的热加载逻辑:


为什么Sonic不直接替换用户ClassPath下面的资源文件呢?因为考虑到业务方WAR包的API项目、Spring Boot、Tomcat项目、Jetty项目等,都是以JAR包来启动的,这样是无法直接修改用户的Class文件的。即使是用户项目可以修改,直接操作用户的Class,也会带来一系列的安全问题。

所以,Sonic采用拓展ClassPath URL路径来实现文件的修改和新增。并且存在这么一种场景,多个业务侧的项目引入相同的JAR包,在JAR里面配置MyBatis的XML和注解。在此类情况下,Sonic没有办法直接来修改JAR包中源文件,通过拓展路径的方式可以不需要关注JAR包,来修改JAR包中某一文件和XML。同理,采用此类方法可以进行整个JAR包的热替换。下面我们简单介绍一下Sonic的核心监听器,如下图所示:


3.4 JVM Class Reload

JVM的字节码批量重载逻辑,通过新的字节码二进制流和旧的Class对象生成ClassDefinition定义,instrumentation.redefineClasses(definitions),来触发JVM重载,重载过后将触发初始化时Spring插件注册的Transfrom。接下来,我们简单讲解一下Spring是怎么重载的。

新增class Sonic如何保证可以加载到Classloader上下文中?由于项目在远程执行,所以运行环境复杂,有可能是JAR包方式启动(Spring Boot),也有可能是普通项目,也有可能是War Web项目,针对此类情况Sonic做了一层Classloader URL拓展。


User ClassLoader是框架自定义的ClassLoader统称,例如Jetty项目是WebAppclassLoader。其中Urlclasspath为当前项目的lib文件件下,例如Spring Boot项目也是从当前项目BOOT-INF/lib/路径中加载CLass等等,不同框架的自定义位置稍有不同。所以针对此类情况,Agent必须拿到用户的自定义Classloader,如果是常规方式启动的,比如普通Spring XML项目,借助Plus(美团内部服务发布平台)发布,此类没有自定义Classloader,是默认AppClassLoader,所以Agent在用户项目启动过程中,借助字节码增强的方式来获取到真正的用户Classloader。


找到用户使用的子Classloader之后,通过反射的方式来获取Classloader中的元素Classpath,其中ClassPath中的URL就是当前项目加载Class时需要的所有运行时Class环境,并且包括三方的JAR包依赖等。

Sonic获取到URL数组,把Sonic自定义的拓展Classpath目录加入到URL数组首位,这样当有新增Class时,Sonic只需要将Class文件复制到拓展Classpath对应的包目录下面即可,当有其他Bean依赖新增的Class时,会从当前目录下面查找类文件。

为什么不直接对Appclassloader进行加强?而是对框架的自定义Classloader进行加强?


考虑这样一个场景,框架自定义类加载器中有ClassA,此时用户新增ClassB需要热加载,B Class里面有A的引用关系,如果增强AppClassLoader,初始化B实例时ClassLoader。loadclass首先从UserClassLoader开始加载ClassB的字节码,依靠双亲委派原则,B被Appclassloader加载,因为B依赖类A,所以当前AppClassLoader加载B一定是加载不到的,此时会抛出ClassNotFoundException异常。所以对类加载器拓展,一定要拓展最上层的类加载器,这样才会达到使用者想要的效果。

3.5 Spring Bean重载

Spring Bean Reload过程中,Bean的销毁和重启流程,主要内容如下图展示:


首先当修改Java Class D时,通过Spring ClasspathScan扫描校验当前修改的Bean是否Sprin Bean(注解校验),然后触发销毁流程(BeanDefinitionRegistry.removeBeanDefinition),此方法会将当前Spring上下文中的Bean D和依赖Spring Bean D的Bean C一并销毁,但是作用范围仅仅在当前Spring上下文。如果C被子上下文中的Bean B依赖,就无法更新子上下文中的依赖关系,当有系统请求时,Bean B中关联的Bean C还是热部署之前的对象,所以热部署失败。

因此,在Spring初始化过程中,需要维护父子上下文的对应关系,当子上下文变时若变更范围涉及到Bean B时,需要重新更新子上下文中的依赖关系,当有多上下文关联时需要维护多上下文环境,且当前上下文环境入口需要Reload。这里的入口是指:Spring MVC Controller、Mthrift和Pigeon,对不同的流量入口,采用不同的Reload策略。RPC框架入口主要操作为解绑注册中心、重新注册、重新加载启动流程等等,对Spring MVC Controller,主要是解绑和注册URL Mappping来实现流量入口类的变化切换。

3.6 Spring XML重载

当用户修改/新增Spring XML时,需要对XML中所有Bean进行重载。


重新Reload之后,将Spring销毁后重启。需要注意的是:XML修改方式改动较大,可能涉及到全局的AOP的配置以及前置和后置处理器相关的内容,影响范围为全局,所以目前只放开普通的XML Bean标签的新增/修改,其他能力酌情逐步放开。

3.7 MyBatis 热部署

Spring MyBatis热部署的主要处理流程是在启动期间获取所有Configuration路径,并维护它和Spring Context的对应关系,在热部署Class、XML时去匹配Configuration,从而重新加载Configuration以达到热部署的目的。


4 总结

4.1 热部署功能一览

上一章节主要讲述了Spring Bean、Spring MVC、MyBatis的重载流程,Sonic还支持其它常用的开发框架,丰富的框架支持和兼容能力是Sonic的基石,下面列举一些Sonic支持的常用的第三方框架:


截止目前,Sonic已经支持绝大部分常用第三方框架的热加载,常规业务开发几乎无需重启服务。并且在美团内部的成功率已经高达99.9%以上,真正地让热部署来代替常规部署构建成为一种可能。

4.2 IDE插件集成

Sonic也提供了功能强大的IDEA插件,让用户进行沉浸式开发,远程热部署也变得更加便利。


4.3 推广使用情况

截止到发稿时,Sonic在美团使用人数3000+,应用项目数量2000+。该项目还获得了美团内部2020年下半年到家研发平台“最佳效率团队”奖。

5 作者简介

凯哥、占峰、李晗、龚炎、程骁、玉龙等,均来自美团/到家研发平台。

来源:tech.meituan.com/2022/03/17/java-hotswap-sonic.html

收起阅读 »

2022年年终杂谈,如何成为出色的工程师

重新认识自己我一直对 NodeJS 工具方向比较感兴趣,今年终于有机会在公司内,开始工具项目的研发,调研并设计了整体架构、独立负责开发工作。去年年末时,我刚刚来到字节半年,其实心态还没有完全转换过来,之前在创业公司,涉及了很多不同端的开发工作。所以我对自己的定...
继续阅读 »

重新认识自己

我一直对 NodeJS 工具方向比较感兴趣,今年终于有机会在公司内,开始工具项目的研发,调研并设计了整体架构、独立负责开发工作。

去年年末时,我刚刚来到字节半年,其实心态还没有完全转换过来,之前在创业公司,涉及了很多不同端的开发工作。所以我对自己的定位,还处于一个支持业务开发的状态,对技术渴望度足够,但对于技术路线有些许迷茫。只能看到一些比较聚焦度比较高的技术名词,例如 WASMWebGL,会对这些技术存在追捧,却并没有做到脚踏实地。

相比去年的我,今年我对一些概念又有了许多新的见解,到底什么样才是出色的工程师?

当今国内工程师的问题

按照国内对工程师的区分,在各厂招聘列表中经常出现的是这几类,前端/后端/客户端工程师。

在我的工作中,经常会接触各端 SDK 的开发的同学,接触过程中,有时感觉到大家会存在一些 gap,也就是说 SDK 同学想去做一些事情,但是在他的角度他很明白底层逻辑,但对于其他端(前/后)同学,他们对底层原理其实并不了解。这就造成开发前,需要很长的时间去对齐功能的逻辑,合作同学也很难理解需求的意义与价值。

如果想摆脱这些困惑,那我认为你需要成为一名「全栈工程师」,其实说是全栈,不如是「软件工程师」,作为一名工程师,需要拥有一些闭环整体开发流程的能力。

例如,以 SDK 开发的角度来说,对于 SDK 同学,实际上最终只负责到上报这个动作,至于之后数据的流向,以及数据清洗,并不在掌控的范畴之内。这虽然降低了 SDK 侧上手的门槛,但并不利于长期的维护。

假如你是一个软件工程师,对于以下流程,你都了如指掌:

数据上报 -> 清洗 -> 存储 -> 消费

那你对于系统的整体认知就会提升,从优化上,可以给出更好的建议;在排查问题时,也可以更快速的定位问题。

何为工程师?

下面我详细谈一谈,如何成为一个有宏观视角的工程师。

首先,我目前的关注点主要在前端方向,如果你有仔细观察,你可以看到最终大部分比较厉害的前端,都是具有一些全栈能力的人。

对于服务端层面,我的建议是把 Golang 学好,这是一个还不错的方向。一个技术栈,如果有很多人关注聚焦,广泛地提出问题,那它的发展前景一定是不错的,起码不会垮掉,也就是说,开发生态是健康的。

对于前端层面,如果想去把前端学的很深入的话,那么前端工具以及工程化,必不可少。

在这一年内,我扩展一些自己原本不是很擅长的领域:

  • 产品

    • 竞品调研

    • PRD 撰写

  • 服务端方向

    • MySQL

    • Rust

    • Golang

  • 前端方向

    • 单测/e2e:Jest、@testing-library/react、Cypress

    • 工程化:Rush.js、Pnpm、Webpack、TypeScript

    • 工具:Babel、CLI 相关的 npm 包工具

    • 插件:Chrome、VSCode

  • 设计

    • Figma 学习

  • 英语学习

除了开发角色,一个合格的工程师,还应该掌握技术方案设计的能力,这样可以将整体的开发流程闭环。也就是说一个人扮演,调研、方案设计、编码、测试的工作。从我的 Roadmap 中,你也可以看出来这一点。为什么要闭环呢,当一个需求,有越来越多的角色参与进来的时候,你会发现方案细节的对齐,变成了一个不简单的工作。

有时候我们经常会讲一个词,融会贯通。当你把一整套研发体系都吃下来的时候,你会发现可以顺利地解决掉项目的问题。

我的工作场景

在我的工作中,会涉及到工具链的开发。首先在开始前,需要做一些竞品调研,方案设计的工作。

开发环节,对前端来说,按照目前的趋势,我们更好的方式是以 Monorepo 的形式去做开发。这里 Pnpm 就是一个很不错的选择,但接下来你会遇到一些问题,例如如何去做这些包的发版编排?

由于在 Workspace 中会存在一些包之间的相互引用,在发版时,也要按照拓扑排序的方式进行发版,这时,我们就可以用到 Rush.js 去做拓扑发版,以及自动生成 Changelog

工具链对于质量需要较强的把控,这时我们就要引入 Jest 做单测,但一些场景下,单测是不够的,这时我们需要引入 e2e 测试。

Monorepo 中,不像单仓中,可能只存在一个 tsconfig,这时会存在配置之间 extends 的关系,需要我们对 tsconfig 的配置了如指掌。

对于多种工具消费方式,例如 CLIChrome 插件等,实则需要公用一些方法与配置,这里就需要抽象出公用的 utils 等。

在开发中,可能会关注一些新闻,比如 Vite 4 启用了 SWC 替代 Babel做编译。那你是否有好奇过,为什么 SWC 会更快,这时候如果学过 Rust,就知道 Rust 特有的语言特性。

总结

我想说的是,作为一个工程师,不要去把自己划分为「前端/后端/ PM」这些更加细分的角色。你都可以去学习任何方面的知识。并且你学的一切知识,都是有意义的。虽然学习的道路很长,但只要坚持下去,你就会朝着优秀的工程师进发。

作者:EricLee
来源:juejin.cn/post/7181000277208760378

收起阅读 »

Java中多线程的ABA问题探讨

前言  本文是笔者在日常开发过程中遇到的对 CAS 、 ABA 问题以及 JUC(java.util.concurrent)中 AtomicReference 相关类的设计的一些思考记录。 对需要处理 ABA 问题,或有诸如笔者一样的设计疑问探索好奇心的读者可...
继续阅读 »

前言

  本文是笔者在日常开发过程中遇到的对 CAS 、 ABA 问题以及 JUC(java.util.concurrent)中 AtomicReference 相关类的设计的一些思考记录。 对需要处理 ABA 问题,或有诸如笔者一样的设计疑问探索好奇心的读者可能会带来一些启发。

本文主体由三部分构成:

  1. 首先阐述多线程场景数据同步的常用语言工具

  2. 接着阐述什么是 ABA 问题,以及产生的原因和可能带来的影响

  3. 再探索 JUC 中官方为解决 ABA 问题而做一些工具类设计

文章的最后会对多线程数据同步常用解决方案做了简短地经验性总结与概括。

受限于笔者的理解与知识水平,文章的一些术语表述难免可能会失偏颇,对于有理解歧义或争议的部分,欢迎大家探讨和指正。

一、异步场景常用工具

在Java中的多线程数据同步的场景,常会出现:

  1. 关键字 volatile

  2. 关键字 synchronized

  3. 可重入锁/读写锁 java.util.concurrent.locks.*

  4. 容器同步包装,如 Collections.synchronizedXxx()

  5. 新的线程安全容器,如 CopyOnWriteArrayList/ConcurrentHashMap

  6. 阻塞队列 java.util.concurrent.BlockingQueue

  7. 原子类 java.util.concurrent.atomic.*

  8. 以及 JUC 中其他工具诸如 CountDownLatch/Exchanger/FutureTask 等角色。

  其中 volatile 关键字用于刷新数据缓存,即保证在 A 线程修改某数据后,B 线程中可见,这里面涉及的线程缓存和指令重排因篇幅原因不在本文探讨范围之内。而不论是 synchronized 关键字下的对象锁,还是基于同步器 AbstractQueuedSynchronizerLock 实现者们,它们都属于悲观锁。而在同步容器包装、新的线程程安全容器和阻塞队列中都使用的是悲观锁;只是各类的内部使用不同的 Lock 实现类和 JUC 工具,另外不同容器在加锁粒度和加锁策略上分别做了处理和优化。

  这里值得一说的,也是本文聚焦的重点则是原子类,即 java.util.concurrent.atomic.* 包下的几个类库诸如 AtomicBoolean/AtomicInteger/AtomicReference

二、CAS 与 ABA 问题

  我们知道在使用悲观锁的场景中,如果有有一个线程抢先取得了锁,那么其他想要获得锁的线程就得被阻塞等待,直到占锁线程完成计算释放锁资源。而现代 CPU 提供了硬件级指令来实现同步原语,也就是说可以让线程在运行过程中检测是否有其他线程也在对同一块内存进行读写,基于此 Java 提供了使用忙循环来取代阻塞的系列工具类 AutomicXxx,这属于是一种乐观锁的实现。其常规使用方式形如:

public class Requester {
   private AtomicBoolean isRequesting = new AtomicBoolean(false)

   public void request() {
       // 修改成功时返回true;compareAndSet 方法由 Native 层调硬件指令实现
       if (!isRequesting.compareAndSet(false, true)) {
           return;
      }
       try {
           // do sth...
      } finally {
           isRequesting.set(false)
      }
  }
}

  进入到 JDK11 AtomicBoolean 的源码中,可以看到 compareAndSet 最终调用 Native 层的方式如下。其实在旧的版本中 JDK 是使用 Unsafe 类处理的,在入参数中有传入状态变量的字段偏移值,新版本则将两者封装到 VarHandle 中采用DL方式查找依赖(笔者猜测可能和JDK9模块化改造有关):

// 旧版
public class AtomicBoolean {
   private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
   private static final long VALUE;
   static {
       try {
           VALUE = U.objectFieldOffset
              (AtomicBoolean.class.getDeclaredField("value"));
      } catch (ReflectiveOperationException e) {
           throw new Error(e);
      }
  }

   private volatile int value;

   public final boolean compareAndSet(boolean expect, boolean update) {
       return U.compareAndSwapInt(this, VALUE, (expect ? 1 : 0), (update ? 1 : 0));
  }
}

// 新版
public class AtomicBoolean {
   private static final VarHandle VALUE;
   static {
       try {
           MethodHandles.Lookup l = MethodHandles.lookup();
           VALUE = l.findVarHandle(AtomicBoolean.class, "value", int.class);
      } catch (ReflectiveOperationException e) {
           throw new ExceptionInInitializerError(e);
      }
  }

   private volatile int value;

   public final boolean compareAndSet(boolean expectedValue, boolean newValue) {
       return VALUE.compareAndSet(this, (expectedValue ? 1 : 0), (newValue ? 1 : 0));
  }
}

  犹如入仓有 thisvalue 的偏移值,则 Native 层可根据此二者值定位到某块栈内存,这样对于基本类型没什么问题。原子类型体系中使用 AtomicReference 来引用复合类型实例,但 Java 中 Object 类型在栈中保存的只是堆中对象数据块的地址,其结构形如下图:


  而实际运行过程中,调用 AtomicReference#compareAndSet() 时,Native层只会对比栈中内存的值,而不会关注其指向的堆中数据。这样说可能有点抽象,看一段实验代码:

StringBuilder varA = new StringBuilder("abc");
StringBuilder varB = new StringBuilder("123");

AtomicReference<StringBuilder> ref = new AtomicReference<>(varA);
ref.compareAndSet(varA, varB); // (1)
System.out.println(ref.get()); // (2) varB->123
varB.append('4'); // (3) changed varB->1234
if (ref.compareAndSet(varB, varA)) { // (4)
   System.out.println("CAS succeed"); // (5) CAS succeed
}
System.out.println(ref.get()); // abc

喜欢动手的读者可以尝试自定义一个类,观察下 Compare 过程是否真的没有调用对象的 equals 方法。

  ref 在经过处理后再 (2) 处引用变量B,而在注释 (3) 处将 B 值修改了,但由于原子类不会检查堆中数据,所以还是能通过注释 (4) 处的相等比较走到注释 (5) 。这也就引入了 所谓的 ABA 问题:

  1. 假设,线程 1 的任务希望将变量从 A 变为 C ,但执行到一半被线程 2 抢走 CPU

  2. 线程 2 将变量从 A 改成了 B ,此时 CPU 时间片又被系统分给了线程 3

  3. 线程 3 讲变量从 B 又设置成一个新的 A 。

  4. 线程 1 获取时间片,检查变量发现其仍然是 A(但 A 对象内部的数据已经改变了),检查通过将变量置为 C 。

  若业务场景中,线程 1 不在意变量经过了一轮变化,也不在意 A 中数据是否有变化,则该问题无关痛痒。而若线程 1 对这两个变化敏感,则将变量置为 C 的操作就不符合预期了。用维基百科的例子来表述,其大意是:

你提着有很多现金的包去机场,这时来了个辣妹挑逗你,并趁你不注意时用一个看起来一样的空包换了你的现金包,然后她就走了;此时你检查了下发现你的包还在,于是就匆忙拿着包赶飞机去了。

换个角度看这几个关键字:

  • 有现金的包:指向堆中数据的栈引用

  • 辣妹挑逗:其他线程抢占 CPU

  • 看起来一样空包:其他线程修改堆中数据

  • 发现包还在:仅检查栈中内存的地址值是否一致

三、用 JUC 工具处理 ABA 问题

  为处理 ABA 问题,JDK 提供了另外两个工具类:AtomicMarkableReferenceAtomicStampedReference 他们除了对比栈中对象的引用地址外,另外还保存了一个 booleanint 类型的标记值,用于 CAS 比较。

StringBuilder varA = new StringBuilder("abc");
StringBuilder varB = new StringBuilder("123");

AtomicStampedReference<StringBuilder> ref = new AtomicStampedReference<>(varA, varA.toString().hashCode());
ref.compareAndSet(varA, varB, varA.toString().hashCode(), varB.toString().hashCode());
System.out.println(ref.get(new int[1]));
varB.append('4');
// CAS失败,因为Stamp值对不上
if (ref.compareAndSet(varB, varA, varB.toString().hashCode(), varA.toString().hashCode())) {
   System.out.println("compareAndSet: succeed");
}
System.out.println(ref.get(new int[1]));

:这种设计和为快速判断文件是否相同,而比较文件摘要值(MD5、SHA值)和预期是否一致的思想倒有异曲同工之妙。

总结

  通常在多线程场景中,这些工具的应用场景具有各自的适用特征:

  1. 若各线程读写数据没有竞争关系,则可考虑仅使用 volatile 关键字;

  2. 若各线程对某数据的读写需要去重,则可优先考虑使用乐观锁实现,即用原子类型;

  3. 若各线程有竞争关系且不去重必须按顺序抢占某资源,即必须用锁阻塞,若没有多条件队列的诉求则可先考虑使用 synchronized 添加对象锁(但需注意锁对象的不可变和私有化),否则考虑用 Lock 实现类,但特别的如需读写分锁以实现共享锁则只能用 Lock 了。

  4. 若需使用线程安全容器,出于性能考虑优先考虑 java.util.concurrent.* 类,如 ConcurrentHashMapCopyOnWriteArrayList;再考虑使用容器同步包装 Collections.synchronizedXxx()。而阻塞队列则多用于生产-消费模型中的任务容器,典型如用在线程池中。

作者:Chavin
来源:juejin.cn/post/7181077489211408443

收起阅读 »

你真的了解 RSA 加密算法吗?

沉淀、分享、成长,让自己和他人都能有所收获!😄记得那是我毕业🎓后的第一个秋天,申请了域名,搭建了论坛。可惜好景不长,没多久进入论坛后就出现各种乱七八糟的广告,而这些广告压根都不是我加的。这是怎么回事?后来我才知道,原来我的论坛没有加 HTTPS 也就是没有 S...
继续阅读 »

沉淀、分享、成长,让自己和他人都能有所收获!😄

记得那是我毕业🎓后的第一个秋天,申请了域名,搭建了论坛。可惜好景不长,没多久进入论坛后就出现各种乱七八糟的广告,而这些广告压根都不是我加的。


这是怎么回事?后来我才知道,原来我的论坛没有加 HTTPS 也就是没有 SSL 证书。那这和数学中的素数有啥关系呢?这是因为每一个 SSL 的生成都用到了 RSA 非对称加密,而 RSA 的加解密就是使用了两个互为质数的大素数生成公钥和私钥的。

这就是我们今天要分享的,关于素数在 RSA 算法中的应用。

一、什么是素数

素数(或质数)指的是大于1的且不能通过两个较小的自然数乘积得来的自然数。而大于1的自然数如果不是素数,则称之为合数。例如:7是素数,因为它的乘积只能写成 1 * 7 或者 7 * 1 这样。而像自然数 8 可以写成 2 * 4,因为它是两个较小数字的乘积。

通常在 Java 程序中,我们可以使用下面的代码判断一个数字是否为素数;

boolean isPrime = number > 0;
// 计算number的平方根为k,可以减少一半的计算量
int k = (int) Math.sqrt(number);
for (int i = 2; i <= k; i++) {
   if (number % i == 0) {
       isPrime = false;
       break;
  }
}
return isPrime;

二、对称加密和非对称加密

假如 Alice 时而需要给北漂搬砖的 Bob 发一些信息,为了安全起见两个人相互协商了一个加密的方式。比如 Alice 发送了一个银行卡密码 142857 给 Bob,Alice 会按照与 Bob 的协商方式,把 142857 * 2 = 285714 的结果传递给 Bob,之后 Bob 再通过把信息除以2拿到结果。

但一来二去,Alice 发的密码、生日、衣服尺寸、鞋子大小,都是乘以2的规律被别人发现。这下这个加密方式就不安全了。而如果每次都给不同的信息维护不同的秘钥又十分麻烦,且这样的秘钥为了安全也得线下沟通,人力成本又非常高。

所以有没有另外一种方式,使用不同的秘钥对信息的加密和解密。当 Bob 想从 Alice 那获取信息,那么 Bob 就给 Alice 一个公钥,让她使用公钥对信息进行加密,而加密后的信息只有 Bob 手里有私钥才能解开。那么这样的信息传递就变得非常安全了。如图所示。

对称加密非对称加密


三、算法公式推导


如果 Alice 希望更安全的给 Bob 发送的信息,那么就需要保证经过公钥加密的信息不那么容易被反推出来。所以这里的信息加密,会需用到求模运算。像计算机中的散列算法,伪随机数都是求模运算的典型应用。

例如;5^3 mod 7 = 6 —— 5的3次幂模7余6

  • 5相当于 Alice 要传递给 Bob 的信息

  • 3相当于是秘钥

  • 6相当于是加密后的信息

经过求模计算的结果6,很难被推到出秘钥信息,只能一个个去验证;

5^1 mod 7 = 5
5^2 mod 7 = 3
5^3 mod 7 = 6
5^4 mod 7 = 2
...

但如果求模的值特别大,例如这样:5^3 mod 78913949018093809389018903794894898493... = 6 那么再想一个个计算就有不靠谱了。所以这也是为什么会使用模运算进行加密,因为对于大数来说对模运算求逆根本没法搞。

根据求模的计算方式,我们得到加密和解密公式;—— 关于加密和解密的公式推到,后文中会给出数学计算公式。


对于两个公式我们做一下更简单的转换;


从转换后的公式可以得知,m 的 ed 次幂,除以 N 求求模可以得到 m 本身。那么 ed 就成了计算公钥加密的重要因素。为此这里需要提到数学中一个非常重要的定理,欧拉定理。—— 1763年,欧拉发现。

欧拉定理:m^φ(n) ≡ 1 (mod n) 对于任何一个与 n 互质的正整数 m,的 φ(n) 次幂并除以 n 去模,结果永远等于1。φ(n) 代表着在小于等于 n 的正整数中,有多少个与 n 互质的数。

例如:φ(8) 小于等于8的正整数中 1、2、3、4、5、6、7、8 有 1、3、5、7 与数字 8 互为质数。所以 φ(8) = 4 但如果是 n 是质数,那么 φ(n) = n - 1 比如 φ(7) 与7互为质数有1、2、3、4、5、6 所有 φ(7) = 6

接下来我们对欧拉公式做一些简单的变换,用于看出ed的作用;


经过推导的结果可以看到 ed = kφ(n) + 1,这样只要算出加密秘钥 e 就可以得到一个对应的解密秘钥 d。那么整套这套计算过程,就是 RSA 算法。

四、关于RSA算法

RSA加密算法是一种非对称加密算法,在公开秘钥加密和电子商业中被广泛使用。


于1977年,三位数学家;罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字命名,叫做RSA算法。

1973年,在英国政府通讯总部工作的数学家克利福德·柯克斯(Clifford Cocks)在一个内部文件中提出了一个与之等效的算法,但该算法被列入机密,直到1997年才得到公开。

RSA 的算法核心在于取了2个素数做乘积求和、欧拉计算等一系列方式算得公钥和私钥,但想通过公钥和加密信息,反推出来私钥就会非常复杂,因为这是相当于对极大整数的因数分解。所以秘钥越长做因数分解越困难,这也就决定了 RSA 算法的可靠性。—— PS:可能以上这段话还不是很好理解,程序员👨🏻‍💻还是要看代码才能悟。接下来我们就来编写一下 RSA 加密代码。

五、实现RSA算法

RSA 的秘钥生成首先需要两个质数p、q,之后根据这两个质数算出公钥和私钥,在根据公钥来对要传递的信息进行加密。接下来我们就要代码实现一下 RSA 算法,读者也可以根据代码的调试去反向理解 RSA 的算法过程,一般这样的学习方式更有抓手的感觉。嘿嘿 抓手

1. 互为质数的p、q

两个互为质数p、q是选择出来的,越大越安全。因为大整数的质因数分解是非常困难的,直到2020年为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被破解的。—— 不知道量子计算机出来以后会不会改变。如果改变,那么程序员又有的忙了。

2. 乘积n

n = p * q 的乘积。

public long n(long p, long q) {
   return p * q;
}

3. 欧拉公式 φ(n)

φ(n) = (p - 1) * (q - 1)

public long euler(long p, long q) {
   return (p - 1) * (q - 1);
}

4. 选取公钥e

e 的值范围在 1 < e < φ(n)

public long e(long euler){
   long e = euler / 10;
   while (gcd(e, euler) != 1){
       e ++;
  }
   return e;
}

5. 选取私钥d

d = (kφ(n) + 1) / e

public long inverse(long e, long euler) {
   return (euler + 1) / e;
}

6. 加密

c = m^e mod n

public long encrypt(long m, long e, long n) {
   BigInteger bM = new BigInteger(String.valueOf(m));
   BigInteger bE = new BigInteger(String.valueOf(e));
   BigInteger bN = new BigInteger(String.valueOf(n));
   return Long.parseLong(bM.modPow(bE, bN).toString());
}

7. 解密

m = c^d mod n

public long decrypt(long c, long d, long n) {
   BigInteger bC = new BigInteger(String.valueOf(c));
   BigInteger bD = new BigInteger(String.valueOf(d));
   BigInteger bN = new BigInteger(String.valueOf(n));
   return Long.parseLong(bC.modPow(bD, bN).toString());
}

8. 测试

@Test
public void test_rsa() {
   RSA rsa = new RSA();
   long p = 3,                         // 选取2个互为质数的p、q
           q = 11,                     // 选取2个互为质数的p、q
           n = rsa.n(p, q),            // n = p * q
           euler = rsa.euler(p, q),    // euler = (p-1)*(q-1)
           e = rsa.e(euler),           // 互为素数的小整数e | 1 < e < euler
           d = rsa.inverse(e, euler),  // ed = φ(n) + 1 | d = (φ(n) + 1)/e
           msg = 5;                    // 传递消息 5
           
   System.out.println("消息:" + msg);
   System.out.println("公钥(n,e):" + "(" + n + "," + e + ")");
   System.out.println("私钥(n,d):" + "(" + n + "," + d + ")");
   
   long encrypt = rsa.encrypt(msg, e, n);
   System.out.println("加密(消息):" + encrypt);
   
   long decrypt = rsa.decrypt(encrypt, d, n);
   System.out.println("解密(消息):" + decrypt);
}

测试结果

消息:5
公钥(n,e)(33,3)
私钥(n,d)(33,7)
加密(消息):26
解密(消息):5
  • 通过选取3、11作为两个互质数,计算出公钥和私钥,分别进行消息的加密和解密。如测试结果消息5的加密后的信息是26,解密后获得原始信息5

六、RSA数学原理

整个 RSA 的加解密是有一套数学基础可以推导验证的,这里小傅哥把学习整理的资料分享给读者,如果感兴趣可以尝试验证。这里的数学公式会涉及到;求模运算、最大公约数、贝祖定理、线性同于方程、中国余数定理、费马小定理。当然还有一些很基础的数论概念;素数、互质数等。以下推理数学内容来自博客:luyuhuang.tech/2019/10/24/…

1. 模运算

1.1 整数除法

定理 1 令 a 为整数, d 为正整数, 则存在唯一的整数 q 和 r, 满足 0⩽r<d, 使得 a=dq+r.

当 r=0 时, 我们称 d 整除 a, 记作 d∣a; 否则称 d 不整除 a, 记作 d∤a

整除有以下基本性质:

定理 2 令 a, b, c 为整数, 其中 a≠0a≠0. 则:

  • 对任意整数 m,n,如果 a∣b 且 a∣c, 则 a∣(mb + nc)

  • 如果 a∣b, 则对于所有整数 c 都有 a∣bc

  • 如果 a∣b 且 b∣c, 则 a∣c

1.2 模算术

在数论中我们特别关心一个整数被一个正整数除时的余数. 我们用 a mod m = b表示整数 a 除以正整数 m 的余数是 b. 为了表示两个整数被一个正整数除时的余数相同, 人们又提出了同余式(congruence).

定义 1 如果 a 和 b 是整数而 m 是正整数, 则当 m 整除 a - b 时称 a 模 m 同余 b. 记作 a ≡ b(mod m)

a ≡ b(mod m) 和 a mod m= b 很相似. 事实上, 如果 a mod m = b, 则 a≡b(mod m). 但他们本质上是两个不同的概念. a mod m = b 表达的是一个函数, 而 a≡b(mod m) 表达的是两个整数之间的关系.

模算术有下列性质:

定理 3 如果 m 是正整数, a, b 是整数, 则有

(a+b)mod m=((a mod m)+(b mod m)) mod m

ab mod m=(a mod m)(b mod m) mod m

根据定理3, 可得以下推论

推论 1 设 m 是正整数, a, b, c 是整数; 如果 a ≡ b(mod m), 则 ac ≡ bc(mod m)

证明 ∵ a ≡ b(mod m), ∴ (a−b) mod m=0 . 那么

(ac−bc) mod m=c(a−b) mod m=(c mod m⋅(a−b) mod m) mod m=0

∴ ac ≡ bc(mod m)

需要注意的是, 推论1反之不成立. 来看推论2:

推论 2 设 m 是正整数, a, b 是整数, c 是不能被 m 整除的整数; 如果 ac ≡ bc(mod m) , 则 a ≡ b(mod m)

证明 ∵ ac ≡ bc(mod m) , 所以有

(ac−bc)mod m=c(a−b)mod m=(c mod m⋅(a−b)mod m) mod m=0

∵ c mod m≠0 ,

∴ (a−b) mod m=0,

∴a ≡ b(mod m) .

2. 最大公约数

如果一个整数 d 能够整除另一个整数 a, 则称 d 是 a 的一个约数(divisor); 如果 d 既能整除 a 又能整除 b, 则称 d 是 a 和 b 的一个公约数(common divisor). 能整除两个整数的最大整数称为这两个整数的最大公约数(greatest common divisor).

定义 2 令 a 和 b 是不全为零的两个整数, 能使 d∣ad∣a 和 d∣bd∣b 的最大整数 d 称为 a 和 b 的最大公约数. 记作 gcd(a,b)

2.1 求最大公约数

如何求两个已知整数的最大公约数呢? 这里我们讨论一个高效的求最大公约数的算法, 称为辗转相除法. 因为这个算法是欧几里得发明的, 所以也称为欧几里得算法. 辗转相除法基于以下定理:

引理 1 令 a=bq+r, 其中 a, b, q 和 r 均为整数. 则有 gcd(a,b)=gcd(b,r)

证明 我们假设 d 是 a 和 b 的公约数, 即 d∣a且 d∣b, 那么根据定理2, d 也能整除 a−bq=r 所以 a 和 b 的任何公约数也是 b 和 r 的公约数;

类似地, 假设 d 是 b 和 r 的公约数, 即 d∣bd∣b 且 d∣rd∣r, 那么根据定理2, d 也能整除 a=bq+r. 所以 b 和 r 的任何公约数也是 a 和 b 的公约数;

因此, a 与 b 和 b 与 r 拥有相同的公约数. 所以 gcd(a,b)=gcd(b,r).

辗转相除法就是利用引理1, 把大数转换成小数. 例如, 求 gcd(287,91) 我们就把用较大的数除以较小的数. 首先用 287 除以 91, 得

287=91⋅3+14

我们有 gcd(287,91)=gcd(91,14) . 问题转换成求 gcd(91,14). 同样地, 用 91 除以 14, 得

91=14⋅6+7

有 gcd(91,14)=gcd(14,7) . 继续用 14 除以 7, 得

14=7⋅2+0

因为 7 整除 14, 所以 gcd(14,7)=7. 所以 gcd(287,91)=gcd(91,14)=gcd(14,7)=7.

我们可以很快写出辗转相除法的代码:

def gcd(a, b):
   if b == 0: return a
   return gcd(b, a % b)

2.2 贝祖定理

现在我们讨论最大公约数的一个重要性质:

定理 4 贝祖定理 如果整数 a, b 不全为零, 则 gcd(a,b)是 a 和 b 的线性组合集 {ax+by∣x,y∈Z}中最小的元素. 这里的 x 和 y 被称为贝祖系数

证明 令 A={ax+by∣x,y∈Z}. 设存在 x0x0, y0y0 使 d0d0 是 A 中的最小正元素, d0=ax0+by0 现在用 d0去除 a, 这就得到唯一的整数 q(商) 和 r(余数) 满足


又 0⩽r<d0, d0 是 A 中最小正元素

∴ r=0 , d0∣a.

同理, 用 d0d0 去除 b, 可得 d0∣b. 所以说 d0 是 a 和 b 的公约数.

设 a 和 b 的最大公约数是 d, 那么 d∣(ax0+by0)即 d∣d0

∴∴ d0 是 a 和 b 的最大公约数.

我们可以对辗转相除法稍作修改, 让它在计算出最大公约数的同时计算出贝祖系数.

def gcd(a, b):
   if b == 0: return a, 1, 0
   d, x, y = gcd(b, a % b)
   return d, y, x - (a / b) * y

3. 线性同余方程

现在我们来讨论求解形如 ax≡b(modm) 的线性同余方程. 求解这样的线性同余方程是数论研究及其应用中的一项基本任务. 如何求解这样的方程呢? 我们要介绍的一个方法是通过求使得方程 ¯aa≡1(mod m) 成立的整数 ¯a. 我们称 ¯a 为 a 模 m 的逆. 下面的定理指出, 当 a 和 m 互素时, a 模 m 的逆必然存在.

定理 5 如果 a 和 m 为互素的整数且 m>1, 则 a 模 m 的逆存在, 并且是唯一的.

证明贝祖定理可知, ∵ gcd(a,m)=1 , ∴ 存在整数 x 和 y 使得 ax+my=1 这蕴含着 ax+my≡1(modm) ∵ my≡0(modm), 所以有 ax≡1(modm)

∴ x 为 a 模 m 的逆.

这样我们就可以调用辗转相除法 gcd(a, m) 求得 a 模 m 的逆.

a 模 m 的逆也被称为 a 在模m乘法群 Z∗m 中的逆元. 这里我并不想引入群论, 有兴趣的同学可参阅算法导论

求得了 a 模 m 的逆 ¯a 现在我们可以来解线性同余方程了. 具体的做法是这样的: 对于方程 ax≡b(modm)a , 我们在方程两边同时乘上 ¯a, 得 ¯aax≡¯ab(modm)

把 ¯aa≡1(modm) 带入上式, 得 x≡¯ab(modm)

x≡¯ab(modm) 就是方程的解. 注意同余方程会有无数个整数解, 所以我们用同余式来表示同余方程的解.


4. 中国余数定理

中国南北朝时期数学著作 孙子算经 中提出了这样一个问题:

有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?

用现代的数学语言表述就是: 下列同余方程组的解释多少?


孙子算经 中首次提到了同余方程组问题及其具体解法. 因此中国剩余定理称为孙子定理.

定理 6 中国余数定理 令 m1,m2,…,mn 为大于 1 且两两互素的正整数, a1,a2,…,an 是任意整数. 则同余方程组


有唯一的模 m=m1m2…mnm=m1m2…mn 的解.

证明 我们使用构造证明法, 构造出这个方程组的解. 首先对于 i=1,2,…,ni=1,2,…,n, 令


即, MiMi 是除了 mimi 之外所有模数的积. ∵∵ m1,m2,…,mn 两两互素, ∴∴ gcd(mi,Mi)=1. 由定理 5 可知, 存在整数 yiyi 是 MiMi 模 mimi 的逆. 即


上式等号两边同时乘 aiai 得


就是第 i 个方程的一个解; 那么怎么构造出方程组的解呢? 我们注意到, 根据 Mi 的定义可得, 对所有的 j≠ij≠i, 都有 aiMiyi≡0(modmj). 因此我们令


就是方程组的解.

有了这个结论, 我们可以解答 孙子算经 中的问题了: 对方程组的每个方程, 求出 MiMi , 然后调用 gcd(M_i, m_i) 求出 yiyi:


最后求出 x=−2⋅35+3⋅21+2⋅15=23≡23(mod105)

5. 费马小定理

现在我们来看数论中另外一个重要的定理, 费马小定理(Fermat's little theorem)

定理 7 费马小定理 如果 a 是一个整数, p 是一个素数, 那么


当 n 不为 p 或 0 时, 由于分子有质数p, 但分母不含p; 故分子的p能保留, 不被约分而除去. 即 p∣(np).

令 b 为任意整数, 根据二项式定理, 我们有


令 a=b+1, 即得 a^p ≡ a(mod p)

当 p 不整除 a 时, 根据推论 2, 有 a^p−1 ≡ 1(mod p)

6. 算法证明

我们终于可以来看 RSA 算法了. 先来看 RSA 算法是怎么运作的:

RSA 算法按照以下过程创建公钥和私钥:

  1. 随机选取两个大素数 p 和 q, p≠qp≠q;

  2. 计算 n=pq

  3. 选取一个与 (p−1)(q−1) 互素的小整数 e;

  4. 求 e 模 (p−1)(q−1) 的逆, 记作 d;

  5. 将 P=(e,n)公开, 是为公钥;

  6. 将 S=(d,n)保密, 是为私钥.


所以 RSA 加密算法是有效的.

(1) 式表明, 不仅可以用公钥加密, 私钥解密, 还可以用私钥加密, 公钥解密. 即加密计算 C=M^d mod n, 解密计算 M=C^e mod n

RSA 算法的安全性基于大整数的质因数分解的困难性. 由于目前没有能在多项式时间内对整数作质因数分解的算法, 因此无法在可行的时间内把 n 分解成 p 和 q 的乘积. 因此就无法求得 e 模 (p−1)(q−1)的逆, 也就无法根据公钥计算出私钥.

七、常见面试题

  • 质数的用途

  • RSA 算法描述

  • RSA 算法加解密的过程

  • RSA 算法使用场景

  • 你了解多少关于 RSA 的数学数论知识


源码:github.com/fuzhengwei/…
作者:小傅哥
来源:juejin.cn/post/7173830290812370958

收起阅读 »

MyBatis-Plus联表查询的短板,终于有一款工具补齐了

mybatis-plus作为mybatis的增强工具,它的出现极大的简化了开发中的数据库操作,但是长久以来,它的联表查询能力一直被大家所诟病。一旦遇到left join或right join的左右连接,你还是得老老实实的打开xml文件,手写上一大段的sql语句...
继续阅读 »

mybatis-plus作为mybatis的增强工具,它的出现极大的简化了开发中的数据库操作,但是长久以来,它的联表查询能力一直被大家所诟病。一旦遇到left joinright join的左右连接,你还是得老老实实的打开xml文件,手写上一大段的sql语句。

直到前几天,偶然碰到了这么一款叫做mybatis-plus-join的工具(后面就简称mpj了),使用了一下,不得不说真香!彻底将我从xml地狱中解放了出来,终于可以以类似mybatis-plusQueryWrapper的方式来进行联表查询了,话不多说,我们下面开始体验。

引入依赖

首先在项目中引入引入依赖坐标,因为mpj中依赖较高版本mybatis-plus中的一些api,所以项目建议直接使用高版本。

<dependency>
   <groupId>com.github.yulichang</groupId>
   <artifactId>mybatis-plus-join</artifactId>
   <version>1.2.4</version>
</dependency>
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.5.1</version>
</dependency>

引入相关依赖后,在springboot项目中,像往常一样正常配置数据源连接信息就可以了。

数据准备

因为要实现联表查询,所以我们先来建几张表进行测试。

订单表:


用户表,包含用户姓名:


商品表,包含商品名称和单价:


在订单表中,通过用户id和商品id与其他两张表进行关联。

修改Mapper

以往在使用myatis-plus的时候,我们的Mapper层接口都是直接继承的BaseMapper,使用mpj后需要对其进行修改,改为继承MPJBaseMapper接口。

@Mapper
public interface OrderMapper extends MPJBaseMapper<Order> {
}

对其余两个表的Mapper接口也进行相同的改造。此外,我们的service也可以选择继承MPJBaseServiceserviceImpl选择继承MPJBaseServiceImpl,这两者为非必须继承。

查询

Mapper接口改造完成后,我们把它注入到Service中,虽然说我们要完成3张表的联表查询,但是以Order作为主表的话,那么只注入这一个对应的OrderMapper就可以,非常简单。

@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService {
   private final OrderMapper orderMapper;
}

MPJLambdaWrapper

接下来,我们体验一下再也不用写sql的联表查询:

public void getOrder() {
   List<OrderDto> list = orderMapper.selectJoinList(OrderDto.class,
    new MPJLambdaWrapper<Order>()
    .selectAll(Order.class)
    .select(Product::getUnitPrice)
    .selectAs(User::getName,OrderDto::getUserName)
    .selectAs(Product::getName,OrderDto::getProductName)
    .leftJoin(User.class, User::getId, Order::getUserId)
    .leftJoin(Product.class, Product::getId, Order::getProductId)
    .eq(Order::getStatus,3));

   list.forEach(System.out::println);
}

不看代码,我们先调用接口来看一下执行结果:


可以看到,成功查询出了关联表中的信息,下面我们一点点介绍上面代码的语义。

首先,调用mapperselectJoinList()方法,进行关联查询,返回多条结果。后面的第一个参数OrderDto.class代表接收返回查询结果的类,作用和我们之前在xml中写的resultType类似。

这个类可以直接继承实体,再添加上需要在关联查询中返回的列即可:

@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class OrderDto extends Order {
   String userName;
   String productName;
   Double unitPrice;
}

接下来的MPJLambdaWrapper就是构建查询条件的核心了,看一下我们在上面用到的几个方法:

  • selectAll():查询指定实体类的全部字段

  • select():查询指定的字段,支持可变长参数同时查询多个字段,但是在同一个select中只能查询相同表的字段,所以如果查询多张表的字段需要分开写

  • selectAs():字段别名查询,用于数据库字段与接收结果的dto中属性名称不一致时转换

  • leftJoin():左连接,其中第一个参数是参与联表的表对应的实体类,第二个参数是这张表联表的ON字段,第三个参数是参与联表的ON的另一个实体类属性

除此之外,还可以正常调用mybatis-plus中的各种原生方法,文档中还提到,默认主表别名是t,其他的表别名以先后调用的顺序使用t1t2t3以此类推。

我们用插件读取日志转化为可读的sql语句,可以看到两条左连接条件都被正确地添加到了sql中:


MPJQueryWrapper

mybatis-plus非常类似,除了LamdaWrapper外还提供了普通QueryWrapper的写法,改造上面的代码:

public void getOrderSimple() {
   List<OrderDto> list = orderMapper.selectJoinList(OrderDto.class,
    new MPJQueryWrapper<Order>()
    .selectAll(Order.class)
    .select("t2.unit_price","t2.name as product_name")
    .select("t1.name as user_name")
    .leftJoin("t_user t1 on t1.id = t.user_id")
    .leftJoin("t_product t2 on t2.id = t.product_id")
    .eq("t.status", "3")
  );

   list.forEach(System.out::println);
}

运行结果与之前完全相同,需要注意的是,这样写时在引用表名时不要使用数据库中的原表名,主表默认使用t,其他表使用join语句中我们为它起的别名,如果使用原表名在运行中会出现报错。

并且,在MPJQueryWrapper中,可以更灵活的支持子查询操作,如果业务比较复杂,那么使用这种方式也是不错的选择。

分页查询

mpj中也能很好的支持列表查询中的分页功能,首先我们要在项目中加入分页拦截器:

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
   MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
   interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
   return interceptor;
}

接下来改造上面的代码,调用selectJoinPage()方法:

public void page() {
   IPage<OrderDto> orderPage = orderMapper.selectJoinPage(
     new Page<OrderDto>(2,10),
     OrderDto.class,
     new MPJLambdaWrapper<Order>()
      .selectAll(Order.class)
      .select(Product::getUnitPrice)
      .selectAs(User::getName, OrderDto::getUserName)
      .selectAs(Product::getName, OrderDto::getProductName)
      .leftJoin(User.class, User::getId, Order::getUserId)
      .leftJoin(Product.class, Product::getId, Order::getProductId)
      .orderByAsc(Order::getId));

   orderPage.getRecords().forEach(System.out::println);
}

注意在这里需要添加一个分页参数的Page对象,我们再执行上面的代码,并对日志进行解析,查看sql语句:


可以看到底层通过添加limit进行了分页,同理,MPJQueryWrapper也可以这样进行分页。

最后

经过简单的测试,个人感觉mpj这款工具在联表查询方面还是比较实用的,能更应对项目中不是非常复杂的场景下的sql查询,大大提高我们的生产效率。当然,在项目的issues中也能看到当前版本中也仍然存在一些问题,希望在后续版本迭代中能继续完善。

作者:码农参上
来源:juejin.cn/post/7173493838143553549

收起阅读 »

Spring Boot 分离配置文件的 N 种方式

今天聊一个小伙伴在星球上的提问:问题不难,解决方案也有很多,因此我决定撸一篇文章和大家仔细说说这个问题。1. 配置文件位置首先小伙伴们要明白,Spring Boot 默认加载的配置文件是 application.properties 或者 applicatio...
继续阅读 »

今天聊一个小伙伴在星球上的提问:


问题不难,解决方案也有很多,因此我决定撸一篇文章和大家仔细说说这个问题。

1. 配置文件位置

首先小伙伴们要明白,Spring Boot 默认加载的配置文件是 application.properties 或者 application.yaml,默认的加载位置一共有五个,五个位置可以分为两类:

从 classpath 下加载,这个又细分为两种:

  1. 直接读取 classpath 下的配置文件,对应到 Spring Boot 项目中,就是 resources 目录下的配置。

  2. 读取 classpath:/config/ 目录下的文件,对应到 Spring Boot 项目中就是 resources/config 目录下的配置。

这两种情况如下图:


从项目所在的当前目录下加载,这个又细分为三种情况:

  1. 从项目当前目录下加载配置文件。

  2. 从项目当前目录下的 config 文件夹中加载配置文件。

  3. 从项目当前目录下的 config 文件夹的子文件夹中加载(孙子文件夹不可以)。

这三种情况如下图:


config 目录下的配置文件可以被加载,config/a 目录下的配置文件也可以被加载,但是 config/a/b 目录下的配置文件不会被加载,因为不是直接子文件夹。

配置文件可以放在这么多不同的位置,如果同一个属性在多个配置文件中都写了,那么后面加载的配置会覆盖掉前面的。例如在 classpath:application.yaml 中设置项目端口号是 8080,在 项目当前目录/config/a/application.yaml 中设置项目端口是 8081,那么最终的项目端口号就是 8081。

这是默认的文件位置。

如果你不想让自己的配置文件叫 application.properties 或者 application.yaml,那么也可以自定义配置文件名称,只需要在项目启动的时候指定配置文件名即可,例如我想设置我的配置文件名为 app.yaml,那么我们可以在启动 jar 包的时候按照如下方式配置,此时系统会自动去上面提到的五个位置查找对应的配置文件:

java -jar boot_config_file-0.0.1-SNAPSHOT.jar --spring.config.name=app

如果项目已经打成 jar 包启动了,那么前面所说的目录中,后三个中的项目当前目录就是指 jar 包所在的目录。

如果你不想去这五个位置查找,那么也可以在启动 jar 包的时候明确指定配置文件的位置和名称,如下:

java -jar boot_config_file-0.0.1-SNAPSHOT.jar --spring.config.location=optional:classpath:/app.yaml

注意,我在 classpath 前面加上了 optional: 表示如果这个配置文件不存在,则按照默认的方式启动,而不会报错说找不到这个配置文件。如果不加这个前缀,那么当系统找不到指定的配置文件时,就会抛出 ConfigDataLocationNotFoundException 异常,进而导致应用启动失败。

如果配置文件和 jar 包在相同的目录结构下,如下图:


那么启动脚本如下:

java -jar boot_config_file-0.0.1-SNAPSHOT.jar --spring.config.location=optional:javaboy/app.yaml

如果 spring.config.location 的配置,只是指定了目录,那么必须以 / 结尾,例如上面这个启动脚本,也可以按照如下方式启动:

java -jar boot_config_file-0.0.1-SNAPSHOT.jar --spring.config.location=optional:javaboy/ --spring.config.name=app

通过 spring.config.location 属性锁定配置文件的位置,通过 spring.config.name 属性锁定配置文件的文件名。

2. 额外位置

前面我们关于配置文件位置的设置,都是覆盖掉已有的配置,如果不想覆盖掉 Spring Boot 默认的配置文件查找策略,又想加入自己的,那么可以按照如下方式指定配置文件位置:

java -jar boot_config_file-0.0.1-SNAPSHOT.jar --spring.config.additional-location=optional:javaboy/app.yaml

如果这个额外指定的配置文件和已有的配置文件有冲突,那么还是以后来者为准。

3. 位置通配符

有一种情况,假设我有 redis 和 mysql 的配置,我想将之放在两个不同的文件夹中以便于管理,像下面这样:


那么在项目启动时,可以通过通配符 * 批量扫描相应的文件夹:

java -jar boot_config_file-0.0.1-SNAPSHOT.jar --spring.config.location=optional:config/*/

使用通配符批量扫描 mysql 和 redis 目录时,默认的加载顺序是按照文件夹的字母排序,即先加载 mysql 目录后加载 redis 目录。

需要注意的是,通配符只能用在外部目录中,不可以用在 classpath 中的目录上。另外,包含了通配符的目录,只能有一个通配符 *,不可以有多个,并且还必须是以 */ 结尾,即一个目录的最后部分可以不确定。

4. 导入外部配置

从 Spring Boot2.4 开始,我们也可以使用 spring.config.import 方法来导入配置文件,相比于 additional-location 配置,这个 import 导入更加灵活,可以导入任意名称的配置文件。

spring.config.import=optional:file:./dev.properties

甚至,这个 spring.config.import 还可以导入无扩展名的配置文件,例如我有一个配置文件,是 properties 格式的,但是这个这个配置文件没有扩展名,现在我想将之作为 properties 格式的配置文件导入,方式如下:

spring.config.import=optional:file:/Users/sang/dev[.properties]

好啦,看完上面的内容,文章一开始的问题答案就不用我多说了吧~

作者:江南一点雨
来源:juejin.cn/post/7168285587374342180

收起阅读 »

听说你学过架构设计?来,弄个短链系统

01 引言1)背景这是本人在面试“字节抖音”部门的一道系统设计题,岗位是“后端高级开发工程师”,二面的时候问到的。一开始,面试官笑眯眯地让我做个自我介绍,然后聊了聊项目。当完美无瑕(吞吞吐吐)地聊完项目,并写了一道算法题之后。面试官就开始发问了:小伙子,简历里...
继续阅读 »

01 引言

1)背景

这是本人在面试“字节抖音”部门的一道系统设计题,岗位是“后端高级开发工程师”,二面的时候问到的。一开始,面试官笑眯眯地让我做个自我介绍,然后聊了聊项目。

当完美无瑕(吞吞吐吐)地聊完项目,并写了一道算法题之后。

面试官就开始发问了:小伙子,简历里面写到了熟悉架构设计是吧,那你知道程序设计的‘三高’指什么吗?

我心想,那不是由于程序员的系统不靠谱,领导不当人,天天加班改 BUG,导致年纪轻轻都高血脂、高血压和高血糖嘛!

但是,既然是面试,那领导肯定不愿意听这,于是我回答:程序三高,就是系统设计时需要考虑的高并发、高性能和高可用:

  • 高并发就是在系统开发的过程中,需要保证系统可以同时并行处理很多请求;

  • 高性能是指程序需要尽可能地占用更少的内存和 CPU,并且处理请求的速度要快;

  • 高可用通常描述系统在一段时间内不可服务的时候很短,比如全年停机不超过 31.5 秒,俗称 6 个 9,即保证可用的时间为 99.9999%。

于是,面试官微微点头,心想小伙子还行,既然这难不住你,那我可得出大招了,就来道系统设计题吧!

2)需求说明

众所周知,当业务场景需要给用户发送网络地址或者二维码时,由于地址的长度比较长,通常为了占用更少的资源和提升用户体验。例如,谷歌搜索“计算机”词条地址如下:

https://www.google.com/search?q=%E8%AE%A1%E7%AE%97%E6%9C%BA&ei=KNZ5Y7y4MpiW-AaI4LSACw&ved=0ahUKEwi87MGgnbz7AhUYC94KHQgwDbAQ4dUDCBA&uact=5&oq=%E8%AE%A1%E7%AE%97%E6%9C%BA&gs_lcp=Cgxnd3Mtd2l6LXNlcnAQAzIECAAQQzIFCAAQgAQyBQgAEIAEMgUIABCABDIFCC4QgAQyBQgAEIAEMgUIABCABDIFCAAQgAQyBQgAEIAEMgUIABCABDoKCAAQRxDWBBCwAzoLCC4QgAQQxwEQ0QM6FggAEOoCELQCEIoDELcDENQDEOUCGAE6BwguENQCEENKBAhBGABKBAhGGABQpBZYzSVglydoA3ABeACAAZ0DiAGdD5IBCTAuNy4xLjAuMZgBAKABAbABCsgBCsABAdoBBAgBGAc&sclient=gws-wiz-serp

很明显,如果将这一长串网址发给用户,是十分不“体面”的。而且,遇到一些有字数限制的系统里面,比如微博发帖子就有字数限制,肯定无法发送这样的长链接地址。一般的短信链接中,大多也都是短链接地址:


所以,为了提升用户体验,以及日常业务的需要。我们需要设计一个短链接生成系统,除了业务功能实现以外,我们还得为全国的网络地址服务。在这么大的用户量下,数据该如何存储,高并发如何处理呢?

02 三种链接生成方法

1)需求分析

我心想,这面试官看着“慈眉善目”还笑眯眯的,但出的题目可不简单,这种类型的系统需要考虑的点太多了,绝对不能掉以轻心。

于是,我分别从链接生成、网址访问、缓存优化和高可用四个方面开始着手设计。

首先,生成短链地址,可以考虑用 UUID 或者自增 ID。对于每一个长链接转短链地址时,都必须生成一个全局唯一的短链值,不然就会发生冲突。所以,短链接的特点是:

  • 数据存储量很大,全国的网址每天至少都是百万个短链接地址需要生成;

  • 并发量也不小,遇到同时来访问系统,按一天 3600 秒来算,平均每秒至少上千个请求数;

  • 短链接不可重复,否则会引起数据访问冲突。

2)雪花算法

首先,生成短链接,可以用雪花算法+哈希的方式来实现。


雪花算法是在分布式场景下,根据时间戳、不同的机器 ID 以及序列号生成的唯一数。它的优点在于简单方便,随取随用。

通过雪花算法取到的唯一数,再用哈希映射,将数字转为一个随机的字符串,如果短链字符串比较长,可以直接取前 6 位。但是,由于哈希映射的结果可能会发生冲突,所以对哈希算法的要求比较高。

2)62 进制数生成短链接

除了雪花算法,还可以用 62 进制数(A-Za-z0-9)来生成短链接地址。首先得到一个自增 ID,再将此值转换为 62 进制(a-zA-Z0-9)的字符串,一个亿的数字转换后也就五六位(1亿 -> zAL6e)。

将短链接服务器域名,与这个字符串进行拼接,就能得到短链接的 URL,比如:t.cn/zAL6e。

而生成自增 ID 需要考虑性能影响和并发安全性,所以我们可以通过 Redis 的 incr 命令来做一个发号器,它是一个原子操作,因此我们不必担心数字的安全性。而 Redis 是内存操作,所以效率也挺高。

3)随机数+布隆过滤器

除了自增 ID 以外,我们还可以生成随机数再转 62 进制的方法来生成短链接。但是,由于随机数可能重复,因此我们需要用布隆过滤器来去重。

布隆过滤器是一个巧妙设计的数据结构,它的原理是将一个值多次哈希,映射到不同的 bit 位上并记录下来。当新的值使用时,通过同样的哈希函数,比对各个 bit 位上是否有值:如果这些 bit 位上都没有值,说明这个数是唯一的;否则,就可能不是唯一的。

当然,这可能会产生误判,布隆过滤器一定可以发现重复的值,但 也可能将不重复的值判断为重复值,误判率大概为 0.05%,是可以接受的范围,而且布隆过滤器的效率极高。

因此,通过布隆过滤器,我们能判断生成的随机数是否重复:如果重复,就重新生成一个;如果不重复,就存入布隆过滤器和数据库,从而保证每次取到的随机数都是唯一的。

4)将短链接存到数据库

存库时,可能会因为库存量和技术栈,选用不同的数据库。但由于公司部门用 MySQL 比较多,且当前题目未提及技术选型,所以我们还是选用 MySQL 作为持久化数据库。

每当生成一个短链接后,需要在 MySQL 存储短链接到长链接的映射关系并加上唯一索引,即 zAL6e -> 真实URL。

03 重定向过程

浏览器访问短链接服务时,根据短链地址取到原始 URL,然后进行网址重定向。我们通常有两种重定向方式:

  • 一种是返回给浏览器 301 响应码永久重定向,让其后续直接访问真实的 URL 地址;

  • 一种是 302 临时重定向,让浏览器当前这次访问真实 URL,但后续请求时还是根据短链地址访问。

虽然用 301 浏览器只需一次请求,后续可以直接从浏览器获取长链接,这种方法可以提升访问速度,但是它没法统计短链接的访问次数。

所以根据业务需要,我们一般选用 302 重定向。

04 缓存设计

由于短链接是分发到多个用户手里的,可能在短时间内会多次访问,所以从 MySQL 写入/获取到长链接后可以放入 redis 缓存。

1)加入缓存

并且,短链接和长链接的对应关系一般不会频繁修改,所以数据库和缓存的一致性通过简单的旁路缓存模式来保证:

  • 读(Read)数据时,若缓存未命中,则先读 DB,从 DB 中取出数据,放入缓存,同时返回响应;

  • 写(Write)数据时,先更新 DB,再删除缓存。

当用户需要生成短链接时,先到这个映射表中看一下有没有对应的短链接地址。有就直接返回,并将这个 key-value 的过期时间增加一小时;没有就重新生成,并且将对应关系存入这个映射表中。

缓存的淘汰策略可以选用:

  • LRU:Least Recently Used,最近最少使用算法,最近经常被读写的短链地址作为热点数据可以一直存在于缓存,淘汰那些很久没有访问过的短链 key;

  • LFU:Least Frequently Userd,最近最不频繁使用算法,最近访问频率高的短链地址作为热点数据,淘汰那些访问频率较低的短链 key。

2)缓存穿透

但是,使用缓存也防止不了一些异常情况,比如“缓存穿透”。所谓缓存穿透,就是查询一个缓存和数据库中都不存在的短链接,如果并发量很大,就会导致所有在缓存中不存在的请求都打到 MySQL 服务器上,导致服务器处理不了这么多请求而阻塞,甚至崩溃。

所以,为了防止不法分子通过类似“缓存穿透”的方式来攻击服务器,我们可以采用两种方法来应对:

  • 对不存在的短链地址加缓存,key 为短链接地址,value 值为空,过期时间可以设置得短一点;

  • 采用布隆过滤器将已有的短链接多次哈希后存起来,当有短链接请求时,先通过布隆过滤器判断一下该地址是否存在数据库中;如果不在,则说明数据库中不存在该地址,就直接返回。

05 高可用设计

由于缓存和数据库持久化依赖于 Redis 和 MySQL,因此 MySQL 和 Redis 的高可用性必须要保证。

1)MySQL 高可用

MySQL 数据库采用主从复制,进行读写分离。Master 节点进行写操作,Slave 节点用作读操作,并且可以用 Keepalived 来实现高可用。

Keepalived 的原理是采用虚拟 IP,检测入口的多个节点,选用一台热备服务器作为主服务器,并分配给它一个虚拟 IP,外部请求都通过这个虚拟 IP 来访问数据库。

同时,Keepalived 会实时检测多个节点的可用状态,当发现一台服务器宕机或出现故障时,会从集群中将这台服务器踢除。如果这台服务器是主服务器,keepalived 会触发选举操作,从服务器集群中再选出一个服务器充当 master 并分配给它相同的虚拟 IP,以此完成故障转移。

并且,在 Keepalived 的支持下,这些操作都不需要人工参与,只需修复故障机器即可。

2)Redis 高可用

由于在大数据高并发的场景下,写请求全部落在 Redis 的 master 节点上,压力太大。如果一味地采用增加内存和 CPU 这种纵向扩容的方式,那么一台机器所面临的磁盘 IO,网络等压力逐渐增大,也会影响性能。

所以 Redis 采用集群模式,实现数据分片。并且,加入了哨兵机制来保证集群的高可用。它的基本原理是哨兵节点监控集群中所有的主从节点,当主节点宕机或者发生故障以后,哨兵节点会标记它为主观下线;当足够多的哨兵节点将 Redis 主节点标记为主观下线,就将其状态改为客观下线

此时,哨兵节点们通过选举机制选出一个领头哨兵,对 Redis 主节点进行故障转移操作,以保障 Redis 集群的高可用,这整个流程都不需要人工干预。

3)系统容错

服务在上线之前,需要做好充分的业务量评估,以及性能测试。做好限流、熔断和服务降级的逻辑,比如:采用令牌桶算法实现限流,hystrix 框架来做熔断,并且将常用配置放到可以热更新的配置中心,方便对其实时更改。

当业务量过大时,将同步任务改为异步任务处理。通过这些服务治理方案,让系统更加稳定。

06 后记

当我答完最后一个字的时候,面试官看着我,眼神中充满了“欣赏”与疑惑。我想,他应该是被我这番表现给镇住了,此次面试应该是十拿九稳。

但是,出奇地,面试官没有对刚才的架构设计提出评价,只看了看我说:“那今天的面试就到这里,你有什么想要问的吗?”

这下,轮到我震惊了,那到底过不过呢?倒是给句话呀,于是我问道:“通过这次面试,您觉得我有哪些方面需要提升呢?”

“算法和项目需要再多练练,但是我发现了你一个优点。”面试官笑了笑接着说,“八股文背的倒是挺不错的!”

悬着的心总算放下,我心想:“哦,那稳了~”

作者:xin猿意码
来源:juejin.cn/post/7168090412370886686

收起阅读 »

我说MySQL每张表最好不超过2000万数据,面试官让我回去等通知?

事情是这样的下面是我朋友的面试记录:面试官:讲一下你实习做了什么。朋友:我在实习期间做了一个存储用户操作记录的功能,主要是从MQ获取上游服务发送过来的用户操作信息,然后把这些信息存到MySQL里面,提供给数仓的同事使用。由于数据量比较大,每天大概有四五千多万条...
继续阅读 »

事情是这样的

下面是我朋友的面试记录:

面试官:讲一下你实习做了什么。

朋友:我在实习期间做了一个存储用户操作记录的功能,主要是从MQ获取上游服务发送过来的用户操作信息,然后把这些信息存到MySQL里面,提供给数仓的同事使用。由于数据量比较大,每天大概有四五千多万条,所以我还给它做了分表的操作。每天定时生成3张表,然后将数据取模分别存到这三张表里,防止表内数据过多导致查询速度降低。

这表述,好像没什么问题是吧,别急,接着看:

面试官:那你为什么要分三张表呢,两张表不行吗?四张表不行吗?

朋友:因为MySQL每张表最好不超过2000万条数据,否则会导致查询速度降低,影响性能。我们每天的数据大概是在五千万条左右,所以分成三张表比较稳妥。

面试官:还有吗?

朋友: 没有了…… 你干嘛,哎呦

面试官:那你先回去等通知吧。

🤣🤣🤣讲完了,看出什么了吗,你们觉得我这位朋友回答的有什么问题吗?

前言

一般来说,MySQL每张表最好不要超过2000万条数据,否则就会导致性能下降。阿里的Java开发手册上也提出:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

但实际上,这个2000万或者500万都只是一个大概的数字,并不适用于所有场景,如果盲目的以为表数据只要不超过2000万条就没问题了,很可能会导致系统的性能大幅下降。

实际情况下,每张表由于自身的字段不同、字段所占用的空间不同等原因,它们在最佳性能下可以存放的数据量也就不同。

那么,该如何计算出每张表适合的数据量呢?别急,慢慢往下看。

本文适合的读者

阅读本文你需要有一定的MySQL基础,最好对InnoDB和B+树都有一定的了解,可能需要有一年以上的MySQL学习经验(大概一年?),知道 “InnoDB中B+树的高度一般保持在三层以内会比较好”。

本文主要是针对 “InnoDB中高度为3的B+树最多可以存多少数据” 这一话题进行讲解的。且本文对数据的计算比较严格(至少比网上95%以上的相关博文都要严格),如果你比较在意这些细节并且目前不太清楚的话,请继续往下阅读。

阅读本文你大概需要花费10-20分钟的时间,如果你在阅读的过程中对数据进行验算的话,可能要花费30分钟左右。


基础知识快速回顾

众所周知,MySQL中InnoDB的存储结构是B+树,B+树大家都熟悉吧?特性大概有以下几点,一起快速回顾一下吧!

注:下面这这些内容都是精华,看不懂或者不理解的同学建议先收藏本文,之后有知识基础了再回来看 。🤣🤣

  1. 一张数据表一般对应一颗或多颗树的存储,树的数量与建索引的数量有关,每个索引都会有一颗单独的树。

  2. 聚簇索引和非聚簇索引:

    主键索引也是聚簇索引,非主键索引都是非聚簇索引,两种索引的非叶子节点都是只存索引数据的,比如索引为id,那非叶子节点就只存id的数据。

    叶子节点的区别如下:

    • 聚簇索引的叶子节点存的是这条数据的所有字段信息。所以我们 select * from table where id = 1 的时候,都是要去叶子节点拿数据的。

    • 非聚簇索引的叶子节点存的是这条数据所对应的主键信息。比如这条非聚簇索引是username,然后表的主键是id,那该非聚簇索引的叶子节点存的就是id,而不存其他字段。 相当于是先从非聚簇索引查到主键的值,再根据主键索引去查数据内容,一般情况下要查两次(除非索引覆盖),这也称之为 回表 ,就有点类似于存了个指针,指向了数据存放的真实地址。

  3. B+树的查询是从上往下一层层查询的,一般情况下我们认为B+树的高度保持在3层是比较好的,也就是上两层是索引,最后一层存数据,这样查表的时候只需要进行3次磁盘IO就可以了(实际上会少一次,因为根节点会常驻内存)。

    如果数据量过大,导致B+数变成4层了,则每次查询就需要进行4次磁盘IO了,从而使性能下降。所以我们才会去计算InnoDB的3层B+树最多可以存多少条数据。

  4. MySQL每个节点大小默认为16KB,也就是每个节点最多存16KB的数据,可以修改,最大64KB,最小4KB。

    扩展:那如果某一行的数据特别大,超过了节点的大小怎么办?

    MySQL5.7文档的解释是:

    • 对于 4KB、8KB、16KB 和 32KB设置 ,最大行长度略小于数据库页面的一半 ,例如:对于默认的 16KB页大小,最大行长度略小于 8KB 。

    • 而对于 64KB 页面,最大行则长度略小于 16KB。

    • 如果行超过最大行长度, 则将可变长度列用外部页存储,直到该行符合最大行长度限制。 就是说把varchar、text这种长度可变的存到外部页中,来减小这一行的数据长度。

    image-20221108112456250

    文档地址:MySQL :: MySQL 5.7 Reference Manual :: 14.12.2 File Space Management

  5. MySQL查询速度主要取决于磁盘的读写速度,因为MySQL查询的时候每次只读取一个节点到内存中,通过这个节点的数据找到下一个要读取的节点位置,再读取下一个节点的数据,直到查询到需要的数据或者发现数据不存在。

    肯定有人要问了,每个节点内的数据难道不用查询吗?这里的耗时怎么不计算?

    这是因为读取完整个节点的数据后,会存到内存当中,在内存中查询节点数据的耗时其实是很短的,再配合MySQL的查询方式,时间复杂度差不多为 O(log2N)O(log_2N)O(log2N) ,相比磁盘IO来说,可以忽略不计。


MySQL B+树每个节点都存里些什么?

在Innodb的B+树中,我们常说的节点被称之为 页(page),每个页当中存储了用户数据,所有的页合在一起组成了一颗B+树(当然实际会复杂很多,但我们只是要计算可以存多少条数据,所以姑且可以这么理解😅)。

是InnoDB存储引擎管理数据库的最小磁盘单位,我们常说每个节点16KB,其实就是指每页的大小为16KB。

这16KB的空间,里面需要存储 页格式 信息和 行格式 信息,其中行格式信息当中又包含一些元数据和用户数据。所以我们在计算的时候,要把这些数据的都计算在内。

页格式

每一页的基本格式,也就是每一页都会包含的一些信息,总结表格如下:

名称空间含义和作用等
File Header38字节文件头,用来记录页的一些头信息。 包括校验和、页号、前后节点的两个指针、 页的类型、表空间等。
Page Header56字节页头,用来记录页的状态信息。 包括页目录的槽数、空闲空间的地址、本页的记录数、 已删除的记录所占用的字节数等。
Infimum & supremum26字节用来限定当前页记录的边界值,包含一个最小值和一个最大值。
User Records不固定用户记录,我们插入的数据就存储在这里。
Free Space不固定空闲空间,用户记录增加的时候从这里取空间。
Page Directort不固定页目录,用来存储页当中用户数据的位置信息。 每个槽会放4-8条用户数据的位置,一个槽占用1-2个字节, 当一个槽位超过8条数据的时候会自动分成两个槽。
File Trailer8字节文件结尾信息,主要是用来校验页面完整性的。

示意图:


页格式这块的内容,我在官网翻了好久,硬是没找到🤧。。。。不知道是没写还是我眼瞎,有找到的朋友希望可以在评论区帮我挂出来😋。

所以上面页格式的表格内容主要是基于一些博客中学习总结的。

另外,当新记录插入到 InnoDB 聚集索引中时,InnoDB 会尝试留出 1/16 的页面空闲以供将来插入和更新索引记录。如果按顺序(升序或降序)插入索引记录,则生成的页大约可用 15/16 的空间。如果以随机顺序插入记录,则页大约可用 1/2 到 15/16 的空间。参考文档:MySQL :: MySQL 5.7 Reference Manual :: 14.6.2.2 The Physical Structure of an InnoDB Index

除了 User RecordsFree Space 以外所占用的内存是 38+56+26+8=12838 + 56 + 26 + 8 = 12838+56+26+8=128 字节,每一页留给用户数据的空间就还剩 16×1516×1024−128=1523216 \times \frac{15}{16} \times 1024 - 128 = 1523216×1615×1024−128=15232 字节(保留了1/16)。

当然,这是最小值,因为我们没有考虑页目录。页目录留在后面根据再去考虑,这个得根据表字段来计算。

行格式

首先,我觉得有必要提一嘴,MySQL5.6的默认行格式为COMPACT(紧凑),5.7及以后的默认行格式为DYNAMIC(动态),不同的行格式存储的方式也是有区别的,还有其他的两种行格式,本文后续的内容主要是基于DYNAMIC(动态)进行讲解的。

官方文档链接:MySQL :: MySQL 5.7 参考手册 :: 14.11 InnoDB 行格式(包括下面的行格式内容大都可以在里面找到)



每行记录都包含以下这些信息,其中大都是可以从官方文档当中找到的。我这里写的不是特别详细,仅写了一些能够我们计算空间的知识,更详细内容可以去网上搜索 “MySQL 行格式”。

名称空间含义和作用等
行记录头信息5字节行记录的标头信息 包含了一些标志位、数据类型等信息 如:删除标志、最小记录标志、排序记录、数据类型、 页中下一条记录的位置等
可变长度字段列表不固定来保存那些可变长度的字段占用的字节数,比如varchar、text、blob等。 若变长字段的长度小于 255字节,就用1字节表示; 若大于 255字节,用2字节表示。 表字段中有几个可变长字段该列表中就有几个值,如果没有就不存。
null值列表不固定用来存储可以为null的字段是否为null。 每个可为null的字段在这里占用一个bit,就是bitmap的思想。 该列表占用的空间是以字节为单位增长的,例如,如果有 9 到 16 个 可以为null的列,则使用两个字节,没有占用1.5字节这种情况。
事务ID和指针字段6+7字节了解MVCC的朋友应该都知道,数据行中包含了一个6字节的事务ID和 一个7字节的指针字段。 如果没有定义主键,则还会多一个6字节的行ID字段 当然我们都有主键,所以这个行ID我们不计算。
实际数据不固定这部分就是我们真实的数据了。

示意图:


另外还有几点需要注意:

溢出页(外部页)的存储

注意:这一点是DYNAMIC的特性。

当使用 DYNAMIC 创建表时,InnoDB 会将较长的可变长度列(比如 VARCHAR、VARBINARY、BLOB 和 TEXT 类型)的值剥离出来,存储到一个溢出页上,只在该列上保留一个 20 字节的指针指向溢出页。

而 COMPACT 行格式(MySQL5.6默认格式)则是将前 768 个字节和 20 字节的指针存储在 B+ 树节点的记录中,其余部分存储在溢出页上。

列是否存储在页外取决于页大小和行的总大小。当一行太长时,选择最长的列进行页外存储,直到聚集索引记录适合 B+ 树页(文档里没说具体是多少😅)。小于或等于 40 字节的 TEXT 和 BLOB 直接存储在行内,不会分页。

优点

DYNAMIC 行格式避免了用大量数据填充 B+ 树节点从而导致长列的问题。

DYNAMIC 行格式的想法是,如果长数据值的一部分存储在页外,则通常将整个值存储在页外是最有效的。

使用 DYNAMIC 格式,较短的列会尽可能保留在 B+ 树节点中,从而最大限度地减少给定行所需的溢出页数。

字符编码不同情况下的存储

char 、varchar、text 等需要设置字符编码的类型,在计算所占用空间时,需要考虑不同编码所占用的空间。

varchar、text等类型会有长度字段列表来记录他们所占用的长度,但char是固定长度的类型,情况比较特殊,假设字段 name 的类型为 char(10) ,则有以下情况:

  • 对于长度固定的字符编码(比如ASCII码),字段 name 将以固定长度格式存储,ASCII码每个字符占一个字节,那 name 就是占用 10 个字节。

  • 对于长度不固定的字符编码(比如utf8mb4),至少将为 name 保留 10 个字节。如果可以,InnoDB会通过修剪尾部空格空间的方式来将其存到 10 个字节中。

    如果空格剪完了还存不下,则将尾随空格修剪为 列值字节长度的最小值(一般是 1 字节)。

    列的最大长度为: 字符编码的最大字符长度×N字符编码的最大字符长度 \times N字符编码的最大字符长度×N,比如 name 字段的编码为 utf8mb4,那就是 4×104 \times 104×10。

  • 大于或等于 768 字节的 char 列会被看成是可变长度字段(就像varchar一样),可以跨页存储。例如,utf8mb4 字符集的最大字节长度为 4,则 char(255) 列将可能会超过 768 个字节,进行跨页存储。

说实话对char的这个设计我是不太理解的,尽管看了很久,包括官方文档和一些博客🤧,希望懂的同学可以在评论区解惑:

对于长度不固定的字符编码这块,char是不是有点像是一个长度可变的类型了?我们常用的 utf8mb4,占用为 1 ~ 4 字节,那么 char(10) 所占用的空间就是 10 ~ 40 字节,这个变化还是挺大的啊,但是它并没有留足够的空间给它,也没有使用可变长度字段列表去记录char字段的空间占用情况,就很特殊?


开始计算

好了,我们已经知道每一页当中具体存储的东西了,现在我们已经具备计算能力了。

由于页的剩余空间我已经在上面页格式的地方计算过了,每页会剩余 15232 字节可用,下面我们直接计算行。

非叶子节点计算

单个节点计算

索引页就是存索引的节点,也就是非叶子节点。

每一条索引记录当中都包含了当前索引的值一个 6字节 的指针信息一个 5 字节的行标头,用来指向下一层数据页的指针。

索引记录当中的指针占用空间我没在官方文档里找到😭,这个 6 字节是我参考其他博文的,他们说源码里写的是6字节,但具体在哪一段源码我也不知道😭。

希望知道的同学可以在评论区解惑。

假设我们的主键id为 bigint 型,也就是8个字节,那索引页中每行数据占用的空间就等于 8+6+5=198 + 6 + 5 = 198+6+5=19 字节。每页可以存 15232÷19≈80115232 \div 19 \approx 80115232÷19≈801 条索引数据。

那算上页目录的话,按每个槽平均6条数据计算的话,至少有 801÷6≈134801 \div 6 \approx 134801÷6≈134 个槽,需要占用 268 字节的空间。

把存数据的空间分一点给槽的话,我算出来大约可以存 787 条索引数据。

如果是主键是 int 型的话,那可以存更多,大约有 993 条索引数据。

前两层非叶子节点计算

在 B+ 树当中,当一个节点索引记录为 NNN 条时,它就会有 NNN 个子节点。由于我们 3 层B+树的前两层都是索引记录,第一层根节点有 NNN 条索引记录,那第二层就会有 NNN 个节点,每个节点数据类型与根节点一致,仍然可以再存 NNN 条记录,第三层的节点个数就会等于 N2N^2N2。

则有:

  • 主键为 bigint 的表可以存放 7872=619369787 ^ 2 = 6193697872=619369 个叶子节点

  • 主键为 int 的表可以存放 9932=986049993 ^ 2 = 9860499932=986049 个叶子节点

OK计算完毕。

数据条数计算

最少存放记录数

前面我们提到,最大行长度略小于数据库页面的一半,之所以是略小于一半,是由于每个页面还留了点空间给页格式 的其他内容,所以我们可以认为每个页面最少能放两条数据,每条数据略小于8KB。如果某行的数据长度超过这个值,那InnoDB肯定会分一些数据到 溢出页 当中去了,所以我们不考虑。

那每条数据8KB的话,每个叶子节点就只能存放 2 条数据,这样的一张表,在主键为 bigint 的情况下,只能存放 2×619369=12387382 \times 619369 = 12387382×619369=1238738 条数据,也就是一百二十多万条,这个数据量,没想到吧🤣🤣。

较多的存放记录数

假设我们的表是这样的:

-- 这是一张非常普通的课程安排表,除id外,仅包含了课程id和老师id两个字段
-- 且这几个字段均为 int 型(当然实际生产中不会这么设计表,这里只是举例)。

CREATE TABLE `course_schedule` (
`id` int NOT NULL,
`teacher_id` int NOT NULL,
`course_id` int NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

先来分析一下这张表的行数据:无null值列表,无可变长字段列表,需要算上事务ID和指针字段,需要算上行记录头,那么每行数据所占用的空间就是 4+4+4+6+7+5=304 + 4 + 4 + 6 + 7 + 5 = 304+4+4+6+7+5=30 字节,每个叶子节点可以存放 15232÷30≈50715232 \div 30 \approx 50715232÷30≈507 条数据。

算上页目录的槽位所占空间,每个叶子节点可以存放 502 条数据,那么三层B+树可以存放的最大数据量就是 502×986049=494,996,598502 \times 986049 = 494,996,598502×986049=494,996,598,将近5亿条数据!没想到吧🤡😏。

常规表的存放记录数

大部分情况下我们的表字段都不是上面那样的,所以我选择了一场比较常规的表来进行分析,看看能存放多少数据。表情况如下:

CREATE TABLE `blog` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '博客id',
`author_id` bigint unsigned NOT NULL COMMENT '作者id',
`title` varchar(50) CHARACTER SET utf8mb4 NOT NULL COMMENT '标题',
`description` varchar(250) CHARACTER SET utf8mb4 NOT NULL COMMENT '描述',
`school_code` bigint unsigned DEFAULT NULL COMMENT '院校代码',
`cover_image` char(32) DEFAULT NULL COMMENT '封面图',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`release_time` datetime DEFAULT NULL COMMENT '首次发表时间',
`modified_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`status` tinyint unsigned NOT NULL COMMENT '发表状态',
`is_delete` tinyint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `author_id` (`author_id`),
KEY `school_code` (`school_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_general_mysql500_ci ROW_FORMAT=DYNAMIC;

这是我的开源项目“校园博客”(GitHub地址:github.com/stick-i/scb…) 中的博客表,用于存放博客的基本数据。

分析一下这张表的行记录:

  1. 行记录头信息:肯定得有,占用5字节。

  2. 可变长度字段列表:表中 title占用1字节,description占用2字节,共3字节。

  3. null值列表:表中仅school_codecover_imagerelease_time3个字段可为null,故仅占用1字节。

  4. 事务ID和指针字段:两个都得有,占用13字节。

  5. 字段内容信息:

    1. id、author_id、school_code 均为bigint型,各占用8字节,共24字节。

    2. create_time、release_time、modified_time 均为datetime类型,各占8字节,共24字节。

    3. status、is_delete 为tinyint类型,各占用1字节,共2字节。

    4. cover_image 为char(32),字符编码为表默认值utf8,由于该字段实际存的内容仅为英文字母(存url的),结合前面讲的字符编码不同情况下的存储 ,故仅占用32字节。

    5. title、description 分别为varchar(50)、varchar(250),这两个应该都不会产生溢出页(不太确定),字符编码均为utf8mb4,实际生产中70%以上都是存的中文(3字节),25%为英文(1字节),还有5%为4字节的表情😁,则存满的情况下将占用 (50+250)×(0.7×3+0.25×1+0.05×4)=765(50 + 250) \times (0.7 \times 3 + 0.25 \times 1 + 0.05 \times 4 ) = 765(50+250)×(0.7×3+0.25×1+0.05×4)=765 字节。

统计上面的所有分析,共占用 869 字节,则每个叶子节点可以存放 15232÷869≈1715232 \div 869 \approx 1715232÷869≈17 条,算上页目录,仍然能放 17 条。

则三层B+树可以存放的最大数据量就是 17×619369=10,529,27317 \times 619369 = 10,529,27317×619369=10,529,273,约一千万条数据,再次没想到吧👴。

数据计算总结

根据上面三种不同情况下的计算,可以看出,InnoDB三层B+树情况下的数据存储量范围为 一百二十多万条将近5亿条,这个跨度还是非常大的,同时我们也计算了一张博客信息表,可以存储 约一千万条 数据。

所以啊,我们在做项目考虑分表的时候还是得多关注一下表的实际情况,而不是盲目的认为两千万数据都是那个临界点。

面试时如果谈到这块的问题,我想面试官也并不是想知道这个数字到底是多少,而是想看你如何分析这个问题,如何得出这个数字的过程。

如果本文中有任何写的不对的地方,欢迎各位朋友在评论区指正🥰。

写在后面的一些话

这篇文章写了整整两周😭😭(虽然第一周在划水),真的超级干货了,前前后后查了好多资料,也看了好多博文,官方文档有些地方写的确实含糊,我看了好久都没看懂😂😂。

作者:阿杆
来源:juejin.cn/post/7165689453124517896

收起阅读 »

B+树的生成过程 怎么去看懂B+树

前提看B+树 看不懂树结构什么意思 这一篇可以帮你理解树结构生成的过程在说B+ 树之前 需要知道 一页的大小是多少show global status like 'innodb_page_size'这个是看出 一页是 16384 也就是 16384/102...
继续阅读 »

前提

看B+树 看不懂树结构什么意思 这一篇可以帮你理解树结构生成的过程

在说B+ 树之前 需要知道 一页的大小是多少

show global status like 'innodb_page_size'


这个是看出 一页是 16384 也就是 16384/1024 = 16kb innodb中一页的大小默认是 16kb

正文

创建表结构 指定引擎为 Innodb

CREATE TABLE tree(
id int PRIMARY key auto_increment,
t_name VARCHAR(20),
t_code int
) ENGINE=INNODB

查看一下当前表的索引情况

show index from tree 

B树和B+树的显示都是BTREE 但是实际使用的B+树 但是B+树也是B树的升级版 称之为B树也是没有问题的


创建数据 这里会有一个小知识点 如果看过上一篇文章的朋友可以明白是为什么

INSERT into tree VALUES(3,"变成派大星",3);
INSERT into tree VALUES(1,"变成派大星",1);
INSERT into tree VALUES(2,"变成派大星",2);
INSERT into tree VALUES(4,"变成派大星",4);
INSERT into tree VALUES(7,"变成派大星",7);
INSERT into tree VALUES(5,"变成派大星",5);
INSERT into tree VALUES(6,"变成派大星",6);
INSERT into tree VALUES(8,"变成派大星",8);


疑问

为什么创建数据的时候数据是乱的但是在创建好数据 被拍好顺序了

基础知识

我们在寻找答案之前 想明白一些基础知识

细心的朋友可以看出来 我们插入Id 时候数据是乱的 插入进去之后 数据就自动帮我通过Id 进行排序了 这是为什么呢?接着往下看

我们如果对于B+树有点了解的话就知道B+树是每页16KB 进行数据储存 在进行数据查询的时候也是一页一页的去查询

相当于下面的数据

首先每一页都有很多数据 就像 我们平常去写分页的时候我们返回给前端的数据也会有很多属性


这个可能比较抽象 我是把他当成平常 分页查询的思想代入进去

我们可以把一页想成是一个对象

@Data
public class page {

List<UserRecords> data;

....省略其余属性
}

我们先看一下 一页数据的图是什么样子 仅仅是进行逻辑思考画的图

这里的Data 就相当于 一页中的数据区域


但是这里是有限制的 上面我们说到 一页的数据只能是16Kb 也就是 一个Page 里面的data只能16Kb 数据 当超过16Kb 就会新开一个对象相当于在进行创建树的时候增加了判断

Java代码思路模拟:


当 Page 对象的大小已经达到16Kb 就算完成这一页 把这一页放到 磁盘中等待使用就行了 到时候进行查询数据的时候会直接返回这一页 里面包含这些数据

我们回到最初的问题 为什么我们在进行插入的时候明明Id 是乱的 等到插入到数据的时候 数据就变成有序的了 我们知道 同时这个数据是根据主键进行排序的 也就Innobd 的数据储存一定是要依赖主键的 有些人会想 我就是不创建主键 他还能排序吗?

疑问二

我们在疑问一的基础上 产生出的疑问 不设置主键 Mysql怎么办

解答

InnoDB对聚簇索引处理如下:

  • 如果定义了主键,那么InnoDB会使用主键作为聚簇索引

  • 如果没有定义主键,那么会使用第一非空的唯一索引(NOT NULL and UNIQUE INDEX)作为聚簇索引

  • 如果既没有主键也找不到合适的非空索引,InnoDB会自动帮你创建一个不可见的、长度为6字节的row_id,而且InnoDB 维护了一个全局的 dictsys.row_id,所以未定义主键的表都共享该row_id,每次插入一条数据,都把全局row_id当成主键id,然后全局row_id加1

很明显,缺少主键的表,InnoDB会内置一列用于聚簇索引来组织数据。而没有建立主键的话就没法通过主键来进行索引,查询的时候都是全表扫描,小数据量没问题,大数据量就会出现性能问题。

但是,问题真的只是查询影响吗?不是的,对于生成的ROW_ID,其自增的实现来源于一个全局的序列,而所以有ROW_ID的表共享该序列,这也意味着插入的时候生成需要共享一个序列,那么高并发插入的时候为了保持唯一性就避免不了锁的竞争,进而影响性能

解答

我们看完疑问二的解答就知道 即便我们不设置主键 数据也会帮我们去生成一个默认的主键 有点像 类默认生成构造器的思想

有了主键之后呢?


为什么会自动排序 大家都知道了 其实在文章之初就会有很多人明白是为什么 大概脑子里会有答案

疑问三

为什么要进行排序

解答

我们都知道 在进行数据查找的时候 比如几个基础的查找算法的 前提都是 先进行排序 再者 List 和 Map 的一些区别肯定都很熟悉了 当然是为了更快 所以无需的Id 会对插入效率造成影响 也就是为什么很多文章说使用自增Id比UUID 或者雪花算效率高的原因 第一个是UUID他们是随机的 每次都要重新排序 甚至可能会因为排序的原因造成页数据的更换 还有就是 UUID 一般都比较长 一页是16Kb 数据越短 一页的数据就会越多 查询的速度也就比较快

这里说完为什么排序 还有一个点就是上面的页目录

疑问三

页目录的作用是什么?

通过上一篇文章可以明白 页目录的作用是减少范围


这里的第三层是数据 上面都是目录 可以增加数据的检索效率


如果没有目录我们需要去直接遍历数据区域 会降低效率 目录能帮我们缩小范围 这里 我们查询 ID = 3 我们可以通过目录知道 1 < 3 < 4 如果 在1 中没有找到对应数据 但是 因为 3 < 4 就不会接着往下查询了 直接返回空结果

当第一页没有的时候去第二页查询 不会直接跳到第二页查询


为了提高效率 当目录数据数量过多时 就会网上延伸一层树 同时可以减少磁盘的IO次数


关于所有叶子节点都处于同一深度是如何实现的?这与B+树具体的插入和删除算法有关。简单解释一下插入时的情况,根据插入值的大小,逐步向下直到对应的叶子节点。如果叶子节点关键字个数小于2t,则直接插入值或者更新卫星数据;如果插入之前叶子节点已经满了,则分裂该叶子节点成两半,并把中间值提上到父节点的关键字中,如果这导致父节点满了的话,则把该父节点分裂,如此递归向上。所以树高是一层层的增加的,叶子节点永远都在同一深度。

小总结

  • 内部节点并不存储真正的信息,而是保存其叶子节点的最小值作为索引。

  • 每次插入删除都进行更新(此时用到parent指针),保持最新状态。

  • B+ 树非叶子节点上是不存储数据的,仅存储键值

  • B+只在底层树储存数据,上层就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

  • B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。

  • 一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。

  • 因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。

  • 那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单

  • 有心的读者可能还发现上图 B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。

  • 其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。

  • 我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。

作者:变成派大星
来源:juejin.cn/post/7162856738692005918

收起阅读 »

一个 MySQL 隐式转换的坑,差点把服务器整崩溃了

本来是一个平静而美好的下午,其他部门的同事要一份数据报表临时汇报使用,因为系统目前没有这个维度的功能,所以需要写个SQL马上出一下,一个同事接到这个任务,于是开始在测试环境拼装这条 SQL,刚过了几分钟,同事已经自信的写好了这条SQL,于是拿给DBA,到线上跑...
继续阅读 »

本来是一个平静而美好的下午,其他部门的同事要一份数据报表临时汇报使用,因为系统目前没有这个维度的功能,所以需要写个SQL马上出一下,一个同事接到这个任务,于是开始在测试环境拼装这条 SQL,刚过了几分钟,同事已经自信的写好了这条SQL,于是拿给DBA,到线上跑一下,用客户端工具导出Excel 就好了,毕竟是临时方案嘛。

就在SQL执行了之后,意外发生了,先是等了一下,发现还没执行成功,猜测可能是数据量大的原因,但是随着时间滴滴答答流逝,逐渐意识到情况不对了,一看监控,CPU已经上去了,但是线上数据量虽然不小,也不至于跑成这样吧,眼看着要跑死了,赶紧把这个事务结束掉了。

什么原因呢?查询的条件和 join 连接的字段基本都有索引,按道理不应该这样啊,于是赶紧把SQL拿下来,也没看出什么问题,于是限制查询条数再跑了一次,很快出结果了,但是结果却大跌眼镜,出来的查询结果并不是预期的。


经过一番检查之后,最终发现了问题所在,是 join 连接中有一个字段写错了,因为这两个字段有一部分名称是相同的,于是智能的 SQL 客户端给出了提示,顺手就给敲上去了。但是接下来,更让人迷惑了,因为要连接的字段是 int 类型,而写错的这个字段是 varchar 类型,难道不应该报错吗?怎么还能正常执行,并且还有预期外的查询结果?

难道是 MySQL 有 bug 了,必须要研究一下了。

复现当时的情景

假设有两张表,这两张表的结构和数据是下面这样的。

第一张 user表。

CREATE TABLE `user` (
 `id` int(11NOT NULL AUTO_INCREMENT,
 `name` varchar(50COLLATE utf8_bin DEFAULT NULL,
 `age` int(3DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


INSERT INTO `user` VALUES (1'张三'28'2022-09-06 07:40:56''2022-09-06 07:40:59');


第二张 order

CREATE TABLE `order` (
 `id` int(11NOT NULL AUTO_INCREMENT,
 `user_id` int(11DEFAULT NULL,
 `order_code` varchar(64COLLATE utf8_bin DEFAULT NULL,
 `money` decimal(20,0DEFAULT NULL,
 `title` varchar(255COLLATE utf8_bin DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


INSERT INTO `order` VALUES (12'1d90530e-6ada-47c1-b2fa-adba4545aabd'100'xxx购买两件商品''2022-09-06 07:42:25''2022-09-06 07:42:27');


目的是查看所有用户的 order 记录,假设数据量比较少,可以直接查,不考虑性能问题。

本来的 SQL 语句应该是这样子的,查询 order表中用户iduser_iduser表的记录。

select o.* from `user` u 
left JOIN `order` o on u.id = o.user_id;

但是呢,因为手抖,将 on 后面的条件写成了 u.id = o.order_code,完全关联错误,这两个字段完全没有联系,而且u.id是 int 类型,o.order_codevarchar类型。

select o.* from `user` u 
left JOIN `order` o on u.id = o.order_code;

这样的话, 当我们执行这条语句的时候,会不会查出数据来呢?

我的第一感觉是,不仅不会查出数据,而且还会报错,因为连接的这两个字段类型都不一样,值更不一样。

结果却被啪啪打脸,不仅没有报错,而且还查出了数据。


可以把这个问题简化一下,简化成下面这条语句,同样也会出现问题。

select * from `order` where order_code = 1;


明明这条记录的 order_code 字段的值是 1d90530e-6ada-47c1-b2fa-adba4545aabd,怎么用 order_code=1的条件就把它给查出来了。

根源所在

相信有的同学已经猜出来了,这里是 MySQL 进行了隐式转换,由于查询条件后面跟的查询值是整型的,所以 MySQL 将 order_code字段进行了字符串到整数类型的转换,而转换后的结果正好是 1

通过 cast函数转换验证一下结果。

select cast('1d90530e-6ada-47c1-b2fa-adba4545aabd' as unsigned);


再用两条 SQL 看一下字符串到整数类型转换的规则。

select cast('223kkk' as unsigned);
select cast('k223kkk' as unsigned);


223kkk转换后的结果是 223,而k223kkk转换后的结果是0。总结一下,转换的规则是:

1、从字符串的左侧开始向右转换,遇到非数字就停止;

2、如果第一个就是非数字,最后的结果就是0;

隐式转换的规则

当操作符与不同类型的操作数一起使用的时候,就会发生隐式转换。

例如算数运算符的前后是不同类型时,会将非数字类型转换为数字,比如 '5a'+2,就会将5a转换为数字类型,然后和2相加,最后的结果就是 7 。


再比如 concat函数是连接两个字符串的,当此函数的参数出现非字符串类型时,就会将其转换为字符串,例如concat(88,'就是发'),最后的结果就是 88就是发


MySQL 官方文档有以下几条关于隐式转换的规则:

1、两个参数至少有一个是 NULL 时,比较的结果也是 NULL,例外是使用 <=> 对两个 NULL 做比较时会返回 1,这两种情况都不需要做类型转换;

也就是两个参数中如果只有一个是NULL,则不管怎么比较结果都是 NULL,而两个 NULL 的值不管是判断大于、小于或等于,其结果都是1。

2、两个参数都是字符串,会按照字符串来比较,不做类型转换;

3、两个参数都是整数,按照整数来比较,不做类型转换;

4、十六进制的值和非数字做比较时,会被当做二进制字符串;

例如下面这条语句,查询 user 表中name字段是 0x61 的记录,0x是16进制写法,其对应的字符串是英文的 'a',也就是它对应的 ASCII 码。

select * from user where name = 0x61;

所以,上面这条语句其实等同于下面这条

select * from user where name = 'a';

可以用 select 0x61;验证一下。

5、有一个参数是 TIMESTAMP 或 DATETIME,并且另外一个参数是常量,常量会被转换为 时间戳;

例如下面这两条SQL,都是将条件后面的值转换为时间戳再比较了,只不过


6、有一个参数是 decimal 类型,如果另外一个参数是 decimal 或者整数,会将整数转换为 decimal 后进行比较,如果另外一个参数是浮点数(一般默认是 double),则会把 decimal 转换为浮点数进行比较;

在不同的数值类型之间,总是会向精度要求更高的那一个类型转换,但是有一点要注意,在MySQL 中浮点数的精度只有53 bit,超过53bit之后的话,如果后面1位是1就进位,如果是0就直接舍弃。所以超大浮点数在比较的时候其实只是取的近似值。

7、所有其他情况下,两个参数都会被转换为浮点数再进行比较;

如果不符合上面6点规则,则统一转成浮点数再进行运算

避免进行隐式转换

我们在平时的开发过程中,尽量要避免隐式转换,因为一旦发生隐式转换除了会降低性能外, 还有很大可能会出现不期望的结果,就像我最开始遇到的那个问题一样。

之所以性能会降低,还有一个原因就是让本来有的索引失效。

select * from `order` where order_code = 1;

order_code 是 varchar 类型,假设我已经在 order_code 上建立了索引,如果是用“=”做查询条件的话,应该直接命中索引才对,查询速度会很快。但是,当查询条件后面的值类型不是 varchar,而是数值类型的话,MySQL 首先要对 order_code 字段做类型转换,转换为数值类型,这时候,之前建的索引也就不会命中,只能走全表扫描,查询性能指数级下降,搞不好,数据库直接查崩了。

作者:古时的风筝
来源:juejin.cn/post/7161991470268825631

收起阅读 »

一台服务器最大能支持多少条TCP连接

一、一台服务器最大能打开的文件数1、限制参数我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:fs.file-max (系统级别参数):该参数描述了整个系统可以打开的最大文件数量。但...
继续阅读 »

一、一台服务器最大能打开的文件数

1、限制参数

我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:

  • fs.file-max (系统级别参数):该参数描述了整个系统可以打开的最大文件数量。但是root用户不会受该参数限制(比如:现在整个系统打开的文件描述符数量已达到fs.file-max ,此时root用户仍然可以使用ps、kill等命令或打开其他文件描述符)

  • soft nofile(进程级别参数):限制单个进程上可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同的值

  • fs.nr_open(进程级别参数):限制单个进程上可以打开的最大文件数。可以针对不同用户配置不同的值

这三个参数之间还有耦合关系,所以配置值的时候还需要注意以下三点:

  1. 如果想加大soft nofile,那么hard nofile参数值也需要一起调整。如果因为hard nofile参数值设置的低,那么soft nofile参数的值设置的再高也没有用,实际生效的值会按照二者最低的来。

  2. 如果增大了hard nofile,那么fs.nr_open也都需要跟着一起调整(fs.nr_open参数值一定要大于hard nofile参数值)。如果不小心把hard nofile的值设置的比fs.nr_open还大,那么后果比较严重。会导致该用户无法登录,如果设置的是*,那么所有用户都无法登录

  3. 如果加大了fs.nr_open,但是是用的echo "xxx" > ../fs/nr_open命令来修改的fs.nr_open的值,那么刚改完可能不会有问题,但是只要机器一重启,那么之前通过echo命令设置的fs.nr_open值便会失效,用户还是无法登录。所以非常不建议使用echo的方式修改内核参数!!!

2、调整服务器能打开的最大文件数示例

假设想让进程可以打开100万个文件描述符,这里用修改conf文件的方式给出一个建议。如果日后工作里有类似的需求可以作为参考。

  • vim /etc/sysctl.conf

fs.file-max=1100000 // 系统级别设置成110万,多留点buffer
fs.nr_open=1100000 // 进程级别也设置成110万,因为要保证比 hard nofile大
  • 使上面的配置生效sysctl -p

  • vim /etc/security/limits.conf

// 用户进程级别都设置成100完
soft nofile 1000000
hard nofile 1000000

二、一台服务器最大能支持多少连接

我们知道TCP连接,从根本上看其实就是client和server端在内存中维护的一组【socket内核对象】(这里也对应着TCP四元组:源IP、源端口、目标IP、目标端口),他们只要能够找到对方,那么就算是一条连接。那么一台服务器最大能建立多少条连接呢?

  • 由于TCP连接本质上可以理解为是client-server端的一对socket内核对象,那么从理论上将应该是【2^32 (ip数) * 2^16 (端口数)】条连接(约等于两百多万亿)

  • 但是实际上由于受其他软硬件的影响,我们一台服务器不可能能建立这么多连接(主要是受CPU和内存限制)。

如果只以ESTABLISH状态的连接来算(这些连接只是建立,但是不收发数据也不处理相关的业务逻辑)那么一台服务器最大能建立多少连接呢?以一台4GB内存的服务器为例!

  • 这种情况下,那么能建立的连接数量主要取决于【内存的大小】(因为如果是)ESTABLISH状态的空闲连接,不会消耗CPU(虽然有TCP保活包传输,但这个影响非常小,可以忽略不计)

  • 我们知道一条ESTABLISH状态的连接大约消耗【3.3KB内存】,那么通过计算得知一台4GB内存的服务器,【可以建立100w+的TCP连接】(当然这里只是计算所有的连接都只建立连接但不发送和处理数据的情况,如果真实场景中有数据往来和处理(数据接收和发送都需要申请内存,数据处理便需要CPU),那便会消耗更高的内存以及占用更多的CPU,并发不可能达到100w+)

上面讨论的都是进建立连接的理想情况,在现实中如果有频繁的数据收发和处理(比如:压缩、加密等),那么一台服务器能支撑1000连接都算好的了,所以一台服务器能支撑多少连接还要结合具体的场景去分析,不能光靠理论值去算。抛开业务逻辑单纯的谈并发没有太大的实际意义

服务器的开销大头往往并不是连接本身,而是每条连接上的数据收发,以及请求业务逻辑处理!!!

三、一台客户端机器最多能发起多少条连接

我们知道客户端每和服务端建立一个连接便会消耗掉client端一个端口。一台机器的端口范围是【0 ~ 65535】,那么是不是说一台client机器最多和一台服务端机器建立65535个连接呢(这65535个端口里还有很多保留端口,可用端口可能只有64000个左右)?

由TCP连接的四元组特性可知,只要四元组里某一个元素不同,那么就认为这是不同的TCP连接。所以需要分情况讨论:

  • 【情况一】、如果一台client仅有一个IP,server端也仅有一个IP并且仅启动一个程序,监听一个端口的情况下,client端和这台server端最大可建立的连接条数就是 65535 个。

    • 因为源IP固定,目标IP和端口固定,四元组中唯一可变化的就是【源端口】,【源端口】的可用范围又是【0 ~ 65535】,所以一台client机器最大能建立65535个连接

  • 【情况二】、如果一台client有多个IP(假设客户端有 n 个IP),server端仅有一个IP并且仅启动一个程序,监听一个端口的情况下,一台client机器最大能建立的连接条数是:n * 65535

    • 因为目标IP和端口固定,有 n 个源IP,四元组中可变化的就是【源端口】+ 【源IP】,【源端口】的可用范围又是【0 ~ 65535】,所以一个IP最大能建立65535个连接,那么n个IP最大就能建立n * 65535个连接了

    • 以现在的技术,给一个client分配多个IP是非常容易的事情,只需要去联系你们网管就可以做到。

  • 【情况三】、如果一台client仅有一个IP,server端也仅有一个IP但是server端启动多个程序,每个程序监听一个端口的情况下(比如server端启动了m个程序,监听了m个不同端口),一台client机器最大能建立的连接数量为:65535 * m

    • 源IP固定,目标IP固定,目标端口数量为m个,可变化的是源端口,而源端口变化范围是【0 ~ 65535】,所以一台client机器最大能建立的TCP连接数量是 65535 * m

  • 其余情况类推,但是客户端的可用端口范围一般达不到65535个,受内核参数net.ipv4.ip_local_port_range限制,如果要修改client所能使用的端口范围,可以修改这个内核参数的值。

  • 所以,不光是一台server端可以接收100w+个TCP连接,一台client照样能发出100w+个连接

四、其他

  • 三次握手里socket的全连接队列长度由参数net.core.somaxconn来控制,默认大小是128,当两台机器离的非常近,但是建立连接的并发又非常高时,可能会导致半连接队列或全连接队列溢出,进而导致server端丢弃握手包。然后造成client超时重传握手包(至少1s以后才会重传),导致三次握手连接建立耗时过长。我们可以调整参数net.core.somaxconn来增加去按连接队列的长度,进而减小丢包的影响

  • 有时候我们通过 ctrl + c方式来终止了某个进程,但是当重启该进程的时候发现报错端口被占用,这种问题是因为【操作系统还没有来得及回收该端口,等一会儿重启应用就好了】

  • client程序在和server端建立连接时,如果client没有调用bind方法传入指定的端口,那么client在和server端建立连接的时候便会自己随机选择一个端口来建立连接。一旦我们client程序调用了bind方法传入了指定的端口,那么client将会使用我们bind里指定的端口来和server建立连接。所以不建议client调用bind方法,bind函数会改变内核选择端口的策略

    public static void main(String[] args) throws IOException {
       SocketChannel sc = SocketChannel.open();
    // 客户端还可以调用bind方法
       sc.bind(new InetSocketAddress("localhost", 9999));
       sc.connect(new InetSocketAddress("localhost", 8080));
       System.out.println("waiting..........");
    }
  • 在Linux一切皆文件,当然也包括之前TCP连接中说的socket。进程打开一个socket的时候需要创建好几个内核对象,换一句直白的话说就是打开文件对象吃内存,所以Linux系统基于安全角度考虑(比如:有用户进程恶意的打开无数的文件描述符,那不得把系统搞奔溃了),在多个位置都限制了可打开的文件描述符的数量

  • 内核是通过【hash表】的方式来管理所有已经建立好连接的socket,以便于有请求到达时快速的通过【TCP四元组】查找到内核中对应的socket对象

    • 在epoll模型中,通过红黑树来管理epoll对象所管理的所有socket,用红黑树结构来平衡快速删除、插入、查找socket的效率

五、相关实际问题

在网络开发中,很多人对一个基础问题始终没有彻底搞明白,那就是一台机器最多能支撑多少条TCP连接。不过由于客户端和服务端对端口使用方式不同,这个问题拆开来理解要容易一些。

注意,这里说的是客户端和服务端都只是角色,并不是指某一台具体的机器。例如对于我们自己开发的应用程序来说,当他响应客户端请求的时候,他就是服务端。当他向MySQL请求数据的时候,他又变成了客户端。

1、"too many open files" 报错是怎么回事,该如何解决

你在线上可能遇到过too many open files这个错误,那么你理解这个报错发生的原理吗?如果让你修复这个错误,应该如何处理呢?

  • 因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制的打开了过多文件而让整个服务器奔溃,Linux对打开的文件描述符数量有限制。如果你的进程触发到内核的限制,那么"too many open files" 报错就产生了

  • 可以通过修改fs.file-maxsoft nofilefs.nr_open这三个参数的值来修改进程能打开的最大文件描述符数量

    • 需要注意这三个参数之间的耦合关系!

2、一台服务端机器最大究竟能支持多少条连接

因为这里要考虑的是最大数,因此先不考虑连接上的数据收发和处理,仅考虑ESTABLISH状态的空连接。那么一台服务端机器上最大可以支持多少条TCP连接?这个连接数会受哪些因素的影响?

  • 在不考虑连接上数据的收发和处理的情况下,仅考虑ESTABLISH状态下的空连接情况下,一台服务器上最大可支持的TCP连接数量基本上可以说是由内存大小来决定的。

  • 四元组唯一确定一条连接,但服务端可以接收来自任意客户端的请求,所以根据这个理论计算出来的数字太大,没有实际意义。另外文件描述符限制其实也是内核为了防止某些应用程序不受限制的打开【文件句柄】而添加的限制。这个限制只要修改几个内核参数就可以加大。

  • 一个socket大约消耗3kb左右的内存,这样真正制约服务端机器最大并发数的就是内存,拿一台4GB内存的服务器来说,可以支持的TCP连接数量大约是100w+

3、一条客户端机器最大究竟能支持多少条连接

和服务端不同的是,客户端每次建立一条连接都需要消耗一个端口。在TCP协议中,端口是一个2字节的整数,因此范围只能是0~65535。那么客户单最大只能支持65535条连接吗?有没有办法突破这个限制,有的话有哪些办法?

  • 客户度每次建立一条连接都需要消耗一个端口。从数字上来看,似乎最多只能建立65535条连接。但实际上我们有两种办法破除65535这个限制

    • 方式一,为客户端配置多IP

    • 方式二,分别连接不同的服务端

  • 所以一台client发起百万条连接是没有任何问题的

4、做一个长连接推送产品,支持1亿用户需要多少台机器

假设你是系统架构师,现在老板给你一个需求,让你做一个类似友盟upush这样的产品。要在服务端机器上保持一个和客户端的长连接,绝大部分情况下连接都是空闲的,每天也就顶多推送两三次左右。总用户规模预计是1亿。那么现在请你来评估一下需要多少台服务器可以支撑这1亿条长连接。

  • 对于长连接推送模块这种服务来说,给客户端发送数据只是偶尔的,一般一天也就顶多一两次。绝大部分情况下TCP连接都是空闲的,CPU开销可以忽略

  • 再基于内存来考虑,加色服务器内存是128G的,那么一台服务器可以考虑支持500w条并发。这样会消耗掉大约不到20GB内存用来保存这500w条连接对应的socket。还剩下100GB以上的内存来应对接收、发送缓冲区等其他的开销足够了。所以,一亿用户,仅仅需要20台服务器就差不多够用了!

参考:《深入理解Linux网络》

作者:文攀
来源:juejin.cn/post/7162824884597293086

收起阅读 »

2023 届秋招回顾,寒气逼人。。。

自我介绍我来自杭州的一所双非一本学校,是一名普通的本科生,专业【软件工程】。初学编程事实上,我从高中毕业起就开始思考未来的工作了,一开始网上都是 Python 相关的新闻,因此从高中毕业的暑假就开始学 Python,当时在新华书店,捧着一本入门书天天看;但是看...
继续阅读 »

自我介绍

我来自杭州的一所双非一本学校,是一名普通的本科生,专业【软件工程】。

初学编程

事实上,我从高中毕业起就开始思考未来的工作了,一开始网上都是 Python 相关的新闻,因此从高中毕业的暑假就开始学 Python,当时在新华书店,捧着一本入门书天天看;

但是看了并没有什么用,除了大一的时候吹牛皮,啥都没学到。

然后自 2020 年初(大一寒假) 疫情爆发,学校线上授课;课程中有【面向对象语言】的学习,自此开始正式的跟着视频学习 Java 了。

第一次实习

2021年暑假(大二暑假),我的绩点排名在学校保研线边缘徘徊,但又不愿去刷那些水课的绩点,因此决定考研或者工作,期间比较迷茫。

当时在网上得到一位大数据方向前辈的指点,他说了一句话:“早,就是优势。”

因此,我决定先去实习,当时在杭州人工智能小镇找了家公司实习。

虽说是实习,但其实基本每天上班啥也不干,主管也没分配任务,就是一直在看书,期间看完了周志明老师的 JVM,以及几本讲并发编程的书。

第二次实习

大三上时,眼看着 Java 越来越卷,自己开始学习了大数据相关的组件,像 Hadoop、HBase、Flume 等等组件,一直学到了实时计算之前。

大三下时,我明白自己是一个心态非常不稳定的人,考研对我来说,最后几个月会非常的难熬,并且考研失败的风险也让我望而却步,因此下定决心本科就业!

寒假的时候跟着视频完成了【谷粒商城】那个项目,之后立刻着手准备找实习。

也就是在这第二段实习过程中(2022上半年),我真正的学到了一些实际的开发技巧。

实习期间,看完了几本深入讲中间件 ZK、Redis、Spring源码 和 代码重构的书。

本次实习,让我受益良多,由衷感谢我的 mentor(导师)和主管!

秋招情况

我从 6 月底开始复习准备,因为准备得比较晚,所以基本没参加提前批。

正式批总共投递了近 150 家公司,笔试了 30 家,面试了 15 个公司,除了海康威视,其他基本都意向或排序了。

大致情况如下:

  • offer:兴业数金

  • 意向:猿辅导,Aloudata

  • 排序 / 审批:华为,网易雷火,荣耀,招银网络,古茗奶茶,CVTE,以及一众独角兽公司

  • 面试挂:海康威视

CVTE 提前批面试(已拒)


大应科技(OC)


e签宝 提前批(已拒)


荣耀 Honor(录用决策中)


猿辅导(OC)


趣链科技(流程中)


海康威视(已挂)


SMART(已拒)


寒王厂(泡池子)


网易雷火(排序中)


招银网络(流程中)


古茗奶茶(流程中)


复习方式

关于焦虑

我们先要肯定一点,在复习的时候,【焦虑】是一件必然的事情,我们要正视焦虑。

就拿我自己举例子吧,【双非本科】的学历会把我放到一个最最糟糕的位置。

自开始复习时,我内心就非常非常的焦虑,胸膛经常会像要爆炸一样的沉闷(真的)...

而我的缓解方式主要分为两种吧:

  • 运动

    • 背一会八股或者刷一会题之后就去走走

    • 每天晚上去操场跑步

  • 心理慰藉

    • 面试前,我会像《三傻大闹宝莱坞》里的阿米尔汗一样,拍着自己的胸口对自己说 “Aal izz well”

    • 给自己想好一个下下策,如果秋招真的找不到工作该怎么办?那至少还有春招,对比明年考研失利的同学,我至少积累了经验!

复习流程

我的整体复习流程分为三步:

  • 处理基础知识

  • 看八股

  • 查漏补缺

阶段一:处理基础知识

对于基础知识部分,我自知《计网》和《操作系统》这两门课学的很差,所以一开始就复习这部分知识。

当时先把两门课的教材翻了一遍,然后做了一些摘抄,但说实话基本没用。

这部分知识,我在面试过程中,大概有 50% 的几率会被问到操作系统,但从来没被问到过计网(幸运)。

之后复习《设计模式》,先跟着一个 csdn 上的博客边看别写,之后找了一个很老的(2003年)博客总结,反复背诵,基本能手写大部分的模式实现了。

这部分知识,我在面试过程中,要求写过 单例 、三大工厂 和 发布订阅 的实现,问过项目中和 Spring 以及其它中间件中用到的设计模式。

阶段二:看八股

全面进军 Java 八股文。

我先看了自己在实习前准备的那些文档,之后网上找了 JavaGuide、JavaKeeper 这两份文档作为补充。

因为自己之前有过两段的实习经验,因此背过很多次八股。

但考虑到本次秋招可能会把战线拉得比较长,因此就自己总结了一份脑图。


阶段三:查漏补缺

经过几轮面试,逐渐察觉到了自己的一些不足,之后针对性的去完善了一下。

这里随便列举几个点,供其它同学参考:

  • 为什么说进程切换开销比线程大?

  • NIO到底有没有阻塞,NIO到底能不能提高 IO 效率?

  • Redis分布式锁的限制,RedLock的实现?

  • ZK 明明有了有序的指令队列,为什么还要用 zxid来辅助排序?

  • basic paxos 和 multi paxos 的使用?

  • 为什么拜占庭将军无解?

  • 还有一些业务场景的选择问题。。。

总结

我一直提醒自己:你是一个双非本科生,这个秋招你如果再不拼命,你就要完蛋了。

我想,我是幸运的:

  • 我很幸运 在实习的时候,有一个好的 mentor,带我开发了字节码相关的组件,让我的简历不容易挂;

  • 我很幸运 在复习的时候,有几位好的朋友,分享经验,加油鼓励,让我没有被焦虑击倒;

  • 我很幸运 在面试的时候,有无私的舍友们,能在我需要笔试面试时,把宿舍让给我,让我没有后顾之忧;

当然,也会有遗憾。每个人心中都有着大厂梦,而今年进大厂确实很难:

  • 我从大一开始就非常渴望进入阿里巴巴,实习的时候五面阿里不得,秋招全部简历挂;

  • 百度+度小满,投了 4 个岗位,全部简历挂;

  • 字节,一开始担心算法没敢投,之后担心基础知识也没敢投,也很遗憾了;

人生,有所得就有所失,有所失就有所得。

最后,想给其他明后年参加秋招的同学一些提醒:

  • 一定要早做准备,早点实习,早点刷算法题,早就是优势;

  • 人生无常,意外太多,绝对不要 all in 一家公司;

  • 鞋合不合适只有脚知道,自己总结的八股会更适合自己;

  • 多刷 力扣 Hot 100,或者 Codetop 热门题,反复刷;

  • 选择大于努力;

在寒气逼人的 2022,我们需要抱团取暖...

作者:OliQ
链接:http://www.cnblogs.com/yuanchuziwen/p/16770895.html

收起阅读 »

看完这篇HTTP,跟面试官扯皮就没问题了

web
最初在有网络之前,我们的电脑都是单机的,单机系统是孤立的,我还记得 05 年前那会儿家里有个电脑,想打电脑游戏还得两个人在一个电脑上玩儿,及其不方便。我就想为什么家里人不让上网,我的同学 xxx 家里有网,每次一提这个就落一通批评:xxx上xxx什xxxx么x...
继续阅读 »

我是一名程序员,我的主要编程语言是 Java,我更是一名 Web 开发人员,所以我必须要了解 HTTP,所以本篇文章就来带你从 HTTP 入门到进阶,看完让你有一种恍然大悟、醍醐灌顶的感觉。

最初在有网络之前,我们的电脑都是单机的,单机系统是孤立的,我还记得 05 年前那会儿家里有个电脑,想打电脑游戏还得两个人在一个电脑上玩儿,及其不方便。我就想为什么家里人不让上网,我的同学 xxx 家里有网,每次一提这个就落一通批评:xxx上xxx什xxxx么xxxx网xxxx看xxxx你xxxx考xxxx的xxxx那xxxx点xxxx分。虽然我家里没有上网,但是此时互联网已经在高速发展了,HTTP 就是高速发展的一个产物。

认识 HTTP

首先你听的最多的应该就是 HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),这你一定能说出来,但是这样还不够,假如你是大厂面试官,这不可能是他想要的最终结果,我们在面试的时候往往把自己知道的尽可能多的说出来,才有和面试官谈价钱的资本。那么什么是超文本传输协议?

超文本传输协议可以进行文字分割:超文本(Hypertext)、传输(Transfer)、协议(Protocol),它们之间的关系如下

img

按照范围的大小 协议 > 传输 > 超文本。下面就分别对这三个名次做一个解释。

什么是超文本

在互联网早期的时候,我们输入的信息只能保存在本地,无法和其他电脑进行交互。我们保存的信息通常都以文本即简单字符的形式存在,文本是一种能够被计算机解析的有意义的二进制数据包。而随着互联网的高速发展,两台电脑之间能够进行数据的传输后,人们不满足只能在两台电脑之间传输文字,还想要传输图片、音频、视频,甚至点击文字或图片能够进行超链接的跳转,那么文本的语义就被扩大了,这种语义扩大后的文本就被称为超文本(Hypertext)

什么是传输

那么我们上面说到,两台计算机之间会形成互联关系进行通信,我们存储的超文本会被解析成为二进制数据包,由传输载体(例如同轴电缆,电话线,光缆)负责把二进制数据包由计算机终端传输到另一个终端的过程(对终端的详细解释可以参考 你说你懂互联网,那这些你知道么?这篇文章)称为传输(transfer)

通常我们把传输数据包的一方称为请求方,把接到二进制数据包的一方称为应答方。请求方和应答方可以进行互换,请求方也可以作为应答方接受数据,应答方也可以作为请求方请求数据,它们之间的关系如下

img

如图所示,A 和 B 是两个不同的端系统,它们之间可以作为信息交换的载体存在,刚开始的时候是 A 作为请求方请求与 B 交换信息,B 作为响应的一方提供信息;随着时间的推移,B 也可以作为请求方请求 A 交换信息,那么 A 也可以作为响应方响应 B 请求的信息。

什么是协议

协议这个名词不仅局限于互联网范畴,也体现在日常生活中,比如情侣双方约定好在哪个地点吃饭,这个约定也是一种协议,比如你应聘成功了,企业会和你签订劳动合同,这种双方的雇佣关系也是一种 协议。注意自己一个人对自己的约定不能成为协议,协议的前提条件必须是多人约定。

那么网络协议是什么呢?

网络协议就是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为网络协议。

没有网络协议的互联网是混乱的,就和人类社会一样,人不能想怎么样就怎么样,你的行为约束是受到法律的约束的;那么互联网中的端系统也不能自己想发什么发什么,也是需要受到通信协议约束的。

那么我们就可以总结一下,什么是 HTTP?可以用下面这个经典的总结回答一下: HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

与 HTTP 有关的组件

随着网络世界演进,HTTP 协议已经几乎成为不可替代的一种协议,在了解了 HTTP 的基本组成后,下面再来带你进一步认识一下 HTTP 协议。

网络模型

网络是一个复杂的系统,不仅包括大量的应用程序、端系统、通信链路、分组交换机等,还有各种各样的协议组成,那么现在我们就来聊一下网络中的协议层次。

为了给网络协议的设计提供一个结构,网络设计者以分层(layer)的方式组织协议,每个协议属于层次模型之一。每一层都是向它的上一层提供服务(service),即所谓的服务模型(service model)。每个分层中所有的协议称为 协议栈(protocol stack)。因特网的协议栈由五个部分组成:物理层、链路层、网络层、运输层和应用层。我们采用自上而下的方法研究其原理,也就是应用层 -> 物理层的方式。

应用层

应用层是网络应用程序和网络协议存放的分层,因特网的应用层包括许多协议,例如我们学 web 离不开的 HTTP,电子邮件传送协议 SMTP、端系统文件上传协议 FTP、还有为我们进行域名解析的 DNS 协议。应用层协议分布在多个端系统上,一个端系统应用程序与另外一个端系统应用程序交换信息分组,我们把位于应用层的信息分组称为 报文(message)

运输层

因特网的运输层在应用程序断点之间传送应用程序报文,在这一层主要有两种传输协议 TCPUDP,利用这两者中的任何一个都能够传输报文,不过这两种协议有巨大的不同。

TCP 向它的应用程序提供了面向连接的服务,它能够控制并确认报文是否到达,并提供了拥塞机制来控制网络传输,因此当网络拥塞时,会抑制其传输速率。

UDP 协议向它的应用程序提供了无连接服务。它不具备可靠性的特征,没有流量控制,也没有拥塞控制。我们把运输层的分组称为 报文段(segment)

网络层

因特网的网络层负责将称为 数据报(datagram) 的网络分层从一台主机移动到另一台主机。网络层一个非常重要的协议是 IP 协议,所有具有网络层的因特网组件都必须运行 IP 协议,IP 协议是一种网际协议,除了 IP 协议外,网络层还包括一些其他网际协议和路由选择协议,一般把网络层就称为 IP 层,由此可知 IP 协议的重要性。

链路层

现在我们有应用程序通信的协议,有了给应用程序提供运输的协议,还有了用于约定发送位置的 IP 协议,那么如何才能真正的发送数据呢?为了将分组从一个节点(主机或路由器)运输到另一个节点,网络层必须依靠链路层提供服务。链路层的例子包括以太网、WiFi 和电缆接入的 DOCSIS 协议,因为数据从源目的地传送通常需要经过几条链路,一个数据包可能被沿途不同的链路层协议处理,我们把链路层的分组称为 帧(frame)

物理层

虽然链路层的作用是将帧从一个端系统运输到另一个端系统,而物理层的作用是将帧中的一个个 比特 从一个节点运输到另一个节点,物理层的协议仍然使用链路层协议,这些协议与实际的物理传输介质有关,例如,以太网有很多物理层协议:关于双绞铜线、关于同轴电缆、关于光纤等等。

五层网络协议的示意图如下

img

OSI 模型

我们上面讨论的计算网络协议模型不是唯一的 协议栈,ISO(国际标准化组织)提出来计算机网络应该按照7层来组织,那么7层网络协议栈与5层的区别在哪里?

img

从图中可以一眼看出,OSI 要比上面的网络模型多了 表示层会话层,其他层基本一致。表示层主要包括数据压缩和数据加密以及数据描述,数据描述使得应用程序不必担心计算机内部存储格式的问题,而会话层提供了数据交换的定界和同步功能,包括建立检查点和恢复方案。

浏览器

就如同各大邮箱使用电子邮件传送协议 SMTP 一样,浏览器是使用 HTTP 协议的主要载体,说到浏览器,你能想起来几种?是的,随着网景大战结束后,浏览器迅速发展,至今已经出现过的浏览器主要有

img

浏览器正式的名字叫做 Web Broser,顾名思义,就是检索、查看互联网上网页资源的应用程序,名字里的 Web,实际上指的就是 World Wide Web,也就是万维网。

我们在地址栏输入URL(即网址),浏览器会向DNS(域名服务器,后面会说)提供网址,由它来完成 URL 到 IP 地址的映射。然后将请求你的请求提交给具体的服务器,在由服务器返回我们要的结果(以HTML编码格式返回给浏览器),浏览器执行HTML编码,将结果显示在浏览器的正文。这就是一个浏览器发起请求和接受响应的过程。

Web 服务器

Web 服务器的正式名称叫做 Web Server,Web 服务器一般指的是网站服务器,上面说到浏览器是 HTTP 请求的发起方,那么 Web 服务器就是 HTTP 请求的应答方,Web 服务器可以向浏览器等 Web 客户端提供文档,也可以放置网站文件,让全世界浏览;可以放置数据文件,让全世界下载。目前最主流的三个Web服务器是Apache、 Nginx 、IIS。

CDN

CDN的全称是Content Delivery Network,即内容分发网络,它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。CDN 是构建在现有网络基础之上的网络,它依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储分发技术

打比方说你要去亚马逊上买书,之前你只能通过购物网站购买后从美国发货过海关等重重关卡送到你的家里,现在在中国建立一个亚马逊分基地,你就不用通过美国进行邮寄,从中国就能把书尽快给你送到。

WAF

WAF 是一种 Web 应用程序防护系统(Web Application Firewall,简称 WAF),它是一种通过执行一系列针对HTTP / HTTPS的安全策略来专门为Web应用提供保护的一款产品,它是应用层面的防火墙,专门检测 HTTP 流量,是防护 Web 应用的安全技术。

WAF 通常位于 Web 服务器之前,可以阻止如 SQL 注入、跨站脚本等攻击,目前应用较多的一个开源项目是 ModSecurity,它能够完全集成进 Apache 或 Nginx。

WebService

WebService 是一种 Web 应用程序,WebService是一种跨编程语言和跨操作系统平台的远程调用技术

Web Service 是一种由 W3C 定义的应用服务开发规范,使用 client-server 主从架构,通常使用 WSDL 定义服务接口,使用 HTTP 协议传输 XML 或 SOAP 消息,它是一个基于 Web(HTTP)的服务架构技术,既可以运行在内网,也可以在适当保护后运行在外网。

HTML

HTML 称为超文本标记语言,是一种标识性的语言。它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的 Internet 资源连接为一个逻辑整体。HTML 文本是由 HTML 命令组成的描述性文本,HTML 命令可以说明文字,图形、动画、声音、表格、链接等。

Web 页面构成

Web 页面(Web page)也叫做文档,是由一个个对象组成的。一个对象(Objecy) 只是一个文件,比如一个 HTML 文件、一个 JPEG 图形、一个 Java 小程序或一个视频片段,它们在网络中可以通过 URL 地址寻址。多数的 Web 页面含有一个 HTML 基本文件 以及几个引用对象。

举个例子,如果一个 Web 页面包含 HTML 文件和5个 JPEG 图形,那么这个 Web 页面就有6个对象:一个 HTML 文件和5个 JPEG 图形。HTML 基本文件通过 URL 地址引用页面中的其他对象。

与 HTTP 有关的协议

在互联网中,任何协议都不会单独的完成信息交换,HTTP 也一样。虽然 HTTP 属于应用层的协议,但是它仍然需要其他层次协议的配合完成信息的交换,那么在完成一次 HTTP 请求和响应的过程中,需要哪些协议的配合呢?一起来看一下

TCP/IP

TCP/IP 协议你一定听过,TCP/IP 我们一般称之为协议簇,什么意思呢?就是 TCP/IP 协议簇中不仅仅只有 TCP 协议和 IP 协议,它是一系列网络通信协议的统称。而其中最核心的两个协议就是 TCP / IP 协议,其他的还有 UDP、ICMP、ARP 等等,共同构成了一个复杂但有层次的协议栈。

TCP 协议的全称是 Transmission Control Protocol 的缩写,意思是传输控制协议,HTTP 使用 TCP 作为通信协议,这是因为 TCP 是一种可靠的协议,而可靠能保证数据不丢失。

IP 协议的全称是 Internet Protocol 的缩写,它主要解决的是通信双方寻址的问题。IP 协议使用 IP 地址 来标识互联网上的每一台计算机,可以把 IP 地址想象成为你手机的电话号码,你要与他人通话必须先要知道他人的手机号码,计算机网络中信息交换必须先要知道对方的 IP 地址。(关于 TCP 和 IP 更多的讨论我们会在后面详解)

DNS

你有没有想过为什么你可以通过键入 http://www.google.com 就能够获取你想要的网站?我们上面说到,计算机网络中的每个端系统都有一个 IP 地址存在,而把 IP 地址转换为便于人类记忆的协议就是 DNS 协议

DNS 的全称是域名系统(Domain Name System,缩写:DNS),它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。

URI / URL

我们上面提到,你可以通过输入 http://www.google.com 地址来访问谷歌的官网,那么这个地址有什么规定吗?我怎么输都可以?AAA.BBB.CCC 是不是也行?当然不是的,你输入的地址格式必须要满足 URI 的规范。

URI的全称是(Uniform Resource Identifier),中文名称是统一资源标识符,使用它就能够唯一地标记互联网上资源。

URL的全称是(Uniform Resource Locator),中文名称是统一资源定位符,也就是我们俗称的网址,它实际上是 URI 的一个子集。

URI 不仅包括 URL,还包括 URN(统一资源名称),它们之间的关系如下

img

HTTPS

HTTP 一般是明文传输,很容易被攻击者窃取重要信息,鉴于此,HTTPS 应运而生。HTTPS 的全称为 (Hyper Text Transfer Protocol over SecureSocket Layer),全称有点长,HTTPS 和 HTTP 有很大的不同在于 HTTPS 是以安全为目标的 HTTP 通道,在 HTTP 的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在 HTTP 的基础上增加了 SSL 层,也就是说 HTTPS = HTTP + SSL。(这块我们后面也会详谈 HTTPS)

HTTP 请求响应过程

你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 http://www.someSchool.edu/someDepartment/home.index,当我们输入网址并点击回车时,浏览器内部会进行如下操作

  • DNS服务器会首先进行域名的映射,找到访问http://www.someSchool.edu所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 http://www.someSchool.edu 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个套接字与其相连。

  • HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 someDepartment/home.index 的资源,我们后面会详细讨论 HTTP 请求报文。

  • HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其存储器(RAM 或磁盘)中检索出对象 http://www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到 HTTP 响应报文中,并通过套接字向客户进行发送。

  • HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。

  • HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。

  • 检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。

至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的请求-响应全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。

HTTP 请求特征

从上面整个过程中我们可以总结出 HTTP 进行分组传输是具有以下特征

  • 支持客户-服务器模式

  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。

  • 灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。

  • 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

  • 无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

详解 HTTP 报文

我们上面描述了一下 HTTP 的请求响应过程,流程比较简单,但是凡事就怕认真,你这一认真,就能拓展出很多东西,比如 HTTP 报文是什么样的,它的组成格式是什么? 下面就来探讨一下

HTTP 协议主要由三大部分组成:

  • 起始行(start line):描述请求或响应的基本信息;

  • 头部字段(header):使用 key-value 形式更详细地说明报文;

  • 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

其中起始行和头部字段并成为 请求头 或者 响应头,统称为 Header;消息正文也叫做实体,称为 body。HTTP 协议规定每次发送的报文必须要有 Header,但是可以没有 body,也就是说头信息是必须的,实体信息可以没有。而且在 header 和 body 之间必须要有一个空行(CRLF),如果用一幅图来表示一下的话,我觉得应该是下面这样

img

我们使用上面的那个例子来看一下 http 的请求报文

img

如图,这是 http://www.someSchool.edu/someDepartment/home.index 请求的请求头,通过观察这个 HTTP 报文我们就能够学到很多东西,首先,我们看到报文是用普通 ASCII 文本书写的,这样保证人能够可以看懂。然后,我们可以看到每一行和下一行之间都会有换行,而且最后一行(请求头部后)再加上一个回车换行符。

每个报文的起始行都是由三个字段组成:方法、URL 字段和 HTTP 版本字段

img

HTTP 请求方法

HTTP 请求方法一般分为 8 种,它们分别是

  • GET 获取资源,GET 方法用来请求访问已被 URI 识别的资源。指定的资源经服务器端解析后返回响应内容。也就是说,如果请求的资源是文本,那就保持原样返回;

  • POST 传输实体,虽然 GET 方法也可以传输主体信息,但是便于区分,我们一般不用 GET 传输实体信息,反而使用 POST 传输实体信息,

  • PUT 传输文件,PUT 方法用来传输文件。就像 FTP 协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。

    但是,鉴于 HTTP 的 PUT 方法自身不带验证机制,任何人都可以上传文件 , 存在安全性问题,因此一般的 W eb 网站不使用该方法。若配合 W eb 应用程序的验证机制,或架构设计采用REST(REpresentational State Transfer,表征状态转移)标准的同类 Web 网站,就可能会开放使用 PUT 方法。

  • HEAD 获得响应首部,HEAD 方法和 GET 方法一样,只是不返回报文主体部分。用于确认 URI 的有效性及资源更新的日期时间等。

  • DELETE 删除文件,DELETE 方法用来删除文件,是与 PUT 相反的方法。DELETE 方法按请求 URI 删除指定的资源。

  • OPTIONS 询问支持的方法,OPTIONS 方法用来查询针对请求 URI 指定的资源支持的方法。

  • TRACE 追踪路径,TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方法。

  • CONNECT 要求用隧道协议连接代理,CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加 密后经网络隧道传输。

我们一般最常用的方法也就是 GET 方法和 POST 方法,其他方法暂时了解即可。下面是 HTTP1.0 和 HTTP1.1 支持的方法清单

img

HTTP 请求 URL

HTTP 协议使用 URI 定位互联网上的资源。正是因为 URI 的特定功能,在互联网上任意位置的资源都能访问到。URL 带有请求对象的标识符。在上面的例子中,浏览器正在请求对象 /somedir/page.html 的资源。

我们再通过一个完整的域名解析一下 URL

比如 http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument 这个 URL 比较繁琐了吧,你把这个 URL 搞懂了其他的 URL 也就不成问题了。

首先出场的是 http

img

http://告诉浏览器使用何种协议。对于大部分 Web 资源,通常使用 HTTP 协议或其安全版本,HTTPS 协议。另外,浏览器也知道如何处理其他协议。例如, mailto: 协议指示浏览器打开邮件客户端;ftp:协议指示浏览器处理文件传输。

第二个出场的是 主机

img

http://www.example.com 既是一个域名,也代表管理该域名的机构。它指示了需要向网络上的哪一台主机发起请求。当然,也可以直接向主机的 IP address 地址发起请求。但直接使用 IP 地址的场景并不常见。

第三个出场的是 端口

img

我们前面说到,两个主机之间要发起 TCP 连接需要两个条件,主机 + 端口。它表示用于访问 Web 服务器上资源的入口。如果访问的该 Web 服务器使用HTTP协议的标准端口(HTTP为80,HTTPS为443)授予对其资源的访问权限,则通常省略此部分。否则端口就是 URI 必须的部分。

上面是请求 URL 所必须包含的部分,下面就是 URL 具体请求资源路径

第四个出场的是 路径

img

/path/to/myfile.html 是 Web 服务器上资源的路径。以端口后面的第一个 / 开始,到 ? 号之前结束,中间的 每一个/ 都代表了层级(上下级)关系。这个 URL 的请求资源是一个 html 页面。

紧跟着路径后面的是 查询参数

img

?key1=value1&key2=value2 是提供给 Web 服务器的额外参数。如果是 GET 请求,一般带有请求 URL 参数,如果是 POST 请求,则不会在路径后面直接加参数。这些参数是用 & 符号分隔的键/值对列表。key1 = value1 是第一对,key2 = value2 是第二对参数

紧跟着参数的是锚点

img

#SomewhereInTheDocument 是资源本身的某一部分的一个锚点。锚点代表资源内的一种“书签”,它给予浏览器显示位于该“加书签”点的内容的指示。 例如,在HTML文档上,浏览器将滚动到定义锚点的那个点上;在视频或音频文档上,浏览器将转到锚点代表的那个时间。值得注意的是 # 号后面的部分,也称为片段标识符,永远不会与请求一起发送到服务器。

HTTP 版本

表示报文使用的 HTTP 协议版本。

请求头部

这部分内容只是大致介绍一下,内容较多,后面会再以一篇文章详述

在表述完了起始行之后我们再来看一下请求头部,现在我们向上找,找到http://www.someSchool.edu/someDepartment/home.index,来看一下它的请求头部

Host: http://www.someschool.edu
Connection: close
User-agent: Mozilla/5.0
Accept-language: fr
复制代码

这个请求头信息比较少,首先 Host 表示的是对象所在的主机。你也许认为这个 Host 是不需要的,因为 URL 不是已经指明了请求对象的路径了吗?这个首部行提供的信息是 Web 代理高速缓存所需要的。Connection: close 表示的是浏览器需要告诉服务器使用的是非持久连接。它要求服务器在发送完响应的对象后就关闭连接。User-agent: 这是请求头用来告诉 Web 服务器,浏览器使用的类型是 Mozilla/5.0,即 Firefox 浏览器。Accept-language 告诉 Web 服务器,浏览器想要得到对象的法语版本,前提是服务器需要支持法语类型,否则将会发送服务器的默认版本。下面我们针对主要的实体字段进行介绍(具体的可以参考 developer.mozilla.org/zh-CN/docs/… MDN 官网学习)

HTTP 的请求标头分为四种: 通用标头请求标头响应标头实体标头,依次来进行详解。

通用标头

通用标头主要有三个,分别是 DateCache-ControlConnection

Date

Date 是一个通用标头,它可以出现在请求标头和响应标头中,它的基本表示如下

Date: Wed, 21 Oct 2015 07:28:00 GMT 
复制代码

表示的是格林威治标准时间,这个时间要比北京时间慢八个小时

img

Cache-Control

Cache-Control 是一个通用标头,他可以出现在请求标头和响应标头中,Cache-Control 的种类比较多,虽然说这是一个通用标头,但是又一些特性是请求标头具有的,有一些是响应标头才有的。主要大类有 可缓存性阈值性重新验证并重新加载其他特性

可缓存性是唯一响应标头才具有的特性,我们会在响应标头中详述。

阈值性,这个我翻译可能不准确,它的原英文是 Expiration,我是根据它的值来翻译的,你看到这些值可能会觉得我翻译的有点道理

  • max-age: 资源被认为仍然有效的最长时间,与 Expires 不同,这个请求是相对于 request标头的时间,而 Expires 是相对于响应标头。(请求标头)

  • s-maxage: 重写了 max-age 和 Expires 请求头,仅仅适用于共享缓存,被私有缓存所忽略(这块不理解,看完响应头的 Cache-Control 再进行理解)(请求标头)

  • max-stale:表示客户端将接受的最大响应时间,以秒为单位。(响应标头)

  • min-fresh: 表示客户端希望响应在指定的最小时间内有效。(响应标头)

Connection

Connection 决定当前事务(一次三次握手和四次挥手)完成后,是否会关闭网络连接。Connection 有两种,一种是持久性连接,即一次事务完成后不关闭网络连接

Connection: keep-alive
复制代码

另一种是非持久性连接,即一次事务完成后关闭网络连接

Connection: close
复制代码

HTTP1.1 其他通用标头如下

img

实体标头

实体标头是描述消息正文内容的 HTTP 标头。实体标头用于 HTTP 请求和响应中。头部Content-LengthContent-LanguageContent-Encoding 是实体头。

  • Content-Length 实体报头指示实体主体的大小,以字节为单位,发送到接收方。

  • Content-Language 实体报头描述了客户端或者服务端能够接受的语言,例如

Content-Language: de-DE
Content-Language: en-US
Content-Language: de-DE, en-CA
复制代码
  • Content-Encoding 这又是一个比较麻烦的属性,这个实体报头用来压缩媒体类型。Content-Encoding 指示对实体应用了何种编码。

    常见的内容编码有这几种: gzip、compress、deflate、identity ,这个属性可以应用在请求报文和响应报文中

Accept-Encoding: gzip, deflate //请求头
Content-Encoding: gzip //响应头
复制代码

下面是一些实体标头字段

img

请求标头

上面给出的例子请求报文的属性比较少,下面给出一个 MDN 官网的例子

GET /home.html HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/testpage.html
Connection: keep-alive
Upgrade-Insecure-Requests: 1
If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
Cache-Control: max-age=0
复制代码

Host

Host 请求头指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号。如果没有给定端口号,会自动使用被请求服务的默认端口(比如请求一个 HTTP 的 URL 会自动使用80作为端口)。

Host: developer.mozilla.org
复制代码

上面的 AccpetAccept-LanguageAccept-Encoding 都是属于内容协商的请求标头,我们会在下面说明

Referer

HTTP Referer 属性是请求标头的一部分,当浏览器向 web 服务器发送请求的时候,一般会带上 Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。

Referer: https://developer.mozilla.org/testpage.html
复制代码

Upgrade-Insecure-Requests

Upgrade-Insecure-Requests 是一个请求标头,用来向服务器端发送信号,表示客户端优先选择加密及带有身份验证的响应。

Upgrade-Insecure-Requests: 1
复制代码

If-Modified-Since

HTTP 的 If-Modified-Since 使其成为条件请求

  • 返回200,只有在给定日期的最后一次修改资源后,服务器才会以200状态发送回请求的资源。

  • 如果请求从开始以来没有被修改过,响应会返回304并且没有任何响应体

If-Modified-Since 通常会与 If-None-Match 搭配使用,If-Modified-Since 用于确认代理或客户端拥有的本地资源的有效性。获取资源的更新日期时间,可通过确认首部字段 Last-Modified 来确定。

大白话说就是如果在 Last-Modified 之后更新了服务器资源,那么服务器会响应200,如果在 Last-Modified 之后没有更新过资源,则返回 304。

If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
复制代码

If-None-Match

If-None-Match HTTP请求标头使请求成为条件请求。 对于 GET 和 HEAD 方法,仅当服务器没有与给定资源匹配的 ETag 时,服务器才会以200状态发送回请求的资源。 对于其他方法,仅当最终现有资源的ETag与列出的任何值都不匹配时,才会处理请求。

If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
复制代码

ETag 属于响应标头,后面进行介绍。

内容协商

内容协商机制是指客户端和服务器端就响应的资源内容进行交涉,然后提供给客户端最为适合的资源。内容协商会以响应资源的语言、字符集、编码方式等作为判断的标准。

img

内容协商主要有以下3种类型:

  • 服务器驱动协商(Server-driven Negotiation)

这种协商方式是由服务器端进行内容协商。服务器端会根据请求首部字段进行自动处理

  • 客户端驱动协商(Agent-driven Negotiation)

这种协商方式是由客户端来进行内容协商。

  • 透明协商(Transparent Negotiation)

是服务器驱动和客户端驱动的结合体,是由服务器端和客户端各自进行内容协商的一种方法。

内容协商的分类有很多种,主要的几种类型是 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language

Accept

接受请求 HTTP 标头会通告客户端其能够理解的 MIME 类型

那么什么是 MIME 类型呢?在回答这个问题前你应该先了解一下什么是 MIME

MIME: MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。

也就是说,MIME 类型其实就是一系列消息内容类型的集合。那么 MIME 类型都有哪些呢?

文本文件: text/html、text/plain、text/css、application/xhtml+xml、application/xml

图片文件: image/jpeg、image/gif、image/png

视频文件: video/mpeg、video/quicktime

应用程序二进制文件: application/octet-stream、application/zip

比如,如果浏览器不支持 PNG 图片的显示,那 Accept 就不指定image/png,而指定可处理的 image/gif 和 image/jpeg 等图片类型。

一般 MIME 类型也会和 q 这个属性一起使用,q 是什么?q 表示的是权重,来看一个例子

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
复制代码

这是什么意思呢?若想要给显示的媒体类型增加优先级,则使用 q= 来额外表示权重值,没有显示权重的时候默认值是1.0 ,我给你列个表格你就明白了

qMIME
1.0text/html
1.0application/xhtml+xml
0.9application/xml
0.8* / *

也就是说,这是一个放置顺序,权重高的在前,低的在后,application/xml;q=0.9 是不可分割的整体。

Accept-Charset

accept-charset 属性规定服务器处理表单数据所接受的字符集。

accept-charset 属性允许您指定一系列字符集,服务器必须支持这些字符集,从而得以正确解释表单中的数据。

该属性的值是用引号包含字符集名称列表。如果可接受字符集与用户所使用的字符即不相匹配的话,浏览器可以选择忽略表单或是将该表单区别对待。

此属性的默认值是 unknown,表示表单的字符集与包含表单的文档的字符集相同。

常用的字符集有: UTF-8 - Unicode 字符编码 ; ISO-8859-1 - 拉丁字母表的字符编码

Accept-Language

首部字段 Accept-Language 用来告知服务器用户代理能够处理的自然语言集(指中文或英文等),以及自然语言集的相对优先级。可一次指定多种自然语言集。 和 Accept 首部字段一样,按权重值 q来表示相对优先级。

Accept-Language: en-US,en;q=0.5
复制代码

请求标头我们大概就介绍这几种,后面会有一篇文章详细深挖所有的响应头的,下面是一个响应头的汇总,基于 HTTP 1.1

img

响应标头

响应标头是可以在 HTTP 响应种使用的 HTTP 标头,这听起来是像一句废话,不过确实是这样解释。并不是所有出现在响应中的标头都是响应标头。还有一些特殊的我们上面说过,有通用标头和实体标头也会出现在响应标头中,比如 Content-Length 就是一个实体标头,但是,在这种情况下,这些实体请求通常称为响应头。下面以一个例子为例和你探讨一下响应头

200 OK
Access-Control-Allow-Origin: *
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Mon, 18 Jul 2016 16:06:00 GMT
Etag: "c561c68d0ba92bbeb8b0f612a9199f722e3a621a"
Keep-Alive: timeout=5, max=997
Last-Modified: Mon, 18 Jul 2016 02:36:04 GMT
Server: Apache
Set-Cookie: mykey=myvalue; expires=Mon, 17-Jul-2017 16:06:00 GMT; Max-Age=31449600; Path=/; secure
Transfer-Encoding: chunked
Vary: Cookie, Accept-Encoding
x-frame-options: DENY
复制代码

响应状态码

首先出现的应该就是 200 OK,这是 HTTP 响应标头的状态码,它表示着响应成功完成。HTTP 响应标头的状态码有很多,并做了如下规定

2xx 为开头的都表示请求成功响应。

状态码含义
200成功响应
204请求处理成功,但是没有资源可以返回
206对资源某一部分进行响应,由Content-Range 指定范围的实体内容。

3xx 为开头的都表示需要进行附加操作以完成请求

状态码含义
301永久性重定向,该状态码表示请求的资源已经重新分配 URI,以后应该使用资源现有的 URI
302临时性重定向。该状态码表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。
303该状态码表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。
304该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。
307临时重定向。该状态码与 302 Found 有着相同的含义。

4xx 的响应结果表明客户端是发生错误的原因所在。

状态码含义
400该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。
401该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。
403该状态码表明对请求资源的访问被服务器拒绝了。
404该状态码表明服务器上无法找到请求的资源。

5xx 为开头的响应标头都表示服务器本身发生错误

状态码含义
500该状态码表明服务器端在执行请求时发生了错误。
503该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

Access-Control-Allow-Origin

一个返回的 HTTP 标头可能会具有 Access-Control-Allow-Origin ,Access-Control-Allow-Origin 指定一个来源,它告诉浏览器允许该来源进行资源访问。 否则-对于没有凭据的请求 *通配符,告诉浏览器允许任何源访问资源。例如,要允许源 https://mozilla.org 的代码访问资源,可以指定:

Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin
复制代码

如果服务器指定单个来源而不是 *通配符的话 ,则服务器还应在 Vary 响应标头中包含 Origin ,以向客户端指示 服务器响应将根据原始请求标头的值而有所不同。

Keep-Alive

上面我们提到,HTTP 报文标头会分为四种,这其实是按着上下文来分类的

还有一种分类是根据代理进行分类,根据代理会分为端到端头逐跳标头

而 Keep-Alive 表示的是 Connection 非持续连接的存活时间,如下

Connection: Keep-Alive
Keep-Alive: timeout=5, max=997
复制代码

Keep-Alive 有两个参数,它们是以逗号分隔的参数列表,每个参数由一个标识符和一个由等号 = 分隔的值组成。

timeout:指示空闲连接必须保持打开状态的最短时间(以秒为单位)。

max:指示在关闭连接之前可以在此连接上发送的最大请求数。

上述 HTTP 代码的意思就是限制最大的超时时间是 5s 和 最大的连接请求是 997 个。

Server

服务器标头包含有关原始服务器用来处理请求的软件的信息。

应该避免使用过于冗长和详细的 Server 值,因为它们可能会泄露内部实施细节,这可能会使攻击者容易地发现并利用已知的安全漏洞。例如下面这种写法

Server: Apache/2.4.1 (Unix)
复制代码

Set-Cookie

Cookie 又是另外一个领域的内容了,我们后面文章会说道 Cookie,这里需要记住 Cookie、Set-Cookie 和 Content-Disposition 等在其他 RFC 中定义的首部字段,它们不是属于 HTTP 1.1 的首部字段,但是使用率仍然很高。

Transfer-Encoding

首部字段 Transfer-Encoding 规定了传输报文主体时采用的编码方式。

Transfer-Encoding: chunked
复制代码

HTTP /1.1 的传输编码方式仅对分块传输编码有效。

X-Frame-Options

HTTP 首部字段是可以自行扩展的。所以在 Web 服务器和浏览器的应用上,会出现各种非标准的首部字段。

首部字段 X-Frame-Options 属于 HTTP 响应首部,用于控制网站内容在其他 Web 网站的 Frame 标签内的显示问题。其主要目的是为了防止点击劫持(clickjacking)攻击。

下面是一个响应头的汇总,基于 HTTP 1.1

img

非 HTTP/1.1 首部字段

在 HTTP 协议通信交互中使用到的首部字段,不限于 RFC2616 中定义的 47 种首部字段。还有 Cookie、Set-Cookie 和 Content-Disposition 等在其他 RFC 中定义的首部字段,它们的使用频率也很高。 这些非正式的首部字段统一归纳在 RFC4229 HTTP Header Field Registrations 中。

End-to-end 首部和 Hop-by-hop 首部

HTTP 首部字段将定义成缓存代理和非缓存代理的行为,分成 2 种类型。

一种是 End-to-end 首部 和 Hop-by-hop 首部

End-to-end(端到端) 首部

这些标头必须发送给消息的最终接收者 : 请求的服务器,或响应的客户端。中间代理必须重新传输未经修改的标头,并且缓存必须存储这些信息

Hop-by-hop(逐跳) 首部

分在此类别中的首部只对单次转发有效,会因通过缓存或代理而不再转发。

下面列举了 HTTP/1.1 中的逐跳首部字段。除这 8 个首部字段之外,其他所有字段都属于端到端首部。

Connection、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、Trailer、TE、Transfer-Encoding、Upgrade

HTTP 的优点和缺点

HTTP 的优点

简单灵活易扩展

HTTP 最重要也是最突出的优点是 简单、灵活、易于扩展

HTTP 的协议比较简单,它的主要组成就是 header + body,头部信息也是简单的文本格式,而且 HTTP 的请求报文根据英文也能猜出来个大概的意思,降低学习门槛,能够让更多的人研究和开发 HTTP 应用。

所以,在简单的基础上,HTTP 协议又多了灵活易扩展 的优点。

HTTP 协议里的请求方法、URI、状态码、原因短语、头字段等每一个核心组成要素都没有被制定死,允许开发者任意定制、扩充或解释,给予了浏览器和服务器最大程度的信任和自由。

应用广泛、环境成熟

因为过于简单,普及,因此应用很广泛。因为 HTTP 协议本身不属于一种语言,它并不限定某种编程语言或者操作系统,所以天然具有跨语言、跨平台的优越性。而且,因为本身的简单特性很容易实现,所以几乎所有的编程语言都有 HTTP 调用库和外围的开发测试工具。

随着移动互联网的发展, HTTP 的触角已经延伸到了世界的每一个角落,从简单的 Web 页面到复杂的 JSON、XML 数据,从台式机上的浏览器到手机上的各种 APP、新闻、论坛、购物、手机游戏,你很难找到一个没有使用 HTTP 的地方。

无状态

无状态其实既是优点又是缺点。因为服务器没有记忆能力,所以就不需要额外的资源来记录状态信息,不仅实现上会简单一些,而且还能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。

HTTP 的缺点

无状态

既然服务器没有记忆能力,它就无法支持需要连续多个步骤的事务操作。每次都得问一遍身份信息,不仅麻烦,而且还增加了不必要的数据传输量。由此出现了 Cookie 技术。

明文

HTTP 协议里还有一把优缺点一体的双刃剑,就是明文传输。明文意思就是协议里的报文(准确地说是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。

对比 TCP、UDP 这样的二进制协议,它的优点显而易见,不需要借助任何外部工具,用浏览器、Wireshark 或者 tcpdump 抓包后,直接用肉眼就可以很容易地查看或者修改,为我们的开发调试工作带来极大的便利。

当然缺点也是显而易见的,就是不安全,可以被监听和被窥探。因为无法判断通信双方的身份,不能判断报文是否被更改过。

性能

HTTP 的性能不算差,但不完全适应现在的互联网,还有很大的提升空间。

img

参考资料:

en.wikipedia.org/wiki/Hypert…

《极客时间》- 透视 HTTP 协议

developer.mozilla.org/en-US/docs/…

baike.baidu.com/item/WEB服务器…

baike.baidu.com/item/内容分发网络…

baike.baidu.com/item/HTML/9…

http://www.jianshu.com/p/3dd8f1879…

《计算机网络-自顶向下方法》

《图解 HTTP》

HTTP协议的内容协商

http://www.w3school.com.cn/tags/att_fo…


作者:程序员cxuan
来源:juejin.cn/post/6844904045572800525

收起阅读 »

为什么B站的弹幕可以不挡人物

那天在B站看视频的时候偶然发现当字幕遇到人物的时候就被裁切了,不会挡住人物,觉得很神奇,于是决定一探究竟。高端的效果,往往只需要采用最朴素的实现方式,忙碌了两个小时,陈师傅打开了F12,豁然开朗。一张图片+一个属性,直接搞定。为了印证我的想法,我决定自己写一个...
继续阅读 »


那天在B站看视频的时候偶然发现当字幕遇到人物的时候就被裁切了,不会挡住人物,觉得很神奇,于是决定一探究竟。

高端的效果,往往只需要采用最朴素的实现方式,忙碌了两个小时,陈师傅打开了F12,豁然开朗。一张图片+一个属性,直接搞定。


为了印证我的想法,我决定自己写一个demo

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.video {
width: 668px;
height: 376px;
position: relative;
-webkit-mask-image: url("mask.svg");
-webkit-mask-size: 668px 376px;
}
.bullet {
position: absolute;
font-size: 20px;
}
</style>
</head>
<body>
<div class="video">
<div class="bullet" style="left: 100px; top: 0;">元芳,你怎么看</div>
<div class="bullet" style="left: 200px; top: 20px;">你难道就是传说中的奶灵</div>
<div class="bullet" style="left: 300px; top: 40px;">你好,我是胖灵</div>
<div class="bullet" style="left: 400px; top: 60px;">这是第一集,还没有舔灵</div>
</div>
</body>
</html>

效果是这样的


加一个红背景,看的清楚一些


至此我们就实现了B站同款的不遮挡人物的弹幕。至于这张图片是怎么来的,肯定是AI识别出来然后生成的,一张图片也就一两K,一次加载很多张也不会造成很大的负担。

最后来看看这个神奇的css属性吧

所以在开发需求的时候可以把它当成一个亮点使用,但是不能强依赖于这个属性做需求。

原文链接:https://juejin.cn/post/7141012605535010823

收起阅读 »

人生中的第一次被辞退

2022年8月26日下午5点半得到的通知,有10天的缓冲但没有补偿,理由是没有没有过试用期,离试用期还有10天。 一、咋进的公司? 公司与甲方签的一个单子快到时间了公司没人写,没怎么面试问了我以前写的项目就让我通过了,工资是不打折的。 二、进公司干了啥? 目前...
继续阅读 »

2022年8月26日下午5点半得到的通知,有10天的缓冲但没有补偿,理由是没有没有过试用期,离试用期还有10天。

一、咋进的公司?

公司与甲方签的一个单子快到时间了公司没人写,没怎么面试问了我以前写的项目就让我通过了,工资是不打折的。

二、进公司干了啥?

目前是80天,30天开发后台管理(81张设计稿,60个接口,vue写的),10天修改后台管理第二版,后面40天就是噩梦了,维护前后端不分离的和前后端分离的jq。(时间只是大概,具体不记得了)


三、辞退原因

公司给的原因:维护开发效率太低。

个人认为的原因:

1、之前没接触jq(进来前没说用jq和要维护前后端不分离的项目)。之前那哥们是一毕业就在这家公司写了两年半jq工资没加第二年还降了,与之相比我这之前没接触jq项目的,我开的工资比他还高但我维护的效率比他低太多了。

2、状态不好。我加了上个前端的微信,他当时走了2个月但他现在还没找到工作在家学习vue,我离职了他都没找到工作,这jq我越写越焦虑,我怕有一天忘记vue、react、uniapp就只会jq,简历上全是jq项目我下份工作怎么找。在这种焦虑中工作不在状态想离职但又怕找不到工作,有点摆烂。

四、感受

1、失落。居然被这样一份工作辞退,开始怀疑自己能不能干这行业,自己怕不是个垃圾(虽然确实是菜狗...)。

2、担忧。目前了解到的找工作的前端,一个找了5个月在家学vue的(这公司上个前端),一个找了6个月的,一个找了2个月但找了比较好的工作,我丫的不会也找几个月吧(看来要练习一下捡瓶子,防止饿死)。

3、解脱、丫的,终于10天后不用维护这些垃圾代码了,焯!!!爽!!!

五、有什么打算?

1、先到杭州见一下老朋友,当然也可能约不出来(尴尬),顺便去面试。
2、回老家一趟,两年没回去想家了。
3、去深圳那个唯一叫我靓仔的地方,之后可能就饿死在那。


作者:张二河
链接:https://juejin.cn/post/7136214855777779749

收起阅读 »

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

你好呀,我是喜提七天居家隔离的歪歪。这篇文章要从一个奇怪的注释说起,就是下面这张图:我们可以不用管具体的代码逻辑,只是单单看这个 for 循环。在循环里面,专门有个变量 j,来记录当前循环次数。第一次循环以及往后每 1000 次循环之后,进入一个 if 逻辑。...
继续阅读 »


你好呀,我是喜提七天居家隔离的歪歪。

这篇文章要从一个奇怪的注释说起,就是下面这张图:


我们可以不用管具体的代码逻辑,只是单单看这个 for 循环。

在循环里面,专门有个变量 j,来记录当前循环次数。

第一次循环以及往后每 1000 次循环之后,进入一个 if 逻辑。

在这个 if 逻辑之上,标注了一个注释:prevent gc.

prevent,这个单词如果不认识的同学记一下,考试肯定要考的:


这个注释翻译一下就是:防止 GC 线程进行垃圾回收。

具体的实现逻辑是这样的:


核心逻辑其实就是这样一行代码:

Thread.sleep(0);

这样就能实现 prevent gc 了?


懵逼吗?

懵逼就对了,懵逼就说明值得把玩把玩。

这个代码片段,其实是出自 RocketMQ 的源码:

org.apache.rocketmq.store.logfile.DefaultMappedFile#warmMappedFile


事先需要说明的是,我并没有找到写这个代码的人问他的意图是什么,所以我只有基于自己的理解去推测他的意图。如果推测的不对,还请多多指教。

虽然这是 RocketMQ 的源码,但是基于我的理解,这个小技巧和 RocketMQ 框架没有任何关系,完全可以脱离于框架存在。

我给出的修改意见是这样的:


把 int 修改为 long,然后就可以直接把 for 循环里面的 if 逻辑删除掉了。


这样一看是不是更加懵逼了?

不要慌,接下来,我给你抽丝剥个茧。

另外,在“剥茧”之前,我先说一下结论:

  • 提出这个修改方案的理论立足点是 Java 的安全点相关的知识,也就是 safepoint。

  • 官方最后没有采纳这个修改方案。

  • 官方采没采纳不重要,重要的是我高低得给你“剥个茧”。


探索

当我知道这个代码片段是属于 RocketMQ 的时候,我想到的第一个点就是从代码提交记录中寻找答案。

看提交者是否在提交代码的时候说明了自己的意图。

于是我把代码拉了下来,一看提交记录是这样的:


我就知道这里不会有答案了。

因为这个类第一次提交的时候就已经包含了这个逻辑,而且对应这次提交的代码也非常多,并没有特别说明对应的功能。

从提交记录上没有获得什么有用的信息。

于是我把目光转向了 github 的 issue,拿着关键词 prevent gc 搜索了一番。

除了第一个链接之外,没有找到什么有用的信息:


而第一个链接对应的 issues 是这个:

github.com/apache/rock…

这个 issues 其实就是我们在讨论这个问题的过程中提出来的,也就是前面出现的修改方案:


也就是说,我想通过源码或者 github 找到这个问题权威的回答,是找不到了。

于是我又去了这个神奇的网站,在里面找到了这个 2018 年提出的问题:

stackoverflow.com/questions/5…


问题和我们的问题一模一样,但是这个问题下面就这一个回答:


这个回答并不好,因为我觉得没答到点上,但是没关系,我刚好可以把这个回答作为抓手,把差的这一点拉通对齐一下,给它赋能。

先看这个回答的第一句话:It does not(它没有)。

问题就来了:“它”是谁?“没有”什么?

“它”,指的就是我们前面出现的代码。

“没有”,是说没有防止 GC 线程进行垃圾回收。

这个的回答说:通过调用 Thread.sleep(0) 的目的是为了让 GC 线程有机会被操作系统选中,从而进行垃圾清理的工作。它的副作用是,可能会更频繁地运行 GC,毕竟你每 1000 次迭代就有一次运行 GC 的机会,但是好处是可以防止长时间的垃圾收集。

换句话说,这个代码是想要“触发”GC,而不是“避免”GC,或者说是“避免”时间很长的 GC。从这个角度来说,程序里面的注释其实是在撒谎或者没写完整。

不是 prevent gc,而是对 gc 采取了“打散运行,削峰填谷”的思想,从而 prevent long time gc。

但是你想想,我们自己编程的时候,正常情况下从来也没冒出过“这个地方应该触发一下 GC”这样想法吧?

因为我们知道,Java 程序员来说,虚拟机有自己的 GC 机制,我们不需要像写 C 或者 C++ 那样得自己管理内存,只要关注于业务代码即可,并没有特别注意 GC 机制。

那么本文中最关键的一个问题就来了:为什么这里要在代码里面特别注意 GC,想要尝试“触发”GC 呢?


先说答案:safepoint,安全点。

关于安全点的描述,我们可以看看《深入理解JVM虚拟机(第三版)》的 3.4.2 小节:


注意书里面的描述:

有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。

换言之:没有到安全点,是不能 STW,从而进行 GC 的。

如果在你的认知里面 GC 线程是随时都可以运行的。那么就需要刷新一下认知了。

接着,让我们把目光放到书的 5.2.8 小节:由安全点导致长时间停顿。

里面有这样一段话:


我把划线的部分单独拿出来,你仔细读一遍:

是HotSpot虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop),相对应地,使用long或者范围更大的数据类型作为索引值的循环就被称为不可数循环(Uncounted Loop),将会被放置安全点。

意思就是在可数循环(Counted Loop)的情况下,HotSpot 虚拟机搞了一个优化,就是等循环结束之后,线程才会进入安全点。

反过来说就是:循环如果没有结束,线程不会进入安全点,GC 线程就得等着当前的线程循环结束,进入安全点,才能开始工作。

什么是可数循环(Counted Loop)?

书里面的这个案例来自于这个链接:

juejin.cn/post/684490… HBase实战:记一次Safepoint导致长时间STW的踩坑之旅

如果你有时间,我建议你把这个案例完整的看一下,我只截取问题解决的部分:


截图中的 while(i < end) 就是一个可数循环,由于执行这个循环的线程需要在循环结束后才进入 Safepoint,所以先进入 Safepoint 的线程需要等待它。从而影响到 GC 线程的运行。

所以,修改方案就是把 int 修改为 long。

原理就是让其变为不可数循环(Uncounted Loop),从而不用等循环结束,在循环期间就能进入 Safepoint。

接着我们再把目光拉回到这里:


这个循环也是一个可数循环。

Thread.sleep(0) 这个代码看起来莫名其妙,但是我是不是可以大胆的猜测一下:故意写这个代码的人,是不是为了在这里放置一个 Safepoint 呢,以达到避免 GC 线程长时间等待,从而加长 stop the world 的时间的目的?

所以,我接下来只需要找到 sleep 会进入 Safepoint 的证据,就能证明我的猜想。

你猜怎么着?

本来是想去看一下源码,结果啪的一下,在源码的注释里面,直接找到了:

hg.openjdk.java.net/jdk8u/jdk8u…


注释里面说,在程序进入 Safepoint 的时候, Java 线程可能正处于框起来的五种不同的状态,针对不同的状态有不同的处理方案。

本来我想一个个的翻译的,但是信息量太大,我消化起来有点费劲儿,所以就不乱说了。

主要聚焦于和本文相关的第二点:Running in native code。

When returning from the native code, a Java thread must check the safepoint _state to see if we must block.

第一句话,就是答案,意思就是一个线程在运行 native 方法后,返回到 Java 线程后,必须进行一次 safepoint 的检测。

同时我在知乎看到了 R 大的这个回答,里面有这样一句,也印证了这个点:

http://www.zhihu.com/question/29…


那么接下来,就是见证奇迹的时刻了:


根据 R 大的说法:正在执行 native 函数的线程看作“已经进入了safepoint”,或者把这种情况叫做“在safe-region里”。

sleep 方法就是一个 native 方法,你说巧不巧?

所以,到这里我们可以确定的是:调用 sleep 方法的线程会进入 Safepoint。

另外,我还找到了一个 2013 年的 R 大关于类似问题讨论的帖子:

hllvm-group.iteye.com/group/topic…


这里就直接点名道姓的指出了:Thread.sleep(0).

这让我想起以前有个面试题问:Thread.sleep(0) 有什么用。

当时我就想:这题真难(S)啊(B)。现在发现原来是我道行不够,小丑竟是我自己。

还真的是有用。

实践

前面其实说的都是理论。

这一部分我们来拿代码实践跑上一把,就拿我之前分享过的《真是绝了!这段被JVM动了手脚的代码!》文章里面的案例。

public class MainTest {

  public static AtomicInteger num = new AtomicInteger(0);

  public static void main(String[] args) throws InterruptedException {
      Runnable runnable=()->{
          for (int i = 0; i < 1000000000; i++) {
              num.getAndAdd(1);
          }
          System.out.println(Thread.currentThread().getName()+"执行结束!");
      };

      Thread t1 = new Thread(runnable);
      Thread t2 = new Thread(runnable);
      t1.start();
      t2.start();
      Thread.sleep(1000);
      System.out.println("num = " + num);
  }
}

这个代码,你直接粘到你的 idea 里面去就能跑。

按照代码来看,主线程休眠 1000ms 后就会输出结果,但是实际情况却是主线程一直在等待 t1,t2 执行结束才继续执行。


这个循环就属于前面说的可数循环(Counted Loop)。

这个程序发生了什么事情呢?

  • 1.启动了两个长的、不间断的循环(内部没有安全点检查)。

  • 2.主线程进入睡眠状态 1 秒钟。

  • 3.在1000 ms之后,JVM尝试在Safepoint停止,以便Java线程进行定期清理,但是直到可数循环完成后才能执行此操作。

  • 4.主线程的 Thread.sleep 方法从 native 返回,发现安全点操作正在进行中,于是把自己挂起,直到操作结束。

所以,当我们把 int 修改为 long 后,程序就表现正常了:


受到 RocketMQ 源码的启示,我们还可以直接把它的代码拿过来:


这样,即使 for 循环的对象是 int 类型,也可以按照预期执行。因为我们相当于在循环体中插入了 Safepoint。

另外,我通过不严谨的方式测试了一下两个方案的耗时:


在我的机器上运行了几次,时间上都差距不大。

但是要论逼格的话,还得是右边的 prevent gc 的写法。没有二十年功力,写不出这一行“看似无用”的代码!

额外提一句

再说一个也是由前面的 RocketMQ 的源码引起的一个思考:


这个方法是在干啥?

预热文件,按照 4K 的大小往 byteBuffer 放 0,对文件进行预热。

byteBuffer.put(i, (byte) 0);

为什么我会对这个 4k 的预热比较敏感呢?

去年的天池大赛有这样的一个赛道:

tianchi.aliyun.com/competition…


其中有两个参赛选大佬都提到了“文件预热”的思路。

我把链接放在下面了,有兴趣的可以去细读一下:

tianchi.aliyun.com/forum/postD…



tianchi.aliyun.com/forum/postD…


最后,谢谢你“点赞”、“评论”我的文章,给我满满的正反馈。谢谢!

来源:juejin.cn/post/7139741080597037063

收起阅读 »

成都核酸系统崩溃原因分析

前言这两天成都核酸检测系统接连崩溃,让工作人员和广大市民苦不堪言,因此决定对其背后的技术进行分析,看看这个系统复杂不复杂,是什么原因导致的崩溃系统架构分析核酸结果查询服务正常,核酸检测记录无法录入,因此推断分析此系统主要分为核酸录入服务和核酸结果查询服务,架构...
继续阅读 »

前言

这两天成都核酸检测系统接连崩溃,让工作人员和广大市民苦不堪言,因此决定对其背后的技术进行分析,看看这个系统复杂不复杂,是什么原因导致的崩溃


系统架构分析

核酸结果查询服务正常,核酸检测记录无法录入,因此推断分析此系统主要分为核酸录入服务和核酸结果查询服务,架构如下


核酸检查结果通过数据同步任务从数据库同步到redis里面,来支撑高并发访问,由于核酸结果查询正常,主要分析核酸录入的瓶颈在哪

指标计算

  • 并发量

2千w人,6小时采样完,系统响应时间为2s,那么并发量为:2千万/(6 * 3600) * 2s=1850,如果考虑到时间在短一点,就按照2小时做完,2千万/(2 * 3600) * 2s=5555

  • 带宽

1M宽带理论速度=1Mbit/s=1024 Kbit/s=128 KByte/s,带宽的计算公式为:并发量*平均报文大小/112KB,假设每个报文大小是2KB,那么带宽为1850 *2KB/112KB=33M

瓶颈分析

带宽

经过前面计算带宽为33M,即使需要100M,这块也不是压力,可能会有人说查询服务带宽没考虑在内,因为查询服务正常,所以不需要考虑,如果是共用一个带宽,那么查询服务也会出现问题

数据库

如果使用单台数据库,没做分库分表,每秒1850次访问写入,如果在数据库配置不高的情况下会出现问题(cpu、内存、io)都有可能出现瓶颈点,其次数据量也会快速膨胀,每天晚上有2千万人做核酸,那么会有2千万甚至更多数据插入,经验值mysql 单表在1千万左右性能最佳,如果在往上增长性能就会逐渐下降,所以如果是单点有可能会出现瓶颈,如果要优化一般会进行不同服务器进行分库存储,如下图master和slave都是独立服务器


应用服务器

核酸写入服务节点如果前期不够,会出现性能瓶颈,但是这个还是比较容易扩容的,不应该在连续2天还会出现这种问题,所以应该不是此问题。

负载均衡器

1850的并发对nginx也是能轻松支撑的,所以不是这块问题

网络

看到网络上说是运营商的网络问题导致的,作为从业多年的技术人员,这种情况我遇到过,运营商的网络有可能出现这种情况,但是几率不高而且会很快恢复

总结

经过分析系统功能点不多,业务逻辑不是特别复杂,并发量也不高,数据库的出现瓶颈几率大点,网络的几率很小。。。

以上内容如果不对,请指正,谢谢!


作者:dweizhao
来源:juejin.cn/post/7139156745682845709

收起阅读 »

动图图解GC算法 - 让垃圾回收动起来!

既然已经卷成了这样,不学也没有办法,Hydra牺牲了周末时间,给大家画了几张动图,希望通过这几张图,能够帮助大家对垃圾收集算法有个更好的理解。废话不多说,首先还是从基础问题开始,看看怎么判断一个对象是否应该被回收。垃圾回收的根本目的是利用一些算法进行内存的管理...
继续阅读 »

提到Java中的垃圾回收,我相信很多小伙伴和我一样,第一反应就是面试必问了,你要是没背过点GC算法、收集器什么的知识,出门都不敢说自己背过八股文。说起来还真是有点尴尬,工作中实际用到这方面知识的场景真是不多,并且这东西学起来也很枯燥,但是奈何面试官就是爱问,我们能有什么办法呢?

既然已经卷成了这样,不学也没有办法,Hydra牺牲了周末时间,给大家画了几张动图,希望通过这几张图,能够帮助大家对垃圾收集算法有个更好的理解。废话不多说,首先还是从基础问题开始,看看怎么判断一个对象是否应该被回收。

判断对象存活

垃圾回收的根本目的是利用一些算法进行内存的管理,从而有效的利用内存空间,在进行垃圾回收前,需要判断对象的存活情况,在jvm中有两种判断对象的存活算法,下面分别进行介绍。

1、引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时计数器就加 1,当引用失效时计数器减 1。当计数器为0的时候,表示当前对象可以被回收。

这种方法的原理很简单,判断起来也很高效,但是存在两个问题:

  • 堆中对象每一次被引用和引用清除时,都需要进行计数器的加减法操作,会带来性能损耗

  • 当两个对象相互引用时,计数器永远不会0。也就是说,即使这两个对象不再被程序使用,仍然没有办法被回收,通过下面的例子看一下循环引用时的计数问题:

public void reference(){
A a = new A();
B b = new B();
a.instance = b;
b.instance = a;    
}

引用计数的变化过程如下图所示:


可以看到,在方法执行完成后,栈中的引用被释放,但是留下了两个对象在堆内存中循环引用,导致了两个实例最后的引用计数都不为0,最终这两个对象的内存将一直得不到释放,也正是因为这一缺陷,使引用计数算法并没有被实际应用在gc过程中。

2、可达性分析算法

可达性分析算法是jvm默认使用的寻找垃圾的算法,需要注意的是,虽然说的是寻找垃圾,但实际上可达性分析算法寻找的是仍然存活的对象。至于这样设计的理由,是因为如果直接寻找没有被引用的垃圾对象,实现起来相对复杂、耗时也会比较长,反过来标记存活的对象会更加省时。

可达性分析算法的基本思路就是,以一系列被称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明该对象不再存活,可以作为垃圾被回收。


在java中,可作为GC Roots的对象有以下几种:

  • 在虚拟机栈(栈帧的本地变量表)中引用的对象

  • 在方法区中静态属性引用的对象

  • 在方法区中常量引用的对象

  • 在本地方法栈中JNI(native方法)引用的对象

  • jvm内部的引用,如基本数据类型对应的Class对象、一些常驻异常对象等,及系统类加载器

  • 被同步锁synchronized持有的对象引用

  • 反映jvm内部情况的 JMXBeanJVMTI中注册的回调本地代码缓存等

  • 此外还有一些临时性的GC Roots,这是因为垃圾收集大多采用分代收集局部回收,考虑到跨代或跨区域引用的对象时,就需要将这部分关联的对象也添加到GC Roots中以确保准确性

其中比较重要、同时提到的比较多的还是前面4种,其他的简单了解一下即可。在了解了jvm是如何寻找垃圾对象之后,我们来看一看不同的垃圾收集算法的执行过程是怎样的。

垃圾收集算法

1、标记-清除算法

标记清除算法是一种非常基础的垃圾收集算法,当堆中的有效内存空间耗尽时,会触发STW(stop the world),然后分标记清除两阶段来进行垃圾收集工作:

  • 标记:从GC Roots的节点开始进行扫描,对所有存活的对象进行标记,将其记录为可达对象

  • 清除:对整个堆内存空间进行扫描,如果发现某个对象未被标记为可达对象,那么将其回收

通过下面的图,简单的看一下两阶段的执行过程:


但是这种算法会带来几个问题:

  • 在进行GC时会产生STW,停止整个应用程序,造成用户体验较差

  • 标记和清除两个阶段的效率都比较低,标记阶段需要从根集合进行扫描,清除阶段需要对堆内所有的对象进行遍历

  • 仅对非存活的对象进行处理,清除之后会产生大量不连续的内存碎片。导致之后程序在运行时需要分配较大的对象时,无法找到足够的连续内存,会再触发一次新的垃圾收集动作

此外,jvm并不是真正的把垃圾对象进行了遍历,把内部的数据都删除了,而是把垃圾对象的首地址和尾地址进行了保存,等到再次分配内存时,直接去地址列表中分配,通过这一措施提高了一些标记清除算法的效率。

2、复制算法

复制算法主要被应用于新生代,它将内存分为大小相同的两块,每次只使用其中的一块。在任意时间点,所有动态分配的对象都只能分配在其中一个内存空间,而另外一个内存空间则是空闲的。复制算法可以分为两步:

  • 当其中一块内存的有效内存空间耗尽后,jvm会停止应用程序运行,开启复制算法的gc线程,将还存活的对象复制到另一块空闲的内存空间。复制后的对象会严格按照内存地址依次排列,同时gc线程会更新存活对象的内存引用地址,指向新的内存地址

  • 在复制完成后,再把使用过的空间一次性清理掉,这样就完成了使用的内存空间和空闲内存空间的对调,使每次的内存回收都是对内存空间的一半进行回收

通过下面的图来看一下复制算法的执行过程:


复制算法的的优点是弥补了标记清除算法中,会出现内存碎片的缺点,但是它也同样存在一些问题:

  • 只使用了一半的内存,所以内存的利用率较低,造成了浪费

  • 如果对象的存活率很高,那么需要将很多对象复制一遍,并且更新它们的应用地址,这一过程花费的时间会非常的长

从上面的缺点可以看出,如果需要使用复制算法,那么有一个前提就是要求对象的存活率要比较低才可以,因此,复制算法更多的被用于对象“朝生暮死”发生更多的新生代中。

3、标记-整理算法

标记整理算法和标记清除算法非常的类似,主要被应用于老年代中。可分为以下两步:

  • 标记:和标记清除算法一样,先进行对象的标记,通过GC Roots节点扫描存活对象进行标记

  • 整理:将所有存活对象往一端空闲空间移动,按照内存地址依次排序,并更新对应引用的指针,然后清理末端内存地址以外的全部内存空间

标记整理算法的执行过程如下图所示:


可以看到,标记整理算法对前面的两种算法进行了改进,一定程度上弥补了它们的缺点:

  • 相对于标记清除算法,弥补了出现内存空间碎片的缺点

  • 相对于复制算法,弥补了浪费一半内存空间的缺点

但是同样,标记整理算法也有它的缺点,一方面它要标记所有存活对象,另一方面还添加了对象的移动操作以及更新引用地址的操作,因此标记整理算法具有更高的使用成本。

4、分代收集算法

实际上,java中的垃圾回收器并不是只使用的一种垃圾收集算法,当前大多采用的都是分代收集算法。jvm一般根据对象存活周期的不同,将内存分为几块,一般是把堆内存分为新生代和老年代,再根据各个年代的特点选择最佳的垃圾收集算法。主要思想如下:

  • 新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要复制少量对象以及更改引用,就可以完成垃圾收集

  • 老年代中,对象存活率比较高,使用复制算法不能很好的提高性能和效率。另外,没有额外的空间对它进行分配担保,因此选择标记清除标记整理算法进行垃圾收集

通过图来简单看一下各种算法的主要应用区域:


至于为什么在某一区域选择某种算法,还是和三种算法的特点息息相关的,再从3个维度进行一下对比:

  • 执行效率:从算法的时间复杂度来看,复制算法最优,标记清除次之,标记整理最低

  • 内存利用率:标记整理算法和标记清除算法较高,复制算法最差

  • 内存整齐程度:复制算法和标记整理算法较整齐,标记清除算法最差

尽管具有很多差异,但是除了都需要进行标记外,还有一个相同点,就是在gc线程开始工作时,都需要STW暂停所有工作线程。

总结

本文中,我们先介绍了垃圾收集的基本问题,什么样的对象可以作为垃圾被回收?jvm中通过可达性分析算法解决了这一关键问题,并在它的基础上衍生出了多种常用的垃圾收集算法,不同算法具有各自的优缺点,根据其特点被应用于各个年代。

虽然这篇文章唠唠叨叨了这么多,不过这些都还是基础的知识,如果想要彻底的掌握jvm中的垃圾收集,后续还有垃圾收集器、内存分配等很多的知识需要理解,不过我们今天就介绍到这里啦,希望通过这一篇图解,能够帮助大家更好的理解垃圾收集算法。

来源:mp.weixin.qq.com/s/DvPaMfn7xEKIilv-_Ojk8g

收起阅读 »

Fastjson反序列化随机性失败

本文主要讲述了一个具有"随机性"的反序列化错误!前言Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!问题代码为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,...
继续阅读 »

本文主要讲述了一个具有"随机性"的反序列化错误!

前言

Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!

问题代码

为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,同时也为了可以本地执行,看一下实际效果。

StewardTipItem

package test;

import java.util.List;

public class StewardTipItem {

   private Integer type;
   
   private List<String> contents;
   
   public StewardTipItem(Integer type, List<String> contents) {
       this.type = type;
       this.contents = contents;
  }
}

StewardTipCategory

反序列化时失败,此类有两个特殊之处:

  1. 返回StewardTipCategory的build方法(忽略返回null值)。

  2. 构造函数『C1』Map<Integer, List> items参数与List items属性同名,但类型不同!

package test;
   
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class StewardTipCategory {
   
   private String category;
   
   private List<StewardTipItem> items;
   
   public StewardTipCategory build() {
       return null;
  }
   
   //C1 下文使用C1引用该构造函数
   public StewardTipCategory(String category, Map<Integer,List<String>> items) {          
       List<StewardTipItem> categoryItems = new ArrayList<>();
   for (Map.Entry<Integer, List<String>> item : items.entrySet()) {
       StewardTipItem tipItem = new StewardTipItem(item.getKey(), item.getValue());                   categoryItems.add(tipItem);
  }
   this.items = categoryItems;
   this.category = category;
}
   
   // C2 下文使用C2引用该构造函数
   public StewardTipCategory(String category, List<StewardTipItem> items) {        
       this.category = category;
       this.items = items;
  }
   
   public String getCategory() {
       return category;
  }
   
   public void setCategory(String category) {
       this.category = category;
  }
   
   public List<StewardTipItem> getItems() {
       return items;
  }
   
   public void setItems(List<StewardTipItem> items) {
       this.items = items;
  }
}

StewardTip

package test;
   
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class StewardTip {

   private List<StewardTipCategory> categories;
   
   public StewardTip(Map<String, Map<Integer, List<String>>> categories) {          
       List<StewardTipCategory> tipCategories = new ArrayList<>();
       for (Map.Entry<String, Map<Integer, List<String>>> category : categories.entrySet()) {             StewardTipCategory tipCategory = new StewardTipCategory(category.getKey(), category.getValue());
           tipCategories.add(tipCategory);
      }
       this.categories = tipCategories;
  }
   
   public StewardTip(List<StewardTipCategory> categories) {
       this.categories = categories;
  }
   
   public List<StewardTipCategory> getCategories() {
       return categories;
  }
   
   public void setCategories(List<StewardTipCategory> categories) {
       this.categories = categories;
  }
}

JSON字符串

{
   "categories":[
      {
            "category":"工艺类",
            "items":[
                {
                    "contents":[
                        "工艺类-提醒项-内容1",
                        "工艺类-提醒项-内容2"
                    ],
                    "type":1
              },
              {
                    "contents":[
                        "工艺类-疑问项-内容1"
                    ],
                    "type":2
              }
          ]
      }
  ]
}

FastJSONTest

package test;

import com.alibaba.fastjson.JSONObject;

public class FastJSONTest {

   public static void main(String[] args) {
       String tip = "{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";        
       try {
           JSONObject.parseObject(tip, StewardTip.class);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
}

堆栈信息

当执行FastJSONTest的main方法时报错:

com.alibaba.fastjson.JSONException: syntax error, expect {, actual [
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:228)
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:67)  
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:43)  
   at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:85)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284)
   at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseArray(ArrayListTypeFieldDeserializer.java:181)
   at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseField(ArrayListTypeFieldDeserializer.java:69)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
   at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:672)  at com.alibaba.fastjson.JSON.parseObject(JSON.java:396)
   at com.alibaba.fastjson.JSON.parseObject(JSON.java:300)
   at com.alibaba.fastjson.JSON.parseObject(JSON.java:573)
   at test.FastJSONTest.main(FastJSONTest.java:17)

问题排查

排查过程有两个难点:

  1. 不能根据报错信息得到异常时JSON字符串的key,position或者其他有价值的提示信息。

  2. 报错并不是每次执行都会发生,存在随机性,执行十次可能报错两三次,没有统计失败率。

经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候debug到代码的方法。

JavaBeanInfo:285行


clazz是StewardTipCategory.class的情况下,提出以下两个问题:Q1:Constructor[] constructors数组的返回值是什么?Q2:constructors数组元素的顺序是什么?

参考java.lang.Class#getDeclaredConstructors的注释,可得到A1:


  • A1

public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)『C1』public test.StewardTipCategory(java.lang.String,java.util.List<test.StewardTipItem>)『C2』

  • A2

build()方法,C1构造函数,C2构造函数三者在Java源文件的顺序决定了constructors数组元素的顺序!下表是经过多次实验得到的一组数据,因为是手动触发,并且次数较少,所以不能保证100%的准确性,只是一种大概率事件。
java.lang.Class#getDeclaredConstructors底层实现是native getDeclaredConstructors0,JVM的这部分代码没有去阅读,所以目前无法解释产生这种现象的原因。

数组元素顺序
build()C1C2随机
C1build()C2C2,C1
C1C2build()C2,C1
build()C2C1随机
C2build()C1C1,C2
C2C1build()C1,C2
C1C2C2,C1
C2C1C1,C2

正是因为java.lang.Class#getDeclaredConstructors返回数组元素顺序的随机性,才导致反序列化失败的随机性!

  1. [C2,C1]反序列化成功!

  2. [C1,C2]反序列化失败!

[C1,C2]顺序下探寻反序列化失败时代码执行的路径。

JavaBeanInfo:492行


com.alibaba.fastjson.util.JavaBeanInfo#build()方法体代码量比较大,忽略执行路径上的无关代码。\

  1. [C1,C2]顺序下代码会执行到492行,并执行两次(StewardTipCategory#category, StewardTipCategory#items各执行一次)。

  2. 结束后创建一个com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer。

JavaBeanDeserializer:49行


JavaBeanDeserializer两个重要属性:

  1. private final FieldDeserializer[] fieldDeserializers;

  2. protected final FieldDeserializer[] sortedFieldDeserializers;

反序列化test.StewardTipCategory#items时fieldDeserializers的详细信息。

com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializercom.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer(属性值null,运行时会根据fieldType获取具体实现类)com.alibaba.fastjson.util.FieldInfo#fieldType(java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)


创建完成执行com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int, int[])

JavaBeanDeserializer:838行


DefaultFieldDeserializer:53行


com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)根据字段类型设置com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer的具体实现类。

DefaultFieldDeserializer:34行


test.StewardTipCategory#items属性的实际类型是List。

反序列化时根据C1构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.MapDeserializer。

执行com.alibaba.fastjson.parser.deserializer.MapDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object)时报错。

MapDeserializer:228行


JavaBeanDeserializer:838行


java.lang.Class#getDeclaredConstructors返回[C2,C1]顺序,反序列化时根据C2构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer,反序列化成功。

问题解决

代码

  1. 删除C1构造函数,使用其他方式创建StewardTipCategory。

  2. 修改C1构造函数参数名称,类型,避免误导Fastjson。

调试

package test;

import com.alibaba.fastjson.JSONObject;

import java.lang.reflect.Constructor;

public class FastJSONTest {

   public static void main(String[] args) {
       Constructor<?>[] declaredConstructors = StewardTipCategory.class.getDeclaredConstructors();
       // if true must fail!
      if ("public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)".equals(declaredConstructors[0].toGenericString())) {                 String tip = "{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";                   try {
               JSONObject.parseObject(tip, StewardTip.class);
          } catch (Exception e) {  
               e.printStackTrace();
          }
      }
  }
}

总结

开发过程中尽量遵照规范/规约,不要特立独行

StewardTipCategory构造函数C1方法签名明显不是一个很好的选择,方法体除了属性赋值,还做了一些额外的类型/数据转换,也应该尽量避免。

专业有深度

开发人员对于使用的技术与框架要有深入的研究,尤其是底层原理,不能停留在使用层面。一些不起眼的事情可能导致不可思议的问题:java.lang.Class#getDeclaredConstructors。

Fastjson

框架实现时要保持严谨,报错信息尽可能清晰明了,StewardTipCategory反序列化失败的原因在于,fastjson只检验了属性名称,构造函数参数个数而没有进一步校验属性类型。

<<重构:改善既有代码的设计>>提倡代码方法块尽量短小精悍,Fastjson某些模块的方法过于臃肿。

吾生也有涯,而知也无涯

作者:阿里巴巴大淘宝技术
来源:juejin.cn/post/7127472762335002637

收起阅读 »

相声:《我是大文豪》

相声:《我是大文豪》表演者:郭德纲/于谦(郭、于上台,众人鼓掌)郭:谢谢大伙儿于:哎郭:大伙儿这么捧,我打心里高兴于:是啊,支持相声嘛郭:我内心也是替这门濒临结扎的艺术,感到欣慰于:您先等会吧郭:怎么了?于:什么叫濒临结扎啊郭:那不经常有个词儿嘛,形容你们这个...
继续阅读 »

相声:《我是大文豪》

表演者:郭德纲/于谦

(郭、于上台,众人鼓掌)

郭:谢谢大伙儿

于:哎

郭:大伙儿这么捧,我打心里高兴

于:是啊,支持相声嘛

郭:我内心也是替这门濒临结扎的艺术,感到欣慰

于:您先等会吧

郭:怎么了?

于:什么叫濒临结扎啊

郭:那不经常有个词儿嘛,形容你们这个艺术正在风雨飘摇

于:那叫濒临失传!

郭:那不一样嘛!

于:不一样!我们这个不上环儿!

郭:什么意思!

于:还什么意思呢!再说了,我们相声有什么濒临失传的,这兴旺着呢!

郭:相比而言嘛,相比我从事的行业,相声太弱了

于:您是什么职业啊?

郭:我的职业是一名文豪

于:没听说过!人都是自称是作家,哪有自称是文豪的

郭:没有吗?

于:您见哪个洗头房的小姐自称职业是花魁的?

郭:那上次那女的这么说合着是骗我!

于:那也是您总去!

郭:算了不提这个了

于:是您也得敢提啊

郭:反正我作为一名文豪,著作等身

于:您写过什么作品?

郭:我爸爸是北京一老作家....

于:我是问您写过什么作品,您扯您爸爸干什么啊

郭:我爸爸那书写的哦,那个好,你不知道,这边看我爸爸的书,那边你媳妇跟人睡觉,你都不着急拦!

于:您有病吧?我问的是您,不是您爸爸

郭:没有天哪有地,没有我爸爸哪有我?没有我哪有你?

于:没有您也有我!

郭:哦那就没有我儿子哪有你?!

于:得,这辈儿下的更快了,那您就说您爸爸

郭:还是的嘛,人活一世最重要的就是孝顺,我不提我爸爸我还是人么!

于:反正瞧您这做派倒不老像人的

郭:你这就是嫉妒!你嫉妒我的书香门第!我爸爸本来是通县一掏大粪的啊...

于:这还书香门第啊!

郭:你听我讲啊!本来是掏大粪的,后来改了

于:改卖农家肥了?

郭:你是人不是?我告诉你我今天手上没带着枪,要不我一刀捅死你!

于:得,您继续说

郭:我爸爸在经历了文革的动乱以后,站出来写了一篇发人深省的小说,一举成名!

于:哦?那听着倒是挺厉害,怎么写的?

郭:就写啊,我爸爸本来品学兼优,就是被四人帮暗害了,导致没考上大学,才小学二年级就被政治迫害辍学了

于:那就跟四人帮一点儿关系没有!就是你爸爸自己不念了!

郭:你还有没有点人性?本来我爸是个清华大学的苗子,被时代耽误了!这是一场浩劫下的惨剧啊!

于:您不要脸这劲儿倒是随您爸爸

郭:你什么意思?你的意思是四人帮是好人,你要替他们翻案是不是?!

于:您甭扣帽子,我不觉得他们是好人,但您爸爸这事儿完全挨不上!

郭:反正我爸爸这篇小说一发表,哎呀整个文坛轰动啊,专家们都说,这是当代文学的代表佳作啊!

于:嗯,专家也是没见过什么好东西

郭:这篇小说算是我爸爸的自传,也奠定了我爸爸的文坛地位

于:说这么热闹,这自传小说叫什么名啊?

郭:《废物》

于:嗯,您爸爸这点上倒是挺实惠

郭:这篇《废物》一出,马上在世界文学界都得到了很大的声望,还得了国际大奖呢!

于:什么国际大奖啊?

郭:梵蒂冈佛学研究会文学进步一等奖!

于:都梵蒂冈了还佛学研究会!这奖水的也够模样了

郭:从此我爸爸就是文坛名人了,陆续出版了很多好书

于:都有什么啊?

郭:讲邻居家搞破鞋的,讲亲戚媳妇儿跟人偷情的,讲农村妇女找姘子的....

于:这不都是一回事吗?!这还用拆成好几本书讲啊!

郭:你懂什么?不同的地区这个婚外恋的状态是不一样的,床上都怎么称呼,私下里遇到本家儿了挨打怎么跑,这你都懂吗?

于:不懂,但这么一看您爸爸对这事儿研究够深的

郭:那是,我爸爸为此去各地采风,也因此成为了伤痕文学的代表人物

于:这跟伤痕文学有什么关系?

郭:一身是伤啊,肩膀上、腿上、脸上,那上次还有个农村老爷们拿个铁锹在他脑袋上拍出个疤呢,跟我父亲说,小贼,再让我看见你跟我媳妇儿不清不楚,爷爷我一铁锹拍死你!

于:哦这么个伤痕啊!那就是搞破鞋让人本家儿打了!

郭:之后我父亲又成为了我们当地的破协..哦不,作协主席

于:得,险些把实话说出来了

郭:你就说吧,我爸爸这个资历,我凭什么不是文豪?

于:这是您爸爸的成就,跟您在文学领域怎么着也没关系啊

郭:我爸爸给我提供了无数写作的素材啊!

于:什么素材?

郭:我迄今为止吧,出版了七本书,怎么样,厉害吧?

于:那倒是不少。都什么书啊?

郭:《我与我父亲》、《父亲下乡》、《父亲回城》、《父亲结婚》、《父亲生活秘史》、《父亲的爱情》、《我的父亲的老丈人》

于:你等会吧!

郭:怎么了?

于:《我的父亲的老丈人》....那你就说是写你姥爷不就得了!费这么大事!

郭:你懂个屁!我说我姥爷谁知道是谁啊?书卖不出去啊!

于:那倒是,您这一辈子就靠您父亲这点儿光环活着呢!

郭:你这就是丧良心,我写这么多书,算上里面的拼音,起码也得有五十万字了,您写的出来?!

于:得,连字儿都写不全,还得用拼音

郭:你这就是嫉妒,你嫉妒没我这么一个好爸爸!

于:您别在这抄便宜啊!是我爸爸没有您爸爸这么好,还是没有您这样的一个好爸爸?

郭:这不是一回事嘛?!

于:差远了!

郭:嗨咱俩计较这些微不足道的事儿干嘛

于:那是,你占便宜当然大度了

郭:我爸爸对我们家真是尽心尽力,呕心沥血,尤其对我,简直是再生父母一样的好啊!

于:您这用词,听着好像您不是亲儿子似的

郭:你别在这起腻啊!不光是我,我媳妇儿都得到我爸爸不少帮助

于:您媳妇也是作家吗?

郭:不是,我媳妇主要是表演舞蹈

于:哦,跳芭蕾的?

郭:不是

于:那是跳拉丁的?

郭:也不是

于:那是民族?

郭:这都什么啊,跟我媳妇儿比不了

于:那您媳妇儿是?

郭:我媳妇吧,以前是在北京一个会所演出

于:然后呢?

郭:后来会所涉黄被关了,就嫁给我了

于:哦合着是跳脱衣舞的啊!

郭:说那么难听!

于:那不就是吗?那您好好意思说您媳妇主要是表演舞蹈!

郭:是啊,只不过不是同一个表

于:是婊子演舞蹈的意思是吗?

郭:我抽你!我媳妇都上我们这的作协晚会了!

于:那甭说,又是您爸爸的功劳

郭:那当然

于:那您媳妇儿这三俗的舞蹈,对社会风气影响也不好啊

郭:那有什么的?我爸爸给在场观众每人发一块白布

于:这干什么用的?

郭:把眼睛蒙上

于:哦,就算是把观众眼睛蒙上也必须让儿媳妇过名人瘾是么?

郭:那当然,我爸爸说了一句至理名言,我听着感动的都不行了

于:怎么说的?

郭:许你们恶心,不许我家里人上不去!

于:去你的吧!

(全文完。本文纯属虚构)

收起阅读 »

反射技巧让你的性能提升N倍

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。文字版: 侧重于细节上的知识点更多、更加详细,揭秘反射真的很耗时吗,射 10 万次耗时多久视频版: 通过动画展示讲解,更加的清楚、直观,视频版本 bilibili 地址: https://www.bili...
继续阅读 »

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。

在之前的文章中提到了一个提升性能非常重要的点,将 Accessible 设置 true 反射速度会进一步提升,如果单看一个程序,可能这点性能微不足道,但是如果放在一个大的复杂的工程下面,运行在大量的低端机下,一行代码提升的性能,可能比你写 100 行代码提升的性能更加显著。

而今天这篇文章从源码的角度分析一下 isAccessible() 方法的作用,为什么将 Accessible 设置为 true 可以提升性能,在开始分析之前,我们先写一段代码。

  • 声明一个普通类,里面有个 public 方法 getName()private 方法 getAddress()

class Person {
   public fun getName(): String {
       return "I am DHL"
  }
   
   private fun getAddress(): String {
       return "BJ"
  }
}
  • 通过反射获取 getName()getAddress() 方法,花 3 秒钟思考一下,下面的代码输出的结果

// public 方法
val method1 = Person::class.declaredFunctions.find { it.name == "getName" }
println("access = ${method1?.isAccessible}")

// private 方法
val method2 = Person::class.declaredFunctions.find { it.name == "getAddress" }
println("access = ${method2?.isAccessible}")

无论是调用 public getName() 方法还是调用 private getAddress() 方法,最后输出的结果都为 false,通过这个例子也间接说明了 isAccessible() 方法并不是用来表示访问权限的。

当我们通过反射调用 private 方法时,都需要执行 setAccessible() 方法设置为 true, 否者会抛出下面的异常。

java.lang.IllegalAccessException: can not access a member of class com.hi.dhl.demo.reflect.Person

如果通过反射调用 public 方法,不设置 Accessibletrue,也可以正常调用,所以有很多小伙伴认为 isAccessible() 方法用来表示访问权限,其实这种理解是错误的。

我们一起来看一下源码是如何解释的,方法 isAccessible() 位于 AccessibleObject 类中。

public class AccessibleObject implements AnnotatedElement {
   ......
   // NOTE: for security purposes, this field must not be visible
   boolean override;
   
   public boolean isAccessible() {
       return override;
  }
   
   public void setAccessible(boolean flag) throws SecurityException {
      ......
  }
   ......
}

AccessibleObjectFieldMethodConstructor 的父类,调用 isAccessible() 返回 override 的值,而字段 override 主要判断是否要进行安全检查。

字段 overrideAccessibleObject 子类当中使用,所以我们一起来看一下它的子类 Method

public Object invoke(Object obj, Object... args){
   // 是否要进行安全检查
   if (!override) {
       // 进行快速验证是否是 Public 方法
       if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
           // 返回调用这个方法的 Class
           Class<?> caller = Reflection.getCallerClass();
           // 做权限访问的校验,缓存调用这个方法的 Class,避免下次在做检查
           checkAccess(caller, clazz, obj, modifiers);
      }
  }
  ......
   return ma.invoke(obj, args);
}

字段 override 提供给子类去重写,它的值决定了是否要进行安全检查,如果要进行安全检查,则会执行 quickCheckMemberAccess() 快速验证是否是 Public 方法,避免调用 getCallerClass()

  • 如果是 Public 方法,避免做安全检查,所以我们在代码中不调用 setAccessible(true) 方法,也不会抛出异常

  • 如果不是 Public 方法则会调用 getCallerClass() 获取调用这个方法的 Class,执行 checkAccess() 方法进行安全检查。

// it is necessary to perform somewhat expensive security checks.
// A more complicated security check cache is needed for Method and Field
// The cache can be either null (empty cache)
volatile Object securityCheckCache; // 缓存调用这个方法的 Class

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers){
   ......
   Object cache = securityCheckCache;  // read volatile
   
   if(cache == 调用这个方法的 Class){
       return;     // ACCESS IS OK
  }
   
   slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
   ......
}

void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,Class<?> targetClass){
   Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);
   Object cache = 调用这个方法的 Class
   securityCheckCache = cache;         // 缓存调用这个方法的 Class
}

源码中注释也说明了,如果要进行安全检查那么它的代价是非常昂贵的,所以用变量 securityCheckCache 缓存调用这个方法的 Class。如果下次使用相同的 Class,就不需要在做安全检查,但是这个缓存有个缺陷,如果换一个调用这个方法的 Class,需要再次做安全检查,并且会覆盖之前的缓存结果。

如果要在运行时修改属性或者调用某个方法时,都要进行安全检查,而安全检查是非常消耗资源的,所以 JDK 提供了一个 setAccessible() 方法,可以绕过安全检查,让开发者自己来决定是否要避开安全检查。

因为反射本身是非常慢的,如果能够避免安全检查,可以进一步提升性能,在之前的文章 揭秘反射真的很耗时吗,射 10 万次耗时多久,针对不同场景,分别测试了反射前后以及关闭安全检查的耗时。

正常调用反射反射优化后反射优化后关掉安全检查
创建对象0.578 ms/op4.710 ms/op1.018 ms/op0.943 ms/op
方法调用0.422 ms/op10.533 ms/op0.844 ms/op0.687 ms/op
属性调用0.241 ms/op12.432 ms/op1.362 ms/op1.202 ms/op
伴生对象0.470 ms/op5.661 ms/op0.840 ms/op0.702 ms/op

从测试结果可以看出来,执行 setAccessible() 方法,设置为 true 关掉安全检查之后,反射速度得到了进一步的提升,更接近于正常调用。


作者:程序员DHL
来源:https://juejin.cn/post/7121901090332737572

收起阅读 »

PyPi存储库遭恶意利用,尽快删除这12个病毒包!

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与...
继续阅读 »

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与其他流行软件包相似的名称来诱骗软件开发人员使用恶意版本,进而对俄罗斯反恐精英(Counter-Strike)1.6 服务器执行 DDoS 的仿冒攻击。

1 恶意仿冒活动

此次排版攻击依赖于开发人员使用错误的名称,导致使用了与合法软件包相似的恶意软件包。例如,此活动中的一些包及其合法对应包(括号中)是 Gesnim (Gensim)、TensorFolw (TensorFlow) 和 ipaddres (ipaddress)。


2 恶意软件包仍在 PyPi 上

上传的恶意 PyPi 包的完整列表是:

  • Gesnim

  • Kears

  • TensorFolw

  • Seabron

  • tqmd

  • lxlm

  • mokc

  • ipaddres

  • ipadress

  • falsk

  • douctils

  • inda

由于软件开发人员通常通过终端获取这些包,因此很容易以错误的顺序输入其名称和字母。由于下载和构建按预期继续,受害者没有意识到错误并感染了他们的设备。

虽然 CheckMarx 向 PyPi 存储库报告了这些包,但在撰写本文时它们仍然在线。

3 定位 CounterSrike 服务器

在他们的应用程序中下载并使用这些恶意 Python 包之一后,setup.py 中的嵌入代码会运行以确认主机是 Windows 系统,如果是,它会从 GitHub 下载有效负载 (test.exe)。


隐藏在设置脚本中的代码 (Checkmarx)

在 VirusTotal(免费的可疑文件分析服务的网站)上扫描时,69 个防病毒引擎中只有 11 个将文件标记为恶意文件,因此它是一种用 C++ 编写的相对较新/隐蔽的恶意软件。

该恶意软件会自行安装并创建一个启动条目以在系统重新启动之间保持持久性,同时它还注入一个过期的系统范围的根证书。

接下来,它连接到硬编码的 URL 以接收其配置。如果第三次尝试失败,它会寻找对发送到 DGA(域生成算法)地址的 HTTP 请求的响应。

“这是我们第一次在软件供应链生态系统中看到恶意软件(菌株)使用 DGA,或者在这种情况下,使用 UGA 为恶意活动的新指令分配生成的名称,”Checkmarx 在报告中评论道。


攻击流程图 (Checkmarx)

在分析师观察到的案例中,配置命令恶意软件将主机招募到 DDoS 机器人中,该机器人开始向反恐精英(CounterStrike)1.6 服务器发送流量。

目标似乎是通过感染足够多的设备来关闭 Counter-Strike 服务器,以使发送的流量使服务器不堪重负。

用于托管恶意软件的 GitHub 存储库已被删除,但攻击者可以通过滥用不同的文件托管服务来恢复恶意操作。

如果你使用了上面提到的 12 个软件包,并且可能出现了打字错误,一定要仔细检查你的项目,确认是否使用了合法的软件包。

4 影响

Pypi 被恶意攻击已非个例。早在今年 6 月,PyPi python 包就被曝发现将被盗的 AWS 密钥发送到不安全的站点。8 月 9 日,又有威胁分析人员在 PyPI 存储库中发现了 10 个恶意 Python 包,它们被用于窃取密码的恶意软件进而感染正在开发的系统。

Python Package Index (PyPi) 是一个包含超过 350000 个开源软件包的存储库,数百万开发人员可以轻松地将其整合到他们的 Python 项目中,以最小的努力构建复杂的应用程序。

由于开源,软件开发人员经常使用它来挑选基于 Python 的项目的构建块,或者与社区分享他们的工作。

但是,由于任何人都可以将包上传到存储库,并且包不会被删除,除非它们被报告为恶意,因此存储库更常被威胁者滥用,他们使用它来窃取开发人员凭据或部署恶意软件。虽然 PyPi 可以快速响应平台上的恶意包报告,但在提交之前由于缺少强有力的审查,因此危险包可能会潜伏一段时间。

参考链接:

https://medium.com/checkmarx-security/typosquatting-campaign-targeting-12-of-pythons-top-packages-downloading-malware-hosted-on-github-9501f35b8efb

作者:云昭

收起阅读 »

千万不要在方法上打断点!有大坑!

你好呀,我是歪歪。我上周遇到了一个莫名其妙的搞心态的问题,浪费了我好几个小时。气死我了,拿这几个小时来敲(摸)代(摸)码(鱼)不香吗?主要是最后问题的解决方式也让我特别的无语,越想越气,写篇文章吐槽一下。先说结论,也就是标题:在本地以 Debug 模式启动项目...
继续阅读 »

千万不要在方法上打断点!有大坑!

你好呀,我是歪歪。

我上周遇到了一个莫名其妙的搞心态的问题,浪费了我好几个小时。

气死我了,拿这几个小时来敲(摸)代(摸)码(鱼)不香吗?

主要是最后问题的解决方式也让我特别的无语,越想越气,写篇文章吐槽一下。

先说结论,也就是标题:

在本地以 Debug 模式启动项目的时候,千万不要在方法上打断点!千万不要!

首先什么是方法断点呢?

比如这样的,打在方法名这一行的断点:

你点击 IDEA 里面的下面这个图标,View Breakpoints,它会给你弹出一个框。

这个弹框里面展示的就是当前项目里面所有的断点,其中有一个复选框,Java Method Breakpoints,就是当前项目里面所有的“方法断点”:

那么这个玩意到底有什么坑呢?

当项目以 Debug 模式启动的时候,非常非常非常严重的拖慢启动速度。

给你看两个截图。

下面这个是我本地的一个非常简单的项目,没有方法断点的时候,只要 1.753 秒就启动完成了:

但是当我加上一个方法断点的时候,启动时间直接来到了 35.035 秒:

从 1.7 秒直接飙升到 35 秒,启动时间涨幅 2000%。

你说遭不遭得住?

遭不住,对不对。

那么我是怎么踩到这个坑的呢?

一个同事说他项目里面遇到一个匪夷所思的 BUG,想让我帮忙一起看看。

于是我先把项目拉了下来,然后简单的看了一下代码,准备把项目先在本地跑起来调试一下。

然而半个小时过去了,项目还没起来。我问他:这个项目本地启动时间怎么这么长呢?

他答:正常来说半分钟应该就启动起来了呀。

接着他还给我演示了一下,在他那边确实 30 多秒就启动成功了。

很明显,一样的代码,一个地方启动慢,一个地方启动快,首先怀疑环境问题。

于是我准备按照下面的流程走一次。

检查设置 -> 清空缓存 -> 换workspace -> 重启 -> 换电脑 -> 辞职

我检查了所有的配置、启动项、网络连接什么的,确保和他本地的环境是一模一样的。

这一套操作下来,差不多一小时过去了,并没有找到什么头绪。

但是那个时候我一点都不慌,我还有终极绝招:重启。

毕竟我的电脑已经好几个月没有关闭过了,重启一下也挺好的。

果然,重启了电脑之后,还是没有任何改变。

正在焦头烂额之际,同事过来问我啥进度了。

我能怎么说?

我只能说:从时间上来说应该解决了,但是实际上我连项目都还没启动成功。

听到这话,他坐在我的工位,准备帮我看一下。

半分钟之后,一个神奇的场景出现了,他在我的电脑上直接就把项目启动起来了。

一盘问,他并没有以 Debug 的模式启动,而是直接运行的。

用脚趾头想也知道,肯定是 Debug 模式在搞事情。

然后基于面向浏览器编程的原则,我现在有了几个关键词:IDEA debug 启动缓慢。

然后发现有很多人遇到了类似的问题,解决方法就是启动的时候取消项目里面的“方法断点”。

但是,遗憾的是,没有大多数文章都是说这样做就好了。但是并没有告诉我为什么这样做就好了。

我很想知道为什么会有这个坑,因为我用方法断点用的还是很多的,关键是以前在使用的过程中完全没有注意到还有这个坑。

“方法断点”还是非常实用的,比如我随便个例子。

之前写事务相关的文章的时候,提到过这样的一个方法:

java.sql.Connection#setAutoCommit

setAutoCommit 这个方法有好几个实现类,我也不知道具体会走哪一个:

所以,调试的时候可以在下面这个接口打上一个断点:

然后重启程序,IDEA 会自动帮你判断走那个实现类的:

但是需要特别说明的是,不是所有的方法断点都会导致启动缓慢的问题。至少在我本地看起来是这样的。

当我把方法断点加在 Mapper 的接口里面的时候,能稳定复现这个问题:

当把方法断点加在项目的其他方法上的时候,不是必现的,偶尔才会出现这个问题。

另外,其实当你以 Debug 模式启动且带有方法断点的时候,IDEA 是会弹出这个提醒,告诉你方法断点会导致 Debug 缓慢的问题:

但是,真男人,从不看提醒。反正我是直接就忽略了,根本没有关心弹窗的内容。

至于为什么会在 Mapper 的接口上打方法断点?

都怪我手贱,行了吧。

到底为什么

在找答案的过程中,我发现了这个 idea 的官方社区的链接:

intellij-support.jetbrains.com/hc/en-us/ar…

这个贴子,是 JetBrains Team 发布的,关于 Debug 功能可能会导致的性能缓慢的问题。

在这个帖子中,第一个性能点,就是 Method breakpoints。

官方是怎么解释这个问题的呢?

我给你翻译一波。

Method breakpoints will slow down debugger a lot because of the JVM design, they are expensive to evaluate.

他们说由于 JVM 的设计,方法断点会大大降低调试器的速度,因为这玩意的 “evaluate” 成本很高。

evaluate,四级单词,好好记一下,考试会考:

大概就是说你要用方法断点的功能,在启动过程中,就涉及到一个关于该断点进行“评估”的成本。成本就是启动缓慢。

怎么解决这个“评估”带来的成本呢?

官方给出的方案很简单粗暴:

不要使用方法断点,不就没有成本了?

所以,Remove,完事:

Remove method breakpoints and consider using the regular line breakpoints.

删除方法断点并考虑使用常规的 line breakpoints。

官方还是很贴心的,怕你不知道怎么 Remove 还专门补充了一句:

To verify that you don't have any method breakpoints open .idea/workspace.xml file in the project root directory (or .iws file if you are using the old project format) and look for any breakpoints inside the method_breakpoints node.

可以通过下面这个方法去验证你是否打开了方法断点。

就是去 .idea/workspace.xml 文件中,找到 method_breakpoints 这个 Node,如果有就 Remove 一下。

然后我看了一下我项目里面对应的文件,没有找到 method_breakpoints 关键字,但是找到了下面这个。

应该是文档发生了变化,问题不大,反正是一个意思,

其实官方给出的这个方法,虽然逼格稍微高一点,但还是我前面给的这个操作更简单:

针对“到底为什么”这个问题。

在这里,官方给的回答,特别的模糊:because of the JVM design。

别问,问就是由于 JVM 设计如此。

我觉得这不是我想要的答案,但是好在我在这个帖子下面找到了一个“好事之人”写的回复:

这个好事之人叫做 Gabi 老铁,我看到他回复的第一句话 “I made some research”,我就知道,这波稳了,找对地方了,答案肯定就藏在他附上的这个链接里面。

Gabi 老铁说:哥子们,我研究了一下这个方法断点为啥会慢的原因,研究报告在这里:

http://www.smartik.net/2017/11/met…

他甚至还来了一个概要:To make the long story short,长话短时。

他真的很贴心,我哭死。

他首先指出了问题的根本原因:

it seems that the root issue is that Method Breakpoints are implemented by using JDPA's Method Entry & Method Exit feature.

根本问题在于方法断点是通过使用 JDPA 的 Method Entry & Method Exit 特性实现的。

有同学就要问了,JDPA,是啥?

是个宝贝:

docs.oracle.com/javase/8/do…

JPDA,全称 Java Platform Debugger Architecture。

IDEA 里面的各种 Debug 功能,就是基于这个玩意来实现的。

不懂也没关系,这个东西面试又不考,在这里知道有这个技术就行。

接着,他用了四个 any 来完成了跳句四押:

This implementation requires the JVM to fire an event each time any thread enters any method and when any thread exits any method.

这个实现,要求 JVM,每次,在任何(any)线程进入任何(any)方法时,以及在任何(any)线程退出任何(any)方法时触发事件。

好家伙,这不就是个 AOP 吗?

这么一说,我就明白为什么方法断点的性能这么差了。要触发这么多进入方法和退出方法的事件,可不得耗费这么多时间吗?

具体的细节,他在前面说的研究报告里面都写清楚了,如果你对细节感兴趣的话,可以咨询阅读一下他的那篇报告。

话说他这个报告的名字也起的挺唬人的:Method Breakpoints are Evil。

我带你看两个关键的地方。

第一个是关于 Method Entry & Method Exit 的:

  • IDE 将断点添加到其内部方法断点 list 中
  • IDE 告诉前端启用 Method Entry & Method Exit 事件
  • 前端(调试器)通过代理将请求传递给 VM
  • 在每个 Method Entry & Method Exit 事件中,通过整个链将通知转发到 IDE
  • IDE 检查其方法断点 list 是否包含当前的这个方法。
  • 如果发现包含,说明这个方法上有一个方法断点,则 IDE 将向 VM 发送一个 SetBreakpoint 请求,打上断点。否则,VM 的线程将被释放,不会发生任何事情

这里是表明,前面我说的那个类似 AOP 的稍微具体一点的操作。

核心意思就一句话:触发的事件太多,导致性能下降厉害。

第二个关键的地方是这样的:

文章的最后给出了五个结论:

  • 方法断点 IDE 的特性,不是 JPDA 的特性
  • 方法断点是真的邪恶,evil 的一比
  • 方法断点将极大的影响调试程序
  • 只有在真正需要时才使用它们
  • 如果必须使用方法作为断点,请考虑关闭方法退出事件

前面四个点没啥说的了。

最后一个点:考虑关闭方法退出事件。

这个点验证起来非常简单,在方法断点上右键可以看到这个选项,Method Entry & Method Exit 默认都是勾选上了:

所以我在本地随便用一个项目验证了一下。

打开 Method Exit 事件,启动耗时:113.244 秒。

关闭 Method Exit 事件,启动耗时:46.754 秒。

你别说,还真有用。

现在我大概是知道为什么方法断点这么慢了。

这真不是 BUG,而是 feature。

而关于方法断点的这个问题,我顺便在社区搜索了一下,最早我追溯到了 2008 年:

这个老哥说他调试 Web 程序的速度慢到无法使用的程度。他的项目只启用了一行断点,没有方法断点。

请求大佬帮他看看。

然后大佬帮他一顿分析也没找到原因。

他自己也特别的纳闷,说:

我啥也没动,太奇怪了。这玩意有时可以,有时不行。

像不像一句经典台词:

但是问题最后还是解决了。怎么解决的呢?

他自己说:

确实是有个方法断点,他也不知道怎么打上这个断点的,可能和我一样,是手抖了吧。

意外收获

在前面出现的官方帖子的最下面,有这样的两个链接:

它指向了这个地方:

http://www.jetbrains.com/help/idea/d…

我把这部分链接都打开看了一遍,经过鉴定,这可真是好东西啊。

这是官方在手摸手教学,教你如何使用 Debug 模式。

我之前看过的一些调试小技巧相关的文章,原来就是翻译自官方这里啊。

我在这里举两个例子,算是一个导读,强烈推荐那些在 Debug 程序的时候,只知道不停的下一步、跳过当前断点等这样的基本操作的同学去仔细阅读,动手实操一把。

首先是这个:

针对 Java 的 Streams 流的调试。

官方给了一个调试的代码示例,我做了一点点微调,你粘过去就能跑:

class PrimeFinder {

    public static void main(String[] args) {
        IntStream.iterate(1, n -> n + 1)
                .limit(100)
                .filter(PrimeTest::isPrime)
                .filter(value -> value > 50)
                .forEach(System.out::println);
    }
}

class PrimeTest {
    static boolean isPrime(int candidate) {
        return candidate == 91 ||
                IntStream.rangeClosed(2, (int) Math.sqrt(candidate))
                        .noneMatch(n -> (candidate % n == 0));
    }
}
复制代码

代码逻辑很简单,就是找 100 以内的,大于 50 的素数。

很明显,在 isPrime 方法里面对 91 这个非素数做了特殊处理,导致程序最终会输出 91,也就是出 BUG 了。

虽然这个 BUG 一目了然,但是不要笑,要忍住,要假装不知道为什么。

现在我们要通过调试的方式找到 BUG。

断点打在这个位置:

以 Debug 的模式运行的时候,有这样的一个图标:

点击之后会有这样的一个弹窗出来:

上面框起来的是对应着程序的每一个方法调用顺序,以及调用完成之后的输出是什么。

下面框起来的这个 “Flat Mode” 点击之后是这样的:

最右边,也就是经过 filter 之后输出的结果。

里面就包含了 91 这个数:

点击这个 “91”,发现在经过第一个 filter 之后,91 这个数据还在。

说明这个地方出问题了。

而这个地方就是前面提到的对 “91” 做了特殊处理的 isPrime 方法。

这样就能有针对性的去分析这个方法,缩小问题排除范围。

这个功能怎么说呢,反正我的评论是:

总之,以上就是 IDEA 对于 Streams 流进行调试的一个简单示例。

接着再演示一个并发相关的:

官方给了这样的一个示例:

public class ConcurrencyTest {
    static final List a = Collections.synchronizedList(new ArrayList());

    public static void main(String[] args) {
        Thread t = new Thread(() -> addIfAbsent(17));
        t.start();
        addIfAbsent(17);
        t.join();
        System.out.println(a);
    }

    private static void addIfAbsent(int x) {
        if (!a.contains(x)) {
            a.add(x);
        }
    }
}
复制代码

代码里面搞一个线程安全的 list 集合,然后主线程和一个异步线程分别往这个 list 里面塞同一个数据。

按照 addIfAbsent 方法的意思,如果要添加的元素在 list 里面存在了,则不添加。

你说这个程序是线程安全的吗?

肯定不是。

你想想,先判断,再添加,经典的非原子性操作。

但是这个程序你拿去直接跑,又不太容易跑出线程不安全的场景:

怎么办?

Debug 就来帮你干这个事儿了。

在这里打一个断点,然后右键断点,选择 “Thread”:

这样程序跑起来的时候主线程和异步线程都会在这个地方停下来:

可以通过 “Frames” 中的下拉框分别选择 Debug 主线程还是异步线程。

由于两个线程都执行到了 add 方法,所以最终的输出是这样的:

这不就出现线程不安全了吗?

即使你知道这个地方是线程不安全的,但是如果没有 Debug 来帮忙调试,要通过程序输出来验证还是比较困难的。

毕竟多线程问题,大多数情况下都不是每次都能必现的问题。

定位到问题之后,官方也给出了正确的代码片段:

好了,说好了是导读,这都是基本操作。还是那句话,如果感兴趣,自己去翻一下,跟着案例操作一下。

就算你看到有人把 Debug 源码,玩出花来了,也无外乎不过是这样的几个基础操作的组合而已。

回首往事

让我们再次回到官方的“关于 Debug 功能可能会导致的性能缓慢的问题”这个帖子里面:

当我看到方框里面框起来的 “Collections classes” 和 “toString()” 方法的时候,眼泪都快下来了。

我最早开始写文章的时候,曾经被这个玩意坑惨了。

三年前,2019 年,我写了这篇文章《这道Java基础题真的有坑!我也没想到还有续集。》

当时 Debug 调试 ArrayList 的时候遇到一个问题,我一度以为我被质子干扰了:

一句话汇总就是在单线程的情况下,程序直接运行的结果和 Debug 输出的结果是不一样的。

当时我是百思不得其解。

直到 8 个月后,写《JDK的BUG导致的内存溢出!反正我是没想到还能有续集》这篇文章的时候才偶然间找到问题的答案。

根本原因就是在 Debug 模式下,IDEA 会自动触发集合类的 toString 方法。而在某些集合类的 toString 方法里面,会有诸如修改头节点的逻辑,导致程序运行结果和预期的不匹配。

也就是对应这句话:

翻译过来就是:老铁请注意,如果 toString 方法中的代码更改了程序的状态,则在 debug 状态下运行时,这些方法也可以更改应用程序的运行结果。

最后的解决方案就是关闭 IDEA 的这两个配置:

同时,我也在官方文档中找到了这个两个配置的解释:

http://www.jetbrains.com/help/idea/c…

主要是为了在 Debug 的过程中用更加友好的形式显示集合类。

啥意思?

给你看个例子。

这是没有勾选前面说的配置的时候,map 集合在 Debug 模式下的样子:

这是勾选之后,map 集合在 Debug 模式下的样子:

很明显,勾选了之后的样子,更加友好。

收起阅读 »

Elasticsearch 为什么会产生文档版本冲突?如何避免?

先让大家直观的看到 Elasticsearch 文档版本冲突。模拟脚本1:循环写入数据 index.sh。模拟脚本2:循环update_by_query 批量更新数据 update.sh。由于:写入脚本 index.sh 比更新脚本 update.sh (执行...
继续阅读 »

1、Elasticsearch 版本冲突复现

先让大家直观的看到 Elasticsearch 文档版本冲突。

1.1 场景1:create 场景

DELETE my-index-000001
# 执行创建并写入
PUT my-index-000001/_create/1
{
"@timestamp": "2099-11-15T13:12:00",
"message": "GET /search HTTP/1.1 200 1070000",
"user": {
  "id": "kimchy"
}
}

# 再次执行会报版本冲突错误。
# 报错信息:[1]: version conflict, document already exists (current version [1])
PUT my-index-000001/_create/1
{
"@timestamp": "2099-11-15T13:12:00",
"message": "GET /search HTTP/1.1 200 1070000",
"user": {
  "id": "kimchy"
}
}


1.2 场景2:批量更新场景模拟

模拟脚本1:循环写入数据 index.sh。


模拟脚本2:循环update_by_query 批量更新数据 update.sh。


由于:写入脚本 index.sh 比更新脚本 update.sh (执行一次,休眠1秒)执行要快,所以更新获取的版本较写入的最新版本要低,会导致版本冲突如下图所示:


1.3 场景3:批量删除场景模拟

写入脚本 index.sh 不变。

删除脚本 delete.sh 如下:


和更新原因一致,由于:写入脚本 index.sh 比删除脚本 delete.sh (执行一次,休眠1秒)执行要快,所以删除获取的版本较写入的最新版本要低,会导致版本冲突如下图所示:


2、Elasticsearch 文档版本定义

执行:

GET test/_doc/1

召回结果如下:


这里的 version 代表文档的版本。

  • 当我们在 Elasticsearch 中创建一个新文档时,它会为该文档分配一个_version: 1。

  • 当我们对该文档进行任何后续更新(更新 update、索引 index 或删除 delete)时,_version都会增加 1。

一句话:Elasticsearch 使用_version来鉴别文档是否已更改。

3、Elasticsearch 文档版本产生背景

试想一下,如果没有文档版本?当有并发访问会怎么办?

前置条件:Elasticsearch 从写入到被检索的时间间隔是由刷新频率 refresh_interval 设定的,该值可以更新,但默认最快是 1 秒。


如上图所示,假设我们有一个人们用来评价 T 恤设计的网站。网站很简单,仅列出了T恤设计,允许用户给T恤投票。如果顺序投票,没有并发请求,直接发起update更新没有问题。

但是,在999累计投票数后,碰巧小明同学和小红同学两位同时(并发)发起投票请求,这时候,如果没有版本控制,将导致最终结果不是预期的1001,而是1000。

所以,为了处理上述场景以及比上述更复杂的并发场景,Elasticsearch 亟需一个内置的文档版本控制系统。这就是 _version 的产生背景。

https://kb.objectrocket.com/elasticsearch/elasticsearch-version-history-what-it-does-and-doesnt-do-501

https://www.elastic.co/cn/blog/elasticsearch-versioning-support

4、常见的并发控制策略

并发控制可以简记为:“防止两个或多个用户同时编辑同一记录而导致最终结果和预期不一致”。

常见的并发控制策略:悲观锁、乐观锁。

4.1 悲观锁

悲观锁,又名:悲观并发控制,英文全称:"Pessimistic Concurrency Control",缩写“PCC”,是一种并发控制的方法。

  • 悲观锁本质:在修改数据之前先锁定,再修改。

  • 悲观锁优点:采用先锁定后修改的保守策略,为数据处理的安全提供了保证。

  • 悲观锁缺点:加锁会有额外的开销,还会增加产生死锁的机会。

  • 悲观锁应用场景:比较适合写入操作比较频繁的场景。

4.2 乐观锁

乐观锁,又名:乐观并发控制,英文全称:“Optimistic Concurrency Control”,缩写OCC”,也是一种并发控制的方法。

  • 乐观锁本质:假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

  • 乐观锁优点:“胆子足够大,足够乐观”,直到提交的时候才去锁定,不会产生任何锁和死锁。

  • 乐观锁缺点:并发写入会有问题,需要有冲突避免策略补救。

  • 乐观锁应用场景:数据竞争(data race)不大、冲突较少的场景、比较适合读取操作比较频繁的场景,确保比其他并发控制方法(如悲观锁)更高的吞吐量。

这里要强调的是,Elasticsearch 采用的乐观锁的机制来处理并发问题。

Elasticsearch 乐观锁本质是:没有给数据加锁,而是基于 version 文档版本实现。每次更新或删除数据的时候,都需要对比版本号。

5、Elasticsearch 文档版本冲突的本质

一句话,Elasticsearch 文档冲突的本质——老版本覆盖掉了新版本。

6、如何解决或者避免 Elasticsearch 文档版本冲突?

6.1 external 外部控制版本号

“external”——我的理解就是“简政放权”,交由外部的数据库或者更确切的说,是写入的数据库或其他第三方库来做控制。

版本号可以设置为外部值(例如,如果在数据库中维护)。要启用此功能,version_type应设置为 external。

使用外部版本类型 external 时,系统会检查传递给索引请求的版本号是否大于当前存储文档的版本。

  • 如果为真,也就是新版本大于已有版本,则文档将被索引并使用新的版本号。

  • 如果提供的值小于或等于存储文档的版本号,则会发生版本冲突,索引操作将失败。

好处:不论何时,ES 中只有最新版本的数据,借助 external 相对有效的解决版本冲突问题。

实战一把:

如果没有 external,执行如下命令:

PUT my-index-000001/_doc/1?version=2
{
"user": {
  "id": "elkbee"
}
}

报错如下:

{
"error" : {
  "root_cause" : [
    {
      "type" : "action_request_validation_exception",
      "reason" : "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;"
    }
  ],
.......省略2行......
"status" : 400
}

啥意思呢?内部版本控制(internal)不能用于乐观锁,也就是直接使用 version 是不可以的。需要使用:if_seq_noif_primary_term,它俩的用法,后文会有专门解读。

如果用 external,执行如下命令:

PUT my-index-000001/_doc/1?version=2&version_type=external
{
"user": {
  "id": "elkbee"
}
}

执行结果如下:

{
"_index" : "my-index-000001",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
  "total" : 2,
  "successful" : 1,
  "failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}

相比于之间没有加 external,加上 external 后,可以实现基于version的文档更新操作。

external_gt 和 external_gte的用法见官方文档,本文不展开,原理同 external。

https://www.elastic.co/guide/en/elasticsearch/reference/8.1/docs-index_.html#index-versioning

6.2 通过if_seq_no 和 if_primary_term 唯一标识避免冲突

索引操作(Index,动词)是有条件的,并且只有在对文档的最后修改分配了由 if_seq_no 和 if_primary_term 参数指定的序列号和 primary term specified(翻译起来拗口,索性用英文)才执行。

如果检测到不匹配,该操作将产生一个 VersionConflictException 409 的状态码。

Step1:写入数据

DELETE products_001
PUT products_001/_doc/1567
{
"product" : "r2d2",
"details" : "A resourceful astromech droid"
}


# 查看ifseqno 和 ifprimaryterm
GET products_001/_doc/1567

返回:

{
"_index" : "products_001",
"_type" : "_doc",
"_id" : "1567",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
  "product" : "r2d2",
  "details" : "A resourceful astromech droid"
}
}

Step2:以这种方式更新,前提是先拿到 if_seq_no 和 if_primary_term

# 模拟数据打tag 过程
PUT products_001/_doc/1567?if_seq_no=0&if_primary_term=1
{
"product": "r2d2",
"details": "A resourceful astromech droid",
"tags": [ "droid" ]
}


# 再获取数据
GET products_001/_doc/1567

返回:

{
"_index" : "products_001",
"_type" : "_doc",
"_id" : "1567",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
  "product" : "r2d2",
  "details" : "A resourceful astromech droid",
  "tags" : [
    "droid"
  ]
}
}

step2 更新数据的时候,是在 step1 的获取已写入文档的 if_seq_no=0 和 if_primary_term=1 基础上完成的。

这样能有效避免冲突。

6.3 批量更新和批量删除忽略冲突实现

如下是在开篇的基础上加了:conflicts=proceed。

conflicts 默认值是终止,而 proceed 代表继续。

POST test/_update_by_query?conflicts=proceed
{
"query": {
  "match": {
    "name": "update"
  }
},
"script": {
  "source": "ctx._source['foo'] = '123ss'",
  "lang": "painless"
}
}

conflicts=proceed 的本质——告诉进程忽略冲突并继续更新其他文档。

开篇不会报 409 错误了,但依然会有版本冲突。但,某些企业级场景是可以用的。


同理,delete_by_query 参数及返回结果均和 update_by_query 一致。


扩展:单个更新 update (区别于批量更新:update_by_query)有 retry_on_conflict 参数,可以设置冲突后重试次数。

7、关于频繁更新带来的性能问题

正如文章开篇演示的,并发更新或者并发删除可能会导致版本冲突。

除了并发性和正确性之外,请注意,非常频繁地更新文档可能会导致性能下降。

如果更新了尚未写入段(segment)的文档,将会导致刷新操作。而刷新频率越小(企业级咨询我见过设置小于1s的,不推荐),势必会导致写入低效。

更多探讨推荐阅读:

https://discuss.elastic.co/t/handling-conflicts/135240/2

8、小结

从实际问题抽象出模拟脚本,让大家看到文档版本冲突是如何产生的。而后,定义了版本冲突并指出了其产生的背景。

接着,详细讲解了解决冲突的两种机制:乐观锁、悲观锁。探讨、验证了解决文档版本冲突的几种方案。

你有没有遇到过本文提及的问题,如何解决的呢?欢迎留言交流。

参考

[1] https://www.anycodings.com/1questions/160352/why-bulk-update-never-conflicts-with-update-by-query-requests-in-elasticsearch

[2] https://learnku.com/articles/43867

[3] https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html

[4] https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#optimistic-concurrency-control-index

[5] https://www.elastic.co/guide/en/elasticsearch/reference/8.1/docs-index_.html#index-versioning

来源:mp.weixin.qq.com/s/9XzOogqfz4tavXqDt1pxgA

收起阅读 »

我们需要从单体转到微服务吗?

起源martinfowler.com/articles/microservices.html。和微服务相对应的是单体架构,先来看看单体架构是怎样的。大多数人做软件开发应该都是从单体架构开始的,以 .NET 程序员来说,从最早的 WebForm、到后来的 MVC...
继续阅读 »

微服务或许你没有真正实践过,但一定听说过,虽然已经到了 2022 年,这个词依然很热,可以通过搜索 google 指数看得到。

起源

“微服务”一词源于 2011 年 5 月在威尼斯附近的一次软件架构师研讨会上进行的架构风格的讨论。2012 年 5 月 讨论小组决定将这种架构风格命名为“微服务”。Fred George 同年在一次技术大会上进行自己的微服务实践分享,并说微服务是一种细粒度的 SOA ,但最终将其发扬光大的是 Martin Fowler 2014 年写的博文《 Microservices 》,原文链接如下:

martinfowler.com/articles/microservices.html

自此以后,微服务就家喻户晓了,Microservices 的 Google 指数也是在 2014 年后就一路飙升。

和微服务相对应的是单体架构,先来看看单体架构是怎样的。

单体架构

大多数人做软件开发应该都是从单体架构开始的,以 .NET 程序员来说,从最早的 WebForm、到后来的 MVC、再到现在的前后端分离,后端使用 .NET 的 WebAPI ,都是整个项目的代码放到一个解决方案中,发布要么直接整个目录进行替换,或者更新有变更的 dll 文件。

包括到现在,这种单体架构的模该还占着很大的比重,凡是存在,必有道理,单体架构有着他的可取之处:

  • 开发方便,.NET 程序员只需只需使用宇宙最强 IDE VS 就可以。

  • 调试方便,在开发阶段,所有的项目都在一个解决方案下,项目之间是可以直接引用,断点可以到达你想要的任何地方。

  • 运行方便,编码完成,只需一个 F5 搞定。

  • 部署方便,无论是之前部署 IIS ,还是现在的容器部署,都只涉及到一个发布目录。

不过,随着产品的功能越来越复杂,代码也会变得越来越复杂,团队的人数也会越来越多,这时单体架构就会带来一些问题:

  • 因为代码库非常的臃肿,从编译、构建、运行到测试这个时间会越来越长。

  • 技术栈几乎是受限的,比如一个 .NET 的工程,基本就是 C# 来开发了,不太可能混杂其他的语言。

  • 不方便横向扩展,只能整套程序进行扩,满足所有模块的需求,对资源的利用率非常差。

  • 不够敏捷,团队成员越来越多多时,都在同一个代码上进行修改、提交、合并,容易引发冲突和其他问题。

  • 一个很小的改动点,容易引发全身问题,导致系统崩溃,因为影响点多,测试成本也会很高。

  • 缺乏可靠性,我们就碰到过因为一个序列化的问题导致 CPU 占用很高,结果整个系统瘫痪了。

微服务架构

上面提到的单体架构存在的问题,采用微服务架构可以很好地解决。微服务的核心是为了解耦,构建成一个松耦合的分布式系统。

一个庞大的单体系统拆分成若干个小的服务,每个服务可以由一个小的团队来维护,团队会更加敏捷,构建发布的时间更短,代码也容易维护。

不同的微服务团队可以采用不同的技术栈,比如工作流引擎使用 .NET ,规则引擎可以使用 Java ,一些全新的模块更容易采用新的技术,人员流动和补充上也更加灵活。

每个服务通常采用独立的数据库,代码或者数据库层面的问题不会导致整个系统的崩溃。

扩展方便,这个很重要,如果监控发现流程引擎的压力很大,可以只针对这个服务进行横向扩展,服务器资源可以得到更好的利用。

上面说的都是好处,但没有任何一种技术是银弹,微服务解决问题的同时,也会带来更多的问题。

1、开发调试变得困难了,需要通过日志的方式或者借助一些远程调试工具。

2、单体架构中,模块之间的调用都是进程内,添加类库的引用后,就是本地方法的调用,微服务各自独立部署,就会涉及到进程间的通信。

3、线上问题往往需要多个服务团队一起来协作解决,会存在互相甩锅的问题。

4、在分布式系统中,事务、数据一致性、联合查询等相比较单体更加复杂。

5、持续集成、部署、运维的复杂度也显著提升。

6、随着服务越来越多,客户端怎样去找这些服务呢?

7、进程内的访问不存在网络的问题,拆分后的服务可能在同个机器的不同进程,更多的时候是不同机器的不同进程,网络问题导致服务不稳定怎么办?

为了解决这些问题,各种中间件和框架就应运而生,又会带来更多的学习成本。

在 .NET 技术栈中,会用到下面这些中间件:

  • 服务注册与发现:Consul。

  • 网关:Ocelot。

  • 熔断降级:Polly。

  • 服务链路追踪:SkyWalking 或 Twitter 的 Zipkin。

  • 配置中心:Apllo。

  • 鉴权中心:IdentityServer4。

在 Java 中也有 Spring Cloud 和 Spring Cloud Alibaba 这种全家桶套件可以使用。

要不要转微服务呢?

从单体到微服务是一个权衡和取舍的问题,切记不要跟风。以我的经验来看,可以分为两类:

  1. 做企业级系统。

  2. 做互联网系统。

做企业级应用大多都是项目交付型的,客户关系维系的好,后面可以做二期、三期,当然也有一锤子买卖的。这其中一个关键点是要快,单从快速来看,采用单体架构,开发、调试、部署都是最快的。

从客户角度来说,只要能满足业务,是单体还是微服务其实不太关心。

做互联网应用,也就是我们常说的 SaaS,也分为两种情况:

1、将现有的私有化部署的系统(单体架构)改造成支持 SaaS 的模式。

这种我也不建议一上来就大刀阔斧地进行微服务改造,可以在代码的结构上做一些调整,比如按照领域去拆分目录,不同领域之间的调用可以再进行一层抽象,目的是为了未来向微服务架构转化。

当团队的技术栈变得丰富了,比如原先只有 .NET ,现在有些模块采用的是 Java ,这时已然是朝着微服务架构发展了,只是粒度比较大而已,相应的一些中间件也需要引入,比如服务网关、服务发现、服务间通信等。

2、从零开始做一个 SaaS 系统。

互联网系统和企业级系统有很大的差别,如果说企业级系统更多关注功能性需求,那么互联网系统除了功能性需求,还需要关注非功能性需求,比如:横向扩展、限流降级、日志追踪、预警、灰度发布等。

即便因为时间关系,一开始是单体架构,我觉得也应该是微服务架构的单体,随着持续迭代和发展,根据实际情况逐步进行拆分。

如果时间上比较充裕,可以一开始就按照微服务架构进行分离,但粒度不要太小。

总结

  1. 解决常说的的三高问题(高并发、高性能、高可用),一个核心的思路就是拆,分而治之,所以说微服务肯定是能解决掉我们的很多问题,也是发展方向。

  2. 实践微服务需要根据当前的实际情况,如果单体运行的很好,也没什么问题,也不要为了炫技进行微服务改造。

  3. 如果决定要实践微服务,先做好单体架构的设计,让代码遵循面向对象的设计原则,否则即便形式上变成了微服务,也不能尝到微服务的甜头。

作者:不止dotNET

收起阅读 »

牛逼,一款 996 代码分析工具

程序员是一个创作型的职业,频繁的加班并不能增加产出,而国内 996 的公司文化,真的一言难尽。但是如果你进到一家公司,你能从哪些观察来判断这家公司的工作强度的(加班文化)?是看大家走得早不早吗?有一定的参考意义,但是如果走得晚呢,可能是大家不敢提前走而在公司耗...
继续阅读 »

程序员是一个创作型的职业,频繁的加班并不能增加产出,而国内 996 的公司文化,真的一言难尽。但是如果你进到一家公司,你能从哪些观察来判断这家公司的工作强度的(加班文化)?是看大家走得早不早吗?有一定的参考意义,但是如果走得晚呢,可能是大家不敢提前走而在公司耗时间。

今天要推荐一个代码分析工具 code996,它可以统计 Git 项目的 commit 时间分布,进而推导出这个项目的编码工作强度。这算是一种对项目更了解的方式,杜绝 996 从了解数据开始。

我们先来看 code996 分析出来的结果示例,以下是分析项目的基本情况:


通过图表查看 commit 提交分布:


对比项目工作时间类型:


如果你对 code996 是如何工作的,以下是作者的说明:


因为代码是公司的很重要的资产,泄露是肯定不行的,为了解决大家的后顾之忧,该项目是完全安全的。


code996 除了能够分析项目的实际工作强度,也能用来分析我们代码编写的情况,对自身了解自己代码编写效率的时段、最近的工作强度等都是非常好的一个输入。

更多项目详情请查看如下链接。

开源项目地址:https://github.com/hellodigua/code996

开源项目作者:hellodigua

收起阅读 »

如何从0到1构建一个稳定、高性能的Redis集群

这篇文章我想和你聊一聊 Redis 的架构演化之路。现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的?你也可以尝试回答一下以下这些问题:我使用 Redi...
继续阅读 »

这篇文章我想和你聊一聊 Redis 的架构演化之路。

现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的?

你也可以尝试回答一下以下这些问题:

  • 我使用 Redis 的场景很简单,只使用单机版 Redis 会有什么问题吗?
  • 我的 Redis 故障宕机了,数据丢失了怎么办?如何能保证我的业务应用不受影响?
  • 为什么需要主从集群?它有什么优势?
  • 什么是分片集群?我真的需要分片集群吗?

如果你对 Redis 已经有些了解,肯定也听说过数据持久化、主从复制、哨兵这些概念,它们之间又有什么区别和联系呢?

如果你存在这样的疑惑,这篇文章,我会从 0 到 1,再从 1 到 N,带你一步步构建出一个稳定、高性能的 Redis 集群。

在这个过程中,你可以了解到 Redis 为了做到稳定、高性能,都采取了哪些优化方案,以及为什么要这么做?

掌握了这些原理,这样平时你在使用 Redis 时,就能够做到「游刃有余」。

这篇文章干货很多,希望你可以耐心读完。

从最简单的开始:单机版 Redis

首先,我们从最简单的场景开始。

假设现在你有一个业务应用,需要引入 Redis 来提高应用的性能,此时你可以选择部署一个单机版的 Redis 来使用,就像这样:

这个架构非常简单,你的业务应用可以把 Redis 当做缓存来使用,从 MySQL 中查询数据,然后写入到 Redis 中,之后业务应用再从 Redis 中读取这些数据,由于 Redis 的数据都存储在内存中,所以这个速度飞快。

如果你的业务体量并不大,那这样的架构模型基本可以满足你的需求。是不是很简单?

随着时间的推移,你的业务体量逐渐发展起来了,Redis 中存储的数据也越来越多,此时你的业务应用对 Redis 的依赖也越来越重。

但是,突然有一天,你的 Redis 因为某些原因宕机了,这时你的所有业务流量,都会打到后端 MySQL 上,这会导致你的 MySQL 压力剧增,严重的话甚至会压垮 MySQL。

这时你应该怎么办?

我猜你的方案肯定是,赶紧重启 Redis,让它可以继续提供服务。

但是,因为之前 Redis 中的数据都在内存中,尽管你现在把 Redis 重启了,之前的数据也都丢失了。重启后的 Redis 虽然可以正常工作,但是由于 Redis 中没有任何数据,业务流量还是都会打到后端 MySQL 上,MySQL 的压力还是很大。

这可怎么办?你陷入了沉思。

有没有什么好的办法解决这个问题?

既然 Redis 只把数据存储在内存中,那是否可以把这些数据也写一份到磁盘上呢?

如果采用这种方式,当 Redis 重启时,我们把磁盘中的数据快速恢复到内存中,这样它就可以继续正常提供服务了。

是的,这是一个很好的解决方案,这个把内存数据写到磁盘上的过程,就是「数据持久化」。

数据持久化:有备无患

现在,你设想的 Redis 数据持久化是这样的:

但是,数据持久化具体应该怎么做呢?

我猜你最容易想到的一个方案是,Redis 每一次执行写操作,除了写内存之外,同时也写一份到磁盘上,就像这样:

没错,这是最简单直接的方案。

但仔细想一下,这个方案有个问题:客户端的每次写操作,既需要写内存,又需要写磁盘,而写磁盘的耗时相比于写内存来说,肯定要慢很多!这势必会影响到 Redis 的性能。

如何规避这个问题?

我们可以这样优化:Redis 写内存由主线程来做,写内存完成后就给客户端返回结果,然后 Redis 用另一个线程去写磁盘,这样就可以避免主线程写磁盘对性能的影响。

这确实是一个好方案。除此之外,我们可以换个角度,思考一下还有什么方式可以持久化数据?

这时你就要结合 Redis 的使用场景来考虑了。

回忆一下,我们在使用 Redis 时,通常把它用作什么场景?

是的,缓存。

把 Redis 当做缓存来用,意味着尽管 Redis 中没有保存全量数据,对于不在缓存中的数据,我们的业务应用依旧可以通过查询后端数据库得到结果,只不过查询后端数据的速度会慢一点而已,但对业务结果其实是没有影响的。

基于这个特点,我们的 Redis 数据持久化还可以用「数据快照」的方式来做。

那什么是数据快照呢?

简单来讲,你可以这么理解:

  1. 你把 Redis 想象成一个水杯,向 Redis 写入数据,就相当于往这个杯子里倒水
  2. 此时你拿一个相机给这个水杯拍一张照片,拍照的这一瞬间,照片中记录到这个水杯中水的容量,就是水杯的数据快照

也就是说,Redis 的数据快照,是记录某一时刻下 Redis 中的数据,然后只需要把这个数据快照写到磁盘上就可以了。

它的优势在于,只在需要持久化时,把数据「一次性」写入磁盘,其它时间都不需要操作磁盘。

基于这个方案,我们可以定时给 Redis 做数据快照,把数据持久化到磁盘上。

其实,上面说的这些持久化方案,就是 Redis 的「RDB」和「AOF」:

  • RDB:只持久化某一时刻的数据快照到磁盘上(创建一个子进程来做)
  • AOF:每一次写操作都持久到磁盘(主线程写内存,根据策略可以配置由主线程还是子线程进行数据持久化)

它们的区别除了上面讲到的,还有以下特点:

  1. RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度也快
  2. AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢

如果让你来选择持久化方案,你可以这样选择:

  1. 如果你的业务对于数据丢失不敏感,采用 RDB 方案持久化数据
  2. 如果你的业务对数据完整性要求比较高,采用 AOF 方案持久化数据

假设你的业务对 Redis 数据完整性要求比较高,选择了 AOF 方案,那此时你又会遇到这些问题:

  1. AOF 记录每一次写操作,随着时间增长,AOF 文件体积会越来越大
  2. 这么大的 AOF 文件,在数据恢复时变得非常慢

这怎么办?数据完整性要求变高了,恢复数据也变困难了?有没有什么方法,可以缩小文件体积?提升恢复速度呢?

我们继续来分析 AOF 的特点。

由于 AOF 文件中记录的都是每一次写操作,但对于同一个 key 可能会发生多次修改,我们只保留最后一次被修改的值,是不是也可以?

是的,这就是我们经常听到的「AOF rewrite」,你也可以把它理解为 AOF 「瘦身」。

我们可以对 AOF 文件定时 rewrite,避免这个文件体积持续膨胀,这样在恢复时就可以缩短恢复时间了。

再进一步思考一下,还有没有办法继续缩小 AOF 文件?

回顾一下我们前面讲到的,RDB 和 AOF 各自的特点:

  1. RDB 以二进制 + 数据压缩方式存储,文件体积小
  2. AOF 记录每一次写命令,数据最全

我们可否利用它们各自的优势呢?

当然可以,这就是 Redis 的「混合持久化」。

具体来说,当 AOF rewrite 时,Redis 先以 RDB 格式在 AOF 文件中写入一个数据快照,再把在这期间产生的每一个写命令,追加到 AOF 文件中。因为 RDB 是二进制压缩写入的,这样 AOF 文件体积就变得更小了。

此时,你在使用 AOF 文件恢复数据时,这个恢复时间就会更短了!

Redis 4.0 以上版本才支持混合持久化。

这么一番优化,你的 Redis 再也不用担心实例宕机了,当发生宕机时,你就可以用持久化文件快速恢复 Redis 中的数据。

但这样就没问题了吗?

仔细想一下,虽然我们已经把持久化的文件优化到最小了,但在恢复数据时依旧是需要时间的,在这期间你的业务应用还是会受到影响,这怎么办?

我们来分析有没有更好的方案。

一个实例宕机,只能用恢复数据来解决,那我们是否可以部署多个 Redis 实例,然后让这些实例数据保持实时同步,这样当一个实例宕机时,我们在剩下的实例中选择一个继续提供服务就好了。

没错,这个方案就是接下来要讲的「主从复制:多副本」。

主从复制:多副本

此时,你可以部署多个 Redis 实例,架构模型就变成了这样:

我们这里把实时读写的节点叫做 master,另一个实时同步数据的节点叫做 slave。

采用多副本的方案,它的优势是:

  1. 缩短不可用时间:master 发生宕机,我们可以手动把 slave 提升为 master 继续提供服务
  2. 提升读性能:让 slave 分担一部分读请求,提升应用的整体性能

这个方案不错,不仅节省了数据恢复的时间,还能提升性能,那它有什么问题吗?

你可以思考一下。

其实,它的问题在于:当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。

虽然比恢复数据要快得多,但还是需要人工介入处理。一旦需要人工介入,就必须要算上人的反应时间、操作时间,所以,在这期间你的业务应用依旧会受到影响。

怎么解决这个问题?我们是否可以把这个切换的过程,变成自动化呢?

对于这种情况,我们需要一个「故障自动切换」机制,这就是我们经常听到的「哨兵」所具备的能力。

哨兵:故障自动切换

现在,我们可以引入一个「观察者」,让这个观察者去实时监测 master 的健康状态,这个观察者就是「哨兵」。

具体如何做?

  1. 哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 哨兵发现异常,发起主从切换

有了这个方案,就不需要人去介入处理了,一切就变得自动化了,是不是很爽?

但这里还有一个问题,如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会误判。

这个问题怎么解决?

答案是,我们可以部署多个哨兵,让它们分布在不同的机器上,它们一起监测 master 的状态,流程就变成了这样:

  1. 多个哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障
  4. 多个哨兵经过协商后,判定 master 故障,则发起主从切换

所以,我们用多个哨兵互相协商来判定 master 的状态,这样一来,就可以大大降低误判的概率。

哨兵协商判定 master 异常后,这里还有一个问题:由哪个哨兵来发起主从切换呢?

答案是,选出一个哨兵「领导者」,由这个领导者进行主从切换。

问题又来了,这个领导者怎么选?

想象一下,在现实生活中,选举是怎么做的?

是的,投票。

在选举哨兵领导者时,我们可以制定这样一个选举规则:

  1. 每个哨兵都询问其它哨兵,请求对方为自己投票
  2. 每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次
  3. 首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换

其实,这个选举的过程就是我们经常听到的:分布式系统领域中的「共识算法」。

什么是共识算法?

我们在多个机器部署哨兵,它们需要共同协作完成一项任务,所以它们就组成了一个「分布式系统」。

在分布式系统领域,多个节点如何就一个问题达成共识的算法,就叫共识算法。

在这个场景下,多个哨兵共同协商,选举出一个都认可的领导者,就是使用共识算法完成的。

这个算法还规定节点的数量必须是奇数个,这样可以保证系统中即使有节点发生了故障,剩余超过「半数」的节点状态正常,依旧可以提供正确的结果,也就是说,这个算法还兼容了存在故障节点的情况。

共识算法在分布式系统领域有很多,例如 Paxos、Raft,哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。

现在,我们用多个哨兵共同监测 Redis 的状态,这样一来,就可以避免误判的问题了,架构模型就变成了这样:

好了,到这里我们先小结一下。

你的 Redis 从最简单的单机版,经过数据持久化、主从多副本、哨兵集群,这一路优化下来,你的 Redis 不管是性能还是稳定性,都越来越高,就算节点发生故障,也不用担心了。

你的 Redis 以这样的架构模式部署,基本上就可以稳定运行很长时间了。

随着时间的发展,你的业务体量开始迎来了爆炸性增长,此时你的架构模型,还能够承担这么大的流量吗?

我们一起来分析一下:

  1. 稳定性:Redis 故障宕机,我们有哨兵 + 副本,可以自动完成主从切换
  2. 性能:读请求量增长,我们可以再部署多个 slave,读写分离,分担读压力
  3. 性能:写请求量增长,但我们只有一个 master 实例,这个实例达到瓶颈怎么办?

看到了么,当你的写请求量越来越大时,一个 master 实例可能就无法承担这么大的写流量了。

要想完美解决这个问题,此时你就需要考虑使用「分片集群」了。

分片集群:横向扩展

什么是「分片集群」?

简单来讲,一个实例扛不住写压力,那我们是否可以部署多个实例,然后把这些实例按照一定规则组织起来,把它们当成一个整体,对外提供服务,这样不就可以解决集中写一个实例的瓶颈问题吗?

所以,现在的架构模型就变成了这样:

现在问题又来了,这么多实例如何组织呢?

我们制定规则如下:

  1. 每个节点各自存储一部分数据,所有节点数据之和才是全量数据
  2. 制定一个路由规则,对于不同的 key,把它路由到固定一个实例上进行读写

而分片集群根据路由规则所在位置的不同,还可以分为两大类:

  1. 客户端分片
  2. 服务端分片

客户端分片指的是,key 的路由规则放在客户端来做,就是下面这样:

这个方案的缺点是,客户端需要维护这个路由规则,也就是说,你需要把路由规则写到你的业务代码中。

如何做到不把路由规则耦合在业务代码中呢?

你可以这样优化,把这个路由规则封装成一个模块,当需要使用时,集成这个模块就可以了。

这就是 Redis Cluster 的采用的方案。

Redis Cluster 内置了哨兵逻辑,无需再部署哨兵。

当你使用 Redis Cluster 时,你的业务应用需要使用配套的 Redis SDK,这个 SDK 内就集成好了路由规则,不需要你自己编写了。

再来看服务端分片。

这种方案指的是,路由规则不放在客户端来做,而是在客户端和服务端之间增加一个「中间代理层」,这个代理就是我们经常听到的 Proxy。

而数据的路由规则,就放在这个 Proxy 层来维护。

这样一来,你就无需关心服务端有多少个 Redis 节点了,只需要和这个 Proxy 交互即可。

Proxy 会把你的请求根据路由规则,转发到对应的 Redis 节点上,而且,当集群实例不足以支撑更大的流量请求时,还可以横向扩容,添加新的 Redis 实例提升性能,这一切对于你的客户端来说,都是透明无感知的。

业界开源的 Redis 分片集群方案,例如 Twemproxy、Codis 就是采用的这种方案。

分片集群在数据扩容时,还涉及到了很多细节,这块内容不是本文章重点,所以暂不详述。

至此,当你使用分片集群后,对于未来更大的流量压力,都可以从容面对了!

总结

好了,我们来总结一下,我们是如何一步步构建一个稳定、高性能的 Redis 集群的。

首先,在使用最简单的单机版 Redis 时,我们发现当 Redis 故障宕机后,数据无法恢复的问题,因此我们想到了「数据持久化」,把内存中的数据也持久化到磁盘上一份,这样 Redis 重启后就可以从磁盘上快速恢复数据。

在进行数据持久化时,我们又面临如何更高效地将数据持久化到磁盘的问题。之后我们发现 Redis 提供了 RDB 和 AOF 两种方案,分别对应了数据快照和实时的命令记录。当我们对数据完整性要求不高时,可以选择 RDB 持久化方案。如果对于数据完整性要求较高,那么可以选择 AOF 持久化方案。

但是我们又发现,AOF 文件体积会随着时间增长变得越来越大,此时我们想到的优化方案是,使用 AOF rewrite 的方式对其进行瘦身,减小文件体积,再后来,我们发现可以结合 RDB 和 AOF 各自的优势,在 AOF rewrite 时使用两者结合的「混合持久化」方式,又进一步减小了 AOF 文件体积。

之后,我们发现尽管可以通过数据恢复的方式还原数据,但恢复数据也是需要花费时间的,这意味着业务应用还是会受到影响。我们进一步优化,采用「多副本」的方案,让多个实例保持实时同步,当一个实例故障时,可以手动把其它实例提升上来继续提供服务。

但是这样也有问题,手动提升实例上来,需要人工介入,人工介入操作也需要时间,我们开始想办法把这个流程变得自动化,所以我们又引入了「哨兵」集群,哨兵集群通过互相协商的方式,发现故障节点,并可以自动完成切换,这样就大幅降低了对业务应用的影响。

最后,我们把关注点聚焦在如何支撑更大的写流量上,所以,我们又引入了「分片集群」来解决这个问题,让多个 Redis 实例分摊写压力,未来面对更大的流量,我们还可以添加新的实例,横向扩展,进一步提升集群的性能。

至此,我们的 Redis 集群才得以长期稳定、高性能的为我们的业务提供服务。

这里我画了一个思维导图,方便你更好地去理解它们之间的关系,以及演化的过程。

后记

看到这里,我想你对如何构建一个稳定、高性能的 Redis 集群问题时,应该会有自己的见解了。

其实,这篇文章所讲的优化思路,围绕的主题就是「架构设计」的核心思想:

  • 高性能:读写分离、分片集群
  • 高可用:数据持久化、多副本、故障自动切换
  • 易扩展:分片集群、横向扩展

当我们讲到哨兵集群、分片集群时,这还涉及到了「分布式系统」相关的知识:

  • 分布式共识:哨兵领导者选举
  • 负载均衡:分片集群数据分片、数据路由

当然,除了 Redis 之外,对于构建任何一个数据集群,你都可以沿用这个思路去思考、去优化,看看它们到底是如何做的。

例如当你在使用 MySQL 时,你可以思考一下 MySQL 与 Redis 有哪些不同?MySQL 为了做到高性能、高可用,又是如何做的?其实思路都是类似的。

我们现在到处可见分布式系统、数据集群,我希望通过这篇文章,你可以理解这些软件是如何一步步演化过来的,在演化过程中,它们遇到了哪些问题,为了解决这些问题,这些软件的设计者设计了怎样的方案,做了哪些取舍?

你只有了解了其中的原理,掌握了分析问题、解决问题的能力,这样在以后的开发过程中,或是学习其它优秀软件时,就能快速地找到「重点」,在最短的时间掌握它,并能在实际应用中发挥它们的优势。

其实这个思考过程,也是做「架构设计」的思路。在做软件架构设计时,你面临的场景就是发现问题、分析问题、解决问题,一步步去演化、升级你的架构,最后在性能、可靠性方面达到一个平衡。虽然各种软件层出不穷,但架构设计的思想不会变,我希望你真正吸收的是这些思想,这样才可以做到以不变应万变。

来源:mp.weixin.qq.com/s/q79ji-cgfUMo7H0p254QRg

收起阅读 »

Go语言负责人离职后,一门国产语言诞生了

Go
1 事件回顾 上周,谷歌Go语言项目负责人Steve Francia宣布辞去职务,而他给出理由是:Go项目的工作停滞不前,让他感到难受。有意思的是,部分国内的Gopher(Go语言爱好者的自称)对Go语言也产生了新想法。比如,国内第一批Go语言爱好者之一的柴树...
继续阅读 »

1 事件回顾

上周,谷歌Go语言项目负责人Steve Francia宣布辞去职务,而他给出理由是:Go项目的工作停滞不前,让他感到难受。

有意思的是,部分国内的Gopher(Go语言爱好者的自称)对Go语言也产生了新想法。比如,国内第一批Go语言爱好者之一的柴树杉、全球Go贡献者榜上长期排名TOP 50的史斌等Gopher,他们决定以Go语言为蓝本,发起新的编程语言:凹语言™(凹读音“Wa”)。

目前凹语言™的代码已经在Github开源,并且提供了简单可执行的示例。根据其仓库的介绍,凹语言™的设计目标有以下几个:

1、披着Go和Rust语法外衣的C++语言

2、凹语言™源码文件后缀为.wa

3、凹语言™编译器兼容WaGo语法,凹语法与WaGo语法在AST层面一致(二者可生成相同的AST并无损的互相转换)

4、凹语言™支持中文/英文双语关键字,即任一关键字均有中文版和英文版,二者在语法层面等价


凹语言™示意,图片来源@GitHub

据柴树杉、史斌等人的说法,Go语言“克制”的风格是他们对编程语言审美的最大公约数。因此,凹语言™项目启动时大量借鉴了Go的设计思想和具体实现。

当然,他们也表示,选择Go语言作为初始的蓝本,是在有限投入下不得不作出的折衷。他们希望随着项目的发展,积累更多原创的设计,为自主创新的大潮贡献一点力量。

虽说柴树杉、史斌等人是资深的Gopher,偏爱Go语言并不难理解,但我们还是忍不住好奇:究竟Go语言有多神奇,让他们对Go语言这么着迷?

2 为什么选中Go语言

从许多使用过Go语言的开发者对Go的评价上看,Go语言在设计上有以下四个特点。

1、简单易用

不同于那些通过相互借鉴而不断增加新特性的主流编程语言(如C++、Java等),Go的设计者们在语言设计之初就拒绝走语言特性融合的道路,而选择了“做减法”。

他们把复杂留给了语言自身的设计和实现,留给了Go核心开发组,而将简单、易用和清晰留给了广大使用Go语言的开发者。因此,Go语言呈现出:

  • 简洁、常规的语法(不需要解析符号表),仅有25个关键字;

  • 没有头文件;

  • 显式依赖(package);

  • 没有循环依赖(package);

  • 常量只是数字;

  • 首字母大小写决定可见性;

  • 任何类型都可以拥有方法(没有类);

  • 没有子类型继承(没有子类);

  • 没有算术转换;

  • 没有构造函数或析构函数;

  • 赋值不是表达式;

  • 在赋值和函数调用中定义的求值顺序(无“序列点”概念);

  • 没有指针算术;

  • 内存总是初始化为零值;

  • 没有类型注解语法(如C++中的const、static等)

  • ……

2、偏好组合

C++、Java等主流面向对象语言,通过庞大的自上而下的类型体系、继承、显式接口实现等机制,将程序的各个部分耦合起来,但在Go语言中我们找不到经典面向对象的语法元素、类型体系和继承机制。

那Go语言是如何将程序的各个部分耦合在一起呢?是组合。

在语言设计层面,Go使用了正交的语法元素,包括Go语言无类型体系,类型之间是独立的,没有子类型的概念;每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的。

各类型之间通过类型嵌入,将已经实现的功能嵌入新类型中,以快速满足新类型的功能需求。在通过新类型实例调用方法时,方法的匹配取决于方法名字,而不是类型。

另外,通过在接口的定义中嵌入接口类型来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为Go语言的一种惯用法。

这是Go语言的一个创新设计:接口只是方法集合,且与实现者之间的关系是隐式的,如此可让程序各个部分之间的耦合降至最低。

3、并发和轻量

Go语言的三位设计者Rob Pike、Robert Griesemer和Ken Thompson曾认为C++标准委员会在思路上是短视的,因为硬件很可能在未来十年内发生重大变化,将语言与当时的硬件紧密耦合起来是十分不明智的,是没法给开发人员在编写大规模并发程序时带去太多帮助的。

因而他们把将面向多核、原生内置并发支持作为新语言的设计原则之一。

Go语言原生支持并发的设计哲学体现在下面两点。

(1)Go语言采用轻量级协程并发模型,使得Go应用在面向多核硬件时更具可扩展性。

(2)Go语言为开发者提供的支持并发的语法元素和机制。

4、面向工程

Go语言的设计者在Go语言最初设计阶段,就将解决工程问题作为Go的设计原则之一,进而考虑Go语法、工具链与标准库的设计,这也是Go与那些偏学院派、偏研究性编程语言在设计思路上的一个重大差异。

这让Go语言的规范足够简单灵活,有其他语言基础的程序员都能迅速上手。更重要的是Go自带完善的工具链,大大提高了团队协作的一致性。比如Gofmt自动排版Go代码,很大程度上杜绝了不同人写的代码排版风格不一致的问题。把编辑器配置成在编辑存档的时候自动运行Gofmt,这样在编写代码的时候可以随意摆放位置,存档的时候自动变成正确排版的代码。此外还有Gofix,Govet等非常有用的工具。

总之,Go在语言层面的简单让Go收获了不逊于C++/Java等的表现力的同时,还获得了更好的可读性、更高的开发效率等在软件工程领域更为重要的元素。

3 凹语言™的未来

虽然今天,Go凭借其优越的性能,已经成为主流编程语言之一(超过75%的CNCF项目,包括Kubernetes和Istio,都是用Go编写的,另外,Go也是主要的云应用程序语言之一),Go语言在中国也相当受欢迎,但我们还是不禁担心脱胎于Go的凹语言™,会有美好的未来吗?


Go语言搜索热度,图片来源@Google Trend

预测未来从来都是困难的,不过,好在凹语言™的前面有一个先行者——Go+语言,我们不妨基于Go+的发展,来大致推测凹语言™的未来。

Go+是七牛云CEO许式伟发明的编程语言,于2020年7月正式发布,2021年10月推出1.0版本,目前最新发布版本是今年6月13日发布的1.1版本。也就是说,从正式发布到现在,经过近两年的时间,Go+还处于初始阶段,距离大规模应用还有一定距离,那么可以预见,凹语言™在未来相当长的时间里,不会进入广大开发者的视野中。

另外,据ECUG Con 2022大会上许式伟发表的看法,虽然大家都比较看重编程语言的性能,但单从性能来看的话,许式伟认为Python在脚本语言里面只能算二流,Python其实并不快。

在许式伟看来,对新生的语言来说,最重要它选择的目标人群。

Go+选择的目标人群是全民,许式伟称其为“连儿童也能掌握的语言”,因而Go+从工程与STEM教育的一体化开始奠定用户基础。

正是Go+的这几个特性,让一部分开发者看好Go+的未来。而对Go+的正向预期,会成为Go+进一步发展的助力。

对凹语言™来说,这个道理也是适用的:凹语言™的发展重点可能不在于性能,而在于其选择哪些人群作为目标受众,以及通过何种方式获得种子用户。

如果日后凹语言™的项目方会公布这些消息,那么凹语言™的未来还是可以期待的。

来源:武穆、信远(51CTO技术栈)

收起阅读 »

快速搭建一个网关服务,动态路由、鉴权的流程,看完秒会(含流程图)

最近发现网易号有盗掘金文章的,xdm有空可以关注一下这个问题,希望帮助到大家同时能够保障自己权益。前言本文记录一下我是如何使用Gateway搭建网关服务及实现动态路由的,帮助大家学习如何快速搭建一个网关服务,了解路由相关配置,鉴权的流程及业务处理,有兴趣的一定...
继续阅读 »


最近发现网易号有盗掘金文章的,xdm有空可以关注一下这个问题,希望帮助到大家同时能够保障自己权益。

前言

本文记录一下我是如何使用Gateway搭建网关服务及实现动态路由的,帮助大家学习如何快速搭建一个网关服务,了解路由相关配置,鉴权的流程及业务处理,有兴趣的一定看到最后,非常适合没接触过网关服务的同学当作入门教程。

搭建服务

框架

  • SpringBoot 2.1

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.1.0.RELEASE</version>
</parent>
  • Spring-cloud-gateway-core

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-gateway-core</artifactId>
</dependency>
  • common-lang3

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
</dependency>

路由配置

网关作为请求统一入口,路由就相当于是每个业务系统的入口,通过路由规则则可以匹配到对应微服务的入口,将请求命中到对应的业务系统中

server:
port: 8080

spring:
cloud:
  gateway:
    enabled: true
    routes:
    - id: demo-server
      uri: http://localhost:8081
      predicates:
      - Path=/demo-server/**
      filters:
        - StripPrefix= 1

routes

配置项描述
id路由唯一id,使用服务名称即可
uri路由服务的访问地址
predicates路由断言
filters过滤规则

解读配置

  • 现在有一个服务demo-server部署在本机,地址和端口为127.0.0.1:8081,所以路由配置uri为http://localhost:8081

  • 使用网关服务路由到此服务,predicates -Path=/demo-server/**,网关服务的端口为8080,启动网关服务,访问localhost:8080/demo-server,路由断言就会将请求路由到demo-server

  • 直接访问demo-server的接口localhost:8081/api/test,通过网关的访问地址则为localhost:8080/demo-server/api/test,predicates配置将请求断言到此路由,filters-StripPrefix=1代表将地址中/后的第一个截取,所以demo-server就截取掉了

使用gateway通过配置文件即可完成路由的配置,非常方便,我们只要充分的了解配置项的含义及规则就可以了;但是这些配置如果要修改则需要重启服务,重启网关服务会导致整个系统不可用,这一点是无法接受的,下面介绍如何通过Nacos实现动态路由

动态路由

使用nacos结合gateway-server实现动态路由,我们需要先部署一个nacos服务,可以使用docker部署或下载源码在本地启动,具体操作可以参考官方文档即可

Nacos配置


groupId: 使用网关服务名称即可

dataId: routes

配置格式: json

[{
     "id": "xxx-server",
     "order": 1, #优先级
     "predicates": [{ #路由断言
         "args": {
             "pattern": "/xxx-server/**"
        },
         "name": "Path"
    }],
     "filters":[{ #过滤规则
         "args": {
             "parts": 0 #k8s服务内部访问容器为http://xxx-server/xxx-server的话,配置0即可
        },
         "name": "StripPrefix" #截取的开始索引
    }],
     "uri": "http://localhost:8080/xxx-server" #目标地址
}]

json格式配置项与yaml中对应,需要了解配置在json中的写法

比对一下json配置与yaml配置

{
   "id":"demo-server",
   "predicates":[
      {
           "args":{
               "pattern":"/demo-server/**"
          },
           "name":"Path"
      }
  ],
   "filters":[
      {
           "args":{
               "parts":1
          },
           "name":"StripPrefix"
      }
  ],
   "uri":"http://localhost:8081"
}
spring:
 cloud:
   gateway:
     enabled: true
     routes:
     - id: demo-server
       uri: http://localhost:8081
       predicates:
       - Path=/demo-server/**
       filters:
         - StripPrefix= 1

代码实现

Nacos实现动态路由的方式核心就是通过Nacos配置监听,配置发生改变后执行网关相关api创建路由


@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {

   private static final Logger LOGGER = LoggerFactory.getLogger(NacosDynamicRouteService.class);

   @Autowired
   private RouteDefinitionWriter routeDefinitionWriter;

   private ApplicationEventPublisher applicationEventPublisher;

   /** 路由id */
   private static List<String> routeIds = Lists.newArrayList();

   /**
    * 监听nacos路由配置,动态改变路由
    * @param configInfo
    */
   @NacosConfigListener(dataId = "routes", groupId = "gateway-server")
   public void routeConfigListener(String configInfo) {
       clearRoute();
       try {
           List<RouteDefinition> gatewayRouteDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
           for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
               addRoute(routeDefinition);
          }
           publish();
           LOGGER.info("Dynamic Routing Publish Success");
      } catch (Exception e) {
           LOGGER.error(e.getMessage(), e);
      }
       
  }


   /**
    * 清空路由
    */
   private void clearRoute() {
       for (String id : routeIds) {
           routeDefinitionWriter.delete(Mono.just(id)).subscribe();
      }
       routeIds.clear();
  }

   @Override
   public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
       this.applicationEventPublisher = applicationEventPublisher;
  }

   /**
    * 添加路由
    *
    * @param definition
    */
   private void addRoute(RouteDefinition definition) {
       try {
           routeDefinitionWriter.save(Mono.just(definition)).subscribe();
           routeIds.add(definition.getId());
      } catch (Exception e) {
           LOGGER.error(e.getMessage(), e);
      }
  }

   /**
    * 发布路由、使路由生效
    */
   private void publish() {
       this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
  }
}

过滤器

gateway提供GlobalFilter及Ordered两个接口用来定义过滤器,我们自定义过滤器只需要实现这个两个接口即可

  • GlobalFilter filter() 实现过滤器业务

  • Ordered getOrder() 定义过滤器执行顺序

通常一个网关服务的过滤主要包含 鉴权(是否登录、是否黑名单、是否免登录接口...) 限流(ip限流等等)功能,我们今天简单介绍鉴权过滤器的流程实现

鉴权过滤器

需要实现鉴权过滤器,我们先得了解登录及鉴权流程,如下图所示

由图可知,我们鉴权过滤核心就是验证token是否有效,所以我们网关服务需要与业务系统在同一个redis库,先给网关添加redis依赖及配置

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
redis:
  host: redis-server
  port: 6379
  password:
  database: 0

代码实现

  • 1.定义过滤器AuthFilter

  • 2.获取请求对象 从请求头或参数或cookie中获取token(支持多种方式传token对于客户端更加友好,比如部分web下载请求会新建一个页面,在请求头中传token处理起来比较麻烦)

  • 3.没有token,返回401

  • 4.有token,查询redis是否有效

  • 5.无效则返回401,有效则完成验证放行

  • 6.重置token过期时间、添加内部请求头信息方便业务系统权限处理

@Component
public class AuthFilter implements GlobalFilter, Ordered {

@Autowired
private RedisTemplate<String, String> redisTemplate;

private static final String TOKEN_HEADER_KEY = "auth_token";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 2.获取token
String token = getToken(request);
ServerHttpResponse response = exchange.getResponse();
if (StringUtils.isBlank(token)) {
// 3.token为空 返回401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 4.验证token是否有效
String userId = getUserIdByToken(token);
if (StringUtils.isBlank(userId)) {
// 5.token无效 返回401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// token有效,后续业务处理
// 从写请求头,方便业务系统从请求头获取用户id进行权限相关处理
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
request = builder.header("user_id", userId).build();
// 延长缓存过期时间-token缓存用户如果一直在操作就会一直重置过期
// 这样避免用户操作过程中突然过期影响业务操作及体验,只有用户操作间隔时间大于缓存过期时间才会过期
resetTokenExpirationTime(token, userId);
// 完成验证
return chain.filter(exchange);
}


@Override
public int getOrder() {
// 优先级 越小越优先
return 0;
}

/**
* 从redis中获取用户id
* 在登录操作时候 登陆成功会生成一个token, redis得key为auth_token:token 值为用户id
*
* @param token
* @return
*/
private String getUserIdByToken(String token) {
String redisKey = String.join(":", "auth_token", token);
return redisTemplate.opsForValue().get(redisKey);
}

/**
* 重置token过期时间
*
* @param token
* @param userId
*/
private void resetTokenExpirationTime(String token, String userId) {
String redisKey = String.join(":", "auth_token", token);
redisTemplate.opsForValue().set(redisKey, userId, 2, TimeUnit.HOURS);
}


/**
* 获取token
*
* @param request
* @return
*/
private static String getToken(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
// 从请求头获取token
String token = headers.getFirst(TOKEN_HEADER_KEY);
if (StringUtils.isBlank(token)) {
// 请求头无token则从url获取token
token = request.getQueryParams().getFirst(TOKEN_HEADER_KEY);
}
if (StringUtils.isBlank(token)) {
// 请求头和url都没有token则从cookies获取
HttpCookie cookie = request.getCookies().getFirst(TOKEN_HEADER_KEY);
if (cookie != null) {
token = cookie.getValue();
}
}
return token;
}
}

总结

Gateway通过配置项可以实现路由功能,整合Nacos及配置监听可以实现动态路由,实现GlobalFilter, Ordered两个接口可以快速实现一个过滤器,文中也详细的介绍了登录后的请求鉴权流程,如果有不清楚地方可以评论区见咯。

来源:juejin.cn/post/7004756545741258765

收起阅读 »

七夕节马上要到了,前端工程师,后端工程师,算法工程师都怎么哄女朋友开心?

这篇文章的前提是,你得有个女朋友,没有就先收藏着吧!七夕节的来源是梁山伯与祝英台的美丽传说,化成了一对蝴蝶~ 美丽的神话!虽然现在一般是过214的情人节了,但是不得不说,古老的传统的文化遗产,还是要继承啊~在互联网公司中,主要的程序员品种包括:前端工程师,后端...
继续阅读 »

这篇文章的前提是,你得有个女朋友,没有就先收藏着吧!

七夕节的来源是梁山伯与祝英台的美丽传说,化成了一对蝴蝶~ 美丽的神话!虽然现在一般是过214的情人节了,但是不得不说,古老的传统的文化遗产,还是要继承啊~

在互联网公司中,主要的程序员品种包括:前端工程师,后端工程师,算法工程师。

对于具体的职业职能划分还不是很清楚的,我们简单的介绍一下不同程序员岗位的职责:

前端程序员:绘制UI界面,与设计和产品经理进行需求的对接,绘制特定的前端界面推向用户

后端程序员:接收前端json字符串,与数据库对接,将json推向前端进行显示

算法工程师:进行特定的规则映射,优化函数的算法模型,改进提高映射准确率。

七夕节到了,怎么结合自身的的专业技能,哄女朋友开心呢?

前端工程师:我先来,画个动态的晚霞页面!

1.定义样式风格:

.star {
 width: 2px;
 height: 2px;
 background: #f7f7b6;
 position: absolute;
 left: 0;
 top: 0;
 backface-visibility: hidden;
}

2.定义动画特性

@keyframes rotate {
 0% {
   transform: perspective(400px) rotateZ(20deg) rotateX(-40deg) rotateY(0);
}

 100% {
   transform: perspective(400px) rotateZ(20deg) rotateX(-40deg) rotateY(-360deg);
}
}

3.定义星空样式数据

export default {
 data() {
   return {
     starsCount: 800, //星星数量
     distance: 900, //间距
  }
}
}

4.定义星星运行速度与规则:

starNodes.forEach((item) => {
     let speed = 0.2 + Math.random() * 1;
     let thisDistance = this.distance + Math.random() * 300;
     item.style.transformOrigin = `0 0 ${thisDistance}px`;
     item.style.transform =
         `
       translate3d(0,0,-${thisDistance}px)
       rotateY(${Math.random() * 360}deg)
       rotateX(${Math.random() * -50}deg)
       scale(${speed},${speed})`;
  });

前端预览效果图:


后端工程师看后,先点了点头,然后表示不服,画页面太肤浅了,我开发一个接口,定时在女朋友生日的时候发送祝福邮件吧!

1.导入pom.xml 文件

        <!-- mail邮件服务启动器 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-mail</artifactId>
       </dependency>

2.application-dev.properties内部增加配置链接

#QQ\u90AE\u7BB1\u90AE\u4EF6\u53D1\u9001\u670D\u52A1\u914D\u7F6E
spring.mail.host=smtp.qq.com
spring.mail.port=587

## qq邮箱
spring.mail.username=#yourname#@qq.com
## 这里填邮箱的授权码
spring.mail.password=#yourpassword#

3.配置邮件发送工具类 MailUtils.java

@Component
public class MailUtils {
   @Autowired
   private JavaMailSenderImpl mailSender;
   
   @Value("${spring.mail.username}")
   private String mailfrom;

   // 发送简单邮件
   public void sendSimpleEmail(String mailto, String title, String content) {
       // 定制邮件发送内容
       SimpleMailMessage message = new SimpleMailMessage();
       message.setFrom(mailfrom);
       message.setTo(mailto);
       message.setSubject(title);
       message.setText(content);
       // 发送邮件
       mailSender.send(message);
  }
}

4.测试使用定时注解进行注释

@Component
class DemoApplicationTests {

   @Autowired
   private MailUtils mailUtils;

   /**
    * 定时邮件发送任务,每月1日中午12点整发送邮件
    */
   @Scheduled(cron = "0 0 12 1 * ?")
   void sendmail(){
       // 定制邮件内容
       StringBuffer content = new StringBuffer();
       content.append("HelloWorld");
       //分别是接收者邮箱,标题,内容
       mailUtils.sendSimpleEmail("123456789@qq.com","自定义标题",content.toString());
  }
}

@scheduled注解 使用方法: cron:秒,分,时,天,月,年,* 号表示 所有的时间均匹配

5.工程进行打包,部署在服务器的容器中运行即可。

算法工程师,又开发接口,又画页面,我就训练一个自动写诗机器人把!

1.定义神经网络RNN结构

def neural_network(model = 'gru', rnn_size = 128, num_layers = 2):
   cell = tf.contrib.rnn.BasicRNNCell(rnn_size, state_is_tuple = True)
   cell = tf.contrib.rnn.MultiRNNCell([cell] * num_layers, state_is_tuple = True)
   initial_state = cell.zero_state(batch_size, tf.float32)
   with tf.variable_scope('rnnlm'):
       softmax_w = tf.get_variable("softmax_w", [rnn_size, len(words)])
       softmax_b = tf.get_variable("softmax_b", [len(words)])
       embedding = tf.get_variable("embedding", [len(words), rnn_size])
       inputs = tf.nn.embedding_lookup(embedding, input_data)
   outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, initial_state = initial_state, scope = 'rnnlm')
   output = tf.reshape(outputs, [-1, rnn_size])
   logits = tf.matmul(output, softmax_w) + softmax_b
   probs = tf.nn.softmax(logits)
   return logits, last_state, probs, cell, initial_state

2.定义模型训练方法:

def train_neural_network():
   logits, last_state, _, _, _ = neural_network()
   targets = tf.reshape(output_targets, [-1])
   loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example([logits], [targets], \
      [tf.ones_like(targets, dtype = tf.float32)], len(words))
   cost = tf.reduce_mean(loss)
   learning_rate = tf.Variable(0.0, trainable = False)
   tvars = tf.trainable_variables()
   grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), 5)
   #optimizer = tf.train.GradientDescentOptimizer(learning_rate)
   optimizer = tf.train.AdamOptimizer(learning_rate)
   train_op = optimizer.apply_gradients(zip(grads, tvars))

   Session_config = tf.ConfigProto(allow_soft_placement = True)
   Session_config.gpu_options.allow_growth = True

   trainds = DataSet(len(poetrys_vector))

   with tf.Session(config = Session_config) as sess:
       sess.run(tf.global_variables_initializer())

       saver = tf.train.Saver(tf.global_variables())
       last_epoch = load_model(sess, saver, 'model/')

       for epoch in range(last_epoch + 1, 100):
           sess.run(tf.assign(learning_rate, 0.002 * (0.97 ** epoch)))
           #sess.run(tf.assign(learning_rate, 0.01))

           all_loss = 0.0
           for batche in range(n_chunk):
               x,y = trainds.next_batch(batch_size)
               train_loss, _, _ = sess.run([cost, last_state, train_op], feed_dict={input_data: x, output_targets: y})

               all_loss = all_loss + train_loss

               if batche % 50 == 1:
                   print(epoch, batche, 0.002 * (0.97 ** epoch),train_loss)

           saver.save(sess, 'model/poetry.module', global_step = epoch)
           print (epoch,' Loss: ', all_loss * 1.0 / n_chunk)

3.数据集预处理

poetry_file ='data/poetry.txt'
# 诗集
poetrys = []
with open(poetry_file, "r", encoding = 'utf-8') as f:
   for line in f:
       try:
           #line = line.decode('UTF-8')
           line = line.strip(u'\n')
           title, content = line.strip(u' ').split(u':')
           content = content.replace(u' ',u'')
           if u'_' in content or u'(' in content or u'(' in content or u'《' in content or u'[' in content:
               continue
           if len(content) < 5 or len(content) > 79:
               continue
           content = u'[' + content + u']'
           poetrys.append(content)
       except Exception as e:
           pass

poetry.txt文件中存放这唐诗的数据集,用来训练模型

4.测试一下训练后的模型效果:

藏头诗创作:“七夕快乐”

模型运算的结果


哈哈哈,各种节日都是程序员的表(zhuang)演(bi) 时间,不过这些都是锦上添花,只有实实在在,真心,才会天长地久啊~

提前祝各位情侣七夕节快乐!

作者:千与编程
来源:juejin.cn/post/6995491512716918814

收起阅读 »

安全对等问题:确保移动应用跨平台安全性

一段时间以来,人们都“知道”,作为移动平台的 Android 不如 iOS 安全,这已经成为常识。似乎除了消费者,每个人都知道。2021 年 8 月对 10000 名移动消费者进行的一项全球调查发现,iOS 和 Android 用户的安全期望基本一致。然而,尽...
继续阅读 »

一段时间以来,人们都“知道”,作为移动平台的 Android 不如 iOS 安全,这已经成为常识。似乎除了消费者,每个人都知道。2021 年 8 月对 10000 名移动消费者进行的一项全球调查发现,iOS 和 Android 用户的安全期望基本一致。

然而,尽管消费者有这样的期望,而且从本质上讲,一个移动平台并不一定比另一个平台更安全,但移动应用很少能实现 Android 和 iOS 的安全功能对等。事实上,许多移动应用甚至缺少最基本的安全保护措施。让我们看看这是为什么。

1

移动应用安全需要多层次防御

大多数安全专家和第三方标准组织都会同意,移动应用安全需要多层次防御,包括以下核心领域的多种安全特性:

  • 代码混淆和应用护盾(Application Shielding):保护移动应用的二进制文件和源代码,防止逆向工程。

  • 数据加密:保护应用中存储和使用的数据。

  • 安全通信:保护在应用和应用后端之间传递的数据,包括确保用于建立可信连接的数字证书的真实性和有效性。

  • 操作系统防护:保护应用免受未经授权的操作系统修改(如 rooting 和越狱)所影响。

开发人员应该在应用的 iOS 和 Android 版本中均衡地实现这些功能的组合,形成一致的安全防御。而且,他们应该在开发周期的早期添加这些功能——这个概念被称为安全“左移”。听起来很容易吧?理论上,是的,但在实践中,如果使用“传统”方法,要实现移动应用多层次安全防御实际上是相当困难的。

多年来,移动开发人员一直试图使用传统的工具集来实现应用内移动应用安全,包括第三方开源库、商业移动应用安全 SDK 或专用编译器。第一个主要的挑战是,移动应用的安全从来无法通过“银弹”实现。由于移动应用在不受保护的环境中运行,并存储和处理大量有价值的信息,有许多方法可以攻击它们。黑客有无穷无尽的、免费提供而又非常强大的工具集可以使用,而且可以全天候地研究和攻击应用而不被发现。

2

移动安全要求

因此,为了构建一个强大的防御体系,移动开发人员需要实施一个既“广”且“深”的多层次防御。所谓“广”,我指的是不同保护类别的多种安全特性,它们彼此相互补充,如加密和混淆。所谓“深”,我指的是每个安全特性都应该有多种检测或保护方法。例如,一个越狱检测 SDK 如果只在应用启动时进行检查,就不会很有效,因为攻击者很容易绕过。

或者考虑下反调试,这是一种重要的运行时防御,可以防止攻击者使用调试器来执行动态分析——他们会在一个受控的环境中运行应用,为的是了解或修改应用的行为。有许多类型的调试器——有一些基于 LLDB——是针对原生代码的,如 C++ 或 Objective C,其他的在 Java 或 Kotlin 层进行检查,诸如此类。每个调试器连接和分析应用的工作方式都略有不同。因此,为了使反调试防御奏效,应用需要识别正在使用的多种调试方法,并动态地进行恰当的防御,因为黑客会继续尝试不同的调试工具或方法,直到他们找到一个可以成功的。

3

防篡改

安全要求清单并不仅限于此。每个应用都需要防篡改功能,如校验和验证、预防二进制补丁,以及应用的重新打包、重新签名、模拟器和仿真器,等等。毫不夸张,仅是针对一个操作系统研究和实现这些功能或保护方法中的一项,就需要至少几个人周的开发时间。而且还要有一个前提,就是移动开发人员已经拥有特定安全领域的专业知识,但情况往往并非如此。复杂度可能会快速增加,到目前为止,我们只讨论了一个保护类别——运行时或动态保护。想象一下,如果提到的每个功能都需要一到两周的开发时间,那么实现全部安全特性得付出多大的时间成本。

4

防越狱 /Rooting

接下来,你还需要操作系统层面的保护,如防越狱 /rooting,在移动操作系统遭破坏的情况下保护应用。越狱 /rooting 使移动应用容易受到攻击,因为它允许对操作系统和文件系统进行完全的管理控制,破坏了整个安全模型。而且,仅仅检测越狱 /rooting 已经不够了,因为黑客们一直在不断地改进他们的工具。要说最先进的越狱和 rooting 工具,在 iOS 上是 Checkra1n,在 Android 上是 Magisk——还有许多其他的工具。其中,还有一些工具用于隐藏或掩盖活动及管理超级用户权限——通常授予恶意应用。朋友们,如果你使用 SDK 或第三方库实现了越狱或 rooting 检测,那么你的保护措施很有可能已经过时或者很容易被绕过,尤其是在没有对应用的源代码进行充分混淆的情况下。

5

代码混淆

如果你使用 SDK 或第三方库来实现安全防护,那在未混淆的应用中几乎没什么用——为什么?因为黑客使用 Hopper、IDA-pro 等开源工具,就可以很容易地反编译或反汇编,找到 SDK 的源代码,或使用类似 Frida 这样的动态二进制工具箱,注入他们自己的恶意代码,修改应用的行为,或简单地禁用安全 SDK。

代码混淆可以防止攻击者了解移动应用的源代码。而且,我们总是建议使用多种混淆方法,包括混淆本地代码或非本地代码和库,以及混淆应用的逻辑结构或控制流。例如,可以使用控制流混淆或重命名函数、类、方法、变量等来实现。不要忘了还要混淆调试信息。

从现实世界的数据中可以看出,大多数移动应用都缺乏足够的混淆,只混淆了应用的一小部分代码,这项对超过 100 万个 Android 应用的研究清楚地说明了这一点。正如该研究指出的那样,造成这种情况的原因是,对于大多数移动开发人员来说,依赖专用编译器的传统混淆方法实在是太复杂和费时,难以全面实施。相反,许多开发人员只实现了单一的混淆功能,或者只混淆了代码库的一小部分。在这项研究中,研究人员发现,大多数应用只实现了类名混淆,这本身很容易被攻陷。拿书打个比方,类名混淆本身就像是混淆了一本书的“目录”,但书中所有实际的页和内容却并没有混淆。这种表面的混淆相当容易被绕过。

6

数据保护和加密

接着说数据保护,你还需要借助加密来保护应用和用户数据——在移动应用中,有很多地方存储着数据,包括沙盒、内存以及应用的代码或字符串。要自己实现加密,有很多棘手的问题需要解决,包括密钥衍生(key derivation)、密码套件和加密算法组合、密钥大小及强度。许多应用使用了多种编程语言,每一种都需要不同的 SDK,或者会导致你无法控制的不兼容性,又或是需要你无法访问的依赖。而数据类型的差异也有复杂性增加和性能下降的风险。

然后,还有一个典型的问题,即在哪里存储加密密钥。如果密钥存储在应用内部,那它们可能会被反向工程的攻击者发现,然后他们就可以用来解密数据。这就是为什么我们说动态密钥生成是一个非常重要的功能。通过动态密钥生成,加密密钥只在运行时生成,而不会存储在应用或移动设备上。此外,密钥只使用一次,可以防止攻击者发现或截获它们。

那么传输中的数据呢?仅靠 TLS 是不够的,因为有很多方法可以侵入应用的连接。检查和验证 TLS 会话和证书很重要,这可以确保所有的证书和 CA 都是有效且真实的,受到行业标准加密的保护。这可以防止黑客获得 TLS 会话的控制权。然后还有证书固定,可以防止连接到遭到入侵的服务器,或保护服务器,拒绝遭到入侵的应用连接(例如,如果你的应用被变成了一个恶意机器人)。

7

欺诈、恶意软件、防盗版

最后,还有反欺诈、反恶意软件和反盗版保护,你可以在上述基线保护的基础上增加防护层,用于防止非常高级或专门的威胁。这些保护措施可能包括可以防止应用覆盖攻击、自动点击器、钩子框架和动态二进制工具、内存注入、键盘记录器、密钥注入或可访问性滥用的功能,所有这些都是移动欺诈或移动恶意软件的常用武器。

不难想象,即使是实现上述功能的一个子集,也需要大量的时间和资源。到目前为止,我只是谈了一个强大的安全防御所需的特性和功能。即使你内部有资源和所需的技能组合,那么拼凑出一个防御体系的行动挑战又是什么呢?让我们探讨一下开发团队可能会遇到的一些实施挑战。

8

不同平台和框架之间的实现差异

鉴于用于构建移动应用的 SDK/ 库及原生或非原生编程语言之间存在无数的框架差异和不兼容,开发人员将面临的下一个问题是如何分别为 Android 和 iOS 实现这些安全功能。虽然软件开发工具包(SDK)提供了一些标准安全功能,但没有 SDK 能普遍覆盖所有的平台或框架。

当开发人员试图使用 SDK 或开源库来实现移动应用安全时,所面临的一个主要挑战在于,这些方法都依赖于源代码,需要对应用代码进行修改。而结果是,这些方法中的每一个都明确地与应用所使用的特定编程语言绑定,并且还暴露给了各种编程语言或是这些语言和框架的包“依赖”。

通常,iOS 应用使用 Objective-C 或 Swift 构建,而 Android 应用使用 Java 或 Kotlin 以及使用 C 和 C++ 编写原生库。例如,假如你想对存储在 Android 和 iOS 应用中的数据进行加密。如果你找到了一些第三方 Android 加密库亦或是 Java 或 Kotlin 的 SDK,它们不一定适用于应用中使用的 C 或 C++ 代码部分(原生库)。

在 iOS 中也是如此。你浏览 StackOverflow 时可能会发现,在 Swift 中常用的 Cryptokit 框架对 Objective C 不起作用。

那么,非原生或跨平台应用呢?它们是完全不同的赛道,因为你要处理的是 JavaScript 等 Web 技术和 React Native、Cordova、Flutter 或 Xamarin 等非原生框架,它们无法直接(或根本不能)使用为原生语言构建的 SDK 或库。此外,对于非原生应用,你可能无法获得相关的源代码文件,从源头实现加密。

关于这个问题,有一个真实的例子,请看 Stack Overflow 上的这篇帖子。开发人员需要在一个 iOS 应用中实现代码混淆,其中 React Native(一个非原生框架)和 Objective C(一种原生编码语言)之间存在多个依赖关系。由于 iOS 项目中没有可以混淆 React Native 代码的内置库,开发人员需要使用一个外部包(依赖关系 #1)。此外,该外部包还依赖下游的一个库或包来混淆 JavaScript 代码(依赖关系 #2)。现在,如果第三方库的开发人员决定废弃该解决方案,会发生什么?我们的一个客户就面临着这样的问题,这导致他们的应用不符合 PCI 标准。

那么,你认为需要多少开发人员来实现我刚才描述的哪怕是一小部分功能?又需要多长时间?你有足够的时间在现有的移动应用发布过程中实现所需的安全功能吗?

9

DevOps 是敏捷 + 自动化,传统安全是单体 + 手动

移动应用是在一个快节奏、灵活且高度自动化的敏捷模式下开发和发布的。为了使构建和发布更快速、更简单,大多数 Android 和 iOS DevOps 团队都围绕 CI/CD 和其他自动化工具构建了最佳管道。另一方面,安全团队无法访问或查看 DevOps 系统,而且大多数安全工具并不是针对敏捷方法构建的,因为它们在很大程度上依赖于手动编程或实施,在这种情况下,单个安全功能的实施时间可能会长于发布时间表允许的时间。

为了弥补这些不足,一些组织在向公共应用商店发布应用之前,会使用代码扫描和渗透测试,以深入探查漏洞和其他移动应用问题。当发现漏洞时,企业就会面临一个艰难的决定:是在未进行必要保护的情况下发布应用,还是推迟发布,让开发人员有时间来解决安全问题。当这种情况发生时,推荐的安全保护措施往往会被忽视。

开发人员并不懒惰,而是他们用于实现安全保护的系统和工具根本无法匹配现代敏捷 /DevOps 开发的快节奏。

10

实现强大的移动应用安全和平台对等的五个步骤

一般来说,自动化是实现安全对等和强大的移动应用安全的关键所在。以下是在应用发布周期内将移动应用安全打造为应用组成部分的五个步骤。

第 1 步:明确希望得到什么样的安全成果

开发、运营和安全团队必须就移动安全预期达成一致。对于组织作为起点的安全目标,人们要有一个共同的理解,如 OWASP Mobile Top 10、TRM 移动应用安全指南和移动应用安全验证标准(MASVS)。一旦确定了目标并选择了标准,所有团队成员都要知道这对他们的工作流有何影响。

第 2 步:移动应用安全的实施必须自动化

安全非常复杂,手动编码很慢,而且容易出错。评估并利用自动化系统,借助人工智能和机器学习(ML)将安全集成到移动应用中。通常情况下,这些都是无代码平台,可以自动将安全构建到移动应用中,它们通常被称为安全构建系统。

第 3 步:将安全作为开发周期的一部分——安全左移

移动应用安全模型左移是指,移动开发人员需要在构建应用的同时构建安全特性。

一旦选择了自动化安全实施平台,就应该将其整合到团队的持续集成(CI)和持续交付(CD)流程中,这可以加速开发生命周期,所有团队——开发、运营和安全——在整个冲刺期间都应该保持密切合作。此外,企业可以为每个 Android 和 iOS 应用所需的特定安全特性创建可重复使用的移动安全模板,从而更接近实现平台对等。

第 4 步:确保即时确认和验证

如果没有办法即时验证所需的安全功能是否包含在发布中,那么在发布会议上就会出现争执,可能导致应用发布或更新延期。验证和确认应该自动记录,防止最后一刻的发布混乱。

第 4 步:确保即时确认和验证

开发团队需要可预测性和明确的预算。通过采用自动化的安全方法,应用开发团队可以减少人员和开发费用的意外变化,因为它消除了手动将安全编码到移动应用时固有的不确定性。

11

小结

安全对等问题是一个大问题,但它是一个更大问题的一部分,即移动应用普遍缺乏安全性。通过在安全实现中采用与特性开发相同或更高程度的自动化,开发人员可以确保他们针对每个平台发布的每一个应用都免受黑客、骗子和网络犯罪分子的侵害。

作者简介:

Alan Bavosa 是 Appdome 的安全产品副总裁。长期以来,他一直担任安全产品执行官,曾是 Palerra(被 Oracle 收购)和 Arcsight(被 HP 收购)的产品主管。

原文链接:

https://www.infoq.com/articles/secure-mobile-apps-parity-problem/


收起阅读 »

MySQL:max_allowed_packet 影响了什么?

数据包如果要发送超过 16M 的数据怎么办?那怎样算一个数据包?下面我们通过测试来讨论 max_allowed_packet 的实际影响。如果 SQL 文件中有单个 SQL 大小超过 max_allowed_packet ,会报错:##导出时设置 mysqld...
继续阅读 »

max_allowed_packet 表示 MySQL Server 或者客户端接收的 packet 的最大大小,packet 即数据包,MySQL Server 和客户端上都有这个限制。

数据包

每个数据包,都由包头、包体两部分组成,包头由 3 字节的包体长度、1 字节的包编号组成。3 字节最多能够表示 2 ^ 24 = 16777216 字节(16 M),也就是说,一个数据包的包体长度必须小于等于 16M 。

如果要发送超过 16M 的数据怎么办?

当要发送大于 16M 的数据时,会把数据拆分成多个 16M 的数据包,除最后一个数据包之外,其它数据包大小都是 16M。而 MySQL Server 收到这样的包后,如果发现包体长度等于 16M ,它就知道本次接收的数据由多个数据包组成,会先把当前数据包的内容写入缓冲区,然后接着读取下一个数据包,并把下一个数据包的内容追加到缓冲区,直到读到结束数据包,就接收到客户端发送的完整数据了。

那怎样算一个数据包?

  • 一个 SQL 是一个数据包

  • 返回查询结果时,一行数据算一个数据包

  • 解析的 binlog ,如果用 mysql 客户端导入,一个 SQL 算一个数据包

  • 在复制中,一个 event 算一个数据包

下面我们通过测试来讨论 max_allowed_packet 的实际影响。

导入 SQL 文件受 max_allowed_packet 限制吗?

如果 SQL 文件中有单个 SQL 大小超过 max_allowed_packet ,会报错:

##导出时设置 mysqldump --net-buffer-length=16M,这样保证导出的sql文件中单个 multiple-row INSERT 大小为 16M
mysqldump -h127.0.0.1 -P13306 -uroot -proot --net-buffer-length=16M \
--set-gtid-purged=off sbtest sbtest1 > /data/backup/sbtest1.sql

##设置max_allowed_packet=1M

##导入报错
[root@localhost data]# mysql -h127.0.0.1 -P13306 -uroot -proot db3 < /data/backup/sbtest1.sql
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1153 (08S01) at line 41: Got a packet bigger than 'max_allowed_packet' bytes

导入解析后的 binlog 受 max_allowed_packet 限制吗?

row 格式的 binlog,单个SQL修改的数据产生的 binlog 如果超过 max_allowed_packet,也会报错。

在恢复数据到指定时间点的场景,解析后的binlog单个事务大小超过1G,并且这个事务只包含一个SQL,此时一定会触发 max_allowed_packet 的报错。但是恢复数据的任务又很重要,怎么办呢?可以将 binlog 改名成 relay log,用 sql 线程回放来绕过这个限制。

查询结果受 max_allowed_packet 限制吗?

查询结果中,只要单行数据不超过客户端设置的 max_allowed_packet 即可:

##插入220M大小的数据
[root@localhost tmp]# dd if=/dev/zero of=20m.img bs=1 count=0 seek=20M
记录了0+0 的读入
记录了0+0 的写出
0字节(0 B)已复制,0.000219914 秒,0.0 kB/秒
[root@localhost tmp]# ll -h 20m.img
-rw-r--r-- 1 root root 20M 6月   6 15:15 20m.img

mysql> create table t1(id int auto_increment primary key,a longblob);
Query OK, 0 rows affected (0.03 sec)

mysql> insert into t1 values(NULL,load_file('/tmp/20m.img'));
Query OK, 1 row affected (0.65 sec)

mysql> insert into t1 values(NULL,load_file('/tmp/20m.img'));
Query OK, 1 row affected (0.65 sec)

##mysql客户端默认 --max-allowed-packet=16M,读取失败
mysql> select * from t1;
ERROR 2020 (HY000): Got packet bigger than 'max_allowed_packet' bytes

##设置 mysql 客户端 --max-allowed-packet=22M,读取成功
[root@localhost ~]# mysql -h127.0.0.1 -P13306 -uroot -proot --max-allowed-packet=23068672 sbtest -e "select * from t1;" > /tmp/t1.txt

[root@localhost ~]# ll -h /tmp/t1.txt
-rw-r--r-- 1 root root 81M 6月   6 15:30 /tmp/t1.txt

load data 文件大小受 max_allowed_packet 限制吗?

load data 文件大小、单行大小都不受 max_allowed_packet 影响:

##将上一个测试中的数据导出,2行数据一共81M
mysql> select * int o outfile '/tmp/t1.csv' from t1;
Query OK, 2 rows affected (0.57 sec)

[root@localhost ~]# ll -h /tmp/t1.csv
-rw-r----- 1 mysql mysql 81M 6月   6 15:32 /tmp/t1.csv

##MySQL Server max_allowed_packet=16M
mysql> select @@max_allowed_packet;
+----------------------+
| @@max_allowed_packet |
+----------------------+
|             16777216 |
+----------------------+
1 row in set (0.00 sec)

##load data 成功,不受 max_allowed_packet 限制
mysql> load data infile '/tmp/t1.csv' into table t1;
Query OK, 2 rows affected (1.10 sec)
Records: 2 Deleted: 0 Skipped: 0 Warnings: 0

binlog 中超过 1G 的 SQL ,是如何突破 max_allowed_packet 复制到从库的?

从库 slave io 线程、slave sql 线程可以处理的最大数据包大小由参数 slave_max_allowed_packet 控制。这是限制 binlog event 大小,而不是单个 SQL 修改数据的大小。

主库 dump 线程会自动设置 max_allowed_packet为1G,不会依赖全局变量 max_allowed_packet。用来控制主库 DUMP 线程每次读取 event 的最大大小。

具体可以参考:mp.weixin.qq.com/s/EfNY_UwEthiu-DEBO7TrsA

另外超过 4G 的大事务,从库心跳会报错:https://opensource.actionsky.com/20201218-mysql/

作者:胡呈清,爱可生 DBA 团队成员,擅长故障分析、性能优化

来源:jianshu.com/u/a95ec11f67a8

收起阅读 »

慢 SQL 分析与优化

背景介绍从系统设计角度看,一个系统从设计搭建到数据逐步增长,SQL 执行效率可能会出现劣化,为继续支撑业务发展,我们需要对慢 SQL 进行分析和优化,严峻的情况下甚至需要对整个系统进行重构。所以我们往往需要在系统设计前对业务进行充分调研、遵守系统设计规范,在系...
继续阅读 »

背景介绍

从系统设计角度看,一个系统从设计搭建到数据逐步增长,SQL 执行效率可能会出现劣化,为继续支撑业务发展,我们需要对慢 SQL 进行分析和优化,严峻的情况下甚至需要对整个系统进行重构。所以我们往往需要在系统设计前对业务进行充分调研、遵守系统设计规范,在系统运行时定期结合当前业务发展情况进行系统瓶颈的分析。

从数据库角度看,每个 SQL 执行都需要消耗一定 I/O 资源,SQL 执行的快慢,决定了资源被占用时间的长短。假如有一条慢 SQL 占用了 30%的资源共计 1 分钟。那么在这 1 分钟时间内,其他 SQL 能够分配的资源总量就是 70%,如此循环,当资源分配完的时候,所有新的 SQL 执行将会排队等待。所以往往一条慢 SQL 会影响到整个业务。

本文仅讨论 MySQL-InnoDB 的情况。

优化方式

SQL 语句执行效率的主要因素

  • 数据量

    • SQL 执行后返回给客户端的数据量的大小;

    • 数据量越大需要扫描的 I/O 次数越多,数据库服务器的 IO 更容易成为瓶颈。

  • 取数据的方式

    • 数据在缓存中还是在磁盘上;

    • 是否能够通过全局索引快速寻址;

    • 是否结合谓词条件命中全局索引加速扫描。

  • 数据加工的方式

    • 排序、子查询、聚合、关联等,一般需要先把数据取到临时表中,再对数据进行加工;

    • 对于数据量比较多的计算,会消耗大量计算节点的 CPU 资源,让数据加工变得更加缓慢;

    • 是否选择了合适的 join 方式

优化思路

  • 减少数据扫描(减少磁盘访问)

    • 尽量在查询中加入一些可以提前过滤数据的谓词条件,比如按照时间过滤数据等,可以减少数据的扫描量,对查询更友好;

    • 在扫描大表数据时是否可以命中索引,减少回表代价,避免全表扫描。

  • 返回更少数据(减少网络传输或磁盘访问)

  • 减少交互次数(减少网络传输)

    • 将数据存放在更快的地方

    • 某条查询涉及到大表,无法进一步优化,如果返回的数据量不大且变化频率不高但访问频率很高,此时应该考虑将返回的数据放在应用端的缓存当中或者 Redis 这样的缓存当中,以提高存取速度。

  • 减少服务器 CPU 开销(减少 CPU 及内存开销)

  • 避免大事务操作

  • 利用更多资源(增加资源)

优化案例

数据分页优化

sele ct * from table_demo where type = ? limit ?,?;

优化方式一:偏移 id

lastId = 0 or min(id)
do {
sele ct * from table_demo where type = ? and id >{#lastId} limit ?;
lastId = max(id)
} while (isNotEmpty)

优化方式二:分段查询

该方式较方式一的优点在于可并行查询,每个分段查询互不依赖;较方式一的缺点在于较依赖数据的连续性,若数据过于分散,代价较高。

minId = min(id) maxId = max(id)
for(int i = minId; i<= maxId; i+=pageSize){
sele ct * from table_demo where type = ? and id between i and i+ pageSize;
}

优化 GROU P BY

提高 GROU P BY 语句的效率, 可以通过将不需要的记录在 GROU P BY 之前过滤掉.下面两个查询返回相同结果但第二个明显就快了许多。

低效:

sele ct job , avg(sal) from table_demo grou p by job having job = ‘manager'

高效:

sele ct job , avg(sal) from table_demo where job = ‘manager' grou p by job

范围查询

联合索引中如果有某个列存在范围(大于小于)查询,其右边的列是否还有意义?

expla in sele ct count(1) from statement where org_code='1012' and trade_date_time >= '2019-05-01 00:00:00' and trade_date_time<='2020-05-01 00:00:00'
expla in sele ct * from statement where org_code='1012' and trade_date_time >= '2019-05-01 00:00:00' and trade_date_time<='2020-05-01 00:00:00' limit 0, 100
expla in sele ct * from statement where org_code='1012' and trade_date_time >= '2019-05-01 00:00:00' and trade_date_time<='2020-05-01 00:00:00'
  • 使用单键索引 trade_date_time 的情况下

    • 从索引里找到所有 trade_date_time 在'2019-05-01' 到'2020-05-01' 区间的主键 id。假设有 100 万个。

    • 对这些 id 进行排序(为的是在下面一步回表操作中优化 I/O 操作,因为很多挨得近的主键可能一次磁盘 I/O 就都取到了)

    • 回表,查出 100 万行记录,然后逐个扫描,筛选出 org_code='1020'的行记录

  • 使用联合索引 trade_date_time, org_code -联合索引 trade_date_time, org_code 底层结构推导如下:


以查找 trade_date_time >='2019-05-01' and trade_date_time <='2020-05-01' and org_code='1020'为例:

  1. 在范围查找的时候,直接找到最大,最小的值,然后进行链表遍历,故仅能用到 trade_date_time 的索引,无法使用到 org_code 索引

  2. 基于 MySQL5.6+的索引下推特性,虽然 org_code 字段无法使用到索引树,但是可以用于过滤回表的主键 id 数。

小结:对于该 case, 索引效果[org_code,trade_date_time] > [trade_date_time, org_code]>[trade_date_time]。实际业务场景中,检索条件中 trade_date_time 基本上肯定会出现,但 org_code 却不一定,故索引的设计还需要结合实际业务需求。

优化 Order by

索引:

KEY `idx_account_trade_date_time` (`account_number`,`trade_date_time`),
KEY `idx_trade_date_times` (`trade_date_time`)
KEY `idx_createtime` (`create_time`),

慢 SQL:

SELE CT id,....,creator,modifier,create_time,update_time FROM statement
WHERE (account_number = 'XXX' AND create_time >= '2022-04-24 06:03:44' AND create_time <= '2022-04-24 08:03:44' AND dc_flag = 'C') ORDER BY trade_date_time DESC,id DESC LIMIT 0,1000;

优化前:SQL 执行超时被 kill 了

SELE CT id,....,creator,modifier,create_time,upda te_time FROM statement
WHERE (account_number = 'XXX' AND create_time >= '2022-04-24 06:03:44' AND create_time <= '2022-04-24 08:03:44' AND dc_flag = 'C') ORDER BY create_time DESC,id DESC LIMIT 0,1000;

优化后:执行总行数为:6 行,耗时 34ms。

MySQL使不使用索引与所查列无关,只与索引本身,where条件,order by 字段,grou p by 字段有关。索引的作用一个是查找,一个是排序。

业务拆分

sele ct * from order where status='S' and update_time < now-5min limit 500

拆分优化:

随着业务数据的增长 status='S'的数据基本占据数据的 90%以上,此时该条件无法走索引。我们可以结合业务特征,对数据获取按日期进行拆分。

date = now; minDate = now - 10 days
while(date > minDate) {
sele ct * from order where order_date={#date} and status='S' and upda te_time < now-5min limit 500
date = data + 1
}

数据库结构优化

  1. 范式优化:表的设计合理化(符合 3NF),比如消除冗余(节省空间);

  2. 反范式优化:比如适当加冗余等(减少 join)

  3. 拆分表:分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘 I/O,一个精心设置的分区可以将数据传输对磁盘 I/O 竞争均匀地分散开。对数据量大的表可采取此方法,可按月建表分区。

SQL 语句优化

SQL 检查状态及分数计算逻辑

  1. 尽量避免使用子查询

  2. 用 IN 来替换 OR

  3. 读取适当的记录 LIMIT M,N,而不要读多余的记录

  4. 禁止不必要的 Order By 排序

  5. 总和查询可以禁止排重用 union all

  6. 避免随机取记录

  7. 将多次插入换成批量 Insert 插入

  8. 只返回必要的列,用具体的字段列表代替 sele ct * 语句

  9. 区分 in 和 exists

  10. 优化 Grou p By 语句

  11. 尽量使用数字型字段

  12. 优化 Join 语句

大表优化

  • 分库分表(水平、垂直)

  • 读写分离

  • 数据定期归档

原理剖析

MySQL 逻辑架构图:


索引的优缺点

优点

  • 提高查询语句的执行效率,减少 IO 操作的次数

  • 创建唯一性索引,可以保证数据库表中每一行数据的唯一性

  • 加了索引的列会进行排序,在使用分组和排序子句进行查询时,可以显著减少查询中分组和排序的时间

缺点

  • 索引需要占物理空间

  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加

  • 当对表中的数据进行增删改查时,索引也要动态的维护,这样就降低了数据的更新效率

索引的数据结构

主键索引


普通索引


组合索引


索引页结构


索引页由七部分组成,其中 Infimum 和 Supremum 也属于记录,只不过是虚拟记录,这里为了与用户记录区分开,还是决定将两者拆开。


数据行格式:

MySQL 有 4 种存储格式:

  1. Compact

  2. Redundant (5.0 版本以前用,已废弃)

  3. Dynamic (MySQL5.7 默认格式)

  4. Compressed


Dynamic 行存储格式下,对于处理行溢出(当一个字段存储长度过大时,会发生行溢出)时,仅存放溢出页内存地址。

索引的设计原则

哪些情况适合建索引

  • 数据又数值有唯一性的限制

  • 频繁作为 where 条件的字段

  • 经常使用 grou p by 和 order by 的字段,既有 gro up by 又有 order by 的字段时,建议建联合索引

  • 经常作为 upda te 或 dele te 条件的字段

  • 经常需要 distinct 的字段

  • 多表连接时的字段建议创建索引,也有注意事项

    • 连接表数量最好不要超过 3 张,每增加一张表就相当于增加了一次嵌套循环,数量级增长会非常快

    • 对多表查询时的 where 条件创建索引

    • 对连接字段创建索引,并且数据类型保持一致

  • 在确定数据范围的情况下尽量使用数据类型较小的,因为索引会也会占用空间

  • 对字符串创建索引时建议使用字符串的前缀作为索引

  • 这样做的好处是:

    • 能节省索引的空间,

    • 虽然不能精确定位,但是能够定位到相同的前缀,然后通过主键查询完整的字符串,这样既能节省空间,又减少了字符串的比较时间,还能解决排序问题。

  • 区分度高(散列性高)的字段适合作为索引。

  • 在多个字段需要创建索引的情况下,联合索引优先于单值索引。使用最频繁的列作为索引的最左侧 。

哪些情况下不需要使用索引

  • 在 where 条件中用不到的字段不需要。

  • 数据量小的不需要建索引,比如数据少于 1000 条。

  • 由大量重复数据的列上不要建索引,比如性别字段中只有男和女时。

  • 避免在经常更新的表或字段中创建过多的索引。

  • 不建议主键使用无序的值作为索引,比如 uuid。

  • 不要定义冗余或重复的索引

  • 例如:已经创建了联合索引 key(id,name)后就不需要再单独建一个 key(id)的索引

索引优化之 MRR

例如有一张表 user,主键 id,普通字段 age,为 age 创建非聚集索引,有一条查询语句 sele ct* user from table where age > 18;(注意查询语句中的结果是*)

在 MySQL5.5 以及之前的版本中如何查询呢?先通过非聚集索引查询到 age>18 的第一条数据,获取到了主键 id;然后根据非聚集索引中的叶子节点存储的主键 id 去聚集索引中查询行数据;根据 age>18 的数据条数每次查询聚集索引,这个过程叫做回表。

上述的步骤有什么缺点呢?如何 age>18 的数据非常多,那么每次回表都需要经过 3 次 IO(假设 B+树的高度是 3),那么会导致查询效率过低。

在 MySQL5.6 时针对上述问题进行了优化,优化器先查询到 age>3 的所有数据的主键 id,对所有主键的 id 进行排序,排序的结果缓存到 read_rnd_buffer,然后通过排好序的主键在聚簇索引中进行查询。

如果两个主键的范围相近,在同一个数据页中就可以之间按照顺序获取,那么磁盘 io 的过程将会大大降低。这个优化的过程就叫做 Multi Range Read(MRR) 多返回查询。

索引下推

假设有索引(name, age), 执行 SQL: sele ct * from tuser where name like '张%' and age=10;


MySQL 5.6 以后, 存储引擎根据(name,age)联合索引,找到,由于联合索引中包含列,所以存储引擎直接在联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描。


索引下推使用条件

  • 只能用于rangerefeq_refref_or_null访问方法;

  • 只能用于InnoDBMyISAM存储引擎及其分区表;

  • 对存储引擎来说,索引下推只适用于二级索引(也叫辅助索引);

索引下推的目的是为了减少回表次数,也就是要减少 IO 操作。对于的聚簇索引来说,数据和索引是在一起的,不存在回表这一说。

  • 引用了子查询的条件不能下推;

  • 引用了存储函数的条件不能下推,因为存储引擎无法调用存储函数。

思考:

  1. MySQL 一张表到底能存多少数据?

  2. 为什么要控制单行数据大小?

  3. 优化案例 4 中优化前的 SQL 为什么走不到索引?

总结

抛开数据库硬件层面,数据库表设计、索引设计、业务代码逻辑、分库分表策略、数据归档策略都对 SQL 执行效率有影响,我们只有在整个设计、开发、运维阶段保持高度敏感、追求极致,才能让我们系统的可用性、伸缩性不会随着业务增长而劣化。

参考资料

  1. https://help.aliyun.com/document_detail/311122.html

  2. https://blog.csdn.net/qq_32099833/article/details/123150701

  3. https://www.cnblogs.com/tufujie/p/9413852.html

来源:字节跳动技术团队

收起阅读 »

B站:2021.07.13 我们是这样崩的

至暗时刻2021年7月13日22:52,SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。基于报警内容,SRE第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现...
继续阅读 »

至暗时刻

2021年7月13日22:52,SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。基于报警内容,SRE第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现问题,紧急发起语音会议,拉各团队相关人员开始紧急处理(为了方便理解,下述事故处理过程做了部分简化)。

初因定位

22:55 远程在家的相关同学登陆VPN后,无法登陆内网鉴权系统(B站内部系统有统一鉴权,需要先获取登录态后才可登陆其他内部系统),导致无法打开内部系统,无法及时查看监控、日志来定位问题。

22:57 在公司Oncall的SRE同学(无需VPN和再次登录内网鉴权系统)发现在线业务主机房七层SLB(基于OpenResty构建) CPU 100%,无法处理用户请求,其他基础设施反馈未出问题,此时已确认是接入层七层SLB故障,排除SLB以下的业务层问题。

23:07 远程在家的同学紧急联系负责VPN和内网鉴权系统的同学后,了解可通过绿色通道登录到内网系统。

23:17 相关同学通过绿色通道陆续登录到内网系统,开始协助处理问题,此时处理事故的核心同学(七层SLB、四层LB、CDN)全部到位。

故障止损

23:20 SLB运维分析发现在故障时流量有突发,怀疑SLB因流量过载不可用。因主机房SLB承载全部在线业务,先Reload SLB未恢复后尝试拒绝用户流量冷重启SLB,冷重启后CPU依然100%,未恢复。

23:22 从用户反馈来看,多活机房服务也不可用。SLB运维分析发现多活机房SLB请求大量超时,但CPU未过载,准备重启多活机房SLB先尝试止损。

23:23 此时内部群里同学反馈主站服务已恢复,观察多活机房SLB监控,请求超时数量大大降低,业务成功率恢复到50%以上。此时做了多活的业务核心功能基本恢复正常,如APP推荐、APP播放、评论&弹幕拉取、动态、追番、影视等。非多活服务暂未恢复。

23:25 - 23:55 未恢复的业务暂无其他立即有效的止损预案,此时尝试恢复主机房的SLB。

  • 我们通过Perf发现SLB CPU热点集中在Lua函数上,怀疑跟最近上线的Lua代码有关,开始尝试回滚最近上线的Lua代码。

  • 近期SLB配合安全同学上线了自研Lua版本的WAF,怀疑CPU热点跟此有关,尝试去掉WAF后重启SLB,SLB未恢复。

  • SLB两周前优化了Nginx在balance_by_lua阶段的重试逻辑,避免请求重试时请求到上一次的不可用节点,此处有一个最多10次的循环逻辑,怀疑此处有性能热点,尝试回滚后重启SLB,未恢复。

  • SLB一周前上线灰度了对 HTTP2 协议的支持,尝试去掉 H2 协议相关的配置并重启SLB,未恢复。

新建源站SLB

00:00 SLB运维尝试回滚相关配置依旧无法恢复SLB后,决定重建一组全新的SLB集群,让CDN把故障业务公网流量调度过来,通过流量隔离观察业务能否恢复。

00:20 SLB新集群初始化完成,开始配置四层LB和公网IP。

01:00 SLB新集群初始化和测试全部完成,CDN开始切量。SLB运维继续排查CPU 100%的问题,切量由业务SRE同学协助。

01:18 直播业务流量切换到SLB新集群,直播业务恢复正常。

01:40 主站、电商、漫画、支付等核心业务陆续切换到SLB新集群,业务恢复。

01:50 此时在线业务基本全部恢复。

恢复SLB

01:00 SLB新集群搭建完成后,在给业务切量止损的同时,SLB运维开始继续分析CPU 100%的原因。

01:10 - 01:27 使用Lua 程序分析工具跑出一份详细的火焰图数据并加以分析,发现 CPU 热点明显集中在对 lua-resty-balancer 模块的调用中,从 SLB 流量入口逻辑一直分析到底层模块调用,发现该模块内有多个函数可能存在热点。

01:28 - 01:38 选择一台SLB节点,在可能存在热点的函数内添加 debug 日志,并重启观察这些热点函数的执行结果。

01:39 - 01:58 在分析 debug 日志后,发现 lua-resty-balancer模块中的 _gcd 函数在某次执行后返回了一个预期外的值:nan,同时发现了触发诱因的条件:某个容器IP的weight=0。

01:59 - 02:06 怀疑是该 _gcd 函数触发了 jit 编译器的某个 bug,运行出错陷入死循环导致SLB CPU 100%,临时解决方案:全局关闭 jit 编译。

02:07 SLB运维修改SLB 集群的配置,关闭 jit 编译并分批重启进程,SLB CPU 全部恢复正常,可正常处理请求。同时保留了一份异常现场下的进程core文件,留作后续分析使用。

02:31 - 03:50 SLB运维修改其他SLB集群的配置,临时关闭 jit 编译,规避风险。

根因定位

11:40 在线下环境成功复现出该 bug,同时发现SLB 即使关闭 jit 编译也仍然存在该问题。此时我们也进一步定位到此问题发生的诱因:在服务的某种特殊发布模式中,会出现容器实例权重为0的情况。

12:30 经过内部讨论,我们认为该问题并未彻底解决,SLB 仍然存在极大风险,为了避免问题的再次产生,最终决定:平台禁止此发布模式;SLB 先忽略注册中心返回的权重,强制指定权重。

13:24 发布平台禁止此发布模式。

14:06 SLB 修改Lua代码忽略注册中心返回的权重。

14:30 SLB 在UAT环境发版升级,并多次验证节点权重符合预期,此问题不再产生。

15:00 - 20:00 生产所有 SLB 集群逐渐灰度并全量升级完成。

原因说明

背景

B站在19年9月份从Tengine迁移到了OpenResty,基于其丰富的Lua能力开发了一个服务发现模块,从我们自研的注册中心同步服务注册信息到Nginx共享内存中,SLB在请求转发时,通过Lua从共享内存中选择节点处理请求,用到了OpenResty的lua-resty-balancer模块。到发生故障时已稳定运行快两年时间。

在故障发生的前两个月,有业务提出想通过服务在注册中心的权重变更来实现SLB的动态调权,从而实现更精细的灰度能力。SLB团队评估了此需求后认为可以支持,开发完成后灰度上线。

诱因

  • 在某种发布模式中,应用的实例权重会短暂的调整为0,此时注册中心返回给SLB的权重是字符串类型的"0"。此发布模式只有生产环境会用到,同时使用的频率极低,在SLB前期灰度过程中未触发此问题。

  • SLB 在balance_by_lua阶段,会将共享内存中保存的服务IP、Port、Weight 作为参数传给lua-resty-balancer模块用于选择upstream server,在节点 weight = "0" 时,balancer 模块中的 _gcd 函数收到的入参 b 可能为 "0"。

根因


  • Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。

  • Lua在对一个数字字符串进行算术操作时,会尝试将这个数字字符串转成一个数字。

  • 在 Lua 语言中,如果执行数学运算 n % 0,则结果会变为 nan(Not A Number)。

  • _gcd函数对入参没有做类型校验,允许参数b传入:"0"。同时因为"0" != 0,所以此函数第一次执行后返回是 _gcd("0",nan)。如果传入的是int 0,则会触发[ if b == 0 ]分支逻辑判断,不会死循环。

  • _gcd("0",nan)函数再次执行时返回值是 _gcd(nan,nan),然后Nginx worker开始陷入死循环,进程 CPU 100%。

问题分析

\1. 为何故障刚发生时无法登陆内网后台?

事后复盘发现,用户在登录内网鉴权系统时,鉴权系统会跳转到多个域名下种登录的Cookie,其中一个域名是由故障的SLB代理的,受SLB故障影响当时此域名无法处理请求,导致用户登录失败。流程如下:


事后我们梳理了办公网系统的访问链路,跟用户链路隔离开,办公网链路不再依赖用户访问链路。

\2. 为何多活SLB在故障开始阶段也不可用?

多活SLB在故障时因CDN流量回源重试和用户重试,流量突增4倍以上,连接数突增100倍到1000W级别,导致这组SLB过载。后因流量下降和重启,逐渐恢复。此SLB集群日常晚高峰CPU使用率30%左右,剩余Buffer不足两倍。如果多活SLB容量充足,理论上可承载住突发流量, 多活业务可立即恢复正常。此处也可以看到,在发生机房级别故障时,多活是业务容灾止损最快的方案,这也是故障后我们重点投入治理的一个方向。


\3. 为何在回滚SLB变更无效后才选择新建源站切量,而不是并行?

我们的SLB团队规模较小,当时只有一位平台开发和一位组件运维。在出现故障时,虽有其他同学协助,但SLB组件的核心变更需要组件运维同学执行或review,所以无法并行。

\4. 为何新建源站切流耗时这么久?

我们的公网架构如下:


此处涉及三个团队:

  • SLB团队:选择SLB机器、SLB机器初始化、SLB配置初始化

  • 四层LB团队:SLB四层LB公网IP配置

  • CDN团队:CDN更新回源公网IP、CDN切量

SLB的预案中只演练过SLB机器初始化、配置初始化,但和四层LB公网IP配置、CDN之间的协作并没有做过全链路演练,元信息在平台之间也没有联动,比如四层LB的Real Server信息提供、公网运营商线路、CDN回源IP的更新等。所以一次完整的新建源站耗时非常久。在事故后这一块的联动和自动化也是我们的重点优化方向,目前一次新集群创建、初始化、四层LB公网IP配置已经能优化到5分钟以内。

\5. 后续根因定位后证明关闭jit编译并没有解决问题,那当晚故障的SLB是如何恢复的?

当晚已定位到诱因是某个容器IP的weight="0"。此应用在1:45时发布完成,weight="0"的诱因已消除。所以后续关闭jit虽然无效,但因为诱因消失,所以重启SLB后恢复正常。

如果当时诱因未消失,SLB关闭jit编译后未恢复,基于定位到的诱因信息:某个容器IP的weight=0,也能定位到此服务和其发布模式,快速定位根因。

优化改进

此事故不管是技术侧还是管理侧都有很多优化改进。此处我们只列举当时制定的技术侧核心优化改进方向。

1. 多活建设

在23:23时,做了多活的业务核心功能基本恢复正常,如APP推荐、APP播放、评论&弹幕拉取、动态、追番、影视等。故障时直播业务也做了多活,但当晚没及时恢复的原因是:直播移动端首页接口虽然实现了多活,但没配置多机房调度。导致在主机房SLB不可用时直播APP首页一直打不开,非常可惜。通过这次事故,我们发现了多活架构存在的一些严重问题:

多活基架能力不足

  • 机房与业务多活定位关系混乱。

  • CDN多机房流量调度不支持用户属性固定路由和分片。

  • 业务多活架构不支持写,写功能当时未恢复。

  • 部分存储组件多活同步和切换能力不足,无法实现多活。

业务多活元信息缺乏平台管理

  • 哪个业务做了多活?

  • 业务是什么类型的多活,同城双活还是异地单元化?

  • 业务哪些URL规则支持多活,目前多活流量调度策略是什么?

  • 上述信息当时只能用文档临时维护,没有平台统一管理和编排。

多活切量容灾能力薄弱

  • 多活切量依赖CDN同学执行,其他人员无权限,效率低。

  • 无切量管理平台,整个切量过程不可视。

  • 接入层、存储层切量分离,切量不可编排。

  • 无业务多活元信息,切量准确率和容灾效果差。

我们之前的多活切量经常是这么一个场景:业务A故障了,要切量到多活机房。SRE跟研发沟通后确认要切域名A+URL A,告知CDN运维。CDN运维切量后研发发现还有个URL没切,再重复一遍上面的流程,所以导致效率极低,容灾效果也很差。

所以我们多活建设的主要方向:

多活基架能力建设

  • 优化多活基础组件的支持能力,如数据层同步组件优化、接入层支持基于用户分片,让业务的多活接入成本更低。

  • 重新梳理各机房在多活架构下的定位,梳理Czone、Gzone、Rzone业务域。

  • 推动不支持多活的核心业务和已实现多活但架构不规范的业务改造优化。

多活管控能力提升

  • 统一管控所有多活业务的元信息、路由规则,联动其他平台,成为多活的元数据中心。

  • 支持多活接入层规则编排、数据层编排、预案编排、流量编排等,接入流程实现自动化和可视化。

  • 抽象多活切量能力,对接CDN、存储等组件,实现一键全链路切量,提升效率和准确率。

  • 支持多活切量时的前置能力预检,切量中风险巡检和核心指标的可观测。

2. SLB治理

架构治理

  • 故障前一个机房内一套SLB统一对外提供代理服务,导致故障域无法隔离。后续SLB需按业务部门拆分集群,核心业务部门独立SLB集群和公网IP。

  • 跟CDN团队、四层LB&网络团队一起讨论确定SLB集群和公网IP隔离的管理方案。

  • 明确SLB能力边界,非SLB必备能力,统一下沉到API Gateway,SLB组件和平台均不再支持,如动态权重的灰度能力。

运维能力

  • SLB管理平台实现Lua代码版本化管理,平台支持版本升级和快速回滚。

  • SLB节点的环境和配置初始化托管到平台,联动四层LB的API,在SLB平台上实现四层LB申请、公网IP申请、节点上线等操作,做到全流程初始化5分钟以内。

  • SLB作为核心服务中的核心,在目前没有弹性扩容的能力下,30%的使用率较高,需要扩容把CPU降低到15%左右。

  • 优化CDN回源超时时间,降低SLB在极端故障场景下连接数。同时对连接数做极限性能压测。

自研能力

  • 运维团队做项目有个弊端,开发完成自测没问题后就开始灰度上线,没有专业的测试团队介入。此组件太过核心,需要引入基础组件测试团队,对SLB输入参数做完整的异常测试。

  • 跟社区一起,Review使用到的OpenResty核心开源库源代码,消除其他风险。基于Lua已有特性和缺陷,提升我们Lua代码的鲁棒性,比如变量类型判断、强制转换等。

  • 招专业做LB的人。我们选择基于Lua开发是因为Lua简单易上手,社区有类似成功案例。团队并没有资深做Nginx组件开发的同学,也没有做C/C++开发的同学。

3. 故障演练

本次事故中,业务多活流量调度、新建源站速度、CDN切量速度&回源超时机制均不符合预期。所以后续要探索机房级别的故障演练方案:

  • 模拟CDN回源单机房故障,跟业务研发和测试一起,通过双端上的业务真实表现来验收多活业务的容灾效果,提前优化业务多活不符合预期的隐患。

  • 灰度特定用户流量到演练的CDN节点,在CDN节点模拟源站故障,观察CDN和源站的容灾效果。

  • 模拟单机房故障,通过多活管控平台,演练业务的多活切量止损预案。

4. 应急响应

B站一直没有NOC/技术支持团队,在出现紧急事故时,故障响应、故障通报、故障协同都是由负责故障处理的SRE同学来承担。如果是普通事故还好,如果是重大事故,信息同步根本来不及。所以事故的应急响应机制必须优化:

  • 优化故障响应制度,明确故障中故障指挥官、故障处理人的职责,分担故障处理人的压力。

  • 事故发生时,故障处理人第一时间找backup作为故障指挥官,负责故障通报和故障协同。在团队里强制执行,让大家养成习惯。

  • 建设易用的故障通告平台,负责故障摘要信息录入和故障中进展同步。

本次故障的诱因是某个服务使用了一种特殊的发布模式触发。我们的事件分析平台目前只提供了面向应用的事件查询能力,缺少面向用户、面向平台、面向组件的事件分析能力:

  • 跟监控团队协作,建设平台控制面事件上报能力,推动更多核心平台接入。

  • SLB建设面向底层引擎的数据面事件变更上报和查询能力,比如服务注册信息变更时某个应用的IP更新、weight变化事件可在平台查询。

  • 扩展事件查询分析能力,除面向应用外,建设面向不同用户、不同团队、不同平台的事件查询分析能力,协助快速定位故障诱因。

总结

此次事故发生时,B站挂了迅速登上全网热搜,作为技术人员,身上的压力可想而知。事故已经发生,我们能做的就是深刻反思,吸取教训,总结经验,砥砺前行。

此篇作为“713事故”系列之第一篇,向大家简要介绍了故障产生的诱因、根因、处理过程、优化改进。后续文章会详细介绍“713事故”后我们是如何执行优化落地的,敬请期待。

最后,想说一句:多活的高可用容灾架构确实生效了。

来源:哔哩哔哩技术

收起阅读 »

大家好啊,世界您好啊,请多关照哈

大家好啊,世界您好啊,请多关照哈,,,,,,,,,,,

大家好啊,世界您好啊,请多关照哈,,,,,,,,,,,

Logstash:如何在 Elasticsearch 中查找和删除重复文档

许多将数据驱动到 Elasticsearch 中的系统将利用 Elasticsearch 为新插入的文档自动生成的 id 值。 但是,如果数据源意外地将同一文档多次发送到Elasticsearch,并且如果将这种自动生成的 id 值用于 Elasticsear...
继续阅读 »

许多将数据驱动到 Elasticsearch 中的系统将利用 Elasticsearch 为新插入的文档自动生成的 id 值。 但是,如果数据源意外地将同一文档多次发送到Elasticsearch,并且如果将这种自动生成的 id 值用于 Elasticsearch 插入的每个文档,则该同一文档将使用不同的id值多次存储在 Elasticsearch 中。 如果发生这种情况,那么可能有必要找到并删除此类重复项。 因此,在此博客文章中,我们介绍如何通过

  • 使用 Logstash

  • 使用 Python 编写的自定义代码从 Elasticsearch 中检测和删除重复文档

示例文档结构

就本博客而言,我们假设 Elasticsearch 集群中的文档具有以下结构。 这对应于包含代表股票市场交易的文档的数据集。

{
   "_index": "stocks",
   "_type": "doc",
   "_id": "6fo3tmMB_ieLOlkwYclP",
   "_version": 1,
   "found": true,
   "_source": {
       "CAC": 1854.6,
       "host": "Alexanders-MBP",
       "SMI": 2061.7,
       "@timestamp": "2017-01-09T02:30:00.000Z",
       "FTSE": 2827.5,
       "DAX": 1527.06,
       "time": "1483929000",
       "message": "1483929000,1527.06,2061.7,1854.6,2827.5\r",
       "@version": "1"
   }
}

给定该示例文档结构,出于本博客的目的,我们任意假设如果多个文档的 [“CAC”,“FTSE”,“SMI”] 字段具有相同的值,则它们是彼此重复的。

使用 Logstash 对 Elasticsearch 文档进行重复数据删除

这种方法已经在之前的文章 “Logstash:处理重复的文档” 已经描述过了。Logstash 可用于检测和删除 Elasticsearch 索引中的重复文档。 在那个文章中,我们已经对这个方法进行了详述,也做了展示。我们也无妨做一个更进一步的描述。

在下面的示例中,我编写了一个简单的 Logstash 配置,该配置从 Elasticsearch 集群上的索引读取文档,然后使用指纹过滤器根据 ["CAC", "FTSE", "SMI"] 字段的哈希值为每个文档计算唯一的 _id 值,最后将每个文档写回到同一 Elasticsearch 集群上的新索引,这样重复的文档将被写入相同的 _id 并因此被消除。

此外,通过少量修改,相同的 Logstash 过滤器也可以应用于写入新创建的索引的将来文档,以确保几乎实时删除重复项。这可以通过更改以下示例中的输入部分以接受来自实时输入源的文档,而不是从现有索引中提取文档来实现。

请注意,使用自定义 id 值(即不是由 Elasticsearch 生成的 _id)将对索引操作的[写入性能产生一些影响](https://www.elastic.co/guide/en/elasticsearch/reference/master/tune-for-indexing-speed.html#use_auto_generated_ids)。

另外,值得注意的是,根据所使用的哈希算法,此方法理论上可能会导致 id 值的[哈希冲突数](https://en.wikipedia.org/wiki/Collision(computer_science))不为零,这在理论上可能导致两个不相同的文档映射到相同的_id,因此导致这些文档之一丢失。对于大多数实际情况,哈希冲突的可能性可能非常低。对不同哈希函数的详细分析不在本博客的讨论范围之内,但是应仔细考虑指纹过滤器中使用的哈希函数,因为它将影响提取性能和哈希冲突次数。

下面给出了使用指纹过滤器对现有索引进行重复数据删除的简单 Logstash 配置。

input {
# Read all documents from Elasticsearch
elasticsearch {
  hosts => "localhost"
  index => "stocks"
  query => '{ "sort": [ "_doc" ] }'
}
}
# This filter has been updated on February 18, 2019
filter {
  fingerprint {
      key => "1234ABCD"
      method => "SHA256"
      source => ["CAC", "FTSE", "SMI"]
      target => "[@metadata][generated_id]"
      concatenate_sources => true # <-- New line added since original post date
  }
}
output {
  stdout { codec => dots }
  elasticsearch {
      index => "stocks_after_fingerprint"
      document_id => "%{[@metadata][generated_id]}"
  }
}

用于 Elasticsearch 文档重复数据删除的自定义 Python 脚本

内存有效的方法

如果不使用 Logstash,则可以使用自定义 python 脚本有效地完成重复数据删除。 对于这种方法,我们计算定义为唯一标识文档的["CAC","FTSE","SMI"] 字段的哈希值 (Hash)。 然后,我们将此哈希用作 python 字典中的键,其中每个字典条目的关联值将是映射到同一哈希的文档 _id 的数组。

如果多个文档具有相同的哈希,则可以删除映射到相同哈希的重复文档。 另外,如果你担心哈希值冲突的可能性,则可以检查映射到同一散列的文档的内容,以查看文档是否确实相同,如果是,则可以消除重复项。

检测算法分析

对于 50GB 的索引,如果我们假设索引包含平均大小为 0.4 kB 的文档,则索引中将有1.25亿个文档。 在这种情况下,使用128位 md5 哈希将重复数据删除数据结构存储在内存中所需的内存量约为128位x 125百万= 2GB 内存,再加上160位_id将需要另外160位x 125百万= 2.5 GB 的内存。 因此,此算法将需要4.5GB 的 RAM 数量级,以将所有相关的数据结构保留在内存中。 如果可以应用下一节中讨论的方法,则可以大大减少内存占用。

算法增强

在本节中,我们对算法进行了增强,以减少内存使用以及连续删除新的重复文档。

如果你要存储时间序列数据,并且知道重复的文档只会在彼此之间的一小段时间内出现,那么您可以通过在文档的子集上重复执行该算法来改善该算法的内存占用量在索引中,每个子集对应一个不同的时间窗口。例如,如果您有多年的数据,则可以在datetime字段(在过滤器上下文中以获得最佳性能)上使用范围查询,一次仅一周查看一次数据集。这将要求算法执行52次(每周一次)-在这种情况下,这种方法将使最坏情况下的内存占用减少52倍。

在上面的示例中,你可能会担心没有检测到跨星期的重复文档。假设你知道重复的文档间隔不能超过2小时。然后,您需要确保算法的每次执行都包含与之前算法执行过的最后一组文档重叠2小时的文档。对于每周示例,因此,您需要查询170小时(1周+ 2小时)的时间序列文档,以确保不会丢失任何重复项。

如果你希望持续定期从索引中清除重复的文档,则可以对最近收到的文档执行此算法。与上述逻辑相同-确保分析中包括最近收到的文档以及与稍旧的文档的足够重叠,以确保不会无意中遗漏重复项。

用于检测重复文档的 Python 代码

以下代码演示了如何可以有效地评估文档以查看它们是否相同,然后根据需要将其删除。 但是,为了防止意外删除文档,在本示例中,我们实际上并未执行删除操作。 这样的功能的实现将是非常直接的。

可以在 github 上找到用于从 Elasticsearch 中删除文档重复数据的代码。

#!/usr/local/bin/python3
import hashlib
from elasticsearch import Elasticsearch
es = Elasticsearch(["localhost:9200"])
dict_of_duplicate_docs = {}
# The following line defines the fields that will be
# used to determine if a document is a duplicate
keys_to_include_in_hash = ["CAC", "FTSE", "SMI"]
# Process documents returned by the current search/scroll
def populate_dict_of_duplicate_docs(hits):
   for item in hits:
       combined_key = ""
       for mykey in keys_to_include_in_hash:
           combined_key += str(item['_source'][mykey])
       _id = item["_id"]
       hashval = hashlib.md5(combined_key.encode('utf-8')).digest()
       # If the hashval is new, then we will create a new key
       # in the dict_of_duplicate_docs, which will be
       # assigned a value of an empty array.
       # We then immediately push the _id onto the array.
       # If hashval already exists, then
       # we will just push the new _id onto the existing array
       dict_of_duplicate_docs.setdefault(hashval, []).append(_id)
# Loop over all documents in the index, and populate the
# dict_of_duplicate_docs data structure.
def scroll_over_all_docs():
   data = es.search(index="stocks", scroll='1m',  body={"query": {"match_all": {}}})
   # Get the scroll ID
   sid = data['_scroll_id']
   scroll_size = len(data['hits']['hits'])
   # Before scroll, process current batch of hits
   populate_dict_of_duplicate_docs(data['hits']['hits'])
   while scroll_size > 0:
       data = es.scroll(scroll_id=sid, scroll='2m')
       # Process current batch of hits
       populate_dict_of_duplicate_docs(data['hits']['hits'])
       # Update the scroll ID
       sid = data['_scroll_id']
       # Get the number of results that returned in the last scroll
       scroll_size = len(data['hits']['hits'])
def loop_over_hashes_and_remove_duplicates():
   # Search through the hash of doc values to see if any
   # duplicate hashes have been found
   for hashval, array_of_ids in dict_of_duplicate_docs.items():
     if len(array_of_ids) > 1:
       print("********** Duplicate docs hash=%s **********" % hashval)
       # Get the documents that have mapped to the current hashval
       matching_docs = es.mget(index="stocks", doc_type="doc", body={"ids": array_of_ids})
       for doc in matching_docs['docs']:
           # In this example, we just print the duplicate docs.
           # This code could be easily modified to delete duplicates
           # here instead of printing them
           print("doc=%s\n" % doc)
def main():
   scroll_over_all_docs()
   loop_over_hashes_and_remove_duplicates()
main()

结论

在此博客文章中,我们展示了两种在 Elasticsearch 中对文档进行重复数据删除的方法。 第一种方法使用 Logstash 删除重复的文档,第二种方法使用自定义的 Python 脚本查找和删除重复的文档。

来源:https://blog.csdn.net/UbuntuTouch/article/details/106643400

原文: How to Find and Remove Duplicate Documents in Elasticsearch | Elastic Blog

收起阅读 »

DevOps之【持续集成】

引言对于客户或者需求方来说,可以集成交付的软件才是有价值的。每个软件都有集成的过程,如果软件规模比较小,比如只有一个人而且没有外部依赖,那集成没什么问题。随着软件变得复杂,依赖变多,开发人员变多,那么早集成、常集成,就可以尽早暴露问题,做出相应的调整,防止在软...
继续阅读 »

引言

对于客户或者需求方来说,可以集成交付的软件才是有价值的。

每个软件都有集成的过程,如果软件规模比较小,比如只有一个人而且没有外部依赖,那集成没什么问题。随着软件变得复杂,依赖变多,开发人员变多,那么早集成、常集成,就可以尽早暴露问题,做出相应的调整,防止在软件后期才发现问题,从而导致软件失败。

定义

大师Martin Fowler对持续集成是这样定义的:

持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

优点

  • 降低软件风险 早集成,常集成,并且做了有效的测试,有利于尽早暴露问题和软件缺陷,了解软件的健康情况。假定越少的软件,对于维护和新业务开发都是有利的。 如果botslab每一个品类的设备都单独分支开发,不能及时集成进来,依赖问题、冲突问题、业务复用问题不能尽早解决,问题累积到一定程度解决成本会变大,业务发展是不会为代码重构让路的,那么软件质量的降低是必然的,很多项目都是这样失败的

  • 减少重复过程 软件集成的过程看起来简单,但是做起来难。软件的编译,测试,审查,部署,反馈,这些重复劳动是非常耗时,且没有意义的,自动化集成可以让开发解放出来,做一些用脑袋的事情。 如果出现疏漏,会给下游的参与者带来额外工作量和项目质量的误判。软件的输出质量是会影响项目计划的,所以持续集成很重要的一点就是自动化

  • 随时生成可以部署的软件 持续集成可以随时随地输出可以部署的软件,这一点对于需求方或客户是明显的好处。我们可以对客户说软件有多么好的架构,多么高质量的代码,但是对于客户来说,一个可以使用的软件才是他的实际资产。持续交付可以尽早的暴露产品问题和开发方向,客户才能给出有效的意见和开发重点。

  • 软件是透明的 持续集成会生成软件构建状态和品质信息,经常集成可以看到一些趋势,预测一些软件质量走向。

  • 团队信心 持续集成可以建立团队的信心,开发清楚的知道自己的代码产生了什么影响,测试对软件质量的预测稳定,产品或客户可以放心的需求了

步骤

  1. 统一的代码库

  2. 自动构建

  3. 自动测试

  4. 每个人每天都要向代码库主干提交代码

  5. 每次代码递交后都会在持续集成服务器上触发一次构建

  6. 保证快速构建

  7. 模拟生产环境的自动测试

  8. 每个人都可以很容易的获取最新可执行的应用程序

  9. 每个人都清楚正在发生的状况

  10. 自动化的部署

原则

  1. 所有的开发人员需要在本地机器上做本地构建,然后再提交的版本控制库中,从而确保他们的变更不会导致持续集成失败。

  2. 开发人员每天至少向版本控制库中提交一次代码。

  3. 开发人员每天至少需要从版本控制库中更新一次代码到本地机器。

  4. 需要有专门的集成服务器来执行集成构建,每天要执行多次构建。

  5. 每次构建都要100%通过。

  6. 每次构建都可以生成可发布的产品。

  7. 修复失败的构建是优先级最高的事情。



作者:QiShare
来源:juejin.cn/post/6986884632222384141

收起阅读 »

[PHP 安全] pcc —— PHP 安全配置检测工具

PHP
背景在 PHP 安全测试中最单调乏味的任务之一就是检查不安全的 PHP 配置项。作为一名 PHP 安全海报的继承者,我们创建了一个脚本用来帮助系统管理员如同安全专家一样尽可能快速且全面地评估 php.ini 和相关主题的状态。在下文中,该脚本被称作“PHP 安...
继续阅读 »

背景

在 PHP 安全测试中最单调乏味的任务之一就是检查不安全的 PHP 配置项。作为一名 PHP 安全海报的继承者,我们创建了一个脚本用来帮助系统管理员如同安全专家一样尽可能快速且全面地评估 php.ini 和相关主题的状态。在下文中,该脚本被称作“PHP 安全配置项检查器”,或者 pcc

https://github.com/sektioneins/pcc

概念

  • 一个便于分发的单文件
  • 有对每个安全相关的 ini 条目的简单测试
  • 包含一些其他测试 - 但不太复杂
  • 兼容 PHP >= 5.4, 或者 >= 5.0
  • 没有复杂/过度设计的代码,例如没有类/接口,测试框架,类库等等。它应该第一眼看上去是显而易见的-甚至对于新手-这个工具怎么使用能用来做什么。
  • 没有(或者少量的)依赖

使用 / 安装

  • CLI:简单调用 php phpconfigcheck.php。然后,添加参数 -a 以便更好的查看隐藏结果, -h 以 HTML 格式输出, -j 以 JSON 格式输出.
  • WEB: 复制这个脚本文件到你的服务器上的任意一个可访问目录,比如 root 目录。参见下面的“防护措施”。
    在非 CLI 模式下默认输出 HTML 格式。可以通过修改设置环境变量PCC_OUTPUT_TYPE=text 或者 PCC_OUTPUT_TYPE=json改变这个行为。
    一些测试用例默认是被隐藏的,特别是skipped、ok和 unknown/untested这些。要显示全部结果,可以用 phpconfigcheck.php?showall=1,但这并不适用于 JSON 输出,它默认返回全部结果。
    在 WEB 模式下控制输出格式用 phpconfigcheck.php?format=...format的值可以是 text, html 或者 json中的一个,例如: phpconfigcheck.php?format=textformat 参数优先于 PCC_OUTPUT_TYPE。

保障措施

大多数情况下,最好是自己来关注与安全性相关的问题比如PHP的配置。脚本已实现下列保障措施:

  • mtime检查:脚本在非CLI环境中只能工作两天。可以通过touch phpconfigcheck.php或者将脚本文件再次复制到你的服务器(例如通过SCP)来重新进行mtime检查。可以通过设置环境量: PCC_DISABLE_MTIME=1,比如在apache的.htaccess文件中设置SetEnv PCC_DISABLE_MTIME 1来禁用mtime检查。
  • 来源IP检查:默认情况下,只有localhost (127.0.0.1 和 ::1)才能访问这个脚本。其他主机可以通过在PCC_ALLOW_IP中添加IP地址或者通配符表达式的方式来访问脚本,比如在.htaccess文件中设置SetEnv PCC_ALLOW_IP 10.0.0.*。你还可以选择通过SSH端口转发访问您的web服务器, 比如 ssh -D 或者 ssh -L

下载

可以通过github下载第一个完整的开发版: https://github.com/sektioneins/pcc

如果有好的建议或者遇到bug请给我们提issue:

截图

HTML输出的列表是根据问题严重性排序的,通过颜色代码的形式列出了所有建议。列表顶部的状态行会显示问题的数量。


转载自: https://cloud.tencent.com/developer/article/1911011

收起阅读 »

Vue2全家桶之一:vue-cli

vue
vue.js有著名的全家桶系列,包含了vue-router,vuex, vue-resource,再加上构建工具vue-cli,就是一个完整的vue项目的核心构成。 1.安装vue-cli② 全局安装vue-cli,在cmd中输入命令:安装成功:打开C:\U...
继续阅读 »

都说Vue2简单上手容易,的确,看了官方文档确实觉得上手很快,除了ES6语法和webpack的配置让你感到陌生,重要的是思路的变换,以前用jq随便拿全局变量和修改dom的锤子不能用了,vue只用关心数据本身,不用再频繁繁琐的操作dom,注册事件、监听事件、取消事件。。。。(确实很烦)。vue的官方文档还是不错的,由浅到深,如果不使用构建工具确实用的很爽,但是这在实际项目应用中是不可能的,当用vue-cli构建一个工程的时候,发现官方文档还是不够用,需要熟练掌握es6,而vue的全家桶(vue-cli,vue-router,vue-resource,vuex)还是都要上的。

vue.js有著名的全家桶系列,包含了vue-router,vuex, vue-resource,再加上构建工具vue-cli,就是一个完整的vue项目的核心构成。

vue-cli这个构建工具大大降低了webpack的使用难度,支持热更新,有webpack-dev-server的支持,相当于启动了一个请求服务器,给你搭建了一个测试环境,只关注开发就OK。

1.安装vue-cli

使用npm(需要安装node环境)全局安装webpack,打开命令行工具输入:npm install webpack -g或者(npm install -g webpack),安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。

全局安装vue-cli,在cmd中输入命令:

npm install --global vue-cli

安装成功:



安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。

打开C:\Users\Andminster\AppData\Roaming\npm目录下可以看到:



打开node_modules也可以看到:

2.用vue-cli来构建项目

① 我首先在D盘新建一个文件夹(dxl_vue)作为项目存放地,然后使用命令行cd进入到项目目录输入:

vue init webpack baoge

baoge是自定义的项目名称,命令执行之后,会在当前目录生成一个以该名称命名的项目文件夹。
输入命令后,会跳出几个选项让你回答:

  • Project name (baoge): -----项目名称,直接回车,按照括号中默认名字(注意这里的名字不能有大写字母,如果有会报错Sorry, name can no longer contain capital letters),阮一峰老师博客为什么文件名要小写 ,可以参考一下。
  • Project description (A Vue.js project): ----项目描述,也可直接点击回车,使用默认名字
  • Author (): ----作者,输入dongxili
    接下来会让用户选择:
  • Runtime + Compiler: recommended for most users 运行加编译,既然已经说了推荐,就选它了
    Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specificHTML) are ONLY allowed in .vue files - render functions are required elsewhere 仅运行时,已经有推荐了就选择第一个了
  • Install vue-router? (Y/n) 是否安装vue-router,这是官方的路由,大多数情况下都使用,这里就输入“y”后回车即可。
  • Use ESLint to lint your code? (Y/n) 是否使用ESLint管理代码,ESLint是个代码风格管理工具,是用来统一代码风格的,一般项目中都会使用。
    接下来也是选择题Pick an ESLint preset (Use arrow keys) 选择一个ESLint预设,编写vue项目时的代码风格,直接y回车
  • Setup unit tests with Karma + Mocha? (Y/n) 是否安装单元测试,我选择安装y回车
  • Setup e2e tests with Nightwatch(Y/n)? 是否安装e2e测试 ,我选择安装y回车

回答完毕后上图就开始构建项目了。

配置完成后,可以看到目录下多出了一个项目文件夹baoge,然后cd进入这个文件夹:
安装依赖

npm install

 ( 如果安装速度太慢。可以安装淘宝镜像,打开命令行工具,输入:
npm install -g cnpm --registry=https://registry.npm.taobao.org
 然后使用cnpm来安装 )

npm install :安装所有的模块,如果是安装具体的哪个个模块,在install 后面输入模块的名字即可。而只输入install就会按照项目的根目录下的package.json文件中依赖的模块安装(这个文件里面是不允许有任何注释的),每个使用npm管理的项目都有这个文件,是npm操作的入口文件。因为是初始项目,还没有任何模块,所以我用npm install 安装所有的模块。安装完成后,目录中会多出来一个node_modules文件夹,这里放的就是所有依赖的模块。


然后现在,baoge文件夹里的目录是这样的:



解释下每个文件夹代表的意思(仔细看一下这张图):

image.png

3.启动项目

npm run dev


如果浏览器打开之后,没有加载出页面,有可能是本地的 8080 端口被占用,需要修改一下配置文件 config里的index.js

还有,如果本地调试项目时,建议将build 里的assetsPublicPath的路径前缀修改为 ' ./ '(开始是 ' / '),因为打包之后,外部引入 js 和 css 文件时,如果路径以 ' / ' 开头,在本地是无法找到对应文件的(服务器上没问题)。所以如果需要在本地打开打包后的文件,就得修改文件路径。
我的端口没有被占用,直接成功(服务启动成功后浏览器会默认打开一个“欢迎页面”):



注意:在进行vue页面调试时,一定要去谷歌商店下载一个vue-tool扩展程序。

4.vue-cli的webpack配置分析

  • package.json可以看到开发和生产环境的入口。

  • 可以看到dev中的设置,build/webpack.dev.conf.js,该文件是开发环境中webpack的配置入口。
  • 在webpack.dev.conf.js中出现webpack.base.conf.js,这个文件是开发环境和生产环境,甚至测试环境,这些环境的公共webpack配置。可以说,这个文件相当重要。
  • 还有config/index.js 、build/utils.js 、build/build.js等,具体请看这篇介绍:
    https://segmentfault.com/a/1190000008644830

5.打包上线

注意,自己的项目文件都需要放到 src 文件夹下。
在项目开发完成之后,可以输入 npm run build 来进行打包工作。

npm run build

另:

1.npm 开启了npm run dev以后怎么退出或关闭?
ctrl+c
2.--save-dev
自动把模块和版本号添加到模块配置文件package.json中的依赖里devdependencies部分
3. --save-dev 与 --save 的区别
--save 安装包信息将加入到dependencies(生产阶段的依赖)
--save-dev 安装包信息将加入到devDependencies(开发阶段的依赖),所以开发阶段一般使用它

打包完成后,会生成 dist 文件夹,如果已经修改了文件路径,可以直接打开本地文件查看。
项目上线时,只需要将 dist 文件夹放到服务器就行了。

转载自: https://cloud.tencent.com/developer/article/1896690

收起阅读 »

Java Exception

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,...
继续阅读 »

Java异常

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的 错误条件。当条件生成时,错误将引发异常。
Java异常类层次结构图:


图1 Java异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。 Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。
Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。

Exception 这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

4.处理异常机制

在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。

抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

捕获异常 :在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同。

由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。

对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。

能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时 系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。

任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Java的throw语句抛出异常。

从方法中抛出的任何异常都必须使用throws子句。

捕捉异常通过try-catch语句或者try-catch-finally语句实现。

总体来说,Java规定:对于可查异常必须捕捉、或者声明抛出。允许忽略不可查的RuntimeException和Error。

4.1 捕获异常:try、catch 和 finally

1.try-catch语句

在Java中,异常通过try-catch语句捕获。其一般语法形式为:

try {  
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
}
catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}

关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域。Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之 外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。

匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

例1 捕捉throw语句抛出的“除数为0”异常。

public class TestException {  
public static void main(String[] args) {
int a = 6;
int b = 0;
try { // try监控区域

if (b == 0) throw new ArithmeticException(); // 通过throw语句抛出异常
System.out.println("a/b的值是:" + a / b);
}
catch (ArithmeticException e) { // catch捕捉异常
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}

运行结果:程序出现异常,变量b不能为0。

程序正常结束。

例1 在try监控区域通过if语句进行判断,当“除数为0”的错误条件成立时引发ArithmeticException异常,创建 ArithmeticException异常对象,并由throw语句将异常抛给Java运行时系统,由系统寻找匹配的异常处理器catch并运行相应异 常处理代码,打印输出“程序出现异常,变量b不能为0。”try-catch语句结束,继续程序流程。

事实上,“除数为0”等ArithmeticException,是RuntimException的子类。而运行时异常将由运行时系统自动抛出,不需要使用throw语句。

例2 捕捉运行时系统自动抛出“除数为0”引发的ArithmeticException异常。

public static void main(String[] args) {  
int a = 6;
int b = 0;
try {
System.out.println("a/b的值是:" + a / b);
} catch (ArithmeticException e) {
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}

运行结果:程序出现异常,变量b不能为0。
程序正常结束。

例2 中的语句:

System.out.println("a/b的值是:" + a/b);

在运行中出现“除数为0”错误,引发ArithmeticException异常。运行时系统创建异常对象并抛出监控区域,转而匹配合适的异常处理器catch,并执行相应的异常处理代码。

由于检查运行时异常的代价远大于捕捉异常所带来的益处,运行时异常不可查。Java编译器允许忽略运行时异常,一个方法可以既不捕捉,也不声明抛出运行时异常。

例3 不捕捉、也不声明抛出运行时异常。

public class TestException {  
public static void main(String[] args) {
int a, b;
a = 6;
b = 0; // 除数b 的值为0
System.out.println(a / b);
}
} 复制

运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zero at Test.TestException.main(TestException.java:8)

例4 程序可能存在除数为0异常和数组下标越界异常。

public class TestException {  
public static void main(String[] args) {
int[] intArray = new int[3];
try {
for (int i = 0; i <= intArray.length; i++) {
intArray[i] = i;
System.out.println("intArray[" + i + "] = " + intArray[i]);
System.out.println("intArray[" + i + "]模 " + (i - 2) + "的值: "
+ intArray[i] % (i - 2));
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("intArray数组下标越界异常。");
} catch (ArithmeticException e) {
System.out.println("除数为0异常。");
}
System.out.println("程序正常结束。");
}
}

运行结果:

intArray[0] = 0

intArray[0]模 -2的值: 0

intArray[1] = 1

intArray[1]模 -1的值: 0

intArray[2] = 2

除数为0异常。

程序正常结束。

例4 程序可能会出现除数为0异常,还可能会出现数组下标越界异常。程序运行过程中ArithmeticException异常类型是先行匹配的,因此执行相匹配的catch语句:

catch (ArithmeticException e){  
System.out.println("除数为0异常。");
}

需要注意的是,一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。

Java通过异常类描述异常类型,异常类的层次结构如图1所示。对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子 句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。

RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在 最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。

收起阅读 »

使用 C# 开发 node.js 插件

项目需求最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接, 第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度, 虽然功能实...
继续阅读 »

项目需求

最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接,
第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度,
虽然功能实现了,但是也有些值得思考的点。

  • 纯 C 编程效率低,木有 trycatch 的语言调试难度也大 (磕磕绊绊的)
  • 编写好的 .node 扩展文件,放在 electron 主进程中运行会有一定的隐患稍有差错会导致软件闪退 (后来用子进程隔离运行)
  • 基于 N-API 方式去编写 Node.js 插件会显得有所束缚,木有那种随心所欲写 C 的那种“顺畅”;尤其是多线程部分

综上考虑,加上通讯功能又是调用 .dll 文件,索性转战 C#,对于 windows 来说再合适不过了;但是问题是 C# 咋编译到 Node.js 中?
答案是“编译不了”。
插件实现的功能只是收到命令后调用 .dll 去操作硬件,再时时能把结果返回即可。
基于这个需求我们用 C# 去调用 .dll 文件,然后再解决派发命令、实时获取结果的通讯问题就OK了,剩下的就都是好处啦

  • C# 编写难度低于 C,又是 windows 亲儿子,基于 .NET Framework 编译后的程序仅 19KB (C实现同样功能编出来的.node文件 565KB)
  • 基于 C# 的插件独立于 Node.js 运行环境,程序出了问题不会影响 electron 应用
  • 木有任何的编程束缚,~亲想咋写就咋写

通讯问题

说这个之前我们还忽略了一个问题,这个 C# 的程序(.exe文件)如果启动?
既然是一个程序(.exe文件),我们双击即可执行;既然双击即可执行,我们就可以用 child_process 模块提供的
spawn 去拉起程序(代替鼠标双击);

好!程序已经启动了,那么该到了如果通讯的环节了。
spawn 的执行就是开启了一个单独的进程,通讯问题也就是进程通讯问题。之前如果你用过 spawn 启动过 Node.js 程序(.js文件),那么你肯定知道通讯使用 send 方法即可;这个是 Node.js 内置的方式

我们启动的进程是 C# 程序,通讯问题只能我们自己来解决了;进程通讯的方式有好多这里不展开。对于前端(web)攻城狮来讲,我们最熟悉的莫过于 http 通讯方式了;就用它!

  • C# 程序端启动开启一个 http 服务等待 Node.js 端发送请求过来;根据参数决定要干啥
  • spawn 启动的应用(进程),会返回一个 ChildProcessWithoutNullStreams (这个我也不能很明确的理解);能够接收到标准的 stdio 输入/输出
    那我们就利用这点使用 ChildProcessWithoutNullStreams.stdout.on('data', chunk => console.log(chunk.toString())) 的方式就可以收到 C# 通过 stdioConsole.WriteLine() 发过来的数据;
    哇!好方便~
  • 可能有人会想到用双工的 web socket 实现通讯,很棒!实现方式确实有很多种,这里用 Console.WriteLine() 通过标准的 stdio 方式实现,算不算是一个开发成本不高的讨巧做法呢!

大致流程


  • 如果觉得这篇文章有难度,可以看简单版的哦 Node.js 利用 stdio 标准输入/输出实现与 C# 程序通讯

开发环境

  • C# 代码部分使用 Visual Studio 2017
  • test.js 代码部分使用 VsCode
using System; 
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text.RegularExpressions;
namespace NodeAddons {
class Program {
static TcpListener listener;
static int port = 8899;
static string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
static void Main(string[] args){
listener = new TcpListener(IPAddress.Any, port);
listener.Start();
// 启用服务器线程
new Thread(new ThreadStart(StartServer)).Start();
Console.WriteLine("Http server run at {0}.", port);
}
// Http 服务器
static void StartServer() {
while(true) {
// 这里会阻塞线程,直到接受到一个请求
Socket socket = listener.AcceptSocket();
// 将请求单独开一个线程处理;while(true)会回到等待下一个请求状态,周而复始
new Thread(new ParameterizedThreadStart(HandleRequest)).Start(socket);
} } // 处理一个请求 static void HandleRequest(object args) { Socket socket = (Socket)args; byte[] receive = new byte[1024]; socket.Receive(receive, receive.Length, SocketFlags.None); string httpRawTxt = Encoding.ASCII.GetString(receive); // 通过 stdio(Console.WriteLine) 实现与 node.js 通讯 // ## 开头、结尾,方便区分这个条输出是给 node.js 通讯用的 Console.WriteLine("##" + httpRawTxt + "##"); SendToBrowser(ref socket, now); } // 发送数据 static void SendToBrowser(ref Socket socket, string body) { string header = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + body.Length + "\r\n" + "Access-Control-Allow-Origin:*\r\n" // 支持跨域 + "\r\n"; // 响应头与响应体分界 byte[] data = Encoding.ASCII.GetBytes(header + body); if (socket.Connected) { int res = socket.Send(data, data.Length, SocketFlags.None); if (res == -1){ Console.WriteLine("Socket Error cannot Send Packet.");}else{ Console.WriteLine(">> [{0}]", now); } socket.Close();}}}}

Node.js 部分

const http = require('http'); 
const cp = require('child_process');
const path = require('path');
// const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons.exe'));
const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons_WithConsole.exe')); handel.stdout.on('data', chunk => { const str = chunk.toString();
// 约定 ##数据## 的字符串为通讯数据
let res = str.match(/##([\S\s]*)##/g);
if (!Array.isArray(res)) return;
res = res[0].match(/(?<=(\?))(.*)(?=(\sHTTP\/1.1))/);
if (!Array.isArray(res)) return;
console.log('[stdout queryString]', res[0]); });
function query(param, cb) {
http.get(`http://127.0.0.1:8899/?${(new URLSearchParams(param)).toString()}`, res => { res.on('data', chunk => { cb(chunk.toString()); });
}); }
query({ name: 'anan', age: 29, time: Date.now() }, httpRawTxt => { console.log('[http response]', httpRawTxt); });
// 监听 Ctrl + c process.on('SIGINT', () => { handel.kill(); process.exit(0); });

测试一下

  • 当然程序不会自己停下来哈,毕竟子进程的 http 服务一直在运行!
    $ node test.js [stdout queryString] name=anan&age=29&time=1595134635733 [http response] 2020-07-19 12:57:15


本文转载自 https://www.jianshu.com/p/9ac4f9ef9625

收起阅读 »

分享4个Linux中Node.js的进程管理器

Node.js进程管理器是一个有用的工具,可以确保Node.js进程或脚本连续(永久)运行,并使其能够在系统引导时自动启动。它允许您监视正在运行的服务,它有助于执行常见的系统管理任务(例如重新启动失败,停止,重新加载配置而无需停机,修改环境变量/设置,显示性能...
继续阅读 »

Node.js进程管理器是一个有用的工具,可以确保Node.js进程或脚本连续(永久)运行,并使其能够在系统引导时自动启动。

它允许您监视正在运行的服务,它有助于执行常见的系统管理任务(例如重新启动失败,停止,重新加载配置而无需停机,修改环境变量/设置,显示性能指标等等)。它还支持应用程序日志记录,群集和负载平衡,以及许多其他有用的流程管理功能。

另请参阅:2019年为开发人员提供的14个最佳NodeJS框架

包管理器尤其适用于在生产环境中部署Node.js应用程序。 在本文中,我们将回顾Linux系统中Node.js应用程序管理的四个进程管理器。

1. PM2

PM2是一个开源,高级,功能丰富,跨平台和最流行的Node.js生产级流程管理器,内置负载均衡器。它允许您列出,监视和处理所有已启动的Nodejs进程,并支持群集模式。


安装PM2以在Linux中运行Nodejs应用程序

它支持应用程序监视:提供一种监视应用程序资源(内存和CPU)使用情况的简单方法。它支持您的流程管理工作流,允许您通过流程文件配置和调整每个应用程序的行为(支持的格式包括Javascript,JSON和YAML)。

应用程序日志始终是生产环境中的关键,在这方面,PM2允许您轻松管理应用程序的日志。它提供了分别处理和显示日志的不同方式和格式。您可以实时显示日志,刷新日志,并在需要时重新加载日志。

重要的是,PM2支持启动脚本,您可以将其配置为在预期或意外的计算机重新启动时自动启动进程。它还支持在当前目录或其子目录中修改文件时自动重新启动应用程序。

此外,PM2还带有一个模块系统,允许用户为Nodejs进程管理创建自定义模块。例如,您可以为日志轮换模块或负载平衡创建模块等等。

最后但同样重要的是,如果您使用Docker容器,PM2允许容器集成,并提供允许您以编程方式使用它的API系统。

2. StrongLoop PM

StrongLoop PM也是一个开源的高级生产过程管理器,用于Node.js应用程序,内置负载平衡,就像PM2一样,它可以通过命令行或图形界面使用。


用于Nodejs的StrongLoop PM进程管理器

它支持应用程序监视(查看性能指标,如事件循环时间、CPU和内存消耗)、多主机部署、集群模式、零停机应用程序重启和升级、故障时自动进程重启以及日志聚合和管理。

此外,它附带Docker支持,允许您将性能指标导出到与状态兼容的服务器,并在第三方控制台(如DataDog、石墨、Splunk以及Syslog和原始日志文件)中查看。

3. Forever

Forever是一个开源,简单且可配置的命令行界面工具,可以连续(Forever)运行给定的脚本。它适用于运行Node.js应用程序和脚本的较小部署。您可以通过两种方式永久使用:通过命令行或将其嵌入代码中。


Forever运行脚本

它允许您管理(启动,列出,停止,停止所有,重新启动,重新启动所有等等。)Node.js进程,它支持监视文件更改,调试模式,应用程序日志,终止进程和退出信号自定义等等。此外,它还支持多种使用选项,您可以直接从命令行传递或将它们传递到JSON文件中。

4. Systemd - 服务和系统管理器

在Linux中,Systemd是一个守护程序,用于管理系统资源,例如进程和文件系统的其他组件。 systemd管理的任何资源都称为一个单元。有不同类型的单元,包括服务,设备,插座,安装,目标和许多其他单元。

Systemd通过称为单元文件的配置文件管理单元。因此,为了像任何其他系统服务一样管理Node.js服务器,您需要为它创建一个单元文件,在这种情况下它将是一个服务文件。

为Node.js服务器创建服务文件后,可以启动它,启用它以在系统引导时自动启动,检查其状态,重新启动(停止并再次启动它)或重新加载其配置,甚至像任何其他系统服务一样停止它。

摘要

Node.js包管理器是在生产环境中部署项目的有用工具。它使应用程序永远存在,并简化了如何控制它。在本文中,我们回顾了Node.js的四个包管理器。如果您有任何疑问或问题,请使用下面的反馈表与我们联系。

本文转载自: https://www.jianshu.com/p/ee49e600dd16
收起阅读 »

Node.js编写组件的几种方式

本文主要备忘为Node.js编写组件的三种实现:纯js实现、v8 API实现(同步&异步)、借助swig框架实现。简介(1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其...
继续阅读 »

Node.js编写组件的几种方式

本文主要备忘为Node.js编写组件的三种实现:纯js实现、v8 API实现(同步&异步)、借助swig框架实现。

关键字:Node.js、C++、v8、swig、异步、回调。

简介

首先介绍使用v8 API跟使用swig框架的不同:

(1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其它脚本语言。

(2)swig为第三方支持,一个强大的组件开发工具,支持为python、lua、js等多种常见脚本语言生成C++组件包装代码,swig使用者只需要编写C++代码和swig配置文件即可开发各种脚本语言的C++组件,不需要了解各种脚本语言的组件开发框架,缺点是不支持javascript的回调,文档和demo代码不完善,使用者不多。

二、纯JS实现Node.js组件

(1)到helloworld目录下执行npm init 初始化package.json,各种选项先不管,默认即可,更多package.json信息参见:https://docs.npmjs.com/files/package.json

(2)组件的实现index.js,例如:

module.exports.Hello = function(name) {
console.log('Hello ' + name);
}

(3)在外层目录执行:npm install ./helloworld,helloworld于是安装到了node_modules目录中。

(4)编写组件使用代码:

var m = require('helloworld');
m.Hello('zhangsan'); //输出: Hello zhangsan

三、 使用v8 API实现JS组件——同步模式

(1)编写binding.gyp, eg:

    { "target_name": "hello", "sources": [ "hello.cpp" ]
}
]
}

关于binding.gyp的更多信息参见:https://github.com/nodejs/node-gyp

(2)编写组件的实现hello.cpp,eg:

#include <node.h>

namespace cpphello { using v8::FunctionCallbackInfo; using v8::Isolate; using v8::Local; using v8::Object; using v8::String; using v8::Value; void Foo(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello World"));
} void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "foo", Foo);
}

NODE_MODULE(cpphello, Init)
}

(3)编译组件

node-gyp configure
node-gyp build

./build/Release/目录下会生成hello.node模块。

(4)编写测试js代码

const m = require('./build/Release/hello')
console.log(m.foo()); //输出 Hello World

(5)增加package.json 用于安装 eg:

{ "name": "hello", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "node test.js" }, "author": "", "license": "ISC" }

(5)安装组件到node_modules

进入到组件目录的上级目录,执行:npm install ./helloc //注:helloc为组件目录

会在当前目录下的node_modules目录下安装hello模块,测试代码这样子写:

var m = require('hello');
console.log(m.foo());

四、 使用v8 API实现JS组件——异步模式

上面三的demo描述的是同步组件,foo()是一个同步函数,也就是foo()函数的调用者需要等待foo()函数执行完才能往下走,当foo()函数是一个有IO耗时操作的函数时,异步的foo()函数可以减少阻塞等待,提高整体性能。

异步组件的实现只需要关注libuv的uv_queue_work API,组件实现时,除了主体代码hello.cpp和组件使用者代码,其它部分都与上面三的demo一致。

hello.cpp:

/* * Node.js cpp Addons demo: async call and call back.
* gcc 4.8.2
* author:cswuyg
* Date:2016.02.22
* */
#include <iostream> #include <node.h> #include <uv.h> #include <sstream> #include <unistd.h> #include <pthread.h>

namespace cpphello { using v8::FunctionCallbackInfo; using v8::Function; using v8::Isolate; using v8::Local; using v8::Object; using v8::Value; using v8::Exception; using v8::Persistent; using v8::HandleScope; using v8::Integer; using v8::String; // async task
struct MyTask{
uv_work_t work; int a{0}; int b{0}; int output{0};
unsigned long long work_tid{0};
unsigned long long main_tid{0};
Persistent<Function> callback;
}; // async function
void query_async(uv_work_t* work) {
MyTask* task = (MyTask*)work->data;
task->output = task->a + task->b;
task->work_tid = pthread_self();
usleep(1000 * 1000 * 1); // 1 second
} // async complete callback
void query_finish(uv_work_t* work, int status) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope handle_scope(isolate);
MyTask* task = (MyTask*)work->data; const unsigned int argc = 3;
std::stringstream stream;
stream << task->main_tid;
std::string main_tid_s{stream.str()};
stream.str("");
stream << task->work_tid;
std::string work_tid_s{stream.str()};

Local<Value> argv[argc] = {
Integer::New(isolate, task->output),
String::NewFromUtf8(isolate, main_tid_s.c_str()),
String::NewFromUtf8(isolate, work_tid_s.c_str())
};
Local<Function>::New(isolate, task->callback)->Call(isolate->GetCurrentContext()->Global(), argc, argv);
task->callback.Reset(); delete task;
} // async main
void async_foo(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope handle_scope(isolate); if (args.Length() != 3) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "arguments num : 3"))); return;
} if (!args[0]->IsNumber() || !args[1]->IsNumber() || !args[2]->IsFunction()) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "arguments error"))); return;
}
MyTask* my_task = new MyTask;
my_task->a = args[0]->ToInteger()->Value();
my_task->b = args[1]->ToInteger()->Value();
my_task->callback.Reset(isolate, Local<Function>::Cast(args[2]));
my_task->work.data = my_task;
my_task->main_tid = pthread_self();
uv_loop_t *loop = uv_default_loop();
uv_queue_work(loop, &my_task->work, query_async, query_finish);
} void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "foo", async_foo);
}

NODE_MODULE(cpphello, Init)
}

异步的思路很简单,实现一个工作函数、一个完成函数、一个承载数据跨线程传输的结构体,调用uv_queue_work即可。难点是对v8 数据结构、API的熟悉。

test.js

// test helloUV module
'use strict';
const m = require('helloUV')

m.foo(1, 2, (a, b, c)=>{
console.log('finish job:' + a);
console.log('main thread:' + b);
console.log('work thread:' + c);
}); /* output:
finish job:3
main thread:139660941432640
work thread:139660876334848 */

五、swig-javascript 实现Node.js组件

利用swig框架编写Node.js组件

(1)编写好组件的实现:.h和.cpp **

eg:

namespace a { class A{ public: int add(int a, int y);
}; int add(int x, int y);
}

(2)编写.i,用于生成swig的包装cpp文件*

eg:

/* File : IExport.i */

%module my_mod

%include "typemaps.i"

%include "std_string.i"

%include "std_vector.i"

%{

#include "export.h"

%}

%apply int *OUTPUT { int *result, int* xx};

%apply std::string *OUTPUT { std::string* result, std::string* yy };

%apply std::string &OUTPUT { std::string& result };

%include "export.h"

namespace std {

%template(vectori) vector<int>;

%template(vectorstr) vector<std::string>;

};

上面的%apply表示代码中的 int* result、int* xx、std::string* result、std::string* yy、std::string& result是输出描述,这是typemap,是一种替换。

C++导出函数返回值一般定义为void,函数参数中的指针参数,如果是返回值的(通过*.i文件中的OUTPUT指定),swig都会把他们处理为JS函数的返回值,如果有多个指针,则JS函数的返回值是list。

%template(vectori) vector 则表示为JS定义了一个类型vectori,这一般是C++函数用到vector 作为参数或者返回值,在编写js代码时,需要用到它。

swig支持的更多的stl类型参见:https://github.com/swig/swig/tree/master/Lib/javascript/v8

(3)编写binding.gyp,用于使用node-gyp编译

**(4)生成warpper cpp文件 **生成时注意v8版本信息,eg:swig -javascript -node -c++ -DV8_VERSION=0x040599 example.i

(5)编译&测试

难点在于stl类型、自定义类型的使用,这方面官方文档太少。

六、其它

在使用v8 API实现Node.js组件时,可以发现跟实现Lua组件的相似之处,Lua有状态机,Node有Isolate。

Node实现对象导出时,需要实现一个构造函数,并为它增加“成员函数”,最后把构造函数导出为类名。Lua实现对象导出时,也需要实现一个创建对象的工厂函数,也需要把“成员函数”们加到table中。最后把工厂函数导出。

Node的js脚本有new关键字,Lua没有,所以Lua对外只提供对象工厂用于创建对象,而Node可以提供对象工厂或者类封装。


转载自 https://cloud.tencent.com/developer/article/1929669

收起阅读 »

从零打造node.js版scf客户端

node.js是一个划时代的技术,它在原有的Web前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来Web开发经验的集大成者。转转公司在使用node.js方面,一起走在前沿。8月16日,转转公司的FE王澍老师,在镜泊湖会议室进行了一场主题为...
继续阅读 »

node.js是一个划时代的技术,它在原有的Web前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来Web开发经验的集大成者。转转公司在使用node.js方面,一起走在前沿。8月16日,转转公司的FE王澍老师,在镜泊湖会议室进行了一场主题为《nodejs全栈之路》的讲座。优秀的语言、平台、工具只有在优秀的程序员的手中才能显现出它的威力。一直听说转转公司在走精英化发展战略,所以学习下转转对node.js的使用方式,就显得很有必要。
对于大多数人使用node.js上的直观感受,就是模块、工具很齐全,要什么有什么。简单request一下模块,就可以开始写javasript代码了。然而出自58同城的转转,同样存在大量服务,使用着58自有的rpc框架scf。scf无论从设计还是实际效果,都算得上业内领先。只不过在跨平台的基础建设上,略显不足。从反编译的源码中,可以找到支持的平台有.net、java、c、php。非java平台的scf版本更新,也有些滞后。之前还听说肖指导管理的应用服务部,以“兼职”的方式开发过c++版客户端。而且也得到umcwrite等服务的实际运用。所以node.js解决好调用scf服务,是真正广泛应用的前提。这也正是我最关心的问题。
王澍老自己的演讲过程并没有介绍scf调用的解决方案,但在提问环节中,进行了解答。我能记住的内容是,目前的采用的方案是使用node-java模块,启动一个jvm进程,最终还是在node.js的项目中编写的java代码,性能尚可接受,但使用中内存占用很大;王澍老师也在尝试自己使用c++开发模块来弃用node-java。
这确实很让我很失望,我所理解的node.js应该是与性能有关的部分,几乎全部是c++编写的。之前肖指导要求发布公共服务,改写成使用scf提供的异步方式执行,借那次机会,我也阅读了一部分反编译的scf源码。感觉如果只是解决node.js调用scf的问题,不应该是个很难的事情。像管理平台、先知等外围功能,可以后期一点点加入。正巧我一直在质疑自己是不是基础差的问题,干脆写一个node.js版的scf客户端,来试试自己的水准。
结合自己之前对node.js的零散知识(其实现在也很零散)。对这次实践提出如下的一些设计要点:
1、序列化版本使用scfv3,虽然难度应该是最大的,但应该能在较长的时间内避免升级序列化版本的琐事。
2、使用管理平台读取配置,禁用scf.config类似的本地配置。想想之前许多部门,推进禁用线上服务直连的过程,就觉得很有必要(管理平台也用线下环境,线下调试根本不是阻碍)。
3、客户端支持全类型,之前偶尔听说了c++版客户端不支持枚举类型,使得有些服务只能调整接口。
4、c++使用libuv库,具备跨平台开发、调试能力。c++版客户端听说只支持linux平台。
5、只提供异步接口,这是当然的,不然node.js就别想用了。

现有的c++客户端,在3、4、5上与我的设想不符合,所以我决定亲自编写。
先是搜了本介绍libuv的pdf——《An Introduction to libuv》,看了几天,对libuv的使用方式有所了解,用上的只有tcp相关接口。(比起java,node.js的资料还是少,介绍的也少有深入的,像这样的底层类库,资料就更少了)
在58作为rd,如果不是做ios,是少有配macbook的员工的。所以我本次是在windows上编写的。不得不说node.js就是霸道,自动安装时,默认全部安装最新的版本。这样在windows平台上编译c++时,就要求visual studio不能低于2015。
网上搜索c++开发node.js模块,基本总是能找到那个addon的示例。可能是由于v8引擎的接口也有过变化,addon的示例使用的类型、接口也存在几种,终于还是试出了自己可以编译过的了。
首先在addon的基础上,写个运用libuv连接tcp的逻辑,一旦试通了,就可以一点点抄写反编译的scf客户端源码了。

在开发过程中,我的设计也进行了一些修改:
1、反序列化逻辑,通过tcp连接,交由一个java程序来执行(基于netty开发)。由于反序列化时,scf的二进制数据是没有足够的类型信息的。大体上,当读取到一个typeid时,如果本地没有对应的类型信息,完全不知道下一个字节是做什么用的。(我其实只希望得到一个类似多叉树的嵌套格式,也做不到。)如果非要使用c++来执行反序列化,也并非不可能。需要将scf反序列化用到的类型信息,整理成一种新的数据格式,存放于c++程序的内存中。为此需要开发一个输出类型配置数据的java离线工具,node.js模块需要开发:读取这个类型配置文件到内存,再将scf反序列化的逻辑使用c++抄一遍。综上来看,使用一个java的反序列化辅助进程,可以在性能几乎无损的情况下,极大的减少了开发量,同时避免了许多反序列化过程中的bug。这不正是一个极简的微服务嘛。
2、javascript入参对象中,需要自带scf序列化相关的类型信息,这样就能在全类型的支持scf对象了。当然我也设想过,有没有机会将序列化,也交由java辅助进程。那样就需要设计一个java对象在javascript中的表示形式,由java辅助进程,先转换为java对象,再序列化。再加上两次额外tcp传输。在没有减少工作量的情况下,浪费了不少性能。当然如果十分拒绝c++开发的话,倒是能因此少写些c++代码。

后续可以做的一些事情:
1、完善的重连、超时处理;
2、管理平台配置热更新;
3、管理平台数据上报;
4、先知;
5、加密、压缩(似乎和node.js的非计算密集场景有些冲突,而且公司的scf配置默认都是关闭这两个的。scf良好的用了这么些年,不开启这两个功能的功劳应该也不小)

当然已开发的内容中,也一定满是bug。等有人用了,我再考虑改bug的事。生产环境下的试错机会,才能让程序真正成长。

收起阅读 »

React-Native与原生模块间的几种通信方式

原理string-NSStringnumber - int/NSInteger/float/double/NSNumberboolean - BOOL/NSNumberarray - NSArrayobject - NSDictionary(NSString型...
继续阅读 »

每种语言都有自己的设计理念、语法、运行环境,这也导致了不同语言间相互交流通信时必须要有中介来翻译,如JAVA与C/C++通过JNI来交流、OC与C/C++需要在.mm文件混编、而JAVA/OC与Lua通信时需要通过C/C++语言来做中介。那么在React-Native中JSX是如何与底层模块进行通信的呢?这里主要以iOS系统来做说明。

原理

通信本质上是信息的交流,具体到计算机语言则是数据的流动。应用中数据在React-Native与原生模块间的流动与共享,完成了与用户的交互,达成了应用的目标。React-Native与OC间通信的数据只能是下面的几种类型(前为JS类型,后为OC类型):

  • string-NSString
  • number - int/NSInteger/float/double/NSNumber
  • boolean - BOOL/NSNumber
  • array - NSArray
  • object - NSDictionary(NSString型key, value可以为这里的其它类型)
  • func - RCTResponseSenderBlock

其它类型的数据需要通过一定的规则转换成这几种类型后(一般都会转换成JSON串)再通信.

React-Native本质是通过JavaScriptCore.framework实现JS代码与OC代码间的互动。因此下面说的几种方式在本质原理上都是相同的,不同的地方只是在于实现形式与方法的差别。

函数调用

在将原生模块封装并提供给React-Native使用时,可以通过RCT_EXPORT_METHOD()宏向React-Native侧定义其可以调用的接口函数,完成两模块间的通信。

//定义了startVPN接口,React-Native将VPN的具体参数通过该接口传入到原生模块,开启指定的VPN
RCT_EXPORT_METHOD(startVPN:(NSDictionary*)config)
{
LSShadowSocksDataMode* mode = [[LSShadowSocksDataMode alloc] initWithDictionary:config];
[self.manager startVPN:mode];
}

除了传入数据外,通过可以通过这种方式从原生侧获取数据。最容易想到的是通过返回值获取,可惜的是RCT_EXPORT_METHOD宏不支持返回值,不过其提供了另外一种实现返回值的方式:

RCT_EXPORT_METHOD(isOpen:(RCTResponseSenderBlock)callback)
{
BOOL open = [self.manager status];
callback(@[[NSNull null], @[@(open)]]);
}

通过回调函数的形式实现返回值的效果,达到了数据交换的目的。

属性共享

这种方式主要针对于UI控件来说的。React-Native中最基础的UI类型是RCTRootView,该类有一个初始化方法initWithBridge:moduleName:initialProperties:,第三个参数initialProperties表示的是UI控件的初始属性值,类型为NSDictionary,其最终会被同步到由第二个参数定义的React-Native类的props中,即完成了两个模块间的数据交流。

NSArray *imageList = @[@"http://foo.com/bar1.png",
@"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
import React, { Component } from 'react';
import {
AppRegistry,
View,
Image,
} from 'react-native';

class ImageBrowserApp extends Component {
renderImage(imgURI) {
return (
<Image source={{uri: imgURI}} />
);
}
render() {
return (
<View>
{this.props.images.map(this.renderImage)}
</View>
);
}
}

AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);

初始化接口只能在UI组件建立时使用,如果需要在UI组件的生命周期内通信呢,RCTRootView提供了appProperties这样一种机制:

NSArray *imageList = @[@"http://foo.com/bar3.png",
@"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList};

通知

OC中使用NSNotificationCenter向整个应用发送通知,所有对该通知感兴趣的对象都会获得该通知并执行相应的动作。React-Native中也提供有类似的机制:RCTEventEmitter。原生模块继承该类后,就可以向React-Native侧发送通知,而React-Native就能够接收到该通知,并处理一并传送过来的数据了。

-(void)vpnStatusChanged:(NSNotification*)notification
{
NEVPNStatus status = [self.manager status];
NSString* value = nil;
switch (status) {
case NEVPNStatusReasserting:
value = @"重新连接中";
break;
case NEVPNStatusConnecting:
value = @"连接中";
break;
case NEVPNStatusConnected:
value = @"已连接";
break;
case NEVPNStatusDisconnecting:
value = @"断开连接中";
break;
case NEVPNStatusDisconnected:
case NEVPNStatusInvalid:
value = @"末连接";
break;
default:
break;
}
if(value){
[self sendEventWithName:@"VpnStatus" body:@{@"status":value}];
}
}

这里将V**的状态通过通知发送到React-Native侧,由React-Native将V**的状态显示的UI界面上。


转载自 https://cloud.tencent.com/developer/article/1930848

收起阅读 »

Java VS .NET:Java与.NET的特点对比

为什么要写Java跟.NET对比?二、项目构建工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。平台工具ken.io的解释JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDKJavaJDKJRE(Java项目运行环境)...
继续阅读 »

一、前言

为什么要写Java跟.NET对比?

.NET出生之后就带着Java的影子。从模仿到创新,.NET平台也越来越成熟。他们不同的支持者也经常因为孰弱孰强的问题争论不休。但是本文并不是为了一分高下。而是针对Java平台跟.NET平台做一些对比。主要围绕项目构建、Web框架、项目部署展开讨论。相信经过这些讨论可以让Java/.NET工程师对Java平台、.NET平台有更好的了解。

二、项目构建

项目构建工具

工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。

1、表面上的工具

平台工具ken.io的解释

.NETVisual Studio微软官方IDE,它具备了开发.NET应用程序的几乎所有工具

JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDK

JavaMaven负责管理项目模板、打包(jar包等),依赖于JDK

JavaJDKJRE(Java项目运行环境),Java工具(编译器等)

.NET工程师要开展工作,安装Visual Studio(后面简称:VS)就可以进行开发了。但是Java开发,只安装IDE是不行的,就算某些IDE会自动安装JDK,甚至是Maven,但是这些还是需要自己配置,不然还可能会踩坑。从开发环境的配置来说,.NET工程师操作上确实简单一些,一直下一步,等待安装完成即可。Java工程师就先要了解下工具,以及各个工具的职责。然后逐一配置。

从这个点上来说,Java的入门曲线会稍陡一些,但是Java工程师也会比.NET工程师更早关注到项目构建的重要环节。

2、实际上的工具

职责.NET平台Java平台ken.io的解释

项目管理VSIDEA/Eclips.NET只有微软官方IDE,Java没有官方的IDE,没有VS好用,但是有多个选择

项目模板VS+MSBuildIDE+Maven.NET项目的模板是VS自带的,是直接符合MSBuild(编译器)标准的,项目由sln+csproj文件组织,Java平台编译器的标准是公开的,目前主流项目都是基于Maven模板来创建,项目由pom.xml文件组织。

编译&调试VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织,但是编译器并不是认识pom.xml,所以编译需要Maven的参与

Package管理NuGetMavenNuget是微软官方开源的VS插件,Maven是Apache下的开源项目。ken.io觉得Maven更灵活、强大。NuGet容易上手。

打包/发布VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织的,但是编译器并不是认识pom.xml,所以打包需要Maven的参与。IDE主要是提供图形化界面替代命令操作

从项目管理上说。VS这个IDE更好用一些,项目模板上,.NET项目模板由于有Visual Studio的存在,可以说简单易用而且丰富,Java平台的Maven模板灵活。

其实大部分差异都是编译器跟模板带来的差异。.NET平台的编译器是独立的,编译器MSBuild有一套标准, 而且Visual Studio提供了丰富好用的项目模板。

Java平台的编译器的编译配置是xml文档,由于Java官方没有项目模板,IDE只负责帮你组织项目,但是并没有模板,你可以将任意目录指定为SourceRoot(代码根目录),ResourceRoot(资源文件根目录:比如配置文件)也可以任意指定,编译的时候,IDE会将你的项目代码,以及编译器所需要的编译描述/配置xml文档告诉编译器该如何编译你的项目。确实非常灵活,但是也增加了项目管理的成本。包的管理也非常麻烦,还好有Maven结束了这个混沌的Java世界。

编码特点

—.NETJava

类的组织namespace:命名空间,name跟目录无关Package:name跟目录名一致

类.cs文件:类名跟文件名无关.java文件,类名跟文件名无关,但一个类文件只能定义一个public类

编译产出.dll,.exe文件.jar,.war文件

三、框架

.NET的Web框架基本上都是微软官方的,官方的框架也最为流行,而Java平台,除了官方提供的Servlet API(相当于.NET的System.Web)其他的基本都由Spring大家族统治了。本次我们主要对比目前Web开发最常用的MVC框架以及持久层框架

功能.NETJavaken.io的说明

Web核心ASP.NETServlet—

Web框架ASP.NET MVCSpring MVCASP.NET MVC是微软官方框架,Srping MVC框架隶属于Spring大家族,依赖于Spring

视图引擎RazorThymeleaf/FreeMarkerRazor是微软官方的视图引擎,非常好用,Spring MVC并没有视图引擎,但是有Thymeleaf,FreeMarker。ken.io更喜欢Razor的风格

持久层Entity FrameworkMyBatisEF是微软官方的持久层框架,易上手、开发效率高、但侵入性强。MyBatis配置灵活,无侵入性。各有利弊。

.NET平台的框架由于都是微软官方的,比较好组织,上手容易。Java平台的框架,灵活可配置。这也是Java平台一贯的风格。但是ken.io不得不吐槽的是,Spring MVC作为一个MVC框架,竟然没有自己的视图引擎,那MVC种的View去哪了?

可能是因为Java作为Web后端的主力平台,确实很少关注视图层,但是Spring MVC没有View层引擎,还是感觉不合适。Thymeleaf跟FreeMarker,ken.io更推荐FreeMarker。因为ken.io更喜欢FreeMaker的语法。可能是用惯了Razor的缘故。

四、项目部署

对于项目部署。.NET平台貌似没得选,只能选Windows+IIS,虽然有Mono,但毕竟不是支持所有的类库。而Java平台既可以选择Windows+Tomcat,也可以选择Linux+Tomcat。但是通常会选择Linux+Tomcat毕竟成本低。

职责.NETJava

操作系统Windows ServerWindows Server、Linux Server

Web服务器IISTomcat(Tomcat是目前最主流的,也有其他的Servlet容易例如:JBoss)

不过Java平台的特性,Java项目的部署会比.NET项目部署偏麻烦一些。

IIS图形化界面一直下一步,再调整下应用程序池的版本就行了。而Tomcat不论是在Windows,还是在Linux,都通过修改配置文件完成站点配置


转载自:https://cloud.tencent.com/developer/article/1926747

收起阅读 »

ASP.NET MVC 与 ASP.NET Web Form 的介绍与区别

是微软提供的以MVC模式为基础的ASP.NET Web应用程序开发框架。Model:领域模型 处理应用程序数据逻辑部分,获取数据,处理数据Controller:控制器 通过Model 读取处理数据,通过View 将结果返回。在 ASP.NET 框架下的一种基于...
继续阅读 »

1 ASP.NET MVC

是微软提供的以MVC模式为基础的ASP.NET Web应用程序开发框架。

MVC 模式分别为:

Model:领域模型 处理应用程序数据逻辑部分,获取数据,处理数据

View:视图 用于处理实际返回给用户的页面

Controller:控制器 通过Model 读取处理数据,通过View 将结果返回。

2 ASP.NET Webform

在 ASP.NET 框架下的一种基于事件模型的开发模式,有开发速度快,容易上手等特点。

3 两者的区别和各自优缺点

ASP.NET 作为微软的Web程序开发框架,MVC与Webform 是不同时期的开发模式,

在ASP.NET 运行处理原理 基本一致.

Webform 优点:可以基于事件模型开发,类似Winform中,所有请求使用ViewState和页面生命周期来维持控件状态,同时控件的开发,加快了开发速度,整体Webform的内部封装比较高。

Webform 缺点: 正是由于封装程度高,Webform非常难扩展,开发人员便利了解内部运行原理,不容易被测试。同时控件的ViewState 增加了网站服务器的传输量,一定程度上影响程序的效率。

MVC 优点 :易于扩展,易于单元测试,易于测试驱动开发。MVC中的一个路由的存在,可以做一些链接伪静态的处理。

总结: MVC 不是取代了Webform,两者适用于不同的开发环境下,都是简单三层中的表示层的开发框架,都是ASP.NET 框架下的开发模式。

1 页面处理流程:
MCV的页面处理流程依旧在ASP.NET原有上有扩展,MVC通过特定的IHttpModule和IHttpHandler 来处理请求,与Webform不同的,Webform中每个aspx页面都会有是一个IHttphandler实例。MVC中 Controller都比是IHttpHandler的子类实例,Action是在MvcHandler中通过MVC的工厂反射执行的,MvcHandler可以自定义。

2 上下文 请求对象: Context Session Request Response Cookie 基本一致

3 配置文件基本一致,但不通用

4 部分服务器控件并不是不可以在MVC中使用

5 在ASP.NET MVC中,包括Membership,healthMonitoring,httpModule,trace在内的内置和自定义的组件模块仍然是继续可用。

附图 :MVC 原理图和介绍

123123123.jpg

1 客户端发出请求给IIS(mvc中为集成模式),执行HttpRunTime的ProcessRequest方法

2 创建了一批MvcApplication对象,存放在应用程序池中,执行第一个MvcApplication对象实例中的 Application的Application_Start()方法、

RouteConfig.RegisterRountes(RouteTable.Routes)-->向路由规则集合注册一条默认的路由规则

3 调用Application对象实例的ProcessRequest方法 ,传入上下文对象HttpContext,开始执行19个管道事件

4 第七个管道事件:

UrlRouting过滤器:

1 获取当前Reuqest对象中的RawUrl:此时 /Home/Index

2 去扫描当前路由规则集合中的所,从上而下开始匹配,匹配成功了--{controller}/{action}/{id}这条路由规则,MVC底层就会根据路由规则解析出控制器名称

HomeController action:Index

3 调用DefaultControllerFactory反射创建控制器类的对象实例,存入RemapHandler中

4 将 控制器和action名称以字符串的形式存入RouteData中

5 第八个管道事件

1 判断当前RemapHandler是否为null, 不为null直接跳过执行后面的管道事件

2null继续创建页面类对象

6 第十一,十二个管道事件

1 获取上下文的RemapHandler中的控制器类的对象实例

2从RoutData中取出当前请求action名称

3 以反射的方式动态执行action方法

4 action返回类型分为:

4.1 如果是一个视图类型:调用具体的视图(.cshtml)编译成页面类,在调用页面类的Excute()方法,将所有的代码执行后写入到Response中

4.2如果是一个非视图类型,直接将结果写入到Response中即可
收起阅读 »

NodeJS 入门了解

1 NodeJS 是什么NodeJS 是 javascript 的一种运行环境,是对 Google V8 引擎进行的封装。是一个服务器端的 javascript 解释器;NodeJS 使用事件驱动,非阻塞 I/O 模型;什么是非阻塞 I/O 模型:阻塞:I/O...
继续阅读 »

1 NodeJS 是什么

  • NodeJS 是 javascript 的一种运行环境,是对 Google V8 引擎进行的封装。是一个服务器端的 javascript 解释器;
  • NodeJS 使用事件驱动,非阻塞 I/O 模型;

什么是非阻塞 I/O 模型:

  • 阻塞:I/O 时进程休眠等待 I/O 完成后再进行下一步;
  • 非阻塞 I/O :I/O 时函数立即返回,进程不等待 I/O 完成;

什么是事件驱动:
I/O 等异步操作结束后的通知。

2 NodeJS 和 npm 的关系

包含关系,NodeJS 中含有 npm,比如说你安装好 NodeJS,你打开 cmd 输入 npm -v 会发现出 npm 的版本号,说明 npm 已经安装好。

引用大神的总结:
其实 npm 是 NodeJS 的包管理器(package manager)。我们在 NodeJS 上开发时,会用到很多别人已经写好的 javascript 代码,如果每当我们需要别人的代码时,都根据名字搜索一下,下载源码,解压,再使用,会非常麻烦。

于是就出现了包管理器 npm。大家把自己写好的源码上传到 npm 官网上,如果要用某个或某些个,直接通过 npm 安装就可以了,不用管那个源码在哪里。并且如果我们要使用模块 A,而模块 A 又依赖模块 B,模块 B 又依赖模块 C 和 D,此时 npm 会根据依赖关系,把所有依赖的包都下载下来并且管理起来。试想如果这些工作全靠我们自己去完成会多么麻烦!

3 NodeJS 的安装

直接网上下载安装就可以了。环境配置,其实就是在 path,加入 NodeJS 的安装目录,这样就可以在控制台使用 NodeJS 的命令。验证,可以在控制台输入:node -vnpm -v


4 初始化 npm 环境

首先保证有 node 和 npm 环境,运行 node -vnpm -v 查看

进入项目目录,运行 npm init 按照步骤填写最终生成 package.json 文件,所有使用 npm 做依赖管理的项目,根目录下都会有一个这个文件,该文件描述了项目的基本信息以及一些第三方依赖项(插件)。详细的使用说明可查阅官网文档

5 安装插件

已知我们将使用 webpack 作为构建工具,那么就需要安装相应插件,运行 npm install webpack webpack-dev-server --save-dev 来安装两个插件。

又已知我们将使用 React ,也需要安装相应插件,运行 npm i react react-dom --save 来安装两个插件。其中 iinstall 的简写形式。

安装完成之后,查看 package.json 可看到多了 devDependenciesdependencies 两项,根目录也多了一个 node_modules 文件夹。

6 --save--save-dev 的区别

npm i 时使用 --save--save-dev,可分别将依赖(插件)记录到 package.json 中的 dependenciesdevDependencies 下面。

dependencies 下记录的是项目在运行时必须依赖的插件,常见的例如 reactjquery 等,即及时项目打包好了、上线了,这些也是需要用的,否则程序无法正常执行。

devDependencies 下记录的是项目在开发过程中使用的插件,例如这里我们开发过程中需要使用 webpack 打包,而我在工作中使用 fis3 打包,但是一旦项目打包发布、上线了之后,webpackfis3 就都没有用了,可卸磨杀驴。

延伸一下,我们的项目有 package.json,其他我们用的项目如 webpack 也有 package.json,见 ./node_modules/webpack/package.json,其中也有 devDependenciesdependencies。当我们使用 npm i webpack 时,./node_modules/webpack/package.json 中的dependencies 会被 npm 安装上,而 devDependencies 也没必要安装。

参考:http://www.imooc.com/article/14499

7 CommonJS

CommonJS 是 node 的模块管理规范

  • 每个文件都是一个模块,有自己的作用域;
  • 在模块内部 module 变量代表模块本身;
  • module.exports 属性代表模块对外接口;

require 规则

  • / 表示绝对路径,./ 表示相对路径;
  • 支持 js、json、node 扩展名,不写依次尝试;
  • 不写路径则认为是 build-in 模块或者各级 node_modules 内的第三方模块

require 特性

  • module 被加载的时候执行,加载后缓存;
  • 一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出

8 global

node 全局对象 global,相当于 web 的 window 对象。

  • CommonJS
  • Buffer、process、console
  • timer
收起阅读 »

nodeJS操纵数据库

下载nodeJS,安装另外一种安装我们node的方式 使用nvm这个软件来安装 node version manger,如果你想同时安装多个node版本 教程:http://www.jianshu.com/p/07c3456e875a2、使用上面装好的n...
继续阅读 »

Node.exe的安装

下载nodeJS,安装

检测是否安装成功 node -v

另外一种安装我们node的方式
使用nvm这个软件来安装
node version manger,如果你想同时安装多个node版本
教程:http://www.jianshu.com/p/07c3456e875a

步骤:
1、安装nvm这个软件: https://github.com/coreybutler/nvm-windows/releases

2、使用上面装好的nvm软件,安装我们需要的node版本了
指令:
nvm install 具体的版本号就行了
nvm uninstall 具体的版本号
nvm list 查看当前安装了哪些版本
nvm use 具体版本号,切换到某个版本

建议:
安装一个高一点的稳定的版本即可,因为软件都是向下兼容

系统环境变量及其作用

系统环境变量

每个系统都会提供一种叫做环境变量的东西,用来简化我们去
访问某一个应用程序可执行文件(.exe)的操作

我们配置了环境变量能做到什么事呢?
在我们终端的任何一个目录下,都可以访问,配置在系统
环境变量里面的可执行文件

如何将一个软件的可执行文件配置在我们的系统环境变量中?
步骤:
1、拷贝一个可执行文件所在的目录,比如:
node.exe所在的目录 C:\Program Files\nodejs

2、系统 > 高级系统设置 > 高级 > 环境变量 >
系统变量 > Path > 填写上你的目录

注意事项:
如果更改了系统的环境变量,就必须把终端重新启动

启动node.exe执行js代码

启动(相当于启动Apache服务器)

1、在我们的node的安装目录下,去双击我们node.exe

2、在终端输入 node即可 node.exe

退出我们的node.exe

1、在终端中输入.exit
2、连续按住两次 CTRL + C

怎么去执行js代码

1、直接在我们启动的node.exe中写代码(在开启的REPL环境中写代码执行)
缺点:
书写不方便,阅读起来也不方便
因为在我们的cmd中写的代码,是放在内存中的,
一旦我们退出了node.exe,原先写的代码都没有了

2、把我们写好的代码放在一个单独的js文件中去执行

在终端中输入 node.exe +执行的文件名称

注意:
1、我们js代码不是在终端中运行的,只是借助终端
去启动我们node.exe,并且最终将结果展现在终端里面而已

2、在运行时候,首先你的终端的目录得切换到你要
执行的文件的目录下面去,然后使用node 文件名称执行即可
我们nodejs的代码是在一个叫做REPL环境中,执行的

REPL

JS的执行

执行js在浏览器端,我是是要依靠浏览器(js的解析引擎)

在服务器端 nodejs开启的REPL环境

官网的解释:
参考:http://shouce.qdfuns.com/nodejs/repl.html

REPL就是当通过node.exe启动之后开辟的一块内存空间,
在这块内容空间里面就可以解释执行我们的js代码

例如:
在终端中输入了 node abc.js 做的事情就是,将abc.js中
写好的js的逻辑代码扔在启动好的node的内容空间中去运行,
我们把启动好的node的这块内存空间称之为REPL环境

模块化思想

为什么前端需要有模块化

1、解决全局变量名污染的问题
2、把相同功能的代码放在一个模块(一个js文件中)方便后期维护
3、便于复用

NodeJS中如何体现模块化

1、Node本身是基于CommonJS规范,
参考:http://javascript.ruanyifeng.com/nodejs/module.html#toc0

2、Node作者在设计这门语言的时候,就严格按照CommonJS
的规范,将它的API设计成模块化了,比如它将开启Web服务这
个功能所有代码都放入一个http模块中

3、Node本质来说就是将相同功能的代码放入到一个.js文件中管理

常用NodeJS中的模块

模块              作用
http 开启一个Web服务,给浏览器提供服务
url 给浏览器发送请求用,还可以传递参数(GET)
querystring 处理浏览器通过GET/POST发送过来的参数
path 查找文件的路径
fs 在服务器端读取文件用的

上面五大核心模块加上其它一些第三方的模块,就可以完成基本的数据库操作了

nodeJS核心模块及其操作

http

使用http模块开启web服务
步骤:
//1、导入我们需要的核心模块(NodeJS提供的模块我们称之为核心模块)
var http = require('http');

//2、利用获取到的核心模块的对象,创建一个server对象
var server = http.createServer();

//3、利用server对象监听浏览器的请求,并且处理(请求-处理-响应)
server.on('request',function(req,res){
res.end("welcome");
});

//4、开启web服务开始监听
server.listen(8080,'127.0.0.1',function(){
console.log('开启服务器成功');
});

url

1、导入url这个核心模块

2、调用url.parse(url字符串,true),如果是true的话代表把我们
的username=zhangsan&pwd=123 字符串解析成js对象

 // 使用url模块获取url中的一些相关信息
const url = require('url')
var testURL = http://127.0.0.1:8899/login?username=zhangsan&pwd=123
console.log(url.parse(testURL,true))//{username:zhangsan,pwd:123}

QueryString

作用:
将GET/POST传递过来的参数,进行解析
GET : ?username=zhangsan&pwd=123
POST : username=zhangsan&pwd=123

使用:
const querystring = require('querystring')

const paramsObj = querystring.parse(键值对的字符串)

GET&POST

相同点:
都是HTTP协议的方法
都能传递参数给服务器

不同点:
1、传参的方式不一样
GET 放在路径后面 ?开始,后面键值对
POST 放在请求体 键值对的方式

2、传参的限制不一样
GET 2048B
POST 2M

3、GET有缓存,POST没有

4、GET传参不安全,POST相对安全

建议:
如果只是单纯的获取数据,就用GET,因为GET有缓存效率高

如果是要向服务器提交数据,就用POST

fs&path

path

作用:获取路径

path.join(__dirname,'你要读取的文件夹下面的文件名称即可')

__dirname全局属性,代表当前文件所在的文件夹路径

path.join会自动判断文件的路径,并且给他加上`/`

fs

作用:读取服务器硬盘上面的某一个文件(操作文件)

fs.readFile : 异步读取服务器硬盘上面的某一个文件
fs:node去读取服务器硬盘中的文件(操作文件)

path:获取文件的路径

上面两个基本上配合起来用

自定义模块

CommonJS规范认为,一个.js文件就可以看成一个模块,如果我们想把模块中定义的变量,方法,对象给外面的js使用,就必须使用CommonJS提供module将我们需要给外面用的东西,导出去

注意点

在commonjs中导入模块用 require
在commonjs中在模块中导出 使用module.exports
如果是自定义模块,在导入自定义模块的时候,得把路径写完整
require导入的东西,就是别的文件modulu.exports导出的东西

Express 框架

基本概念

它是对HTTP封装,用来简化我们网络功能那一块

官网:http://www.expressjs.com.cn/ 官方解释:
基于 Node.js 平台,快速、开放、极简的 web 开发框架。

重点

1、如何去接收GET/POST传递过来的参数
2、如何通过Express进行分门别类的处理路由
3、静态资源的处理

使用

1、Hello World 案例

步骤:
1、导入包
2、创建一个app
3、请求处理响应
4、开启web服务,开始监听

2、获取GET/POST参数
GET参数:登录 http://127.0.0.1:3000/login?username=zhangsan&pwd=123

可以直接在我们的req.query中就可以获取了

POST参数:因为express没有直接提供获取POST参数的方法,需要借助一个第三方包 body-parser
参考: https://www.npmjs.com/package/body-parser

步骤:
1、npm install body-parser --save
2、导包
3、实现某些方法

最后通过req.body即可以获取到post提交过来的参数

路由处理

前端路由:
作用:当触发了某个超链接之后,根据路由的配置,决定
跳转到哪个页面,最终将这个页面呈现出来

后台的路由
作用:就是用来分门别类的出路用户发送过来的请求

    http://127.0.0.1:3000/login
http://127.0.0.1:3000/register

http://127.0.0.1:3000/getGoodsList
http://127.0.0.1:3000/getGoodsInfo

jd购物
男士:(专门创建一个man.js文件来实现男士区域商品的请求)
http://www.jd.com/man/xz
http://www.jd.com/man/ld
http://www.jd.com/man/px

女士:(专门创建一个girl.js文件来实现女士区域商品的请求)
http://www.jd.com/girl/xs
http://www.jd.com/girl/bag
http://www.jd.com/girl/kh

express中代码实现?

步骤:
1、先要创建一个单独的路由(js文件),来处理某一类
请求下面的所有用户请求,并且需要导出去
1.1 导入包 express
1.2 创建一个路由对象
const manRouter = express.Router()
1.3 在具体的路由js中处理属于我们该文件的路由
manRouter.get(xxx)
manRouter.post(xxx)
1.4 将上面创建的路由对象导出去,在入口文件中使用

2、在入口文件中,导入我们的路由文件,并且使用就可以了

//导入路由文件
const manRouter = require(path.join(__dirname,"man/manRouter.js"))
//在入口文件中使用
app.use('/man',manRouter)
```

## Express中静态资源的处理
Express希望对我们后台静态资源处理,达到简单的目的,
然后只希望我们程序员写一句话就能搞定

步骤:
1、在我们入口文件中设置静态资源的根目录
注意点:一定要在路由处理之前设置

app.use(express.static(path.join(__dirname,'statics')))
```

2、在我们的页面中,按照我们Express的规则来请求后台
静态资源数据
写link的href,script的src写的时候,除开静态资源根
路径之外,按照他在服务器上面的路径规则写

mongodb数据库

数据库

保存数据的仓库,数据库本质也是一个文件,只是说和普通的
文件不太一样,他有自己的存储规则,让我们保存数据和查询
数据更加方便

存储文件的介质

localStorage 文本文件
大型数据或是海量数据的时候必须要用到数据库

数据库的分类

客户端:
iOS/Android/前端
iOS/Android SQLite 在iOS/Android存储App的数据

服务端:
关系型数据库
部门---员工
mysql
sqlserver
oracle

非关系型数据库
JSON对象的形式来存储

MongoDB : 简单,你会js、JSON就能操作 Redis Memcached

数据库的作用

1、保存应用程序产生的数据(用户注册数据,用户的个人信息等等)
2、当应用程序需要数据的时候,提供给应用程序去展示

安装mongodb服务端

步骤:
1、安装mongodb服务端软件
2、设置mongodb的环境变量,重启终端验证 mongo -version
3、建立一个文件夹,用来存储mongodb数据库产生的数
据(建议放在C盘根目录 mongodb_datas)
4、启动
mongod --dbpath c:/mongodb_datas

启动服务端有几种方式

1、方式一,直接在cmd中输入 mongod --dbpath c:/mongodb_datas
32位: mongod --dbpath c:/mongodb_datas --journal --storageEngine=mmapv1

2、方式二,可以把mongod --dbpath c:/mongodb_datas做成一个批处理文件
32位: mongod --dbpath c:/mongodb_datas --journal --storageEngine=mmapv1

使用robomongo这个小机器人来操作我们的数据库中的数据

步骤:
1、连接到我们mongodb数据库服务端,并且连接成功之
后,服务端会给我们返回一个操作数据库的db对象

2、拿着上一步返回的db对象,对mongodb数据库中的数据进行操作了

连接成功之后,我们要来操作数据的话
1、创建一个数据库 (相当于在excel中创建空白工作簿)
2、创建集合 (相当于在excel创建工作表单)
数据的一个集合,把相关联的数据放在一个集合中
3、确立表头,插入数据、删除数据、修改数据、查询数据

MongoDB数据库中的概念

数据库 : 一个App中对应一个数据库

集合:相当于Excel中表单,一堆数据的集合,相关联的数据,
会放在一个集合中

文档:相当于excel中的每一行数据

一个数据中可以有多个集合(学生集合、食品集合)
一个集合可以有多条文档(多条数据)

在NodeJS中使用mongodb这个第三方包来操作我们mongodb数据库中的数据

参考: https://www.npmjs.com/package/mongodb

前提准备:
1、使用npm i mongodb --save来安装

正式集成:
1、导入包
2、拿到我们mongoClient对象
3、使用mongoClient连接到mongodb的服务端,返回操作数据库的db对象
4、通过db对象,拿到数据集合

db.collection('集合的名称')
5、调用集合的增,删,改,查的方法,来操作数据库中的数据
收起阅读 »

nodejs中的fs模块

对于文件处理的四个操作 增删改查 简称 curd(create-update-read-del)需要使用到的模块叫File System 简称fs 是nodejs 自带的一个库const fs=require('fs');1、使用 fs.mkdir...
继续阅读 »

对于文件处理的四个操作 增删改查 简称 curd(create-update-read-del)

需要使用到的模块叫File System 简称fs 是nodejs 自带的一个库

const fs=require('fs');

1、使用 fs.mkdir 创建目录css


2、fs.readdir 读取当前目录下的文件node02

同步读取 异步读取 同步读取时候 用try catch 处理报错 异步 直接用回调函数中的参数处理。


3、fs.rename 重命名html 下的index为base


4、 fs.unlink 删除文件t.txt


收起阅读 »