NO.14 Spring 事务
事务分为两种:声明式事务和编程式事务。
编程式事务:需要编写代码控制事务在哪里开始,哪里提交,哪里回滚。
声明式事务:由Spring自动控制,事务在业务逻辑方法执行前开始,在业务逻辑方法正常结束后提交,在业务逻辑方法抛出异常时回滚。
之前学习MySQL/JDBC时,学习了编程式事务,需要手动开启事务,进行手动提交。Spring的事务则采用AOP的思想。
事务的传播行为:
1)PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。应用率达99%。
2)PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
3)PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。简单说,有事务就执行,没有事务就不执行。
4)PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起/不使用事务。不使用事务,那么执行的速度较快。
5)PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
6)PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
7)PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
假如说保存订单操作,需要保存商品信息(订单项信息),订单信息,删除购物车信息等业务操作,每个业务都需要一个事务,假如说保存订单是一个大事务,当保存商品信息时,采用PROPAGATION_REQUIRES_NEW方式,已知已经有一个订单,则把订单挂起,重新创建一个事务。订单信息保存时,商品信息的事务就结束了,此时,仍有订单,需挂起,重新new一个事务。业务操作的事务都完成后,订单的事务才算完成。可见,PROPAGATION_REQUIRES_NEW方式不会引起垃圾数据。
使用事务时会出现几个特性,当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题。
并发事务所导致的问题可以分为下面三种类型:
<1>脏读:对于两个事务T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段.。之后, 若 T2 回滚, T1读取的内容就是临时且无效的。
比方说有一条数据,A操作读取这条数据,B操作修改这条数据。A读取后,B修改,B操作修改完成后,A读取的就是不正确的,这样的数据对A来说,就是脏数据。也就是说,A读取数据读取到内存中,B进行修改,将sql写到内存里,但数据还没有持久化或者还没修改硬盘上的数据,预处理/编程式事务数据提交的方式类似这种。
使用事务时,数据先写到内存里,最后提交,才把数据写到磁盘上。数据的update操作并非直接写到磁盘上,而是先写到内存中,只有commit操作后,才会将数据写到硬盘上。
如果不使用事务,数据直接提交到硬盘上,是自动提交的,也就是auto commit。手动写事务,是将auto commit设置成false,关掉了。
<2>不可重复读:对于两个事务T1, T2, T1读取了一个字段, 然后T2更新了该字段。 之后, T1再次读取同一个字段, 值就不同了。
<3>幻读:对于两个事务T1, T2, T1从一个表中读取了一个字段, 然后T2在该表中插入了一些新的行.。之后, 如果T1再次读取同一个表, 就会多出几行。
下面给出一个例子:
比如说,买兰州拉面。
StuCardService类:
package com.xt.spring.tx.xml;
import java.math.BigDecimal;
public class StuCardService {
private StuCardDao scDao;
/**
* 支付步骤:
* 1.获取学生卡的信息
* 2.获取商户信息
* 3.计算商户账户money并更新
* 4.计算学生卡账户money并更新
* @param sourceCardNo
* @param targetCardNo
* @param money
*/
public void buySomeFood(String sourceCardNo, String targetCardNo, String money){
StuCard sourceCard = scDao.getStuCardInfoByCardNo(sourceCardNo);
StuCard targetCard = scDao.getStuCardInfoByCardNo(targetCardNo);
targetCard.setMoney(targetCard.getMoney().add(new BigDecimal(money)));
scDao.updateCardMoney(targetCard);
sourceCard.setMoney(sourceCard.getMoney().subtract(new BigDecimal(money)));
if(sourceCard.getMoney().compareTo(new BigDecimal("0"))<0){
throw new NotEnoughMoneyException("余额不足,请充值!");
}
scDao.updateCardMoney(sourceCard);
}
public StuCardDao getScDao() {
return scDao;
}
public void setScDao(StuCardDao scDao) {
this.scDao = scDao;
}
}
StuCardDaoImpl类:
package com.xt.spring.tx.xml;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
public class StuCardDaoImpl implements StuCardDao{
private JdbcTemplate jdbcTemplate;
/**
* 更新卡片money信息
*/
public void updateCardMoney(StuCard sc) {
String sql = "update stu_info set money = ? where card_no = ?";
jdbcTemplate.update(sql, sc.getMoney(),sc.getCardNo());
}
/**
* 根据卡号查询卡的信息
*/
public StuCard getStuCardInfoByCardNo(String cardNo) {
String sql = "select * from stu_info where card_no = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{cardNo}, new BeanPropertyRowMapper<StuCard>(StuCard.class));
}
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
StuCard类:
package com.xt.spring.tx.xml;
import java.math.BigDecimal;
public class StuCard {
private String cardNo;
private BigDecimal money;
private String name;
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
public BigDecimal getMoney() {
return money;
}
public void setMoney(BigDecimal money) {
this.money = money;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
同样需要配置文件:
applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd ">
<!--
Spring jdbc配置步骤:
1、配置数据源 (连接池)
2、配置spring jdbc模板 jdbcTemplate
-->
<context:property-placeholder location="classpath:DB.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${mysql_driver}"></property>
<property name="url" value="${mysql_url}"></property>
<property name="username" value="${mysql_user}"></property>
<property name="password" value="${mysql_passwd}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="npJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
applicationContext-xml.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd ">
<!--
在spring中,配置文件整合可以使用标签<import引入配置文件
-->
<import resource="applicationContext.xml"/>
<bean id="stuCardDao" class="com.xt.spring.tx.xml.StuCardDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="stuCardService" class="com.xt.spring.tx.xml.StuCardService">
<property name="scDao" ref="stuCardDao"></property>
</bean>
</beans>
测试类:
package com.xt.spring.tx.xml;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TxTest {
private ApplicationContext ioc;
@Before
public void iocInit(){
ioc = new ClassPathXmlApplicationContext("spring/applicationContext-xml.xml");
}
@Test
public void testBean(){
StuCardService scs = ioc.getBean("stuCardService",StuCardService.class);
scs.buySomeFood("20180302", "20180301", "200");
}
}
异常:
package com.xt.spring.tx.xml;
public class NotEnoughMoneyException extends RuntimeException{
/**
*
*/
private static final long serialVersionUID = 1252964253402478818L;
public NotEnoughMoneyException() {
super();
}
public NotEnoughMoneyException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public NotEnoughMoneyException(String message, Throwable cause) {
super(message, cause);
}
public NotEnoughMoneyException(String message) {
super(message);
}
public NotEnoughMoneyException(Throwable cause) {
super(cause);
}
}
倘若原始数据:
小红消费200,运行后可知:
显然不符实际。
这时我们需要用到事务。
<?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
http://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
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<!--
在spring中,配置文件整合可以使用标签<import引入配置文件
-->
<import resource="applicationContext.xml"/>
<bean id="stuCardDao" class="com.xt.spring.tx.xml.StuCardDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="stuCardService" class="com.xt.spring.tx.xml.StuCardService">
<property name="scDao" ref="stuCardDao"></property>
</bean>
<!-- 定义一个通知类
txAdvice需要配置事务管理器
事务的传播行为需要依赖事务管理器
<tx:attributes>事务的属性
配置事务的属性,需要tx命名空间
事务传播行为:
如果方法名称与当前配置的<tx:method name=""/>方法名称相同,传播行为就采用propagation="REQUIRED"
一方面定义了方法的命名规范,另一方面,这类方法所使用的事务的传播行为都要配置好
-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="buySomeFood" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 配置事务切入点,需要拦截哪些类,使用AOP,同样需要AOP的命名空间-->
<aop:config>
<aop:pointcut expression="execution(* com.xt.spring.tx.xml.StuCardService.*(..))" id="stuCardServicePointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="stuCardServicePointCut"/>
</aop:config>
</beans>
可得结果:
想要开发事务,只需在配置文件中做个配置即可。
倘若想要拦截所有service/dao,可将service/dao全部打包放在一个包中。
则切点配置为<aop:pointcut expression="execution(* com.xt.spring.tx.*.service.*.*(..))" id=""/>
,第一个” * “代表功能,第二个” * “代表service层的Java文件,第三个” * “代表所有方法。
以上就是将事务与业务逻辑分离,面向切面的理论。