0%

内置监控工具 - jstat

作用

jstat全称JVM Statistics Monitoring Tool,用于监控JVM的各种运行状态。

TIPS
此命令是实验性的,不受支持。

参考文档:
Java 8:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
Java 11:https://docs.oracle.com/en/java/javase/11/tools/jstat.html

使用说明

命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜ jstat -h
Usage: jstat --help|-options
       jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]


Definitions:
  <option>      指定参数,取值可用jstat -options查看
  <vmid>        VM标识,格式为<lvmid>[@<hostname>[:<port>]]
                <lvmid>:如果lvmid是本地VM,那么用进程号即可;
                <hostname>:目标JVM的主机名;
                <port>:目标主机的rmiregistry端口;
  -t            用来展示每次采样花费的时间
  <lines>       每抽样几次就列一个标题,默认0,显示数据第一行的列标题
  <interval>    抽样的周期,格式使用:<n>["ms"|"s"],n是数字,ms/s是时间单位,默认是ms
  <count>       采样多少次停止
  -J<flag>      将<flag>传给运行时系统,例如:-J-Xms48m
  -? -h --help  Prints this help message.
  -help         Prints this help message.

option取值如下:

  • class:显示类加载器的统计信息
  • compiler:显示有关Java HotSpot VM即时编译器行为的统计信息
  • gc:显示有关垃圾收集堆行为的统计信息
  • gccapacity:统计各个分代(新生代,老年代,持久代)的容量情况
  • gccause:显示引起垃圾收集事件的原因
  • gcnew:显示有关新生代行为的统计信息
  • gcnewcapacity:显示新生代容量
  • gcold:显示老年代、元空间区的统计信息
  • gcoldcapacity:显示老年代的容量
  • gcmetacapacity:显示元空间的容量
  • gcutil:显示有关垃圾收集统计信息的摘要
  • printcompilation:显示Java HotSpot VM编译方法统计信息

输出信息

class:

  • Loaded:当前加载的类的数量
  • Bytes:当前加载的空间(单位KB)
  • Unloaded:卸载的类的数量Number of classes unloaded.
  • Bytes:当前卸载的空间(单位KB)
  • Time:执行类加载/卸载操作所花费的时间
  • compiler:

compiler:

  • Compiled:执行了多少次编译任务
  • Failed:多少次编译任务执行失败
  • Invalid:无效的编译任务数
  • Time:执行编译任务所花费的时间
  • FailedType:上次失败的编译的编译类型
  • FailedMethod:上次失败的编译的类名和方法

gc:

  • S0C:第一个存活区(S0)的容量(KB)
  • S1C:第二个存活区(S1)的容量(KB)
  • S0U:第一个存活区(S0)使用的大小(KB)
  • S1U:第二个存活区(S1)使用的大小(KB)
  • EC:伊甸园空间容量(KB)
  • EU:伊甸园使用的大小(KB)
  • OC:老年代容量(KB)
  • OU:老年代使用的大小(KB)
  • MC:元空间的大小(KB)
  • MU:元空间使用的大小(KB)
  • CCSC:压缩的类空间大小(KB)
  • CCSU:压缩类空间使用的大小(KB)
  • YGC:年轻代垃圾收集事件的数量
  • YGCT:年轻代垃圾回收时间
  • FGC:Full GC事件的数量
  • FGCT:Full GC回收时间
  • GCT:垃圾收集总时间

gccapacity:

  • NGCMN:最小新生代容量(KB)
  • NGCMX:最大新生代容量(KB)
  • NGC:当前的新生代容量(KB)
  • S0C:第一个存活区(S0)的当前容量(KB)
  • S1C:第二个存活区(S1)的当前容量(KB)
  • EC:当前伊甸园容量(KB)
  • OGCMN:最小老年代容量(KB)
  • OGCMX:最大老年代容量(KB)
  • OGC:当前老年代容量(KB)
  • OC:当前old space容量(KB)
  • MCMN:最小元空间容量(KB)
  • MCMX:最大元空间容量(KB)
  • MC:当前元空间的容量(KB)
  • CCSMN:压缩的类空间最小容量(KB)
  • CCSMX:压缩的类空间最大容量(KB)
  • CCSC:当前压缩的类空间大小(KB)
  • YGC:年轻代GC事件的数量
  • FGC:Full GC事件的数量

