本文收录在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
- 在本地新建消息表
- 消息和业务在同一个事务里提交
- 通过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代码层面,更多的是关注事务之间的关系
而在分布式事务中,为了高可用,在事务一致性上进行了妥协,一般只保证最终一致性
网友评论