1事务概述
- 1)在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
- 2)事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
- 3)事务的四个关键属性(ACID)
①原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
②一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
③隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
④持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
2 Spring事务管理
编程式事务管理
- 1)使用原生的JDBC API进行事务管理
①获取数据库连接Connection对象
②取消事务的自动提交
③执行操作
④正常完成操作时手动提交事务
⑤执行失败时回滚事务
⑥关闭相关资源 - 2)评价
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型 的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务 的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务 管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块 都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
声明式事务管理
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式的事务管理。
Spring提供的事务管理器
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
事务管理器的主要实现
1)DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
2)JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理
3)HibernateTransactionManager:用Hibernate框架存取数据库
3 测试数据准备
需求
数据库表
CREATE TABLE `book` (
`bid` int(11) NOT NULL AUTO_INCREMENT,
`bname` varchar(255) DEFAULT NULL,
`price` int(11) DEFAULT NULL,
PRIMARY KEY (`bid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `money` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=1002 DEFAULT CHARSET=utf8;
CREATE TABLE `stock` (
`sid` int(11) NOT NULL AUTO_INCREMENT,
`st` int(11) DEFAULT NULL,
PRIMARY KEY (`sid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `book` VALUES ('1', 'java从入门到放弃', '50');
INSERT INTO `book` VALUES ('2', 'mysql从删库到跑路', '100');
INSERT INTO `money` VALUES ('1001', '120');
INSERT INTO `stock` VALUES ('1', '10');
INSERT INTO `stock` VALUES ('2', '10');
4 初步实现
- 1)配置xml文件
<!--通过数据源配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSourse"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourse"/>
</bean>
<!--开启注解驱动,即对事务相关的注解进行扫描,解析含义,并执行功能-->
<tx:annotation-driven/>
</beans>
如我的项目模块划分:
- 2)在需要进行事务控制的方法上加注解 @Transactional
5 事务的传播行为
简介
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
事务传播属性可以在@Transactional注解的propagation属性中定义。
测试
- 1). 说明
①REQUIRED传播行为
当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了。
②. REQUIRES_NEW传播行为
表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。
补充
在Spring 2.x事务通知中,可以像下面这样在tx:method元素中设定传播事务属性。
6 事务的隔离级别
数据库事务并发问题
假设现在有两个事务:Transaction01和Transaction02并发执行。
- 1)脏读
①Transaction01将某条记录的AGE值从20修改为30。
②Transaction02读取了Transaction01更新后的值:30。
③Transaction01回滚,AGE值恢复到了20。
④Transaction02读取到的30就是一个无效的值。 - 2)不可重复读
①Transaction01读取了AGE值为20。
②Transaction02将AGE值修改为30。
③Transaction01再次读取AGE值为30,和第一次读取不一致。 - 3)幻读
①Transaction01读取了STUDENT表中的一部分数据。
②Transaction02向STUDENT表中插入了新的行。
③Transaction01读取了STUDENT表时,多出了一些行。
隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
- 1)读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。 - 2)读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。 - 3)可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。 - 4)串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。 - 5)各个隔离级别解决并发问题的能力见下表
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
- 6)各种数据库产品对事务隔离级别的支持程度
Oracle | MySQL | |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √ (默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
在Spring中指定事务隔离级别
- 1)注解
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别 - 2)XML
在Spring 2.x事务通知中,可以在tx:method元素中指定隔离级别
7 触发事务回滚的异常
默认情况
捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。
设置途经
- 1)注解@Transactional 注解
① rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个
② noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个
- 2)XML
在Spring 2.x事务通知中,可以在tx:method元素中指定回滚规则。如果有不止一种异常则用逗号分隔。
8 事务的超时和只读属性
简介
由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。
如果一个事务只读取数据但不做修改,数据库引擎可以对这个事务进行优化。
超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
设置
- 1)注解
@Transaction注解
- 2)XML
在Spring 2.x事务通知中,超时和只读属性可以在tx:method元素中进行指定
9 基于XML文档的声明式事务配置
<!-- 配置事务切面 -->
<aop:config>
<aop:pointcut
expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))"
id="txPointCut"/>
<!-- 将切入点表达式和事务属性配置关联到一起 -->
<aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/>
</aop:config>
<!-- 配置基于XML的声明式事务 -->
<tx:advice id="myTx" transaction-manager="transactionManager">
<tx:attributes>
<!-- 设置具体方法的事务属性 -->
<tx:method name="find*" read-only="true"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="purchase"
isolation="READ_COMMITTED"
no-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException"
propagation="REQUIRES_NEW"
read-only="false"
timeout="10"/>
</tx:attributes>
</tx:advice>
10 详细代码块:
配置xml文件以及db.properties
<?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.atguigu.spring.day03_Spring_JdbcTemplate.book"/>
<!--引入资源文件-->
<context:property-placeholder location="db.properties"/>
<!--创建数据源-->
<bean id="dataSourse" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--通过数据源配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSourse"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourse"/>
</bean>
<!--开启注解驱动,即对事务相关的注解进行扫描,解析含义,并执行功能-->
<tx:annotation-driven/>
</beans>
db.properties:
# k = v
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.username=root
jdbc.password=1234
项目模块划分:
BookController:
@Controller
public class BookController {
@Autowired
private BookService bookService;
@Autowired
private Cashier cashier;
public void buyBook(){
bookService.buyBook("1","1001");
}
public void checkOut(){
List<String> bids = new ArrayList<>();
bids.add("1");
bids.add("2");
cashier.checkOut("1001",bids);
}
}
BookDao和BookDaoImpl :
public interface BookDao {
Integer selectPrice(String bid);
void updateSt(String bid);
void updateBalance(String uid,Integer price);
}
@Repository
public class BookDaoImpl implements BookDao{
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer selectPrice(String bid) {
Integer price = jdbcTemplate.queryForObject("select price from book where bid = ?", new Object[]{bid}, Integer.class);
return price;
}
@Override
public void updateSt(String bid) {
//获取该书籍的库存
Integer stock = jdbcTemplate.queryForObject("select st from stock where sid = ?", new Object[]{bid}, Integer.class);
if (stock<0){
throw new RuntimeException("库存不足!!");
}else {
jdbcTemplate.update("update stock set st = st - 1 where sid = ?",bid);
}
}
@Override
public void updateBalance(String uid,Integer price) {
Integer balance = jdbcTemplate.queryForObject("select balance from money where uid = ?", new Object[]{uid}, Integer.class);
if(balance < price){
throw new RuntimeException("余额不足!!");
}else {
jdbcTemplate.update("update money set balance = balance - ? where uid = ?",price,uid);
}
}
}
BookService和BookServiceImpl:买一本书
public interface BookService {
void buyBook(String bid,String uid);
}
@Service
public class BookServiceImpl implements BookService{
@Autowired
private BookDao dao;
/**
* @Transactional :对方法中所有的操作作为一个事务来处理
* 在方法上使用只对方法有效果
* 在类上使用,对类中的所有方法都有效果
* propagation:A方法和B方法都有事务,当A在调用B时,会将A中的事务传播给B方法,B方法对于事务的处理方式就是事务的传播方式
*Propagation.REQUIRED 必须使用调用者的事务
*Propagation.REQUIRES_NEW 将调用者的事务挂起,不使用调用者的事务,使用新的事务进行处理
*
* isolation:
* 读未提交 :脏读 1
* 读已提交 :不可重复读 2
* 可重复读 :幻读 4
* 串行化 : 性能低消耗大 8
*
* timeout: 在事务强制回滚前最多可以执行(等待)的时间
*
* readOnly: 指定当前事务中的一系列操作是否为只读
* 若设置为只读,不管事务中有没有写的操作,mysql都会在请求访问数据时不加锁,提高性能
* 但是如果有写的操作的情况,建议一定不能设置只读
*
* rollbackFor | rollbackForClassName | norollbackFor | norollbackForClassName
*
*
*/
@Transactional(propagation = Propagation.REQUIRED,timeout = 3)
public void buyBook(String bid, String uid) {
Integer price = dao.selectPrice(bid);
dao.updateSt(bid);
dao.updateBalance(uid,price);
}
}
Cashier和CashierImpl: 买多本书
public interface Cashier {
void checkOut(String uid, List<String> bids);
}
@Service
@Transactional
public class CashierImpl implements Cashier{
@Autowired
private BookService service;
@Override
public void checkOut(String uid, List<String> bids) {
for (String bid:bids
) {
service.buyBook(bid,uid);
}
}
}
测试类test:
public class test {
@Test
public void test(){
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("book.xml");
BookController bookController = ac.getBean("bookController", BookController.class);
bookController.checkOut();
}
}
数据库初始数据:
书:
库存:
余额:
当数据库余额够时:
余额减少:
库存减少:
当余额不足时:
将余额调到100:
结果:
事务回滚:
余额和库存并不会单独减少!