最近看到一个开源项目/公开课 CS5411/4411 at Cornell,关于如何实现一个操作系统。突然对 Java 中native 调用比较感兴趣。 特意选择 System.out.println() 与操作系统的交互来了解OS System Call 实现过程。
System 使用 PrintStream 打印信息,Java 中的文件流使用的包装者模式,真正调用输出流是 FileDescriptor.out。
// System.out 初始化,包括 stdIn, stdOut, stdErr
FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
更多详细解读 System源码浅析- initializeSystemClass(initProperties)
FileDescriptor 是 Java I/O库中的一种抽象,用于表示打开的文件、套接字或其他数据源句柄,不依赖具体的操作系统特性🎯。以下会分析 Windows 系统的实现。
/**
* 标准输出流对象(In Java)/句柄(In Windows), 对应的是控制台。
*/
public static final FileDescriptor out = standardStream(1);
// set 是 native 方法,依赖具体的操作系统实现(native code)
private static native long set(int d);
private static FileDescriptor standardStream(int fd) {
FileDescriptor desc = new FileDescriptor();
desc.handle = set(fd);
return desc;
}
这里查询的 OpenJdk 基于 Windows 操作系统的实现源码。src 下载地址 🎯。
直接调用 Windows API 获取控制台输入缓冲区的句柄
/*
* 用途:Java_java_io_FileDescriptor_set 赋值标准设备 stdIn, stdOut, stdErr
* @see java/io/io_util_md.h
*/
#define SET_HANDLE(fd) \
if (fd == 0) { \
return (jlong)GetStdHandle(STD_INPUT_HANDLE); \
} else if (fd == 1) { \
// 标准输出设备, GetStdHandle是一个Windows API函数。
return (jlong)GetStdHandle(STD_OUTPUT_HANDLE); \
} else if (fd == 2) { \
return (jlong)GetStdHandle(STD_ERROR_HANDLE); \
} else { \
return (jlong)-1; \
} \
GetStdHandle 函数提供了一种机制,用于检索与进程关联的标准输入 (STDIN)、标准输出 (STDOUT) 和标准错误 (STDERR) 句柄。
GetStdHandle 返回的句柄可供需要在控制台中进行读取或写入的应用程序使用。
在控制台创建过程中,系统将创建这些句柄。如上述 native 获取方式。🧲
[GetStdHandle 函数 - Windows Console | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/console/getstdhandle) 文档 |
值 | 含义 |
---|---|
STD_INPUT_HANDLE((DWORD)-10) |
标准输入设备。 最初,这是输入缓冲区 CONIN$ 的控制台。 |
STD_OUTPUT_HANDLE((DWORD)-11) |
标准输出设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$ 。 |
STD_ERROR_HANDLE((DWORD)-12) |
标准错误设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$ 。 |
#include<windows.h>
/**
* 不用stdio.h能在控制台输出信息吗?
* 在Windows下,可以直接使用Windows API来完成. 使用 GetStdHandle WriteConsole 函数来在控制台输出信息
* https://www.cnblogs.com/jisuanjizhishizatan/p/16149561.html
*/
int main(){
const char *str="Use <windows.h> to output in C++.\n";
HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);
WriteConsole(handle,str,strlen(str),NULL,NULL);
return 0;
}
注意事项:
Window 环境编译C/C++ 可以使用 GCC 编译器。一般是安装 MinGW 插件。由于安装过 Ruby, 使用的是附带的 C:\Programs\Ruby27-x64\msys64\mingw64\bin
。
编译上述的代码 gcc console.c -o console.exe
使用 cmd 命令行工具,使用 git bash 打印会有问题,非 Windows 原生组件获取不到控制台句柄。❌
JVM 的存在,方便了跨平台开发,另外以方便也屏蔽了操作系统底层的一些对接。
从 Java SDK 到 native 实现的探索,举一反三,其他特性的 native 实现按图索骥。🙉
对于基础 API 的原理探索,在使用和调优上会更有目的和方向。
越接近底层,越接近真相。
h2database 是使用Java 编写的开源数据库,兼容ANSI-SQL89。
即实现了常规基于 BTree 的存储引擎,又支持日志结构存储引擎。功能非常丰富(死锁检测机制、事务特性、MVCC、运维工具等),数据库学习非常好的案例。
本文理论结合实践,通过BTree 索引的设计和实现,更好的理解数据库索引相关的知识点以及优化原理。
h2database 默认使用的 MVStore 存储引擎,如果要使用 基于 BTree 的存储引擎,需要特别指定(如下示例代码 jdbcUrl)。
以下是常规存储引擎(BTree 结构) 相关的关键类。
org.h2.table.RegularTable
org.h2.index.PageBtreeIndex (SQL Index 本体实现)
org.h2.store.PageStore (存储层,对接逻辑层和文件系统)
BTree 的数据结构可以从网上查到详细的描述和讲解,不做过多赘述。
需要特别说明的是:PageStore。我们数据查询和优化关键的缓存、磁盘读取、undo log都是由 PageStore 完成。可以看到详细的文档和完整的实现。
提供索引数据新增的调用链。同样的,索引的删除和查询都会涉及到,方便 debug 参考。
org.h2.command.dml.Insert#insertRows (Insert SQL 触发数据和索引新增)
org.h2.mvstore.db.RegularTable#addRow (处理完的数据Row, 执行新增)
org.h2.index.PageBtreeIndex#add (逻辑层增加索引数据)
org.h2.index.PageDataIndex#addTry (存储层增加索引数据)
org.h2.index.PageDataLeaf#addRowTry (存储层新增实现)
// 示例代码
// CREATE TABLE city (id INT(10) NOT NULL AUTO_INCREMENT, code VARCHAR(40) NOT NULL, name VARCHAR(40) NOT NULL);
public static void main(String[] args) throws SQLException {
// 注意:MV_STORE=false,MVStore is used as default storage
Connection conn = DriverManager.getConnection("jdbc:h2:~/test;MV_STORE=false", "sa", "");
Statement statement = conn.createStatement();
// CREATE INDEX IDX_NAME ON city(code); 添加数据触发 BTree 索引新增
// -- SQL 实例化为:IDX_NAME:16:org.h2.index.PageBtreeIndex
statement.executeUpdate("INSERT INTO city(code,name) values('cch','长春')");
statement.close();
conn.close();
}
结合上述的示例代码,从索引新增的流程实现来了解BTree 索引的特性以及使用的注意事项。从底层实现分析索引的运行,对 SQL 索引使用和优化有进一步认识。
public void addRow(Session session, Row row) {
// MVCC 控制机制,记录和比对当前事务的 id
lastModificationId = database.getNextModificationDataId();
if (database.isMultiVersion()) {
row.setSessionId(session.getId());
}
int i = 0;
try {
// 根据设计规范,indexes 肯定会有一个聚集索引(h2 称之为scan index)。①
for (int size = indexes.size(); i < size; i++) {
Index index = indexes.get(i);
index.add(session, row);
checkRowCount(session, index, 1);
}
// 记录当前 table 的数据行数,事务回滚后会相应递减。
rowCount++;
} catch (Throwable e) {
try {
while (--i >= 0) {
Index index = indexes.get(i);
// 对应的,如果发生任何异常,会移除对应的索引数据。
index.remove(session, row);
}
}
throw de;
}
}
① 同Mysql InnoDB 数据存储一样, RegularTable 必有,且只有一个聚集索引。以主键(或者隐含自增id)为key, 存储完整的数据。
索引中的 key 是查询要搜索的内容,而其值可以是以下两种情况之一:它可以是实际的行(文档,顶点),也可以是对存储在别处的行的引用。在后一种情况下,行被存储的地方被称为 堆文件(heap file),并且存储的数据没有特定的顺序(根据索引相关的)。
从索引到堆文件的额外跳跃对读取来说性能损失太大,因此可能希望将被索引的行直接存储在索引中。这被称为聚集索引(clustered index)。
基于主键扫描即可唯一确定、并且获取到数据,聚集索引性能比非主键索引少一次扫描
public void add(Session session, Row row) {
// 索引key 生成 ②
if (mainIndexColumn != -1) {
// 如果主键非 long, 使用 org.h2.value.Value#convertTo 尝试把主键转为 long
row.setKey(row.getValue(mainIndexColumn).getLong());
} else {
if (row.getKey() == 0) {
row.setKey((int) ++lastKey);
retry = true;
}
}
// 添加行数据到聚集索引 ③
while (true) {
try {
addTry(session, row);
break;
} catch (DbException e) {
if (!retry) {
throw getNewDuplicateKeyException();
}
}
}
}
② 对于有主键的情况,会获取当前 row 主键的值,转为long value。对于没有指定主键的情况,从当前聚集索引属性 lastKey 自增得到唯一 key。
只有指定主键的情况,才会校验数据重复(也就是索引key 重复,自增 lastKey 是不会有重复值的问题)。
③ 聚集索引 PageDataIndex 按照BTree 结构查找对应的key 位置,按照主键/key 的顺序,将 Row 存储到page 中。非聚集索引 PageBtreeIndex 也是这样的处理流程。
这其中涉及到三个问题:
如何查找 key 的位置,也就是 BTree 位置的计算?
如何计算 Row (实际数据)存储 Page 中的 offsets?
Row 是怎样写入到磁盘中的,何时写入的?
B 树将数据库分解成固定大小的 块(block) 或 分页(page),传统上大小为 4KB(有时会更大),并且一次只能读取或写入一个页面。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在硬盘而不是在内存中。(对应h2 database PageBtreeLeaf 和 PageBtreeNode)
不同于 PageDataIndex ,PageBtreeIndex 按照 column.value 顺序来存储。添加的过程就是比对查找 column.value,确定在块(block)中offsets 的下标 x。剩下就是计算数据的offset 并存入下标 x 中。
/**
* Find an entry. 二分查找 compare 所在的位置。这个位置存储 compare 的offset。
* org.h2.index.PageBtree#find(org.h2.result.SearchRow, boolean, boolean, boolean)
* @param compare 查找的row, 对应上述示例 compare.value = 'cch'
* @return the index of the found row
*/
int find(SearchRow compare, boolean bigger, boolean add, boolean compareKeys) {
// 目前 page 持有的数据量 ④
int l = 0, r = entryCount;
int comp = 1;
while (l < r) {
int i = (l + r) >>> 1;
// 根据 offsets[i],读取对应的 row 数据 ⑤
SearchRow row = getRow(i);
// 比大小 ⑥
comp = index.compareRows(row, compare);
if (comp == 0) {
// 唯一索引校验 ⑦
if (add && index.indexType.isUnique()) {
if (!index.containsNullAndAllowMultipleNull(compare)) {
throw index.getDuplicateKeyException(compare.toString());
}
}
}
if (comp > 0 || (!bigger && comp == 0)) {
r = i;
} else {
l = i + 1;
}
}
return l;
}
④ 每个块(page)entryCount ,两个方法初始化。根据块分配和实例创建初始化,或者 PageStore 读取块文件,从Page Data 解析得到。
⑤ 反序列化过程,从page 文件字节码(4k的字节数组),根据协议读取数据并实例化为 row 对象。参考: org.h2.index.PageBtreeIndex#readRow(org.h2.store.Data, int, boolean, boolean) 。
⑥ 全类型支持大小比对,具体的规则参考:org.h2.index.BaseIndex#compareRows
⑦ 如果数据中存在重复的键值,则不能创建唯一索引、UNIQUE 约束或 PRIMARY KEY 约束。h2database 兼容多种数据库模式,MySQL NULL 非唯一,MSSQLServer NULL 唯一,仅允许出现一次。
private int addRow(SearchRow row, boolean tryOnly) {
// 计算数据所占字节的长度
int rowLength = index.getRowSize(data, row, onlyPosition);
// 块大小,默认 4k
int pageSize = index.getPageStore().getPageSize();
// 块文件可用的 offset 获取
int last = entryCount == 0 ? pageSize : offsets[entryCount - 1];
if (last - rowLength < start + OFFSET_LENGTH) {
// 校验和尝试分配计算,这其中就涉及到分割页面生长 B 树的过程 ⑧
}
// undo log 让B树更可靠 ⑨
index.getPageStore().logUndo(this, data);
if (!optimizeUpdate) {
readAllRows();
}
int x = find(row, false, true, true);
// 新索引数据的offset 插入到 offsets 数组中。使用 System.arraycopy(x + 1) 来挪动数据。
offsets = insert(offsets, entryCount, x, offset);
// 重新计算 offsets,写磁盘就按照 offsets 来写入数据。
add(offsets, x + 1, entryCount + 1, -rowLength);
// 追加实际数据 row
rows = insert(rows, entryCount, x, row);
entryCount++;
// 标识 page.setChanged(true);
index.getPageStore().update(this);
return -1;
}
⑧如果你想添加一个新的键,你需要找到其范围能包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以反映新的键范围分区
⑨为了使数据库能处理异常崩溃的场景,B 树实现通常会带有一个额外的硬盘数据结构:预写式日志(WAL,即 write-ahead log,也称为 重做日志,即 redo log)。这是一个仅追加的文件,每个 B 树的修改在其能被应用到树本身的页面之前都必须先写入到该文件。当数据库在崩溃后恢复时,这个日志将被用来使 B 树恢复到一致的状态。
查询优化实质上就是访问数据量的优化,磁盘IO 的优化。
如果数据全部缓存到内存中,实际上就是计算量的优化,CPU 使用的优化。
索引是有序的,实际上就是指块文件内的 offsets 是以数组形式体现的。 特殊的是,在h2database 中,offsets数组元素也是有序的(例如:[4090, 4084, 4078, 4072, 4066, 4060, 4054, 4048, 4042]),应该是方便磁盘顺序读,防止磁盘碎片化。
理论上,聚集索引扫描 IO 比 BTree 索引要多,因为同样的块文件内,BTree 索引 存储的数据量更大,所占的块文件更少。如果一个table 列足够少,聚集索引扫描效率更高。
建表需要谨慎,每个列的字段长度尽可能的短,来节省页面空间。
合理使用覆盖索引查询,避免回表查询。 如述示例,select id from city where code = 'cch'
,扫描一次 BTree 索引即可得到结果。如果 select name from city where code = 'cch'
, 需要扫描一次 BTree 索引得到索引key (主键),再遍历扫描聚集索引,根据 key 得到结果。
合理的使用缓存,让磁盘IO 的影响降到最低。 比如合理配置缓存大小,冷热数据区分查询等。
这个问题非常有趣,不是SpringMVC 的问题,是使用了两种请求方式暴露出来的。
功能模块中,提供两个 Http 服务。一个是列表查询(application/json 请求),一个是列表导出(表单请求)。运行发现新增的参数,同样的 Http 请求,一个有值,一个没有🙄
代码如下:
/**
* application/json 请求
* @param param RequestResponseBodyMethodProcessr 处理 HttpServletRequest 参数
*/
@PostMapping(value = "query")
public ResponseResult<Page<SomeData>> queryByCondition(@RequestBody SomeParam param){
}
/**
* application/x-www-form-urlencoded 请求
* @param param ServletModelAttributeMethodProcessor 处理 HttpServletRequest 参数
*/
@PostMapping(value = "export")
public void exportExcel(SomeParam param) {
}
public class SomeParam {
// 这个是原有的,有 get set 方法
private String field1;
// 这个是新增的,没有get set 方法 🎈。 问题就出在这里。
private String field2;
}
处理 Http Body 的数据。解析注解 RequestBody 的参数。
针对 MimeType 为 application/json 的请求,按照json 格式进行反序列化。
默认参数处理器 MappingJackson2HttpMessageConverter
string 反序列化为对象,使用的是 com.fasterxml.jackson.databind.ObjectMapper。
上述工程中,对 ObjectMapper 开启 private 属性检测。新增的属性可以正常反序列化。
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
参考: [Jackson - Decide What Fields Get (De)Serialized | Baeldung](https://www.baeldung.com/jackson-field-serializable-deserializable-or-not) |
如果没有 setter 方法,jackson 会操作 field 来完成赋值。
/**
* This concrete sub-class implements property that is set directly assigning to a Field.
*/
public final static class FieldProperty extends SettableBeanProperty {
@Override
public final void set(Object instance, Object value) throws IOException {
try {
_field.set(instance, value);
} catch (Exception e) {
_throwAsIOE(e, value);
}
}
}
自定义 Class 参数解析
通过解析 request parameters, 用来构造和初始化对应的方法入参。
主要通过 ServletRequestDataBinder.bind(request) 来完成。
/**
* Apply given property values to the target object.
* By default, unknown fields will be ignored.
*
* @see org.springframework.validation.DataBinder#applyPropertyValues
*/
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
// 默认使用 BeanWrapperImpl.setPropertyValue()
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
}
}
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {
// 通过遍历 request parameters 来尝试对 target 进行赋值
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ? ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
try {
// etPropertyValue 使用 JDK 的 Introspector 来进行序列化操作。
// 没有setter 方法,自然没法赋值。
setPropertyValue(pv);
}
}
}
Servlet 规范中,针对如何处理异常,有对应的定义。
Tomcat 是一个Servlet容器,对于Servlet 规范的一种实现。
Servlet 3.0 引入了 Servlet 容器初始化时的可插拔性,允许开发者通过
ServletContainerInitializer
接口注册组件,这为 SpringBoot 框架集成提供了便利。SpringBoot 集成功能就包括自动注册异常处理,无需开发者关注容器层配置。
以下分别从Servlet 规范,Tomcat 实现,SpringBoot 默认配置三层来分析。
在Servlet规范中,
error-page
元素用于定义如何处理在Web应用程序中发生的错误。这个元素可以在 web.xml 部署描述文件中配置,用于指定特定错误代码或 Java 异常类型的处理页面。
error-page
的示例-HTTP状态码<!-- 当发生404错误时,会显示/error-404.html页面。-->
<error-page>
<error-code>404</error-code>
<location>/error-404.html</location>
</error-page>
error-page
的示例-Java异常类型<!-- 当发生NullPointerException时,会显示/error-help.html页面。-->
<error-page>
<exception-type>java.lang.NullPointerException</exception-type>
<location>/error-help.html</location>
</error-page>
error-page 解析和实例化过程,参考: org.apache.catalina.core.StandardContext#addErrorPage
SpringBoot 初始化的过程中,会调用 addErrorPage,进行默认异常配置
error-page 配置的执行过程,就是服务器内部转发的过程。 即 RequestDispatcher.forward(request, response)
/**
* Return the error page entry for the specified Java exception type,
* if any; otherwise return <code>null</code>.
*
* @see org.apache.catalina.core.StandardContext#findErrorPage(java.lang.String)
*/
@Override
public ErrorPage findErrorPage(String exceptionType) {
synchronized (exceptionPages) {
return (exceptionPages.get(exceptionType));
}
}
/**
* 处理HTTP状态码或Java异常,转发到指定的 errorPage 页面。
*
* @param errorPage The errorPage directive we are obeying
* @see org.apache.catalina.core.StandardHostValve#custom
*/
private boolean custom(Request request, Response response, ErrorPage er rorPage) {
try {
// Forward control to the specified location
ServletContext servletContext = request.getContext().getServletContext();
RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage. getLocation());
// 熟悉的 servlet 操作
if (response.isCommitted()) {
// 它用于在当前响应中包含另一个资源的内容。调用 include 方法不会清除响应缓冲区,当前Servlet的输出和被包含资源的输出将一起发送给客户端。
rd.include(request.getRequest(), response.getResponse());
} else {
// 它用于将请求完全转发到另一个资源。新的资源将处理整个请求,并且原始请求的响应将被丢弃。
rd.forward(request.getRequest(), response.getResponse());
}
// Indicate that we have successfully processed this custom page
return (true);
}
}
Spring Boot 提供默认的错误处理机制,BasicErrorController
。通过类似 Tomcat 添加error-page配置的方式,把默认的 ErrorPage 注册到 Java Web 容器中.
对应的异常处理器是 BasicErrorController ,转发到当前Controller 的异常,返回 ResponseEntity<>(exception, status) 给前台。
参考:org.springframework.boot.autoconfigure.web.BasicErrorController#error
/**
* configures the container's error pages.
* @see org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// 默认使用/error, @see BasicErrorController's RequestMapping("${server.error.path:${error.path:/error}}")
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath());
// 约等于 Tomcat container.addErrorPages
errorPageRegistry.addErrorPages(errorPage);
}
}
在SpringMVC中,
@ExceptionHandler
注解用于处理 Controller 中的特定异常。Controller 发生注解指定的异常类型时,调用这个方法来处理异常。
配置示例:
@Controller
public class MyController {
@ExceptionHandler(value = { NullPointerException.class })
public ModelAndView handleMyCustomException(NullPointerException ex) {
ModelAndView mav = new ModelAndView();
mav.addObject("error", ex.getMessage());
mav.setViewName("errorPage");
return mav;
}
}
error-page 属于 Servlet 规范,各种 Java Web 容器都有对应的实现
SpringBoot 在初始化 web 环境过程中,就默认注册了异常处理,当应用异常发生时,托底处理,返回错误信息。
SpringMVC 的异常处理配置是框架内部的机制,即使 SpringMVC 无法处理的异常,还有 SpringBoot 配置到 Web 容器(Tomcat)中的异常处理。
如果SpringBoot 异常处理 BasicErrorController 发生了异常,还有Web 容器(Tomcat)默认的异常处理机制来兜底。
全局的异常处理设计,使得异常处理逻辑与业务逻辑分离,有助于代码的维护和清晰性。
从 error-page 在各层次的配置和作用原理中,又可以学习到架构分层的思想。
在应用工程里,存在一个后台执行 SQL 脚本的接口。在大量的使用后,提示异常:
Could not create connection to database server. Attempted reconnect 3 times. Giving up.
同时,其他连接这个 mysql 的应用,也提示异常。执行 SQL 使用数据库连接池,理论上不应该出现连接超量的情况。
// 通过指定数据源,执行对应的 update SQL
@PostMapping("/update/{dataSource}/{count}")
public ResponseResult<String> update(@PathVariable String dataSource, @PathVariable Integer count , @RequestBody String sql) {
// 获取数据源
Map<String, DataSource> dataSourceMap = getDataSources();
DataSource source = dataSourceMap.get(dataSource);
// 执行
return ResponseResult.ok(sqlService.execute(source,sql,count));
}
代码比较简单,猜测可能有以下两个问题:
在执行过程中,没有close connection,导致连接池一直在创建新的 connection。
获取数据源的过程中,存在数据源对象泄露,导致创建了很多 DataSource。
jdbc 关闭连接的顺序是:ResultSet → Statement → Connection
在工程代码中,没有发现手动关闭 Statement 的操作。是不是会阻塞 connection close ?
/**
* HikariDataSource Connection 会自动关闭 Statement
* @see com.zaxxer.hikari.pool.ProxyConnection This is the proxy class for java.sql.Connection.
*/
@Override
public final void close() throws SQLException {
// automatic resource cleanup
closeStatements();
if (delegate != ClosedConnection.CLOSED_CONNECTION) {
try {
// fall back operation...
if (isCommitStateDirty && !isAutoCommit) {
delegate.rollback();
}
}
finally {
// return resource
delegate = ClosedConnection.CLOSED_CONNECTION;
poolEntry.recycle(lastAccess);
}
}
}
// 关闭 statement, 会级联关闭 results
// @see com.mysql.jdbc.StatementImpl#realClose
protected synchronized void realClose(boolean calledExplicitly, boolean closeOpenResults) throws SQLException {
if (closeOpenResults) {
if (this.results != null) {
try {
this.results.close();
} catch (Exception var4) {
}
}
this.closeAllOpenResults();
}
}
/**
* We only create connections if we need another idle connection or have threads still waiting
* for a new connection. Otherwise we bail out of the request to create.
*
* @return true if we should create a connection, false if the need has disappeared
* @see com.zaxxer.hikari.pool.HikariPool.PoolEntryCreator#shouldCreateAnotherConnection
*/
private synchronized boolean shouldCreateAnotherConnection() {
// The property controls the maximum number of connections that HikariCP will keep in the pool, including both idle and in-use connections.
return getTotalConnections() < config.getMaximumPoolSize() &&
(connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle());
}
终于理解Spring Boot 为什么青睐HikariCP了,图解的太透彻了!
源码结合实践,数据库连接池肯定不会无限制创建连接。
public Map<String, DataSource> getDataSources(){
// 没有缓存或者单例处理,每调用一次,初始化一次 DataSource 😅
Map<String, DataSource> dataSources = new HashMap<>();
datadaSourcesConfig.forEach((k, v) -> {
// spring boot, Convenience class for building a DataSource with common implementations and properties.
DataSourceBuilder builder = DataSourceBuilder.create();
builder.url(v.getUrl());
builder.username(v.getUsername());
DataSource dataSource = builder.build();
dataSources.put(k, dataSource);
});
return dataSources;
}
先入为主的想法,会让排查思路受困,不能快速找到问题。例如:出现too many connections
异常,就直接认为是使用 connection 过程中有问题,其实是创建了太多的连接池,间接持有了很多 connection 导致的。
对于所使用的 DataSource,以及 jdbc 底层的执行机制不了解,导致排查过程中疑点众多,不能快速定位。
从线上的异常堆栈也能看出来,是初始化数据源过程抛出异常。因为不了解 HikariDataSource 工作机制,忽略这个重要信息。
框架和工具的自动化处理,让编程工作变得更加方便。例如: jdbc connection close 模板代码,已经没有必要。
Could not create connection to database server. Attempted reconnect 3 times. Giving up. – mysql-connector-java-5.1.47.jar
// 启用自动连接,HighAvailability
private BooleanConnectionProperty autoReconnect = new BooleanConnectionProperty("autoReconnect", false, Messages.getString("ConnectionProperties.autoReconnect"), "1.1", HA_CATEGORY, 0);
// 最多尝试连接次数,默认 3 次。
private IntegerConnectionProperty maxReconnects = new IntegerConnectionProperty("maxReconnects", 3, 1, Integer.MAX_VALUE, Messages.getString("ConnectionProperties.maxReconnects"), "1.1", HA_CATEGORY, 4);
// 异常提示配置 mysql-connector-java-5.1.47.jar!\com\mysql\jdbc\LocalizedErrorMessages.properties
Connection.UnableToConnectWithRetries=Could not create connection to database server. \
Attempted reconnect {0} times. Giving up.
// autoReconnect
public void createNewIO(boolean isForReconnect) throws SQLException {
synchronized (getConnectionMutex()) {
// jdbc.url autoReconnect 指定为 true,识别为 HighAvailability
if (!getHighAvailability()) {
connectOneTryOnly(isForReconnect, mergedProps);
return;
}
// maxReconnects 默认为 3,重试失败的提示就是: Attempted reconnect 3 times. Giving up.
connectWithRetries(isForReconnect, mergedProps);
}
}
-- 查看最大连接数
show variables LIKE "max_connections"
-- 修改最大连接数(临时修改)
set GLOBAL max_connections=1024
About Pool Sizing · brettwooldridge/HikariCP Wiki · GitHub
Even a computer with one CPU core can “simultaneously” support dozens or hundreds of threads. But we all [should] know that this is merely a trick by the operating system though the magic of time-slicing. 并发和并行的区别。
In reality, that single core can only execute one thread at a time; then the OS switches contexts and that core executes code for another thread, and so on. 多线程的切换是有代价的。
When we look at what the major bottlenecks for a database are, they can be summarized as three basic categories: CPU, Disk, Network. 数据库性能分析点
On a server with 8 computing cores, setting the number of connections to 8 would provide optimal performance, and anything beyond this would start slowing down due to the overhead of context switching. 理想情况
And it is during this time that the OS could put that CPU resource to better use by executing some more code for another thread. So, because threads become blocked on I/O, we can actually get more work done by having a number of connections/threads that is greater than the number of physical computing cores. 综合各种情况,需要一套公式和压测,决定如何充分发挥服务器性能