spring的聲明式事務
什麼是事務?
一組操作,形成一個業務,那麼這組操作要麼都成功,要麼都失敗。保證業務操作完整性一種操作。
示例:比如轉賬,張三賬戶扣錢,和李四賬戶加錢,這兩個操作一定要同時成功。
Spring JdbcTemplate
在spring中為了更加友善的操作JDBC,在JDBC的基礎之上定義了一個抽象層,此設計的目的是為不同類型的JDBC操作提供模闆方法,每個模闆方法都能控制整個過程,并允許覆寫過程中的特定任務,通過這種方式,可以盡可能保留靈活性,将資料庫存取的工作量降到最低。
引入依賴
<!--spring整合第三方ORM架構的包,這個依賴還會同時引入spring-jdbc和spring-tx(事務)的包還有springIoc的基礎jar包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!-- <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.6.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.6.RELEASE</version>
<scope>compile</scope>
</dependency>-->
驅動和資料庫版本對應關系:https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-versions.html
配置連接配接池和JdbcTemplate對象
<!--配置掃描-->
<context:component-scan base-package="com.blog"/>
<!--引入外部配置檔案-->
<context:property-placeholder location="db.properties"/>
<!--配置連接配接池對象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
<property name="url" value="${mysql.url}"/>
<property name="driverClassName" value="${mysql.driverClassName}"/>
</bean>
<!--配置jdbc-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--具名參數jdbc處理類-->
<bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate" id="namedParameterJdbcTemplate">
<constructor-arg type="javax.sql.DataSource" ref="dataSource"/>
</bean>
測試方法展示
ClassPathXmlApplicationContext context;
@Before
public void before(){
context = new ClassPathXmlApplicationContext("classpath:spring-ioc.xml");
}
@Test
public void test1(){
DruidDataSource dataSource = context.getBean("dataSource", DruidDataSource.class);
System.out.println(dataSource);
}
/*
* jdbcTemplate連接配接測試
* */
@Test
public void test2(){
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
Integer integer = jdbcTemplate.queryForObject("select count(1) from user", Integer.class);
System.out.println(integer);
}
/*
* jdbcTemplate連接配接測試
* */
@Test
public void test3(){
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
/*
* 如果和資料庫字段一緻
* */
// User user = jdbcTemplate.queryForObject("select count(1) from User", new BeanPropertyRowMapper<>(User.class));
User o = jdbcTemplate.queryForObject("select * from user where id=1",
(resultSet, i) -> {
/*從結果集中擷取資料*/
User user = new User();
user.setBalance(resultSet.getInt("BALANCE"));
user.setId(resultSet.getInt("ID"));
user.setCardno(resultSet.getString("CARD_NO"));
return user;
});
System.out.println(o);
}
/*
* 查詢實體list
* */
@Test
public void test4(){
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
List<User> userList = jdbcTemplate.query("select * from user", new RowMapper<User>() {
@Override
public User mapRow(ResultSet resultSet, int i) throws SQLException {
/*從結果集中擷取資料*/
User user = new User();
user.setBalance(resultSet.getInt("BALANCE"));
user.setId(resultSet.getInt("ID"));
user.setCardno(resultSet.getString("CARD_NO"));
return user;
}
});
System.out.println(userList);
}
/*
* 新增
* */
@Test
public void test5(){
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
int i = jdbcTemplate.update("insert into user (REAL_NAME, CARD_NO, BALANCE) VALUES (?,?,?)", "李四", "133", "546");
System.out.println(i);
}
/*
* 修改
* */
@Test
public void test6(){
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
int i = jdbcTemplate.update("update user set BALANCE = ? where ID = 3", "546");
System.out.println(i);
}
/*
* 删除
* */
@Test
public void test7(){
JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
int i = jdbcTemplate.update("delete from user where ID =?", 3);
System.out.println(i);
}
/**
* 具名參數處理NamedParameterJdbcTemplate
*/
@Test
public void test08(){
NamedParameterJdbcTemplate jdbcTemplate = context.getBean(NamedParameterJdbcTemplate.class);
Map<String,Object> map=new HashMap<>();
map.put("id",2);
// 修改類同
User user = jdbcTemplate.queryForObject("select * from user where ID = :id", map, new RowMapper<User>() {
@Override
public User mapRow(ResultSet resultSet, int i) throws SQLException {
/*從結果集中擷取資料*/
User user = new User();
// 如果查詢到的資料為0條,傳回null
if(i == 0){
return null;
}
user.setBalance(resultSet.getInt("BALANCE"));
user.setId(resultSet.getInt("ID"));
user.setCardno(resultSet.getString("CARD_NO"));
return user;
}
});
System.out.println(user);
}
dao層使用示例
@Repository
public class UserDaoImpl implements UserDao {
private JdbcTemplate jdbcTemplate;
/*
* JdbcTemplate線程安全的,這以為你可以在多個dao中使用同一個JdbcTemplate的執行個體,當然也可以在同一個dao中定義為私有屬性
* 官方:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate-idioms
* 這個是官方推薦用法。一個dao對應一個JdbcTemplate
* */
@Autowired
public UserDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
事務四大特性ACID
ACID 四大特性
- A (Atomicity)原子性:原子性指的是 在一組業務操作下 要麼都成功 要麼都失敗在一組增删改查的業務下 要麼都送出 要麼都復原
- C(Consistency) 一緻性:事務前後的資料要保證資料的一緻性。由一個一緻性狀态變為另一個一緻性狀态。比如張三給李四轉1000元,但是張三李四總的金額保持不變。
- I (Isolation)隔離性:多線程情況下,一個事務的執行不應該被另一個事務影響。
- D (Durability)持久性:事務送出後對資料的改變時永久性的。
總結:在事務控制方面,主要有兩個分類:
程式設計式事務
在代碼中直接加入處理事務的邏輯,可能需要在代碼中顯式調用開啟事務,送出,復原,例、beginTransaction()、commit()、rollback()等事務管理相關的方法
connetion.autoCommit(false);
connction.commint()
catch(){
connction.rollback();
}
聲明式事務
在方法的外部添加注解或者直接在配置檔案中定義,将事務管理代碼從業務方法中分離出來,以聲明的方式來實作事務管理。spring的AOP恰好可以完成此功能:事務管理代碼的固定模式作為一種橫切關注點,通過AOP方法子產品化,進而實作聲明式事務
spring聲明式事務使用
配置事務管理器和開啟基于注解的事務控制模式
<!--配置事務管理器,由于事務底層操作都是通過連接配接來進行事務開啟,送出復原等操作,是以需要配置資料源-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--開啟基于注解的事務控制模式,基于tx命名空間
xmlns:tx="http://www.springframework.org/schema/tx
如果注解的配置和xml都配置,注解優先
@EnableTransactionManagement 加在配置類上
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
在需要的方法上面添加注解 @Transactional
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User getUser(){
return userDao.getUser();
}
/*
* 可以标記在類上面(表示所有方法都加上這個注解),也可以标記在方法上面,如果同時有這個注解,以方法上的為準
* 建議寫在方法上,力度更細
* 建議下載下傳業務邏輯層。
* */
@Override
@Transactional
public void trans() {
userDao.sub();
System.out.println("張三扣錢完成");
int i = 1/0;
userDao.add();
}
}
@Transactional事務屬性配置
isolation:設定事務的隔離級别
propagation:事務的傳播行為
noRollbackFor:那些異常事務可以不復原
noRollbackForClassName:填寫的參數是全類名
rollbackFor:哪些異常事務需要復原
rollbackForClassName:填寫的參數是全類名
readOnly:設定事務是否為隻讀事務
timeout:事務超出指定執行時長後自動終止并復原,機關是秒
isolation設定事務的隔離級别
用來處理并發事務下的一些問題
/*
*Isolation設定事務的隔離級别
* Isolation.DEFAULT 使用資料庫預設的資料庫隔離級别--預設
* Isolation.READ_UNCOMMITTED 讀未送出
* Isolation.READ_COMMITTED 讀已送出(不可重複讀)
* Isolation.REPEATABLE_READ 可重複讀
* Isolation.SERIALIZABLE 串行話
* */
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void trans() {
//修改,删除等多個操作
}
事務隔離級别
事務隔離級别 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
讀未送出(read-uncommitted) | 是 | 是 | 是 |
讀已送出(read-committed) | 否 | 是 | 是 |
可重複讀(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
髒讀:一個事務有a和b兩個操作,a操作修改了資料庫,這個時候另一個事務讀取到了a操作修改的資料,然後b操作執行失敗,事務復原。
不可重複讀: 事務a讀取資料庫資料,事務b在此過程中修改了資料庫的資料,造成事務a兩次讀取事務結果不一緻。而且不一緻後還可以修改。
可重複讀:事務a讀取資料後,事務b修改事務a讀取的資料後,是以事務a再次擷取資料還是一樣的。
串行話:a和b兩個事務不在同時執行,而是強行控制先後執行
查詢資料庫的預設隔離級别
select @@tx_isolation
程式測試-可重複讀–幻讀:
參考:
https://blog.csdn.net/sanyuesan0000/article/details/90235335?utm_term=mysql%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%BD%B1%E5%93%8D&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allsobaiduweb~default-0-90235335&spm=3001.4430
https://blog.csdn.net/qq_31930499/article/details/110393988
可重複讀下出現幻讀情況測試
mvcc -->多版本并發控制
在《高性能MySQL》中對MVCC的解釋如下
InnoDB的mvcc,是通過在每行記錄後面儲存兩個隐藏的列來實作的。這兩個列,一個儲存了行的建立時間,一個儲存行的過期時間(或删除時間)。當然存儲的并不是實際的時間值,而是系統版本号(system version number),每開始一個新的事務,系統版本号都會自動遞增。事務開始時刻的系統版本号會作為事務的版本号,用來和查詢到的每行記錄的版本号進行比較。下面看一下在REPEATABLE READ隔離級别下, MVCC具體是如何操作的。
SELECT
InnoDB會根據以下兩個條件檢查每行記錄:
a. InnoDB隻查找版本早于目前事務版本的資料行(也就是,行的系統版本号小于或等于事務的系統版本号),這樣可以確定事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
b.行的删除版本要麼未定義,要麼大于目前事務版本号。這可以確定事務讀取到的行,在事務開始之前未被删除。隻有符合上述兩個條件的記錄,才能傳回作為查詢結果。
INSERT
InnoDB為新插入的每一行儲存目前系統版本号作為行版本号。
DELETE
InnoDB為删除的每一行儲存目前系統版本号作為行删除辨別。
UPDATE
InnoDB為插入一行新記錄,儲存目前系統版本号作為行版本号,同時儲存目前系紡版本号到原來的行作為行删除辨別。
一下是測試過程
事務a | 事務b | |
---|---|---|
T1 | select * from user | |
T2 | update user set balance=balance-200 | |
T3 | update user set balance=balance-200 | insert into user (REAL_NAME, CARD_NO, BALANCE) VALUES (?,?,?)", “李四”, “133”, “546” |
T4 | commit | |
T5 | select * from user | |
T6 | commit | |
T7 | ||

測試結果了解:事務a查詢後執行更新操作,由于和事務b更新的是相同的資料,且事務b先執行,是以此時事務a因為鎖的問題陷入等待,是以此時事務b送出後才可以修改,此時事務b修改和新增的資料會标記上對應的版本号,此時事務a修改資料,由于修改時目前讀的操作,會擷取到事務b已經送出的資料,事務b再進行修改後再次更新了所有資料的版本号,包括事務b剛新增的那一條,是以事務a再次查詢會多查出來一條。
propagation事務的傳播特性
事務的傳播特性指的是當一個事務方法被另一個事務方法調用時,這個事務方法
應該如何進行?
希望如果外部存在事務就用外部的, 外部不存在就自己開啟事務
a上開啟的事務叫目前事務,相對于b和c也叫外部事務
a(){
b();
c();
}
事務傳播行為 | 外部不存在事務 | 外部存在事務 | 使用場景 |
---|---|---|---|
REQUIRED(預設) | 開啟新的事務 | 融合到外部事務中 | @Transactional(propagation = Propagation.REQUIRED)适用增删改查 |
SUPPORTS | 不開啟新的事務 | 融合到外部事務中 | @Transactional(propagation = Propagation.SUPPORTS)适用查詢 |
REQUIRES_NEW | 開啟新的事務 | 挂起外部事務,建立新的事務 | @Transactional(propagation = Propagation.REQUIRES_NEW)适用内部事務和外部事務不存在業務關聯情況,如日志 |
NOT_SUPPORTED | 不開啟新的事務 | 挂起外部事務 | @Transactional(propagation =Propagation.NOT_SUPPORTED)不常用 |
NEVER | 不開啟新的事務 | 抛出異常 | @Transactional(propagation = Propagation.NEVER )不常用 |
MANDATORY | 抛出異常 | 融合到外部事務中 | @Transactional(propagation = Propagation.MANDATORY)不常用 |
timeout事務執行逾時時間
指定事務等待的最長時間(秒)
目前事務通路資料時,有可能通路的資料被别的資料進行加鎖的處理,那麼此時事務就必須等待,如果等待時間過長給使用者造成的體驗感差。
設定事務隻讀(readOnly)
readonly:隻會設定在查詢的業務方法中
connection.setReadOnly(true) 通知資料庫,目前資料庫操作是隻讀,資料庫就會對目前隻讀做相應優化
使用場景:
如果你一次執行單條查詢語句,則沒有必要啟用事務支援,資料庫默
認支援SQL執行期間的讀一緻性;
如果你一次執行多條查詢語句,例如統計查詢,報表查詢,在這種場
景下,多條查詢SQL必須保證整體的讀一緻性,否則,在前條SQL查詢之後,後
條SQL查詢之前,資料被其他使用者改變,則該次整體的統計查詢将會出現讀資料
不一緻的狀态,此時,應該啟用事務支援(如:設定不可重複度、幻影讀級
别)。
異常屬性
設定 目前事務出現的那些異常就進行復原或者送出。
預設對于RuntimeException 及其子類 采用的是復原的政策。
預設對于Exception 及其子類 采用的是送出的政策。
1、設定哪些異常不復原(noRollbackFor)
2、設定哪些異常復原(rollbackFor )
@Transactional(timeout = 3,rollbackFor = {FileNotFoundException.class})
在實戰中事務的使用方式
如果目前業務方法是一組 增、改、删 可以這樣設定事務
@Transactional
如果目前業務方法是一組 查詢 可以這樣設定事務
@Transactionl(readOnly=true)
如果目前業務方法是單個 查詢 可以這樣設定事務
@Transactionl(propagation=propagation.SUPPORTS ,readOnly=true)
基于xml的使用方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置掃描-->
<context:component-scan base-package="com.blog"/>
<!--引入外部配置檔案-->
<context:property-placeholder location="db.properties"/>
<!--配置連接配接池對象-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
<property name="url" value="${mysql.url}"/>
<property name="driverClassName" value="${mysql.driverClassName}"/>
</bean>
<!--配置jdbc-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--具名參數jdbc處理類-->
<bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate" id="namedParameterJdbcTemplate">
<constructor-arg type="javax.sql.DataSource" ref="dataSource"/>
</bean>
<!--配置事務管理器,由于事務底層操作都是通過連接配接來進行事務開啟,送出復原等操作,是以需要配置資料源-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--開啟基于注解的事務控制模式,基于tx命名空間
xmlns:tx="http://www.springframework.org/schema/tx -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
<!--聲明式事務通過aop實作,在方法的不同位置,通過連接配接對象開啟,復原,送出事務 -->
<aop:config>
<!--比對業務實作層所有類和方法-->
<aop:pointcut id="transactionCut" expression="execution(* com.blog.service.impl.*.*(..))"/>
<aop:advisor advice-ref="myAdvice" pointcut-ref="transactionCut"/>
</aop:config>
<!--明确切點比對到的方法那些要聲明事務-->
<tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<!-- 通配符-->
<tx:method name="update*"/>
<tx:method name="delete*"/>
<tx:method name="add*"/>
<!--配置get開頭的方法為隻讀,且當目前事務不存在時不開啟事務,存在時融入目前事務-->
<tx:method name="get" read-only="true" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
</beans>