概述

PaddleOCR 是由百度飞桨(PaddlePaddle)团队开发的一款开源的光学字符识别(Optical Character Recognition, OCR)工具库。它专门设计用于文本检测和识别任务,能够识别图像中的文本信息,并将其转换成可编辑的文本格式。PaddleOCR 具有以下特点:

  1. 多语言支持:PaddleOCR 不仅支持中文,还支持多种语言的文本识别。

  2. 多种识别模型:提供了多种文本识别模型,包括但不限于通用识别模型、超轻量级模型等,以适应不同的应用场景和性能需求。

  3. 端到端识别:PaddleOCR 提供了端到端的识别流程,包括文本检测、方向分类、文本识别等步骤。

  4. 易用性:PaddleOCR 提供了丰富的 API 和命令行工具,使得开发者可以快速集成和使用 OCR 功能。

  5. 高性能:基于深度学习技术,PaddleOCR 在保证高精度的同时,也具有较高的识别速度。

  6. 可扩展性:PaddleOCR 允许用户自定义训练数据和模型,以适应特定的应用需求。

  7. 社区支持:作为开源项目,PaddleOCR 拥有活跃的社区,用户可以在社区中获取帮助和进行交流。

PaddleOCR 适用于各种场景下的文本识别任务,如文档扫描、票据识别、街景文字提取等,是企业和开发者在进行 OCR 相关开发时的一个有力工具。

效果预览

机票的识别结果:

照片识别结果:

从结果上看识别的准确率还是很高的,

镜像获取

本篇文章介绍如果通过 docker 部署 cpu 版本,有以下三种方式获取

  1. 通过 dockerfile 自己打包镜像使用 https://gitee.com/noogel/docker-paddleocr
  2. 通过拉取 dockerhub 的镜像。由于目前很难找到代理,倾向于通过第一中方式自己打包镜像。
  3. 联系作者要单独镜像文件。

其中第一中方式使用的是 cpu 版本服务端识别模型,需要部署的 cpu 有比较好的性能,特点是多语言识别,且识别结果很准确。

1
2
3
4
5
ENV PPOCR=v2.7.5
ENV DET=ch_PP-OCRv4_det_server_infer.tar
ENV CLS=ch_ppocr_mobile_v2.0_cls_slim_infer.tar
ENV REC=ch_PP-OCRv4_rec_server_infer.tar
ENV PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python

如果你想切换使用不同的模型可以在 github 文档中自行选择:https://github.com/PaddlePaddle/PaddleOCR/blob/main/deploy/slim/auto_compression/README.md

测试代码

然而 docker 提供的只是 API 接口形式访问,需要先将图片读取转化为 base64 编码,再通过 POST 请求后获取解析结果。下面是测试代码。

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
42
43
44
45
46
47
48
49
50
import os
import time
import requests
import json
import base64


def cv2_to_base64(image):
return base64.b64encode(image).decode("utf8")

def main(args):
headers = {"Content-type": "application/json"}
cnt = 0
total_time = 0
for directory, _, file_list in os.walk(args.image_dir):
for file_name in file_list:
file_path = os.path.join(directory, file_name)
img = open(file_path, "rb").read()
if img is None:
print("error in loading image:{}".format(file_path))
continue
# seed http request
starttime = time.time()
data = {"images": [cv2_to_base64(img)]}
r = requests.post(url=args.server_url, headers=headers, data=json.dumps(data))
elapse = time.time() - starttime
total_time += elapse
print("Predict time of %s: %.3fs" % (file_path, elapse))
res = r.json()["results"][0]
print("Result:" + " ".join([val['text'] for val in res]))
cnt += 1
if cnt % 100 == 0:
print("{} processed".format(cnt))
print("avg time cost: {}".format(float(total_time) / cnt))


def parse_args():
import argparse

parser = argparse.ArgumentParser(description="args for hub serving")
parser.add_argument("--server_url", type=str, required=True)
parser.add_argument("--image_dir", type=str, required=True)
parser.add_argument("--output", type=str, default="./hubserving_result")
args = parser.parse_args()
return args


if __name__ == "__main__":
args = parse_args()
main(args)

最后

关注到 PaddleOCR 是因为日常想找照片的时候,因为照片太多根本搜索不到,而照片存储在 NAS 上,又没有提供很好的图文检索功能,于是在自研的一个项目中想着集成图文检索功能。我是码力欧 https://noge.top ,如果你也有类似的需要,欢迎交流使用经验。