gccause:其他展示列和-gcutil一致

  • LGCC:导致GC的原因
  • GCC:导致当前GC的原因

gcnew:

  • S0C:第一个存活区(S0)的容量(KB)
  • S1C:第二个存活区(S1)的容量(KB)
  • S0U:第一个存活区(S0)的利用率(KB)
  • S1U:第二个存活区(S1)的利用率(KB)
  • TT:老年代阈值
  • MTT:最大老年代阈值
  • DSS:期望的存活区大小(KB)
  • EC:当前伊甸园容量(KB)
  • EU:伊甸园利用率(KB)
  • YGC:年轻代GC事件的数量
  • YGCT:年轻代垃圾回收时间

gcnewcapacity:

  • NGCMN:最小年轻代容量(KB)
  • NGCMX:最大年轻代容量(KB)
  • NGC:当前年轻代容量(KB)
  • S0CMX:最大S0容量(KB)
  • S0C:当前S0容量(KB)
  • S1CMX:最大S1容量(KB)
  • S1C:当前S1容量(KB)
  • ECMX:最大伊甸园容量(KB)
  • EC:当前伊甸园容量(KB)
  • YGC:年轻代GC事件的数量
  • FGC:Full GC事件的数量

gcold:

  • MC:当前元空间使用大小(KB)
  • MU:元空间利用率(KB)
  • CCSC:压缩的类的大小(KB)
  • CCSU:使用的压缩类空间(KB)
  • OC:当前的老年代空间容量(KB)
  • OU:来年代空间利用率(KB)
  • YGC:年轻代GC事件的数量
  • FGC:Full GC事件的数量
  • FGCT:Full GC垃圾收集时间
  • GCT:总垃圾收集时间

gcoldcapacity:

  • OGCMN:最小老年代容量(KB)
  • OGCMX:最大老年代容量(KB)
  • OGC:当前老年代容量(KB)
  • OC:当前old space容量(KB)
  • YGC:年轻代GC事件的数量
  • FGC:Full GC事件的数量
  • FGCT:Full GC垃圾收集时间
  • GCT:总垃圾收集时间

gcmetacapacity:

  • MCMN:最小元空间容量(KB)
  • MCMX:最大元空间容量(KB)
  • MC:元空间大小(KB)
  • CCSMN:压缩的类空间最小容量(KB)
  • CCSMX:压缩的类空间最大容量(KB)
  • YGC:年轻代GC事件的数量
  • FGC:Full GC事件的数量
  • FGCT:Full GC垃圾收集时间
  • GCT:总垃圾收集时间

gcutil:

  • S0:第一个存活区(S0)利用率
  • S1:第二个存活区(S1)利用率
  • E:Eden空间利用率
  • O:老年代空间利用率
  • M:元空间利用率
  • CCS:压缩的类空间利用率
  • YGC:年轻代GC事件的数量
  • YGCT:年轻代垃圾回收时间
  • FGC:Full GC事件的数量
  • FGCT:Full GC垃圾收集时间
  • GCT:总垃圾收集时间

printcompilation:

  • Compiled:由最近编译的方法去执行的编译任务数
  • Size:最近编译的方法的字节码的字节数
  • Type:最近编译的方法的编译类型。
  • Method:标识最近编译的方法的类名和方法名。类名使用 / 代替点 . 作为名称空间分隔符;方法名称是指定类中的方法。这两个字段的格式与HotSpot -XX:+PrintCompilation 选项一致。

使用示例

示例1:查看21891这个进程的gc相关信息,每隔250ms采样1次,采样7次

1
jstat -gcutil 21891 250 7

示例2:显示有关新生代行为的统计信息,重复列标题:

1
jstat -gcnew -h3 21891 250

示例3:查看remote.domain机器上的40496这个进程有关垃圾收集统计信息的摘要,每隔1秒采样1次:

