在mysql階段的文章中,已經介紹過事務了。本篇文章是對mysql事務的總結和對使用Spring框架來實現事務操作的講解。
事務回顧
什么是事務
事務時一組操作的集合,是一個不可分割的操作。
事務會把所有操作作為一個整體,一起向數據庫提交或者撤銷操作請求。所以這組操作要么同時成功,要么同時失敗。
為什么需要事務
我們在程序開發的時候,會有事務的需求。
比如:轉賬操作。
第一步:A:-100元
第二步:B:+100元
事務的操作
事務的操作主要有三步:
1、開啟事務:start transaction(一組操作開啟事務)
2、提交事務:commit(這組操作全部成功,提交事務)
3、回滾事務:rollback(這組操作中間任何一個操作出現異常,回滾事務)
Spring中事務的實現
Spring中的事務實現操作分為兩類:
1、編程式事務(手動寫代碼操作事務)
2、聲明式事務(利用注解自動開啟和提交事務)
假設現在有需求:用戶注冊,注冊時在日志表中插入一條操作記錄。
數據準備:
DROP TABLE
IFEXISTS user_info;
CREATE TABLE user_info (`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR ( 128 ) NOT NULL,`password` VARCHAR ( 128 ) NOT NULL,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now(),PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '日志表';-- 操作日志表
DROP TABLE
IFEXISTS log_info;
CREATE TABLE log_info (`id` INT PRIMARY KEY auto_increment,`user_name` VARCHAR ( 128 ) NOT NULL,`op` VARCHAR ( 256 ) NOT NULL,`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';
代碼準備:
1、創建項目,引入SpringWeb,Mybatis,mysql等依賴
2、配置文件
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 5028driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration: # 配置打印 MyBatis?志log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true #配置駝峰?動轉換
實體類:
Userinfo:
@Data
public class Userinfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;
}
Loginfo:
@Data
public class LogInfo {private Integer id;private String userName;private String op;private Date createTime;private Date updateTime;
}
Mapper:
UserinfoMapper:
@Mapper
public interface UserinfoMapper {@Insert("insert into user_info(user_name,password)values (#{userName},#{password})")Integer insert(String userName,String password);
}
LoginfoMapper:
@Mapper
public interface LoginfoMapper {@Insert("insert into log_info(user_name,op) values (#{userName},#{op})")Integer insertLog(String name,String op);
}
Service:
UserService:
@Service
public class UserService {@Autowiredprivate UserinfoMapper userinfoMapper;public Integer insert(String userName,String password){return userinfoMapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;public Integer login(String userName,String op){return loginfoMapper.insertLog(userName,op);}
}
Controller:
UserController:
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/r1")public boolean login(String userName,String password){userService.insert(userName,password);return true;}
}
Spring編程式事務(了解)
Spring手動操作事務有三個操作步驟:
- 開啟事務
- 提交事務
- 回滾事務
SpringBoot內置了兩個對象:
- DataSourceTransactionManager 事務管理器,用來開啟、提交或回滾事務
- TransactionDefinition是事務的屬性,在獲取事務的時候需要將TransactionDefinition傳遞進去從而獲得一個事務TransactionStatus
下面是代碼實現:
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;//JDBC事務管理器@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;//定義事務屬性@Autowiredprivate TransactionDefinition transactionDefinition;@RequestMapping("/r1")public boolean login(String userName,String password){//開啟事務TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);//用戶注冊userService.insert(userName,password);//提交事務dataSourceTransactionManager.commit(transactionStatus);//回滾事務
// dataSourceTransactionManager.rollback(transactionStatus);return true;}
}
使用postMan進行測試:
提交事務:
回滾事務:
刷新之后數據庫的數據并沒有增加:
Spring聲明式事務@Transactional
聲明式事務只要在需要事務的方法上添加@Transactional注解就可以實現了。無需手動開啟事務和提交事務,進入方法時自動開啟事務,方法執行完會自動提交事務,如果中途發生了沒有處理的異常會自動回滾事務。
代碼實現:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用戶注冊userService.insert(userName,password);return true;}
}
日志:
刷新數據庫,發現數據插入成功:
修改程序,使它出現異常:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用戶注冊userService.insert(userName,password);//制造異常int a = 10/0;return true;}
}
重新測試:
日志:
刷新數據庫,發現并沒有數據插入:
對比日志:
那如果我們需要讓它發生異常時不發生回滾呢?
此時我們可以使用try-catch將異常捕獲住,代碼修改如下:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用戶注冊userService.insert(userName,password);try {//制造異常int a = 10/0;} catch (Exception e) {e.getMessage();}return true;}
}
重新測試:
日志:
數據庫:
那我們如果在異常捕獲后需要事務進行回滾呢?有以下兩種方式:
1、重新拋出異常
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用戶注冊userService.insert(userName,password);try {//制造異常int a = 10/0;} catch (Exception e) {//將異常重新拋出throw e;}return true;}
}
測試:
2、手動回滾事務
使用TransationAspectSupport.currentTransactionStatus()得到當前事務,并使用setRollbackOnly設置setRollbackOnly。
代碼:
@RequestMapping("/trans")
@RestController
public class TransController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/r1")public boolean login(String userName,String password){//用戶注冊userService.insert(userName,password);try {//制造異常int a = 10/0;} catch (Exception e) {//手動回滾TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return true;}
}
測試:
@Transactional詳解
1、rollbackFor
上面我們已經知道了,@Transactional注解會開始事務并且自動提交/回滾事務。
我們將異常類型改為IOException再進行測試:
@Transactional@RequestMapping("/r2")public boolean login2(String userName,String password) throws IOException {//用戶注冊userService.insert(userName,password);if(true){throw new IOException();}return true;}
測試:
此時我們發現雖然程序已經拋出異常,但是事務仍然提交了:
數據庫也新增了一條數據:
咦?這是為什么呢?不是拋出異常后,事務就應該自動回滾嗎?
這是因為事務回滾的默認是遇到運行時異常進行回滾,我們上面的算數異常就屬于運行時異常的子類。因此,能夠正常進行回滾。
如何解決呢?
通過@Transactional中的rollbackfor屬性進行解決:
@Transactional(rollbackFor = Exception.class)@RequestMapping("/r2")public boolean login2(String userName,String password) throws IOException {//用戶注冊userService.insert(userName,password);if(true){throw new IOException();}return true;}
測試:
可以看到事務發生了回滾:
數據庫:
對上面內容的總結:
事務隔離級別
回顧Mysql事務隔離級別
1、讀未提交:讀未提交,也叫未提交讀。該隔離級別的事務可以看到其他事務中未提交的事務。
因為其他事務未提交的數據可能會發生回滾,但是該隔離級別卻可以讀到,我們把該級別讀到的數據稱為臟數據,這個問題稱之為臟讀
臟讀問題:
2、讀提交:讀已提交,也叫提交讀,該隔離級別的事務能讀取到已提交事務的數據。
該隔離級別不會有臟讀問題,但由于在事務執行中可以讀取到其他事務提交的結果,所以在不同的時間的相同sql查詢可能會得到不同的結果,這種現象叫做不可重復讀(前后多次讀取,數據內容不一致)
不可重復讀:
3、可重復讀(mysql默認的隔離級別):事務不會讀到其他事務對已有數據的修改,即使其他事務已經提交,也可以確保同一事務多次查詢結果一致,但是其他事務新插入的數據,是可以感知到的,這也就引發了幻讀問題。
此隔離級別事務執行時,另一個事務成功插入了某條數據,但因為它每次查詢的結果都是一樣的(修改能查詢到是因為它涉及到了表中的所有數據行),所以會導致查詢不到這條數據,這個現象稱為幻讀(前后多次讀取,數據總量不同)。
幻讀:
4、串行化:序列化,事務最高隔離級別。它會強制事務排序,使之不會發生沖突,從而解決了臟讀,不可重復讀和幻讀問題,但因為執行效率第,所以真正使用的場景并不多。
Spring事務隔離級別
Spring中的事務隔離級別有5種:
1、Isolation.DEFAULT:以連接的數據庫的事務隔離級別為主。
2、Isolation.READ_UNCOMMITTED:讀未提交,對應SQL標準的READ UNCOMMITTED。
3、Isolation.READ_COMITTED:讀已提交,對應SQL標準中的READ COMMITTED。
4、Isolation.REPEATABLE_READ:可重復讀,對應SQL標準中REPEATABLE READ。
5、Isolation.SERIALIZABLE:串行化,對應SQL標準中的SERIALIZABLE。
我們可以通過@Transactional中的islation屬性設置事務隔離級別:
//設置為讀已提交@Transactional(isolation = Isolation.READ_COMMITTED)@RequestMapping("/r3")public boolean login3(String userName,String password) throws IOException {//用戶注冊userService.insert(userName,password);if(true){throw new IOException();}return true;}
Spring事務傳播機制
事務傳播機制:是多個事務方法存在調用關系時,事務時如何在這些方法間進行傳播的。
比如:Controller中的方法A調用Service中的方法B,它們都是被@Transactional修飾。A方法運行時,會開啟事務,當A調用B時,B方法本身也有事務,此時方法B運行時,是加入A的事務還是在創建一個新的事務呢?
這就涉及到了事務的傳播機制。
打個比方,公司的流程管理:
執行任務之前需要先寫執行文檔,任務執行結束,再寫總結匯報。
此時A部門有一項工作是和B部門一起干的,此時B部門是直接使用A部門的文檔,還是新建一個文檔呢?
事務隔離級別解決的是多個事務同時調用一個數據庫的問題:
而事務傳播機制解決的是一個事務再多個方法中傳遞的問題
事務傳播級別有哪些
@Transactional注解支持事務傳播機制的設置,通過propagation屬性來指定傳播行為。
Spring事務傳播機制有以下七種:
1、Propagtion.REQUIRED:默認的事務傳播級別。如果當前存在事務,則加入該事務。如果沒有事務,則創建一個新的事務。
2、Propagtion.SUPPORTS:如果當前存在事務,則加入該事務。如果當前沒有事務,則以非事務的方式繼續運行。
3、Propagtion.MANDATORY:強制性。如果當前存在事務,則加入該事務。如果當前沒有事務,則拋出異常
4、Propagation.REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起,也就是說不管外部方法是否開啟事務,Propagation.REQUIRES_NEW修飾的內部方法都會開啟新的事務且開啟的事務相互獨立,互不干擾。
5、Propagtion.NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起(不使用)。
6、Propagtion.NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
7、Propagtion.NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行。如果當前沒有事務則該取值等價于Propagtion.REQUIRED。
舉例記憶:
事務傳播機制場景演示
此時,用戶注冊不僅要在用戶表中添加數據,在日志表中也需要進行登記。
REQUIRE
Controller:
@RequestMapping("/user2")
@RestController
public class UserController2 {@Autowiredprivate UserService userService;@Autowiredprivate LogService logService;@Transactional@RequestMapping("/register")public boolean register(String userName,String password){/*** 用戶表和注冊表的插入理應再Service完成* 此處為了方便,直接在Controller中完成*/if(!StringUtils.hasLength(userName)||!StringUtils.hasLength(password)){return false;}Integer result = userService.insert(userName,password);System.out.println("result:"+result);//插入日志表Integer insert = logService.insert(userName, "用戶注冊");System.out.println("insert:"+insert);return true;}
}
Service:
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);return result;}
}
UserSerVice:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
mapper:
UserinfoMapper:
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info (user_name,password) values (#{userName},#{password})")Integer insert(String userName,String password);}
LoginfoMapper:
@Mapper
public interface LoginfoMapper {@Insert("insert into log_info(user_name,op) values (#{userName},#{op}) ")Integer insertLog(String userName,String op);
}
我們嘗試在其中的一個Service中制造異常:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.REQUIRED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);//制造異常int a = 10/0;return result;}
}
測試:
從日志上可以看出,事務發生了回滾:
總結:
REQUIRE_NEW
修改Service代碼即可:
UserService:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.REQUIRES_NEW)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);//制造異常int a = 10/0;return result;}
}
再次進行測試,通過日志可以看到,日志表的事務發生了回滾,而用戶表的事務提交了:
總結:
NEVER
UserService:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.NEVER)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NEVER)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);return result;}
}
這里我們不制造異常,但是讓Controller存在事務進行測試:
可以看到仍然報了500:
NESTED
UserService:
@Service
public class UserService {@Autowiredprivate UserInfoMapper mapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String password) {return mapper.insert(userName,password);}
}
LogService:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);return result;}
}
測試沒有異常的情況:
事務得到了提交:
測試有異常發生的情況:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);int a = 10/0;return result;}
}
事務回滾:
看起來NESTED傳播機制好像跟REQUIRE機制沒什么區別:但實際上NESTED可以實現部分回滾,使得其他事務能夠被提交。
部分回滾:
@Service
public class LogService {@Autowiredprivate LoginfoMapper loginfoMapper;@Transactional(propagation = Propagation.NESTED)public Integer insert(String userName, String op) {Integer result = loginfoMapper.insertLog(userName, op);try {int a = 10/0;} catch (Exception e) {//部分回滾TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return result;}
}
重新測試:
事務得到提交:
總結: