性能優(yōu)化!三種場(chǎng)景下使用@Transactional對(duì)性能影響太大了
環(huán)境:SpringBoot3.4.2
1. 簡(jiǎn)介
@Transactional 是 Spring 中用于聲明式事務(wù)管理的核心注解,旨在簡(jiǎn)化數(shù)據(jù)庫(kù)事務(wù)操作。在傳統(tǒng)的編程式事務(wù)中,我們需手動(dòng)編寫事務(wù)的開啟、提交或回滾代碼,而通過(guò) @Transactional 注解將事務(wù)邏輯與業(yè)務(wù)代碼解耦。只需在方法或類上添加該注解,Spring 會(huì)基于 AOP(面向切面編程)自動(dòng)攔截調(diào)用,在方法執(zhí)行前開啟事務(wù),執(zhí)行后根據(jù)異常情況提交或回滾。這種設(shè)計(jì)顯著提升了代碼的可讀性和可維護(hù)性。
但如果濫用@Transactional,會(huì)對(duì)系統(tǒng)性能產(chǎn)生顯著負(fù)面影響,主要體現(xiàn)在以下幾個(gè)方面:
- 過(guò)度使用會(huì)導(dǎo)致事務(wù)范圍過(guò)大,延長(zhǎng)數(shù)據(jù)庫(kù)連接占用時(shí)間,增加鎖競(jìng)爭(zhēng)和死鎖風(fēng)險(xiǎn)
- 不必要的細(xì)粒度事務(wù)會(huì)引發(fā)頻繁的提交和回滾操作,加重?cái)?shù)據(jù)庫(kù)負(fù)載
- 在非關(guān)鍵數(shù)據(jù)操作或只讀場(chǎng)景中濫用事務(wù),會(huì)無(wú)謂消耗系統(tǒng)資源,降低整體吞吐量。
本篇文章會(huì)介紹基于 JPA 和 JDBC 時(shí),@Transactional 注解對(duì)查詢性能的影響。
純查詢到底要不要事務(wù)?
2.實(shí)戰(zhàn)案例
2.1 準(zhǔn)備環(huán)境
配置文件
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/batch
username: root
password: 123123
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 10
---
spring:
jpa:
generateDdl: false
hibernate:
ddlAuto: update
openInView: true
show-sql: false創(chuàng)建實(shí)體對(duì)象
@Entity
@Table(name = "o_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id ;
private String name ;
private Integer age ;
private String phone ;
private String sex ;
// getters, setters
}準(zhǔn)備接口
@GetMapping("")
public ResponseEntity<?> query() {
return ResponseEntity.ok(this.userService.queryUser()) ;
}準(zhǔn)備數(shù)據(jù)(500w)
圖片
2.2 使用Repository查詢測(cè)試
測(cè)試1,不使用@Transactional注解
private final UserRepository userRepository ;
public User queryUser() {
return this.userRepository.findById(4888888).orElse(null) ;
}使用JMeter測(cè)試結(jié)果如下:
圖片
吞吐量平均:9700
測(cè)試2,使用@Transactional
@Transactional
public User queryUser() {}使用JMeter測(cè)試結(jié)果如下:
圖片
吞吐量平均:12700
這是否打破了你原有的認(rèn)知呢?按照常規(guī)理論,使用 @Transactional 注解通常會(huì)使性能變差,然而當(dāng)前呈現(xiàn)的數(shù)據(jù)卻表明,使用該注解后性能反而有所提升。
測(cè)試3,使用只讀事務(wù)
@Transactional(readOnly = true)
public User queryUser() {}使用JMeter測(cè)試結(jié)果如下:
圖片
吞吐量平均:9300
該結(jié)果與不使用注解相差不大。
思考:為什么使用了@Transactional注解反而性能更高呢?歡迎大家留言討論。
2.3 使用JDBC查詢
測(cè)試1,不使用@Transactional注解
private final JdbcTemplate jdbcTemplate ;
public User queryUser() {
return this.jdbcTemplate.queryForObject("select id, name, age, phone, sex from o_user where id = 4888888", new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User() ;
user.setId(rs.getInt("id")) ;
user.setAge(rs.getInt("age")) ;
user.setName(rs.getString("name")) ;
user.setPhone(rs.getString("phone")) ;
user.setSex(rs.getString("sex")) ;
return user ;
}
}) ;
}JMeter測(cè)試結(jié)果:
圖片
吞吐量平均:25000
JPA是簡(jiǎn)單了,代價(jià)就是性能太差了。
測(cè)試2,使用@Transactional注解
@Transactional
public User queryUser() {}JMeter測(cè)試結(jié)果:

吞吐量平均:13100
這倒是符合我們的預(yù)期,使用了@Transactional注解性能明顯下降。
測(cè)試3,使用只讀事務(wù)
@Transactional(readOnly = true)
public User queryUser() {}JMeter測(cè)試結(jié)果:
圖片
同樣符合預(yù)期,與讀寫事務(wù)差不多。
2.4 使用EntityManager查詢
測(cè)試1,不使用@Transactional注解
private final EntityManager em ;
public User queryUser() {
return this.em.find(User.class, 4888888) ;
}JMeter測(cè)試結(jié)果:
圖片
吞吐量平均:24000
測(cè)試2,使用@Transactional注解
@Transactional
public User queryUser() {
return this.em.find(User.class, 4888888) ;
}JMeter測(cè)試結(jié)果:
圖片
吞吐量平均:13000
測(cè)試3,使用只讀事務(wù)
@Transactional(readOnly = true)
public User queryUser() {}JMeter測(cè)試結(jié)果:
圖片
吞吐量平均:9800
2.5 性能柱狀圖
圖片
2.6 查詢使用事務(wù)總結(jié)
- 保證一致性:在一個(gè)事務(wù)中,所有查詢看到的是同一時(shí)間點(diǎn)的數(shù)據(jù)快照(取決于隔離級(jí)別),避免了中途數(shù)據(jù)被其他事務(wù)修改導(dǎo)致的不一致
- 性能優(yōu)化:Spring 提供 @Transactional(readOnly = true),明確標(biāo)記為只讀事務(wù)。這可以讓底層數(shù)據(jù)庫(kù)(如 Oracle、MySQL InnoDB)進(jìn)行優(yōu)化,例如啟用只讀快照、減少鎖競(jìng)爭(zhēng)等。
- 連接復(fù)用:在一個(gè)事務(wù)中的多個(gè)操作可以復(fù)用同一個(gè)數(shù)據(jù)庫(kù)連接,減少連接創(chuàng)建/釋放開銷。
- 與寫操作兼容:如果將來(lái)該查詢方法被包含在一個(gè)更大的寫事務(wù)中,有 @Transactional 可以無(wú)縫集成。
如下多個(gè)查詢使用事務(wù)保證了同一時(shí)間點(diǎn)的數(shù)據(jù):
private final UserRepository userRepository ;
private final OrderRepository orderRepository ;
@Transactional(readOnly = true)
public UserInfoDto getUserInfo(Long userId) {
User user = userRepository.findById(userId);
List<Order> orders = orderRepository.findByUserId(userId);
return new UserInfoDto(user, orders);
}


























