0%

盒模型(Box Model)

盒模型的主要区域:内容盒子(Content Box)、内边距盒子(Padding Box)、边框盒子(Border Box)、外边距盒子(Margin Box)。

Box-Model

Content Box:这是内容所在的区域。此内容可以控制其父级的大小,因此通常是最可变大小的区域。

Padding Box:内边距盒子围绕内容盒子,是由 padding 属性创建的空间。如果我们的盒子设置了溢出规则,比如 overflow:auto 或者 overflow:scroll,滚动条也会占用这个空间。

Border Box:边框盒子围绕着内边距盒子,其空间被 border 值占用。边框是盒子的边界。

Margin Box:最后一个区域,即外边距盒子,是盒子周围的空间,由盒子上的 margin 规则定义。轮廓 outline 和盒子阴影 box-shadow 等属性也占据了这个空间,因为它们被绘制在顶部,所以它们不会影响我们盒子的大小。你可以在盒子上有一个 200pxoutline-width,并且包括边框在内的所有内容都将是完全相同的大小。

阅读全文 »

随着时间的流逝,进程间通信技术已经发生了巨大的发展。出现了各种这样的技术来满足现代需求并提供更好和更有效的开发体验。让我们看一些最常用的进程间通信技术,并与 gprc 做个比较。

常规 RPC

RPC 是用于构建客户服务应用程序的流行的进程间通信技术。使用 RPC,客户端可以远程调用方法,就像调用本地方法一样。早期有流行的 RPC 实现,例如通用对象请求代理体系结构(CORBA)和 Java 远程方法调用(RMI),它们用于构建和连接服务或应用程序。但是,大多数此类常规 RPC 实现极其复杂,因为它们建立在诸如 TCP 之类的通信协议之上,这阻碍了不同程序间的交互,而且各自的规范很臃肿。

RPC(Remote Procedure Call)—远程过程调用,是指计算机程序使过程(子程序)在不同的地址空间(通常在共享网络的另一台计算机上)执行时,其编码就像是普通的(本地)过程调用,无需程序员为远程交互明确编码细节。

阅读全文 »

类加载

在 Java 类或接口编译为 Class 二进制字节码后,当我们用到该类或接口时,虚拟机就会将描述类的数据加载到方法区内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

类型的加载、连接和初始化过程发生在程序运行期间。

Java 虚拟机中类加载的全过程包含:加载、验证、准备、解析和初始化,这里我们只介绍加载一个阶段。

字节码验证器

加载

加载阶段,虚拟机规范要求虚拟机主要完成一下 3 件事情:

  1. 通过一个类的完全限定名称来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表类的 java.lang.Class 对象,作为方法区的这个类的各种数据的访问入口。

HotSpot 虚拟机是将生成的该 Class 对象放在了方法区中,而不是堆中。

阅读全文 »

P(rocess)M(anager)2 Runtime 是具有内置 Load Balancer 的 Node.js 应用程序的项目进程管理器。它允许永久保持应用程序的在线,无需停机即可重新加载它们,并可以完成常见的 Devops 任务。

安装使用

使用 npm 安装全局指令:

1
$ npm i pm2 -g

系统启动或重启时自动启动/禁用自启动:

1
$ pm2 startup/unstartup
阅读全文 »

什么是代理

代理是一种设计模式。当我们想要添加或修改现有类的某些功能时,我们创建并使用代理对象。通常,代理对象具有与原始代理对象相同的方法,并且在 Java 代理类中通常扩展原始类。代理的主要目的是控制对目标对象的访问,而不是增强目标对象的功能。

这样,代理类可以通过方便的方式实现许多功能:

  • 方法开始和结束时日志
  • 访问控制,过滤恶意请求
  • 本地执行远程服务
  • 缓存请求结果
  • 对参数执行额外检查
  • 模拟原始类的行为
  • 实现对昂贵资源的懒加载
  • 智能引用,可在没有客户端使用某个重量级对象时立即销毁该对象
  • etc…

在实际应用中,代理类不直接实现功能。遵循单一责任原则,代理类仅执行代理,并且实际行为在处理程序中实现。

与静态代理相比,动态代理需要在运行时进行 Java 反射的字节码生成。使用动态方法,无需创建代理类,这可以带来更多便利。

阅读全文 »

什么是缓存

术语缓存在计算机中无处不在。在应用程序设计的上下文中,它经常被用来描述应用程序开发人员利用单独的内存或低延迟的数据结构。缓存,用于临时存储或缓存信息的副本或引用,应用程序可能会在稍后的某个时间点重复使用,从而减轻重新访问或重新创建它的成本。

在 JCache 的上下文中,术语缓存描述了 Java 的技术开发人员使用缓存提供程序临时缓存 Java 对象。

什么时候引入缓存

引入缓存会提高系统复杂度,因为你要考虑缓存的失效、更新、一致性等问题。

冒着上述种种风险,仍能说服你引入缓存的理由,总结起来无外乎以下两种:

  • 为缓解 CPU 压力而做缓存:譬如把方法运行结果存储起来、把原本要实时计算的内容提前算好、把一些公用的数据进行复用,这可以节省 CPU 算力,顺带提升响应性能。
  • 为缓解 I/O 压力而做缓存:譬如把原本对网络、磁盘等较慢介质的读写访问变为对内存等较快介质的访问,将原本对单点部件(如数据库)的读写访问变为到可扩缩部件(如缓存中间件)的访问,顺带提升响应性能。

