目录
(1)创建项目 spring-trans,引入 Spring Web,MyBatis,MySQL等依赖
(2)在需要事务的方法上添加 @Transactional 注解
一、事务回顾
事务是在数据库阶段学习的内容,也是数据库里的机制。
1、什么是事务?
事务是一组操作的集合,是一个不可分割的操作。
事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。
2、为什么需要事务?
我们在进行程序开发时,也会有事务的需求。
比如转账操作:
第一步:A 账户 - 100元。 第二步:B 账户 + 100元。
如果没有事务,第一步执行成功了,第二步执行失败了,那么 A账户的100元就平白无故消失了。如果使用事务就可以解决这个问题,让这个组操作要么一起成功,要么一起失败。
比如秒杀系统:
第一步:下单成功。 第二步:扣减库存。
下单成功后,库存也需要同步减少。如果下单成功,库存扣减失败,那么就会造成下单超出的情况。所以就需要把这两部操作放在同一个事务中。要么一起成功,要么一起失败。
这里理解事务概念为主,实际企业开发时,并不是简单的通过事务来处理。
3、事务的操作
事务的操作主要有三步:
1、开启事务 start transaction / begin(一组操作前开启事务)
2、提交事务:commit(这组操作全部成功,提交事务)
3、回滚事务:rollback(这组操作中间任何一个操作出现异常,回滚事务)
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
二、Spring 中事务的实现
Spring 对事务也进行了实现,Spring 中的事务操作分为两类:
1、编程式事务(手动写代码操作事务)。
2、声明式事务(利用注解自动开启和提交事务)。
在学习事务之前,我们先准备数据和数据的访问代码
需求:用户注册,注册时在日志表中插入一条操作记录。
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
use trans_test;
-- 用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
`id` INT NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR (128) NOT NULL,
`password` VARCHAR (128) NOT NULL,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '用户表';
-- 操作日志表
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info (
`id` INT PRIMARY KEY auto_increment,
`user_name` VARCHAR ( 128 ) NOT NULL,
`op` VARCHAR ( 256 ) NOT NULL,
`create_time` DATETIME DEFAULT now(),
`update_time` DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset 'utf8mb4';
1、代码准备:
(1)创建项目 spring-trans,引入 Spring Web,MyBatis,MySQL等依赖
(2)配置文件
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # 配置打印 MyBatis⽇志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #配置驼峰⾃动转换
(3)实体类
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Date createTime;
private Date updateTime;
}
import lombok.Data;
import java.util.Date;
@Data
public class LogInfo {
private Integer id;
private String userName;
private String op;
private Date createTime;
private Date updateTime;
}
(4)Mapper
UserInfoMapper
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info(user_name, password) values (#{name}, #{password})")
Integer insert(String name, String password);
}
LogInfoMapper
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info(user_name, op) values (#{name}, #{op})")
Integer insertLog(String name, String op);
}
(5)Service
UserService:
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public Integer registry(String name, String password) {
return userInfoMapper.insert(name, password);
}
}
LogService:
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
public void insertLog(String name, String op) {
//记录用户操作
logInfoMapper.insertLog(name, "用户注册");
}
}
(6)Controller
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/registry")
public Boolean registry(String name, String password) {
//用户注册
Integer ret = userService.registry(name, password);
return true;
}
}
2、Spring编程式事务(了解)
Spring 手动操作事务和上面 MYSQL操作事务类似,有3个重要操作步骤:
1、开启事务(获取事务)
2、提交事务
3、回滚事务
SpringBoot 内置了两个对象:
1、DataSourceTransactionManager 事务管理器。用来获取事务(开启事务),提交或回滚事务。
2、TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去,从而获得一个事务 TransactionStatus。
下面根据代码的实现来学习:
@RequestMapping("/user")
@RestController
public class UserController {
//JDBC 事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
//定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;
@RequestMapping("/registry")
public Boolean registry(String name, String password) {
//开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
//用户注册
Integer ret = userService.registry(name, password);
//提交事务
dataSourceTransactionManager.commit(transactionStatus);
//回滚事务
//dataSourceTransactionManager.rollback(transactionStatus);
return true;
}
}
(1)观察事务提交
//提交事务
dataSourceTransactionManager.commit(transactionStatus);
运行程序:127.0.0.1:8080/user/registry?name="lisi"&&password="123"
(2)观察事务回滚
//回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
运行程序:127.0.0.1:8080/user/registry?name="zhangsan"&&password="1234"
数据库没多出数据:
和事务提交相比,日志少了一行。
以上代码虽然可以实现事务,但操作也很繁琐,有没有更简单的实现方法呢?接下来我们学习声明式事务(注解)。
3、Spring 声明式事务 @Transactional
声明式事务的实现很简单,两步操作:
(1)添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
(2)在需要事务的方法上添加 @Transactional 注解
无需手动开启事务和提交事务,进入方法时自动开启事务。方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务。
我们来看代码的实现:
@RequestMapping("/trans")
@RestController
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
Integer ret = userService.registry(name, password);
if(ret > 0) return "注册成功";
return "注册失败";
}
}
运行程序:127.0.0.1:8080/trans/registry?name="wangwu"&&password="12345",发现数据插入成功。
现在修改程序,使之出现异常:
@RequestMapping("/trans")
@RestController
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
Integer ret = userService.registry(name, password);
//强制程序抛出异常
int a = 10/0;
if(ret > 0) return "注册成功";
return "注册失败";
}
}
运行程序:127.0.0.1:8080/trans/registry?name="wangwu"&&password="12345",发现数据插入失败。事务发生回滚了。
上面因为有异常,事务回滚了。
我们一般会在业务逻辑层当中来控制事务,因为在业务逻辑层当中,一个业务功能可能会包含多个数据访问的操作。在业务逻辑层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。上述代码在Controller中书写,只是为了方便学习。
(3)Transactional 作用
@Transactional 可以用来修饰方法或类:
1、修饰方法时:只有修饰 public 方法时才生效(修饰其他方法时不会报错,也不生效)。[推荐]
2、修饰类时:对 @Transactional 修饰的类中所有的 public 方法都生效。
方法 / 类被 @Transactional 注解修饰时,在目标方法执行开始前,会自动开启事务,方法执行结束之后,自动提价事务。
如果在方法执行过程中,出现异常,且异常未被捕获,就进行事务回滚操作。
如果异常被程序捕获,方法就被认为是成功执行,依然会提交事务。
修改上述代码,对异常进行捕获:
@RequestMapping("/trans")
@RestController
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
Integer ret = userService.registry(name, password);
//捕获异常
try {
int a = 10/0;
} catch (Exception e) {
e.printStackTrace();
}
if(ret > 0) return "注册成功";
return "注册失败";
}
}
运行程序:127.0.0.1:8080/trans/registry?name="zhaoliu"&&password="12345",数据库新增了一行数据
说明事务没有回滚,而是提交事务了。
上面运行程序,发现虽然程序出错了,但是由于异常被捕获了,所以事务依然得到了提交。如果需要事务进行回滚,有以下两种方式:
1、重新抛出异常
2、手动回滚事务
1、重新抛出异常
@RequestMapping("/trans")
@RestController
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
Integer ret = userService.registry(name, password);
//强制程序抛出异常
try {
int a = 10/0;
} catch (Exception e) {
// e.printStackTrace();
//将异常抛出去
throw e;
}
if(ret > 0) return "注册成功";
return "注册失败";
}
}
运行程序:127.0.0.1:8080/trans/registry?name="zhaoliu"&&password="12345",发现
手动抛出异常后,说明事务回滚了。(异常并没有被捕获)
2、手动回滚事务
使用 TransactionAspectSupport.currentTransactionStatus() 得到当前事务,并使用 SetRollbackOnly 设置 setRollbackOnly。
@RequestMapping("/trans")
@RestController
public class TransactionalController {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/registry")
public String registry(String name, String password) {
//用户注册
Integer ret = userService.registry(name, password);
//强制程序抛出异常
try {
int a = 10/0;
} catch (Exception e) {
// e.printStackTrace();
//将异常抛出去
// throw e;
//手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
if(ret > 0) return "注册成功";
return "注册失败";
}
}
运行程序:127.0.0.1:8080/trans/registry?name="zhaoliu"&&password="12345",发现事务回滚了。