Spring框架学习(14):Spring的事务管理

事务的意思是原子事务,即要么完整的完成这个事务,要么在报错时使系统的状态回到执行事务之前。这么做的意义是在进行事务处理时会涉及到持久层的访问和读写,举个简单的例子,一个简单的转账操作,A转100块给B,那么对应的数据库操作是A的账户余额减100,B的账户余额加100,这两个步骤构成了简单的转账的事务。这两个步骤对数据库的读写肯定会存在时间的先后,那么万一A的账户余额减少了100后在进行B的账户余额加100时,出现了错误怎么办呢,例如网络通信断开或者数据库访问错误等,这时候这个事务就出现了错误,我们应该对整个事务进行回退将系统恢复到事务执行之前,即让A和B的账户余额都回到转账之前的状态,然后再告知用户转账操作失败。在Spring中,它为我们提供了配置事务的方法,可以实现事务的原子性。

这里用的例子是从书店购书的例子,购书这个事务分两个步骤,一是书本库存减一,二是用户余额扣去书费。当书本库存不足时,需要对这个事务进行回退,当用户余额不足时也要进行回退。类的关系如下:


数据库的内容如下:


工程的目录如下:


一、使用注解的方式进行事务管理

类的代码如下:

package tx;

public interface BookShopDao {
	
	//根据书号获取书的单价
	public int findBookPriceByIsbn(String isbn);
	
	//更新书的库存,使对应库存减一
	public void updateBookStock(String isbn);
	
	//更新用户的账户余额,使username 的balance减去price
	public void updataUserAccount(String username, int price);
}
package tx;

import javax.security.auth.login.AccountException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {

	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	@Override
	public int findBookPriceByIsbn(String isbn) {
		// TODO Auto-generated method stub
		String sql = "SELECT price FROM book WHERE isbn = ?";
		return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
	}

	@Override
	public void updateBookStock(String isbn) {
		// TODO Auto-generated method stub
		//检测书的库存是否足够,若不够则抛出异常
		String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?";
		int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
		if (stock == 0) {
			throw new BookStockException("库存不足");
		}
		
		String sql = "UPDATE book_stock SET stock = stock - 1 WHERE isbn = ?";
		jdbcTemplate.update(sql, isbn);
	}

	@Override
	public void updataUserAccount(String username, int price) {
		// TODO Auto-generated method stub
		//检测余额是否足够,若不够则抛出异常
		String sql2 = "SELECT balance FROM account WHERE username = ?";
		int balance = jdbcTemplate.queryForObject(sql2, Integer.class, username);
		if (balance < price) {
			throw new UserAccountException("余额不足");
		}
		String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
		jdbcTemplate.update(sql, price, username);
	}

}

package tx;

public interface BookShopService {
	
	public void purchase(String username, String isbn);
	
}

package tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;


@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {

	@Autowired
	private BookShopDao bookShopDao;
	
	//添加事物注解
	//1.使用propagation指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时
	//如何使用事务,默认取值为REQUIRED,即使用调用方法的事务
	//REQUIRED_NEW创建新的事务执行
	//2.使用isolation指定事务的隔离级别,最常用的取值为READ_COMMITTED
	//3.默认情况下Spring的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置
	//noRollBackFor不对什么异常回滚 :noRollbackFor={UserAccountException.class}
	//4.使用readOnly指定事务是否为只读,优化数据库引擎可以不加锁,加快速度
	//readOnly=true
	//5.timeout单位是秒,在强制回滚之前事务可以占用的时间
	@Transactional(propagation=Propagation.REQUIRES_NEW,
			isolation=Isolation.READ_COMMITTED,
			timeout=3)
	@Override
	public void purchase(String username, String isbn) {
		// TODO Auto-generated method stub
		/*模拟事务超时
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		*/
		//1.获取书的单价
		int price = bookShopDao.findBookPriceByIsbn(isbn);
		//2.更新书的库存
		bookShopDao.updateBookStock(isbn);
		//3.更新用户余额
		bookShopDao.updataUserAccount(username, price);
	}

}
然后是两个异常类分别是库存不足和余额不足
package tx;

public class BookStockException extends RuntimeException {

	
	private static final long serialVersionUID = 1L;
	
	public BookStockException() {
		super();
		// TODO Auto-generated constructor stub
	}

	public BookStockException(String message, Throwable cause,
			boolean enableSuppression, boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
		// TODO Auto-generated constructor stub
	}

	public BookStockException(String message, Throwable cause) {
		super(message, cause);
		// TODO Auto-generated constructor stub
	}

	public BookStockException(String message) {
		super(message);
		// TODO Auto-generated constructor stub
	}

	public BookStockException(Throwable cause) {
		super(cause);
		// TODO Auto-generated constructor stub
	}
	
	
	
}

package tx;

public class UserAccountException extends RuntimeException {

	private static final long serialVersionUID = 1L;
	
	public UserAccountException() {
		super();
		// TODO Auto-generated constructor stub
	}

	public UserAccountException(String message, Throwable cause,
			boolean enableSuppression, boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
		// TODO Auto-generated constructor stub
	}

	public UserAccountException(String message, Throwable cause) {
		super(message, cause);
		// TODO Auto-generated constructor stub
	}

	public UserAccountException(String message) {
		super(message);
		// TODO Auto-generated constructor stub
	}

	public UserAccountException(Throwable cause) {
		super(cause);
		// TODO Auto-generated constructor stub
	}

}
bean的配置文件如下:

<?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"
	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-3.2.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

	<context:component-scan base-package="tx"></context:component-scan>
	
	<!-- 导入资源文件 -->
	<context:property-placeholder location="classpath:db.properties"/>

	<!-- 配置c3p0数据源 -->
	<bean id="dataSource"
		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>
		
		<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
	</bean>
	
	<!-- 配置Spring的JdbcTemplate -->
	<bean id="jdbcTemplate"
		class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<!-- 配置 NameParameterJdbcTemplate,该对象可以使用具名参数,其没有无参数的构造器,所以必须为其构造器指定参数-->
	<bean id="namedParameterJdbcTemplate"
		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>
	
	<!-- 启用事务注解 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

property文件:

jdbc.user=root
jdbc.password=1234
jdbc.driverclass=com.mysql.cj.jdbc.Driver
jdbc.jdbcurl=jdbc:mysql:///spring-5?useSSL=true&serverTimezone=UTC

jdbc.initPoolSize=5
jdbc.maxPoolSize=10

bean的配置和property属性文件和上一篇文章的差不多,这里不多做介绍

使用junit测试一下功能:

package tx;

import static org.junit.Assert.*;

import java.util.Arrays;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTransactionTest {

	private ApplicationContext ctx = null;
	private BookShopDao bookShopDao = null;
	private BookShopService bookShopService  = null;
	private Cashier cashier = null;
	
	{
		ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
		bookShopDao = ctx.getBean(BookShopDao.class);
		bookShopService = ctx.getBean(BookShopService.class);
	}
	
	
	
	@Test
	public void testBookShoService() {
		bookShopService.purchase("AA", "1001");
	}
	
	@Test
	public void testBookShopDaoUpdate() {
		bookShopDao.updataUserAccount("AA", 100);
	}
	
	@Test
	public void testBookShopDaoUpdateBookStock() {
		bookShopDao.updateBookStock("1001");
	}
	
	@Test
	public void testBookShopDapFindPriceByIsbn() {
		System.out.println(bookShopDao.findBookPriceByIsbn("1001"));
	}

}
运行一下可以发现运行结果正常,再次查看数据库也会发现一切正常。读者可以自行修改数据库,产生购买失败的情况,可以看到失败的事件会被回滚。

现在我们来考虑一个新的问题,当我们在一个事务中执行了一连串的事件时,当其中的一个事件出错了,需不需要将前面执行成功的事务也回滚呢?

写一个cashier类来测试一下:

package tx;

import java.util.List;

public interface Cashier {
	public void checkout(String username, List<String> isbns);
}

package tx;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service("cashier")
public class CashierImpl implements Cashier {

	
	@Autowired
	private BookShopService bookShopService;
	
	//添加事务注解
	@Transactional
	@Override
	public void checkout(String username, List<String> isbns) {
		// TODO Auto-generated method stub
		for (String isbn : isbns) {
			bookShopService.purchase(username, isbn);
		}
	}

}
我们运行这段代码的话可以看到结果是只回滚了发生错误的事务,前面正常执行的事务没有被回滚,这是因为我们在前面的代码中设置了事务的传播属性
propagation=Propagation.REQUIRES_NEW,

即在处理该事件时新开一个事件来处理,因此这个事件与调用它的事件不是处于同一个事件当中的,因此不会将调用其的事件也回滚。

除此之外Spring事务的事务传播行为还有6种:

REQUIRED:业务方法需要在一个容器里运行。如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。

NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。

MANDATORY:该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果在没有事务的环境下被调用,容器抛出例外。

SUPPORTS:该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。

NEVER:该方法绝对不能在事务范围内执行。如果在就抛例外。只有该方法没有关联到任何事务,才正常执行。

NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。 


二、使用xml文件的方式进行事务管理

同样的,除了使用注解我们还可以通过xml文件配置的方式进行事务管理

还是使用上面的代码,但是需要将它们的注解全部去掉,然后在bean的配置文件中进行事务管理,配置文件如下:



<?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-3.2.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">

	<context:component-scan base-package="tx"></context:component-scan>
	
	<!-- 导入资源文件 -->
	<context:property-placeholder location="classpath:db.properties"/>

	<!-- 配置c3p0数据源 -->
	<bean id="dataSource"
		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>
		
		<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
	</bean>
	
	<!-- 配置Spring的JdbcTemplate -->
	<bean id="jdbcTemplate"
		class="org.springframework.jdbc.core.JdbcTemplate">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<!-- 配置bean -->
	<bean id="bookShopDao" class="tx.xml.BookShopDaoImpl">
		<property name="jdbcTemplate" ref="jdbcTemplate"></property>
	</bean>
	
	<bean id="bookShopService" class="tx.xml.service.impl.BookShopServiceImpl">
		<property name="bookShopDao" ref="bookShopDao"></property>
	</bean>
	
	<bean id="cashier" class="tx.xml.service.impl.CashierImpl">
		<property name="bookShopService" ref="bookShopService"></property>
	</bean>
	
	<!-- 1.配置事务管理器 -->
	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<!-- 2.配置事务属性 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<!-- 根据方法名指定事务的属性 -->
			<tx:method name="purchase" propagation="REQUIRES_NEW"/>
			<tx:method name="get*" read-only="true"/>
			<tx:method name="find*" read-only="true"/>
			<tx:method name="*"/>
		</tx:attributes>
	</tx:advice>
	
	<!-- 这一步一定要记得添加编织器的库AspectJWeaver.jar,不然会报错 -->
	<!-- 配置事务切入点,以及把事务切入点和事务属性关联起来 -->
	<aop:config>
		<aop:pointcut expression="execution(* tx.xml.service.*.*(..))" 
			id="txPointCut"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
	</aop:config>
</beans>

一般来说,只要依赖的jar包加对了,版本也正确,是不会出什么问题的。以上就是Spring的事务管理。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值