jdbc mysql設定rewriteBatchedStatements引數實現高效能批量處理 executeBatch返回值問題
一、摘要
利用jdbc預處理PreparedStatement.executeBatch可實現sql批處理,但是資料庫層面是否真正實現批處理,不同資料庫表現不一。以mysql為例,只有jdbcUrl設定了rewriteBatchedStatements=true引數,mysql驅動才會真正執行sql批處理,從而顯著提高效能。但是一旦設定rewriteBatchedStatements=true後,PreparedStatement.executeBatch()的返回結果也會發生變化,為此程式碼需要特殊處理。
二、有無引數rewriteBatchedStatements比較
1、資料表 jdbc_student
CREATE TABLE `jdbc_student` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2、沒有rewriteBatchedStatements=true引數
java程式碼
public static void main(String[] args) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver").newInstance();
String jdbcUrl = "jdbc:mysql://ip:3306/dbname?useUnicode=true&characterEncoding=utf-8";
conn = DriverManager.getConnection(jdbcUrl,"user", "pwd");
int batchSize = 5000;
int count = 0;
conn.setAutoCommit( false); //設定自動提交為false
PreparedStatement ps = conn.prepareStatement("insert into jdbc_student (name, age) values (?,?)");
for (int i = 1; i <= batchSize; i++) {
ps.setString(1, "name: " + i); //設定第2個引數, name
ps.setInt(2, i % 30 + 10); //設定第3個引數, age
ps.addBatch(); //將該條記錄新增到批處理中
}
Long t1 = System.currentTimeMillis();
System.out.println("開始執行: " + t1);
int rows[] = ps.executeBatch();
for(int row : rows) {
count += row;
}
conn.commit(); //提交
ps.close();
Long t2 = System.currentTimeMillis();
System.out.println("執行結束:" + t2 + ", 耗時:"+(t2-t1)/1000+"秒, batchRows: "+batchSize+"條, affectedRows: " + count);
if(conn!=null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
if(conn!=null) {
try {
conn.close();
} catch (Exception ee) {
//TODO
}
}
}
}
輸出結果
開始執行: 1540629124028
執行結束: 1540629189490, 耗時:65秒, batchRows: 5000條, affectedRows: 5000
3、設定rewriteBatchedStatements=true引數
java程式碼
public static void main(String[] args) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver").newInstance();
String jdbcUrl = "jdbc:mysql://ip:3306/dbname?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=utf-8";
conn = DriverManager.getConnection(jdbcUrl,"user", "pwd");
int batchSize = 5000;
int count = 0;
conn.setAutoCommit(false); //設定自動提交為false
PreparedStatement ps = conn.prepareStatement("insert into jdbc_student (name, age) values (?,?)");
for (int i = 1; i <= batchSize; i++) {
ps.setString(1, "name: " + i); //設定第2個引數, name
ps.setInt(2, i % 30 + 10); //設定第3個引數, age
ps.addBatch(); //將該條記錄新增到批處理中
}
Long t1 = System.currentTimeMillis();
System.out.println("開始執行: " + t1);
int rows[] = ps.executeBatch();
for(int row : rows) {
count += row;
}
conn.commit(); //提交
ps.close();
Long t2 = System.currentTimeMillis();
System.out.println("執行結束:" + t2 + ", 耗時:"+(t2-t1)/1000+"秒, batchRows: "+batchSize+"條, affectedRows: " + count);
if(conn!=null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
if(conn!=null) {
try {
conn.close();
} catch (Exception ee) {
//TODO
}
}
}
}
輸出結果
開始執行: 1540629414786
執行結束: 1540629414931, 耗時:0秒, batchRows: 5000條, affectedRows: 25000000
三、executeBatch返回值處理
1、有無rewriteBatchedStatements引數,兩者執行效能真是天壤之別:
無rewriteBatchedStatements引數,執行時間:65秒
有rewriteBatchedStatements引數,執行時間:不到1秒
2、executeBatch返回值:
無rewriteBatchedStatements引數,返回值:int[]累加,5000
有rewriteBatchedStatements引數,返回值:int[]累加,25000000
3、有rewriteBatchedStatements引數,executeBatch返回值解釋:
1)設定rewriteBatchedStatements引數後,可以簡單理解為資料庫將5000條insert-sql語句打包成一條sql語句,執行這條sql語句返回總影響行數5000, 如:
INSERT INTO jdbc_student
VALUES (‘55000’, ‘name: 1’, ‘11’), (‘55001’, ‘name: 1’, ‘11’), (‘55002’, ‘name: 2’, ‘12’),;
2)但是驅動executeBatch返回的是每條sql語句對應的影響記錄數陣列,即[5000,5000,5000,…,5000],所以累計就是5000*5000 = 25000000
3)update, delete, 返回的仍然是[1,1,1…1]無需特殊處理。
4、executeBatch返回值應用
以銷售模組更新訂單主細表為例,假設訂單細表有10條行專案,分2張出庫單出庫,分別有不同人員操作(誰先操作不確定):
1)第一張出庫單出庫4個訂單行專案,出庫後
訂單細表行專案狀態:已排程->已出庫
訂單主表狀態:已排程->(行專案是否全部出庫 ? ‘已出庫’ : ‘部分出庫’)
2)第二張出庫單出庫6個訂單行專案,出庫後
訂單細表行專案狀態:已排程->已出庫
訂單主表狀態:已排程->(行專案是否全部出庫 ? ‘已出庫’ : ‘部分出庫’)
sql批處理的封裝,以java程式碼為例,假設呼叫sql批處理程式碼封裝如下:
public int executeBatch(String sql, List<Map> paras) {
//預處理sql:insert into jdbc_student (name, age) values (?,?)
//paras,引數集合,[{name:"name1",age:11},{name:"name2",age:12},...]
//返回影響記錄條數
}
更新訂單主細表狀態程式碼邏輯:
- 開啟事務
- 更新訂單細表(狀態, 操作時間, 操作人, 出庫數量等): 已排程->已出庫,jdbc批處理實現,判斷executeBatch(sql, paras) == paras.size()
- 查詢細表是否全部已出庫?並更新主表狀態
- 提交事務
以出庫單1為例:
- 沒rewriteBatchedStatements引數,executeBatch(sql, paras) == paras.size() == 4
- 有rewriteBatchedStatements引數,executeBatch(sql, paras) = 16,paras.size() = 4,兩者不等,程式報錯!
實際業務中,通常paras.size即executeBatch返回結果,因此判斷是否相等修改如下即可:
executeBatch(sql, paras) == paras.size() || executeBatch(sql, paras) == paras.size() * paras.size()