0%

最近在看与RPC相关的东西,在GitHub上看到一个使用Java实现的简单RPC框架,于是自己也想用Java实现一个简单的RPC,以便加深对于RPC框架的理解。本篇文章主要是记录如何使用ZooKeeper作为RPC框架的注册中心,实现服务的注册和发现。

什么是RPC?

RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样。正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

基于ZooKeeper实现的服务注册中心

如果对于dubbo这款国产RPC框架有一定的了解,就知道最开始它是基于ZooKeeper实现服务的注册和发现的。关于服务的注册和发现,主要是把服务名以及服务相关的服务器IP地址注册到注册中心,在使用服务的时候,只需要根据服务名,就可以得到所有服务地址IP,然后根据一定的负载均衡策略来选择IP地址。

下图是服务的注册和发现接口:

服务的注册

在ZooKeeper的节点概念中,Znode有四种类型,PERSISTENT(持久节点)、PERSISTENT_SEQUENTIAL(持久的连续节点)、EPHEMERAL(临时节点)、EPHEMERAL_SEQUENTIAL(临时的连续节点)。Znode的类型在创建时确定并且之后不能再修改。

关于服务的注册,其实就是把服务和IP注册到ZooKeeper的节点中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private ZkClient zkClient;

public ZooKeeperServiceRegistry(String zkAddress) {
// 创建 ZooKeeper 客户端
zkClient = new ZkClient(zkAddress, ZkConstants.SESSION_TIMEOUT, ZkConstants.CONNECTION_TIMEOUT);
log.info("connect zookeeper");
}

@Override
public void register(String serviceName, String serviceAddress) {

try {
String registryPath = ZkConstants.REGISTRY_PATH;
if (!zkClient.exists(registryPath)) {
zkClient.createPersistent(registryPath);
log.info("zk create registry node: {}", registryPath);
}
//创建服务节点(持久化)
String servicePath = registryPath + "/" + serviceName;
if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath);
log.info("zk create service node: {}", servicePath);
}
//创建 address 节点(临时)
String addressPath = servicePath + "/address-";
String addressNode = zkClient.createEphemeralSequential(addressPath, serviceAddress);
log.info("zk create ip address node: {}",addressNode);
} catch (Exception e) {
e.printStackTrace();
log.error("zk create error: {}", e.getMessage());
}

}

服务的发现

通过ZooKeeper的节点把服务名和IP写入其节点中,这样就实现了最简单的服务注册,下面来看下服务的发现。

服务的发现就是根据服务名来获取ZooKeeper节点中的IP地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private String zkAddress;
public ZooKeeperServiceDiscovery(String zkAddress) {
this.zkAddress = zkAddress;
}


@Override
public String discover(String serviceName) {
ZkClient zkClient = new ZkClient(zkAddress, ZkConstants.SESSION_TIMEOUT, ZkConstants.CONNECTION_TIMEOUT);
log.info("connect zookeeper....");
try {
String servicePath = ZkConstants.REGISTRY_PATH + "/" + serviceName;
if (!zkClient.exists(servicePath)) {
throw new SystemException(String.format("can not find any service node on path: %s", servicePath));
}
//获取路径的子节点
List<String> addressList = zkClient.getChildren(servicePath);
if (CollectionUtils.isEmpty(addressList)) {
throw new SystemException(String.format("can not find any address node on path: %s", servicePath));
}
//获取 address 节点
String address;
if (Objects.equals(addressList.size(), 1)) {
//如果只有一个地址,则获取地址
address = addressList.get(0);
log.info("get only address node: {}", address);
} else {
//如果有多个ip,随机选择一个
address = addressList.get(ThreadLocalRandom.current().nextInt(addressList.size()));
log.info("get random address node:{}", address);
}
//获取 address 节点的值
String addressPath = servicePath + "/" + address;
return zkClient.readData(addressPath);
} finally {
zkClient.close();
}
}

总结

通过测试样例,实现了最简单的服务注册和发现功能。

1
2
3
4
5
6
7
public static void main(String[] args) {
ServiceRegistry registry = new ZooKeeperServiceRegistry("127.0.0.1:2181");
registry.register("rpc", "192.168.20.49:8080");
ServiceDiscovery discovery = new ZooKeeperServiceDiscovery("127.0.0.1:2181");
String address = discovery.discover("rpc");
System.out.println("服务RPC的地址是:" + address);
}

输出:

1
服务RPC的地址是:192.168.20.49:8080

参考

如果有什么错误的地方,希望指出。