这边文章是想总结一下软件系统的设计表达方式,通过什么样的方式可以让你更有效的表达你的系统设计,如何让别人更清晰的去理解你的系统,以及在多人协作开发的时候如何让别人理解不同模块间的职责,明确每个人要做的事情。旨在提高协作效率,让你的系统不再沉默。在软件开发的各层次设计中最主要的是也是开发人员最不喜欢的是写文档,而文档中的图表也是你表达的一个关键点,接下来是要总结下不同表达层次的图表使用示例。总结下平时用到的一些图表以及使用场景。

流程图

泳道图

思维导图

时序图

ER图

架构图

知识图谱

甘特图

模板来源: https://v3.processon.com/popular

这个标题看起有点鸡汤文,不过我还是建议对以下总结出的几点做些深入思考,这些会在今后的工作中越来越多的感受到它的作用。

寻找你行业内的专家

找到你所属行业内的专家,这些人往往做事高效并且很有才华。你要做的是跟随他们所关注的方向,学习他们做事的方法,思考如何应用到你的工作和生活上。找到他们,和他们去交流思考,提出自己的观点和想法。不要仅仅把眼光放到身边的人身上,这样会局限住你的视野。

每天都写新代码

工作重复枯燥?也许有时候我们只是懒得思考,用最顺手的方式把工作做完,容易形成惯性思维。为什么会有很多的复制粘贴?简单的修改来适配当前需求,这里我们更需要的是想想能不能把这段逻辑抽象出来变得更通用,整个模块的设计是否不够合理,多想一想多做一点,下一次再来需求也许可以提升十倍的效率。

底层的原理更重要

客观的说,更快进步的方法之一是忽略掉那些并不能提高技能的东西,比如语言语法和配置工具,这些技能属于“知其然”,而你更需要的是“知其所以然”。有一次去医院科室挂号使用的是先到先叫的模式,而在急诊室挂号是按照轻重缓急分成四个等级的,危重病人优先抢救的模式。这不就和操作系统中的任务调度概念是一样的,优先级调度模式,这些底层的概念才是一通百通真正提高帮助你的东西。我在尝试去找行业经典论文看。

学会调研

作为程序员会比较容易脑子一热,有一个想法很容易趁热着急写代码,但往往缺乏思考写出来的代码不能尽如人意。这时候你更需要的是慢下来,好好思考一下,也许这些别人已经做过,有更好的方案,看看别人是如何做的。先调研再实施,这样会彻底改变你解决问题的思路。

学好英语

真的是这样,如果你英语不好,那么会比别人走更多的弯路,就像走在密林深处看不清路一样。不得不承认很多优秀框架的官方文档还是英文为主,如果再经过翻译里面的很多语义语境会丢失,在项目的社区中,你还能与作者们去交流你学习中遇到的问题。

如何去做

说了这么多,看着就好像道理我都懂,但是我不知道怎么做。我这里先总结几个点,也是自己在不断尝试学习的方法。

  1. 看行业经典论文,比如 mapreduce、raft 这些都是一通百通的底层概念。
  2. 研究优秀框架的源代码,理解核心原理,尝试造轮子。
  3. 每天学英语,尝试在开源社区与作者们进行互动。
  4. 找到一两位行业专家,向他们学习和请教问题。
  5. 坚持以上几点。

end.

ORM(Object/Relational Mapper),即“对象-关系型数据映射组件”。对于O/R,即 Object(对象)和Relational(关系型数据),表示必须同时使用面向对象和关系型数据进行开发。本文简述通过 Java 动态代理机制实现关系数据与 POJO 对象的映射。

代理

静态代理

静态代理其实就是指设计模式中的代理模式。
代理模式为其他对象提供一种代理以控制对这个对象的访问。

静态代理模式在增强现有的接口业务功能方面有很大的优点,但是大量使用这种静态代理,会使我们系统内的类的规模增大,并且不易维护。

动态代理

为了解决静态代理的问题,引入动态代理的概念,在编译时或者运行时,可以在需要代理的地方动态生成代理,减轻代理类和类在系统中冗余的问题。

Java 动态代理基于经典代理模式,引入了一个 InvocationHandler,InvocationHandler 负责统一管理所有的方法调用。

InvocationHandler

InvocationHandler 接口定义:

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

每一个动态代理类都必须要实现 InvocationHandler 这个接口,通过代理类的实例调用一个方法时,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用。

Proxy

Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法,可以获得一个动态的代理对象:

1
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,  InvocationHandler h)  throws IllegalArgumentException

实现

参照 mybaits 的用法实现基本的映射能力。

注解

首先定义了三个注解,一个作用在类上 DaoMapper 作用在类上标记这是一个映射类,然后定义注解 Selector 作用在方法上标记查询作用,定义注解 Param 作用在参数上为预编译位的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DaoMapper {
}

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Selector {
String value();
}

@Documented
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String value();
}

定义一个实体类,与数据库的表字段映射上。增强 feature 可以自动做驼峰转换,这里没有实现。

1
2
3
4
5
6
7
8
9
@Data
public class BaseLineModel {
public static final String TABLE = "baseline";

private Integer id;
private String report_name;
private Integer report_period;
private LocalDateTime creation_date;
}

定义dao层接口,加上注解

1
2
3
4
5
6
7
@DaoMapper
public interface BaseLineDao {

@Selector("select * from "+ BaseLineModel.TABLE +" where report_name = #{reportName}")
BaseLineModel select(@Param("reportName") String report_name);
}

JDBC OP

做到一个很简单的 JDBC 操作工具类,字段映射处理也写到了这里。实现了查询操作,将入参 sql template 以及参数按顺序传入,生成 prepareStatement 后执行,再将返回结果映射到 model 对象。这里的连接池管理、自动重连、配置管理等增强 features 非重点,不做实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 查询
* @param clazz model类
* @param sql
* @param params
* @param <T>
* @return
*/
public <T> T query(Class<T> clazz, String sql, Object... params) throws SQLException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
Object model = clazz.newInstance();
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cat", "root", "123456")) {
PreparedStatement statement = conn.prepareStatement(sql);
int flag = 1;
for (Object obj : params) {
setValue(statement, flag, obj);
flag++;
}
ResultSet resultSet = statement.executeQuery();
resultSet.afterLast();
resultSet.previous();
fullRes(resultSet, model);
}
return (T) model;
}

映射函数,通过自动寻找 setter 方法填充结果,这里只实现了三种字段。

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
private static void fullRes(ResultSet resultSet, Object model) throws SQLException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Field[] declaredFields = model.getClass().getDeclaredFields();
for (Field field : declaredFields) {
String fieldName = field.getName();
if (fieldName.toUpperCase().equals(fieldName)) {
continue;
}
String setFuncName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
String fieldType = field.getGenericType().toString();

Object object = resultSet.getObject(fieldName);
if (fieldType.equals("class java.lang.String")) {
Method m = model.getClass().getMethod(setFuncName, String.class);
m.invoke(model, object);
} else if (fieldType.equals("class java.lang.Integer")) {
Method m = model.getClass().getMethod(setFuncName, Integer.class);
m.invoke(model, object);
} else if (fieldType.equals("class java.time.LocalDateTime")) {
Method m = model.getClass().getMethod(setFuncName, LocalDateTime.class);
if (object instanceof Timestamp) {
object = ((Timestamp) object).toLocalDateTime();
}
m.invoke(model, object);
}
}
}

动态代理部分

