01 引言
Spring
事务是什么?Spring事务是一个并发控制的单位,开启事务之后,在同一个事务里的一系列操作要么全成功、要么全失败。简单来讲就是一损俱损,一荣俱荣。
举一个经典的银行转账案例,用户A给用户B赚了100元,实际的操作拆解为2步:
- 第一:用户A的账户余额需要减少100元
- 第二:用户B的账户余额需要增加100元
这两部操作,只有全部成功才会真正完成。倘若第一步成功了,用户A的账户余额减少了,然而第二步失败了,用户B就收不到钱,那么就会出现严重的问题。事务就可以很好的解决这类问题,要么都成功,要么都不成功才是我们想看到的结果。
02 事务的特点
事务主要有四个特性:原子性、一致性、隔离性、持久性。也是我们常说的ACID。
-
原子性(
Atomicity
):事务中的所有操作,或者全部完成,或者全部不完成,不会停止在在中间某个环节。
-
一致性(
Consistency
):在事务开始之前和事务结束以后,保证数据库的完整性没有被破坏。例如上述案例中两个用户相互转账,不管怎么转,两个账户的总额一定不会变。
-
隔离性(
Isolation
):多个事务之间是独立的,不相互影响的。
-
持久性(
Durability
):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
03 事务的分类
事务的本质是数据库的一种机制,有些数据库支持事务,有些不支持事务。数据库也就是我们常说的数据源。而Spring
事务则是Spring
对数据源的一种支持。
Spring
事务分为两类:声明式事务和编程式事务。
3.1 代码准备
在介绍声明式事务和编程式事务之前,我们先准备一些公用代码。
所需依赖
<!-- mysql驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JDBC工具 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
接口方法
public interface UserInfoService {
void save(List<UserInfo> list);
}
方法实现
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private JdbcTemplate jdbcTemplate;
//@Transactional
@Override
public void save(List<UserInfo> list) {
if (!CollectionUtils.isEmpty(list)) {
for (int i = 0; i < list.size() ; i++) {
if (1 == i) {
System.out.println("异常出现的位置:" + i);
throw new RuntimeException("异常出现了,脚标:" + i);
}
UserInfo userInfo = list.get(i);
String sql = """
insert into user_info(name,age,sex,job,birthday,created_time,update_time)
values(?,?,?,?,?,?,?)
""";
jdbcTemplate.update(sql, userInfo.getName(), userInfo.getAge(), userInfo.getSex(), userInfo.getJob(), userInfo.getBirthday(), userInfo.getCreatedTime(), userInfo.getUpdateTime());
}
}
}
}
3.2 声明式事务
声明式事务是借助注解@Transactional
实现的,Spring boot中
要想开启事务就需要声明@Transactional
注解。事务支持的依赖来自spring-tx
。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
不加事务的案例
假设我们需要插入5条数据:用户名称为哪吒0
到哪吒4
。
在插入第一条之后,模拟异常。看看最终插入了几条数据。
最终的结果直插入一条数据,异常之后的数据都,没有进入数据库。
添加事务
删除刚才插入的数据。方法增加@Transactional
。
结果就会发现,一条记录都没有入库。这就是事务的魅力。
事务回滚的异常限制
案例我们模拟的异常是RuntimeException
,如果是其他异常会不会回滚呢?我们需要了解一下@Transactional
注解的一个属性rollbackFor
注释里指出默认只回滚RuntimeException
和Error
两种。看看具体的实现:
了解其属性之后,我们如何突破这种限制呢?只需要配置该属性,指定异常的类型即可,如:
@Transactional(rollbackFor = Exception.class)
3.3 编程式事务
编程式事需要侵入到了业务代码里面,类似try...catch
。因为侵入到业务代码,所以使用上更加加灵活。Spring
提供两种方式的编程式事务管理:TransactionTemplate
和PlatformTransactionManager
。
TransactionTemplate
使用
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public void save(List<UserInfo> list) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@SneakyThrows
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (!CollectionUtils.isEmpty(list)) {
for (int i = 0; i < list.size(); i++) {
if (1 == i) {
System.out.println("异常出现的位置:" + i);
throw new Exception("异常出现了,脚标:" + i);
}
UserInfo userInfo = list.get(i);
String sql = """
insert into user_info(name,age,sex,job,birthday,created_time,update_time)
values(?,?,?,?,?,?,?)
""";
jdbcTemplate.update(sql, userInfo.getName(), userInfo.getAge(), userInfo.getSex(), userInfo.getJob(), userInfo.getBirthday(), userInfo.getCreatedTime(), userInfo.getUpdateTime());
}
}
}
});
}
其中包含两个方法:
TransactionCallbackWithoutResult
TransactionCallback
分别来处理有没有返回值的情况。
PlatformTransactionManager
因为TransactionTemplate
本身也是使用了PlatformTransactionManager
,如果使用它就需要我们手动控制提交还是回滚。
3.4 两种事务类型对比
编程式事务和声明式事务的最终达到的结果是一样的,但是编程式事务更加细粒度的事控制事务。但是代码的侵入性相对较高。
声明式事务一般属于大事务,最终取决于方法拆分的够不够细。企业中大事务坑会造成内存溢出、系统假死的想象。使用应当谨慎!
04 小结
上面分享了事务的基本概念以及两种事务的类型,以及使用案例。使用的时候选择合适的事务类型。
事务有好有坏,事务可以帮助我们实现数据的一致性。但是占用的资源较多,可能会影响系统的可用性。大事务容易造成假死或内存溢出,需要细粒度的控制事务的使用。