0%

今年链游的数量和玩家人数都有了大规模的增长,
来自DappRadar的一份报告显示,
10月份平均每天有超过100万个独立的活跃钱包地址连接到链游app中,
而资本市场当月更有1.2亿美金投入链游相关的平台项目中。

一方是玩家大量的涌入链游,另一方则是链游项目在资本的助推下不断涌现。

这时候连接的价值日益显现:
比如YGG等公会的出现,连接了游戏和玩家两方,
通过游戏道具的租借和专业的培训,
使玩家进场成本更低收益更高,也使游戏方获得了更多的用户。
而当游戏越来越多,单一公会其实无法带领玩家触达每一个链游,
各类公会就出现分而治之的局面,各自招募玩家奔向不同的链游。

这时候,就出现了一种平台的机会,
能够连接所有的玩家,链游和不同的游戏公会,
给它们一个互相发现、互相匹配并获取收益的渠道。
今天我们要讲的GuildFi就是这样一个平台,
专注于与玩家、游戏和公会之间的连接。

GuildFi是什么

弥补链游市场需求和现实的差距

在介绍GuildFi之前,我们不妨先看看目前链游市场上的参与者和他们的需求。

  1. 玩家:需要低成本的寻找+进入更多收益高的游戏,并把游戏中投入的时间价值最大化;
  2. 游戏:需要低成本且广泛的吸引玩家参与;
  3. 公会:需要快速创建公会并招募玩家参与,并需要找到更多值得入驻的游戏。

而在目前的链游市场中,现实情况并不能完全满足他们的需求:

  1. 玩家:零散的寻找要进入的游戏,支付高额进场费用,研究打金攻略,投入产出比不稳定;
  2. 游戏:多渠道通过广告、运营活动和社交媒体进行营销和获客,存在投放宣传成本;
  3. 公会:各自创建公会并找寻玩家,创建公会需要大量启动资金,且寻找玩家成本高

可以明显的看出,三类角色的需求和现实情况仍存在着差距
因此目前的链游市场其实可以构建一个平台来连接玩家、游戏和公会,
并解决他们在现实情况中遇到的问题。

GuildFi: 连接链游市场各参与方的多功能服务平台

GuildFi的目标是努力缩小现在链游市场上各参与者的需求和现实差距,
其实现手段则是为玩家、游戏和公会提供各自急缺的服务,
并作为一个平台将服务资源在三者之间共享,以达到相互发现,提升效率的目的。
总体来看,GuildFi提供了四大服务:游戏平台、公会专区、NFT专区、工具专区

每一个服务中又细分成了不同的功能模块。
考虑到GuildFi中的模块较多,单纯的介绍功能未必能完全理解它的设计用意,
我们仍然接着上面玩家、游戏和公会各自遇到的问题来讲解GuildFi的功能设计,
看看它是如何对症下药来解决问题的。

1.玩家:通过「发现游戏」和「发现公会」功能,降低游戏寻找成本和进场成本
在GuildFi的游戏平台功能中,有2个叫做发现「游戏发现」和「发现公会」的功能。
在游戏发现方面:
即平台会列出目前值得参与的游戏列表,
并提供该游戏的基础信息和相关的数据分析。
对于数据分析功能,目前官网显示功能即将上线,
但我们可以猜测,这类分析一定与游戏活跃用户数、投入产出比等信息相关,
借此来为玩家提供是否参与游戏的判断依据。

在游戏方面,官网上目前支持的游戏仅有2款,分别是Cyball和Axie,
而在官方的白皮书中我们则看到了另外两款引人关注的作品也将整合进入GuildFi:
Sandbox和Star Atlas。

这几款游戏的号召力不必多提,前期选入游戏发现列表中也是GuildFi吸引用户的举措,
但能够争取到这几款游戏加入,足见GuildFi在链游领域的资源。

对于另外一个名为「公会发现」的功能,其实不难理解,
当链游市场上有很多公会时,玩家可能不知道加入其中的哪一个,
而GuildFi也提供了公会列表,并列出了不同公会的所在区域和与用户分成的比例。

此外最重要的是,公会往往会提供“奖学金”计划,
通过租赁游戏进场所需的NFT,降低玩家进入游戏的成本,
并提供专业的辅导和教学来告诉玩家如何提高打金水平和收益。
这些信息若都能集成在GuildFi中,将极大程度的降低玩家的学习成本和进场成本,
这就解决了前文所述的玩家在现实中面临的问题。

2.玩家:通过一站式账号ID和经验值系统,最大化玩家的时间价值
在游戏平台中还存在两个功能分别是GuildFi ID和“游玩证明”,
前者是该平台的通用ID,仅需简单的注册步骤,
即可达到“一个账号,畅玩平台全部游戏”的效果,而不同游戏的时长,成就等也都集成在账号中,
这非常类似苹果商店或者谷歌商店的账号,能够管理手机内所有下载游戏的信息。

而与ID关联的则是任务和奖励系统,平台会自动关联你玩的所有游戏,
并设计一系列的任务让你达成,成功后可以获得GXP(GuildFi的游戏经验值系统),
这些奖励可以让你获得额外的NFT空投,新游戏提前体验以及GF通证。

这种统一账户ID和游戏经验值奖励的设计,则最大化了玩家投入时间的价值,
而以前玩家玩不同的游戏仅能获得游戏内的奖励,
现在则多了一层GuildFi平台提供的奖励,一举多得。

3.公会:提供公会网络+一站式公会组建服务,降低公会组建和运营成本
GuildFi作为一个平台,自然会有自己的基金和天使投资,
通过背后资金的支持,初始创建的公会可以向平台借入NFT资产,
而公会则可以将这些资产再借出给招募的玩家,这就降低了公会的资金成本。

此外,如果多个公会都入驻了GuildFi平台,他们之间也可以互相出借NFT资产,
这也达到了公会间资源的优化配置。
在招募玩家方面,GuildFi也内置了一个“奖学金门户”,
使公会能够更为快捷的通过平台招募到玩家,这也对应着前文介绍的面向玩家的“公会发现”,
达到了玩家和公会之间匹配效率的提升。

更为重要的是,GuildFi平台推出了一项“公会即服务”的功能,
这个概念与时下流行的“云服务”和“软件即服务”的概念类似,
即由GuildFi提供各种公会所需要的模块,如创建ID、注资、运营、资产管理等,
公会进来可以按照自己目前发展的阶段,按需选取相应的模块和服务,从而完善自己的公会建设。
而以上这些则解决了公会目前在链游领域面临的种种问题。

4.游戏:Metadrop作为营销手段吸引更多用户参与
对于玩家来说,吸引他们参与某个游戏的理由,
除了打金收益外,不定时的NFT和通证空投也是其中之一。
对玩家来说的“薅羊毛”,其实对游戏方来说则是一种重要的获客手段。

GuildFi平台也提供了一个Metadrop专区,专供游戏方来发布空投,
而玩家则依据前文所述的GXP等来获得不同层级的空投奖励,
此举也更好的连接了玩家和游戏方,而这种直接的激励都可以在同一个平台上完成,
在平台粘性增加的同时,接入平台的游戏方均可受益。

最后,游戏的工具专区在官网中并没有得到体现,
白皮书中描述该区域主要是用于公会奖学金的账户管理和支付管理,
同时还能看到不同游戏的排行榜以及PVP模拟等,
这一部分内容是GuildFi基于Axie的公会经验所开发的,
实际成品功能的普适性有待后续观察。

GuildFi的基本面:优秀资方加持+丰富的公会经验

GuildFi已经完成了600万美元的种子轮融资,
除了Definance和hashed领投之外,
Aniomca,coinbase及coin98等也参与了投资,
这些知名的资方入局,也体现了资本对于构建GuildFi平台的看好,
毕竟目前市场上公会众多,而连接公会游戏和玩家的平台则没有出现龙头。

而关于GuildFi,其前身也是一个泰国本地的游戏公会,
成立社区超过5年,并活跃在Axie、THG和传奇4等游戏中,
举办过不少公会活动,如举办杯赛和组织团队击杀游戏BOSS等。

这些公会经验在建立GuildFi平台时可以得到复用,并指导其他公会的创建和运营。

GF通证介绍

在了解完GuildFi的全部功能后,其通证GF的作用就变得非常易于理解,
随着GuildFi的功能落地和发展,GF对GuildFi的功能存在价值捕获的效应:

  1. 价值发现:GuildFi里的游戏玩家越多,游戏生态越庞大,则意味着平台收入的提高和流量的增加,这将对GF通证形成利好。
  2. GF DAO会员权益:持有GF,玩家可以抢先体验游戏,参与稀有NFT分配和空投,并获得质押奖励。此外,GuildFi为公会提供的各种创建服务也会产生收益,玩家可以依据持有的GF比例获得不同额度的分成。
  3. 治理与决策:投票哪些游戏可以加入GuildFi等。

