本篇博客利用例子来讲解为什么使用事务以及事务的一些属性和配置,耐心看完会有很大的帮助!!不对的地方也希望大家共同指出。
1. 事务概述
1. 在Java EE企业级开发的时候,为了在每次操作完成的同时,也保证数据的完整性,就需要引入数据库事物的概念。事务是什么?简单的来说就是一组由逻辑上紧密关联而合并成一个整体的多个数据库操作,通过事物的操作,我们可以保证数据的在每次访问或修改时都能保持一个相对准确的结果。事物的操作要么全部执行成功,要么全部执行失败。
2. 事务的经典四大特性:
(1)原子性:事务的原子性要求事务中的所有操作要么都执行成功,要么都不成功。
(2)一致性:事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。
(3)隔离性:事务的隔离性要求多个事务在并发执行过程中不会互相干扰。
(4)持久性:事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。
2. 声明式事务管理
1. Spring提供了优秀的声明式事务管理,相比于之前的编程式事务管理(将事务代码写在业务模块中,代码冗余),而Spring声明式事务管理全程不用写任何java代码,只需要简单的配置即可完成事务声明。Spring的声明式事务管理将管理代码从业务方法中分类出来,以声明的方式来实现事务管理(底层其实就是使用AOP思想)。Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
2. Spring提供的事务管理器是从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。Spring的核心事务管理抽象是PlatformTransactionManager,它底层有三个实现方法提供我们使用:DataSourceTransactionManager、JtaTransactionManager、HibernateTransactionManager。我们一般操作JDBC就使用DataSourceTransactionManager即可。
3. 数据测试声明式事务管理
3.1 想法:我们编写一个需求来演示事务的功能以及事物的一些概念。首先我们模拟用户购买书籍的过程,创建三张表,分别为book表(书号,书名)、book_stock表(书号,库存)、account表(用户名,余额)。我们通过代码模拟购书流程,模拟用户余额不足或者书籍库存不足这些情况,通过每一次的操作观察数据库每个表里的数据更改与否,来引入事务管理的功能。
3.2 首先我们建表(mysql):
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,
) ;
CREATE TABLE account (
username VARCHAR (50) PRIMARY KEY,
balance INT,
) ;
INSERT INTO `test`.`book` (`isbn`,`book_name`, `price`) VALUES ("001",'白夜行', 60);
INSERT INTO `test`.`book` (`isbn`,`book_name`, `price`) VALUES ("002",'围城', 30);
INSERT INTO `test`.`book_stock` (`isbn`,`stock`) VALUES ("001",10);
INSERT INTO `test`.`book_stock` (`isbn`,`stock`) VALUES ("002",10);
INSERT INTO `test`.`account` (`username`,`balance`) VALUES ("wei",100);
3.3 配置xml配置文件:sping-tx.xml,配置之前我们需要先写一个外部数据源文件db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456
导入依赖:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.jpattern/jporm-jdbctemplate -->
<dependency>
<groupId>com.jpattern</groupId>
<artifactId>jporm-jdbctemplate</artifactId>
<version>4.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alexkasko.springjdbc/springjdbc-iterable -->
<dependency>
<groupId>com.alexkasko.springjdbc</groupId>
<artifactId>springjdbc-iterable</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
然后就是我们的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/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.wei.spring.tx.annotation"></context:component-scan>
<!-- 数据源-->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!-- 事务管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务注解
transaction-manager:用来指定事务管理器,如果事务管理器的id是transactionManager,可以省略不指定
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>
3.4 编写BookShopDao接口,来声明我们所用的方法:顾客如果买书成功,我们首先需要将书的信息价格查出来,然后对应将书的库存 -1,对应用户的余额 - 书的价格,这些信息都需要随着买书成功得到相应的更新,所以我声明这三个方法。
public interface BookShopDao {
//根据书号查询书的价格
public int findPriceByIsbn(String isbn);
//更新书的库存
public void updateStock(String isbn);
//更新用户的余额
public void updateUserAccount(String username,Integer price);
}
3.5 编写BookShopDao的实现类BookShopDaoImpl,就是要把我们三个方法进行实现,把我们的方法写完整。查询书的价格、更新书的库存,更新用户账户余额。但是要注意书的库存以及用户账户余额都有不足的时候,所以我们自定义了俩个异常:库存不足异常(BookStockException)以及余额不足(UserAccountException)异常,并且输出不足提示。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
//根据书号查询书的价格
public int findPriceByIsbn(String isbn) {
String sql = "select price from book where isbn = ?";
return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
}
//更新书的库存
public void updateStock(String isbn) {
//判断库存是否足够
String sql = "select stock from book_stock where isbn = ?";
Integer stock = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
if (stock <= 0 ){
throw new BookStockException("库存不足!!");
}
sql = "update book_stock set stock = stock - 1 where isbn = ?";
jdbcTemplate.update(sql,isbn);
}
//更新用户的余额
public void updateUserAccount(String username, Integer price) {
String sql ="select balance from account where username = ? ";
Integer balance = jdbcTemplate.queryForObject(sql, Integer.class,username);
if (balance < price){
throw new UserAccountException("余额不足!!!");
}
sql = "update account set balance = balance - ? where username = ? ";
// sql = "update account set balance = balance - ? where username = ? ";
jdbcTemplate.update(sql,price,username);
}
}
俩个异常类(实现里面的构造器)
public class BookStockException extends RuntimeException {
public BookStockException() {
}
public BookStockException(String message) {
super(message);
}
public BookStockException(String message, Throwable cause) {
super(message, cause);
}
public BookStockException(Throwable cause) {
super(cause);
}
public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
public class UserAccountException extends RuntimeException {
public UserAccountException() {
}
public UserAccountException(String message) {
super(message);
}
public UserAccountException(String message, Throwable cause) {
super(message, cause);
}
public UserAccountException(Throwable cause) {
super(cause);
}
public UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
3.6 编写BookStockService,来写一个buyBook的方法,来调用我们的dao层的方法完成买书的过程。
public interface BookStockService {
public void byBook(String username,String isbn);
}
3.7 编写BookStockService的实现类BookStockServiceImpl。当我们买一本书时,首先要查询到书的价格,然后更新更新书的库存,更新用户的余额,都满足就可以完成买书操作。
@Service
public class BookStockServiceImpl implements BookStockService {
@Autowired
private BookShopDao bookShopDao;
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
}
3.8 然后就是编写测试类进行买书。
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.ArrayList;
public class testTx {
private BookShopDao bookShopDao;
private BookStockService bookStockService;
@Before
public void init(){
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-tx.xml");
bookShopDao = ctx.getBean("bookShopDaoImpl", BookShopDao.class);
bookStockService = ctx.getBean("bookStockServiceImpl", BookStockService.class);
}
@Test
public void testtx1(){
bookStockService.byBook("wei","001");
}
}
我们运行可以发现。001这本书的库存少了一本,用户的账户余额也对应的少了第一本书的价格,剩余40元,那就说明我再一次购买第一本书的时候,会报错,因为我的余额已经不足,我们再买一本,来观察。运行发现,首先程序会报错:com.wei.spring.tx.annotation.UserAccountException: 余额不足!!!,然后我们查看数据库数据:发现第一本书的库存变成了8,就是又减少了一本,但是我余额却没有减少,因为已近不够买一本书的价钱了,但是书却减少了,这个在我们的生活中是肯定不能出现的,这就是程序得确定,所以我们需要引入事务的概念,然我们运行程序时来保证我们数据的完整性。
3.8 为方法添加事务:我们只需要在buyBook方法上加@Transactional注解,就为我们的方法加上了事务。
@Service
public class BookStockServiceImpl implements BookStockService {
@Autowired
private BookShopDao bookShopDao;
@Transactional
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
}
我们将数据库里的库存和余额都还原,我们再进行测试。第一次当然也是购买成功,第二次首先还是会报错:余额不足,但是数据库发现库存这次没减少,说明事物起到了作用。事务的配置保证了我们每次访问数据库的一致性和准确性。事务就这么简单吗?当然不是,事务中还可以配置很多配置,接下来我们在通过一个例子来讲解。
3.9 我们编写一个方法来模拟同时购买多本书,我们已经配置过得事务属性还会出现什么样的情况?
public interface Cashier {
//结账,模拟买多本书
public void checkOut(String username, List<String> isbns);
}
3.10 编写Cashier的实现类CashierImpl
@Service
public class CashierImpl implements Cashier{
@Autowired
BookStockService bookStockService;
public void checkOut(String username, List<String> isbns) {
for (String isbn : isbns) {
bookStockService.byBook(username,isbn);
}
}
}
3.11 我们接着测试我们checkOut模拟同时购买多本书。在我们的测试类中加入一个junit测试模块,运行前我们将数据库账户余额改为80,也就是说我们要测试同时购买俩本书,但是买一本钱够,买两本不够的情况。
@Test
public void testCashier(){
ArrayList<String> isbns = new ArrayList<>();
isbns.add("001");
isbns.add("002");
cashier.checkOut("wei",isbns);
}
测试我们发现,第一本书库存减少了一本,余额也减少了对应的钱数,但是我们第二本书没有购买成功,控制台报错余额不足,但这样的情况类似于我们tb添加购物车的商品一起付款时,你的钱不够,却拿你的钱付了能买成功的商品,买不成功的就显示余额不足,这样的情况我们也不允许发生,这样是不对的。为了将这俩个操作同时成功或者同时失败,我们就需要将同时购买的方法也加上事务@Transactional注解
@Service
public class CashierImpl implements Cashier{
@Autowired
BookStockService bookStockService;
@Transactional
public void checkOut(String username, List<String> isbns) {
for (String isbn : isbns) {
bookStockService.byBook(username,isbn);
}
}
}
这样我们把数据还原测试发现,这样同时购买两本书因为余额不够,都会够买不成功。
3.11 如果我在加入@Transactional注解时,想要完成 “如果余额够一本书的价格就买一本,剩下的购买不成功”,我们就需要引入事物的传播行为的概念。
事务的传播行为(Propagation):事务与事务之间是有传播性的,一个事物被另一个事务方法调用时,当前的事务如何使用事务,这个需要在@Transactional注解中声明Propagation:也就是事务的传播行为。
事务的传播行为有俩种,分别为Propagation.REQUIRED和Propagation.REQUIRES_NEW。Propagation.REQUIRES是默认值,也就是我们不配置就是默认值。这俩个配置都是什么意思?
Propagation.REQUIRED:默认值,使用调用者的事务。那上面的例子说明,我们如果不设置也就是使用这个默认传播行为,购买俩本书的时候都不会成功,也就是使用chekOut这个方法的事务,前面购买一本的时候成功了因为余额充足,但是在购买第二本的时候余额不够,就会购买失败,也就是chekOut这个事务失败,而事务都具有回滚特性,所以出现失败直接回滚,导致第一本购买的也被回滚,最后结果出现俩本都购买不成功的现象。
Propagation.REQUIRES_NEW:将调用者的事务挂起,重新开启事务来使用。还是那前面同时购买俩本的例子。配置了这个属性新的方法执行时会开启一个新的事务,也就是checkOut方法和之前购买一本的方法分别启动俩个事务管理器,虽然第二本购买的时候出现了购买失败,但是他只会回滚自己的事务,导致第二本购买失败,第一本的事务被挂起,不受影响,所以会出现一本成功一本失败。
配置代码如下: @Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
3.12 事务的隔离级别(isolation):如果两个事务Transaction01和Transaction02并发执行,为了使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。
事务有四种隔离级别:
1.读未提交(READ UNCOMMITTED):一个事务进行修改操作,修改的结果还没提交却别第二个事务的查询操作查询到,这样会出现的问题是:脏读 (解决办法:读已提交)。
2.读已提交(READ COMMITTED): 一个事物在进行修改操作,当数据还未提价,事务二进行读取操作,读完之后事务一进行提交,事务二再读的时候读回来的就是新的值,俩次读取值不一致,这样存在问题是:不可重复读:主要描述修改操作(解决办法:可重复读)。
4.可重复读(REPEATABLE READ):一个事务在对数据进行修改操作,还没提交时,事务二进行读取,事务二没办法读取到任何数据,当第二次再去读取时发现事务一已经将数据更改并且提交上去,事务二俩次读取的数据不一致,这样就存在问题:幻读:主要描述插入操作(解决办法:串行化 )mysql默认隔离级别。
8.串行化(SERIALIZABLE) :当一个事务在操作时其他事务必须等待,排队一个一个进行操作,这样存在的问题是:效率低。具体怎么配置事务的隔离级别如下:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
3.13 事务的回滚与不回滚:默认情况下,Spring捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。
事务的回滚异常有四种设置属性:可以设置捕获什么异常时进行回滚或者不回滚
rollbackFor:指定遇到时必须进行回滚的异常类型,可以为多个 rollbackForClassName:通过一个字符串来指定异常进行回滚 noRollbackFor:指定遇到时不回滚的异常类型,可以为多个 noRollbackClassName:通过一个字符串来指定异常进行不回滚
具体配置代码如下:
@Transactional(noRollbackFor = {UserAccountException.class})
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
3.14 事务的只读设置
readOnly: true:只读。代表着只会对数据库进行读取操作,不会有修改的操作。如果确保当前的事务只有读取操作,就有必要设置只读,可以帮助数据库引擎优化事务。 false:非只读。代表不仅会有读取数据操作还会有修改操作,就会有加锁操作
具体设置代码如下:
@Transactional(readOnly = false)
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
3.15 事务的超时设置:
超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。值得一提得是:事务的超时设置需要进行严格的计算,不可以随便填写。
具体设置代码如下:
@Transactional(timeout = 3)
public void byBook(String username, String isbn) {
Integer price = bookShopDao.findPriceByIsbn(isbn);
bookShopDao.updateStock(isbn);
bookShopDao.updateUserAccount(username,price);
}
注意:这是通过注解的方法来配置事务,配置事务还可以通过xml文件来配置,但是使用注解的方式比价好理解也比较好用,推荐使用注解方式来配置事务。