前几天有个面试,在面试最后的时候,面试官说问个比较偏僻的知识点,问了关于Java引用的。于是我就把四种引用说了下。然后又问,你知道引用队列嘛?然后我懵逼了,只能说我不知道。

关于Java中的引用,可以看上面的链接,引用主要用于GC中的。

引用队列 ReferenceQueue 是用来配合引用工作的,没有 ReferenceQueue 一样可以运行。创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列的队列末尾,这相当于是一种通知机制。当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。JVM提供了一个ReferenceHandler线程,将引用加入到注册的引用队列中。

关于引用队列,其类位于ref中,如图所示:

1
2
3
4
5
6
7
8
9
10
11
// 引用队列
ReferenceQueue<String> rq = newReferenceQueue<String>();
// 软引用
SoftReference<String> sr = newSoftReference<String>(new String("Soft"),rq);
// 弱引用
WeakReference<String> wr = newWeakReference<String>(new String("Weak"),rq);
// 幽灵引用
PhantomReference<String> pr = newPhantomReference<String>(new String("Phantom"),rq);

// 从引用队列中弹出一个对象引用
Reference<? extends String> ref = rq.poll();

ReferenceQueue 提供了三种方法来移除队列:

  • poll():用于移除并返回该队列中的下一个引用对象,如果队列为空,则返回null
  • remove():用于移除并返回该队列中的下一个引用对象,该方法会在队列返回可用引用对象之前一直阻塞
  • remove (long timeout):用于移除并返回队列中的下一个引用对象。该方法会在队列返回可用引用对象之前一直阻塞,或者在超出指定超时后结束。如果超出指定超时,则返回null。如果指定超时为0,意味着将无限期地等待。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ReferenceQueueDemo {

private static ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
private static int _1M = 1024 * 1024;

public static void main(String[] args) {

Object object = new Object();
Map<Object, Object> map = new HashMap<>();

Thread thread = new Thread(() -> {
try {
int cnt = 0;
WeakReference<byte[]> k;
while ((k = (WeakReference) referenceQueue.remove()) != null) {
System.out.println((cnt++) + "回收了:" + k);
}
} catch (InterruptedException e) {
//结束循环
}
});
thread.setDaemon(true);
thread.start();

for (int i = 0; i < 10000; i++) {
byte[] bytes = new byte[_1M];
WeakReference<byte[]> weakReference = new WeakReference<>(bytes, referenceQueue);
map.put(weakReference, object);
}
System.out.println("map.size->" + map.size());

}
}

因为map的key是WeakReference,所以在内存不足的时候,weakReference所指向的对象就会被GC,在对象被GC的同时,会把该对象的包装类即weakReference放入到ReferenceQueue里面。但是这个map的大小是10000。

在对于MySQL的优化,网上有很多小技巧,比如加索引。不过前几天在极客时间上买了门《MySQL实战45讲》。这篇文章主要是在学习过程中关于MySQL原理的一些笔记。

在学习如何优化的过程中,最好对于MySQL查询的过程有一定的理解,这样有利于如何进行优化。下面这张图片是MySQL的逻辑框架:

MySQL从图中可以看出,一般分为三部分:客户端、核心服务、存储引擎。客户端这个就不说了,主要是Java这些客户端;而关于存储引擎的,在之前整理的一篇文章有简绍——MySQL的存储引擎 —— InnoDB和MyIsAM。所以今天主要是讲解下关于核心服务。

MySQL优化原理

MySQL查询过程

mysql> select * from T where ID=10;

当我们输入上面这一条SQL查询语句的时候,发生了什么?

这里面主要涉及的是核心服务中的模块:连接器、查询缓存、分析器、优化器、执行器等,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

连接器

连接器主要的功能是跟客户端建立连接,获取权限,维持和管理连接。一般的命令:

mysql -h$ip -P$port -u$user -p

查询缓存

当MySQL获取到一个查询SQL的时候,会查看缓存,判断这条SQL是否已经执行过了。之前的执行结果会已key-value保存,key是查询SQL,value是查询结果。

之前看网上说,在MySQL8里面,已经去掉了缓存模块。从这里可以看出,在工作中,不建议使用MySQL的缓存。主要是当一个表更新数据的时候,这张表的缓存数据都会被清空,所以缓存适合哪种表内数据变化不大的表。

分析器

如果上面没有命中缓存,就开始真正的执行SQL了。在这一步,MySQL对SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。

Unknown column ‘k’ in ‘where clause 这种错误也是在这一层中出现的。

优化器

这一步需要对解析后的SQL进行优化,比如使用什么索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。

