数据库事务的基本知识
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,不会回滚当前方法的事务,这个效果是通过数据库的保存点技术来实现的,至于数据库的保存点技术,可以自行了解一下。
好了,今天就到这里了,感兴趣的小伙伴赶紧去试试吧,拜拜。