這樣一優化系統整體性能立馬提升
環境:Spring5.3.23
1. 案例代碼
先看下示例代碼:
static class PersonService {
@Resource
private JdbcTemplate jdbcTemplate;
/**
* 這里的代碼,先執行了更新操作,然后執行查詢操作。
* 業務代碼非常的簡單
*/
@Transactional
public void operator() {
// 1
int res = this.jdbcTemplate.update("update p_user t set t.address='akf' where t.id = 9000000") ;
System.out.printf("更新: %d 條數據%n", res) ;
// 2
List<Map<String, Object>> users = this.jdbcTemplate.queryForObject("select * from p_user x where x.username='h4F7i4B4'", (rs, rowNum) -> {
List<Map<String, Object>> datas = new ArrayList<>() ;
while (rs.next()) {
Map<String, Object> obj = new HashMap<>() ;
obj.put("id", rs.getObject(1)) ;
obj.put("username", rs.getObject(2)) ;
obj.put("email", rs.getObject(3)) ;
obj.put("password", rs.getObject(4)) ;
obj.put("address", rs.getObject(5)) ;
obj.put("age", rs.getObject(6)) ;
datas.add(obj) ;
}
return datas ;
}) ;
System.out.println(users) ;
}
}
先思考上面代碼在什么樣的場景下可以做相應的優化?
2. 環境準備
本地環境我準備了p_user表數據量2000W。
圖片
數據庫索引情況:
圖片
這里除了主鍵,沒有任何其它的索引。
這里不建立任何二級索引是為了模擬查詢慢的場景。
執行上面的查詢SQL:
圖片
執行時間5s;感覺還好。
3. 代碼分析
在上面的代碼中2個關鍵的數據庫操作分別是更新與查詢,這兩個查詢是沒有任何關聯的,相關獨立;而在整個方法上是添加了@Transaction注解,整個方法的執行都是在一個事務中執行。那么這里的查詢如果非常的慢是不是會對當前的修改記錄或者是整個p_user表造成影響呢?
結合上面的數據表情況,當前表是沒有任何索引的(除主鍵)。如果這里的查詢比較慢,會發生什么情況呢?
首先你要知道MySQL中update語句會上什么鎖?
回顧MySQL鎖:
MySQL的UPDATE語句默認會帶上一個寫鎖(Write Lock)。在MySQL中,寫鎖會阻塞其他事務對同一行的讀取和寫入操作,以確保數據的一致性和完整性。
當執行UPDATE語句時,MySQL會在相關的行上獲取寫鎖。這樣,其他事務無法修改或刪除這些行,直到寫鎖被釋放。這有助于防止在并發操作中發生數據沖突或不一致的情況。
還有點要注意mysql的鎖機制是基于事務的,所以通常我們需要在一個事務中進行操作。隨著事務的提交或回滾進行鎖的釋放
知道了update語句默認會帶上寫鎖,那么這里的update鎖的是id等于9000000的數據。前面提到了,只要事務提交或者回滾后鎖才會被釋放。
那如果這里我們接下來的查詢比較慢那是不是我們這個鎖的釋放時間就會變長,其它事務將會被阻塞。一旦發生了阻塞我們系統的整體性能可能會受到影響。這里的update語句只會對id為9000000的數據上鎖,如果咱們的更新語句是范圍的或者條件是沒有索引的那很可能就成了表鎖,那這時候系統的性能會變的非常糟糕。也就是我們對這條id為9000000的數據要鎖至少5s時間。
該如何優化上面的代碼呢?
4. 代碼優化
通過上面的分析,由于先執行了update語句,然后執行查詢語句,如果查詢比較慢那么我們的update語句形成的寫鎖(行鎖,間隙鎖或者表鎖)時間會變長,對系統的整體性能會造成影響。所以這里我們只需要將查詢和修改操作順序進行下調整即可。
@Transactional
public void operator() {
// 1
List<Map<String, Object>> users = this.jdbcTemplate.queryForObject("select * from p_user x where x.username='h4F7i4B4'", (rs, rowNum) -> {
List<Map<String, Object>> datas = new ArrayList<>() ;
while (rs.next()) {
Map<String, Object> obj = new HashMap<>() ;
obj.put("id", rs.getObject(1)) ;
// ...
datas.add(obj) ;
}
return datas ;
}) ;
System.out.println(users) ;
// 2
int res = this.jdbcTemplate.update("update p_user t set t.address='akf' where t.id = 9000000") ;
System.out.printf("更新: %d 條數據%n", res) ;
}
通過上面的優化,雖然該接口自身的性能并沒有提升,但是在該接口中update語句形成的鎖時間將大大的減少(在這里查詢語句是沒有鎖的),如果同一時刻存在其它事務修改當前的數據不至于被阻塞太長時間,那其它接口的性能整體不就提高了么。
在上面的代碼中首先是2個操作互不相干,其實完全可以把不需要事務的操作放到其它方法中(注意事務失效問題)。
static class PersonService {
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private PersonService ps ;
public void query() {
ps.update() ;
List<Map<String, Object>> users = this.jdbcTemplate.queryForObject("select * from p_user x where x.username='h4F7i4B4'", (rs, rowNum) -> {
List<Map<String, Object>> datas = new ArrayList<>() ;
while (rs.next()) {
Map<String, Object> obj = new HashMap<>() ;
obj.put("id", rs.getObject(1)) ;
// ...
datas.add(obj) ;
}
return datas ;
}) ;
System.out.println(users) ;
}
@Transactional
public void update() {
int res = this.jdbcTemplate.update("update p_user t set t.address='akf' where t.id = 9000000") ;
System.out.printf("更新: %d 條數據%n", res) ;
}
}
上面代碼己注入自己,解決在非事務方法中調用事務方法二導致事務失效。當然也可以通過AopContext來解決(不推薦),也可以把事務方法放到其它類中都可以解決。
完畢!!!