1
jstat -gcutil 40496@remote.domain 1000

前言

线程就是一个放线程的池子。

使用线程池的好处:

  • 重用已存在的线程,从而减少对象创建和销毁的开销。
  • 控制并发,从而提高资源利用率,有效避免过多的资源竞争,提升性能
  • 功能强大,有定时执行、定期执行、单线程执行、并发控制等等
阅读全文 »

前言

ab全称Apache HTTP server benchmarking tool的缩写,是Apache自带的网站benchmark测试工具。使用非常方便,简约而不简单。

官方网站:https://httpd.apache.org/docs/2.4/programs/ab.html

TIPS

可达到ab相同效果的工具:

安装

macOS

系统自带,直接使用即可。

CentOS 7

运行如下命令即可安装

1
yum -y install httpd-tools

Windows

1
./ab xxxx

TIPS

由于笔者没有Windows环境,所以此步骤无法亲测。
亲们可百度 “Windows 安装 ab”,参照结果安装即可。资料非常丰富。

使用说明

ab是一个命令行工具,使用说明如下:

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
➜ ab -help

Usage: ab [options] [http[s]://]hostname[:port]/path

Options are:

-n requests 指定请求多少次

-c concurrency 指定并发数(指定同时发送几个请求)

-t timelimit 测试持续的最长时间,默认无限制,此参数隐含-n 50000

-s timeout 每个请求的超时时间,默认30秒

-b windowsize 指定TCP收发缓存大小,单位字节

-B address 指定在发起连接时绑定的IP地址

-p postfile 指定想要POST的文件,需和-T参数配合使用

-u putfile 指定想要PUT的文件,需和-T参数配合使用

-T content-type 指定POST/PUT文件时的Content-type。默认text/plain

-v verbosity 详细模式,打印更多日志

-w 将结果输出到html表格中

-i 使用HEAD方式代替GET发起请求

-x attributes 插入字符串作为table标签的属性

-y attributes 插入字符串作为tr标签的属性

-z attributes 插入字符串作为td或th标签的属性

-C attribute 添加Cookie,例如Apache=1234;可重复该参数选项以添加多个Cookie

-H attribute 添加任意的请求头,例如Accept-Encoding: gzip;可重复该参数选项以添加多个

-A attribute 添加Basic WWW认证信息,用户名和密码之间用:分隔

-P attribute 添加Basic Proxy认证信息,用户名和密码之间用:分隔

-X proxy:port 指定代理地址

-V 打印ab的版本信息

-k 使用HTTP的KeepAlive特性

-d 不显示百分比

-S 不显示预估和警告信息

-q 默认情况下,如果处理的请求数大于150,ab每处理大约10%或者100个请求时,打印一下进度信息。使用该参数后则不打印进度。

-g filename 输出结果信息到gnuplot格式的文件中

-e filename 输出结果信息到CSV格式的文件中

-r 指定接收到错误信息时不退出程序

-h 显示使用说明

-Z ciphersuite 指定SSL/TLS密码套件

-f protocol 指定SSL/TLS协议(SSL3, TLS1, TLS1.1, TLS1.2 or ALL)

示例

1
ab -c 100 -n 10000 http://www.baidu.com/

表示并发100,请求baidu 10000次。

TIPS

http://www.baidu.com/ 不能写成 http://www.baidu.com

这是因为 http://www.baidu.com 不符合ab要求的 [http[s]://]hostname[:port]/path 格式要求。这是个小坑,需要注意一下。

报表解读

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
➜ ab -c 10 -n 1000 http://www.baidu.com/

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>

Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/

Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking www.baidu.com (be patient)

Completed 100 requests

Completed 200 requests

Completed 300 requests

Completed 400 requests

Completed 500 requests

Completed 600 requests

Completed 700 requests

Completed 800 requests

Completed 900 requests

Completed 1000 requests

Finished 1000 requests

# 展示测试地址所使用的服务器软件及版本
Server Software: BWS/1.1
# 测试地址的主机名
Server Hostname: www.baidu.com
# 测试地址的端口
Server Port: 80
# 测试地址的路径
Document Path: /
# 测试地址的文档大小
Document Length: 255598 bytes
# 并发数
Concurrency Level: 10
# 测试花费了多久
Time taken for tests: 81.881 seconds
# 测试总共请求了多少次
Complete requests: 1000
# 失败的请求数
Failed requests: 979

(Connect: 0, Receive: 0, Length: 979, Exceptions: 0)

# 传输的总数据量
Total transferred: 256761473 bytes

# HTML文档的总数据量
HTML transferred: 255605512 bytes

# 平均每秒的请求数,也叫RPS,该值越大表示服务器吞吐量越大,性能表现越好
Requests per second: 12.21 [#/sec] (mean)

# 请求平均耗时,越小说明响应越快
Time per request: 818.815 [ms] (mean)

# 服务器平均处理时间,其实是服务器吞吐量的倒数
Time per request: 81.881 [ms] (mean, across all concurrent requests)

# 每秒获取的数据长度,单位单位:KB/s
Transfer rate: 3062.28 [Kbytes/sec] received

# 连接时间统计信息
Connection Times (ms)

# 最小 平均 中值 最大

​ min mean[+/-sd] median max

# 连接时间
Connect: 17 154 118.0 135 1416
# 处理时间
Processing: 329 661 211.2 602 1824
# 等待时间
Waiting: 22 160 88.4 143 1089
# 总计时间
Total: 391 816 263.4 731 2521

# 请求耗时的统计信息。例如请求经过排序后,50百分位的请求花费了731毫秒;99百分位的请求花费了1942毫秒等
Percentage of the requests served within a certain time (ms)

50% 731

66% 804

75% 873

80% 943

90% 1154

95% 1332

98% 1726

99% 1942

100% 2521 (longest request)

前言

通过对象池编写一个数据库连接池,深入学习,巩固对象池用法。

  • 能代替其它数据库连接池产品

    • Hikari、DBCP、Tomcat、Druid…
  • 带监控,随时了解了解连接池的情况

ConnectionPooledObjectFactory

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
public class ConnectionPooledObjectFactory
implements PooledObjectFactory<MyConnection> {
private ObjectPool<MyConnection> objectPool;

public ObjectPool<MyConnection> getObjectPool() {
return objectPool;
}

public void setObjectPool(ObjectPool<MyConnection> objectPool) {
this.objectPool = objectPool;
}

@Override
public PooledObject<MyConnection> makeObject() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/xxxx?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true",
"root",
"root123"
);
MyConnection myConnection = new MyConnection();
myConnection.setConnection(connection);
myConnection.setObjectPool(objectPool);
return new DefaultPooledObject<>(myConnection);
}

@Override
public void destroyObject(PooledObject<MyConnection> p) throws Exception {
p.getObject().close();
}

@Override
public boolean validateObject(PooledObject<MyConnection> p) {
Connection connection = p.getObject();
try {
PreparedStatement statement = connection.prepareStatement("SELECT 1");
ResultSet resultSet = statement.executeQuery();
int i = resultSet.getInt(1);
return i == 1;
} catch (SQLException e) {
return false;
}
}

@Override
public void activateObject(PooledObject<MyConnection> p) throws Exception {
// 可以把connection额外的配置放到这里
}

@Override
public void passivateObject(PooledObject<MyConnection> p) throws Exception {
// 钝化
MyConnection myConnection = p.getObject();
Statement statement = myConnection.getStatement();
if (statement != null) {
statement.close();
}
}
}

