事务的意思是原子事务,即要么完整的完成这个事务,要么在报错时使系统的状态回到执行事务之前。这么做的意义是在进行事务处理时会涉及到持久层的访问和读写,举个简单的例子,一个简单的转账操作,A转100块给B,那么对应的数据库操作是A的账户余额减100,B的账户余额加100,这两个步骤构成了简单的转账的事务。这两个步骤对数据库的读写肯定会存在时间的先后,那么万一A的账户余额减少了100后在进行B的账户余额加100时,出现了错误怎么办呢,例如网络通信断开或者数据库访问错误等,这时候这个事务就出现了错误,我们应该对整个事务进行回退将系统恢复到事务执行之前,即让A和B的账户余额都回到转账之前的状态,然后再告知用户转账操作失败。在Spring中,它为我们提供了配置事务的方法,可以实现事务的原子性。
这里用的例子是从书店购书的例子,购书这个事务分两个步骤,一是书本库存减一,二是用户余额扣去书费。当书本库存不足时,需要对这个事务进行回退,当用户余额不足时也要进行回退。类的关系如下:
数据库的内容如下:
工程的目录如下:
一、使用注解的方式进行事务管理
类的代码如下:
package tx;
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
//更新书的库存,使对应库存减一
public void updateBookStock(String isbn);
//更新用户的账户余额,使username 的balance减去price
public void updataUserAccount(String username, int price);
}
package tx;
import javax.security.auth.login.AccountException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
// TODO Auto-generated method stub
String sql = "SELECT price FROM book WHERE isbn = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
}
@Override
public void updateBookStock(String isbn) {
// TODO Auto-generated method stub
//检测书的库存是否足够,若不够则抛出异常
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 updataUserAccount(String username, int price) {
// TODO Auto-generated method stub
//检测余额是否足够,若不够则抛出异常
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);
}
}
package tx;
public interface BookShopService {
public void purchase(String username, String isbn);
}
package tx;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
//添加事物注解
//1.使用propagation指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时
//如何使用事务,默认取值为REQUIRED,即使用调用方法的事务
//REQUIRED_NEW创建新的事务执行
//2.使用isolation指定事务的隔离级别,最常用的取值为READ_COMMITTED
//3.默认情况下Spring的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置
//noRollBackFor不对什么异常回滚 :noRollbackFor={UserAccountException.class}
//4.使用readOnly指定事务是否为只读,优化数据库引擎可以不加锁,加快速度
//readOnly=true
//5.timeout单位是秒,在强制回滚之前事务可以占用的时间
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
timeout=3)
@Override
public void purchase(String username, String isbn) {
// TODO Auto-generated method stub
/*模拟事务超时
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
*/
//1.获取书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2.更新书的库存
bookShopDao.updateBookStock(isbn);
//3.更新用户余额
bookShopDao.updataUserAccount(username, price);
}
}
然后是两个异常类分别是库存不足和余额不足
package tx;
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
}
}
package tx;
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
}
}
bean的配置文件如下:
property文件:
jdbc.user=root
jdbc.password=1234
jdbc.driverclass=com.mysql.cj.jdbc.Driver
jdbc.jdbcurl=jdbc:mysql:///spring-5?useSSL=true&serverTimezone=UTC
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
bean的配置和property属性文件和上一篇文章的差不多,这里不多做介绍
使用junit测试一下功能:
package tx;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringTransactionTest {
private ApplicationContext ctx = null;
private BookShopDao bookShopDao = null;
private BookShopService bookShopService = null;
private Cashier cashier = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
bookShopDao = ctx.getBean(BookShopDao.class);
bookShopService = ctx.getBean(BookShopService.class);
}
@Test
public void testBookShoService() {
bookShopService.purchase("AA", "1001");
}
@Test
public void testBookShopDaoUpdate() {
bookShopDao.updataUserAccount("AA", 100);
}
@Test
public void testBookShopDaoUpdateBookStock() {
bookShopDao.updateBookStock("1001");
}
@Test
public void testBookShopDapFindPriceByIsbn() {
System.out.println(bookShopDao.findBookPriceByIsbn("1001"));
}
}
运行一下可以发现运行结果正常,再次查看数据库也会发现一切正常。读者可以自行修改数据库,产生购买失败的情况,可以看到失败的事件会被回滚。
现在我们来考虑一个新的问题,当我们在一个事务中执行了一连串的事件时,当其中的一个事件出错了,需不需要将前面执行成功的事务也回滚呢?
写一个cashier类来测试一下:
package tx;
import java.util.List;
public interface Cashier {
public void checkout(String username, List isbns);
}
package tx;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service("cashier")
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService;
//添加事务注解
@Transactional
@Override
public void checkout(String username, List isbns) {
// TODO Auto-generated method stub
for (String isbn : isbns) {
bookShopService.purchase(username, isbn);
}
}
}
我们运行这段代码的话可以看到结果是只回滚了发生错误的事务,前面正常执行的事务没有被回滚,这是因为我们在前面的代码中设置了事务的传播属性
即在处理该事件时新开一个事件来处理,因此这个事件与调用它的事件不是处于同一个事件当中的,因此不会将调用其的事件也回滚。
除此之外Spring事务的事务传播行为还有6种:
REQUIRED:业务方法需要在一个容器里运行。如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。
NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。
MANDATORY:该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果在没有事务的环境下被调用,容器抛出例外。
SUPPORTS:该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。
NEVER:该方法绝对不能在事务范围内执行。如果在就抛例外。只有该方法没有关联到任何事务,才正常执行。
NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。
同样的,除了使用注解我们还可以通过xml文件配置的方式进行事务管理
还是使用上面的代码,但是需要将它们的注解全部去掉,然后在bean的配置文件中进行事务管理,配置文件如下: