您现在的位置是:首页 >技术交流 >Spring数据库事务处理网站首页技术交流

Spring数据库事务处理

hello fafa 2024-07-23 00:01:02
简介Spring数据库事务处理

数据库事务的基本知识

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
    @Lazy
    private 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);
    }

	@Override
    public 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,不会回滚当前方法的事务,这个效果是通过数据库的保存点技术来实现的,至于数据库的保存点技术,可以自行了解一下。

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

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。