执行器

这一步主要先获取是否对该表有权限操作,然后就是从存储引擎中获取数据。

更新语句

更新SQL执行和上面的过程大致相同。分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用 ID 这个索引。然后,执行器负责具体执行,找到这一行,然后更新。

更新模块主要在涉及了两个日志模块:redo log(重做日志) 和 binlog(归档日志)。

redo log

当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log 里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

在InnoDB中,redo log 日志是固定大小的,比如分配4个文件,每个文件1G,这样就有4G。在写的时候,就会从头开始写,写到末尾又从新开始循环写。

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。

binlog

redo log 是位于InnoDB存储引擎中的,而binlog 则是位于server层的。

区别

  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server层实现的,所有引擎都可
    以使用。
  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给=2这一行的c字段加1”。
  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志

UPDATE 语句更新流程

  • 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  • 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  • 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  • 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  • 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

两阶段提交

为了保证两份日志中的逻辑的完整性和正确性,MySQL使用的是两阶段提交来保证数据的完整性。

参考

阅读

如果本文有错,希望在下面的留言区指正。

在开篇,先提出一个问题,在Java中,通过继承 Thread 或者实现 Runable 创建一个线程的时候,如何获取该线程的返回结果呢?

在并发编程中,使用非阻塞模式的时候,就是出现上面的问题。这个时候就需要用到这次所讲的内容了——Future。

Future 主要功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Future<V> {

//使用该方法来取消一个任务,若取消成功,则返回true,否则返回false
boolean cancel(boolean mayInterruptIfRunning);

//判断任务是否已经取消
boolean isCancelled();

//判断任务是否已经完成
boolean isDone();

//当任务结束返回一个结果,如果调用时,为返回结果,则阻塞
V get() throws InterruptedException, ExecutionException;

//在指定时间内获取指定结果,如果没有获取,则返回null
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}

Future 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class FutureTest {

public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Task task = new Task();
Future<Integer> result = executor.submit(task);
executor.shutdown();

try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}

System.out.println("主线程在执行任务");

try {
System.out.println("task运行结果" + result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}

System.out.println("所有任务执行完毕");
}

}
public class Task implements Callable<Integer> {

@Override
public Integer call() throws Exception {

System.out.println("子线程在进行计算");
Thread.sleep(3000);
int sum = 0;
for (int i = 0; i < 100; i++)
sum += i;
return sum;
}
}

Future 适用场景

在之前的一篇关于线程池中,详细介绍了Java的一些线程池知识点。那么对于使用线程池,除了管理线程资源外,如何能够实现节约时间呢?

比如现在一个请求中,给前端的返回结果,需要通过查询A、B、C,最后返回给前端,这三个查询分别耗时 10ms、20ms、10ms。如果正常的查询需要耗时40ms(忽略别的影响查询时间的因素)。但是如果把这三个查询交给线程池进行异步查询,那么,它的最终耗时是由最大耗时的那个查询决定的,这时就会发现查询变快了,只耗时20ms。

但是使用线程池的时候,这就和上面一开始的问题类似了,如何去获取线程池返回的结果。线程池代码参考

如果有错希望指出。本文是在看到一些关于JVM参数调优文章后的一些内容摘要。

堆大小设置

-Xms

设置JVM 初始内存,即JVM启动时分配的内存。此值可以设置与 -Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。

-Xmx

设置JVM 运行过程中分配的最大可用内存。

-Xss

-Xss128k:设置每个线程的堆栈大小。

-Xmn

设置年轻代大小。

打印 GC

  • -XX:+PrintGC:打印 GC 信息
  • -XX:+PrintGCDetails
  • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息

-XX:+AlwaysPreTouch

JAVA进程启动的时候,虽然我们可以为JVM指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配,这样会造成以下问题:

  • 第1次YGC之前Eden区分配对象的速度较慢;
  • YGC的时候,Young区的对象要晋升到Old区的时候,这个时候需要操作系统真正分配内存,这样就会加大YGC的停顿时间;

AlwaysPreTouch 参数可以优化上面的问题,但是它的副作用会导致 JVM 进程启动时间变长。

启动时间变长原因

在没有配置-XX:+AlwaysPreTouch参数即默认情况下,JVM参数-Xms申明的堆只是在虚拟内存中分配,而不是在物理内存中分配:它被以一种内部数据结构的形式记录,从而避免被其他进程使用这些内存。这些内存页直到被访问时,才会在物理内存中分配。当JVM需要内存的时候,操作系统将根据需要分配内存页。

