事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成,它具有ACID特性。
为了在spring中更好的使用事务处理,首先介绍Spring中用于数据库操作的核心模板类JDBCTemplate
。
JdbcTemplate是Spring框架中用于数据库操作的核心模板类。它封装了JDBC API,简化了数据库操作,并且提供了异常处理等功能。
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>6.0.9version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.16version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.33version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.10.0-M1version>
<scope>compilescope>
dependency>
jdbc.username=root
jdbc.password=zkpk
jdbc.url=jdbc:mysql://localhost:3306/spring
jdbc.driver=com.mysql.cj.jdbc.Driver
<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>
<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>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource">property>
bean>
beans>
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;
@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*/
}
数据库事务(transaction)是访问并可能操作各个数据项的一个数据库操作序列,这些操作要么全部执行,要么都不执行,是一个不可分割的单位。事务由事务开始与事务结束事件执行的全部数据库操作组成。
原子性(atomicity)
:一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
一致性(consistency)
:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation)
:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability)
:持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
在Spring体系中,关于事务的管理有两种模式,分别是编程式事务和声明式事务。
事务功能的相关操作全部通过自己编写代码实现。
细节没有被屏蔽
:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
代码复用性不高
:如果没有有效抽取出来,每个实现功能都需要自己编写代码,代码没有得到复用。
将固定功能的代码进行封装,使用者只需要在配置文件中进行简单的配置即可完成操作。
情景:用户买书过程
<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>
<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>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="druidDataSource">property>
bean>
beans>
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);
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);
}
}
@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)'
可以使用添加事务的方式解决上面出现的问题。
在spring配置文件中添加
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource">property>
bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
因为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注解标识的位置
:标识在方法上,则只会影响该方法,标识在类上,则会影响类中所有的方法。
在数据异常情况下同样会报相同错误,但表中数据不会被修改。
Data truncation: BIGINT UNSIGNED value is out of range in '(`spring`.`t_user`.`balance` - xxx)'
readOnly
:只读,设置为true的时候,只能查询、不能修改和删除。
当数据库进行改、写操作会抛出异常。
timeout
:超时,在设置的超时时间之内没有完成,抛出异常并回滚。
回滚策略,设置哪些异常回滚。
rollbackFor
:需要设置一个异常的Class类型的对象
rollbackForClassName
:需要设置一个字符串类型的全类名
noRollbackFor
:需要设置一个Class类型的对象
norollbackForClassName
:需要设置一个字符串类型的全类名
isolation
:设置隔离级别,解决读的问题。
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);
}
创建模块,导入相关的依赖
①开启组件扫描
②创建数据源
③创建JdbcTemplate
,注入数据源
④创建事务管理器,注入数据源
⑤配置事务通知
⑥配置切入点表达式,把事务添加到方法上。
<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>
<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>
<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的依赖。)
@SpringJUnitConfig(locations = "classpath:bean-xml.xml")
public class TestXmlTx {
@Autowired
private BookController bookController;
@Test
public void testXml(){
bookController.buyBook(1,1);
}
}