定义一个 MapperMethod 类,实例化的时候提取接口方法的注解信息解析成 JDBC 需要的参数以及记录接口方法的返回对象, execute 执行。

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 MapperMethod<T> {
private String sql;
private Class<?> resType;
private int[] paramsIndex;


public MapperMethod(Method method) {
this.resType = method.getReturnType();
String sourceSql = method.getAnnotation(Selector.class).value();
Parameter[] parameters = method.getParameters();
int flag = 0;
this.paramsIndex = new int[parameters.length];
for (Parameter parameter: parameters) {
String paramName = parameter.getAnnotation(Param.class).value();
String paramFullName = String.format("#{%s}", paramName);
int indexOf = sourceSql.indexOf(paramFullName);
this.paramsIndex[flag] = indexOf;
flag++;
this.sql = sourceSql.replace(paramFullName, "?");
}
}

public Object execute(Object[] objects) {
JdbcUtil jdbcUtil = new JdbcUtil();
try {
return jdbcUtil.query(this.resType, this.sql, objects);
} catch (SQLException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return null;
}
}

定义动态代理类,在实例化的时候记录代理接口,以及代理方法类缓存,调用接口的时候会被动态代理到 invoke 函数执行,然后交由 MapperMethod 代理方法实例执行。

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;

public class MapperProxy<T> implements InvocationHandler {

private final Class<T> mapperInterface;

private final Map<Method, MapperMethod> methodCache;

public MapperProxy(Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(objects);
}

private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (Objects.isNull(mapperMethod)) {
mapperMethod = new MapperMethod(method);
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}

最后代理工厂类,接收被 DaoMapper 作用的接口,并通过 newInstance 方法创建代理类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;

private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
if (Objects.isNull(mapperInterface.getAnnotation(DaoMapper.class))) {
throw new RuntimeException("缺少注解 DaoMapper");
}
this.mapperInterface = mapperInterface;
}


public T newInstance() {
final MapperProxy<T> mapperProxy = new MapperProxy<>(mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
}

执行,创建一个代理工厂,然后创建 BaseLineDao 的代理对象, 调用 select 方法,实际上调用到代理对象的 invoke 方法,然后交由 mapperMethod.execute 方法执行:

1
2
3
4
5
6
public static void main(String[] args) {
MapperProxyFactory mapperProxyFactory = new MapperProxyFactory(BaseLineDao.class);
BaseLineDao baseLineDao = (BaseLineDao) mapperProxyFactory.newInstance();
BaseLineModel test1 = baseLineDao.select("TEST1");
System.out.println(test1);
}

扩展

TODO:

  1. Java动态代理与 cglib 动态代理的异同点。
  2. 动态代理的实现原理。

总结

通过这个个简单的实践,了解了 Java 动态代理的使用方法以及对象关系数据的映射处理。

参考

https://zhuanlan.zhihu.com/p/60805342
https://www.zhihu.com/question/20794107/answer/658139129

锁解决的问题是并发操作引起的脏读、数据不一致问题。

基本原理

volatile

在Java中允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保使用排它锁来单独获得这个变量,Java中提供了 volatile,使之在多处理器开发中保证变量的可见性,当一个线程改变了共享变量,另一个线程能够及时读到这个修改的值。恰当的使用它会比 synchronized 成本更低,因为不会引起上下文的切换和调度。

synchronized

通过锁机制实现同步,在Java中每一个对象都可以作为锁,有以下三种形式:

  • 对于普通同步方法,锁的是当前实例对象。
  • 对于静态同步方法,所得是当前类 class 对象。
  • 对于同步方法块,锁的是括号内指定的对象。

为了减少获得锁和释放锁带来的性能消耗,Java SE 1.6 引入了偏向锁和轻量级锁。偏向锁的核心思想是:如果一个线程获得了锁,就进入偏向模式,当这个线程再次请求锁时,如果没有其它线程获取过该锁,无需再做任何同步操作,可以节省大量锁申请的操作,来提高性能。如果偏向锁获取失败,会通过轻量级锁的方式获取,如果获取成功则进入临界区,如果失败则表示有其它线程争夺到锁,当前线程锁请求会膨胀为重量级锁

锁粗化 是指在遇到一连串连续的对同一个锁不断的进行请求和释放的操作时,会把所有的锁操作整合成对锁的一次请求,减少锁请求的同步次数。

锁消除 是指在编译期,通过对上下文的扫描,去除不可能存在共享资源竞争的锁。

自旋锁 是指在锁膨胀后,避免线程真正的在操作系统层面被挂起,通过对线程做几个空循环,以期望在这之后能获取到锁,顺利的进入临界区,如果还获取不到,则会真正被操作系统层面挂起。

CAS

指的是比较并交换,它是一个原子操作,比较一个内存位置的值并且只有相等时修改这个内存位置的值并更新值,保证新的值总是基于最新的信息计算的。在 JVM 中 CAS 操作是利用处理器提供的 CMPXCHS 指令实现。是实现我们平时所说的自旋锁或乐观锁的核心操作。

优点是竞争小的时候使用系统开销小;对应缺点是循环时间长开销大、ABA问题、只能保证一个变量的原子操作。

ABA 问题

问题产生原因是两个线程处理的时间差导致,具体如下图:

解决 ABA 问题可以增加一个版本号,在每次修改值的时候增加一个版本号。

产生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();

new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t修改后的值:" + atomicReference.get());
},"t2").start();
}

