Spring数据库事务处理

news/2024/12/3 1:43:47/

数据库事务的基本知识

ACID

在这里插入图片描述

两类丢失更新

事务回滚丢失更新:

在这里插入图片描述
目前大部分数据库已经通过锁的机制来避免了事务回滚丢失更新。
数据库锁的机制:
锁可以分为乐观锁和悲观锁,而悲观锁又分为:读锁(共享锁)和写锁(排它锁),而数据库实现了悲观锁中的读锁和写锁,而乐观锁则需要开发人员自己实现。

数据库在设计这两种锁的时候,这两种锁间的关系如下:读锁与读锁可以共存,读锁与写锁互斥,写锁与写锁互斥。

比如说,当a操作某条数据时,数据库就会给这条数据加锁,其他人只能查看这条数据,但是却不能操作,只有当a事务提交结束以后,锁被取消了,其他人才可以修改这条数据。

事务提交丢失更新

在这里插入图片描述
这是高并发编程中需要重点关注的问题,数据库为了压制此类丢失更新,提出了事务之间的隔离级别的概念。

数据库事务的隔离级别

未提交读

允许一个事务读取到另一个事务没有提交的数据。
这种隔离级别可以拥有很好的并发能力,但是对于数据的一致性无法保证,所以适用于追求高并发性,但是对数据一致性要求低的场景。
另外,未提交读的隔离级别会造成脏读的现象:
在这里插入图片描述

读写提交

一个事务只能读取另一个事务已经提交的数据,而不能读取未提交的数据。
这种隔离级别可以避免脏读的发生。
在这里插入图片描述
虽然读写提交的隔离级别克服了脏读的发生,但是又会出现不可重复读的现象:
在这里插入图片描述

可重复读

可重复读的隔离级别是为了克服不可重复读的问题:
在这里插入图片描述
可重复读的隔离级别虽然克服了不可重复读的问题,但是会引入幻读的问题:
在这里插入图片描述
幻读和可重复读的区别:
幻读是针对于统计的场景,而可重复读是针对于一条数据而言的。

串行化

为了解决上述的各种问题,数据库提出了串行化的隔离级别。
这种级别下,所有的sql都会按照顺序执行,可以完全保证数据的一致性。

合理使用数据库隔离级别

在这里插入图片描述
虽然串行化可以解决脏读、不可重复读、幻读等问题,但是它也有一个很显著的特点,就是并发能力低下,这四种隔离级别的并发能力排名如下:
未提交读 > 读写提交 > 可重复读 > 串行化
所以使用时需要根据具体的业务场景来权衡使用。

另外,不同数据对于隔离级别的支持也是不一样的,比如:
Oracle:读写提交、串行化
MySQL:未提交读、读写提交、可重复读、串行化
PG:读写提交、可重复读、串行化

Spring数据库事务简介:

在 Spring 中,事务管理器的顶层接口为PlatformTransactionManager,Spring 还为此定义了一系列的接口和类,它们之间的关系如图所示:
在这里插入图片描述
当我们引入其它框架时,还会有其它的事务管理器的类,比方说我们引入 Hibernate ,那么 Spring还会提供HibernateTransactionManager 与之对应并给我们使用 。这里我们以 MyBatis 框架为例,去讨论Spring 数据库事务方面的问题,最常用到的事务管理器是 DataSourceTransactionManager 。从上图中可以看到它也是一个实现了接口 PlatfonnTransactionManager 的类。

PlatfonnTransactionManager接口的源码:

下面我们看一下PlatfonnTransactionManager接口的源码:

public interface PlatformTransactionManager {// 获取事务TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;// 事务提交void commit(TransactionStatus status) throws TransactionException;// 事务回滚void rollback(TransactionStatus status) throws TransactionException;
}

可以看到它里面定义了获取事务、提交事务、回滚事务的方法。而mybatis的事务管理器DataSourceTransactionManager实现了PlatformTransactionManager 接口,所以它也拥有了这些方法。

事务的传播行为

所谓传播行为,就是方法之间调用时,事务采取的策略。

在Spring的事务机制中,对于数据库而言存在7中传播行为,它们都被定义在Propagation枚举中,该枚举源码如下:

public enum Propagation {// 需要事务,它也是默认的传播行为// 如果当前存在事务,就加入到当前事务中一起运行// 如果当前不存在事务,就新建一个事务来运行方法// 使用频次高REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),// 支持事务// 如果当前存在事务,就加入到当前事务中一起运行// 如果不存在,就继续采用无事务的方式运行SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),// 必须使用事务// 如果当前存在事务,就加入到当前事务中一起运行// 如果不存在事务,就抛出异常MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),// 无论当前是否存在事务,都会创建一个新的事务来运行// 使用频次高REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),// 不支持事务// 如果当前存在事务,将挂起事务运行NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),// 不支持事务// 如果存在事务,就抛出异常// 如果不存在,就继续采用无事务的方式运行NEVER(TransactionDefinition.PROPAGATION_NEVER),// 事务嵌套// 当前方法调用子方法时,如果子方法出现异常,则只回滚子方法执行过的sql,不会回滚当前方法的事务// 使用频次高NESTED(TransactionDefinition.PROPAGATION_NESTED);
}

