java中对事务怎么理解_浅析Java中的事务,从ACID到BASE

本文收录在javaskill.cn中,内有完整的JAVA知识地图,欢迎访问

1. 数据库中的事务

Java中的事务管理,最终都是体现在数据上,因此,了解数据库对事务的处理是非常必要的

1.1 ACID

Atomicity、Consistency、Isolation、Durability

原子性、一致性、隔离性、持久性

原子性

事务中的操作必须全部成功或全部失败

一致性

事务必须使数据库从一个一致性状态转变到另一个一致性状态

栗子:A有100元,B有100元,AB共200元,无论A和B怎么转账(不考虑手续费),A和B一共有200元

隔离性

事物之间不互相干扰,存在多种隔离级别

持久性

事务一旦提交,对数据的改变就是永久性的,即使遇到故障,也不会丢失提交事务的操作

1.2 脏读、不可重复读和幻读

脏读

指一个事务读取了另一个未提交的事务中的数据

不可重复读

指一个事务中,对一个值多次读取返回的值不一致(读取了其他已提交事务的数据)

幻读

栗子: T1把表A中的某个字段从1改为2,T2对表A进行了插入并提交,并且该字段为1,T1修改提交后,发现还有一条数据没有修改(注意和不可重复读的区别)

1.3 数据库的四种隔离级别

Serializable

避免脏读、不可重复读、幻读

Repeatable Read

避免脏读、不可重复读

Read Committed

避免脏读

Read Uncommitted

毛都避免不了

1.4 如何保证持久性和一致性

持久性和一致性的概念清楚了,那么数据库如何保证这一点呢?如果事务提交后,主机突然断电了呢?

概念很简单,数据库操作事务的时候,会记下这个事务的redo操作日志,在真正操作数据库之前,会把日志写入磁盘,发生异常情况后,会根据当前数据的情况进行undo或者redo,以此保证一致性和持久性,这里不再深究

2. Spring中的事务

上升到Java中,最常使用的应该是Spring中的事务操作。不管是声明式事务还是手动开启事务,在Java中,所关注的不再是数据层面的一致性(数据库已经帮我们保证了),而是事务之间的关系。

通常,事务边界都是设定在Service层,如果一个Service层中的事务方法,调用另一个事务方法,事务是怎样传播的呢?

事务传播

Spring中共有7种不同的传播行为,以被调用方法的视角,可以把它们分为两类

被调用方支持事务

1.1 PROPAGATION_REQUIRED 必须要有事务,有就加入,无则创建

1.2 PROPAGATION_SUPPORTS 支持当前事务,有就加入,没有拉倒

1.3 PROPAGATION_MANDATORY 使用当前事务,有就加入,没有报错

1.4 PROPAGATION_REQUIRES_NEW 使用新事务,外层事务挂起,独立提交回滚

1.5 PROPAGATION_NESTED 使用嵌套事务,独立回滚(出错回滚自身),不独立提交,没有事务则创建

被调用方不支持事务

2.1 PROPAGATION_NOT_SUPPORTED 不使用事务,有就挂起,没有拉倒

2.2 PROPAGATION_NEVER 坚决不使用事务,有就报错

需要注意1.4和1.5的区别,关键在于是否独立提交和回滚

3. 分布式事务

首先要明确的一点,在分布式事务中,ACID已经不适用了。在集群环境下,想要保证ACID几乎是不可能的任务,即使能够达到,效率也是非常低下的。所以,在集群环境下,分布式事务一般追求的是最终一致性。

3.1 BASE理论

Basically Available 基本可用

Soft state 软状态

Eventually consistent 最终一致

分布式系统中,可用性往往比一致性更重要(想象一下,支付宝为了保证强一致性,即A转100给B,A账户马上扣100,B账户马上加100,但是三天两头无服务),BASE理论就是在可用性和一致性中做出了权衡,核心思想是,我们无法做到强一致性,但是每个应用可以结合自身的特点,用适当的方式来达到最终一致性(A支付100元给B,B可能马上收到,也可能5分钟后收到,但是最终一定会收到)。

3.2 TCC补偿事务

TCC的核心是采用了补偿机制,针对每个操作,都要有一个与之对应的补偿(回滚)操作,分为三个阶段:

Try 预留业务资源

尝试执行业务

完成所有业务检查

预留必须业务资源

Confirm 确认执行业务操作,需幂等

真正执行业务

不做业务检查

只使用try阶段预留的资源

Cancel 取消执行业务操作,需幂等

释放try阶段预留的资源

和数据库中的事务操作进行对比,可以找到类似之处,锁定行->操作行->出错回滚

举个实际的例子来加深理解

假设有A、B、C三个账户,A和B向C支付100元,A支付40元,B支付60元,需要在一个事务中完成

try

检测A、B、C三个账户的状态,是否允许转账

检测A账户是否有40元,有则冻结

检测B账户是否有60元,有则冻结

confirm

扣除A、B的冻结金额,增加C账户的金额,不做任何业务检查

cancel

恢复A或B的冻结金额

如果在try阶段发现,A的账户冻结40元成功,B冻结失败,则调用A的cancel方法,恢复A的冻结金额

3.3 本地消息表

这种思路来源于ebay

93c61beb71a4

在本地新建消息表

消息和业务在同一个事务里提交

通过MQ通知消费方

消费方处理消息后通知修改消息状态

消息发送失败,重试

定时扫描未处理的消息进行重发

消费方业务失败,调用生产方补偿方法进行回滚

这种方式遵循BASE理论,保证的是最终一致性,在实际使用中,比TCC更好处理,少写很多代码。需要注意的是,消息处理需要幂等

3.4 MQ事务消息

阿里巴巴的Rocket MQ支持事务消息,Rabbit MQ和Kafka都不支持

3.3中之所以要使用本地消息表,因为更新数据库和发送MQ消息不是一个原子操作,无论谁先谁后,都会有问题

先更新DB,发送消息失败了,怎么办?

先发送消息,DB更新失败了,消息已经发了,怎么办?

3.3中采用了本地消息表,通过消息表中的消息状态来控制重发,以达到最终一致的目的

事务消息模拟了这种操作,只不过把维护消息状态的过程,从数据库转移到了MQ中间件

具体来说,就是把消息发送,分解成两个阶段,准备和确认

具体到业务中,分解成了三步操作

发送Prepared消息

更新数据库

根据2的结果,发送Confirm或Cancel,确认或取消消息

取消的消息会被丢弃,确认后的消息才会真正的发送给消费者

如果第三步失败了,RocketMQ会主动(默认1分钟)询问发送方,喂?这条消息还要吗?此时发送方可以查询本地业务状态,确定消息是否需要发送,以此确保最终一致性

总结

从ACID到BASE,对于事务,不同视角,对它的理解也不同

在数据库层面,通过日志文件确保了事务的一致性,以及确定了不同的事务隔离级别

在Java代码层面,更多的是关注事务之间的关系

而在分布式事务中,为了高可用,在事务一致性上进行了妥协,一般只保证最终一致性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值