MyDataSource

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
public class MyDataSource implements DataSource {
private GenericObjectPool<MyConnection> pool;

public GenericObjectPool<MyConnection> getPool() {
return pool;
}

public void setPool(GenericObjectPool<MyConnection> pool) {
this.pool = pool;
}

public MyDataSource() {
ConnectionPooledObjectFactory factory = new ConnectionPooledObjectFactory();
this.pool = new GenericObjectPool<>(factory);
factory.setObjectPool(pool);
}

@Override
public Connection getConnection() throws SQLException {
try {
return this.pool.borrowObject();
} catch (Exception e) {
throw new SQLException("获取连接失败!", e);
}
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
return this.getConnection();
}

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
throw new UnsupportedOperationException("不支持的操作!");
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
throw new UnsupportedOperationException("不支持的操作!");
}

@Override
public PrintWriter getLogWriter() throws SQLException {
throw new UnsupportedOperationException("不支持的操作!");
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {
throw new UnsupportedOperationException("不支持的操作!");
}

@Override
public void setLoginTimeout(int seconds) throws SQLException {
throw new UnsupportedOperationException("不支持的操作!");
}

@Override
public int getLoginTimeout() throws SQLException {
throw new UnsupportedOperationException("不支持的操作!");
}

@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new UnsupportedOperationException("不支持的操作!");
}
}

