1. 事务简介
事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性.
事务就是一系列的动作,它们被当做一个单独的工作单元.
这些动作要么全部完成,要么全部不起作用.
事务的四个关键属性(ACID
)
1.原子性(atomicity
): 事务是一个原子操作,由一系列动作组成.
事务的原子性确保动作要么全部完成要么完全不起作用.
2.一致性(consistency
): 一旦所有事务动作完成,事务就被提交.
数据和资源就处于一种满足业务规则的一致性状态中.
3.隔离性(isolation
):可能有许多事务会同时处理相同的数据,
因此每个事务都应该与其他事务隔离开来,防止数据损坏.
4.持久性(durability
):一旦事务完成,无论发生什么系统错误,它的结果都不应该
受到影响.通常情况下,事务的结果被写到持久化存储器中.
2. JDBC事务处理
public void purchase(String isbn,String username){
Connection conn=null;
try{
//相当于前置通知
conn = dataSource.getConnection();
conn.setAutoCommit(false);
//...(业务方法)
//相当于返回通知
conn.commit();
}catch(SQLException e){
//相当于异常通知
e.printStackTrace;
if(conn!=null){
try{
conn.rollback();
}catch(SQLException e1){
e1.printStackTrace();
}
throw new RuntimeException(e);
}
}finally{
//相当于后置通知
if(conn !=null){
try{
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
}
3. Spring中的事务管理
作为企业级应用程序框架,Spring在不同的事务管理API之上定义了一个抽象层.
而应用程序开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式事务管理.
编程式事务管理: 将事务管理代码嵌入到业务方法中来控制事务的提交和回滚.
在编程式事务管理时,必须在每个事务操作中包含额外的事务管理代码。
声明式事务管理: 大多数情况下必编程式事务管理更好用.
它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理.
事务管理作为一种横切关注点,可以通过AOP方法模块化.
Spring通过SpringAOP框架支持声明式事务管理。
4. Spring中的事务管理器
Spring从不同的事务管理API中抽象了一整套的事务机制.
开发人员不必了解底层的事务API,就可以利用这些事务机制.
有了这些事务机制,事务管理代码就能独立于特定的事务技术了.
Spring的核心事务管理抽象是org.springframework.transaction
管理封装了一组独立于技术的方法.
Interface PlatformTransactionManager
无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器是必须的.
Spring中的事务管理器的不同实现
org.springframework.jdbc.datasource
Class DataSourceTransactionManager
在应用程序中只需要处理一个数据源,而且通过JDBC存取
org.springframework.transaction.jta
Class JtaTransactionManager
在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理
org.springframework.orm.hibernate
Class HibernateTransactionManager
用Hibernate框架存取数据库
事务管理器以普通的Bean形式声明在Spring IOC容器中.
事务管理器需求:
5. 借书事务代码实现
5.1 建立数据表
一张book表,描述书
一张stock表,描述书的库存
一张account表,描述用户的账户情况。
代码如下:
use spring;
create table book(
isbn varchar(50) primary key,
book_name varchar(100),
price int
);
create table book_stock(
isbn varchar(50) primary key,
stock int,
check(stock > 0)
);
create table account(
username varchar(50) primary key,
balance int,
check( balance > 0 )
);
5.2 配置文件和属性文件
- 导入资源文件
- 配置数据源
- 配置jdbcTemplate Bean
- 自动扫描包配置
配置事务管理器
具体配置代码可参阅《Spring-JDBC-3》。
5.3 Dao层代码实现
Dao接口代码
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
//更新书的库存,使书号对应的库存减一
public void updateBookStock(String isbn);
//更新用户的账户余额,使balance减去price
public void updateUserAccount(String username,int price);
}
Dao实现类代码(加注解,注入jdbcTemplate)
@Repository("bookShopDao")
public class BookShopDaoImple implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql="select price from book where isbn=?";
return jdbcTemplate.queryForObject(sql, Integer.class,isbn);
}
@Override
public void updateBookStock(String isbn) {
//检查书的库存是否足够,若不够,则抛出异常
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 updateUserAccount(String username, int price) {
//验证余额是否足够,若不够,则抛出异常
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);
}
}
在处理过程中,还会用到两个自定义的异常类。用来处理库存不足和余额不足的问题。
BookStockException
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
}
}
UserAccountException
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
}
}
5.4 Service层代码实现
service接口代码
public interface BookShopService {
//借书
public void purchase(String username,String isbn);
}
service实现类代码
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
//添加事务注解
//使用propagation指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时
//如何使用事务。默认取值为REQUIRED,即使用调用方法的事务。
//REQUIRES_NEW:使用自己的事务,调用方法的事务被挂起。
@Transactional(propagation=Propagation.REQUIRES_NEW)
@Override
public void purchase(String username, String isbn) {
//1. 获取书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2. 更新书的库存
bookShopDao.updateBookStock(isbn);
//3. 更新用户余额
bookShopDao.updateUserAccount(username, price);
}
}
6. 事务的传播属性
传播属性 | 描述 |
---|---|
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行。 |
REQUIRES_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起。 |
supports | 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中。 |
not_supporte | 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。 |
mandatory | 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常。 |
never | 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。 |
nested | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行。 |
required:
requires_new
7. 并发事务所导致的问题
当同一应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时
可能会出现许多意外的问题。
并发事务所导致的问题可以分为下面三种类型:
脏读
: 对于两个事务T1
,T2
,
T1
读取了已经被T2
更新但是还没有被提交的字段。
若T2
回滚,T1
读取的内容就是临时而且无效的。
不可重复读
: 对于两个事务T1
,T2
T1
读取了一个字段,然后T2更新了该字段.
之后,T1再次读取同一个字段,值就不同了.
幻读
: 对于两个事务T1
,T2
,
T1
从一个表中读取了一个字段,然后T2
在该表中插入了一些新的行.
之后,如果T1再次读取同一个表,就会多出几行.
7.1 事务的隔离级别
从理论上来说,事务应该彼此完全隔离,
以避免并发事务所导致的问题,
然而,那样会对性能产生极大的影响。
因为事务必须按顺序运行。
在实际开发中,为了提升性能,事务会以较低的隔离级别运行。
7.2 超时和只读属性
由于事务可以在行和表上获得锁,因此长事务会占用资源
并对整体性能产生影响。
如果一个事务只读数据但不做修改,数据库引擎可以对这个事务进行优化。
超时事务属性: 事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
只读事务属性: 表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。
事务的隔离级别可以通过隔离事务属性指定。
1. 使用isolation指定事务的隔离级别,最常用的值为READ_COMMITTED
默认情况Spring的声明式事务对所有运行时异常进行回滚,也可以通过对应的属性进行设置。通常情况下取默认值即可。
2. 指定readonly指定事务是否为只读。表示这个事务只读数据但不更新数据,这样可以帮助数据库引擎优化事务。若真的是一个只读取数据库值的方法,应设置readonly=true.
3. 使用timeout指定强制回滚之前事务可以占用的时间,单位是秒。
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
noRollbackFor={UserAccountException.class},
readonly=false,
timeout=1)
8. xml配置事务及其属性
<!-- 配置Bean -->
<bean id="bookShopDao"
class="com.atguigu.spring.tx.xml.BookShopDaoImple">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="bookShopService"
class="com.atguigu.spring.tx.xml.service.impl.BookShopServiceImpl">
<property name="bookShopDao" ref="bookShopDao"></property>
</bean>
<bean id="cashier"
class="com.atguigu.spring.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:attributes>
</tx:advice>
<!-- 3.配置事务切入点,以及把事务切入点和事务属性关联起来 -->
<aop:config>
<aop:pointcut expression="execution(* com.atguigu.spring.tx.xml.service.*.*(..))"
id="txPointCut"></aop:pointcut>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>