REQUIRED、REQUIRES_NEW、NESTED这三种使用频次较高,需要重点关注。

@Transactional注解

Spring对于事务的处理主要采用声明式事务的方式,也就是通过@Transactional注解来进行数据库事务的管理。
下面是它的源码:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {// 通过bean的name来指定事务管理器@AliasFor("transactionManager")String value() default "";// 和value属性一样@AliasFor("value")String transactionManager() default "";// 设置事务传播行为Propagation propagation() default Propagation.REQUIRED;// 设置事务隔离级别Isolation isolation() default Isolation.DEFAULT;// 指定超时时间(单位:秒)int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;// 是否只读事务boolean readOnly() default false;// 方法在发生指定异常时进行回滚,默认是所有异常都会回滚Class<? extends Throwable>[] rollbackFor() default {};// 方法在发生指定异常名称时进行回滚,默认是所有异常都会回滚String[] rollbackForClassName() default {};// 方法在发生指定异常时不进行回滚,默认是所有异常都会回滚Class<? extends Throwable>[] noRollbackFor() default {};// 方法在发生指定异常名称时不进行回滚,默认是所有异常都会回滚String[] noRollbackForClassName() default {};
}

可以看到,我们在使用@Transactional注解时,是可以自己设置事务的隔离级别、传播行为、回滚机制等等。

@Transactional实战

下面让我们一起实际应用下@Transactional对于数据库事务的控制。
首先我们创建一个保存数据库的,代码如下:

REQUIRED传播行为测试

@Service
@Slf4j
public class UserServiceImpl implements IUserService {......@Autowired@Lazyprivate IUserService userService;@Override@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public int insert(User user) {long oldId = user.getId();user.setId(null);userMapper.insert(user);if(oldId == 10){throw new IllegalArgumentException("事务回滚测试" + user.getId());}user.setUpdatedBy(10288931L);return userMapper.update(user);}@Overridepublic boolean save(int times) {for(int timesTmp = 1; timesTmp <= times; timesTmp++){User user = easyRandom.nextObject(User.class);user.setId((long) timesTmp);try {userService.insert(user);}catch (Exception e){log.warn("出现了异常:{}", e.getMessage());}}return true;}
}

可以看到,我们将方法insert的事务传播行为设置成了REQUIRED,也就是:
// 需要事务,它也是默认的传播行为
// 如果当前存在事务,就加入到当前事务中一起运行
// 如果当前不存在事务,就新建一个事务来运行方法
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

注意:这里调用insert方法时需要通过代理对象调用,因为Spring对于事务的处理是基于AOP实现的,所以如果不通过代理对象调用,就无法触发事务,也就是事务会失效。

然后将日志级别设置为DEBUG,这样就可以看到事务相关的日志了:

logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG

按照REQUIRED传播行为的特点,此时调用它的save方法是没有事务的,所以insert方法会单独创建自己的事务来运行,然后我们创建测试用的mapper和controller(这两个就省略了,写法都很简单),调用接口,可以看到日志如下:
在这里插入图片描述
可以看到我新增了5个user信息,日志中也是给insert方法创建了5个事务来运行,这和REQUIRED传播行为的特点是一致的。

此时我们给save方法也加上日志,如下所示:
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)

这个时候,对于insert方法来说,就是有事务的环境了,按照REQUIRED传播行为的特点,insert方法就不会自己单独创建事务了,而是沿用save方法的事务,我们再次调用接口,得到日志如下:

在这里插入图片描述
可以看到只是给save方法创建了事务,并没有给insert方法单独创建事务
在这里插入图片描述
可以看到insert方法运行时,是加入到原有的事务之中的,这和REQUIRED传播行为的特点是一致的。

REQUIRES_NEW传播行为测试

现在我们将insert方法的事务传播行为设置为REQUIRES_NEW,其特点如下所示:
// 无论当前是否存在事务,都会创建一个新的事务来运行

接下来我们再调用一下接口,得到如下日志:
在这里插入图片描述
可以看到这里是创建了6个事务,其中有一个是save方法的事务,其他5个都是insert方法自己的事务。这和REQUIRED传播行为的特点是一致的。

NESTED传播行为测试

