Java Buffer緩沖區(qū)操作與內(nèi)存管理最佳實踐
Buffer緩沖區(qū)操作與內(nèi)存管理
1 Buffer的設計原理和內(nèi)存模型
1.1 Buffer到底是什么
Buffer就是Java NIO里的數(shù)據(jù)容器,專門用來存放各種基本類型的數(shù)據(jù)。你可以把它想象成一個智能的數(shù)組,不僅能存數(shù)據(jù),還知道自己當前讀到哪了、寫到哪了。
和Channel配合使用時,Buffer就像是數(shù)據(jù)的中轉(zhuǎn)站。Channel負責傳輸,Buffer負責存儲,兩者分工明確。
Buffer有幾個設計特點:
- 專一性:每種數(shù)據(jù)類型都有對應的Buffer,比如ByteBuffer、IntBuffer
- 內(nèi)存靈活:可以用堆內(nèi)存,也可以用堆外內(nèi)存
- 狀態(tài)清晰:讀模式和寫模式分得很清楚
- 自動跟蹤:會自動記錄當前操作的位置
1.2 Buffer家族成員
Java NIO的Buffer家族結(jié)構(gòu)很簡單,就是一個抽象父類加上各種具體實現(xiàn):
Buffer (抽象類) ├── ByteBuffer // 最常用的,處理字節(jié)數(shù)據(jù) │ └── MappedByteBuffer // 內(nèi)存映射文件專用 │ ├── DirectByteBuffer // 直接內(nèi)存實現(xiàn) │ └── FileChannelImpl.MappedByteBufferAdapter ├── CharBuffer // 處理字符 ├── DoubleBuffer // 處理雙精度浮點數(shù) ├── FloatBuffer // 處理單精度浮點數(shù) ├── IntBuffer // 處理整數(shù) ├── LongBuffer // 處理長整數(shù) └── ShortBuffer // 處理短整數(shù)
ByteBuffer是老大,用得最多,因為網(wǎng)絡傳輸和文件操作基本都是字節(jié)流。MappedByteBuffer是個特殊的存在,專門用來做內(nèi)存映射文件,讀寫大文件時特別有用。
1.3 Buffer的內(nèi)存模型
Buffer的內(nèi)存可以從兩個角度來看:
1.3.1 邏輯上怎么理解
邏輯上,Buffer就是一個有序的數(shù)組,里面裝著同一種類型的數(shù)據(jù)。每個位置都有索引,你可以直接跳到任意位置讀寫數(shù)據(jù)。
Buffer用三個重要的指針來管理這個數(shù)組:position(當前位置)、limit(邊界)和capacity(總?cè)萘浚_@三個指針決定了你能在哪讀、在哪寫、總共有多大空間。
1.3.2 物理上怎么實現(xiàn)
物理實現(xiàn)上,Buffer有兩種存儲方式:
堆緩沖區(qū)(HeapBuffer):
- 數(shù)據(jù)存在JVM堆內(nèi)存里
- 底層就是個普通的Java數(shù)組
- 會被垃圾回收器管理
- 創(chuàng)建方式:
ByteBuffer.allocate(1024)
直接緩沖區(qū)(DirectBuffer):
- 數(shù)據(jù)存在操作系統(tǒng)的原生內(nèi)存里(堆外內(nèi)存)
- 不占用JVM堆空間
- 垃圾回收器管不著,需要手動或等待回收
- 創(chuàng)建方式:
ByteBuffer.allocateDirect(1024)
還有個特殊的MappedByteBuffer,它把文件直接映射到內(nèi)存里,讀寫文件就像操作內(nèi)存一樣快。
2 position、limit、capacity三大屬性詳解
Buffer有三個關鍵屬性:position、limit和capacity。這三個屬性就像是Buffer的GPS,告訴你現(xiàn)在在哪、能到哪、總共有多大。
2.1 capacity(容量)
capacity就是Buffer的總?cè)萘?,?chuàng)建時就定死了,后面改不了。就像買了個1024字節(jié)的水桶,不管你裝多少水,桶的容量就是1024。
- 含義:Buffer最多能裝多少個元素
- 特點:一旦創(chuàng)建就固定了
- 范圍:capacity ≥ 0
// 創(chuàng)建一個能裝1024個字節(jié)的Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); int capacity = buffer.capacity(); // 返回1024,永遠不變
2.2 position(位置)
position是個移動的指針,指向下一個要操作的位置。每次讀寫數(shù)據(jù),這個指針就會自動往前移。
- 含義:下一個要讀或?qū)懙奈恢?/li>
- 特點:會隨著操作自動移動
- 范圍:0 ≤ position ≤ limit
寫模式時,position指向下一個要寫入的地方;讀模式時,position指向下一個要讀取的地方。
ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte) 'A'); // position從0跳到1 buffer.put((byte) 'B'); // position從1跳到2 int currentPosition = buffer.position(); // 現(xiàn)在是2
2.3 limit(限制)
limit是個邊界線,告訴你最多能操作到哪里。超過這條線就不能讀寫了。
- 含義:第一個不能碰的位置
- 特點:可以手動調(diào)整
- 范圍:position ≤ limit ≤ capacity
寫模式時,limit就是capacity(能寫滿整個Buffer);讀模式時,limit是之前寫了多少數(shù)據(jù)。
ByteBuffer buffer = ByteBuffer.allocate(10); // 寫模式:limit = capacity = 10,可以寫滿 int limitInWriteMode = buffer.limit(); // 返回10 // 寫入3個字節(jié)后切換到讀模式 buffer.put((byte) 'A').put((byte) 'B').put((byte) 'C'); buffer.flip(); // 切換到讀模式 // 讀模式:limit = 3,只能讀這3個字節(jié) int limitInReadMode = buffer.limit(); // 返回3
2.4 三個屬性的關系圖解
用一個例子來看看這三個屬性是怎么配合工作的:
// 創(chuàng)建一個能裝8個字節(jié)的Buffer
ByteBuffer buffer = ByteBuffer.allocate(8);
System.out.println("剛創(chuàng)建時:");
System.out.println("capacity: " + buffer.capacity()); // 8,總?cè)萘?
System.out.println("limit: " + buffer.limit()); // 8,能寫到哪
System.out.println("position: " + buffer.position()); // 0,當前位置
// 寫入數(shù)據(jù)
buffer.put("ABCD".getBytes());
System.out.println("\n寫入ABCD后:");
System.out.println("capacity: " + buffer.capacity()); // 8,總?cè)萘坎蛔?
System.out.println("limit: " + buffer.limit()); // 8,還能繼續(xù)寫
System.out.println("position: " + buffer.position()); // 4,指針移到第4位
// 切換到讀模式
buffer.flip();
System.out.println("\nflip()切換讀模式后:");
System.out.println("capacity: " + buffer.capacity()); // 8,總?cè)萘坎蛔?
System.out.println("limit: " + buffer.limit()); // 4,只能讀4個字節(jié)
System.out.println("position: " + buffer.position()); // 0,從頭開始讀狀態(tài)變化圖示:
剛創(chuàng)建(寫模式):
[0][1][2][3][4][5][6][7]
^ ^
position limit/capacity
寫入ABCD后:
[A][B][C][D][ ][ ][ ][ ]
^ ^
position limit/capacity
flip()后(讀模式):
[A][B][C][D][ ][ ][ ][ ]
^ ^ ^
position limit capacity2.5 mark(標記)
mark就像是在Buffer上做個書簽,記住某個位置,以后可以快速跳回來。
- 含義:臨時記住的位置
- 特點:可有可無,默認沒有
- 范圍:mark ≤ position
ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put((byte) 'A'); buffer.put((byte) 'B'); buffer.mark(); // 在position=2的地方做個書簽 buffer.put((byte) 'C'); buffer.reset(); // 跳回書簽位置(position=2)
3 讀寫模式切換和常用操作方法
3.1 讀寫模式切換
Buffer就像一個雙向通道,可以往里寫數(shù)據(jù),也可以從里面讀數(shù)據(jù)。但不能同時進行,需要在寫模式和讀模式之間切換。
3.1.1 flip():寫模式切換到讀模式
public final Buffer flip() {
limit = position; // 設置讀取邊界
position = 0; // 從頭開始讀
mark = -1; // 清除書簽
return this;
}flip()就像翻書一樣,做三件事:
- 把當前寫到的位置設為讀取邊界(limit = position)
- 把讀取指針拉回到開頭(position = 0)
- 清除之前的書簽(mark = -1)
這樣就能從頭開始讀取剛才寫入的數(shù)據(jù)了。
3.1.2 clear():讀模式切換到寫模式
public final Buffer clear() {
position = 0; // 從頭開始寫
limit = capacity; // 可以寫滿整個Buffer
mark = -1; // 清除書簽
return this;
}clear()就像清空黑板重新寫字,做三件事:
- 把寫入指針拉回到開頭(position = 0)
- 允許寫滿整個Buffer(limit = capacity)
- 清除之前的書簽(mark = -1)
注意:clear()并不會真的清除數(shù)據(jù),只是重置了指針,舊數(shù)據(jù)會被新數(shù)據(jù)覆蓋。
3.1.3 compact():部分讀模式切換到寫模式
public ByteBuffer compact() {
// 把沒讀完的數(shù)據(jù)移到前面
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
position(remaining()); // position指向未讀數(shù)據(jù)后面
limit(capacity()); // 可以寫滿整個Buffer
discardMark(); // 清除書簽
return this;
}compact()比較聰明,它會保留沒讀完的數(shù)據(jù):
- 把沒讀完的數(shù)據(jù)移到Buffer開頭
- position指向這些數(shù)據(jù)的后面(可以繼續(xù)寫新數(shù)據(jù))
- limit設為capacity(允許寫滿)
- 清除書簽
這樣既保留了未讀數(shù)據(jù),又能繼續(xù)寫入新數(shù)據(jù)。
3.2 常用操作方法
3.2.1 分配Buffer
// 在JVM堆內(nèi)存里創(chuàng)建,速度快 ByteBuffer heapBuffer = ByteBuffer.allocate(1024); // 在系統(tǒng)內(nèi)存里創(chuàng)建,I/O效率高 ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 把現(xiàn)有數(shù)組包裝成Buffer byte[] array = new byte[1024]; ByteBuffer wrappedBuffer = ByteBuffer.wrap(array);
3.2.2 寫入數(shù)據(jù)
// 寫一個字節(jié),position自動+1 buffer.put((byte) 127); // 寫一串字節(jié) byte[] data = "Hello".getBytes(); buffer.put(data); // 在指定位置寫入,不影響position buffer.put(3, (byte) 65); // 在第3個位置寫入'A' // 從數(shù)組的某個位置開始,寫入指定長度 buffer.put(data, 1, 3); // 從data[1]開始寫3個字節(jié)
3.2.3 讀取數(shù)據(jù)
// 讀一個字節(jié),position自動+1 byte b = buffer.get(); // 讀一串字節(jié)到數(shù)組里 byte[] data = new byte[10]; buffer.get(data); // 從指定位置讀取,不影響position byte b = buffer.get(3); // 讀取第3個位置的字節(jié) // 讀指定長度到數(shù)組的某個位置 buffer.get(data, 1, 3); // 讀3個字節(jié)到data[1]開始的位置
3.2.4 其他常用操作
// 做書簽和跳回書簽 buffer.mark(); // 在當前position做個書簽 buffer.reset(); // 跳回書簽位置 // 倒帶,position回到0 buffer.rewind(); // 檢查還能讀多少 boolean hasRemaining = buffer.hasRemaining(); // 還有數(shù)據(jù)嗎? int remaining = buffer.remaining(); // 還剩多少個(limit - position) // 手動移動position buffer.position(buffer.position() + 3); // 跳過3個位置 // 復制Buffer,共享數(shù)據(jù)但各自有獨立的指針 ByteBuffer duplicate = buffer.duplicate(); // 創(chuàng)建只讀版本,不能修改數(shù)據(jù) ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); // 切片,共享一部分數(shù)據(jù) ByteBuffer slicedBuffer = buffer.slice(2, 5); // 從位置2開始,長度5的片段
3.3 Buffer操作的最佳實踐
- 檢查返回值:讀寫操作可能沒有處理完所有數(shù)據(jù)
- 記得flip():寫完數(shù)據(jù)要調(diào)用flip()才能讀取
- 重復利用:用clear()或compact()重用Buffer,別老是new
- 選對類型:什么數(shù)據(jù)用什么Buffer(ByteBuffer、IntBuffer等)
- 直接緩沖區(qū)要小心:它不歸垃圾回收器管,用完要手動釋放
4 直接緩沖區(qū)vs非直接緩沖區(qū)的性能差異
4.1 兩種緩沖區(qū)的實現(xiàn)機制
4.1.1 非直接緩沖區(qū)(HeapBuffer)
非直接緩沖區(qū)就是在JVM堆內(nèi)存里創(chuàng)建的Buffer,底層用的是普通Java數(shù)組。做I/O操作時,JVM需要把數(shù)據(jù)在堆內(nèi)存和系統(tǒng)內(nèi)存之間復制一遍。
ByteBuffer heapBuffer = ByteBuffer.allocate(1024); // 在堆內(nèi)存里創(chuàng)建
內(nèi)部實現(xiàn):
// HeapByteBuffer的底層實現(xiàn)(簡化版)
final byte[] hb; // 就是個普通數(shù)組
final int offset; // 數(shù)組里的起始位置
// 讀取操作
public byte get() {
return hb[ix(nextGetIndex())];
}
// 寫入操作
public ByteBuffer put(byte b) {
hb[ix(nextPutIndex())] = b;
return this;
}4.1.2 直接緩沖區(qū)(DirectBuffer)
直接緩沖區(qū)是在系統(tǒng)內(nèi)存里創(chuàng)建的Buffer,不在JVM堆里。它通過JNI調(diào)用系統(tǒng)API分配內(nèi)存,Java通過Unsafe類來訪問這塊內(nèi)存。
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 在系統(tǒng)內(nèi)存里創(chuàng)建
內(nèi)部實現(xiàn):
// DirectByteBuffer的底層實現(xiàn)(簡化版)
private long address; // 系統(tǒng)內(nèi)存地址
// 讀取操作
public byte get() {
return Unsafe.getByte(ix(nextGetIndex())); // 直接從系統(tǒng)內(nèi)存讀
}
// 寫入操作
public ByteBuffer put(byte b) {
Unsafe.putByte(ix(nextPutIndex()), b); // 直接寫到系統(tǒng)內(nèi)存
return this;
}4.2 性能差異分析
4.2.1 內(nèi)存分配性能
| 緩沖區(qū)類型 | 分配速度 | 釋放速度 | 內(nèi)存壓力 |
|---|---|---|---|
| 非直接緩沖區(qū) | 快 | GC自動回收 | 占用堆內(nèi)存 |
| 直接緩沖區(qū) | 慢(要調(diào)系統(tǒng)API) | 看GC臉色或手動釋放 | 不占堆內(nèi)存 |
直接緩沖區(qū)創(chuàng)建比較慢,因為要調(diào)用系統(tǒng)API分配內(nèi)存。但一旦創(chuàng)建好了,做I/O操作時通常比非直接緩沖區(qū)快。
4.2.2 I/O操作性能
| 緩沖區(qū)類型 | 讀寫性能 | 數(shù)據(jù)復制 | 適用場景 |
|---|---|---|---|
| 非直接緩沖區(qū) | 一般 | 要在堆內(nèi)存和系統(tǒng)內(nèi)存間復制 | 小數(shù)據(jù)、偶爾用用 |
| 直接緩沖區(qū) | 快 | 不用復制 | 大數(shù)據(jù)、頻繁I/O |
直接緩沖區(qū)做I/O時比較快,因為不用在堆內(nèi)存和系統(tǒng)內(nèi)存之間復制數(shù)據(jù)。用Channel傳輸數(shù)據(jù)時,直接緩沖區(qū)可以直接參與,而非直接緩沖區(qū)還得先復制到一個臨時的直接緩沖區(qū)里。
4.2.3 內(nèi)存訪問性能
| 緩沖區(qū)類型 | 讀寫速度 | CPU緩存友好性 | JVM優(yōu)化 |
|---|---|---|---|
| 非直接緩沖區(qū) | 一般更快 | 好 | JIT能優(yōu)化 |
| 直接緩沖區(qū) | 通過JNI訪問,可能慢點 | 一般 | 優(yōu)化有限 |
如果只是在Java代碼里頻繁讀寫B(tài)uffer,非直接緩沖區(qū)通常更快,因為它在JVM堆里,JIT編譯器能優(yōu)化,對CPU緩存也更友好。
4.3 性能測試案例
來個實際測試,看看兩種Buffer在不同場景下的表現(xiàn):
public class BufferPerformanceTest {
private static final int BUFFER_SIZE = 1024 * 1024; // 1MB大小
private static final int ITERATIONS = 1000; // 測試1000次
public static void main(String[] args) throws Exception {
// 測試創(chuàng)建速度
testAllocation();
// 測試I/O速度
testIO();
// 測試讀寫速度
testMemoryAccess();
}
private static void testAllocation() {
long start, end;
// 測試堆內(nèi)存Buffer創(chuàng)建速度
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); // 在堆里創(chuàng)建
buffer.put((byte) 1); // 寫點數(shù)據(jù)
}
end = System.nanoTime();
System.out.println("堆Buffer創(chuàng)建: " + (end - start) / 1000000 + "ms");
// 測試直接Buffer創(chuàng)建速度
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); // 在系統(tǒng)內(nèi)存創(chuàng)建
buffer.put((byte) 1); // 寫點數(shù)據(jù)
}
end = System.nanoTime();
System.out.println("直接Buffer創(chuàng)建: " + (end - start) / 1000000 + "ms");
}
private static void testIO() throws Exception {
File tempFile = File.createTempFile("buffer-test", ".tmp"); // 創(chuàng)建臨時文件
tempFile.deleteOnExit(); // 程序結(jié)束時刪除
// 準備測試數(shù)據(jù)
ByteBuffer data = ByteBuffer.allocate(BUFFER_SIZE);
while (data.hasRemaining()) {
data.put((byte) 'A'); // 填充數(shù)據(jù)
}
data.flip(); // 切換到讀模式
// 測試堆Buffer的I/O速度
ByteBuffer heapBuffer = ByteBuffer.allocate(BUFFER_SIZE);
testFileIO(heapBuffer, tempFile, "堆Buffer I/O");
// 測試直接Buffer的I/O速度
ByteBuffer directBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
testFileIO(directBuffer, tempFile, "直接Buffer I/O");
}
private static void testFileIO(ByteBuffer buffer, File file, String label) throws Exception {
FileChannel channel = new FileOutputStream(file).getChannel(); // 獲取文件通道
long start = System.nanoTime(); // 開始計時
for (int i = 0; i < ITERATIONS; i++) {
buffer.clear(); // 清空Buffer,準備寫入
buffer.put(new byte[BUFFER_SIZE]); // 寫入數(shù)據(jù)
buffer.flip(); // 切換到讀模式
while (buffer.hasRemaining()) {
channel.write(buffer); // 寫到文件
}
}
channel.close(); // 關閉通道
long end = System.nanoTime(); // 結(jié)束計時
System.out.println(label + ": " + (end - start) / 1000000 + "ms");
}
private static void testMemoryAccess() {
ByteBuffer heapBuffer = ByteBuffer.allocate(BUFFER_SIZE); // 堆Buffer
ByteBuffer directBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE); // 直接Buffer
// 測試堆Buffer讀寫速度
long start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
for (int j = 0; j < 1024; j++) {
heapBuffer.put(j, (byte) j); // 在指定位置寫入
}
for (int j = 0; j < 1024; j++) {
byte b = heapBuffer.get(j); // 從指定位置讀取
}
}
long end = System.nanoTime();
System.out.println("堆Buffer讀寫: " + (end - start) / 1000000 + "ms");
// 測試直接Buffer讀寫速度
start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
for (int j = 0; j < 1024; j++) {
directBuffer.put(j, (byte) j); // 在指定位置寫入
}
for (int j = 0; j < 1024; j++) {
byte b = directBuffer.get(j); // 從指定位置讀取
}
}
end = System.nanoTime();
System.out.println("直接Buffer讀寫: " + (end - start) / 1000000 + "ms");
}
}4.4 選擇合適的緩沖區(qū)類型
根據(jù)性能特點,可以按照以下原則選擇Buffer類型:
| 場景 | 推薦Buffer類型 | 原因 |
|---|---|---|
| 大文件I/O | 直接Buffer | 不用復制內(nèi)存,I/O快 |
| 網(wǎng)絡通信 | 直接Buffer | 減少數(shù)據(jù)復制,吞吐量高 |
| 臨時小數(shù)據(jù)處理 | 堆Buffer | 創(chuàng)建快,GC友好 |
| 頻繁創(chuàng)建銷毀 | 堆Buffer | 避免直接Buffer創(chuàng)建的開銷 |
| 長期重用的Buffer | 直接Buffer | 一次創(chuàng)建多次使用,攤銷成本 |
在物聯(lián)網(wǎng)平臺的實際應用中,通常會根據(jù)不同場景選擇不同Buffer類型:
- 設備數(shù)據(jù)采集:用直接Buffer處理大量傳感器數(shù)據(jù)
- 配置信息傳輸:用堆Buffer處理小型配置數(shù)據(jù)
- 文件存儲:用直接Buffer或MappedByteBuffer處理大型日志文件
- 實時數(shù)據(jù)處理:用直接Buffer提高網(wǎng)絡通信效率
5 總結(jié)
Buffer是Java NIO的核心組件,就像一個智能的數(shù)據(jù)容器,讓我們能高效地處理各種數(shù)據(jù)。通過掌握Buffer的工作原理、核心屬性和操作方法,以及兩種Buffer類型的性能特點,我們就能在物聯(lián)網(wǎng)平臺等高性能應用中做出更好的技術選擇。
Buffer的核心價值:
- 和Channel完美配合:讓I/O操作變得高效
- 靈活的內(nèi)存管理:既能用堆內(nèi)存,也能用系統(tǒng)內(nèi)存
- 精確的狀態(tài)控制:通過position、limit、capacity準確控制數(shù)據(jù)讀寫
- 類型安全:針對不同數(shù)據(jù)類型提供專門的Buffer
掌握了Buffer,我們就為學習Java NIO打下了堅實基礎。下一篇文章,我們將深入探討Selector選擇器,看看它如何實現(xiàn)多路復用I/O,讓一個線程同時處理多個連接。
到此這篇關于Java Buffer緩沖區(qū)操作與內(nèi)存管理的文章就介紹到這了,更多相關Java Buffer緩沖區(qū)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JAVA面試題 從源碼角度分析StringBuffer和StringBuilder的區(qū)別
這篇文章主要介紹了JAVA面試題 從源碼角度分析StringBuffer和StringBuilder的區(qū)別,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,下面我們來一起學習下吧2019-07-07
為什么ConcurrentHashMap的key value不能為null,map可以?
這篇文章主要介紹了為什么ConcurrentHashMap的key value不能為null,map可以呢?具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01
實戰(zhàn)分布式醫(yī)療掛號通用模塊統(tǒng)一返回結(jié)果異常日志處理
這篇文章主要為大家介紹了實戰(zhàn)分布式醫(yī)療掛號系統(tǒng)之統(tǒng)一返回結(jié)果統(tǒng)一異常處理,統(tǒng)一日志處理到通用模塊示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-04-04
SpringBoot2 整合 ClickHouse數(shù)據(jù)庫案例解析
這篇文章主要介紹了SpringBoot2 整合 ClickHouse數(shù)據(jù)庫案例解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-10-10

