声明式事务
①基本原理:AOP[1]前置通知:开启事务
[2]返回通知:提交事务
[3]异常通知:回滚事务
[4]后置通知:释放资源
②事务管理器
③导入jar包
[1]IOC容器需要的jar包
[2]AOP需要的jar包
[3]JdbcTemplate操作需要的jar包
[5]MySQL驱动和C3P0
④配置
[1]配置数据源
[2]配置JdbcTemplate,并装配数据源
[3]配置事务管理器,并装配数据源
<!-- 配置数据库连接池 -->
<bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>
<!-- 配置JDBCTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="comboPooledDataSource"></property>
</bean>
<!-- 配置事务管理器,并为事务管理器配置数据源!-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="comboPooledDataSource"></property>
</bean>
[4]开启基于注解的声明式事务功能
<!-- 开启基于注解的声明式事务功能,需要设置transaction-manager属性-->
<!-- 如果 事务管理器的id正好是transaction-manager的默认值transactionManager,则可以省略-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
[5]在事务方法上加@Transactional注解
事务属性的设置
①事务的传播行为②事务的隔离级别
③事务根据什么异常不进行回滚
④事务的超时属性
⑤事务的只读属性
1.事务的传播行为
[1]解释:一个事务运行在另一个有事务的方法中,那么当前方法是开启新事务还是在原有的事务中运行。
[2]设置事务方法在调用其他事务方法时,自己的事务如何传播给被调用的方法。[默认是required][3]设置方式
简介
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
说明
①REQUIRED传播行为
当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开 始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了。
②REQUIRES_NEW传播行为
表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。
案例演示:
在上面的dao中增加一个方法,用于演示事务!
//为了测试事务的传播行为,需要增加一个数据库操作
public void updatePrice(String isbn, int price){
String sql = "UPDATE book SET price = ? WHERE isbn = ?";
jdbcTemplate.update(sql, price,isbn);
}
同样,在service类中添加一个方法,如下所示:
@Transactional
public void updatePrice(String isbn, int price){
bookDao.updatePrice(isbn, price);
}
此时当前service类中就有两个事务方法了,然后我们现在新创建一个service类,加入到IOC容器中,并将bookService注入,
然后新建一个事务方法,如下所示:
@Component
public class MultiTX {
@Autowired
private BookService bookService;
@Transactional
public void multiTx(){
bookService.doCash("ISBN-003","Tom");
bookService.updatePrice("ISBN-005",888);
}
}
为了演示该service方法中调用的另外两个方法是不是使用了当前service方法的事务,这里我们将该service方法调用的第二个方法
弄出一个异常,然后看看该service方法调用的第一个service方法是不是回滚了!
先统一设置一下表中的数据:
UPDATE `account` SET balance = 10000;
UPDATE book_stock SET stock = 1000;
UPDATE book SET price = 1000;
用测试类进行测试:先正常测试一遍,然后再整出个异常测试一下:
public class TestDataSource {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
private BookDao bean = ioc.getBean(BookDao.class);
private BookService bookService = ioc.getBean(BookService.class);
private MultiTX multiTx = ioc.getBean(MultiTX.class);
@Test
public void test04() throws SQLException {
multiTx.multiTx();
}
}
会发现@Transactional注解默认使用的传播属性就是required!
此时可以将BookService类中的两个方法都加上propagation=Propagation.REQUIRES_NEW 属性,也就是如下所示:
@Transactional(propagation=Propagation.REQUIRES_NEW)
然后在执行刚才的有异常的测试方法,可以发现被包含的两个事务无法回滚,因为把外层事务挂起了!
2.事务的隔离级别 :
用于解决并发问题[在两次获取价格的方法中间打断点]
isolation=Isolation. READ_COMMITTED先恢复数据表中的数据:
UPDATE `account` SET balance = 10000;
UPDATE book_stock SET stock = 1000;
UPDATE book SET price = 1000;
修改service类中的doCash方法为:【即两次获取价格!】
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void doCash(String isbn,String username){
int price = bookDao.findPriceByIsbn(isbn);
System.out.println("price1:"+price);
bookDao.updateStockByIsbn(isbn);
bookDao.updateBalance(username, price);
price = bookDao.findPriceByIsbn(isbn);
System.out.println("price2:"+price);
}
在测试方法中调用BookService类中的doCash方法,然后在doCash方法中两次获取价格的方法中间打断点,就会发现
第一次是1000元,然后我们修改了数据库之后,读出来的还是1000元,这是因为数据库默认的隔离级别就是可重复读!
如果我们在doCash方法的事务注解上加一个isolation属性,如下所示:
@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED)
public void doCash(String isbn,String username){
int price = bookDao.findPriceByIsbn(isbn);
System.out.println("price1:"+price);
bookDao.updateStockByIsbn(isbn);
bookDao.updateBalance(username, price);
price = bookDao.findPriceByIsbn(isbn);
System.out.println("price2:"+price);
}
就会发现如果在两次获取价格之间打了断点,然后修改了值的话就会读出不一样的效果,说明事务的隔离级别设置就生效了!
3.事务根据什么异常不进行回滚
默认情况
捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。
设置途经
①注解
@Transactional注解
[1]rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个
[2]noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个
②XML
在Spring2.x事务通知中,可以在<tx:method>元素中指定回滚规则。如果有不止一种异常则用逗号分隔。
默认情况下,出现异常就回滚,noRollbackFor属性可以设置出现什么异常不进行回滚:
noRollbackFor=ArithmeticException.class
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
noRollbackFor=ArithmeticException.class)
public void doCash(String isbn,String username){
int price = bookDao.findPriceByIsbn(isbn);
System.out.println("price1:"+price);
bookDao.updateStockByIsbn(isbn);
bookDao.updateBalance(username, price);
System.out.println(10/0);//出了异常信息
}
4.事务的超时属性【timeout=3】
[1]数据库事务在执行过程中,会占用数据库资源,所以如果某一个事务执行的时间太长,那么就会导致资源被长时间占用,影响其他事务执行。[死循环,网络问题]
[2]可以通过设置超时属性,将超时没有执行完的事务回滚,相当于对该操作进行撤销。
5.事务的只读属性【readOnly=true】
数据库会对事务进行优化,如果是一个查询操作,那么数据库可以有针对性的进行优化。我们可以通过设置事务属性,告诉数据库当前操作是一个只读操作,便于数据库进行优化。
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
noRollbackFor=ArithmeticException.class,
timeout=3)
public void doCash(String isbn,String username){
int price = bookDao.findPriceByIsbn(isbn);
System.out.println("price1:"+price);
bookDao.updateStockByIsbn(isbn);
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
bookDao.updateBalance(username, price);
}
基于XML的声明式事务
<!-- 配置基于XML文件的声明式事务 -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut expression="execution(* com.neuedu.tx.service.BookService.*(String, String))" id="txPointCut"/>
<!-- 将事务切入点和事务建议的配置联系起来 -->
<aop:advisor advice-ref="bookTransaction" pointcut-ref="txPointCut"/>
</aop:config>
<!-- 设置事务属性 -->
<tx:advice id="bookTransaction" transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<tx:method name="doCash"
propagation="REQUIRED"
isolation="READ_COMMITTED"
read-only="false"
no-rollback-for="java.lang.ArithmeticException"
timeout="3" />
<!-- 将某一类方法统一设置为只读 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
</tx:attributes>
</tx:advice>