值得一提的是,GF目前正在官方进行通证拍卖。

起始时间为12月1日-12月4日,并采用价高者得的荷兰拍卖形式, 有兴趣的读者可以关注。

GuildFi的潜力和风险

综合来看,GuildFi目前在做的是典型的平台建设,
即通过平台来匹配市场上的供需方,实现资源最优配置。

这种做法类似于滴滴打车平台, 通过吸引打车人和车主入驻,且无论车辆属于哪家服务公司, 都能满足运输能力和出行需求的高效匹配。

这里的打车人=玩家,这辆车=游戏, 车主或者服务公司=公会,而滴滴=GuildFi

而平台经济往往具备两个优势, 即“赢家通吃”和“先发制人”,

一旦用户粘性构建起来后, 其他想要效仿该平台则难以获得同级别的流量。

也正因为GuildFi在做连接玩家、游戏和公会间的平台,

它的定位和我们熟知的YGG也是不一样的,

GuildFi可以被理解为: YGG + 公会搭建支持 + 公会资源贡献 + 平台经济

但我们也应该清楚的认识到,
做一个连接的平台也不存在技术壁垒,
其他的公会也可以争相模仿,如果竞对拥有更快的建设速度和更加雄厚的资本支持,
那么GuildFi的优势也有可能被瓦解。

此外,一些优质的链游出于利益的考虑可能也不会选择和平台合作,
类似著名手游元神不和发行渠道合作,独自负责宣发而避免高额的抽成,
这时候GuildFi的连接游戏方的作用也会受到影响。

而无论如何,一个完整的元宇宙概念里,
游戏,玩家和公会应该是可以高效的相互链接的,
在目前元宇宙中的角色们还处在一个个孤岛中的情况下,
构建连接是完全值得提倡的。
虽然连接不同角色的道路比较难走,
涉及到各类资源的整合,
但最曲折的路有时最简捷,
这条路可能也是通向元宇宙繁荣的捷径。

概要

本文主要介绍如何通过浏览器WEB使用websocket、protobuf协议与netty服务端通信。
主要涉及到的技术点:js、websocket、java、netty、protobuf,下面会详细介绍如何使用。

前端

主要涉及到四个文件,看截图,完成这些,客户端这边就OK了。

ptoto文件

  1. SubscribeReq.proto,用于发送到服务端:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    syntax="proto3";
    package req;

    message SubscribeReq {
    int32 subReqId = 1;
    string userName = 2;
    string productName = 3;
    string address = 4;
    }
  2. SubscribeResp.proto,用于解析服务端发送过来的消息:
    1
    2
    3
    4
    5
    6
    7
    8
    syntax="proto3";
    package res;

    message SubscribeResp{
    int32 subReqId = 1;
    int32 respCode = 2;
    string desc = 3;
    }
    注:package对应客户端中的代码root.lookupType(className),className:#{package}.#{message}

protobuf.min.js

protobuf.min.js下载地址

  1. 下载
    选择最近版本zip下载
  2. 解压,获取目标文件
    protobuf.min.js文件在解压后dist目录下,如图:
  3. 放到html同级目录,并在html中引用

html客户端

TestProtobuf.html:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="protobuf.min.js"></script>
<title>Title</title>
</head>
<body>
<button onclick="sendMsg()">手动发送</button>
</body>
<script>
let buffer;

/**
* 手动发送消息
*/
function sendMsg(){
let payload = {subReqId: 1, userName: "cjf", productName: "hello server 我是客户端 111 !!", address:"地址"};
let message = reqMessage.create(payload); // or use .fromObject if conversion is necessary
buffer = reqMessage.encode(message).finish();
webSocket.send(buffer);
}
// 客户端请求对象
let reqMessage;
// 服务端返回对象
let resMessage;

/**
* 初始化 reqMessage
* @param fileName
* @param className
* @param type 1:request,2:response
*/
function initCusMsg(fileName, className, type) {

return protobuf.load(fileName)
.then((root) => {

if (type === 1) {
reqMessage = root.lookupType(className);
return reqMessage;
} else if (type === 2) {
resMessage = root.lookupType(className);
return resMessage;
}
});
}

const address = "ws://127.0.0.1:9999/ws";

let webSocket = new WebSocket(address);

webSocket.onopen = function () {


console.log("webSocket连接建立成功... " + "服务端address: " + address);
//连接成功 发送消息

let reqMsgPromise = initCusMsg("SubscribeReq.proto", "req.SubscribeReq", 1);

reqMsgPromise.then((cusMsg) => {

//参考 https://github.com/protobufjs/protobuf.js#using-proto-files

// Exemplary payload
let cuTime = new Date().getTime();
let payload = {
subReqId: 1, userName: "cjf", productName: "hello server 我是客户端 2222!!", address:"地址"};
// Verify the payload if necessary (i.e. when possibly incomplete or invalid)
let errMsg = cusMsg.verify(payload);
if (errMsg) {

throw Error(errMsg);
}
// Create a new message
let message = cusMsg.create(payload); // or use .fromObject if conversion is necessary
// Encode a message to an Uint8Array (browser) or Buffer (node)
buffer = cusMsg.encode(message).finish();

webSocket.send(buffer);
});
};

//接收消息
let reader = new FileReader();
//监听服务端响应消息
webSocket.onmessage = function (event) {
let resMsgPromise = initCusMsg("SubscribeResp.proto", "res.SubscribeResp", 2);

resMsgPromise.then((cusMsg) => {
reader.readAsArrayBuffer(event.data);
reader.onload = () => {

let arrayBuffer = reader.result;
let buffer = new Uint8Array(arrayBuffer);

let resObject = resMessage.decode(buffer);
let errMsg = cusMsg.verify(resObject);
if (errMsg) {

throw Error(errMsg);
}
console.log(resObject)
};
});
}
</script>
</body>
</html>

后端

主要涉及到几个文件,看截图,完成这些,服务端这边就OK了。

ptoto文件

  1. SubscribeReq.proto:定义解析客户端发过来的数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    syntax="proto3";
    option java_package = "com.gate.protobuf";
    option java_outer_classname = "SubscribeReqProto";

    message SubscribeReq {
    int32 subReqId = 1;
    string userName = 2;
    string productName = 3;
    string address = 4;
    }

  2. SubscribeResp.proto:定义发送到客户端的数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    syntax="proto3";
    option java_package = "com.gate.protobuf";
    option java_outer_classname="SubscribeRespProto";

    message SubscribeResp{
    required int32 subReqId = 1;
    required int32 respCode = 2;
    required string desc = 3;
    }

  3. 下载protobuf-java-3.19.1.zip,解压后可以配置下全局命令执行。

  4. 生成java文件
    执行:

    1
    2
    protoc --java_out=./ SubscribeReq.proto
    protoc --java_out=./ SubscribeResp.proto

    生成SubscribeReqProto.java、SubscribeRespProto.java

注:java_package路径对应生成的java包路径, java_outer_classname对应生成的java文件名

依赖包引用

1
2
3
4
5
6
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.19.1</version>
</dependency>

注:protobuf-java的版本要对应protoc的版本

netty服务端

Server.java代码:

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
public class Server {
/**
* 自定义ChannelInitializer
*/
public void start() {
short port = 9999;
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap.group(boos, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new CustomChannelInitializer());
try {
Channel channel = serverBootstrap.bind(port).sync().channel();
System.out.println("服务端启动 端口:" + port);
channel.closeFuture().await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
new Server().start();
}
}

CustomChannelInitializer.java代码:

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
public class CustomChannelInitializer  extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();

pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(1024 * 64));

pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

//将WebSocketFrame转为ByteBuf 以便后面的 ProtobufDecoder 解码
pipeline.addLast(new MessageToMessageDecoder<WebSocketFrame>() {

@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {

ByteBuf byteBuf = frame.content();
byteBuf.retain();
out.add(byteBuf);
}
});

pipeline.addLast(new ProtobufDecoder(SubscribeReqProto.SubscribeReq.getDefaultInstance()));
//自定义入站处理
pipeline.addLast(new TestProtoBufInboundHandler());