MyConnection

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
// 1. 回收statement
// 2. 回收resultSet
// 3. 复用Connection
public class MyConnection implements Connection {
private Connection connection;
private Statement statement;
private ObjectPool<MyConnection> objectPool;

// 篇幅有限,省略其它方法

@Override
public Statement createStatement() throws SQLException {
return this.connection.createStatement();
}

@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
PreparedStatement prepareStatement = this.connection.prepareStatement(sql);
this.statement = prepareStatement;
return prepareStatement;
}

@Override
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
PreparedStatement statement = this.connection.prepareStatement(sql, columnNames);
this.statement = statement;
return statement;
}

@Override
public void close() throws SQLException {
// 如果底层的Connection已经关闭
if (this.isClosed()) {
try {
objectPool.invalidateObject(this);
} catch (Exception e) {
throw new SQLException(e);
}
}
// 底层Connection没有关闭,可以继续复用
else {
try {
objectPool.returnObject(this);
} catch (Exception e) {
this.connection.close();
throw new SQLException(e);
}
}
}

}

如何使用?

1
2
3
4
5
@Bean
@Primary
public DataSource dataSource(){
return new DMDataSource();
}

如何使用 spring-boot-starter-actuator 添加监控?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 可以用/actuator/datasource
@Endpoint(id = "datasource")
public class DataSourceEnpoint {
private MyDataSource dataSource;

public DataSourceEnpoint(MyDataSource dataSource) {
this.dataSource = dataSource;
}

@ReadOperation
public Map<String, Object> pool() {
GenericObjectPool<MyConnection> pool = dataSource.getPool();
HashMap<String, Object> map = Maps.newHashMap();
map.put("numActive", pool.getNumActive());
map.put("numIdle", pool.getNumIdle());
map.put("createdCount", pool.getCreatedCount());
return map;
}
}

1 前言

池化技术是性能调优的重要措施,池化的思想是把对象放到池子里面,当要使用的时候从池子里面拿对象,用完之后在放回池子里面。这样可以降低资源分配和释放资源的开销,从而提升性能。

阅读全文 »

转载自https://blog.csdn.net/luzhensmart/article/details/103782340

1: Java Minor GC、Major GC和Full GC之间的区别

  • Minor GC

    Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。

    阅读全文 »

转载自https://blog.csdn.net/luzhensmart/article/details/103782340

1: Java Minor GC、Major GC和Full GC之间的区别

  • Minor GC

    Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。

    阅读全文 »

1 概述

  • CAP原理

  • ACID原理与BASE原理

  • 基于XA协议的两阶段提交

  • 事务补偿机制

  • 基于本地消息表的最终一致方案

  • 基于MQ消息队列的最终一致方案

2 CAP原理

2.1 简介

在分布式系统中,我们经常听到CAP原理这个词,它是什么意思呢?其实和C、A、P这3个字母有关,C、A、P分别是这3个词的首字母。下面我们就看一下这3个词分别是什么意思?

  • C - Consistent ,一致性。具体是指,操作成功以后,所有的节点,在同一时间,看到的数据都是完全一致的。所以,一致性,说的就是数据一致性。
  • A - Availability ,可用性。指服务一致可用,在规定的时间内完成响应。
  • P - Partition tolerance ,分区容错性。指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。

