问题
今天项目测试一个方法的时候,发现日志报错
日志报错大致如下:Connection is read-only. Queries leading to data modification are not allowed
org.springframework.dao.TransientDataAccessResourceException:
### Error updating database. Cause: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
### The error may involve com.o2o.app.repository.AccountOrderMybatisDao.updateOrder-Inline
### The error occurred while setting parameters
### SQL: UPDATE t_account_order SET ORDER_STATUS=?, STATUS_DESCRIPTION=?, IS_DELETE = ? WHERE TRADE_CODE = ? and TRADE_TYPE=?
### Cause: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed; SQL []; Connection is
read-only. Queries leading to data modification are not allowed; nested exception is java.sql.SQLException:
Connectionis read-only. Queries leading to data modification are not allowed
第一眼看上去,这不是Spring事务配置了只读事务属性,当执行sql写操作当然会失败了,为了更好解决这个bug,让我们先温习一下事务以及Spring在事务传播做了那些事?
- 事务介绍
- 事务(Transaction):指的是要做的事情,在计算机术语指的是访问并能更新数据库数据的一个程序执行单元
- 由于我们日常开发,需要经常对关系型数据库打交道,这里简单介绍一下关系型数据库的事务四大属性
事务名称 | 解释 |
---|---|
原子性(Atomicity) | 事务是一个原子操作,原子操作简单理解指的是这个操作要么全部成功,要么全部失败 |
一致性(Consistency) | 事务无论成功与否,数据库必须保证所处的数据不应被破坏,举个例子:A给B无论成功或失败转账,那么A的钱+B的钱前后应该总和相等 |
隔离性(Isolation) | 同一份的数据可能有很多事务进行操作,因此要将各种事务隔离开,防止数据被损坏 |
持久性(Durability) | 事务如果一旦完成,结果都应不变,因为这样无论系统发送了什么错误,都能进行数据恢复 |
- Spring事务核心类和接口
- 如下图所示:在spring-tx包下有三个spring事务管理非常重要的接口 :
PlatformTransactionManager
,TransactionDefinition
,TransactionStatus
spring并不实现各个数据库持久层的事务实现,而是提供对应的事务管理器,如下图所示:
我们首先查看PlatformTransactionManager
接口的源代码:
TransactionStatus getTransaction(TransactionDefinition definition)
官方的解释是: Return a currently active transaction or create a new one, according to the specified propagation behavior
这句话的意思是根据指定的事务行为,返回当前的事务或者新建一个事务。
void commit(TransactionStatus status)
官方解释是:Commit the given transaction, with regard to its status. If the transaction has been marked rollback-only programmatically, perform a rollback.
这句话的意思是根据事务的状态提交事务,如果事务标记了rollback-only,请执行会滚。
void rollback(TransactionStatus status)
官方的解释是: Perform a rollback of the given transaction,即对事务进行回滚。
看到这里,我们需要明确三个接口中入参的TransactionDefinition是个什么东西呢?
让我们先大致查看一下TransactionDefinition接口的方法和成员变量,以下将会对此接口的方法做个简单的介绍,如下图所示:
- 事务的传播行为
- 当事务方法被调用时候,必须指定事务如何传播,下面是事务传播行为的介绍:
事务名称 | 解释 |
---|---|
PROPAGATION_REQUIRED | 支持当前的事务,如果当前事务不存在就新建一个事务 |
PROPAGATION_SUPPORTS | 支持当前事务,如果事务不存在,将以非事务方式运行 |
PROPAGATION_MANSATORY | 支持当前事务,如果事务不存在将抛异常 |
PROPAGATION_REQUIRES_NEW | 如果当前事务存在,将当前事务挂起并创建新的事务,如果当前事务不存在就新建一个事务 |
PROPAGATION_NOT_SUPPORTED | 不支持当前事务,以非事务的方式运行 |
PROPAGATION_NEVER | 不支持当前事务,如果当前事务存在就抛异常 |
PROPAGATION_NESTED | 如果当前事务存在,则执行一个内嵌的事务 |
- 事务的隔离级别
- 典型的事务隔离不同所造成问题如下:
1.脏读:脏读发送在A事务读取B事务已经改写但是还未提交的数据,若此时B事务回滚了,那么A事务获取就是脏数据
2.不可重复读:不可重复读发送在当A事务执行2次查询,每一次获取的数据结果都不相同,这是由于B事务在A事务2次查询期间进行了更新
3.幻读: 幻读发送在当A事务读取了几行数据,紧接着B事务进行输入的插入,在随后的查询中A事务就会读了原本不存在的记录
不可重复读特指修改的记录,而幻读指的是新增或删除的记录
- 只读属性
- 如果设置了只读事务,只读事务常常用于做查询使用,此时的增删改,将会报Connection is read-only. Queries leading to data modification are not allowed的异常。
- 事务的超时
- 一个正常和良好的程序,事务的行为时间并不会很长,较长的事务运行时间,会占用数据库资源,所以这里就设置超时时间,若指定时间内没有执行完事务,将会自动进行回滚
- 事务的名称
- 在一个事务行为中配置获取事务的名称,如我们常见的save,add,del 等等…
以上温习过Spring事务管理器和传播行为后,所以既然报错Connection is read-only. Queries leading to data modification are not allowed所以我们在接口方法的实现,加了以下的注解: @Transactional(propagation = Propagation.REQUIRED, readOnly = false),but!当我再次请求接口的时候,发现依然还是报同样的错误,百度一下,发现有相关问题博客的收集:Spring下默认事务机制中@Transactional 无效的原因
Method visibility and @Transactional 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.
大概意思是:如果你是使用默认的Spring Aop代理方式将@Transaction注解应该用于公共可见即public的,如果对protected,或者private的方法加入@Transaction注解,则会无效。如果想在私有方法是使事务有效,可以用AspectJ进行实现。
but,我们的注解没有加在protected和private方法上,但是依然无效,why?
此次注解失效原因像下面简单的例子一样:在电商系统中,存在待支付的订单,假设有一个订单编号为201904191102的订单要进行支付,首先我需要刷新支付页面,就需要调用收银台接口,由于一直使用的scala开发,所以下面的代码使用scala做演示:
def refreshCashier(orderId: String): OrderInfo
def updateWallet(order:orderInfo):PayDto
override def refreshCashier(orderId: String): OrderInfo = {
// 伪代码,在这个方法里面调用 updateWallet方法
}
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
override def updateWallet(order:orderInfo): PayDto = {
// 这里发送了异常
throw new AppException()
}
在上面的代码中,refreshCashier
方法调用了updateWallet
方法的时候,当updateWallet
方法出错报异常,事务并没有回滚,这是因为Spring Aop动态代理会为每个class对象生成代理对象,只有在代理对象之间进行调用的时候,将会触发切面相关的逻辑处理。
所以要保证整个方法调用链的事务性,在refreshCashier方法加上@Transaction注解,此时才能保证,updateWallet方法出错时候,整个方法能进行事务的回滚。这样完美,问题解决了.
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。