事物回顧
在數據庫階段我們已學習過事務了,我們講到:
事物是一組操作的集合,它是一個不可分割的工作單位。事務會把所有的操作作為一個整體,一起向數據庫提交或者是撤銷操作請求。所以這組操作要么同時成功,要么同時失敗。
怎么樣來控制這組操作,讓這組操作同時成功或同時失敗呢?此時就要涉及到事務的具體操作了。
事務的操作主要有三步:
開啟事務(一組操作開始前,開啟事務):start transaction / begin ;
提交事務(這組操作全部成功后,提交事務):commit ;
回滾事務(中間任何一個操作出現異常,回滾事務):rollback ;
Spring事務管理
案例
簡單的回顧了事務的概念以及事務的基本操作之后,接下來我們看一個事務管理案例:解散部門 (解散部門就是刪除部門)
需求:當部門解散了不僅需要把部門信息刪除了,還需要把該部門下的員工數據也刪除了。
步驟:
? ? ● 根據ID刪除部門數據
? ? ● 根據部門ID刪除該部門下的員工
代碼實現:
DeptServiceImpl
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Autowiredprivate EmpMapper empMapper;//根據部門id,刪除部門信息及部門下的所有員工@Overridepublic void delete(Integer id){//根據部門id刪除部門信息deptMapper.deleteById(id);//刪除部門下的所有員工信息empMapper.deleteByDeptId(id); }
}
DeptMapper
@Mapper
public interface DeptMapper {/*** 根據id刪除部門信息* @param id 部門id*/@Delete("delete from dept where id = #{id}")void deleteById(Integer id);
}
EmpMapper
@Mapper
public interface EmpMapper {//根據部門id刪除部門下所有員工@Delete("delete from emp where dept_id=#{deptId}")public int deleteByDeptId(Integer deptId);}
重啟SpringBoot服務,使用postman測試部門刪除:
代碼正常情況下,dept表和Emp表中的數據已刪除
修改DeptServiceImpl類中代碼,添加可能出現異常的代碼:
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Autowiredprivate EmpMapper empMapper;//根據部門id,刪除部門信息及部門下的所有員工@Overridepublic void delete(Integer id){//根據部門id刪除部門信息deptMapper.deleteById(id);//模擬:異常發生int i = 1/0;//刪除部門下的所有員工信息empMapper.deleteByDeptId(id); }
}
重啟SpringBoot服務,使用postman測試部門刪除:
查看數據庫表:
● 刪除了2號部門
● 2號部門下的員工數據沒有刪除
以上程序出現的問題:即使程序運行拋出了異常,部門依然刪除了,但是部門下的員工卻沒有刪除,造成了數據的不一致。
原因分析
原因:
● 先執行根據id刪除部門的操作,這步執行完畢,數據庫表 dept 中的數據就已經刪除了。
● 執行 1/0 操作,拋出異常
● 拋出異常之前,下面所有的代碼都不會執行了,根據部門ID刪除該部門下的員工,這個操作也不會執行 。
此時就出現問題了,部門刪除了,部門下的員工還在,業務操作前后數據不一致。
而要想保證操作前后,數據的一致性,就需要讓解散部門中涉及到的兩個業務操作,要么全部成功,要么全部失敗。那我們如何,讓這兩個操作要么全部成功,要么全部失敗呢 ?
那就可以通過事務來實現,因為一個事務中的多個業務操作,要么全部成功,要么全部失敗。
此時,我們就需要在delete刪除業務功能中添加事務。
在方法運行之前,開啟事務,如果方法成功執行,就提交事務,如果方法執行的過程當中出現異常了,就回滾事務。
思考:開發中所有的業務操作,一旦我們要進行控制事務,是不是都是這樣的套路?
答案:是的。
所以在spring框架當中就已經把事務控制的代碼都已經封裝好了,并不需要我們手動實現。我們使用了spring框架,我們只需要通過一個簡單的注解@Transactional就搞定了。
Transactional注解
@Transactional作用:就是在當前這個方法執行開始之前來開啟事務,方法執行完畢之后提交事務。如果在這個方法執行的過程當中出現了異常,就會進行事務的回滾操作。
@Transactional注解:我們一般會在業務層當中來控制事務,因為在業務層當中,一個業務功能可能會包含多個數據訪問的操作。在業務層來控制事務,我們就可以將多個數據訪問操作控制在一個事務范圍內。
@Transactional注解書寫位置:
● 方法
? ? ○ 當前方法交給spring進行事務管理
● 類
? ? ○ 當前類中所有的方法都交由spring進行事務管理
● 接口
? ? ○ 接口下所有的實現類當中所有的方法都交給spring 進行事務管理
接下來,我們就可以在業務方法delete上加上 @Transactional 來控制事務 。
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Autowiredprivate EmpMapper empMapper;@Override@Transactional //當前方法添加了事務管理public void delete(Integer id){//根據部門id刪除部門信息deptMapper.deleteById(id);//模擬:異常發生int i = 1/0;//刪除部門下的所有員工信息empMapper.deleteByDeptId(id); }
}
在業務功能上添加@Transactional注解進行事務管理后,我們重啟SpringBoot服務,使用postman測試:
添加Spring事務管理后,由于服務端程序引發了異常,所以事務進行回滾。
說明:可以在application.yml配置文件中開啟事務管理日志,這樣就可以在控制看到和事務相關的日志信息了
#spring事務管理日志
logging:level:org.springframework.jdbc.support.JdbcTransactionManager: debug
事務進階
前面我們通過spring事務管理注解@Transactional已經控制了業務層方法的事務。接下來我們要來詳細的介紹一下@Transactional事務管理注解的使用細節。我們這里主要介紹@Transactional注解當中的兩個常見的屬性:
? ? 1.異常回滾的屬性:rollbackFor
? ? 2.事務傳播行為:propagation
我們先來學習下rollbackFor屬性。
rollbackFor
我們在之前編寫的業務方法上添加了@Transactional注解,來實現事務管理。
@Transactional
public void delete(Integer id){//根據部門id刪除部門信息deptMapper.deleteById(id);//模擬:異常發生int i = 1/0;//刪除部門下的所有員工信息empMapper.deleteByDeptId(id);
}
以上業務功能delete()方法在運行時,會引發除0的算數運算異常(運行時異常),出現異常之后,由于我們在方法上加了@Transactional注解進行事務管理,所以發生異常會執行rollback回滾操作,從而保證事務操作前后數據是一致的。
下面我們在做一個測試,我們修改業務功能代碼,在模擬異常的位置上直接拋出Exception異常(編譯時異常)
@Transactional
public void delete(Integer id) throws Exception {//根據部門id刪除部門信息deptMapper.deleteById(id);//模擬:異常發生if(true){throw new Exception("出現異常了~~~");}//刪除部門下的所有員工信息empMapper.deleteByDeptId(id);
}
說明:在service中向上拋出一個Exception編譯時異常之后,由于是controller調用service,所以在controller中要有異常處理代碼,此時我們選擇在controller中繼續把異常向上拋。
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id) throws Exception {//日志記錄log.info("根據id刪除部門");//調用service層功能deptService.delete(id);//響應return Result.success();
}
重新啟動服務后測試:
拋出異常之后事務會不會回滾
使用postman測試,刪除5號部門
發生了Exception異常,但事務依然提交了
通過以上測試可以得出一個結論:默認情況下,只有出現RuntimeException(運行時異常)才會回滾事務。
假如我們想讓所有的異常都回滾,需要來配置@Transactional注解當中的rollbackFor屬性,通過rollbackFor這個屬性可以指定出現何種異常類型回滾事務。
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Autowiredprivate EmpMapper empMapper;@Override@Transactional(rollbackFor=Exception.class)public void delete(Integer id){//根據部門id刪除部門信息deptMapper.deleteById(id);//模擬:異常發生int num = id/0;//刪除部門下的所有員工信息empMapper.deleteByDeptId(id); }
}
接下來我們重新啟動服務,測試刪除部門的操作:
控制臺日志:執行了刪除3號部門的操作, 因為異常又進行了事務回滾
數據表:3號部門沒有刪除
結論:
? ? ● 在Spring的事務管理中,默認只有運行時異常 RuntimeException才會回滾。
? ? ● 如果還需要回滾指定類型的異常,可以通過rollbackFor屬性來指定。
propagation
介紹
我們接著繼續學習@Transactional注解當中的第二個屬性propagation,這個屬性是用來配置事務的傳播行為的。
什么是事務的傳播行為呢?
? ? ● 就是當一個事務方法被另一個事務方法調用時,這個事務方法應該如何進行事務控制。
例如:兩個事務方法,一個A方法,一個B方法。在這兩個方法上都添加了@Transactional注解,就代表這兩個方法都具有事務,而在A方法當中又去調用了B方法。
所謂事務的傳播行為,指的就是在A方法運行的時候,首先會開啟一個事務,在A方法當中又調用了B方法, B方法自身也具有事務,那么B方法在運行的時候,到底是加入到A方法的事務當中來,還是B方法在運行的時候新建一個事務?這個就涉及到了事務的傳播行為。
我們要想控制事務的傳播行為,在@Transactional注解的后面指定一個屬性propagation,通過 propagation 屬性來指定傳播行為。接下來我們就來介紹一下常見的事務傳播行為。
屬性值 | 含義 |
---|---|
REQUIRED | 【默認值】需要事務,有則加入,無則創建新事務 |
REQUIRES_NEW | 需要新事務,無論有無,總是創建新事務 |
SUPPORTS | 支持事務,有則加入,無則在無事務狀態中運行 |
NOT_SUPPORTED | 不支持事務,在無事務狀態下運行,如果當前存在已有事務,則掛起當前事務 |
MANDATORY | 必須有事務,否則拋異常 |
NEVER | 必須沒事務,否則拋異常 |
對于這些事務傳播行為,我們只需要關注以下兩個就可以了:
? ? 1.REQUIRED(默認值)
? ? 2.REQUIRES_NEW
案例
接下來我們就通過一個案例來演示下事務傳播行為propagation屬性的使用。
需求:解散部門時需要記錄操作日志
由于解散部門是一個非常重要而且非常危險的操作,所以在業務當中要求每一次執行解散部門的操作都需要留下痕跡,就是要記錄操作日志。而且還要求無論是執行成功了還是執行失敗了,都需要留下痕跡。
步驟:
? ? 1.執行解散部門的業務:先刪除部門,再刪除部門下的員工(前面已實現)
? ? 2.記錄解散部門的日志,到日志表(未實現)
準備工作:
創建數據庫表 dept_log 日志表:
create table dept_log(id int auto_increment comment '主鍵ID' primary key,create_time datetime null comment '操作時間',description varchar(300) null comment '操作描述'
)comment '部門操作日志表';
實體類:DeptLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {private Integer id;private LocalDateTime createTime;private String description;
}
Mapper接口:DeptLogMapper
@Mapper
public interface DeptLogMapper {@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")void insert(DeptLog log);}
業務接口:DeptLogService
public interface DeptLogService {void insert(DeptLog deptLog);
}
業務實現類:DeptLogServiceImpl
@Service
public class DeptLogServiceImpl implements DeptLogService {@Autowiredprivate DeptLogMapper deptLogMapper;@Transactional //事務傳播行為:有事務就加入、沒有事務就新建事務@Overridepublic void insert(DeptLog deptLog) {deptLogMapper.insert(deptLog);}
}
代碼實現:
業務實現類:DeptServiceImpl
@Slf4j
@Service
//@Transactional //當前業務實現類中的所有的方法,都添加了spring事務管理機制
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Autowiredprivate EmpMapper empMapper;@Autowiredprivate DeptLogService deptLogService;//根據部門id,刪除部門信息及部門下的所有員工@Override@Log@Transactional(rollbackFor = Exception.class) public void delete(Integer id) throws Exception {try {//根據部門id刪除部門信息deptMapper.deleteById(id);//模擬:異常if(true){throw new Exception("出現異常了~~~");}//刪除部門下的所有員工信息empMapper.deleteByDeptId(id);}finally {//不論是否有異常,最終都要執行的代碼:記錄日志DeptLog deptLog = new DeptLog();deptLog.setCreateTime(LocalDateTime.now());deptLog.setDescription("執行了解散部門的操作,此時解散的是"+id+"號部門");//調用其他業務類中的方法deptLogService.insert(deptLog);}}//省略其他代碼...
}
測試:
重新啟動SpringBoot服務,測試刪除3號部門后會發生什么?
? ? ● 執行了刪除3號部門操作
? ? ● 執行了插入部門日志操作
? ? ● 程序發生Exception異常
? ? ● 執行事務回滾(刪除、插入操作因為在一個事務范圍內,兩個操作都會被回滾)
然后在dept_log表中沒有記錄日志數據
原因分析:
接下來我們就需要來分析一下具體是什么原因導致的日志沒有成功的記錄。
在執行delete操作時開啟了一個事務
當執行insert操作時,insert設置的事務傳播行是默認值REQUIRED,表示有事務就加入,沒有則新建事務
此時:delete和insert操作使用了同一個事務,同一個事務中的多個操作,要么同時成功,要么同時失敗,所以當異常發生時進行事務回滾,就會回滾delete和insert操作
解決方案:
在DeptLogServiceImpl類中insert方法上,添加@Transactional(propagation = Propagation.REQUIRES_NEW)
Propagation.REQUIRES_NEW :不論是否有事務,都創建新事務 ,運行在一個獨立的事務中。
@Service
public class DeptLogServiceImpl implements DeptLogService {@Autowiredprivate DeptLogMapper deptLogMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)//事務傳播行為:不論是否有事務,都新建事務@Overridepublic void insert(DeptLog deptLog) {deptLogMapper.insert(deptLog);}
}
重啟SpringBoot服務,再次測試刪除3號部門:
那此時,DeptServiceImpl中的delete方法運行時,會開啟一個事務。 當調用 deptLogService.insert(deptLog) 時,也會創建一個新的事務,那此時,當insert方法運行完畢之后,事務就已經提交了。 即使外部的事務出現異常,內部已經提交的事務,也不會回滾了,因為是兩個獨立的事務。
到此事務傳播行為已演示完成,事務的傳播行為我們只需要掌握兩個:REQUIRED、REQUIRES_NEW。
REQUIRED :大部分情況下都是用該傳播行為即可。
REQUIRES_NEW :當我們不希望事務之間相互影響時,可以使用該傳播行為。比如:下訂單前需要記錄日志,不論訂單保存成功與否,都需要保證日志記錄能夠記錄成功。