CAP原理指出,这3个指标不能同时满足,最多只能满足其中的两个。

2.2 详解

我们之所以使用分布式系统,就是为了在某个节点不可用的情况下,整个服务对外还是可用的,这正是满足P(分区容错性)。如果我们的服务不满足P(分区容错性),那么我们的系统也就不是分布式系统了,所以,在分布式系统中,P(分布容错性)总是成立的。那么,A(可用性)和C(一致性)能不能同时满足呢?我们看一下下面的图例。

A和B是两个数据节点,A向B同步数据,并且作为一个整体对外提供服务。由于我们的系统保证了P(分区容错性),那么A和B的同步,我们允许出现故障。接下来我们再保证A(可用性),也就是说A和B同步出现问题时,客户端还能够访问我们的系统,那么客户端既可能访问A也可能访问B,这时,A和B的数据是不一致的,所以C(一致性)不能满足。

如果我们满足C(一致性),也就是说客户端无论访问A还是访问B,得到的结果都是一样的,那么现在A和B的数据不一致,需要等到A和B的数据一致以后,也就是同步恢复以后,才可对外提供服务。这样我们虽然满足了C(一致性),却不能满足A(可用性)。

所以,我们的系统在满足P(分区容错性)的同时,只能在A(可用性)和C(一致性)当中选择一个不能CAP同时满足。我们的分布式系统只能是AP或者CP。

3 ACID与BASE

在关系型数据库中,最大的特点就是事务处理,也就是ACID。ACID是事务处理的4个特性。

  • A - Atomicity(原子性),事务中的操作要么都做,要么都不做。

  • C - Consistency(一致性),系统必须始终处在强一致状态下。

  • I - Isolation(隔离性),一个事务的执行不能被其他事务所干扰。

  • D - Durability(持久性),一个已提交的事务对数据库中数据的改变是永久性的。

ACID强调的是强一致性,要么全做,要么全不做,所有的用户看到的都是一致的数据。传统的数据库都有ACID特性,它们在CAP原理中,保证的是CA。但是在分布式系统大行其道的今天,满足CA特性的系统很难生存下去。ACID也逐渐的向BASE转换。那么什么是BASE呢?

BASE是Basically Available(基本可用), Soft-state(软状态), Eventually consistent(最终一致)的缩写。

  • Basically Available,基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

    电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

  • 软状态( Soft State)

    软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有两到三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。

  • 最终一致性( Eventual Consistency)

    最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

BASE模型是传统ACID模型的反面,不同与ACID,BASE强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了。

在分布式事务的解决方案中,它们都是依赖了ACID或者BASE的模型而实现的。像基于XA协议的两阶段提交和实物补偿机制就是基于ACID实现的。而基于本地消息表和基于MQ的最终一致方案都是通过BASE原理实现的。这几种分布式事务的解决方案,我们会在视频的课程中给大家讲解。

4 由分库分表引发的事物问题和解决方案

4.1 由分库分表引发的事物问题

传统的应用都是单一数据库事物,所有的业务表都在同一数据库内,这时候数据库的事物可以得到很好的支持,如下图所示:

image-20210724171356938

然后在同一个业务内,再把数据库进行水平切分多个数据库,多个数据库之间是没有办法无法统一事务管理的,就会造成数据不一致的情况。如果按照垂直切分成多个数据库,如下图所示:

image-20210724172152429

比如:

  • 一个下单操作,用户使用积分购买商品

  • 用户库扣减积分,订单库生成订单,商品库扣减库存

  • 由于它们不在同一数据库,不能改保证事务统一

  • 任何一个环节出错,其他两个数据的事务将不能回滚

4.2 解决方案

那么如何解决呢?

  • 基于XA协议的两阶段提交
  • 事务补偿机制
  • 基于本地消息表+定时任务的最终一致性方案
  • 基于MQ的最终一致方案

4.2.1 基于XA协议的两阶段提交

