什么是事务
事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。
如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。
当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。
由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。
事务的四大特性
(1)原子性
原子性是指事务包含的所有操作要么成功,要么全部失败回滚。因此事务的操作如果成功就必须要完成应用到数据库中,如果操作失败则不能对数据库有任何影响。
⑵ 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
⑶ 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
⑷ 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。
为什么要使用事务?
使用事务是为了保证事务中的所有操作在数据库中要么全部正确操作,要不全部不反应。举个例子来说,就是当你网上买商品时,减少你账户余额和减少商品库存这两个动作应该是要全部发生或者全部都不发生。怎么才能保证这一点?答案是,使用事务。
如果不使用事务
则执行下面的 purchaseBook 方法后,就可能会出现商品库存减少了但是用户余额却没有减少的情况,这对于商家来说是不能接受的。
public void purchaseBook(Integer userId, String isbn, int num) {
//1. 获取书的单价
double price = bookShopDao.findBookPriceByIsbn(isbn);
double amounts = AmountTransUtil.mul(price, num);
//2. 更新数的库存
bookShopDao.reduceBookStock(isbn, num);
//3. 更新用户余额
bookShopDao.reduceUserAccount(userId, amounts);
}
/**
* 减少isbn对应的书籍的数量
* @param isbn
*/
public void reduceBookStock(String isbn, int num) {
String sql2 = "select stock from book_stock where isbn = ?";
Integer stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if (stock == null)
throw new BookStockException("找不到该书本库存的信息,请确认输入的isbn是否准确");
if (stock.intValue() <= 0)
throw new BookStockException("库存不足");
String sql = "update book_stock set stock = stock - ? where isbn = ? and stock >= ?";
int updateCount = jdbcTemplate.update(sql, num, isbn, num);
if (updateCount != 1) {
throw new BookStockException("库存不足");
}
}
/**
* 更新用户余额(将其余额减去花费的钱)
* @param id
* @param amounts 必须为大于0的数字
*/
public void reduceUserAccount(int id, double amounts) {
String sql2 = "select balance from account where id = ?";
double balance = jdbcTemplate.queryForObject(sql2, Double.class, id);
if (balance < amounts)
throw new UserAccountException("余额不足");
String sql = "update account set balance = balance - ? where id = ? and balance >= ?";
int updateCount = jdbcTemplate.update(sql, amounts, id, amounts);
if (updateCount != 1) {
throw new UserAccountException("余额不足或者账户不存在");
}
}
如何使用事务?
这里只介绍基于注解的Spring事务。在Spring配置文件上配置aop、transactionManager,并开启事务注解,然后只需在purchaseBook方法上面一行添加”@Transactional”。
<!-- 使用 AspectJ 注解起作用:自动为匹配的类生成代理对象 -->
<!--基于接口的代理,使用jdk实现 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<!-- jdbc start -->
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:app.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="jdbcUrl" value="${jdbc.jdbcUrl}" />
<property name="driverClass" value="${jdbc.driverClass}" />
<property name="initialPoolSize" value="${jdbc.initPoolSize}" />
<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource" />
</bean>
<!-- jdbc end -->
<!-- tx start -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- tx end -->
@Transactional
public void purchaseBook(Integer userId, String isbn, int num) {
//省略内容
}
注意,上面的 reduceBookStock 和 reduceUserAccount 方法抛出的异常必须是
RuntimeException的子类。因为Spring事务的处理机制就是当事务方法中抛出RuntimeException 后,就会回滚事务,回滚后,将原异常重新抛出。
事务的传播行为是什么呢?
事务的传播的规则,公用7种类型:
(1)PROPAGATION.REQUIRED(默认) (重要)
如果外层方法有事务,则当前方法加入外层事务;如果外层方法没有事务,则当前方法新建一个事务。
(2)PROPAGATION.REQUIRES_NEW
当前方法总是开启一个新的事务,如果外层方法有事务,则将外层事务挂起,先执行当前方法的事务(外层事务和当前方法的事务是两个不同的事务)。
当当前方法发生回滚并抛出RuntimeException时,如果该异常被捕获,则外层方法的事务不会因此回滚;如果该异常没有被捕获,则外层方法的事务就会因此而回滚。
当外层方法发生回滚时,如果其回滚发生在当前方法前,则当前方法得不到执行;如果其回滚发生在当前方法之后,则当前方法不会因此而回滚。
(3)PROPAGATION.NESTED
如果外层方法没事务,则当前方法新建一个事务;如果外层方法有事务,则把当前方法当成外层事务的一部分(使用savepoint实现),外层方法事务的rolback或者commit都会影响当前方法[注2],而当前方法的rolback不会导致外层事务回滚,除非rollback过程抛出了RuntimeException且该异常没有被捕获。
(4)PROPAGATION.SUPPORTS (重要)
如果外层方法没事务,那当前方法就按普通方法执行;如果外层方法有事务,则使用外层方法的事务。
(5)PROPAGATION.NOT_SUPPORTED
当前方法总是非事务地执行,如果外层方法有事务则把事务挂起,当前方法还是以普通方法执行。
(6)PROPAGATION.NEVER
如果外层方法没事务,那当前方法就按普通方法执行;如果外层方法有事务,则当前方法抛出异常。
(7)PROPAGATION.MANDATORY
如果外层方法没事务,则当前方法就会抛出异常;如果外层方法有事务,则当前方法使用外层事务。
@Transactional
public void purchaseBook1(Integer userId, String isbn, int num) {
//省略内容
}
@Transactional(propagation=Propagation.REQUIRED)
public void purchaseBook2(Integer userId, String isbn, int num) {
//省略内容
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void purchaseBook3(Integer userId, String isbn, int num) {
//省略内容
}
下面是这几种传播行为的使用方式。其中purchaseBook1方法和purchaseBook2方法是等效的,因为propagation的默认值就是Propagation.REQUIRED。
@Transactional
public void purchaseBook1(Integer userId, String isbn, int num) {
//省略内容
}
@Transactional(propagation=Propagation.REQUIRED)
public void purchaseBook2(Integer userId, String isbn, int num) {
//省略内容
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void purchaseBook3(Integer userId, String isbn, int num) {
//省略内容
}
// ...
其他的一些注意事项:
@Transactional 注解应该只被应用到 public 可见度的方法上,否则不会生效。
当@Transactional 注解在接口方法上,只有代理类是基于接口的代理[注3]时,Transactional 注解才起作用的。通常,建议将@Transactional 注解在具体类的方法上。
注1:外层方法并不单指当前方法的上一级方法。如果方法X调用方法A,方法A调用当前方法,那么方法X和A都是外层方法。
注2:如果外层事务rollback了,当前方法中的数据库操作就不会反映在数据库中;如果外层事务commit了,当前方法中的数据库操作就会反映在数据库中。
注3:
当你在Spring中配置了 <aop:aspectj-autoproxy></aop:aspectj-autoproxy> 时,那么代理类就是基于接口的代理。
当你在Spring中配置了 <aop:aspectj-autoproxy proxy-target-class="true"/> 时,那么代理类就是基于类的代理。