配置-XX:+AlwaysPreTouch参数后,JVM将-Xms指定的堆内存中每个字节都写入’0’,这样的话,除了在虚拟内存中以内部数据结构保留之外,还会在物理内存中分配。并且由于touch这个行为是单线程的,因此它将会让JVM进程启动变慢。所以,要么选择减少接下来对每个缓存页的第一次访问时间,要么选择减少JVM进程启动时间,这是一种trade-off。

IO 最早指的是文件的Input/Output,之后 IO 也包括网络 IO。

网络 IO

网络 IO 编程只要一个线程过来就需要创建一个线程,这样会导致资源不够用,很浪费资源。

问题

  • 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起
  • 线程切换效率低下:单机 CPU 核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
  • 除了以上两个问题,IO 编程中,我们看到数据读写是以字节流为单位。

NIO

NIO 模型中,它把这么多 while 死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个 while 死循环就能监测1w个连接是否有数据可读的。这就是 NIO 模型中 selector 的作用,一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据。

NIO 主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。Selector 用于监听多个通道的时间。因此,单个线程可以监听多个数据通道。

Channel

Buffer

Buffer,缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、网络读取数据的通道,但是读取或写入的数据必须经由 Buffer。

Selector

Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。

IO 和 NIO 的区别

读写方式

IO 读写是面向的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,你需要自己缓存数据。

Java NIO 的读写是面向 Buffer 的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

阻塞与非阻塞IO

Java IO 的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

选择器(Selectors)

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

网络 IO 模型

  • 同步
  • 异步
  • 阻塞式 IO
  • 非阻塞式 IO

同步与异步

同步和异步关注的是消息通信机制(谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用主动等待这个调用*的结果

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当
用者不会立刻得到结果。而是在
调用发出后,被调用者*通过状知来通知调用者,或通过回调函数处理这个调用

阻塞与非阻塞

塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

Netty

Nety是一款高性能、异步事件驱动的 NIO 框架,基于 Java NIO 提供的 API 实现。它提供了对 TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有IO操作都是异步非阻塞的。

参考

如果有什么错误的地方,希望指出。本文是本人对 Docker 网络通信学习的笔记。

Docker容器和服务如此强大的原因之一是您可以将它们连接在一起,或者将它们连接到非Docker工作负载。Docker容器和服务甚至不需要知道它们部署在Docker上,或者它们的对等体是否也是Docker工作负载。无论您的Docker主机是运行Linux,Windows还是两者兼而有之,您都可以使用Docker以与平台无关的方式管理它们。(PS:官网翻译)

网络驱动

  • bridge:默认网络驱动程序。如果未指定驱动程序,则这是您要创建的网络类型。当您的应用程序在需要通信的独立容器中运行时,通常会使用桥接网络。
  • host:对于独立容器,删除容器和Docker主机之间的网络隔离,并直接使用主机的网络。host 仅适用于Docker 17.06及更高版本上的群集服务。
  • none:对于此容器,禁用所有网络。通常与自定义网络驱动程序一起使用。none不适用于群组服务
  • overlay:Macvlan网络允许您为容器分配MAC地址,使其显示为网络上的物理设备。Docker守护程序通过其MAC地址将流量路由到容器。macvlan 在处理期望直接连接到物理网络的传统应用程序时,使用驱动程序有时是最佳选择,而不是通过Docker主机的网络堆栈进行路由。
  • Network plugins:您可以使用Docker安装和使用第三方网络插件。

bridge

在网络方面,桥接网络是链路层设备,它在网络段之间转发流量。网桥可以是硬件设备或在主机内核中运行的软件设备。

Docker 的桥接网络使用软件桥接器,改桥接器允许连接到同一网桥的容器进行通信,同时提供与未连接到该网桥的容器的隔离。

桥接网络适用于在同一个 Docker守护程序主机上运行的容器。对于在不同Docker守护程序主机上运行的容器之间的通信,您可以在操作系统级别管理路由,也可以使用覆盖网络。

启动Docker时,会自动创建默认桥接网络(也称为bridge),并且除非另行指定,否则新启动的容器将连接到该网络。您还可以创建用户定义的自定义网桥。用户定义的网桥优于默认bridge 网络。

在Docker进行启动时,会在主机上创建一个名为 docker0 的虚拟网桥,在主机上启动的 Docker 容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器通过交换机连载了一个二层网络中。

host

如果host对容器使用网络驱动程序,则该容器的网络堆栈不会与Docker主机隔离。例如,如果您运行绑定到端口80 host的容器并使用网络,则容器的应用程序将在主机IP地址的端口80上可用。

主机网络驱动程序仅适用于Linux主机。

none

overlay

该overlay网络驱动程序会创建多个码头工人守护主机之间的分布式网络。该网络位于(覆盖)特定于主机的网络之上,允许连接到它的容器(包括群集服务容器)安全地进行通信。Docker透明地处理每个数据包与正确的Docker守护程序主机和正确的目标容器的路由。

参考

如果本文有错,希望在下面的留言区指正。

在之前的一些源码分析中,为了实现并发,Doug Lea 大佬在Java8及以上,大量使用了 CAS 操作。JDK 提供的关于 CAS 原子操作的类在下面工具包里面:

JDK为Java基本类型都提供了CAS工具类。

CAS

AtomicInteger 为例,进行分析:

1
2
3
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

如上面的源码,对于 CAS 操作,这里会出现3个值,expect、update、value。只有当expect和内存中的value相同时,才把value更新为update。

ABA 问题

假设如下事件序列:

线程 1 从内存位置V中取出A。
线程 2 从位置V中取出A。
线程 2 进行了一些操作,将B写入位置V。
线程 2 将A再次写入位置V。
线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。

尽管上面的CAS操作成功了,数据也没有问题,但是程序失去了对数据变换的敏感性,不知道数据的变换。

比如发生扣款/收款行为时,应当收到短信通知这个场景,
1、时刻1 : 500元
2、时刻2:转给了 A 10 元 490 元
3、时刻3:B 转入 10 元 500 元
应当收到两条短信,而不是最后我的账户余额没有变化,就一条短信都收不到

上面的图片来源不知道是谁的,只是在一个技术群里和别人聊CAS时别人发的。

解决方法

AtomicStampedReference

JDK 为了解决 ABA 问题,提供了一些方法,如 AtomicStampedReference,在版本的更新过程中,添加了一个 stamp 邮戳来标记数据的版本。

1
2
3
4
5
6
7
8
9
//比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
public boolean compareAndSet(V expectedReference, V newReference,
int expectedStamp, int newStamp)
//获得当前对象引用
public V getReference()
//获得当前时间戳
public int getStamp()
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp)