//出站处理 将protoBuf实例转为WebSocketFrame
pipeline.addLast(new ProtobufEncoder() {

@Override
protected void encode(ChannelHandlerContext ctx, MessageLiteOrBuilder msg, List<Object> out) throws Exception {

SubscribeRespProto.SubscribeResp mpMsg = (SubscribeRespProto.SubscribeResp) msg;
WebSocketFrame frame = new BinaryWebSocketFrame(Unpooled.wrappedBuffer(mpMsg.toByteArray()));
out.add(frame);
}
});
}
}

TestProtoBufInboundHandler.java代码:

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
public class TestProtoBufInboundHandler extends SimpleChannelInboundHandler<SubscribeReqProto.SubscribeReq> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, SubscribeReqProto.SubscribeReq msg) throws Exception {
Gson gson = new Gson();
System.out.printf("服务端收到请求数据:%s", gson.toJson(msg));

//响应消息
SubscribeRespProto.SubscribeResp.Builder builder = SubscribeRespProto.SubscribeResp.newBuilder();
builder.setRespCode(200);
builder.setSubReqId(1);
builder.setDesc("success receive!");
SubscribeRespProto.SubscribeResp build = builder.build();

ctx.channel().writeAndFlush(build);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();//将消息从发送缓冲区中写入socketchannel中
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}
}

启动调试

完成以上操作、配置后,就可以先启动Server.java,再打开TestProtobuf.html,进行调试了。

在我们平时的开发过程中,经常可能会出现大量If else的场景,代码显的很臃肿,非常不优雅,且难以维护。那我们有没有办法处理呢?

工厂模式+枚举

工厂模式
将操作进行抽象给出一个操作接口

1
2
3
public interface IOperation {
int apply(int a, int b);
}
1
然后实现加减乘除四个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Addition implements IOperation {

@Override
public int apply(int a, int b) {
return a + b;
}


public enum OperationEnum {
// 唯一枚举:
INSTANCE;

private Addition type = new Addition();

public Addition getType() {
return this.type;
}

public void setType(Addition type) {
this.type = type;
}
}
}

使用枚举

1
2
3
4
5
6
7
8
9
public enum OperatorEnum {
ADD(1, "加法", Addition.OperatorEnum.INSTANCE.getType())

private final int code;

private final String remark;

private final Operation operation;
}

使用

1
2
3
4
int code= 1
IOperation operation = OperatorEnum.valueOf(1).getOperation();
result = shelfType.handleRefundSuccess(reqDTO);

责任链模式

https://www.cnblogs.com/xrq730/p/10633761.html
https://www.cnblogs.com/java-my-life/archive/2012/05/28/2516865.html

命令模式

https://cloud.tencent.com/developer/article/1559667

规则引擎

https://cloud.tencent.com/developer/article/1559667

使用poi操作word

  1. maven依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- word, ppt, excel 文件的读取 -->
    <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.0.0</version>
    </dependency>
    <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>5.0.0</version>
    </dependency>
  2. 代码

    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
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.poi.xwpf.usermodel.*;
    import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;

    import java.io.*;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;

    /**
    * 相关服务接口
    *

    */
    @Slf4j
    public class WordGenerator {

    /**
    * 修改word到指定的目录
    * @param filename 源文件
    * @param data 模板数据
    * @param dest 目标目录
    * @throws IOException
    */
    public static void modifyWordToDest(File filename, Map<String, Object> data, String dest) throws IOException {
    FileInputStream is = new FileInputStream(filename.getAbsoluteFile());
    modifyWordToDest(is, filename.getName(), data, dest);
    }

    /**
    * 修改word到指定的目录
    * @param is 文件输入流
    * @param fileName 生成文件名
    * @param data 模板数据
    * @param dest 目标目录
    * @throws IOException
    */
    public static void modifyWordToDest(InputStream is, String fileName, Map<String, Object> data, String dest) throws IOException {
    String tmpFilePath = dest + File.separator + fileName;
    File dir = new File(dest);
    if (!dir.exists()) {
    dir.mkdirs();
    }
    FileOutputStream os = new FileOutputStream(tmpFilePath, false);
    try {
    XWPFDocument document = new XWPFDocument(is);

    // 替换段落里面的占位符
    replaceInPara(document, data);

    // 替换表格里的占位符
    tableSearchAndReplace(document, data);

    // 输出
    document.write(os);
    } catch (Exception e) {
    log.error("WordGenerator modifyWordToDest error, fileName:{}",fileName,e);
    } finally {
    close(is);
    close(os);
    }
    }

    /**
    * poi 查找word表格中占位符并替换
    * 表格占位符格式:${name}
    * @param document
    * @param data
    */
    public static final void tableSearchAndReplace(XWPFDocument document, Map<String, Object> data) {
    // 替换表格中的指定文字
    Iterator<XWPFTable> itTable = document.getTablesIterator();

    while (itTable.hasNext()) {
    XWPFTable table = (XWPFTable) itTable.next();

    // 动态处理表格中的list,动态新增行
    Iterator<Map.Entry<String, Object>> dataIterator = data.entrySet().iterator();
    while (dataIterator.hasNext()) {
    Map.Entry<String, Object> entry = dataIterator.next();
    if (entry.getValue() instanceof List) {
    int rcount = table.getNumberOfRows();
    for (int i = rcount - 1; i >= 0; i--) {
    XWPFTableRow row = table.getRow(i);
    boolean findListPara = false;
    List<XWPFTableCell> cells = row.getTableCells();
    for (XWPFTableCell cell : cells) {
    //表格中处理段落(回车)
    List<XWPFParagraph> cellParList= cell.getParagraphs();
    for (XWPFParagraph xwpfParagraph : cellParList) {
    findListPara = findListParaInPara(xwpfParagraph, entry.getKey());
    if (findListPara) {
    break;
    }
    }
    if (findListPara) {
    break;
    }
    }

    /** 处理找到list参数的这行数据 **/
    if (findListPara) {
    // 遍历dataMap,valueList
    List valueList = (List) entry.getValue();
    for (int j = 0; j < valueList.size(); j++) {
    // 设置map值
    ObjectMapper mapObject = new ObjectMapper();
    Map<String, Object> dataMap = mapObject.convertValue(valueList.get(j), Map.class);

    // copy模板行为新增行
    XWPFTableRow sourceRow = new XWPFTableRow((CTRow)table.getRow(i).getCtRow().copy(), table);
    // 新增行替换参数
    replaceInPara(sourceRow, dataMap);
    // 添加到模板行之后
    table.addRow(sourceRow, i + j + 1);

    if (j == valueList.size() - 1) {
    // 最后一次移除模板行
    table.removeRow(i);
    }
    }
    }
    }
    }
    }

    // 处理表格中的替代符
    int rcount = table.getNumberOfRows();
    for (int i = 0; i < rcount; i++) {
    replaceInPara(table.getRow(i), data);
    }
    }
    }

    private static void replaceInPara(XWPFTableRow row, Map<String, Object> data) {
    List<XWPFTableCell> cells = row.getTableCells();
    for (XWPFTableCell cell : cells) {
    //表格中处理段落(回车)
    List<XWPFParagraph> cellParList= cell.getParagraphs();
    for (XWPFParagraph xwpfParagraph : cellParList) {
    replaceInPara(xwpfParagraph, data);
    }
    }
    }

    /**
    * 替换段落里面的变量 段落占位符格式:${name}
    * @param doc
    * @param params
    */
    private static void replaceInPara(XWPFDocument doc, Map<String, Object> params) {
    Iterator<XWPFParagraph> iterator = doc.getParagraphsIterator();
    XWPFParagraph para;
    while (iterator.hasNext()) {
    para = iterator.next();
    replaceInPara(para, params);
    }
    }

    /**
    * 替换段落里面的变量
    * @param para
    * @param params
    */
    private static void replaceInPara(XWPFParagraph para, Map<String, Object> params) {
    List<XWPFRun> runs;
    Matcher matcher;
    String runText = "";
    int fontSize = 15; // 默认字号
    String fontFamily = "楷体"; // 默认字体
    boolean bold = false; // 默认不加粗

    if (matcher(para.getParagraphText()).find()) {
    runs = para.getRuns();
    if (runs.size() > 0) {
    int j = runs.size();
    for (int i = 0; i < j; i++) {
    XWPFRun run = runs.get(0);
    fontSize = run.getFontSize();
    fontFamily = run.getFontFamily();
    bold = run.isBold();
    String i1 = run.toString();
    runText += i1;
    // 删除
    para.removeRun(0);
    }
    }
    matcher = matcher(runText);

    if (matcher.find()) {
    while ((matcher = matcher(runText)).find()) {
    runText = matcher.replaceFirst(params.get(matcher.group(1)) != null ? String.valueOf(params.get(matcher.group(1))):"");
    }
    // 直接调用XWPFRun的setText()方法设置文本时,在底层会重新创建一个XWPFRun,把文本附加在当前文本后面,
    // 所以我们不能直接设值,需要先删除当前run,然后再自己手动插入一个新的run。
    XWPFRun xwpfRun = para.insertNewRun(0);
    xwpfRun.setBold(bold);
    xwpfRun.setFontSize(fontSize);
    xwpfRun.setFontFamily(fontFamily);
    xwpfRun.setText(runText);
    }
    }
    }
    /**
    * 查询段落里面是list类型的变量
    * @param para
    * @param key
    */
    private static boolean findListParaInPara(XWPFParagraph para, String key) {
    List<XWPFRun> runs;
    String runText = "";
    Matcher matcher;

    if (matcher(para.getParagraphText()).find()) {
    runs = para.getRuns();
    if (runs.size() > 0) {
    int j = runs.size();
    for (int i = 0; i < j; i++) {
    XWPFRun run = runs.get(i);
    runText += run.toString();
    }
    }

    matcher = matcher(runText);

    if (matcher.find()) {
    if (matcher.group(1).equals(key)) {
    return true;
    }
    }
    }
    return false;
    }
    /**
    * 正则匹配字符串
    *
    * @param str
    * @return
    */
    private static Matcher matcher(String str) {
    Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);
    Matcher matcher = pattern.matcher(str);
    return matcher;
    }

    private static void close(InputStream is) {
    if (is != null) {
    try {
    is.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    private static void close(OutputStream os) {
    if (os != null) {
    try {
    os.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }
    ```
    3. 准备模板文件
    ![docx模板](../image/javaWordTemplate.jpg)

    4. 结果
    ![生成的目录](../image/javaWordFolder.jpg)
    ![目录内容](../image/javaWordResult.jpg)

    ## 遇到的问题及相应的解决方案

    ### jar启动项目遇到的问题
    1. 获取resource目录
    正确方式:

    getClass().getClassLoader().getResourceAsStream(“doc/a.docx”)

    1
    2

    错误方式:

    getClass().getClassLoader().getResource(“doc/a.docx”)

    1
    2
    3
    错误方式在jar包启动的项目中会有问题,具体原因可以网上查下原因,不展开

    2. 获取jar包所在目录路径

    ApplicationHome h = new ApplicationHome(getClass());
    File jarF = h.getSource();
    String sysResPath = jarF.getParentFile().toString();

    [参考](https://blog.csdn.net/liangcha007/article/details/88526181)
  3. getResourceAsStream碰到中文目录
    在本地启动过程时,文件目录采用中文命名(注:不是文件里面的中文内容),例如:new File(“目录/测试.docx”) ,没有任何问题,可以正常读取到。但部署到服务器上通过jar启动,则提示java.io.FileNotFoundException,暂时还未找到解决办法,如果有人了解,可以联系我,一起讨论下。

word中表格动态新增行

可以参考代码

NIO

  1. 三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

原生NIO存在的问题

  1. NIO的类库和API繁琐,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、ServerSocket、ByteBuffer等。
  2. 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
  3. 开发工作量和难度都非常大:例如客户端面临断线重连、网络闪断、半包读写、失败缓存、网络堵塞和异常流的处理等等。
  4. JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。

线程模型

单 Reactor 模式

单 Reactor 单线程

NIO 群聊

单 Reactor 多线程


优点:可以充分的利用多核cpu的处理能力。
缺点:多线程数据共享和访问比较复杂,reactor 处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈。

主从 Reactor 多线程

  1. Reactor 主线程 MainReactor 对象通过 select 监听链接事件,收到时间后,通过 Acceptor 处理链接事件。
  2. 当 Acceptor 处理链接事件后,MainReactor 将连接分配给 SubReactor.
  3. subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理。
  4. 当有新事件发生时,subreactor 就会调用对应的 handler 处理。
  5. handler 通过 read 读取数据,分发给后面的 worker 线程处理。
  6. worker 线程池分配独立的 worker 线程进行业务处理,并返回结果。
  7. handler 收到响应的结果后,再通过 send 将结果返回给client。
  8. Reactor 主线程可以对应多个 Reactor 子线程,即 MainReactor 可以关联多个 subreactor

方案优缺点

优点:

  1. 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
  2. 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。

缺点:

  1. 编程复杂度较高

结合实例:这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

Scalable IO in Java 书中作者 Doug Lea 对 Reactor 模型的理解

Netty 模型

工作原理示意图
工作原理示意图-解析

  1. Netty抽象出两组线程池BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写。
  2. BossGroup和WorkerGroup类型都是NioEventLoopGroup
  3. NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环是NioEventLoop
  4. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
  5. NioEventLoopGroup 可以有多个线程,即可以含有多个NioEventLoop
  6. 每个Boss NioEventLoop 执行的步骤有3步:
    1. 轮询accept事件
    2. 处理accept事件,与client建立连接,生成 NioSocketChannel,并将其注册到某个worker NIOEventLoop 上的selector
    3. 处理任务队列的任务,即 runAllTasks
  7. 每个Worker NIOEventLoop 循环执行的步骤
    1. 轮询read,write事件
    2. 处理i/o事件,即read,write事件,在对应 NioSocketChannel 处理
    3. 处理任务队列的任务,即 runAllTasks
  8. 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道),pipeline 中包含了 channel,即通过pipeline 可以获取到对应通道,管道中维护了很多的处理器。

Protobuf

http + json -> tcp + protobuf
Protobuf优点:

  1. 序列化后体积相比Json和XML很小,适合网络传输
  2. 支持跨平台多语言
  3. 消息格式升级和兼容性还不错
  4. 序列化反序列化速度很快,快于Json的处理速速

Protobuf缺点:

  1. 二进制格式导致可读性差
  2. 缺乏自描述
  3. 通用性差

文档

Handler

  1. 不论解码器handler还是编码器handler,接收的消息类型必须与待处理的消息类型一致,否则该handler不会被执行
  2. 在解码器进行数据解码时,需要判断缓存区(ByteBuf)的数据是否足够,否则接收到的结果与期望结果可能不一致

语言指南

参考资料

课件
笔记
https://cloud.tencent.com/developer/article/1754078

以太坊客户端安装

以太坊客户端介绍

我们选择其中一个主流的客户端go-ethereum(Geth)进行安装。以Windows为例:
执行:
pkg install go-ethereum

其他环境安装

链搭建

以搭建私链为例:

  1. 新建目录(eth-pri)

  2. 执行启动命令

    1
    geth --datadir ether-pri --rpcapi="db,eth,net,web3,miner,personal,web3" --http --dev --dev.period 10 --http.corsdomain "https://remix.ethereum.org,http://remix.ethereum.org"

    参数说明

  3. 连接客户端

    1
    geth attach http://localhost:8545
  4. 命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    账号
    创建:personal.newAccount()
    获取账号:eth.accounts
    获取余额:eth.getBalance("sssss")
    解锁账号:personal.unlockAccount(user1,password)

    挖矿
    开始:miner.start()
    结束:miner.stop()
    设置挖矿奖励用户:miner.setEtherbase(user3)

    转账
    u1 = eth.accounts[0]
    u2 = eth.accounts[1]
    eth.sendTransaction({from:u1, to:u2, value:web3.toWei(10, "ether")})

    交易前解锁账号
    personal.unlockAcount(u1, password)

    交易完需要挖矿来确认

    其他命令操作 可以参考Web3js

合约

合约开发

solidity英文开发文档
solidity中文开发文档

(合约调用的四种方式](https://blog.csdn.net/TurkeyCock/article/details/83826531)
library:公共代码复用,不允许定义任何storage类型的变量,不能修改合约的状态。

合约编译、部署

Remix

remix访问本地:

  1. uninstall the old one: npm uninstall -g remixd
  2. install the new: npm install -g @remix-project/remixd
  3. share folder: remixd -s <absolute-path> --remix-ide https://remix.ethereum.org

Truffle

文档
Truffle、Solidity合约项目Demo

  1. 初始化truffle项目:
    1
    2
    npm init -y
    truffle init
  2. solidity安装外部依赖:
    1
    2
    // ERC20依赖
    npm install @openzeppelin/contracts

合约调试

Buidler内置了Buidler EVM ,这是一个专为开发而设计的以太坊网络。它允许你部署合约,运行测试和调试代码。可以将合约部署到buidler-evm上,调试完再部署到实际使用的链上。
使用步骤:

  1. 启动(每次启动貌似起个新链,之前部署的合约都不存在了)
    npx buidler node
  2. 合约项目下载Buidler依赖
    npm install @nomiclabs/buidler
  3. 合约引入console依赖
    import "@nomiclabs/buidler/console.sol";
  4. 使用日志打印
    console.log("value:", 11);
    注:
    支持的solidity版本 < 0.8.0,如果solidity版本>=0.8.0,可以修改console源码,把byte类型相关去除

其他资料:
https://cloud.tencent.com/developer/article/1679411
https://www.blockvalue.com/news/2019100619910.html

前端开发

Demo地址
Vue + Element-Ui + Web3js

  1. 创建Vue项目
    1
    2
    3
    4
    安装vue:npm install -g vue-cli
    初始化vue项目:vue init webpack my-project
    安装web3:npm install web3@1.3.1
    启动项目:npm run dev

Java调用合约

  1. pom.xml引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>org.web3j</groupId>
    <artifactId>core</artifactId>
    <version>4.8.4</version>
    </dependency>

    <dependency>
    <groupId>org.web3j</groupId>
    <artifactId>codegen</artifactId>
    <version>4.8.4</version>
    </dependency>
  2. 通过abi文件生成java类

    1
    2
    3
    4
    private static void transformABI() throws Exception {
    //org.web3j.codegen.TruffleJsonFunctionWrapperGenerator /path/to/<truffle-smart-contract-output>.json -o /path/to/src/main/java -p com.your.organisation.name
    TruffleJsonFunctionWrapperGenerator.main(new String[]{"D:\\developtools\\truffle\\workspace\\truffleTest\\build\\contracts\\GuessVote.json","-o","D:/git/netease-leihuo/ethereum-test/src/main/java","-p","com.cjf.web3j.abi"});
    }
  3. 合约中方法调用

    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
    // 默认链接到,或者自己指定 http://localhost:8545/
    Web3j web3 = Web3j.build(new HttpService());
    //获取链接到的client的版本
    Web3ClientVersion web3ClientVersion = web3.web3ClientVersion().send();
    String clientVersion = web3ClientVersion.getWeb3ClientVersion();
    System.out.println(clientVersion);

    //获取当前gasprice
    EthGasPrice gasPrice = web3.ethGasPrice().send();

    BigInteger priceResult = gasPrice.getGasPrice();
    System.out.println("current gas price:" + priceResult);

    //获取块的gasLimit
    EthBlock.Block block = web3.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).send().getBlock();
    System.out.println("BLOCK GAS LIMIT:" + block.getGasLimit());

    StaticGasProvider gasProvider = new StaticGasProvider(priceResult, block.getGasLimit());

    //使用私钥创建Credentials
    Credentials owner = Credentials.create("****");

    //需要指定chainId来创建TransactionManager ,否则交易会因为防重放攻击而无法被执行
    TransactionManager transactionManager = new RawTransactionManager(web3, owner, 1337);

    //获取一个链上已经存在的合约
    GuessVote guessVote = GuessVote.load(GuessVote.getPreviouslyDeployedAddress("1337"), web3, transactionManager, gasProvider);

    // 调用合约中的方法
    TransactionReceipt s = guessVote.generateVoteResult().send();
    System.out.println(s);

MetaMask钱包

MetaMask文档
安装:npm install metamask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取最新区块
const newestBlockNum = parseInt(
await ethereum.request({
method: "eth_blockNumber",
params: [],
}),
16
);

// 获取账号
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});

// 获取账号余额
const balance = await this.usstContract.methods
.balanceOf(this.account)
.call();

部署到测试链

币安测试链

文档
水龙头相关文档
水龙头其他文档

注:ip限制,充值不上换代理。

部署测试链步骤:

  1. 修改truffle-config.js
    1
    2
    3
    4
    const HDWalletProvider = require('@truffle/hdwallet-provider');

    const fs = require('fs');
    const mnemonic = fs.readFileSync(".secret").toString().trim();
    networks增加配置
    1
    2
    3
    4
    5
    6
    7
    testnet: {
    provider: () => new HDWalletProvider(mnemonic, `https://data-seed-prebsc-2-s1.binance.org:8545`),
    network_id: 97,
    confirmations: 10,
    timeoutBlocks: 200,
    skipDryRun: true
    }
  2. 部署命令
    truffle migrate --network testnet

火币测试链

官网地址

1
2
3
4
测试网地址:
chainid 256
RPC wss://ws-testnet.hecochain.com
https://http-testnet.hecochain.com

浏览器
测试币水龙头

FAQ

  1. Exception in thread “main” java.lang.UnsupportedOperationException: Unsupported type encountered: tuple
    1
    https://stackoverflow.com/questions/48877910/how-can-i-return-an-array-of-struct-in-solidity
  2. 合约多字段返回
    java:元组
    js:struct[],调用call方法需要传account
    参考文档

为什么需要搜索引擎?

用数据库,也可以实现搜索的功能,为什么还需要搜索引擎呢?

就像 Stackoverflow 的网友说的:

A relational database can store data and also index it. A search engine can index data but also store it.

数据库(理论上来讲,ES 也是数据库,这里的数据库,指的是关系型数据库),首先是存储,搜索只是顺便提供的功能,

而搜索引擎,首先是搜索,但是不把数据存下来就搜不了,所以只好存一存。

术业有专攻,专攻搜索的搜索引擎,自然会提供更强大的搜索能力。

搜索引擎凭什么比关系型数据库查询快?

Elasticsearch 是通过 Lucene 的倒排索引技术实现比关系型数据库更快的过滤。特别是它对多条件的过滤支持非常好,比如年龄在 18 和 30 之间,性别为女性这样的组合查询。倒排索引很多地方都有介绍,

笼统的来说,b-tree 索引是为写入优化的索引结构。当我们不需要支持快速的更新的时候,可以用预先排序等方式换取更小的存储空间,更快的检索速度等好处,其代价就是更新慢。要进一步深入的化,还是要看一下 Lucene 的倒排索引是怎么构成的。

先看倒排索引的构成

这里对应的名字概念,举个实际的例子

docid age sex
1 18
2 20
3 18

这里每一行是一个 document。每个 document 都有一个 docid。那么给这些 document 建立的倒排索引就是:age和sex

可以看到,倒排索引是 per field 的,一个字段有一个自己的倒排索引。18,20 这些叫做 term,而 [1,3] 就是 posting list。Posting list 就是一个 int 的数组,存储了所有符合某个 term 的文档 id。那么什么是 term dictionary 和 term index?

假设我们有很多个 term,比如:

Carla,Sara,Elin,Ada,Patty,Kate,Selena

如果按照这样的顺序排列,找出某个特定的 term 一定很慢,因为 term 没有排序,需要全部过滤一遍才能找出特定的 term。排序之后就变成了:

Ada,Carla,Elin,Kate,Patty,Sara,Selena

这样我们可以用二分查找的方式,比全遍历更快地找出目标的 term。这个就是 term dictionary。有了 term dictionary 之后,可以用 logN 次磁盘查找得到目标。但是磁盘的随机读操作仍然是非常昂贵的(一次 random access 大概需要 10ms 的时间)。所以尽量少的读磁盘,有必要把一些数据缓存到内存里。但是整个 term dictionary 本身又太大了,无法完整地放到内存里。于是就有了 term index。term index 有点像一本字典的目录。比如:

A 开头的 term ……………. Xxx 页

C 开头的 term ……………. Xxx 页

E 开头的 term ……………. Xxx 页

实际的 term index 是一棵 trie 树:

例子是一个包含 “A”, “to”, “tea”, “ted”, “ten”, “i”, “in”, 和 “inn” 的 trie 树。这棵树不会包含所有的 term,它包含的是 term 的一些前缀。通过 term index 可以快速地定位到 term dictionary 的某个 offset,然后从这个位置再往后顺序查找。再加上一些压缩技术(搜索 Lucene Finite State Transducers) term index 的尺寸可以只有所有 term 的尺寸的几十分之一,使得用内存缓存整个 term index 变成可能。整体上来说就是这样的效果。

现在我们可以回答“为什么 Elasticsearch/Lucene 检索可以比 mysql 快了。Mysql 只有 term dictionary 这一层,是以 b-tree 排序的方式存储在磁盘上的。检索一个 term 需要若干次的 random access 的磁盘操作。而 Lucene 在 term dictionary 的基础上添加了 term index 来加速检索,term index 以树的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数。

额外值得一提的两点是:term index 在内存中是以 FST(finite state transducers)的形式保存的,其特点是非常节省内存。Term dictionary 在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 term dictionary 可以比 b-tree 更节约磁盘空间。

如何联合索引查询?

所以给定查询过滤条件 age=18 的过程就是先从 term index 找到 18 在 term dictionary 的大概位置,然后再从 term dictionary 里精确地找到 18 这个 term,然后得到一个 posting list 或者一个指向 posting list 位置的指针。然后再查询 gender= 女 的过程也是类似的。最后得出 age=18 AND gender= 女 就是把两个 posting list 做一个“与”的合并。

这个理论上的“与”合并的操作可不容易。对于 mysql 来说,如果你给 age 和 gender 两个字段都建立了索引,查询的时候只会选择其中最 selective 的来用,然后另外一个条件是在遍历行的过程中在内存中计算之后过滤掉。那么要如何才能联合使用两个索引呢?有两种办法:

  • 使用 skip list 数据结构。同时遍历 gender 和 age 的 posting list,互相 skip;
  • 使用 bitset 数据结构,对 gender 和 age 两个 filter 分别求出 bitset,对两个 bitset 做 AN 操作。

PostgreSQL 从 8.4 版本开始支持通过 bitmap 联合使用两个索引,就是利用了 bitset 数据结构来做到的。当然一些商业的关系型数据库也支持类似的联合索引的功能。Elasticsearch 支持以上两种的联合索引方式,如果查询的 filter 缓存到了内存中(以 bitset 的形式),那么合并就是两个 bitset 的 AND。如果查询的 filter 没有缓存,那么就用 skip list 的方式去遍历两个 on disk 的 posting list。

利用 Skip List 合并

以上是三个 posting list。我们现在需要把它们用 AND 的关系合并,得出 posting list 的交集。首先选择最短的 posting list,然后从小到大遍历。遍历的过程可以跳过一些元素,比如我们遍历到绿色的 13 的时候,就可以跳过蓝色的 3 了,因为 3 比 13 要小。

整个过程如下

1
2
3
4
5
6
7
8
9
10
Next -> 2
Advance(2) -> 13
Advance(13) -> 13
Already on 13
Advance(13) -> 13 MATCH!!!
Next -> 17
Advance(17) -> 22
Advance(22) -> 98
Advance(98) -> 98
Advance(98) -> 98 MATCH!!!

最后得出的交集是 [13,98],所需的时间比完整遍历三个 posting list 要快得多。但是前提是每个 list 需要指出 Advance 这个操作,快速移动指向的位置。什么样的 list 可以这样 Advance 往前做蛙跳?skip list:

从概念上来说,对于一个很长的 posting list,比如:

[1,3,13,101,105,108,255,256,257]

我们可以把这个 list 分成三个 block:

[1,3,13] [101,105,108] [255,256,257]

然后可以构建出 skip list 的第二层:

[1,101,255]

1,101,255 分别指向自己对应的 block。这样就可以很快地跨 block 的移动指向位置了。

Lucene 自然会对这个 block 再次进行压缩。其压缩方式叫做 Frame Of Reference 编码。示例如下:

考虑到频繁出现的 term(所谓 low cardinality 的值),比如 gender 里的男或者女。如果有 1 百万个文档,那么性别为男的 posting list 里就会有 50 万个 int 值。用 Frame of Reference 编码进行压缩可以极大减少磁盘占用。这个优化对于减少索引尺寸有非常重要的意义。当然 mysql b-tree 里也有一个类似的 posting list 的东西,是未经过这样压缩的。

因为这个 Frame of Reference 的编码是有解压缩成本的。利用 skip list,除了跳过了遍历的成本,也跳过了解压缩这些压缩过的 block 的过程,从而节省了 cpu。

利用 bitset 合并

Bitset 是一种很直观的数据结构,对应 posting list 如:

[1,3,4,7,10]

对应的 bitset 就是:

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

每个文档按照文档 id 排序对应其中的一个 bit。Bitset 自身就有压缩的特点,其用一个 byte 就可以代表 8 个文档。所以 100 万个文档只需要 12.5 万个 byte。但是考虑到文档可能有数十亿之多,在内存里保存 bitset 仍然是很奢侈的事情。而且对于个每一个 filter 都要消耗一个 bitset,比如 age=18 缓存起来的话是一个 bitset,18<=age<25 是另外一个 filter 缓存起来也要一个 bitset。

所以秘诀就在于需要有一个数据结构:

  • 可以很压缩地保存上亿个 bit 代表对应的文档是否匹配 filter;
  • 这个压缩的 bitset 仍然可以很快地进行 AND 和 OR 的逻辑操作。

Lucene 使用的这个数据结构叫做 Roaring Bitmap。

其压缩的思路其实很简单。与其保存 100 个 0,占用 100 个 bit。还不如保存 0 一次,然后声明这个 0 重复了 100 遍。

这两种合并使用索引的方式都有其用途。Elasticsearch 对其性能有详细的对比( https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps )。简单的结论是:因为 Frame of Reference 编码是如此 高效,对于简单的相等条件的过滤缓存成纯内存的 bitset 还不如需要访问磁盘的 skip list 的方式要快。

如何减少文档数?

一种常见的压缩存储时间序列的方式是把多个数据点合并成一行。Opentsdb 支持海量数据的一个绝招就是定期把很多行数据合并成一行,这个过程叫 compaction。类似的 vivdcortext 使用 mysql 存储的时候,也把一分钟的很多数据点合并存储到 mysql 的一行里以减少行数。

这个过程可以示例如下:

12:05:00 10
12:05:01 15
12:05:02 14
12:05:03 16

合并之后就变成了:

可以看到,行变成了列了。每一列可以代表这一分钟内一秒的数据。

Elasticsearch 有一个功能可以实现类似的优化效果,那就是 Nested Document。我们可以把一段时间的很多个数据点打包存储到一个父文档里,变成其嵌套的子文档。示例如下:

1
2
3
{timestamp:12:05:01, idc:sz, value1:10,value2:11}
{timestamp:12:05:02, idc:sz, value1:9,value2:9}
{timestamp:12:05:02, idc:sz, value1:18,value:17}

可以打包成:

1
2
3
4
5
6
7
8
{
max_timestamp:12:05:02, min_timestamp: 1205:01, idc:sz,
records: [
{timestamp:12:05:01, value1:10,value2:11}
{timestamp:12:05:02, value1:9,value2:9}
{timestamp:12:05:02, value1:18,value:17}
]
}

这样可以把数据点公共的维度字段上移到父文档里,而不用在每个子文档里重复存储,从而减少索引的尺寸。

(图片来源: https://www.youtube.com/watch?v=Su5SHc_uJw8 ,Faceting with Lucene Block Join Query)

在存储的时候,无论父文档还是子文档,对于 Lucene 来说都是文档,都会有文档 Id。但是对于嵌套文档来说,可以保存起子文档和父文档的文档 id 是连续的,而且父文档总是最后一个。有这样一个排序性作为保障,那么有一个所有父文档的 posting list 就可以跟踪所有的父子关系。也可以很容易地在父子文档 id 之间做转换。把父子关系也理解为一个 filter,那么查询时检索的时候不过是又 AND 了另外一个 filter 而已。前面我们已经看到了 Elasticsearch 可以非常高效地处理多 filter 的情况,充分利用底层的索引。

使用了嵌套文档之后,对于 term 的 posting list 只需要保存父文档的 doc id 就可以了,可以比保存所有的数据点的 doc id 要少很多。如果我们可以在一个父文档里塞入 50 个嵌套文档,那么 posting list 可以变成之前的 1/50。

参考

时间序列数据库的秘密 (2)——索引:https://www.infoq.cn/article/database-timestamp-02/

为什么需要 Elasticsearch:https://zhuanlan.zhihu.com/p/73585202

前言

本文是个人的笔记记录,非详细的文档介绍。

GitHub创建个人仓库

点击GitHub中的New repository创建新仓库,仓库名应该为:用户名.http://github.io 这个用户名使用你的GitHub帐号名称代替,这是固定写法,比如我的仓库名为:ChenJFMorton.github.io

安装Git

过程略

安装Node.js

Hexo基于Node.js,Node.js下载地址:Download | Node.js 下载安装包,注意安装Node.js会包含环境变量及npm的安装,安装后,检测Node.js是否安装成功,在命令行中输入 node -v

安装Hexo

Hexo就是我们的个人博客网站的框架, 这里需要自己在电脑常里创建一个文件夹,可以命名为Blog,Hexo框架与以后你自己发布的网页都在这个文件夹中。

进入文件夹中,使用npm命令安装Hexo,输入:

1
npm install -g hexo-cli 

这个安装时间较长耐心等待,安装完成后,初始化我们的博客,输入:

1
2
3
4
hexo init blog
cd blog
npm install
hexo s

发布新文章

hexo g -d

前言

本文是针对有一定Java虚拟机基础知识后,在实际工作中解决JVM相关问题及如何进行优化,偏方法论。

Java内存区域及收集器介绍

jdk1.7及之前

jdk1.8

1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存

垃圾收集器:

收集算法、垃圾收集器详细介绍:略,待补充。

垃圾收集从2个方面不断的改进:提高垃圾回收效率;提高用户体验(低停顿);

HotSpot虚拟机的垃圾收集器(介绍见参考一)
## JVM调优目标
  • 一、何时需要做jvm调优?

    ​ 1、heap 内存(老年代)持续上涨达到设置的最大内存值;

    ​ 2、Full GC 次数频繁;

    ​ 3、GC 停顿时间过长(超过1秒);

    ​ 4、应用出现OutOfMemory 等内存异常;

    ​ 5、应用中有使用本地缓存且占用大量内存空间;

    ​ 6、系统吞吐量与响应性能不高或下降。

  • 二、 JVM调优原则

    ​ 1、多数的Java应用不需要在服务器上进行JVM优化;

    ​ 2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;

    ​ 3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);

    ​ 4、减少创建对象的数量;

    ​ 5、减少使用全局变量和大对象;

    ​ 6、JVM优化是到最后不得已才采用的手段;

    ​ 7、在实际使用中,分析GC情况优化代码比优化JVM参数更好;

  • 三、 JVM调优目标

    ​ 1、GC低停顿;

    ​ 2、GC低频率;

    ​ 3、低内存占用;

    ​ 4、高吞吐量;

JVM调优参数简介

堆设置

参数 说明
-Xmn1200m 设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-Xms4g 初始化堆内存大小为4GB
-Xmx4g 堆内存最大值为4GB
-Xss512k 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
-XX:MaxDirectMemorySize 堆外内存/直接内存的大小,默认为堆内存减去一个Survivor区的大小
-XX:MaxMetaspaceSize=512m 设置元数据区最大值512M(jdk1.8
-XX:MaxPermSize=256m 设置持久代大小为256MB。(jdk1.7及以下
-XX:MaxTenuringThreshold=15 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论
-XX:MetaspaceSize=128m 设置元数据区初始值128M(jdk1.8
-XX:NewRatio=4 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:PermSize=100m 初始化永久代大小为100MB。(jdk1.7及以下
-XX:ReservedCodeCacheSize JIT编译后二进制代码的存放区,满了之后就不再编译。默认开多层编译240M,可以在JMX里看看CodeCache的大小
-XX:SurvivorRatio=8 设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

GC设置

参数 说明
-XX:+HeapDumpOnOutOfMemoryError 发生内存溢出是进行heap-dump
-XX:HeapDumpPath=/path/to/java_pid.hprof 这个参数与-XX:+HeapDumpOnOutOfMemoryError共同作用,设置heap-dump时内容输出文件
-XX:ErrorFile=/path/to/hs_err_pid.log 指定致命错误日志位置。一般在JVM发生致命错误时会输出类似hs_err_pid.log的文件,默认是在工作目录中(如果没有权限,会尝试在/tmp中创建),不过还是自己指定位置更好一些,便于收集和查找,避免丢失
-XX:StringTableSize=1000003 指定字符串常量池大小,默认值是60013。对Java稍微有点常识的应该知道,字符串是常量,创建之后就不可修改了,这些常量所在的地方叫做字符串常量池。如果自己系统中有很多字符串的操作,且这些字符串值比较固定,在允许的情况下,可以适当调大一些池子大小

GC日志

GC过程可以通过GC日志来提供优化依据。

参数 说明
-XX:+PrintGCDetails 启用gc日志打印功能
-Xloggc:/path/to/gc.log 指定gc日志位置
-XX:+PrintHeapAtGC 打印GC前后的详细堆栈信息
-XX:+PrintGCDateStamps 打印可读的日期而不是时间戳
-XX:+PrintGCApplicationStoppedTime 打印所有引起JVM停顿时间,如果真的发现了一些不知什么的停顿,再临时加上-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1找原因
-XX:+PrintGCApplicationConcurrentTime 打印JVM在两次停顿之间正常运行时间,与-XX:+PrintGCApplicationStoppedTime一起使用效果更佳
-XX:+PrintTenuringDistribution 查看每次minor GC后新的存活周期的阈值
-XX:+UseGCLogFileRotation 与 -XX:NumberOfGCLogFiles=10 与 -XX:GCLogFileSize=10M GC日志在重启之后会清空,但是如果一个应用长时间不重启,那GC日志会增加,所以添加这3个参数,是GC日志滚动写入文件,但是如果重启,可能名字会出现混乱
-XX:PrintFLSStatistics=1 打印每次GC前后内存碎片的统计信息

收集器

参数 说明1 说明2
-XX:+UseConcMarkSweepGC 设置CMS收集器 (只针对老年代) 并发收集、低停顿;对CPU资源敏感,CMS默认启动的回收线程数是(CPU数量+3)/4;
-XX:+UseParNewGC 设置年轻代为多线程收集 可以不设置,jdk 8中设置-XX:+UseConcMarkSweepGC,自动启用-XX:+UseParNewGC
-XX:+CMSClassUnloadingEnabled 配合-XX:+UseConcMarkSweepGC使用,垃圾回收会清理持久代,移除不再使用的classes
-XX:+ UseG1GC 允许使用垃圾优先(G1)垃圾收集器。它是一个服务器式垃圾收集器,针对具有大量RAM的多处理器计算机。它以高概率满足GC暂停时间目标,同时保持良好的吞吐量。 G1收集器推荐用于需要大堆(大小约为6 GB或更大)且GC延迟要求有限的应用(稳定且可预测的暂停时间低于0.5秒)。
G1其他参数详情请见参考一

其他参数设置

参数 说明
-ea 启用断言,这个没有什么好说的,可以选择启用,或这选择不启用,没有什么大的差异。完全根据自己的系统进行处理
-XX:+UseThreadPriorities 启用线程优先级,主要是因为我们可以给予周期性任务更低的优先级,以避免干扰客户端工作。在我当前的环境中,是默认启用的
-XX:ThreadPriorityPolicy=42 允许降低线程优先级
-XX:+HeapDumpOnOutOfMemoryError 发生内存溢出是进行heap-dump
-XX:HeapDumpPath=/path/to/java_pid.hprof 这个参数与-XX:+HeapDumpOnOutOfMemoryError共同作用,设置heap-dump时内容输出文件
-XX:ErrorFile=/path/to/hs_err_pid.log 指定致命错误日志位置。一般在JVM发生致命错误时会输出类似hs_err_pid.log的文件,默认是在工作目录中(如果没有权限,会尝试在/tmp中创建),不过还是自己指定位置更好一些,便于收集和查找,避免丢失
-XX:StringTableSize=1000003 指定字符串常量池大小,默认值是60013。对Java稍微有点常识的应该知道,字符串是常量,创建之后就不可修改了,这些常量所在的地方叫做字符串常量池。如果自己系统中有很多字符串的操作,且这些字符串值比较固定,在允许的情况下,可以适当调大一些池子大小
-XX:+AlwaysPreTouch 在启动时把所有参数定义的内存全部捋一遍。使用这个参数可能会使启动变慢,但是在后面内存使用过程中会更快。可以保证内存页面连续分配,新生代晋升时不会因为申请内存页面使GC停顿加长。通常只有在内存大于32G的时候才会有感觉
-XX:-UseBiasedLocking 禁用偏向锁(在存在大量锁对象的创建且高度并发的环境下(即非多线程高并发应用)禁用偏向锁能够带来一定的性能优化)
-XX:AutoBoxCacheMax=20000 增加数字对象自动装箱的范围,JDK默认-128~127的int和long,超出范围就会即时创建对象,所以,增加范围可以提高性能,但是也是需要测试
-XX:-OmitStackTraceInFastThrow 不忽略重复异常的栈,这是JDK的优化,大量重复的JDK异常不再打印其StackTrace。但是如果系统是长时间不重启的系统,在同一个地方跑了N多次异常,结果就被JDK忽略了,那岂不是查看日志的时候就看不到具体的StackTrace,那还怎么调试,所以还是关了的好
-XX:+PerfDisableSharedMem 启用标准内存使用。JVM控制分为标准或共享内存,区别在于一个是在JVM内存中,一个是生成/tmp/hsperfdata_{userid}/{pid}文件,存储统计数据,通过mmap映射到内存中,别的进程可以通过文件访问内容。通过这个参数,可以禁止JVM写在文件中写统计数据,代价就是jps、jstat这些命令用不了了,只能通过jmx获取数据。但是在问题排查是,jps、jstat这些小工具是很好用的,比jmx这种很重的东西好用很多,所以需要自己取舍。这里有个GC停顿的例子
-Djava.net.preferIPv4Stack=true 这个参数是属于网络问题的一个参数,可以根据需要设置。在某些开启ipv6的机器中,通过InetAddress.getLocalHost().getHostName()可以获取完整的机器名,但是在ipv4的机器中,可能通过这个方法获取的机器名不完整,可以通过这个参数来获取完整机器名

JVM如何调优

步骤

​ 第1步:分析GC日志(详见参考四、五)及dump文件,判断是否需要优化,确定瓶颈问题点;

第2步:确定JVM调优量化目标;

​ 第3步:确定JVM调优参数(根据历史JVM参数来调整);

  • 调优步骤:
    1、确定老年代内存占用大小,使用以下命令, concurrent mark-sweep generation项对应的used就是老年代的内存占用
    1
    jmap -heap 【pid】
    2、设置堆内存大小,-Xms、-Xmx,建议-Xms=-Xmx,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍,参考Xms和Xmx参数设置为相同值好处
    3、设置年轻代内存大小,-Xmn,建议整个堆3/8
    4、设置元空间大小,-XX:MetaspaceSize、-XX:MaxMetaspaceSize,建议MetaspaceSize和MaxMetaspaceSize设置一样大,具体设置多大,建议稳定运行一段时间后通过jstat -gc pid确认且这个值大一些,对于大部分项目256m即可,参考MetaspaceSize的误解Metaspace解密

​ 第4步:调优一台服务器,对比观察调优前后的差异;

​ 第5步:不断的分析和调整,直到找到合适的JVM参数配置;

​ 第6步:找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。

命令

jps:查询java进程

jstat:虚拟机运行状态信息

jmap:生成堆存储快照

jstack:生成虚拟机当前时刻的线程快照,帮助定位线程出现长时间停顿的原因

参考:https://www.jianshu.com/p/c6a04c88900a

实例

不同的环境有不同的方案,可以参考,不过还是建议在自己的环境中有针对的验证之后再使用,毕竟各自的环境都有差异。

gc查看

查看gc.log状态,可参考 参考四、参考五

结论:没有Full GC;每次从年轻代移到了老年代的内存占老年代总内存比例并不大:

查看star.hprof文件(使用JProfiler),如果有的话;

查询某一时刻虚拟机运行状态;

./jdk/jdk1.8.0_101/bin/jstat -gc 【pid】 3600s

参数 说明 设置的值
S0C Survivor0空间的大小。单位KB。 316992.0(0.3G)
S1C Survivor1空间的大小。单位KB。 316992.0(0.3G)
S0U Survivor0已用空间的大小。单位KB。 0
S1U Survivor1已用空间的大小。单位KB。 17521.7
EC Eden空间的大小。单位KB。 2536320.0(2.4G)
EU Eden已用空间的大小。单位KB。
OC 老年代空间的大小。单位KB。 3121152.0(3G)
OU 老年代已用空间的大小。单位KB。 221391(0.21G)
MC 方法区大小 125824.0(0.12G)
MU 方法区使用大小 119041(0.11G)
CCSC 压缩类空间大小 13824.0(0.01G)
CCSU 压缩类空间使用大小 12536
YGC 年轻代垃圾回收次数 261
YGCT 年轻代垃圾回收消耗时间 10.931(0.04s/次)
FGC 老年代垃圾回收次数 0
FGCT 老年代垃圾回收消耗时间 0
GCT 垃圾回收消耗总时间 10.931

查看系统jvm参数状态

1
jinfo -flags 【pid】 查看jvm的所有设置参数

机器状况

内存 CPU 磁盘空间
8G 4*4 60G

当前jvm参数配置:

1
-Xms6144m -Xmx6144m -XX:MaxPermSize=256m -verbose:gc -XX:+PrintGCDetails -Xloggc:/../tomcat/logs/gc.log -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -Xmn3096m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+ExplicitGCInvokesConcurrent -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/../tomcat/logs/star.hprof 
参数 说明 建议
-Xms6144m 初始化堆内存大小为6GB 低于系统内存
-Xmx6144m 堆内存最大值为6GB 低于系统内存
-XX:MaxPermSize=256m 设置永久代最大为256MB(无用) jdk1.8不需要设置
-verbose:gc 输出虚拟机中GC的详细情况
-XX:+PrintGCDetails 输出虚拟机中GC的详细情况
-Xloggc:/../tomcat/logs/gc.log 设置gc日志文件位子
-XX:+PrintGCTimeStamps 打印gc时间时间戳,gc.log
-XX:MetaspaceSize=256m 设置元数据区初始值256M (永久代) 该值越大触发Full GC的时机就越晚,
-XX:MaxMetaspaceSize=256m 设置元数据区最大值256M (永久代)
-Xmn3096m 设置年轻代区域3G Sun官方推荐配置为整个堆的3/8
-XX:+UseConcMarkSweepGC 设置CMS收集器 (只针对老年代) 并发收集、低停顿;对CPU资源敏感,CMS默认启动的回收线程数是(CPU数量+3)/4;
-XX:+UseParNewGC 设置年轻代为多线程收集 可以不设置,jdk 8中设置-XX:+UseConcMarkSweepGC,自动启用-XX:+UseParNewGC
-XX:+CMSClassUnloadingEnabled 配合-XX:+UseConcMarkSweepGC使用,垃圾回收会清理持久代,移除不再使用的classes
-XX:+ExplicitGCInvokesConcurrent 允许使用System.gc()请求调用并发GC 。默认情况下禁用此选项,并且只能与该-XX:+UseConcMarkSweepGC选项一起启用
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 通过System.gc()在并发GC周期期间使用请求和卸载类,可以调用并发GC。默认情况下禁用此选项,并且只能与该-XX:+UseConcMarkSweepGC选项一起启用
-XX:+HeapDumpOnOutOfMemoryError 通过使用堆分析器(HPROF)将Java堆转储到当前目录中的文件
-XX:HeapDumpPath=/home/ewallet/loan-star/loan-star-pre/default/tomcat/logs/star.hprof 设置用于写入堆分析器(HPROF)提供的堆转储的路径和文件名。默认情况下,该文件在当前工作目录中创建,并且名为java_pidpid.hprof,其中pid是导致错误的进程的标识符

总结

当前机器的配置足够,在目前情况下优化参数不会有太大的提升效果。

假如在降低机器配置前提下,那需要依赖数据,做相应的调整,具体要看机器成本而定。

参考

附一 找到占用cpu最高的一个线程

  • 第一步,找到占用cpu最高的一个线程【n】

    方法:直接top获得【pid】,然后shift+h或者top -Hp 【pid】

  • 第二步,将其转化成16进制。假使我们得到的线程号为【n】,接下来将它转成16进制,记为spid

    方法:printf 0x%x 【n】

  • 第三步,执行以下命令,打印后面100行分析问题

    1
    jstack -l 【pid】| grep 【spid】 -A 100 

    如果需要导出完整的线程栈,使用

    1
    jstack pid > pid.tdump 

    插件使用:https://github.com/oldratlee/useful-scripts

附二 堆内存操作

查看jvm的所有设置参数

1
jinfo -flags 【pid】

查看当前堆内存使用情况

1
jmap -heap 【pid】

gc情况查看

1
2
3
jstat -gcutil 【pid】

jstat -gc 【pid】 2s 3

导出堆文件

  • 导出整个堆文件
    1
    jmap -dump:format=b,file=heap.hprof 【pid】
  • 导出存活的堆文件(慎用,会进行一次Full GC
    1
    jmap -dump:live,format=b,file=heap.hprof 【pid】

附三 参考资料

参考一:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 参数

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html(收集器

https://www.oracle.com/technetwork/java/tuning-139912.html(调优试例

参考二:书籍:深入理解Java虚拟机(第二版)

​ 书籍:Java Performance:The Definitive Guide

参考三:https://blog.csdn.net/linhu007/article/details/48897597(CMS与G1

参考四:https://blog.csdn.net/zc19921215/article/details/83029952

参考五:https://www.aliyun.com/jiaocheng/1341282.html