什么是事务
数据库事务: 数据库事务通常指对数据库进行读或写的一个操作序列;
它的存在包含有以下两个目的:
-
为数据库操作提供了一个从失败中恢复到正常状态的方法, 同时提供了数据库即使在异常状态下仍能保持一致性的方法; -
当多个应用程序在并发访问数据库时, 可以在这些应用程序之间提供一个隔离方法, 以防止彼此的操作互相干扰;
系统中的事务: 处理一系列业务处理的执行逻辑单元,该单元里的一系列类操作要不全部成功要不全部失败
为什么使用事务
? 可以保证数据的一致性和完整性(避免异常和错误等导致的数据信息异常)
事物的特性
-
原子性(atomicity); 一个事务是一个不可分割的工作单位, 事务中包括的操作要么都做, 要都回滚; 举例来说, 你去菜市场买鸡蛋,你最少买一个鸡蛋,你不能买半个鸡蛋, -
一致性(consistency); 事务必须是使数据库从一个一致性状态变到另一个一致性状态; 举例来说, 假设你微信钱包有500我的钱包也有500,不管咱俩如何转账,事物结束后咱们俩钱包里的钱加起来还得是1000; -
隔离性(isolation); 一个事务的执行不能被其他事务干扰; 即一个事务内部的操作及使用的数据对并发的其他事务是隔离的, 并发执行的各个事务之间不能互相干扰; -
持久性(durability); 持久性也称永久性(permanence), 指一个事务一旦提交, 它对数据库中数据的改变应该是永久性的; 接下来的其他操作或故障不应该对其有任何影响;
Spring 事务
Spring事务本质是对数据库事务的支持, 如果数据库不支持事务(例如MySQL的MyISAM引擎不支持事务), 则Spring事务也不会生效;
Spring事务实际使用AOP拦截注解方法, 然后使用动态代理处理事务方法, 捕获处理过程中的异常, Spring事务其实是把异常交给Spring处理;
Spring使用事务
Spring中使用 @Transactional 注解来开启事务;
@Transactional用在方法上对该方法有事务(推荐); @Transactional用在类上对类中所有方法都有事务(推荐); @Transactional用在接口上对该接口的所有实现都有事务(不推荐, 但根据业务可以放在接口方法上);
@Transactional参数介绍:
- propagation: 指定事务传播机制, 即当前事务被其他事务调用时, 如何使用事务, 默认值为REQUIRED;
- isolation: 指定事务隔离级别, 最常用的取值是READ_COMMITTED;
- noRollbackFor: 指定不回滚的异常, 通常默认值(Exception.class)即可;
- readOnly: 指定事务是否为只读, 表示该事物只读取数据, 不进行修改, 可帮助数据库引擎优化事务, 此时需要设置readOnly = true;
- timeout: 可强制指定回滚的超时时间, 默认单位为秒(s), 默认为本地系统的超时时间;
案例假设:
①保存用户到数据库 ②记录用户操作日志 是一个原子操作;
如果①和②之间出了问题, 如果没有事务的话, 可能导致用户记录到了数据库, 但日志里面却没有记录, 造成业务不完整; 如果加入了事务, 那么就可以避免这种问题;
代码验证:
不加事务的代码:
其中先保存user到数据库, 然后打印1/0, 这步会报错, 然后保存log;
启动项目, 然后调用接口, 可以看到报了错:/ by zero;
然后我们查看数据库, 发现user表已经有了数据, 也就是报错之前的操作保存到了数据库;
而log表里却没有日志, 即报错之后的数据没有保存成功;
此时就造成了数据的不完整, 两步操作要么应该都完成, 要么应该都失败;
我们下面加入事务来解决这个问题
重启项目后继续测试, 发现两个表里都没有数据, 说明事务生效了, 两步操作同时失败;
在Springboot中使用事务非常的方便, 不过如果只有一步操作的话就不需要加入事务, 因为事务也是要耗费更多的资源的;
Spring事务失效场景
- 数据库引擎是否支持事务(Mysql 的 MyIsam引擎不支持事务);
- 注解所在的类是否被加载为 Bean(是否被Spring 管理);
- 注解所在的方法是否为 public 修饰的;
- 是否存在自身调用的问题;
- 所用数据源是否加载了事务管理器;
- @Transactional的扩展配置propagation是否正确;
- 异常没有被抛出, 或异常类型错误;
- 方法用final修饰或static修饰;
- 多线程调用;
数据库引擎不支持事务
这里以 MySQL 为例, 其 MyISAM 引擎是不支持事务操作的, InnoDB 才是支持事务的引擎, 一般要支持事务都会使用 InnoDB;
测试: 我们修改user表的引擎为MyISAM
继续测试, 然后我们查看数据库, 发现user表已经有了数据, 也就是报错之前的操作保存到了数据库;
如果要保证事务, 数据库的引擎必须要支持事务才可以;
Bean没有被 Spring 管理
如下面例子所示:
public class TransactionTestImpl implements TransactionTest {
@Transactional
@Override
public void addUser() {
}
}
如果此时把 @Service 注解注释掉, 这个类就不会被加载成一个 Bean, 那这个类就不会被 Spring 管理了, 事务自然就失效了;
**@Transaction 可以用在类上、接口上、public方法上, 如果将@Trasaction用在了非public方法上, 事务将无效; **
以下来自 Spring 官方文档:
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.
在使用代理时, 应该只对具有公共可见性的方法应用@Transactional注释; 如果使用@Transactional注释注释了受保护的、私有的或包可见的方法, 则不会引发错误, 但注释的方法不会显示配置的事务设置; 如果需要对非公共方法进行注释, 请考虑使用Aspectj;
自身调用问题
本类方法不经过代理, 无法进行增强, 必须通过代理对象访问方法, 事务才会生效;
@Override
public void addUser() {
add();
}
@Transactional
public void add() {
User user = new User(1, "张三", 18);
userMapper.insert(user);
System.out.println(1 / 0);
Log log = new Log(user.getId(), "create user :" + user.getId());
logMapper.insert(log);
}
异常没有抛出
Spring事务只有捕捉到了业务抛出去的异常, 才能进行后续的处理, 如果业务自己捕获了异常, 则事务无法感知;
@Transactional
@Override
public void addUser() {
add();
}
public void add() {
User user = new User(1, "张三", 18);
userMapper.insert(user);
try {
System.out.println(1 / 0);
} catch (Exception e) {
}
Log log = new Log(user.getId(), "create user :" + user.getId());
logMapper.insert(log);
}
异常类型错误
并不是任何异常情况下, Spring都会回滚事务, 默认情况下, RuntimeException和Error的情况下, Spring事务才会回滚;
@Transactional
@Override
public void addUser() throws Exception {
add();
}
public void add() throws Exception {
User user = new User(1, "张三", 18);
userMapper.insert(user);
try {
System.out.println(1 / 0);
} catch (Exception e) {
throw new Exception("更新错误");
}
Log log = new Log(user.getId(), "create user :" + user.getId());
logMapper.insert(log);
}
}
方法用final修饰或static修饰
因为Spring事务是用动态代理实现, 因此如果方法使用了final修饰, 则代理类无法对目标方法进行重写, 植入事务功能;
@Transactional
public final boolean add(User user, UserService userService) {
boolean isSuccess = userService.save(user);
try {
int i = 1 % 0;
} catch (Exception e) {
throw new RuntimeException();
}
return isSuccess;
}
多线程调用
因为Spring的事务是通过数据库连接来实现, 而数据库连接Spring是放在threadLocal里面; 同一个事务, 只能用同一个数据库连接; 而多线程场景下, 拿到的数据库连接是不一样的, 即是属于不同事务;
@Transactional
@Override
public void addUser() {
add();
}
public void add() {
Runnable runnable = () -> {
User user = new User(1, "张三", 18);
userMapper.insert(user);
System.out.println(1 / 0);
Log log = new Log(user.getId(), "create user :" + user.getId());
logMapper.insert(log);
};
new Thread(runnable).start();
}
事务失效总结:
Spring 事务失效的场景有很多, 但是可以分为这几类
是否支持事务: 数据库是否支持事务, 多线程调用也不支持事务;
异常的类型和捕获: Spring必须感知到异常才能开启事务, 默认情况下, RuntimeException和Error异常才会回滚;
方法修饰符: Spring事务只对public方法生效, final和static修饰的方法也不会生效;
内部调用: 一个类中的A方法(没事物)调用B方法(有事物), AB方法事务都不会生效;
方法能否生成代理: 方法要被Spring容器管理事务才能生效, 方法如果不能被重写也不会生成代理;
不同Service方法间调用: 当@Transactional 注解作用在方法A上时, 事务起作用, 方法A中的数据回滚, 方法saveClassInfo中的数据回滚;
当@Transactional 注解作用在方法saveClassInfo上时, 事务对A不起作用, 方法A中的数据提交, 方法saveClassInfo数据回滚;
Spring事务隔离级别
Spring事务本质上使用数据库事务, 而数据库事务本质上使用数据库锁, 所以Spring事务本质上使用数据库锁, 开启Spring事务意味着使用数据库锁;
Spring事务隔离级别比数据库事务隔离级别多一个default;
-
Default(默认) 这是一个PlatfromTransactionManager默认的隔离级别, 使用数据库默认的事务隔离级别; 另外四个与JDBC的隔离级别相对应; -
ReadUncommitted(读未提交) 这是事务最低的隔离级别, 它允许另外一个事务可以看到这个事务未提交的数据; 这种隔离级别会产生脏读, 不可重复读和幻像读; -
ReadCommitted(读已提交) 保证一个事务修改的数据提交后才能被另外一个事务读取, 另外一个事务不能读取该事务未提交的数据; 这种事务隔离级别可以避免脏读出现, 但是可能会出现不可重复读和幻读; -
RepeatableRead(可重复读) 这种事务隔离级别可以防止脏读、不可重复读, 但是可能出现幻读; 它除了保证一个事务不能读取另一个事务未提交的数据外, 还保证了不可重复读; -
Serializable(串行化) 这是花费最高代价但是最可靠的事务隔离级别, 事务被处理为顺序执行; 除了防止脏读、不可重复读外, 还避免了幻读;
隔离级别越高, 越能保证数据的完整性和一致性, 但是对并发性能的影响也越大; 对于多数应用程序, 可以优先考虑把数据库系统的隔离级别设为Read Committed; 它能够避免脏读取, 而且具有较好的并发性能;
Spring事务传播属性
Spring在TransactionDefinition接口中规定了7种事务传播行为, 他们规定了事务和事务之间发生嵌套时事务如何进行传播;
所谓事务的传播行为是指, 如果在开始当前事务之前, 一个事务上下文已经存在, 此时有若干选项可以指定一个事务性方法的执行行为;
-
Required 默认事务类型, 如果没有, 就新建一个事务;如果有, 就加入当前事务; 适合绝大多数情况; -
RequiresNew 如果没有, 就新建一个事务;如果有, 就将当前事务挂起; -
Nested 如果当前存在事务, 则在嵌套事务内执行; 如果当前没有事务, 则执行与PROPAGATION_REQUIRED类似的操作; 特点就是会设置回滚点; -
Supports 如果没有, 就以非事务方式执行;如果有, 就使用当前事务; -
NotSupported 如果没有, 就以非事务方式执行;如果有, 就将当前事务挂起; 即无论如何不支持事务; -
Never 如果没有, 就以非事务方式执行;如果有, 就抛出异常; -
Mandatory 如果没有, 就抛出异常;如果有, 就使用当前事务; 即强制要有事务;
|