解决:

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
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

public static void main(String[] args) {
new Thread(() -> {
System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp());

//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
},"t1").start();

new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println("t2拿到的初始版本号:" + stamp);

//睡眠3秒,是为了让t1线程完成ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最新版本号:" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前 值:" + atomicStampedReference.getReference());
},"t2").start();
}

算法概述

Raft 算法是解决分布式系统一致性问题的,与 Paxos 实现的功能相同,相对来说更容易实现和理解。这些一致性协议可以保证在集群中大部分节点(半数以上节点)可用的情况下,集群依然可以工作并给出一个正确的结果。
Raft 将一致性问题分解为多个子模块解决:

  • Leader 选举 Leader election
  • 日志同步 log replication
  • 安全性 safety
  • 日志压缩 log compaction
  • 成员变更 membership change

Raft 将系统中的角色分为:

  • Leader 接受客户端请求,并且向 Follower 同步请求日志,当日志同步到大多数节点上后告诉 Follower 提交日志。
  • Follower 接受并持久化 Leader 同步的日志,在 Leader 通知可以提交后提交日志。
  • Candidate 是选举过程中的临时角色。

Raft 要求系统在任何一个时刻最多只有一个 Leader,正常工作期间只有 Leader 和 Follower。
Raft 算法角色状态转换如下:

Follower 只响应其它服务器的请求,如果 Flower 超时没有接受到 Leader 的消息,它会成为一个 Candidate 状态并开始一次 Leader 选举,收到大多数服务器投票的 Candidate 会成为新的 Leader,Leader 在宕机之前会一直保持 Leader 状态。

Raft 算法将时间分为一个个的任期 term,每一个 term 的开始都是 Leader 选举,在成功选举 Leader 之后,Leader 会在整个 term 内管理整个集群,如果 Leader 选举失败,这个 term 就会因为没有 Leader 而结束。

Leader 选举(Leader election)

Raft 使用心跳触发 Leader 选举。当服务器启动时,初始化为 Follower。Leader 向所有 Follower 周期性发送 heartbeat。如果 Follower 选举超时,会等待一段随机时间后再发起一次 Leader选举。选举出 Leader 后,会定期向所有 Follower 发送 heartbeat 维持状态,如果 Follower 一段时间没有收到心跳则认为 Leader 已经挂了,再次发起Leader选举过程。

日志复制 (log replication)

Leader 选举出来后,就开始接收客户端的请求,把日志条目加入到日志处理中,然后并行的向其它服务器发起请求复制日志条目。当这条日志被复制到大多数服务器中,Leader会把这条日志状态改变向客户端返回执行结果。

如果某个Follower没有复制成功,则Leader会无限的重试直到Follower最终存储了所有的日志条目。日志由有序编号和日志条目组成,每条日志条目包含它被创建时的任期号 term,和用于状态机执行的命令。

安全性 (safety)

Raft增加两条极限值来保证安全性:

  1. 拥有最新已提交的log entry 的 Follower 才有资格成为 Leader
  2. Leader只能推进commit index 来提交当前term的已经复制到大多数节点上的日志,旧的term日志会跟随当前term的日志来间接提交。

日志压缩 (log compaction)

通过定期记录 snapshot 来解决,每个副本独立的对自己系统状态进行snapshot,并且是已提交的日志进行。snapshot 包含日志元数据,最后一条已提交的 log entry 的 log index 和 term。Leader会发送snapshot给最后日志太多的Follower,或者新加入的机器。
copy-on-write https://blog.csdn.net/u012501054/article/details/90241124
做一次snapshot可能耗时过长,会影响正常日志同步。可以通过使用copy-on-write技术避免snapshot过程影响正常日志同步。

成员变更 (membership change)

不同节点之间同步成员变更存在间隙,会导致一致性问题。Raft提出两阶段成员变更方法,集群从旧成员配置切换过度成员配置,叫做共同一致,是指旧成员配置和新成员配置组合,一旦共同一致被提交,系统再切换到新成员配置。

Raft与Multi-Paxos的不同:

QA

https://zhuanlan.zhihu.com/p/32052223

Java 类从源码到实例化对象需要经历几个过程

  1. 编写Java源码(.java文件)
  2. 编译成Java字节码(.class文件)
  3. 类加载器读取字节码转换成java.lang.Class实例
  4. JVM 通过 newInstance 等方法创建真正对象

