事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性事务就是一系列的动作,它们被当做一个单独的工作单元。这些动作要么全部完成,要么全部不起作用。
事务的四个关键属性(ACID):
原子性(atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成要么完全不起作用。
一致性(consistency):一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中。
隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏。
持久性(durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中。
传统的JDBC处理事务的方式通常通过如下代码实现:
很容易理解,事务的功能模块是可以被抽象成一个切面的。事实上,Spring可以通过Spring AOP框架支持声明式事务。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。
Spring从不同的事务管理API中抽象了一整套的事务机制。开发人员不必了解底层的事务API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了。
Spring的核心事务管理抽象是TransactionManager,它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
现在以一个例子来介绍Spring中声明式事务的使用:设想一个买书行为,如果购买成功,则对应书本库存减1,对应用户余额减少(减少量为书本的价格),库存为0时无法购买,余额不足时也无法购买。那么买书行为便是一种事务,即要么购买成功,库存减少,余额也减少;即要么购买失败,库存不变,用户余额也不变。不能出现库存减少而用户余额不变或者用户余额减少而库存不变的情况。
Spring中有两种方式来支持声明式事务,一种是通过@Transactional注解声明,一种是通过xml文件声明,下面逐一介绍。
现在模拟不添加事务进行管理的情况,首先创建数据表:
book表
创建相应的接口和类:
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByBookNo(int bookNo);
//更新数的库存. 使书号对应的库存 - 1
public void updateBookStock(int bookNo);
//更新用户的账户余额: 使 username 的 balance - price
public void updateUserAccount(String username, int price);
}
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByBookNo(int bookNo) {
String sql = "SELECT price FROM book where id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookNo);
}
@Override
public void updateBookStock(int bookNo) {
String sql1 = "SELECT stock FROM book where id = ?";
int stock = jdbcTemplate.queryForObject(sql1, Integer.class, bookNo);
if (stock <= 0)
throw new BookStockException("库存不足");
String sql2 = "UPDATE book SET stock = stock - 1 WHERE id = ?";
jdbcTemplate.update(sql2, bookNo);
}
@Override
public void updateUserAccount(String username, int price) {
String sql1 = "SELECT balance FROM account where name = ?";
int balance = jdbcTemplate.queryForObject(sql1, Integer.class, username);
if (balance < price)
throw new UserAccountException("余额不足");
String sql2 = "UPDATE account SET balance = balance - ? WHERE name = ?";
jdbcTemplate.update(sql2, price, username);
}
}
public interface BookShopService {
//购买图书
public void purchase(String username, int bookNo);
}
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
BookShopDao bookShopDao;
@Override
public void purchase(String username, int bookNo) {
int price = bookShopDao.findBookPriceByBookNo(bookNo);
bookShopDao.updateBookStock(bookNo);
bookShopDao.updateUserAccount(username, price);
}
}
//定义库存不足的异常类
public class BookStockException extends RuntimeException {
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
}
}
//定义余额不足的异常类
public class UserAccountException extends RuntimeException {
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
}
}
编写Spring的配置文件applicationContext-tx-annotation.xml:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
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-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<context:component-scan base-package="com.MySpring">context:component-scan>
<context:property-placeholder location="classpath:db.properties" />
<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="driverClass" value="${jdbc.driverClass}">property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}">property>
<property name="initialPoolSize" value="${jdbc.initialPoolSize}">property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource">property>
bean>
beans>
编写测试类:
public class Test {
@org.junit.Test
public void test() throws SQLException {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-tx-annotation.xml");
BookShopService bookShopService = (BookShopService) ctx.getBean("bookShopService");
//使用户Jack购买一本编号为1000的书
bookShopService.purchase("Jack", 1000);
}
}
先运行一遍测试程序,由于余额足够购买,所以可以看到书本库存减少了1,而且用户余额减少了50:
然后再运行一次程序,这时候,由于用户余额不够再购买一本编号为1000的书,所以会抛出余额不足的异常:
但是,查看数据库表,却发现虽然用户余额没有改变,但是书本的库存却又减少了1:
这是因为,在service层的purchase方法中,先执行更新库存的操作,这时,库存是足够的,所以库存更新成功,但是执行到更新用户余额的操作时,由于余额不足,所以抛出了异常,余额没有更新。这显然是不符合要求的,正常的情况应该是,当余额更新不成功时,需要进行回滚,使得库存的更新操作也不执行,这正是事务的功能。下面将介绍在Spring中如何实现声明式事务。
Spring通过注解的方式实现声明式事务的步骤为:
首先在spring配置文件applicationContext-tx-annotation.xml中进行如下配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
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-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<context:component-scan base-package="com.MySpring">context:component-scan>
<context:property-placeholder location="classpath:db.properties" />
<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="driverClass" value="${jdbc.driverClass}">property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}">property>
<property name="initialPoolSize" value="${jdbc.initialPoolSize}">property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource">property>
bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:annotation-driven transaction-manager="transactionManager" />
beans>
然后在需要使用事务的方法上添加@Transactional注解:(如果将@Transactional添加在类名上,则声明这个类的所有方法都需要使用事务进行管理)
@Transactional
@Override
public void purchase(String username, int bookNo) {
int price = bookShopDao.findBookPriceByBookNo(bookNo);
bookShopDao.updateBookStock(bookNo);
bookShopDao.updateUserAccount(username, price);
}
现在,将数据库中的记录置为原始状态,再进行实验,运行第一次,发下库存和余额都扣除成功:
运行第二次,会抛出余额不足的异常,但是库存和余额都没有更新:
这就达到了我们期望的效果。此外,@Transactional注解有如下属性:
propagation:指定事务的传播行为,有如下属性值可选(前两个较为常用):
isolation:指定事务的隔离级别。
rollbackFor:指定对哪些异常进行回滚。
noRollbackFor:指定对哪些异常不进行回滚。
readOnly:指定是否为只读,表示这个事务是否只读取数据而不会更新数据,可以帮助数据库引擎优化事务。
timeout:指定事务在强制回滚之前可以保持多久,这样可以防止长期运行的事务占用资源。
通过xml文件来实现Spring的声明式事务,只需要在Spring的配置文件applicationContext-tx-xml.xml中进行如下配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
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-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<context:component-scan base-package="com.MySpring">context:component-scan>
<context:property-placeholder location="classpath:db.properties" />
<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="driverClass" value="${jdbc.driverClass}">property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}">property>
<property name="initialPoolSize" value="${jdbc.initialPoolSize}">property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource">property>
bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource">property>
bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="purchase" />
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut expression="execution(* com.MySpring.service.impl.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
aop:config>
beans>
通过测试可以发现,与上面的方法所达到的效果一样。
注意,在配置文件中,tx:method节点用于指定事务的属性,而且可以使用通配符。例如,可以通过如下配置指定所有以get开头的方法的事务为只读。
<tx:method name="get*" read-only="true"/>