XA 协议原理简介

  • XA 是由 X/Open 组织提出的分布式事务的规范

  • 有一个事务管理器(TM)和多个资源管理器(RM)组成

    TM:数据源,RM:数据库

  • 提交阶段分为两个阶段:prepare 和 commit

    也就是说在提交之前,让所有的数据库先进行准备,准备好了之后通知事务管理器,然后由事务管理器统一的提交事务,如下图所示:

    image-20210724175921780

    image-20210724180456245

  • XA协议保证了数据的强一致性

  • commit 阶段出现问题,事务不一致,需人工处理

  • 但是两阶段提交效率低下,性能与本地事务相差 10 倍

  • MySql5.7 及以上均支持XA协议

  • Mysql Connector/J 5.0以上支持XA协议

  • Java系统中,数据源可以采用 Atomikos(充当事务管理器)

4.2.1.1 使用Mycat 或者Sharding-Jdbc

配置Mycat

通过修改conf/server.xml配置分布式事务,如下图所示:

至于Sharding-Jdbc则默认为我们开启了分布式事务

4.2.2 事务补偿机制

什么事务补偿机制?

其实就是针对每个操作,都要注册一个与其对应的补偿(撤销)操作,在执行失败时,调用补偿操作,撤销之前的操作。

例如一个A给B转账的例子:

image-20210725155803128

  • A 和 B 在不两家不同的银行
  • A 账户减 200元,B 账户加 200 元
  • 两个操作要保证原子性,要么都成功,要么都失败
  • 由于A和B在两家不同的银行,所以存在分布式事务问题
  • 转账接口需要提供补偿机制
  • 如果A在扣减的过程中出现问题,直接抛出异常,事务回滚
  • B在增加余额的过程中,出现问题,要调用A的补偿接口
  • A之前的扣减操作,得到了补偿,进行了撤销
  • 保证了A和B的帐是没有问题的

如果A补偿操作也失败了呢?

我们可以为A补偿操作添加有限次数的重试机制,超出重试次数后,人工介入。

事务补偿机制看起来有点繁琐,不推荐使用。

事务补偿机制的优缺点:

  • 优点:逻辑清晰、流程简单

  • 缺点:数据一致性比XA还要差,可能出错的点比较多

  • TCC属于应用层的一种补偿方式,程序员需要写大量的代码

4.2.3 基于本地消息表的的最终一致性方案

image-20210726213335173

  • 采用BASE原理,保证事务最终一致
  • 在一致性方面,允许一段时间内的不一致,但最终会一致
  • 在实际的系统中,要根据具体情况,判断是否采用
  • 基于本地消息表的方案中,将本事务外操作,记录在消息表中
  • 其他的事务要提供操作接口(例如支付宝、微信支付的接口)
  • 定时任务轮训本地消息表,将未执行的的消息发送给操作接口
  • 操作接口处理成功,返回成功标识,处理失败返回失败标识
  • 定时任务接到标识,更新消息的状态
  • 定时任务按照一定的周期反复执行
  • 对于屡次失败的消息,可以设置最大失败次数
  • 超过最大失败次数的消息,不再进行接口调用
  • 等待人工处理

那么这个方案有什么优缺点呢?

  • 优点:避免了了分布式事务,实现了最终一致性
  • 缺点:要注意重试时的幂等性操作

4.2.4 基于MQ的最终一致性方案

  • 原理、流程与本地消息表类似
  • 不同点1:本地消息表改为 MQ
  • 不同点2:定时任务改为MQ的消费者

这样就不在依赖于定时任务了,架构图如下所示:

image-20210728232239034

  1. 首先,业务系统将数据保存到数据库中

  2. 然后,业务系统把另外一条消息存放到消息队列中

  3. 消费者从消息队列中去取消息

  4. 消费者执行分布式事务的第二段的服务,把数据落入到数据库中去

优点

  • 不依赖于定时任务,基于MQ更高效、更可靠
  • 适合公司内部的系统
  • 不同公司之间无法基于MQ,本地消息表更合适

1 前言

在分库分表的过程中,因为拆分的实体表的ID有可能是重复的,正式由于这个问题才会有分布式全局ID这个方案的出现。

阅读全文 »