ClassLoader 是 Java 最基本的类加载器,用来实例化不同的类对象。Java类的来源可以有内部自带的核心类$JAVA_HOME/jre/lib/,核心扩展类$JAVA_HOME/jre/lib/ext,动态远程加载的.class文件,分别由不同的 ClassLoader 来协作加载。

在 mac 机器上可以使用 mweb 来写博客,比较好用的地方就是可以直接把剪贴板的图片粘贴上来,缺点是 mac 键盘超难用并且不支持窗口内开启命令行。平时在家的时候都用 Ubuntu 台式机,博客使用 VS Code 编写,一直以来阻挡我的是图片的粘贴特别费劲,今天发现一个很好用的插件 pasteimage,可以直接将剪贴板图片粘贴到 markdown 使用,并且支持配置保存路径。

然后按照教程配置好参数:

1
2
3
4
5
6
{
"pasteImage.path": "${projectRoot}/source/resource/img",
"pasteImage.basePath": "${projectRoot}/source",
"pasteImage.forceUnixStyleSeparator": true,
"pasteImage.prefix": "/"
}

就可以直接将图片粘贴到 markdown 中,其中遇到个问题就是配置不生效,会导致文件直接保存到当前文件目录,具体配置方法可以参考下面连接。

https://www.crifan.com/vscode_how_to_config_setting_plugin/ 这篇文章写的很详细了。
https://github.com/mushanshitiancai/vscode-paste-image 这篇是配置教程,里面有些地方比较容易被误导。

对于Linux系统需要有 xclip 支持,使用的时候会给提示的。

另外记录一下 Ubuntu 的截屏和粘贴快捷键:

1
2
Ctrl + Shift + Print Screen  // 区域截屏到剪贴板
Ctrl + Alt + s // 在 VS Code 中粘贴

在 Chrome 上有个很好用的插件 FeHelper,应该是每个开发人员都在使用的,功能很全面。想着能不能搞个桌面版的,这样多屏环境下可以不用在众多 tab 页中找功能了。

桌面版实现,界面比较丑陋,不过用起来方便就行。这样可以在多屏环境下一个屏用来开啊,另一个屏可以观察辅助信息和使用小工具。

了解到 electron 是一个开源跨平台框架,可以使用 nodejs 做后端和 chromium 做前端开发。像 atom 和 vs code 使用这个框架开发的。觉得还是和方便的,主要是跨平台。而这个插件也是基于 nodejs 开发的,应该可以迁移过来。然后按照官方文档开发,通过 iframe load 不同页面。

项目地址:https://github.com/noogel/xyzToolbox

Mac 安装包下载地址:https://pan.baidu.com/s/1SYjVX2Dhz6TTbaif1Bk8RA 密码:64oo

拉取项目和子模块

1
2
3
git clone https://xxx.git
git submodule init
git submodule update

打包命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
环境安装
npm install

# Linux打包成AppImage文件
# 在Linux环境上执行
node_modules/.bin/electron-builder -l AppImage

# Windows打包成exe安装文件
# 在Windows环境下执行
node_modules/.bin/electron-builder -w nsis
node_modules/.bin/electron-builder -w --ia32 nsis

# 如果在非Windows上打包win程序,也可以借助docker 如下
# docker run --rm -it -v ${PWD}:/project electronuserland/builder:wine sh -c "node_modules/.bin/electron-builder -w nsis"

# Mac打包成dmg文件
# 在Mac环境下执行
node_modules/.bin/electron-builder -m dmg

打包参考链接

https://qii404.me/2019/07/10/electron.html

JVM Runtime Data Area

根据 JVM 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

线程私有:程序计数器,虚拟机栈,本地方法栈。
线程共享:堆、元空间、直接内存

  1. 元空间(Metaspace),JDK 8 之前 HotSpot 虚拟机使用永久代来实现的方法区(方便内存管理), JDK 8 废弃了永久代,将原来永久代的字符串常量池、静态变量、类型信息等全部移到了元空间中。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

  2. 虚拟机栈(JVM Stacks),每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。

  3. 本地方法栈(Native Method Stack),与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。

  4. 程序计数器(Program Counter Register),程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。

  5. 堆内存(Heap),堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在堆中。

  6. 直接内存(Direct Memory),直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

关于方区