事务管理用来确保数据的完整性和一致性。事务就是一系列的工作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用。
事务的四个关键属性(ACID)
Spring中的事务管理
Spring在不同的事务管理API之上定义了一个抽象层,Spring既支持编程式事务管理,也支持声明式的事务管理。
Spring的核心事务管理抽象是org.springframework.transaction.PlatformTransactionManager
,这是一个接口,封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略,事务管理器都是必须的。
事务管理器的不同实现:
org.springframework.jdbc.datasource.DataSourceTransactionManager
: 在应用程序中只需要处理一个数据源,而且通过JDBC存取org.springframework.transaction.jta.JtaTransactionManager
在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理事务管理器最终以普通的Bean形式声明在Spring IOC容器中
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行
Spring支持的事务传播行为
传播属性 | 描述 |
---|---|
REQURED | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行 |
REQUIRED_NEW | 当前的定义方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起 |
并发事务(当同一个应用程序或不同应用程序中的多个事务在同一个数据集上并行执行时)可能导致的问题:
隔离级别 | 描述 |
---|---|
READ_UNCOMMITED | 允许事务读取未其他事务提交的变更,脏读,不可重复读和幻读的问题都会出现 |
READ_COMMITED | 一个事务只能看见已经提交事务所做的改变,可以避免脏读,但不可重复读和幻读的问题仍旧会出现,这是默认的隔离级别 |
REPEATABLE——READ | 确保事务可以多次从一个字段读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但幻读的问题仍然存在 |
SERIALZABLE | 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务执行插入,更新和删除操作,所有并发都可以避免,但性能十分低下 |
注意:事务的隔离级别受到数据库的限制,不同的数据库支持的的隔离级别不一定相同
代码
// BookShopDao.java
public interface BookShopDao {
// 根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
// 更新书的库存,使书号对应的库存 -1
public void updateBookStock(String isbn);
// 更新用户的账户余额:使username的balacne - price
public void updateUserAccount(String username,int price);
}
// BookShopDaoImpl.java
@Repository("bookShopDao") // 可以不命名,默认使用类名首字母小写
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql = "SELECT price FROM book WHERE id = ?";
return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
}
@Override
public void updateBookStock(String isbn) {
// 检查书的库存是否足够,若不够,则抛出异常
String sql2 = "SELECT stock FROM book_stock WHERE book_id = ?";
int stock = jdbcTemplate.queryForObject(sql2,Integer.class,isbn);
if(stock == 0){
throw new BookStockException("库存不足!");
}
String sql = "UPDATE book_stock SET stock = stock - 1 WHERE book_id = ?";
jdbcTemplate.update(sql,isbn);
}
@Override
public void updateUserAccount(String username, int price) {
String sql2 = "SELECT balance FROM accout WHERE user = ?";
int balance = jdbcTemplate.queryForObject(sql2,Integer.class,username);
if(balance < price){
throw new UserAccountException("余额不足!");
}
String sql = "UPDATE accout SET balance = balance - ? WHERE user = ?";
jdbcTemplate.update(sql,price,username);
}
}
// BookShopService.java
public interface BookShopService {
public void purchase(String username,String isbn);
}
// BookShopServiceImpl.java
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
// -->添加事务注解
// -->使用propagation 指定事务的传播行为,即当前的事务方法被另一个事务方法调用时
// 如何使用事务,默认取值为 REQUIRED,即使用调用方法的事务
// REQUIRED_NEW:事务自己的事务,调用的事务方法的事务被挂起
// -->使用isolation 指定事务的隔离级别,最常用的取值为READ_COMMITTED
// 默认情况下Spring的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置,通常情况下默认值即可
// -->使用timeout指定强制回滚之前事务可以占用的时间
// -->使用readOnly指定事务是否为只读。表示这个事务只读取事务但不更新数据
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED,
noRollbackFor = {UserAccountException.class},
timeout = 3,readOnly = false)
@Override
public void purchase(String username, String isbn) {
// 1. 获取书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
// 2. 更新书的库存
bookShopDao.updateBookStock(isbn);
// 3. 更新用户余额
bookShopDao.updateUserAccount(username,price);
}
}
// BookStockException.java
public class BookStockException extends RuntimeException {
private static final long sericlVersionUID = 1L;
public BookStockException(){
super();
}
public BookStockException(String message,Throwable cause,
boolean enableSuppression,boolean writeableStackTrace){
super(message,cause,enableSuppression,writeableStackTrace);
}
public BookStockException(String message,Throwable cause){
super(message,cause);
}
public BookStockException(String message){
super(message);
}
public BookStockException(Throwable cause){
super(cause);
}
}
// SpringTransactionTest.java
public class SpringTransactionTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
private BookShopService bookShopService = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
bookShopDao = ctx.getBean(BookShopDao.class);
bookShopService = ctx.getBean(BookShopService.class);
}
@Test
public void testBookShopDaoFindPriceByIsbn(){
System.out.println(bookShopDao.findBookPriceByIsbn("2"));
}
@Test
public void testBookShopDaoUpdateBookStock(){
bookShopDao.updateBookStock("1");
}
@Test
public void testUpdateUserAccount(){
bookShopDao.updateUserAccount("ada",1);
}
@Test
public void testBookShopService(){
bookShopService.purchase("ada","1");
}
}
// UserAccountException.java
public class UserAccountException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UserAccountException(){
super();
}
public UserAccountException(String message,Throwable cause,
boolean enableSuppression,boolean writeableStackTrace){
super(message,cause,enableSuppression,writeableStackTrace);
}
public UserAccountException(String message,Throwable cause){
super(message,cause);
}
public UserAccountException(String message){
super(message);
}
public UserAccountException(Throwable cause){
super(cause);
}
}
// 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"
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/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.sangyu.test12"/>
<!--导入资源文件-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置c3p0数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="User" value="${jdbc.user}"/>
<property name="Password" value="${jdbc.password}"/>
<property name="DriverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
<property name="InitialPoolSize" value="${jdbc.initPoolSize}"/>
<property name="MaxPoolSize" value="${jdbc.maxPoolSize}"/>
</bean>
<!-- 配置 Spirng 的 JdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置NamedParameterJdbcTemplate,该对象可以使用具名参数,其没有无参数的构造器,所以必须为其构造器指定参数-->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!--配置bean-->
<bean class="com.sangyu.test12.BookShopDao" abstract="true"></bean>
<bean class="com.sangyu.test12.BookShopService" abstract="true"></bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>