虛擬服務(wù)器建網(wǎng)站2023最新15件重大新聞
事務(wù)是一組操作的集合, 是一個(gè)不可分割的操作.會(huì)把所有的操作作為一個(gè)整體, 一起向數(shù)據(jù)庫(kù)提交或者是撤銷(xiāo)操作請(qǐng)求. 所以這組操作要么同時(shí)成功, 要么同時(shí)失敗.
為什么需要事務(wù)?
我們?cè)谶M(jìn)行程序開(kāi)發(fā)時(shí), 也會(huì)有事務(wù)的需求.
比如轉(zhuǎn)賬操作:
第一步:A 賬戶(hù) -100 元.
第二步:B 賬戶(hù) +100 元.
如果沒(méi)有事務(wù),第一步執(zhí)行成功了, 第二步執(zhí)行失敗了, 那么A 賬戶(hù)的100 元就平白無(wú)故消失了. 如果使用事務(wù)就可以解決這個(gè)問(wèn)題, 讓這一組操作要么一起成功, 要么一起失敗.
比如秒殺系統(tǒng),
第一步: 下單成功
第二步: 扣減庫(kù)存
下單成功后, 庫(kù)存也需要同步減少. 如果下單成功, 庫(kù)存扣減失敗, 那么就會(huì)造成下單超出的情況. 所以就需要把這兩步操作放在同一個(gè)事務(wù)中. 要么一起成功, 要么一起失敗.
理解事務(wù)概念為主, 實(shí)際企業(yè)開(kāi)發(fā)時(shí), 并不是簡(jiǎn)單的通過(guò)事務(wù)來(lái)處理.
事務(wù)的操作
事務(wù)的操作主要有三步:
- 開(kāi)啟事start transaction/ begin (一組操作前開(kāi)啟事務(wù))
- 提交事務(wù): commit (這組操作全部成功, 提交事務(wù))
- 回滾事務(wù): rollback (這組操作中間任何一個(gè)操作出現(xiàn)異常, 回滾事務(wù))
-- 開(kāi)啟事務(wù)
start transaction;
-- 提交事務(wù)
commit;
-- 回滾事務(wù)
rollback;
Spring中事務(wù)的實(shí)現(xiàn)
Spring 中的事務(wù)操作分為兩類(lèi):
- 編程式事務(wù)(手動(dòng)寫(xiě)代碼操作事務(wù)).
- 聲明式事務(wù)(利用注解自動(dòng)開(kāi)啟和提交事務(wù)).
-- 創(chuàng)建數(shù)據(jù)庫(kù)
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
-- 用戶(hù)表
DROP TABLE IF EXISTS 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 = '用戶(hù)表';
-- 操作日志表
DROP TABLE IF EXISTS 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';
配置文件
# 數(shù)據(jù)庫(kù)連接配置
spring:datasource:url: jdbc:mysql://localhost:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123driver-class-name: com.mysql.cj.jdbc.Driver# 設(shè)置動(dòng)態(tài)代理的方式 true jdk代理, false cglib代理aop:proxy-target-class: true
mybatis:configuration:map-underscore-to-camel-case: true #配置駝峰自動(dòng)轉(zhuǎn)換
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql語(yǔ)句mapper-locations: classpath:mapper/**Mapper.xml
# 設(shè)置日志文件的文件名
logging:file:name: logger/spring-book.log
實(shí)體類(lèi)
@Data
public class LogInfo {private Integer id;private String userName;private String op;private Date createTime;private Date updateTime;
}@Data
public class UserInfo {private Integer id;private String userName;private String password;private Date createTime;private Date updateTime;
}
mapper
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info(`user_name`,`password`)values(#{name},#{password})")Integer insert(String name,String password);
}import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LogInfoMapper {@Insert("insert into log_info(`user_name`,`op`)values(#{name},#{op})")Integer insertLog(String name,String op);
}
service
@Slf4j
@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public void registryUser(String name,String password){
//插入用戶(hù)信息userInfoMapper.insert(name,password);}
}@Slf4j
@Service
public class LogService {@Autowiredprivate LogInfoMapper logInfoMapper;public void insertLog(String name,String op){//記錄用戶(hù)操作logInfoMapper.insertLog(name,"用戶(hù)注冊(cè)");}
}
controller
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name,String password){//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);return "注冊(cè)成功";}
}
Spring編程式事務(wù)
Spring 手動(dòng)操作事務(wù)和上面 MySQL 操作事務(wù)類(lèi)似, 有 3 個(gè)重要操作步驟:
? 開(kāi)啟事務(wù)(獲取事務(wù))
? 提交事務(wù)
? 回滾事務(wù)
SpringBoot 內(nèi)置了兩個(gè)對(duì)象:
- DataSourceTransactionManager 事務(wù)管理器. 用來(lái)獲取事務(wù)(開(kāi)啟事務(wù)), 提交或回滾事務(wù)的
- TransactionDefinition 是事務(wù)的屬性, 在獲取事務(wù)的時(shí)候需要將TransactionDefinition 傳遞進(jìn)去從而獲得一個(gè)事務(wù) TransactionStatus
事務(wù)交提
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name,String password){TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);// 提交事務(wù)dataSourceTransactionManager.commit(transaction);//回滾事務(wù)//dataSourceTransactionManager.rollback(transactionStatus);return "注冊(cè)成功";}
}
http://localhost:8080/user/registry?name=user1&password=123123
事務(wù)回滾
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;@Autowiredprivate UserService userService;@RequestMapping("/registry")public String registry(String name,String password){TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);//回滾事務(wù)dataSourceTransactionManager.rollback(transactionStatus);return "注冊(cè)成功";}
}
http://localhost:8080/user/registry?name=user2&password=123123
雖然返回結(jié)果時(shí)注冊(cè)成功,但是數(shù)據(jù)庫(kù)中并沒(méi)有插入數(shù)據(jù)。
Spring聲明式事務(wù)@Transactional
聲明式事務(wù)的實(shí)現(xiàn)很簡(jiǎn)單, 只需要在需要事務(wù)的方法上添加 @Transactional 注解就可以實(shí)現(xiàn)了.無(wú)需手動(dòng)開(kāi)啟事務(wù)和提交事務(wù), 進(jìn)入方法時(shí)自動(dòng)開(kāi)啟事務(wù), 方法執(zhí)行完會(huì)自動(dòng)提交事務(wù), 如果中途發(fā)生了沒(méi)有處理的異常會(huì)自動(dòng)回滾事務(wù).
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name,String password){userService.registryUser(name,password);return "注冊(cè)成功";}
}
http://localhost:8080/user/registry?name=user2&password=123123
數(shù)據(jù)庫(kù)中插入了數(shù)據(jù)。
使程序出現(xiàn)異常
@RequestMapping("/user")
@RestController
public class UserController {@Autowiredprivate UserService userService;@Transactional@RequestMapping("/registry")public String registry(String name,String password){userService.registryUser(name,password);int a = 10 / 0;return "注冊(cè)成功";}
}
事務(wù)會(huì)進(jìn)行回滾,所以數(shù)據(jù)庫(kù)中沒(méi)有新插入的數(shù)據(jù)。
我們一般會(huì)在業(yè)務(wù)邏輯層當(dāng)中來(lái)控制事務(wù), 因?yàn)樵跇I(yè)務(wù)邏輯層當(dāng)中, 一個(gè)業(yè)務(wù)功能可能會(huì)包含多個(gè)數(shù)據(jù)訪問(wèn)的操作. 在業(yè)務(wù)邏輯層來(lái)控制事務(wù), 我們就可以將多個(gè)數(shù)據(jù)訪問(wèn)操作控制在一個(gè)事務(wù)范圍內(nèi).
Transactional作用
@Transactional 可以用來(lái)修飾方法或類(lèi):
? 修飾方法時(shí): 只有修飾public 方法時(shí)才生效(修飾其他方法時(shí)不會(huì)報(bào)錯(cuò), 也不生效)[推薦]
? 修飾類(lèi)時(shí): 對(duì)@Transactional 修飾的類(lèi)中所有的 public 方法都生效.
方法/類(lèi)被 @Transactional 注解修飾時(shí), 在目標(biāo)方法執(zhí)行開(kāi)始之前, 會(huì)自動(dòng)開(kāi)啟事務(wù), 方法執(zhí)行結(jié)之后, 自動(dòng)提交事務(wù).如果在方法執(zhí)行過(guò)程中, 出現(xiàn)異常, 且異常未被捕獲, 就進(jìn)行事務(wù)回滾操作.如果異常被程序捕獲, 方法就被認(rèn)為是成功執(zhí)行, 依然會(huì)提交事務(wù).
@Transactional
@RequestMapping("/registry")
public String registry(String name,String password){//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);log.info("用戶(hù)數(shù)據(jù)插入成功");//對(duì)異常進(jìn)行捕獲try {//強(qiáng)制程序拋出異常int a = 10/0;}catch (Exception e){e.printStackTrace();}return "注冊(cè)成功";
}
運(yùn)行程序, 發(fā)現(xiàn)雖然程序出錯(cuò)了, 但是由于異常被捕獲了, 所以事務(wù)依然得到了提交.
如果需要事務(wù)進(jìn)行回滾, 有以下兩種方式:
- 重新拋出異常
@Transactional
@RequestMapping("/registry")
public String registry(String name,String password){//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);log.info("用戶(hù)數(shù)據(jù)插入成功");//對(duì)異常進(jìn)行捕獲try {//強(qiáng)制程序拋出異常int a = 10/0;}catch (Exception e){//將異常重新拋出去throw e;}return "注冊(cè)成功";
}
- 手動(dòng)回滾事務(wù)
// 使用 TransactionAspectSupport.currentTransactionStatus() 得到當(dāng)前的事務(wù), 并
// 使用 setRollbackOnly 設(shè)置setRollbackOnly
@Transactional
@RequestMapping("/registry")
public String registry(String name,String password){//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);log.info("用戶(hù)數(shù)據(jù)插入成功");//對(duì)異常進(jìn)行捕獲try {//強(qiáng)制程序拋出異常int a = 10/0;}catch (Exception e){// 手動(dòng)回滾事務(wù)TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return "注冊(cè)成功";
}
Transactional詳解
通過(guò)上面的代碼, 我們學(xué)習(xí)了@Transactional 的基本使用. 接下來(lái)我們學(xué)習(xí)@Transactional注解的使用細(xì)節(jié).
我們主要學(xué)習(xí)@Transactional 注解當(dāng)中的三個(gè)常見(jiàn)屬性:
- rollbackFor: 異?;貪L屬性. 指定能夠觸發(fā)事務(wù)回滾的異常類(lèi)型. 可以指定多個(gè)異常類(lèi)型
- Isolation: 事務(wù)的隔離級(jí)別. 默認(rèn)值為Isolation.DEFAULT
- propagation: 事務(wù)的傳播機(jī)制. 默認(rèn)值為 Propagation.REQUIRED
rollbackFor
@Transactional 默認(rèn)只在遇到運(yùn)行時(shí)異常和Error時(shí)才會(huì)回滾, 非運(yùn)行時(shí)異常不回滾. 即Exception的子類(lèi)中, 除了RuntimeException及其子類(lèi).
修改一下代碼
@Transactional
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);log.info("用戶(hù)數(shù)據(jù)插入成功");if (true){throw new IOException();}return "r2";
}
發(fā)現(xiàn)雖然程序拋出了異常, 但是事務(wù)依然進(jìn)行了提交.
如果我們需要所有異常都回滾, 需要來(lái)配置@Transactional 注解當(dāng)中的rollbackFor 屬性, 通過(guò)rollbackFor 這個(gè)屬性指定出現(xiàn)何種異常類(lèi)型時(shí)事務(wù)進(jìn)行回滾.
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/r2")
public String r2(String name,String password) throws IOException {//用戶(hù)注冊(cè)u(píng)serService.registryUser(name,password);log.info("用戶(hù)數(shù)據(jù)插入成功");if (true){throw new IOException();}return "r2";
}
數(shù)據(jù)回滾了。
在Spring的事務(wù)管理中,默認(rèn)只在遇到運(yùn)行時(shí)異常RuntimeException和Error時(shí)才會(huì)回滾.
如果需要回滾指定類(lèi)型的異常, 可以通過(guò)rollbackFor屬性來(lái)指定.
事務(wù)隔離級(jí)別
MySQL事務(wù)隔離級(jí)別
- 讀未提交(READ UNCOMMITTED): 讀未提交, 也叫未提交讀. 該隔離級(jí)別的事務(wù)可以看到其他事務(wù)中未提交的數(shù)據(jù).(因?yàn)槠渌聞?wù)未提交的數(shù)據(jù)可能會(huì)發(fā)生回滾, 但是該隔離級(jí)別卻可以讀到, 我們把該級(jí)別讀到的數(shù)
據(jù)稱(chēng)之為臟數(shù)據(jù), 這個(gè)問(wèn)題稱(chēng)之為臟讀.) - 讀提交(READ COMMITTED): 讀已提交, 也叫提交讀. 該隔離級(jí)別的事務(wù)能讀取到已經(jīng)提交事務(wù)的數(shù)據(jù),(該隔離級(jí)別不會(huì)有臟讀的問(wèn)題.但由于在事務(wù)的執(zhí)行中可以讀取到其他事務(wù)提交的結(jié)果, 所以在不同時(shí)間的相同 SQL 查詢(xún)可能會(huì)得到不同的結(jié)果, 這種現(xiàn)象叫做不可重復(fù)讀)
- 可重復(fù)讀(REPEATABLE READ): 事務(wù)不會(huì)讀到其他事務(wù)對(duì)已有數(shù)據(jù)的修改, 即使其他事務(wù)已提交. 也就可以確保同一事務(wù)多次查詢(xún)的結(jié)果一致, 但是其他事務(wù)新插入的數(shù)據(jù), 是可以感知到的. 這也就引發(fā)了幻讀問(wèn)題. 可重復(fù)讀, 是 MySQL 的默認(rèn)事務(wù)隔離級(jí)別.(比如此級(jí)別的事務(wù)正在執(zhí)行時(shí), 另一個(gè)事務(wù)成功的插入了某條數(shù)據(jù), 但因?yàn)樗看尾樵?xún)的結(jié)果都是一樣的, 所以會(huì)導(dǎo)致查詢(xún)不到這條數(shù)據(jù), 自己重復(fù)插入時(shí)又失敗(因?yàn)槲ㄒ患s束的原因). 明明在事務(wù)中查詢(xún)不到這條信息,但自己就是插入不進(jìn)去, 這個(gè)現(xiàn)象叫幻讀.)
- 串行化(SERIALIZABLE): 序列化, 事務(wù)最高隔離級(jí)別. 它會(huì)強(qiáng)制事務(wù)排序, 使之不會(huì)發(fā)生沖突, 從而解決了臟讀, 不可重復(fù)讀和幻讀問(wèn)題, 但因?yàn)閳?zhí)行效率低, 所以真正使用的場(chǎng)景并不多.
Spring事務(wù)隔離級(jí)別
Spring 中事務(wù)隔離級(jí)別有5 種:
- Isolation.DEFAULT : 以連接的數(shù)據(jù)庫(kù)的事務(wù)隔離級(jí)別為主.
- Isolation.READ_UNCOMMITTED : 讀未提交, 對(duì)應(yīng)SQL標(biāo)準(zhǔn)中 READ UNCOMMITTED
- Isolation.READ_COMMITTED : 讀已提交,對(duì)應(yīng)SQL標(biāo)準(zhǔn)中 READ COMMITTED
- Isolation.REPEATABLE_READ : 可重復(fù)讀, 對(duì)應(yīng)SQL標(biāo)準(zhǔn)中 REPEATABLE READ
- Isolation.SERIALIZABLE : 串行化, 對(duì)應(yīng)SQL標(biāo)準(zhǔn)中 SERIALIZABLE
public enum Isolation {DEFAULT(-1),READ_UNCOMMITTED(1),READ_COMMITTED(2),REPEATABLE_READ(4),SERIALIZABLE(8);private final int value;private Isolation(int value) {this.value = value;}public int value() {return this.value;}
}
Spring 中事務(wù)隔離級(jí)別可以通過(guò) @Transactional 中的 isolation 屬性進(jìn)行設(shè)置
@Transactional(isolation = Isolation.READ_COMMITTED)
@RequestMapping("/r3")
public String r3(String name,String password) throws IOException {//... 代碼省略return "r3";
}
Spring事務(wù)傳播機(jī)制
事務(wù)傳播機(jī)制就是: 多個(gè)事務(wù)方法存在調(diào)用關(guān)系時(shí), 事務(wù)是如何在這些方法間進(jìn)行傳播的.
比如有兩個(gè)方法A, B都被@Transactional 修飾, A方法調(diào)用B方法,A方法運(yùn)行時(shí), 會(huì)開(kāi)啟一個(gè)事務(wù). 當(dāng)A調(diào)用B時(shí), B方法本身也有事務(wù), 此時(shí)B方法運(yùn)行時(shí), 是加入A的事務(wù), 還是創(chuàng)建一個(gè)新的事務(wù)呢?
這個(gè)就涉及到了事務(wù)的傳播機(jī)制.
事務(wù)隔離級(jí)別解決的是多個(gè)事務(wù)同時(shí)調(diào)用一個(gè)數(shù)據(jù)庫(kù)的問(wèn)題
而事務(wù)傳播機(jī)制解決的是一個(gè)事務(wù)在多個(gè)節(jié)點(diǎn)(方法)中傳遞的問(wèn)題
事務(wù)傳播機(jī)制有哪些
@Transactional 注解支持事務(wù)傳播機(jī)制的設(shè)置, 通過(guò) propagation 屬性來(lái)指定傳播行為.
Spring 事務(wù)傳播機(jī)制有以下 7 種:
- Propagation.REQUIRED : 默認(rèn)的事務(wù)傳播級(jí)別. 如果當(dāng)前存在事務(wù), 則加入該事務(wù). 如果當(dāng)前沒(méi)有事務(wù), 則創(chuàng)建一個(gè)新的事務(wù).
- Propagation.SUPPORTS : 如果當(dāng)前存在事務(wù), 則加入該事務(wù). 如果當(dāng)前沒(méi)有事務(wù), 則以非事務(wù)的方式繼續(xù)運(yùn)行.
- Propagation.MANDATORY :強(qiáng)制性. 如果當(dāng)前存在事務(wù), 則加入該事務(wù). 如果當(dāng)前沒(méi)有事務(wù), 則拋出異常.
- Propagation.REQUIRES_NEW : 創(chuàng)建一個(gè)新的事務(wù). 如果當(dāng)前存在事務(wù), 則把當(dāng)前事務(wù)掛起. 也就是說(shuō)不管外部方法是否開(kāi)啟事務(wù), Propagation.REQUIRES_NEW 修飾的內(nèi)部方法都會(huì)新開(kāi)啟自己的事務(wù), 且開(kāi)啟的事務(wù)相互獨(dú)立, 互不干擾.
- Propagation.NOT_SUPPORTED : 以非事務(wù)方式運(yùn)行, 如果當(dāng)前存在事務(wù), 則把當(dāng)前事務(wù)掛起(不用).
- Propagation.NEVER : 以非事務(wù)方式運(yùn)行, 如果當(dāng)前存在事務(wù), 則拋出異常.
- Propagation.NESTED : 如果當(dāng)前存在事務(wù), 則創(chuàng)建一個(gè)事務(wù)作為當(dāng)前事務(wù)的嵌套事務(wù)來(lái)運(yùn)行.如果當(dāng)前沒(méi)有事務(wù), 則該取值等價(jià)于 PROPAGATION_REQUIRED .
public enum Propagation {REQUIRED(0),SUPPORTS(1),MANDATORY(2),REQUIRES_NEW(3),NOT_SUPPORTED(4),NEVER(5),NESTED(6);private final int value;private Propagation(int value) {this.value = value;}public int value() {return this.value;}
}
- Spring中使用事務(wù), 有兩種方式: 編程式事務(wù)(手動(dòng)操作)和聲明式事務(wù). 其中聲明式事務(wù)使用較多,在方法上添加 @Transactional 就可以實(shí)現(xiàn)了
- 通過(guò) @Transactional(isolation = Isolation.SERIALIZABLE) 設(shè)置事務(wù)的隔離級(jí)別. Spring 中的事務(wù)隔離級(jí)別有 5 種
- 通過(guò) @Transactional(propagation = Propagation.REQUIRED) 設(shè)置事務(wù)的傳播機(jī)制, Spring 中的事務(wù)傳播級(jí)別有 7 種, 重點(diǎn)關(guān)注 REQUIRED (默認(rèn)值) 和 REQUIRES_NEW