1、环境准备
1.1、填写数据库 tx.sql
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50528
Source Host : localhost:3306
Source Database : tx
Target Server Type : MYSQL
Target Server Version : 50528
File Encoding : 65001
Date: 2020-02-13 19:19:32
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `account`
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`username` varchar(10) NOT NULL DEFAULT '',
`balance` double DEFAULT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO account VALUES ('lisi', '1000');
INSERT INTO account VALUES ('zhangsan', '1000');
-- ----------------------------
-- Table structure for `book`
-- ----------------------------
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` int(10) NOT NULL,
`book_name` varchar(10) DEFAULT NULL,
`price` double DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of book
-- ----------------------------
INSERT INTO book VALUES ('1', '西游记', '100');
INSERT INTO book VALUES ('2', '水浒传', '100');
INSERT INTO book VALUES ('3', '三国演义', '100');
INSERT INTO book VALUES ('4', '红楼梦', '100');
-- ----------------------------
-- Table structure for `book_stock`
-- ----------------------------
DROP TABLE IF EXISTS `book_stock`;
CREATE TABLE `book_stock` (
`id` int(255) NOT NULL DEFAULT '0',
`stock` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of book_stock
-- ----------------------------
INSERT INTO book_stock VALUES ('1', '1000');
INSERT INTO book_stock VALUES ('2', '1000');
INSERT INTO book_stock VALUES ('3', '1000');
INSERT INTO book_stock VALUES ('4', '1000');
1.2、BookDao
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDao {
@Autowired
JdbcTemplate jdbcTemplate;
/**
* 减去某个用户的余额
* @param userName
* @param price
*/
public void updateBalance(String userName,int price){
String sql = "update account set balance=balance-? where username=?";
jdbcTemplate.update(sql,price,userName);
}
/**
* 按照图书的id来获取图书的价格
* @param id
* @return
*/
public int getPrice(int id){
String sql = "select price from book where id=?";
return jdbcTemplate.queryForObject(sql,Integer.class,id);
}
/**
* 减库存,减去某本书的库存
* @param id
*/
public void updateStock(int id){
String sql = "update book_stock set stock=stock-1 where id=?";
jdbcTemplate.update(sql,id);
}
}
1.3、BookService
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账:传入哪个用户买了哪本书
*
* @param username
* @param id
*/
public void checkout(String username, int id) {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
//int i = 1/0;
bookDao.updateBalance(username, price);
}
}
1.4、测试类
@Test
public void test9() throws SQLException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
BookService bookService = context.getBean("bookService", BookService.class);
bookService.checkout("zhangsan",1);
}
总结:在事务控制方面,主要有两个分类:
编程式事务
在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法
声明式事务
在方法的外部添加注解或者直接在配置文件中定义,将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。spring的AOP恰好可以完成此功能:事务管理代码的固定模式作为一种横切关注点,通过AOP方法模块化,进而实现声明式事务。
2、声明式事务的简单配置
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
2.1、在配置文件中添加事务管理器
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd">
<!--先配置好context的命名空间,再配置property-placeholder后,才会有value的提示-->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--jdbcTemplate注册为bean对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
<!--<constructor-arg index="0" ref="dataSource"/>-->
</bean>
<!--组件扫描,包内所有的注解均生效,自动注入spring容器-->
<context:component-scan base-package="entity"/>
<!--事务控制-->
<!--配置事务管理器的bean-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启基于注解的事务控制模式,依赖tx名称空间-->
<!--
<tx:annotation-driven/>:支持事务注解的(@Transactional)
<mvc:annotation-driven/>:支持MVC注解
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
2.2、BookService添加事务注解
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账:传入哪个用户买了哪本书
*
* @param username
* @param id
*/
/**
* 事务配置的属性
isolation:设置事务的隔离级别
propagation:事务的传播行为
noRollbackFor:那些异常事务可以不回滚
noRollbackForClassName:填写的参数是全类名
rollbackFor:哪些异常事务需要回滚
rollbackForClassName:填写的参数是全类名
readOnly:设置事务是否为只读事务
timeout:事务超出指定执行时长后自动终止并回滚,单位是秒
*/
@Transactional
public void checkout(String username, int id) {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
//测试事务,添加事务注解后,就安全了,如果其中有一个失败就会回滚
//int i = 1/0;
bookDao.updateBalance(username, price);
}
}
3、测试超时属性
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账:传入哪个用户买了哪本书
*
* @param username
* @param id
*/
@Transactional(timeout=4)
public void checkout(String username, int id) {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bookDao.updateBalance(username, price);
}
}
4、设置事务只读
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
对于只读查询,可以指定事务类型为readonly,即只读事务。由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账:传入哪个用户买了哪本书
*
* @param username
* @param id
*/
@Transactional(timeout = 3,readOnly = true)
public void checkout(String username, int id) {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
bookDao.updateBalance(username, price);
}
}
5、设置哪些异常不回滚
注意:运行时异常默认回滚,编译时异常默认不回滚
回滚后数据库表数据不发生变化,不回滚数据库表数据发生变化
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账:传入哪个用户买了哪本书
*
* @param username
* @param id
* 两种设置异常不回滚方法均可,作用相同
*/
@Transactional(noRollbackFor = {ArithmeticException.class})
public void checkout(String username, int id) {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
int i = 1/0;
bookDao.updateBalance(username, price);
}
@Transactional(noRollbackForClassName = {"java.lang.ArithmeticException"})
public void checkout(String username,int id){
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
bookDao.updateBalance(username,price);
int i = 1/0;
}
}
6、设置哪些异常回滚
回滚后的数据库数据不发生变化
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账:传入哪个用户买了哪本书
*
* @param username
* @param id
*/
@Transactional(rollbackFor = {FileNotFoundException.class})
public void checkout(String username, int id) throws FileNotFoundException {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
bookDao.updateBalance(username, price);
//设置 FileNotFoundException 文件找不到异常
new FileInputStream("aaa.txt");
}
}
7、设置隔离级别
package entity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 隔离级别
* 1. @Transactional(isolation = Isolation.READ_UNCOMMITTED):
* 读取未提交数据(会出现脏读,不可重复读) 基本不使用
*
* 2. @Transactional(isolation = Isolation.READ_COMMITTED):
* 读取已提交数据(会出现不可重复读和幻读)
*
* 3. @Transactional(isolation = Isolation.REPEATABLE_READ):
* 可重复读(会出现幻读)
*
* 4. @Transactional(isolation = Isolation.SERIALIZABLE):串行化
*
* 隔离级别会引发3类问题
* 脏读
* 幻读
* 不可重复读
*/
@Transactional(isolation = Isolation.SERIALIZABLE)
public void checkout(String username, int id) throws FileNotFoundException {
bookDao.updateStock(id);
int price = bookDao.getPrice(id);
bookDao.updateBalance(username, price);
new FileInputStream("aaa.txt");
}
}
8、事务的传播特性
事务的传播特性指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行?
spring的事务传播行为一共有7种:
1.PROPAGATION_REQUIRED – 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
2.PROPAGATION_SUPPORTS – 支持当前事务,如果当前没有事务,就以非事务方式执行。
3.PROPAGATION_MANDATORY – 支持当前事务,如果当前没有事务,就抛出异常。
4.PROPAGATION_REQUIRES_NEW – 新建事务,如果当前存在事务,把当前事务挂起。
5.PROPAGATION_NOT_SUPPORTED – 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6.PROPAGATION_NEVER – 以非事务方式执行,如果当前存在事务,则抛出异常。
7.PROPAGATION_NESTED – 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
备注:常用的两个事务传播属性是1和4,即PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW
9、基于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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置组件扫描包,此包下的组件的注解均生效,自动注入spring容器中-->
<context:component-scan base-package="com.lian"/>
<!--绑定db.properties-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置druid数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<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="druidDataSource"/>
</bean>
<!--配置事务-->
<!--声明一个事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>
<!--
基于xml配置的事务:依赖tx名称空间和aop名称空间
1、spring中提供事务管理器(切面),配置这个事务管理器
2、配置出事务方法
3、告诉spring哪些方法是事务方法(事务切面按照我们的切入点表达式去切入事务方法)
-->
<aop:config>
<aop:pointcut id="txpointcut" expression="execution(* com.lian.service.*.*(..))"/>
<!--事务建议:advice-ref:指向事务管理器的配置-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="txpointcut"/>
</aop:config>
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<!--配置事务的属性-->
<tx:attributes>
<!--配置在哪些方法上添加事务-->
<tx:method name="addBook" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
<tx:method name="updatePrice" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>
</beans>