请注意,缓存虽然是典型以空间换时间来提升性能的手段,但它的出发点是缓解 CPU 和 I/O 资源在峰值流量下的压力,“顺带”而非“专门”地提升响应性能。这里的言外之意是如果可以通过增强 CPU、I/O 本身的性能(譬如扩展服务器的数量)来满足需要的话,那升级硬件往往是更好的解决方案,即使需要一些额外的投入成本,也通常要优于引入缓存后可能带来的风险。

缓存设计模式

Cache Aside

其中最简单、成本最低的 Cache Aside 模式是指:

  • 读数据时,先读缓存,缓存没有的话,再读数据源,然后将数据放入缓存,再响应请求。
  • 写数据时,先写数据源,然后失效(而不是更新)掉缓存。

读数据方面一般没什么出错的余地,但是写数据时,就有必要专门强调两点:一是先后顺序是先数据源后缓存。试想一下,如果采用先失效缓存后写数据源的顺序,那一定存在一段时间缓存已经删除完毕,但数据源还未修改完成,此时新的查询请求到来,缓存未能命中,就会直接流到真实数据源中。这样请求读到的数据依然是旧数据,随后又重新回填到缓存中。当数据源的修改完成后,结果就成了数据在数据源中是新的,在缓存中是老的,两者就会有不一致的情况。另一点是应当失效缓存,而不是去尝试更新缓存,这很容易理解,如果去更新缓存,更新过程中数据源又被其他请求再次修改的话,缓存又要面临处理多次赋值的复杂时序问题。所以直接失效缓存,等下次用到该数据时自动回填,期间无论数据源中的值被改了多少次都不会造成任何影响。

Cache Aside 模式依然是不能保证在一致性上绝对不出问题的,否则就无须设计出 Paxos 这样复杂的共识算法了。典型的出错场景是如果某个数据是从未被缓存过,请求会直接流到真实数据源中,如果数据源中的写操作发生在查询请求之后,结果回填到缓存之前,也会出现缓存中回填的内容与数据库的实际数据不一致的情况。但这种情况的概率是很低的,Cache Aside 模式仍然是以低成本更新缓存,并且获得相对可靠结果的解决方案。

Read/Write Through

Write Through 将数据同时写入高速缓存和相应的主内存位置。缓存的数据允许按需快速检索,而主存储器中的相同数据可确保在发生崩溃,电源故障或其他系统中断时不会丢失任何内容。

尽管直写可以最大程度地减少数据丢失的风险,但是每个写操作必须执行两次,并且这种冗余需要时间。活动的应用程序必须等待,直到将每个数据块都写入主内存和高速缓存中,然后才能开始下一个操作。因此,“数据保险”是以牺牲系统速度为代价的。

直写是无法容忍数据丢失的应用程序(例如银行和医疗设备控制)中首选的数据存储方法。

Write Behind Caching

回写在每次发生更改时,数据都会先写入高速缓存,但仅在指定的时间间隔或特定条件下,数据才会写入主存储中的相应位置。

JSR107 (JCache)

JCache 是 Java 的缓存 API。它由 JSR107 定义。它定义了供开发人员使用的标准 Java 缓存 API 和供实现者使用的标准 SPI(“服务提供者接口”)。

标准定义文档

阅读全文 »

问题

作为一个例子,我将使用在线图书订购应用程序的简化版本。在这样的应用程序中,我可能会创建一个如下所示的实体来代表采购订单:

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class PurchaseOrder {

@Id
private String id;
private String customerId;

@OneToMany(cascade = ALL, fetch = EAGER)
@JoinColumn(name = "purchase_order_id")
private List<PurchaseOrderItem> purchaseOrderItems = new ArrayList<>();
}
阅读全文 »

概述

fork/join 框架在 Java 7 中呈现。它提供了一些工具,通过尝试使用所有可用的处理器内核来帮助加速并行处理 - 这是通过分而治之的方法实现的——分治算法。

Java 8 的并行流背后使用的基础架构就是该框架。

在实践中,这意味着框架首先“fork(分叉)”,递归地将任务分解为较小的独立子任务,直到它们足够简单以便异步执行,也就是任务分发。

之后,“join(并入)”部分开始,其中所有子任务的结果递归地连接成单个结果,或者在返回 void 的任务的情况下,程序只是等待直到执行每个子任务,也就是任务细分执行,并等待返回。

为了提供有效的并行执行,fork/join 框架使用一个名为 ForkJoinPool 的线程池,它管理 ForkJoinWorkerThread 类型的工作线程。

阅读全文 »

流的创建

空流

如果创建空流,则应使用 Stream.empty() 方法。

通常情况下创建空流的目的是避免返回 null:

1
2
3
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

从数组或集合创建流

我们可以从数组使用 stream() 或者 of() 方法来创建流:

1
public static <T> Stream<T> stream(T[] array)

返回以指定数组作为源的顺序Stream。

1
static <T> Stream<T> of(T... values)
阅读全文 »

概述

在关系型数据库中我们没有直接的方法去映射类的继承到数据库表中。为了解决这个问题,JPA 标准提供了几种策略:

  • MappedSuperclass - 父类,不能是实体
  • Single Table - 来自具有共同祖先的不同类的实体被放置在单个表中
  • Joined Table - 每个类都有自己的表,查询子类实体需要连接表
  • Table-Per-Class - 类的所有的属性都在一张表中,所以不需要连接
    每种策略都会产生不同的数据库结构。
阅读全文 »