这篇文章一起来回顾复习下spring的事务操作.事务是spring的重点, 也是面试的必问知识点之一.
说来这次面试期间,也问到了我,由于平时用到的比较少,也没有关注过这一块的东西,所以回答的不是特别好,所以借这一篇文章来回顾总结一下,有需要的朋友,也可以点赞收藏一下,复习一下这方面的知识,为年后的面试做准备.
首先,了解一下什么是事务?
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。这里简单提一下事务的四个基本属性,
A(Atomic) 原子性
事务必须是原子工作单元;对于其[数据修改]事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行.
C(Consistent) 一致性
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。
I(Insulation) 隔离性
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
D(Duration) 一致性
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
了解了事务之后,我们为什么要使用事务呢?换句话说,用事务是为了解决什么问题呢?
首先我们来看一个业务场景:Tom在书店买书,java和Oracle,2种书,单价都是100,库存量都是10本,Tom目前身上有150元.现在Tom买1本书的钱是足够的,ok,买起来,交易结束后,对于Tom来说,买到了1本书,还剩下50元.正好要出门时接到jack的电话,原来是jack要Tom帮他捎本java,他要用来复习,那接下来的交易是否可以正常进行呢?常识来说,50元买价值100元的东西肯定是买不到的,那我们看看程序中是什么情况?
首先,需要构建三张表,余额表,商品表,和商品库存表,如下:
然后定义接口如下:
public interface BookShopDao {
/**
* 根据书名获取书的单价
*/
public int findBookPriceByIsbn(String isbn);
/**
* 更新书的库存,使书号对应的库存-1
*/
public void updateBookStock(String isbn);
/**
* 更新用户的余额:使username的balance-price
* @param name
* @param price
*/
public void updateUserAccount(String name,int price);
}
@Repository
public class BookShopDaoImpl 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 name, int price) {
//验证余额是否足够,不足则抛出异常
String sql2 ="SELECT balance FROM account WHERE username = ?";
int balance = jdbcTemplate.queryForObject(sql2, Integer.class, name);
if(balance <price) {
throw new UserAccountException("余额不足");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql,price,name);
}
}
上面代码中有点要注意:库存余量是否充足,余额是否充足,需要在代码中去自己判断,mysql不会帮我们加,例如,当库存数为0时,如果仍需要减1,值会变为-1,这不是我们想要的结果.
接下里定义一个service:
public interface BookShopService {
/**
* 购物方法
* @param username
* @param isbn
*/
public void purchase(String username,String isbn);
}
@Service
public class BookShopServiceImpl implements BookShopService{
@Autowired
private BookShopDao shopDao;
/**
* @param username
* @param isbn
*/
@Override
public void purchase(String username, String isbn) {
//1.获取书的单价
int price = shopDao.findBookPriceByIsbn(isbn);
//更新书的库存
shopDao.updateBookStock(isbn);
//更新余额
shopDao.updateUserAccount(username,price);
}
}
到此,基本购买流程都已经实现,我们来写一个测试方法测试一下购买的结果是什么?
由图中可以看出,程序报了"余额不足"的异常,tom的余额没有减少,但是书店的库存量却减少了,这明显是违反常理的,书店不会白白把书送给tom的,怎么办呢?事务就可以帮助我们解决这个难题.
这里要先了解下事务的分类:
- 编程式事务
将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式管理事务当中,必须在每个事务操作中包含额外的事务管理代码,繁琐,不便.
- 声明式事务
是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需通过基于@Transactional注解的方式或者配置文件中做相关的事务规则声明,便可以将事务规则应用到业务逻辑中。
采用声明式事务,基于@Transactional注解,首先看下配置文件:
<?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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--扫描包-->
<context:component-scan base-package="com.springtest"></context:component-scan>
<!--导入资源文件-->
<context:property-placeholder location="classpath:db.properties" />
<!--配置数据源-->
<bean id="jdbcSource" 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.initialPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!--配置spring的jdbctemplate模版-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="jdbcSource"></property>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="jdbcSource"></property>
</bean>
<!--启用事务注解-->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
接下来给方法purchase()加上注解
@Transactional()
@Override
public void purchase(String username, String isbn) {
//1.获取书的单价
int price = shopDao.findBookPriceByIsbn(isbn);
//更新书的库存
shopDao.updateBookStock(isbn);
//更新余额
shopDao.updateUserAccount(username,price);
}
结果如下:
由图观之,异常出现之后,事务发生了回滚,库存不再减少,钱也不会再减少,结果正常.
拓展问题(面试):Q1: 假如此时BookShopServiceImpl中另外一个方法调用了purchase方法,那么在另外一个方法中,事务是否起作用呢?
Q2:假如此时另外一个类中方法调用了BookShopServiceImpl类中的purchase方法,那么事务又是否起作用呢?
我们来一一验证一下,首先Q1
@Service
public class BookShopServiceImpl implements BookShopService{
@Autowired
private BookShopDao shopDao;
@Transactional()
@Override
public void purchase(String username, String isbn) {
//1.获取书的单价
int price = shopDao.findBookPriceByIsbn(isbn);
//更新书的库存
shopDao.updateBookStock(isbn);
//更新余额
shopDao.updateUserAccount(username,price);
}
@Override
public void purchaseAgain(String username, String isbn) {
purchase(username,isbn);
}
}
测试结果:
测试前,数据库数据为:
结果观之,事务并没有起作用,原因是什么?
启用事务首先调用的是AOP代理对象而不是目标对象,首先执行事务切面,事务切面内部通过TransactionInterceptor环绕增强进行事务的增强,即进入目标方法之前开启事务,退出目标方法时提交/回滚事务.而类内部的自我调用将无法实施切面中的增强.,解决方案的话限于篇幅,以后再写,这里知道原因就可以了.
接下来验证Q2,首先创建一个新的接口和实现类,里面调用BookShopService 的purchase方法,观察结果
@Service
public class TestBookShopServiceImpl implements TestBookShopService {
@Autowired
private BookShopService shopService;
@Override
public void testBookPurchase(String name, String isbn) {
shopService.purchase(name,isbn);
}
}
观察结果,在余额不足的情况下,外部方法调用purchase方法,抛出异常时,事务回滚,库存没有减少,原因同Q1相同,但正好相反,但是走了AOP代理,所以事务起作用了.
那么如果在内部的方法purchaseAgain,和外部的方法中加入事务控制又会是怎样的情况呢?
这里直接给出结论:
purchaseAgain方法加入注解@Transactional后,调用purchase方法(无论是否添加@Transactional),事务控制起作用;外部类的testBookPurchase方法调用本类的purchase方法,事务控制也是起作用的.
由此引入spring关于事务的传播行为的介绍:spring的事务传播行为一共分为以下几种:
- REQUIRED(
常用
) - REQUIRES_NEW(
常用
) - SUPPORTS
- NOT_SUPPORTED
- NEVER
- NESTED
- MANDATORY
在@Transactional注解中是propagation属性;
分别介绍:
PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。(是spring 的默认事务传播行为)。
PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。
PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。
PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
事务的传播行为定义了事务的控制范围,那么事务的隔离级别定义的则是事务在数据库读写方面的控制范围.
有的时候,在程序并发的情况下,会发生以下的神奇情况:
- 脏读:对于两个事务T1,T2,T1读取了T2更新但是还未提交的字段,之后,若T2回滚,那么T1读取的内容就是临时且无效的
- 不可重复读:对于两个事务T1,T2, T1读取了一个字段,然后被T2更新了,之后T1再次读取,字段值变掉了.
- 幻读:两个事务T1,T2, T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行,之后,如果T1再次读取同一个表,就会多出几行数据.
那么以上的问题要如何来解决呢,spring给出了它的解决方案,将事务的隔离性分为以下几个等级 - READ_UNCOMMITTED
- READ_COMMITTED
- REPEATABLE_READ
- SERIALIZABLE
在@Transactional注解中是propagation属性;
分别介绍:
READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。 这种隔离级别会产生脏读,不可重复读和幻像读;
READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 这种隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读;
REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读;
SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 除了防止脏读,不可重复读外,还避免了幻像读;
以上几种隔离界别, 在了解了其作用及其可避免的情况之后,我们在工作中视情况采用,不过一般默认情况就可以处理大多数情况了.
最后小结
这篇文章回顾了spring的事务相关的技术要点,包括什么是事务,事务的四个基本属性,为什么要使用事务,事务的分类,事务的传播种类以及事务的隔离级别.大体上涵盖了事务的相关知识,但是并没有深入到源码级别来研究事务的相关实现,有机会一定要深入源码了解实现,这样才能对知识的学习理解达到庖丁解牛的地步,对自己以后的知识积累和提升也会有很大的帮助.