編程式事務
在Spring中事務管理的方式有兩種,編程式事務和聲明式事務。先詳細介紹一下兩種事務的實現方式
配置類
@Configuration
@EnableTransactionManagement
@ComponentScan("com.javashitang")
public class AppConfig {
@Bean
public DruidDataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true");
ds.setUsername("test");
ds.setPassword("test");
ds.setInitialSize(5);
return ds;
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate() {
return new TransactionTemplate(dataSourceTransactionManager());
}
}
public interface UserService {
void addUser(String name, String location);
default void doAdd(String name) {};
}
@Service
public class UserServiceV1Impl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public void addUser(String name, String location) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
String sql = "insert into user (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{name});
throw new RuntimeException("保存用戶信息失敗");
} catch (Exception e) {
e.printStackTrace();
status.setRollbackOnly();
}
}
});
}
}
可以看到編程式事務的方式並不優雅,因爲業務代碼和事務代碼耦合到一塊,當發生異常的時候還得需要手動回滾事務(比使用JDBC方便多類,JDBC得先關閉自動自動提交,然後根據情況手動提交或者回滾事務)
如果讓你優化事務方法的執行?你會如何做?
「其實我們完全可以用AOP來優化這種代碼,設置好切點,當方法執行成功時提交事務,當方法發生異常時回滾事務,這就是聲明式事務的實現原理」
使用AOP後,當我們調用事務方法時,會調用到生成的代理對象,代理對象中加入了事務提交和回滾的邏輯。
聲明式事務
Spring aop動態代理的方式有如下幾種方法
-
JDK動態代理實現(基於接口)(JdkDynamicAopProxy) -
CGLIB動態代理實現(動態生成子類的方式)(CglibAopProxy) -
AspectJ適配實現
spring aop默認只會使用JDK和CGLIB來生成代理對象
@Transactional可以用在哪裏?
@Transactional可以用在類,方法,接口上
-
用在類上,該類的所有public方法都具有事務 -
用在方法上,方法具有事務。當類和方法同時配置事務的時候,方法的屬性會覆蓋類的屬性 -
用在接口上,一般不建議這樣使用,因爲只有基於接口的代理會生效,如果Spring AOP使用cglib來實現動態代理,會導致事務失效(因爲註解不能被繼承)
@Transactional失效的場景
-
@Transactional註解應用到非public方法(除非特殊配置,例如使用AspectJ 靜態織入實現 AOP) -
自調用,因爲@Transactional是基於動態代理實現的 -
異常在代碼中被你自己try catch了 -
異常類型不正確,默認只支持RuntimeException和Error,不支持檢查異常 -
事務傳播配置不符合業務邏輯
@Transactional註解應用到非public方法
「爲什麼只有public方法上的@Transactional註解纔會生效?」
首相JDK動態代理肯定只能是public,因爲接口的權限修飾符只能是public。cglib代理的方式是可以代理protected方法的(private不行哈,子類訪問不了父類的private方法)如果支持protected,可能會造成當切換代理的實現方式時表現不同,增大出現bug的可能醒,所以統一一下。
「如果想讓非public方法也生效,你可以考慮使用AspectJ」
自調用,因爲@Transactional是基於動態代理實現的
當自調用時,方法執行不會經過代理對象,所以會導致事務失效。例如通過如下方式調用addUser方法時,事務會失效
// 事務失效
@Service
public class UserServiceV2Impl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void addUser(String name, String location) {
doAdd(name);
}
@Transactional
public void doAdd(String name) {
String sql = "insert into user (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{name});
throw new RuntimeException("保存用戶失敗");
}
}
可以通過如下方式解決
-
@Autowired注入自己,假如爲self,然後通過self調用方法 -
@Autowired ApplicationContext,從ApplicationContext通過getBean獲取自己,然後再調用
// 事務生效
@Service
public class UserServiceV2Impl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserService userService;
@Override
public void addUser(String name, String location) {
userService.doAdd(name);
}
@Override
@Transactional
public void doAdd(String name) {
String sql = "insert into user (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{name});
throw new RuntimeException("保存用戶失敗");
}
}
異常在代碼中被你自己try catch了
這個邏輯從源碼理解比較清晰,只有當執行事務拋出異常才能進入completeTransactionAfterThrowing方法,這個方法裏面有回滾的邏輯,如果事務方法都沒拋出異常就只會正常提交
// org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
// 執行事務方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
異常類型不正確,默認只支持RuntimeException和Error,不支持檢查異常
異常體系圖如下。當拋出檢查異常時,spring事務不會回滾。如果拋出任何異常都回滾,可以配置rollbackFor爲Exception
@Transactional(rollbackFor = Exception.class)
事務傳播配置不符合業務邏輯
假如說有這樣一個場景,用戶註冊,依次保存用戶基本信息到user表中,用戶住址信息到地址表中,當保存用戶住址信息失敗時,我們也要保證用戶信息註冊成功。
public interface LocationService {
void addLocation(String location);
}
@Service
public class LocationServiceImpl implements LocationService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
@Transactional
public void addLocation(String location) {
String sql = "insert into location (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{location});
throw new RuntimeException("保存地址異常");
}
}
@Service
public class UserServiceV3Impl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private LocationService locationService;
@Override
@Transactional
public void addUser(String name, String location) {
String sql = "insert into user (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{name});
locationService.addLocation(location);
}
}
調用發現user表和location表都沒有插入數據,並不符合我們期望,你可能會說拋出異常了,事務當然回滾了。好,我們把調用locationService的部分加上try catch
@Service
public class UserServiceV3Impl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private LocationService locationService;
@Override
@Transactional
public void addUser(String name, String location) {
String sql = "insert into user (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{name});
try {
locationService.addLocation(location);
} catch (Exception e) {
e.printStackTrace();
}
}
}
調用發現user表和location表還是都沒有插入數據。這是因爲在LocationServiceImpl中事務已經被標記成回滾了,所以最終事務還會回滾。
要想最終解決就不得不提到Spring的事務傳播行爲了,不清楚的小夥伴看《面試官:Spring事務的傳播行爲有幾種?》
Transactional的事務傳播行爲默認爲Propagation.REQUIRED。「如果當前存在事務,則加入該事務。如果當前沒有事務,則創建一個新的事務」
此時我們把LocationServiceImpl中Transactional的事務傳播行爲改成Propagation.REQUIRES_NEW即可
「創建一個新事務,如果當前存在事務,則把當前事務掛起」
所以最終的解決代碼如下
@Service
public class UserServiceV3Impl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private LocationService locationService;
@Override
@Transactional
public void addUser(String name, String location) {
String sql = "insert into user (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{name});
try {
locationService.addLocation(location);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service
public class LocationServiceImpl implements LocationService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addLocation(String location) {
String sql = "insert into location (`name`) values (?)";
jdbcTemplate.update(sql, new Object[]{location});
throw new RuntimeException("保存地址異常");
}
}
本文分享自微信公衆號 - Java識堂(erlieStar)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。