spring-事务
事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成,它具有ACID特性。
为了在spring中更好的使用事务处理,首先介绍Spring中用于数据库操作的核心模板类JDBCTemplate
。
JDBCTemplate
JdbcTemplate是Spring框架中用于数据库操作的核心模板类。它封装了JDBC API,简化了数据库操作,并且提供了异常处理等功能。
使用步骤
1、依赖引入
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0-M1</version>
<scope>compile</scope>
</dependency>
2、创建jdbc.properties文件
jdbc.username=root
jdbc.password=zkpk
jdbc.url=jdbc:mysql://localhost:3306/spring
jdbc.driver=com.mysql.cj.jdbc.Driver
3、创建spring配置文件
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--引入外部属性文件,创建数据源对象-->
<context:property erty-placeholder>
<!--创建Druid连接池的数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--创建jdbcTemplate对象,注入数据源-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
</beans>
4、创建表并加入数据
DROP TABLE IF EXISTS `t_emp`;
CREATE TABLE `t_emp` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int NULL DEFAULT NULL COMMENT '年龄',
`sex` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '性别',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
5、实现CRUD
增删改操作
@SpringJUnitConfig(locations = "classpath:bean.xml")
public class JDBCTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
//添加、修改和删除
@Test
public void testUpdate(){
/* //1、添加操作
//①编写sql
String sql = "insert into t_emp values(null, ?, ?, ?)";
//②调用jdbcTemplate中的方法,传入相关的参数
// Object[] params = {"小明", 23, "男"};
int effect = jdbcTemplate.update(sql, params);
int effect1 = jdbcTemplate.update(sql, "小明", 23, "男");
int effect2 = jdbcTemplate.update(sql, "louie", 24, "男");
int effect3 = jdbcTemplate.update(sql, "Alex", 22, "男");
// System.out.println("effect = " + effect1);
/*effect = 1*/
/*effect1 = 1*/
/* //2、修改操作
String sql = "update t_emp set name = ? where id = ?";
int row = jdbcTemplate.update(sql, "Khan", 2);
System.out.println("row = " + row);
/*row = 1*/
//3、删除
String sql = "delete from t_emp where id = ?";
int delete = jdbcTemplate.update(sql, 2);
System.out.println("delete = " + delete);
/*delete = 1*/
}
}
查询操作
//查询返回一个对象
@Test
public void testSelectObject(){
String sql = "select * from t_emp where id = ?";
/*//写法一
//RowMapper用来对象封装
Emp empResult = jdbcTemplate.queryForObject(sql,
(rs, rowNum) -> {
Emp emp = new Emp();
emp.setId(rs.getInt("id"));
emp.setName(rs.getString("name"));
emp.setAge(rs.getInt("age"));
emp.setSex(rs.getString("sex"));
return emp;
}, 1);
System.out.println("empResult = " + empResult);*/
/*empResult = Emp{id=1, name='小明', age=23, sex='男'}*/
//写法二
Emp result = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Emp.class), 1);
System.out.println(result);
/*Emp{id=1, name='小明', age=23, sex='男'}*/
}
//查询返回list集合
@Test
public void testSelectList(){
String sql = "select * from t_emp";
List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));
for (Emp emp : list) {
System.out.println("emp = " + emp);
}
/*
emp = Emp{id=1, name='小明', age=23, sex='男'}
emp = Emp{id=3, name='louie', age=24, sex='男'}
emp = Emp{id=4, name='Alex', age=22, sex='男'}
* */
}
//返回单个值
@Test
public void testSelectOne(){
String sql = "select count(1) from t_emp";
Integer sum = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("sum = " + sum);
/*sum = 3*/
}
事务的概念及Spring事务的管理模式
事务的基本概念
数据库事务(transaction)是访问并可能操作各个数据项的一个数据库操作序列,这些操作要么全部执行,要么都不执行,是一个不可分割的单位。事务由事务开始与事务结束事件执行的全部数据库操作组成。
特性(ACID)
原子性(atomicity)
:一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
一致性(consistency)
:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation)
:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability)
:持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
Spring事务的管理模式
在Spring体系中,关于事务的管理有两种模式,分别是编程式事务和声明式事务。
编程式事务
事务功能的相关操作全部通过自己编写代码实现。
缺点
细节没有被屏蔽
:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
代码复用性不高
:如果没有有效抽取出来,每个实现功能都需要自己编写代码,代码没有得到复用。
声明式事务
将固定功能的代码进行封装,使用者只需要在配置文件中进行简单的配置即可完成操作。
优点
- 高开发效率
- 消除了冗余代码
- 框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行健壮性、性能等各个方面的优化。
基于注解的声明式事务
情景:用户买书过程
实现步骤
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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.louis.affair"></context:component-scan>
<!--引入外部属性文件,创建数据源对象-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--创建Druid连接池的数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--创建jdbcTemplate对象,注入数据源-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
</beans>
2、创建表
t_book
DROP TABLE IF EXISTS `t_book`;
CREATE TABLE `t_book` (
`book_id` int NOT NULL COMMENT '主键',
`book_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图书名称',
`price` int NULL DEFAULT NULL COMMENT '价格',
`stock` int UNSIGNED NULL DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `t_book` VALUES (1, '机器学习', 120, 100);
INSERT INTO `t_book` VALUES (2, 'KAfka', 80, 100);
t_user
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`user_id` int NOT NULL COMMENT '主键',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
`balance` int UNSIGNED NULL DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `t_user` VALUES (1, 'Louie', 500);
3、创建controller、service和dao层
controller
@Controller
public class BookController {
@Autowired
private BookService bookService;
//买书的方法:图书id和用户id
public void buyBook(Integer bookId, Integer userId){
//调用service方法
bookService.buyBook(bookId, userId);
}
}
service
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
//根据图书id查询图书价格
Integer price = bookDao.selectBookById(bookId);
//更新图书库存
bookDao.updateBookById(bookId);
//更新用户表余额
bookDao.updateUserBalance(userId, price);
}
}
dao
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int selectBookById(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class, bookId);
return price;
}
@Override
public void updateBookById(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}
@Override
public void updateUserBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql, price, userId);
}
}
4、测试
@SpringJUnitConfig(locations = "classpath:bean.xml")
public class TestAffair {
@Autowired
private BookController controller;
@Test
public void testAffair(){
controller.buyBook(1, 1);
}
}
但是当用户余额不足时:(设置用户余额为50, 设置图书库存均为100)
再次运行会出现类似下面的异常情况。
Data truncation: BIGINT UNSIGNED value is out of range in '(`spring`.`t_user`.`balance` - xxx)'
可以使用添加事务的方式解决上面出现的问题。
步骤
1、添加事务配置
在spring配置文件中添加
<!--事务控制的配置-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--
开启事务的注解驱动
通过注解@Transactional所标识的方法或标识的类中所由的方法,都会被事务管理器管理事务
-->
<!--transaction-manager属性的默认值时transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性-->
<tx:annotation-driven transaction-manager="transactionManager"/>
2、添加事务注解
因为service层标识业务逻辑层,一个方法标识一个完整的功能,因此处理事务一般在service层处理,在相关业务操作上添加注解@Transactional。
@Override
@Transactional
public void buyBook(Integer bookId, Integer userId) {
//根据图书id查询图书价格
Integer price = bookDao.selectBookById(bookId);
//更新图书库存
bookDao.updateBookById(bookId);
//更新用户表余额
bookDao.updateUserBalance(userId, price);
}
@Transactional注解标识的位置
:标识在方法上,则只会影响该方法,标识在类上,则会影响类中所有的方法。
3、测试(恢复库存条件下)
在数据异常情况下同样会报相同错误,但表中数据不会被修改。
Data truncation: BIGINT UNSIGNED value is out of range in '(`spring`.`t_user`.`balance` - xxx)'
@Transactional属性
readOnly
:只读,设置为true的时候,只能查询、不能修改和删除。
当数据库进行改、写操作会抛出异常。
timeout
:超时,在设置的超时时间之内没有完成,抛出异常并回滚。
回滚策略,设置哪些异常回滚。
rollbackFor
:需要设置一个异常的Class类型的对象
rollbackForClassName
:需要设置一个字符串类型的全类名
noRollbackFor
:需要设置一个Class类型的对象
norollbackForClassName
:需要设置一个字符串类型的全类名
isolation
:设置隔离级别,解决读的问题。
propagation
:传播行为,事务方法之间的调用,事务如何使用。
传播行为(propagation)
Spring定义了一个枚举,一共有七种传播行为。
REQUIRED
:spring默认的事务传播行为,A方法调用B方法,如果A方法有事务,则B方法加入到A方法中的事务中,否则B方法自己开启一个新事务。
SUPPORTS
:A方法调用B方法,如果A方法有事务,则B方法加入到A方法中的事务中,否则B方法自己使用非事务方式执行。
MANDATORY
:只能在存在事务的方法中被调用,A方法调用B方法,如果A方法没事务,则B方法会抛出异常。
REQUIRES_NEW
:A方法调用B方法,如果A方法有事务,则B方法把A方法的事务挂起,B方法自己重新开启一个新事务。
NOT_SUPPORTED
:A方法调用B方法,如果A方法有事务,则B方法挂起A方法中的事务中,否则B方法自己使用非事务方式执行。
NEVER
:不支持事务,A方法调用B方法,如果A方法有事务,则B方法会抛出异常
NESTED
:同 Propagation.REQUIRED
,不过此传播属性还可以 保存状态节点,从而避免所有嵌套事务都回滚。
事务传播
添加用户多购买类
CheckoutServiceImpl
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
@Transactional()
public void buyBook(Integer bookId, Integer userId) {
//根据图书id查询图书价格
Integer price = bookDao.selectBookById(bookId);
//更新图书库存
bookDao.updateBookById(bookId);
//更新用户表余额
bookDao.updateUserBalance(userId, price);
}
}
添加Service实现
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Autowired
private BookService bookService;
/**
* 顾客买多本书
* @param bookIds
* @param userId
*/
@Transactional
@Override
public void checkout(Integer[] bookIds, Integer userId) {
for(Integer bookId:bookIds){
//调用业务逻辑层的方法
bookService.buyBook(bookId, userId);
}
}
}
测试
@SpringJUnitConfig(locations = "classpath:bean.xml")
public class TestAffair {
@Autowired
private BookController controller;
// @Test
// public void testAffair(){
// controller.buyBook(1, 1);
// }
@Test
public void testTransaction(){
Integer[] bookIds = {1, 2};
controller.checkout(bookIds, 1);
}
}
设置用户余额为150,设置事务传播属性为默认值去购买两本书,但是当前的余额只够买一本书的时候会报错,并且进行了回滚操作。如果设置事务传播属性为REQUIRES_NEW,则第一本书会成功购买。
全注解配置事务
配置类
@Configuration//表示这是一个配置类
@ComponentScan("com.louis.affair")
@EnableTransactionManagement//表示开启事务管理
public class SpringConfig {
//连接池
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring");
dataSource.setUsername("root");
dataSource.setPassword("zkpk");
return dataSource;
}
//jdbcTemplate部分
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
//事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
测试
@Test
public void testAllAnnotation(){
AnnotationConfigApplicationContext annotationConfig = new AnnotationConfigApplicationContext(SpringConfig.class);
BookController bookController = annotationConfig.getBean(BookController.class);
Integer[] bookIds = {1,2};
bookController.checkout(bookIds, 1);
}
基于XML的声明式事务(具体实现)
步骤
1、环境准备
创建模块,导入相关的依赖
2、创建spring配置文件
①开启组件扫描
②创建数据源
③创建JdbcTemplate
,注入数据源
④创建事务管理器,注入数据源
⑤配置事务通知
⑥配置切入点表达式,把事务添加到方法上。
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.louis.affair.xmltx"></context:component-scan>
<!--创建数据源对象-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
</bean>
<!--创建JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入数据源-->
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"></property>
</bean>
<!--配置事务增强(通知)-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--设置方法的规则. 例:表示以get开头的方法-->
<tx:method name="buy*" read-only="true"/>
<tx:method name="update*" read-only="false" propagation="REQUIRED"></tx:method>
</tx:attributes>
</tx:advice>
<!--配置切入点和通知使用的方法-->
<aop:config>
<aop:pointcut id="pt" expression="execution(* com.louis.affair.xmltx.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>
</beans>
设置用户余额为50,书库存均为100,(要记得添加aspectJ的依赖。)
3、测试
@SpringJUnitConfig(locations = "classpath:bean-xml.xml")
public class TestXmlTx {
@Autowired
private BookController bookController;
@Test
public void testXml(){
bookController.buyBook(1,1);
}
}