Spring事務(wù)的這十種坑,坑坑致命!
1.錯(cuò)誤的訪問權(quán)限
public class UserService {
private UserMapper userMapper;
private void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我們可以看到add方法的訪問權(quán)限被定義成了private,這樣會導(dǎo)致事務(wù)失效,spring要求被代理方法必須是public的。
AbstractFallbackTransactionAttributeSource類的computeTransactionAttribute方法中有個(gè)判斷,如果目標(biāo)方法不是public,則TransactionAttribute返回null,即不支持事務(wù)。
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
2.方法被定義成final的
public class UserService {
private UserMapper userMapper;
public final void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我們可以看到add方法被定義成了final的,這樣會導(dǎo)致spring aop生成的代理對象不能復(fù)寫該方法,而讓事務(wù)失效。
3.方法內(nèi)部調(diào)用
public class UserService {
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
public void updateStatus(UserModel userModel) {
// doSameThing();
}
}
我們看到在事務(wù)方法add中,直接調(diào)用事務(wù)方法updateStatus。從前面介紹的內(nèi)容可以知道,updateStatus方法擁有事務(wù)的能力是因?yàn)閟pring aop生成代理了對象,但是這種方法直接調(diào)用了this對象的方法,所以updateStatus方法不會生成事務(wù)。
4.當(dāng)前實(shí)體沒有被spring管理
//@Service
public class UserService {
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我們可以看到UserService類沒有定義@Service注解,即沒有交給spring管理bean實(shí)例,所以它的add方法也不會生成事務(wù)。
5.錯(cuò)誤的spring事務(wù)傳播特性
public class UserService {
private UserMapper userMapper;
(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我們可以看到add方法的事務(wù)傳播特性定義成了Propagation.NEVER,這種類型的傳播特性不支持事務(wù),如果有事務(wù)則會拋異常。只有這三種傳播特性才會創(chuàng)建新事務(wù):PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED。
6.數(shù)據(jù)庫不支持事務(wù)
msql8以前的版本數(shù)據(jù)庫引擎是支持myslam和innerdb的。我以前也用過,對應(yīng)查多寫少的單表操作,可能會把表的數(shù)據(jù)庫引擎定義成myslam,這樣可以提升查詢效率。但是,要千萬記得一件事情,myslam只支持表鎖,并且不支持事務(wù)。所以,對這類表的寫入操作事務(wù)會失效。
7.自己吞掉了異常
public class UserService {
private UserMapper userMapper;
public void add(UserModel userModel) {
try {
userMapper.insertUser(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
這種情況下事務(wù)不會回滾,因?yàn)殚_發(fā)者自己捕獲了異常,又沒有拋出。事務(wù)的AOP無法捕獲異常,導(dǎo)致即使出現(xiàn)了異常,事務(wù)也不會回滾。
8.拋出的異常不正確
public class UserService {
private UserMapper userMapper;
public void add(UserModel userModel) throws Exception {
try {
userMapper.insertUser(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
這種情況下,開發(fā)人員自己捕獲了異常,又拋出了異常:Exception,事務(wù)也不會回滾。因?yàn)閟pring事務(wù),默認(rèn)情況下只會回滾RuntimeException(運(yùn)行時(shí)異常)和Error(錯(cuò)誤),不會回滾Exception。
9.多線程調(diào)用
public class UserService {
private UserMapper userMapper;
private RoleService roleService;
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
public class RoleService {
public void doOtherThing() {
System.out.println("保存role表數(shù)據(jù)");
}
}
我們可以看到事務(wù)方法add中,調(diào)用了事務(wù)方法doOtherThing,但是事務(wù)方法doOtherThing是在另外一個(gè)線程中調(diào)用的,這樣會導(dǎo)致兩個(gè)事務(wù)方法不在同一個(gè)線程中,獲取到的數(shù)據(jù)庫連接不一樣,從而是兩個(gè)不同的事務(wù)。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。
如果看過spring事務(wù)源碼的朋友,可能會知道spring的事務(wù)是通過數(shù)據(jù)庫連接來實(shí)現(xiàn)的。當(dāng)前線程中保存了一個(gè)map,key是數(shù)據(jù)源,value是數(shù)據(jù)庫連接。
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
我們說的同一個(gè)事務(wù),其實(shí)是指同一個(gè)數(shù)據(jù)庫連接,只有擁有同一個(gè)數(shù)據(jù)庫連接才能同時(shí)提交和回滾。如果在不同的線程,拿到的數(shù)據(jù)庫連接肯定是不一樣的,所以是不同的事務(wù)。
10.嵌套事務(wù)多回滾了
public class UserService {
private UserMapper userMapper;
private RoleService roleService;
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
public class RoleService {
(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("保存role表數(shù)據(jù)");
}
}
這種情況使用了嵌套的內(nèi)部事務(wù),原本是希望調(diào)用roleService.doOtherThing方法時(shí),如果出現(xiàn)了異常,只回滾doOtherThing方法里的內(nèi)容,不回滾 userMapper.insertUser里的內(nèi)容,即回滾保存點(diǎn)。。但事實(shí)是,insertUser也回滾了。
why?
因?yàn)閐oOtherThing方法出現(xiàn)了異常,沒有手動捕獲,會繼續(xù)往上拋,到外層add方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個(gè)事務(wù),不只回滾單個(gè)保存點(diǎn)。
怎么樣才能只回滾保存點(diǎn)呢?
public class UserService {
private UserMapper userMapper;
private RoleService roleService;
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
在代碼中手動把內(nèi)部嵌套事務(wù)放在try/catch中,并且不繼續(xù)往拋異常。
介紹到這里,你會發(fā)現(xiàn)spring事務(wù)的坑還是挺多的~