具体关于 CAS 操作源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}

如上面所示,在更新的过程中,除了比较内存中value的预期值,还比较了 stamp 的预期值,只有两者都相同的时候,才会把内存中的值更新掉。

AtomicMarkableReference

AtomicMarkableReferenceAtomicStampedReference 功能相似,但AtomicMarkableReference 描述更加简单的是与否的关系。它的定义就是将状态戳简化为true|false。如下:

1
2
3
4
5
6
7
8
9
10
11
12
public boolean compareAndSet(V       expectedReference,
V newReference,
boolean expectedMark,
boolean newMark) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedMark == current.mark &&
((newReference == current.reference &&
newMark == current.mark) ||
casPair(current, Pair.of(newReference, newMark)));
}

如果本文有错,希望在下面的留言区指正。

最近在看书的时候,看到说使用 BloomFilter 来进行判断一个元素是否最有可能属于一个集合,或者它是绝对不属于这个集合。BloomFilter 不适合“零错误”的场合,只能在能容忍地错误率的场合下使用,BloomFilter 通过极少的错误换取了存储空间的极大节省。

在Java中并不提供 BloomFilter 集合框架,使用者需要导入google guava jar包,提供了 BloomFilter。

BloomFilter 是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。

原理

BloomFilter 的数据结构是由两部分构成:

  • 一堆散列函数
  • 一个位数组

如下所示,定义一个10位的数组:
[0,0,0,0,0,0,0,0,0,0]

在添加元素,先对添加的元素使用 k 个hash函数,来计算出 k 个在数组中的位置,然后,将这些位置的 bit 置为 1。

例如,把输入的x经过两次hash,给出的位置分别是0和4,将这两个位置bit置为1。

[1, 0, 0, 0, 1, 0, 0, 0, 0, 0]

为什么会出现可能出错

如下图所示,现在输入 x 和 y 两个数,分别把相应的位置置为1,当我们输入下一个元素 a 是,经过 hash 函数,得出的位置 3 和 13(下标从0开始),在数组中,其实不存在 a,但是这两个位置分别是 x和y的某个hash位置,所以就会出现开始所说的,某个元素可能存在集合中。

删除不可取

在 Bloomfilter 中不可删除元素,如果删除元素,会导致其他的一些元素不能被找到,当这些元素的某些下标和被删元素相同。

参考