##一、什么是事务管理
- 事务管理是企业级应用程序开发中必不可少的技术, 用来确保数据的完整性和一致性.。
- 事务就是一系列的动作, 它们被当做一个单独的工作单元. 这些动作要么全部完成, 要么全部不起作用。
事务的四个关键属性(ACID):
属性 | 解释 |
---|---|
原子性(atomicity) | 事务是一个原子操作, 由一系列动作组成. 事务的原子性确保动作要么全部完成要么完全不起作用 |
一致性(consistency) | 一旦所有事务动作完成, 事务就被提交. 数据和资源就处于一种满足业务规则的一致性状态中 |
隔离性(isolation) | 可能有许多事务会同时处理相同的数据, 因此每个事物都应该与其他事务隔离开来, 防止数据损坏 |
持久性(durability) | 一旦事务完成, 无论发生什么系统错误, 它的结果都不应该受到影响. 通常情况下, 事务的结果被写到持久化存储器中 |
- Spring 的核心事务管理抽象是
PlatformTransactionManage
r它为事务管理封装了一组独立于技术的方法. 无论使用
Spring 的哪种事务管理策略(编程式或声明式), 事务管理器都是必须的。 DataSourceTransactionManager
:在应用程序中只需要处理一个数据源, 而且通过 JDBC 存取。JtaTransactionManager
: 在 JavaEE 应用服务器上用 JTA(Java Transaction API) 进行事务管理HibernateTransactionManager
:用 Hibernate 框架存取数据库- 事务管理器以普通的 Bean 形式声明在 Spring IOC 容器中
##二、为什么要使用事务管理
举个例子:
在一个卖书的商城系统中,肯定会有一个数据库表记录书本价格、书本的库存、用户的余额。当一个用户想买一本价值100元的书,在他结账的时候用户余额会减去100元,而书的库存就会减去1本,这就是一个事务单元。当用户的余额只剩下90元,不足以购买100元的书,这个时候肯定会抛出一个余额不足的异常,虽然账户余额由于异常没有再减钱,但是减去书本库存并没有出现异常,所以依旧会减去1本书的库存,这样做的话就不符合常规,常规应该时在扣钱的时候库存才减去。为了避免这种情况,事务管理就诞生了,它要求一个事务单元要么全都完成,只要当中有一个流程失败,整个事务单元就会回滚到原来没改动前的状态。
##三、使用注解方式进行事务管理流程
一般在xml配置文件中启用事务注解即可
##四、实例的传播属性
事务传播属性可以在 @Transactional 注解的 propagation 属性中定义
参考资料:孟凡柱的专栏
传播属性 | 解释 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 支持当前事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。 |
参考资料:尚硅谷
REQUIRED
和REQUIRES_NEW
区别
REQUIRED
传播行为:当 bookService 的 purchase() 方法被另一个事务方法 checkout() 调用时, 它默认会在现有的事务内运行. 这个默认的传播行为就是 REQUIRED. 因此在 checkout() 方法的开始和终止边界内只有一个事务. 这个事务只在 checkout() 方法结束的时候被提交, 结果用户一本书都买不了
REQUIRES_NEW
传播行为:另一种常见的传播行为是 REQUIRES_NEW. 它表示该方法必须启动一个新事务, 并在自己的事务内运行. 如果有事务在运行, 就应该先挂起它.
##五、示例代码
在进行本实验之前请做好JdbcTemplate
的配置,如果没有了解过JdbcTemplate
,请翻阅我的【Spring4.0】系列日志,里面有提到JdbcTemplate
相关内容。
###(1)了解需求
需要创建的类:
数据库的表:
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT /;
/!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS /;
/!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION /;
/!40101 SET NAMES utf8 /;
/!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE /;
/!40103 SET TIME_ZONE=’+00:00’ /;
/!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 /;
/!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 /;
/!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=‘NO_AUTO_VALUE_ON_ZERO’ /;
/!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
–
– Table structure for table account
DROP TABLE IF EXISTS account
;
/*!40101 SET @saved_cs_client = @@character_set_client /;
/!40101 SET character_set_client = utf8 /;
CREATE TABLE account
(
username
varchar(50) NOT NULL,
balance
int(11) DEFAULT NULL,
PRIMARY KEY (username
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/!40101 SET character_set_client = @saved_cs_client */;
–
– Dumping data for table account
LOCK TABLES account
WRITE;
/*!40000 ALTER TABLE account
DISABLE KEYS /;
INSERT INTO account
VALUES (‘AA’,200);
/!40000 ALTER TABLE account
ENABLE KEYS */;
UNLOCK TABLES;
–
– Table structure for table book
DROP TABLE IF EXISTS book
;
/*!40101 SET @saved_cs_client = @@character_set_client /;
/!40101 SET character_set_client = utf8 /;
CREATE TABLE book
(
isbn
varchar(50) NOT NULL,
book_name
varchar(100) DEFAULT NULL,
price
int(11) DEFAULT NULL,
PRIMARY KEY (isbn
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/!40101 SET character_set_client = @saved_cs_client */;
–
– Dumping data for table book
LOCK TABLES book
WRITE;
/*!40000 ALTER TABLE book
DISABLE KEYS /;
INSERT INTO book
VALUES (‘1001’,‘Java’,100),(‘1002’,‘Oracle’,70);
/!40000 ALTER TABLE book
ENABLE KEYS */;
UNLOCK TABLES;
–
– Table structure for table book_stock
DROP TABLE IF EXISTS book_stock
;
/*!40101 SET @saved_cs_client = @@character_set_client /;
/!40101 SET character_set_client = utf8 /;
CREATE TABLE book_stock
(
isbn
varchar(50) NOT NULL,
stock
int(11) DEFAULT NULL,
PRIMARY KEY (isbn
),
CONSTRAINT book_stock_isbn
FOREIGN KEY (isbn
) REFERENCES book
(isbn
) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/!40101 SET character_set_client = @saved_cs_client */;
–
– Dumping data for table book_stock
LOCK TABLES book_stock
WRITE;
/*!40000 ALTER TABLE book_stock
DISABLE KEYS /;
INSERT INTO book_stock
VALUES (‘1001’,10),(‘1002’,10);
/!40000 ALTER TABLE book_stock
ENABLE KEYS /;
UNLOCK TABLES;
/!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE /;
/!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS /;
/!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS /;
/!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT /;
/!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS /;
/!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION /;
/!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
– Dump completed on 2018-08-06 21:12:05
###(3)导入相关的包
![这里写图片描述](https://img-blog.csdn.net/20180806211916500?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTk2OTc4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
###(4)创建外部配置文件`db.properties`
在src根目录下创建`db.properties`外部配置文件,用于连接MySQL数据库
jdbc.user=root
jdbc.password=123456
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/spring
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
###(5)创建`applicationContext-tx-xml.xml`配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- 自动扫描 -->
<context:component-scan base-package="com.spring"></context:component-scan>
<!-- 导入外部资源文件 -->
<context:property-placeholder location="classpath:db.properties" />
<!-- 配置C3P0数据源 -->
<bean id="dataSource" 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.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置Spring 的JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
```
###(6)创建接口BookShopServiceDao
和BookShopService
BookShopServiceDao
package com.spring.tx;
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
//更新书的库存,使书号对应的库存-1
public void updateBookStock(String isbn);
//更新用户账户余额:使username的balance - price
public void updateUserAccount(String username,int price);
}
BookShopService
package com.spring.tx;
public interface BookShopDao {
//根据书号获取书的单价
public int findBookPriceByIsbn(String isbn);
//更新书的库存,使书号对应的库存-1
public void updateBookStock(String isbn);
//更新用户账户余额:使username的balance - price
public void updateUserAccount(String username,int price);
}
###(7)创建异常类UserAccountException
和BookStockException
两个类继承RuntimeException并且把相对应的构造器重写即可
UserAccountException
package com.spring.tx;
public class UserAccountException extends RuntimeException{
/**
*
*/
private static final long serialVersionUID = 3217299177229543901L;
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
}
}
BookStockException
package com.spring.tx;
public class BookStockException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = -8345575141732656052L;
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
}
}
###(8)创建实现类BookShopServiceImpl
和BookShopServiceImpl
BookShopServiceImpl
package com.spring.tx;
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) {
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);
}
}
BookShopServiceImpl
package com.spring.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;
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
/*
* 添加事务注解(non-Javadoc)
*1.使用propagation 指定事务的传播行为,即当前的事务方法被另一个事务方法调用时
*如何使用事务,默认值为REQUIRED,即使用调用方法的事务
*REQUIRES_NEW:使用自己的事务,调用事务方法的事务挂起
*2.使用isolation指定事务隔离级别,最常用的值为READ_COMMITTED
*3.默认情况下Spring的声明式事务对所有异常进行回滚,但是也可以通过属性进行设置,正常情况下默认值即可
*noRollbackFor对某些类不进行回滚
*RollbackFor
*4.使用readOnly属性指定事务是否为只读,表示这个事务数据但不更新数据,这样可以帮数据库引擎优化事务
*5.使用timeOut指定强制回滚之前事务可以占用的时间,单位是秒
* @see com.spring.tx.BookShopService#purchase(java.lang.String, java.lang.String)
*/
@Transactional(propagation = Propagation.REQUIRED,isolation=Isolation.READ_COMMITTED)
@Override
public void purchase(String username, String isbn) {
//1.获取书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2.更新数据库库存
bookShopDao.updateBookStock(isbn);
//3.更新用户价格
bookShopDao.updateUserAccount(username, price);
}
}