spring事务 只读此文
Posted 风随心飞飞
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring事务 只读此文相关的知识,希望对你有一定的参考价值。
文章目录
一. 事务概述
事务在逻辑上是一组操作,要么执行,要不都不执行。主要是针对数据库而言的,比如说 mysql。
1.1. MySQL 数据库事务
MYSQL 数据库ACID 的 4 个重要特性:
特性 | 描述 |
---|---|
原子性(Atomicity) | 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样 |
一致性(Consistency) | 在事务开始之前和事务结束以后,数据库的完整性没有被破坏 |
事务隔离(Isolation) | 数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致 |
持久性(Durability) | 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失 |
MYSQL 数据库事务隔离级别:
隔离级别 | 描述 |
---|---|
未提交读(Read uncommitted) | 最低的隔离级别,允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。如果另一个事务回滚,那么当前事务读到的数据就是脏数据 |
提交读(read committed) | 一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致 |
可重复读(repeatable read) | 一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了 |
串行化(Serializable) | 最严格的隔离级别,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别 |
1.2 spring的事务支持:
spring 支持两种事务方式:编程式事务 和 声明式事务。
/**
* 模拟转账
*/
@Transactional
public void handle()
// 转账
transfer(double money);
// 减自己的钱
Reduce(double money);
1.2.1 编程式事务:
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。
方式一:使用 TransactionTemplate 来管理事务
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction()
transactionTemplate.execute(new TransactionCallbackWithoutResult()
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus)
try
// .... 业务代码
catch (Exception e)
//回滚
transactionStatus.setRollbackOnly();
);
方式二:使用 TransactionManager 来管理事务
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction()
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try
// .... 业务代码
transactionManager.commit(status);
catch (Exception e)
transactionManager.rollback(status);
注意:就编程式事务管理而言,Spring 更推荐使用 TransactionTemplate。
在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。
1.2.2 声明式事务
声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。
当然了,要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中的AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
事务管理模型:
Spring 将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:
public interface TransactionManager
该接口有两个子接口,分别是编程式事务接口 ReactiveTransactionManager 和声明式事务接口 PlatformTransactionManager。我们来重点说说 PlatformTransactionManager,该接口定义了 3 个接口方法:
interface PlatformTransactionManager extends TransactionManager
// 根据事务定义获取事务状态
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 事务回滚
void rollback(TransactionStatus status) throws TransactionException;
通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
参数 TransactionDefinition 和 @Transactional 注解是对应的,比如说 @Transactional 注解中定义的事务传播行为、隔离级别、事务超时时间、事务是否只读等属性,在 TransactionDefinition 都可以找得到。
返回类型 TransactionStatus 主要用来存储当前事务的一些状态和数据,比如说事务资源(connection)、回滚状态等。
TransactionDefinition如下:
public interface TransactionDefinition
// 事务的传播行为
default int getPropagationBehavior()
return PROPAGATION_REQUIRED;
// 事务的隔离级别
default int getIsolationLevel()
return ISOLATION_DEFAULT;
// 事务超时时间
default int getTimeout()
return TIMEOUT_DEFAULT;
// 事务是否只读
default boolean isReadOnly()
return false;
Transactional注解如下:
@Target(ElementType.TYPE, ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
@Transactional 注解中的 propagation 对应 TransactionDefinition 中的 getPropagationBehavior,默认值为 Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)。
@Transactional 注解中的 isolation 对应 TransactionDefinition 中的 getIsolationLevel,默认值为 DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)。
@Transactional 注解中的 timeout 对应 TransactionDefinition 中的 getTimeout,默认值为TransactionDefinition.TIMEOUT_DEFAULT。
@Transactional 注解中的 readOnly 对应 TransactionDefinition 中的 isReadOnly,默认值为 false。
说到这,我们来详细地说明一下 Spring 事务的传播行为、事务的隔离级别、事务的超时时间、事务的只读属性,以及事务的回滚规则。
说到这,我们来详细地说明一下 Spring 事务的传播行为、事务的隔离级别、事务的超时时间、事务的只读属性,以及事务的回滚规则。
1.2.3 事务传播行为:
当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam)
TransactionDefinition 一共定义了 7 种事务传播行为,其中PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW 两种传播行为是比较常用的。
1. PROPAGATION_REQUIRED
这也是 @Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:
如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。
也就是说如果a方法和b方法都添加了注解,在默认传播模式下,a方法内部调用b方法,会把两个方法的事务合并为一个事务。
2. PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。
当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 ,总结就是a不影响b,b影响a
3. PROPAGATION_NESTED
如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。
当类A中的 a 方法用默认 Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.NESTED模式,然后在 在a 方法里调用 b方法操作数据库,然而 b方法抛出异常后,a方法是不的回滚 ,总结就是b不影响a,a影响b。
4. PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
5. PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
6. PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
7. PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
1.2.4 事务隔离级别
前面我们已经了解了数据库的事务隔离级别,再来理解 Spring 的事务隔离级别就容易多了。
TransactionDefinition 中一共定义了 5 种事务隔离级别:
隔离级别 | 描述 |
---|---|
ISOLATION_DEFAULT | 使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。 |
ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,可能会出现脏读、幻读或者不可重复读 |
ISOLATION_READ_COMMITTED | 允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。 |
ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。 |
ISOLATION_SERIALIZABLE | 最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能 |
通常情况下,我们采用默认的隔离级别 ISOLATION_DEFAULT 就可以了,也就是交给数据库来决定。
1.2.5 事务的超时时间
事务超时**timeout **,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。
假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。
1.2.6 事务的只读属性
事务的只读属性readOnly, 如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。
为什么一个查询操作还要启用事务支持呢?
这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。
那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。
1.2.7 事务的回滚策略
**回滚策略rollbackFor **,用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
如果你想要回滚特定的异常类型的话,可以这样设置:
@Transactional(rollbackFor= MyException.class)
事务的不回滚策略
**不回滚策略noRollbackFor **,用于指定不触发事务回滚的异常类型,可以指定多个异常类型。
二. spring事务(注解 @Transactional )失效的12种场景
在某些业务场景下,如果一个请求中,需要同时写入多张表的数据或者执行多条sql。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。
2.1 事务不生效【七种】
2.1.1 访问权限问题 (只有public方法会生效)
众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:
@Service
public class UserService
@Transactional
private void add(UserModel userModel)
saveData(userModel);
updateData(userModel);
我们可以看到add方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须得是public的。
说白了,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass)
// Don't allow no-public methods as required.可以看到, 这里不支持public类型的方法
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers()))
return null;
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null)
return txAttr;
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method))
return txAttr;
if (specificMethod != method)
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null)
return txAttr;
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method))
return txAttr;
return null;
也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。
2.1.2 方法用final修饰,不会生效
有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:
@Service
public class UserService
@Transactional
public final void add(UserModel userModel)
saveData(userModel);
updateData(userModel);
我们可以看到add方法被定义成了final的,这样会导致事务失效。
为什么?
如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
2.1.3同一个类中的方法直接内部调用,会导致事务失效
有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:
@Service
public class UserService
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel)
userMapper.insertUser(userModel);
updateStatus(userModel);
@Transactional
public void updateStatus(UserModel userModel)
doSameThing();
我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
方法1: 新加一个Service方法
这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:
@Servcie
public class ServiceA
@Autowired
prvate ServiceB serviceB;
public void save(User user)
queryData1();
queryData2();
serviceB.doSave(user);
@Servcie
public class ServiceB
@Transactional(rollbackFor=Exception.class)
public void doSave(User user)
addData1();
updateData2();
方法2:在该Service类中注入自己
如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:
@Servcie
public class ServiceA
@Autowired
prvate ServiceA serviceA;
public void save(User user)
queryData1();
queryData2();
serviceA.doSave(user);
@Transactional(rollbackFor=Exception.class)
public void doSave(User user)
addData1();
updateData2();
可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?
答案:不会。
其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。
方法3:通过AopContent类
在该Service类中使用AopContext.currentProxy()获取代理对象
上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:
@Servcie
public class ServiceA
public void save(User user)
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
@Transactional(rollbackFor=Exception.class)
public void doSave(User user)
addData1();
updateData2();
注意:此方法我在实际使用的过程中会报错
Cannot find current proxy: Set ‘exposeProxy’ property on Advised to
‘true’ to
报错解决方法: 参考
在讲述Spring事务失效的原因及解决方案之前,我们先回顾一下代理模式 我们知道,
spring的声明式事务是基于代理模式的。那么说事务之前我们还是大致的介绍一下代理模式吧。 其实代理模式相当简单,
就是将另一个类包裹在我们的类外面, 在调用我们创建的方法之前, 先经过外面的方法, 进行一些处理, 返回之前, 再进行一些操作。比如:
... public User getUserByName(String name) return userDao.getUserByName(name); ... ```那么如果配置了事务, 就相当于又创建了一个类: ```java public class UserServiceProxy extends UserService private UserService userService; ... public User getUserByName(String name) User user = null; try // 在这里开启事务 user = userService.getUserByName(name); // 在这里提交事务 catch(Exception e) // 在这里回滚事务 // 这块应该需要向外抛异常, 否则我们就无法获取异常信息了. // 至于方法声明没有添加异常声明, 是因为覆写方法, 异常必须和父类声明的异常"兼容". // 这块应该是利用的java虚拟机并不区分普通异常和运行时异常的特点. throw e; return user; ... ```然后我们使用的是 UserServiceProxy 类, 所以就可以”免费”得到事务的支持: ```java @Autowired private UserService userService; // 这里spring注入的实际上是UserServiceProxy的对象 private void test() // 由于userService是UserServiceProxy的对象, 所以拥有了事务管理的能力 userService.getUserByName("aa"); ``` ***Spring事务失效的原因:*** 通过对Spring事务代理模式的分析,我们不难发现Spring事务失效的原因有以下几种情况: 1. private、static、final的使用 、 2. 通过this.xxx()调用当前类的方法 3. 使用默认的事务处理方式 4. 线程Thread中声明式事务不起作用 ***Spring事务失效的解决方案:*** 1. private、static、final的使用 这一原因的解决方案很简单,我们只需要:不在类和方法上使用此类关键字即可。 2. 通过this.xxx()调用当前类的方法 这一原因的解决方案如下: ```java @Service public class TaskService @Autowired private TaskManageDAO taskManageDAO; @Transactional public void test1() try this.test2();//这里调用会使事务失效,两条数据都会被保存 /* 原因是:JDK的动态代理。 在SpringIoC容器中返回的调用的对象是代理对象而不是真实的对象 只有被动态代理直接调用的才会产生事务。 这里的this是(TaskService)真实对象而不是代理对象 */ //解决方法 TaskService proxy =(TaskService) AopContext.currentProxy(); proxy.test2(); catch (Exception e) e.printStackTrace(); Task task = new Task(); task.setCompleteBy("wjl练习1"); task.setCompleteTime(new Date()); taskManageDAO.save(task); @Transactional(propagation = Propagation.REQUIRES_NEW) // 这个事务的意思是如果前面方
spring事务再次理解
2.2.3 只读
事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。
2.2.4 事务超时
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
2.2.3 只读
事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。
2.2.4 事务超时
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
@Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED, noRollbackFor={UserAccountException.class}, readOnly=true, timeout=3) @Override public void purchase(String username, String isbn) { //1.获取书的单价 int price = bookShopDao.findBookPriceByIsbn(isbn); //2.更新书的库存 bookShopDao.updateBookStock(isbn); //3.更新用户余额 bookShopDao.updateUserAccount(username, price); } }
@Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED, noRollbackFor={UserAccountException.class}, readOnly=true, timeout=3) @Override public void purchase(String username, String isbn) { //1.获取书的单价 int price = bookShopDao.findBookPriceByIsbn(isbn); //2.更新书的库存 bookShopDao.updateBookStock(isbn); //3.更新用户余额 bookShopDao.updateUserAccount(username, price); } }
以上是关于spring事务 只读此文的主要内容,如果未能解决你的问题,请参考以下文章