// 事务嵌套
// 如果没有事务,则创建一个自己的事务
// 如果当前有事务,则创建一个嵌套的事务
// 当前方法调用子方法时,如果子方法出现异常,则只回滚子方法执行过的sql,不会回滚当前方法的事务

更改insert方法的事务如下:
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.NESTED, rollbackFor = Exception.class)

同时去掉save方法的事务,再次调用接口,得到如下日志:
在这里插入图片描述
此时save方法没有添加事务,所以按照NESTED传播行为的特点,insert方法会创建自己的事务。

然后我们再将save方法添加上事务,再次调用接口,得到日志如下:
在这里插入图片描述
在这里插入图片描述
因为此时save方法添加了事务,所以insert方法会创建一个嵌套在save方法里面的事务。

有个注意点:对于嵌套事务来说,当前方法调用子方法时,如果子方法出现异常,则只回滚子方法执行过的sql,不会回滚当前方法的事务,这个效果是通过数据库的保存点技术来实现的,至于数据库的保存点技术,可以自行了解一下。

好了,今天就到这里了,感兴趣的小伙伴赶紧去试试吧,拜拜。


http://www.ppmy.cn/news/179693.html

相关文章

Y480安装联想一键恢复

或多或少有人错手把一键还原弄没了&#xff0c;新机似乎少了这个东西总觉得不完美…… 当你看到自己的一键还原按键变成了另一个开机键&#xff0c;当你为你的新机失去这个一键还原而感到无奈&#xff0c;请认真看下面的文字&#xff01; 先送上下载一键还原7.0的地址吧htt…

有关联想拯救者Y7000重装window10系统

文章目录 1 联想拯救者使用U盘重装系统不需要进入bios2 总结 由于C盘爆满了&#xff0c;所以选择重装系统来重新给C盘分下区&#xff0c;给C盘分大点。然后重装系统的具体流程参照的是博客使用U盘重装Windows10系统详细步骤及配图【官方纯净版】。然后写这个博客的目的是记录一…

ORTP库局域网图传和VLC实时预览

​ 1.ORTP的引入 1.1、视频网络传输的2种方式 (1)基于下载&#xff1a;http or ftp&#xff08;网站播放视频&#xff0c;追求清晰度&#xff0c;哪怕时间晚一点&#xff09; (2)基于实时&#xff1a;RTP/RTSP/RTCP&#xff08;直播、监控&#xff0c;追求实时&#xff0c;…

哪款蓝牙耳机性价比最高?2023性价比高的蓝牙耳机推荐

近年来&#xff0c;随着蓝牙耳机市场的快速膨胀&#xff0c;蓝牙耳机的使用频率越来越高。那么&#xff0c;在众多的蓝牙耳机当中&#xff0c;哪款蓝牙耳机性价比最高&#xff1f;下面&#xff0c;我来给大家推荐几款性价比高的蓝牙耳机。 一、南卡小音舱蓝牙耳机 售价&#…

无线蓝牙耳机哪款性价比最高?2022蓝牙耳机性价比排行榜

如今购买蓝牙耳机的人是越来越多了&#xff0c;也是由此证明了无线蓝牙耳机在当下市场的火爆性&#xff0c;这主要还是因为蓝牙耳机外出方便携带的原因吧。但也因市面上的蓝牙耳机太多了&#xff0c;使得很多小伙伴们在挑选的时候会比较迷茫&#xff0c;不过不要担心&#xff0…

高性价比真无线耳机哪款好?2022性价比蓝牙耳机推荐

性价比高的蓝牙耳机有哪些&#xff1f;高性价比高的东西最终总能让人产生购买欲望。这几款高性价比蓝耳机都是性价比蓝牙耳机排行榜的前几名。想购买高性价比蓝牙耳机&#xff0c;可以从以下性价比高排行榜中挑选。 1.南卡小音舱蓝牙耳机 参考价格&#xff1a;199元 蓝牙版本…

400元左右的蓝牙耳机啥牌子好?400元价位蓝牙耳机推荐

随着人们越来越倾向于使用随身便携的电子产品&#xff0c;轻松上阵、无线自由的TWS蓝牙耳机越来越受消费者的青睐&#xff0c;成为现在耳机行业的新星&#xff0c;下面整理了几款400元价位的耳机品牌。 一、南卡小音舱Lite2蓝牙耳机 参考价格&#xff1a;239元 佩戴方式&…

2022有什么耳机音质好又便宜?便宜音质好的蓝牙耳机推荐

蓝牙耳机怎么挑选&#xff1f;有没有性价比高的蓝牙耳机推荐&#xff1f;这是很多朋友都咨询过我的问题&#xff0c;作为蓝牙耳机的常年使用者&#xff0c;对于蓝牙耳机十分了解&#xff0c;也因此比较了解市面上的蓝牙耳机&#xff0c;现在市面上涌现出了不少优质的平价国货产…