美文网首页
浅析Java中的事务,从ACID到BASE

浅析Java中的事务,从ACID到BASE

作者: TickTock_0211 | 来源:发表于2018-05-16 14:39 被阅读0次

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

    1. 数据库中的事务

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

    1.1 ACID

    Atomicity、Consistency、Isolation、Durability
    原子性、一致性、隔离性、持久性

    1. 原子性
      事务中的操作必须全部成功或全部失败
    2. 一致性
      事务必须使数据库从一个一致性状态转变到另一个一致性状态
      栗子:A有100元,B有100元,AB共200元,无论A和B怎么转账(不考虑手续费),A和B一共有200元
    3. 隔离性
      事物之间不互相干扰,存在多种隔离级别
    4. 持久性
      事务一旦提交,对数据的改变就是永久性的,即使遇到故障,也不会丢失提交事务的操作

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

    1. 脏读
      指一个事务读取了另一个未提交的事务中的数据
    2. 不可重复读
      指一个事务中,对一个值多次读取返回的值不一致(读取了其他已提交事务的数据)
    3. 幻读
      栗子: T1把表A中的某个字段从1改为2,T2对表A进行了插入并提交,并且该字段为1,T1修改提交后,发现还有一条数据没有修改(注意和不可重复读的区别)

    1.3 数据库的四种隔离级别

    1. Serializable
      避免脏读、不可重复读、幻读
    2. Repeatable Read
      避免脏读、不可重复读
    3. Read Committed
      避免脏读
    4. Read Uncommitted
      毛都避免不了

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

    持久性和一致性的概念清楚了,那么数据库如何保证这一点呢?如果事务提交后,主机突然断电了呢?
    概念很简单,数据库操作事务的时候,会记下这个事务的redo操作日志,在真正操作数据库之前,会把日志写入磁盘,发生异常情况后,会根据当前数据的情况进行undo或者redo,以此保证一致性和持久性,这里不再深究

    2. Spring中的事务

    上升到Java中,最常使用的应该是Spring中的事务操作。不管是声明式事务还是手动开启事务,在Java中,所关注的不再是数据层面的一致性(数据库已经帮我们保证了),而是事务之间的关系。
    通常,事务边界都是设定在Service层,如果一个Service层中的事务方法,调用另一个事务方法,事务是怎样传播的呢?

    事务传播

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

    1. 被调用方支持事务
      1.1 PROPAGATION_REQUIRED 必须要有事务,有就加入,无则创建
      1.2 PROPAGATION_SUPPORTS 支持当前事务,有就加入,没有拉倒
      1.3 PROPAGATION_MANDATORY 使用当前事务,有就加入,没有报错
      1.4 PROPAGATION_REQUIRES_NEW 使用新事务,外层事务挂起,独立提交回滚
      1.5 PROPAGATION_NESTED 使用嵌套事务,独立回滚(出错回滚自身),不独立提交,没有事务则创建
    2. 被调用方不支持事务
      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的核心是采用了补偿机制,针对每个操作,都要有一个与之对应的补偿(回滚)操作,分为三个阶段:

    1. Try 预留业务资源
      尝试执行业务
      完成所有业务检查
      预留必须业务资源
    2. Confirm 确认执行业务操作,需幂等
      真正执行业务
      不做业务检查
      只使用try阶段预留的资源
    3. 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中间件

    具体来说,就是把消息发送,分解成两个阶段,准备和确认
    具体到业务中,分解成了三步操作

    1. 发送Prepared消息
    2. 更新数据库
    3. 根据2的结果,发送Confirm或Cancel,确认或取消消息

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

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

    总结

    从ACID到BASE,对于事务,不同视角,对它的理解也不同
    在数据库层面,通过日志文件确保了事务的一致性,以及确定了不同的事务隔离级别
    在Java代码层面,更多的是关注事务之间的关系
    而在分布式事务中,为了高可用,在事务一致性上进行了妥协,一般只保证最终一致性

    相关文章

      网友评论

          本文标题:浅析Java中的事务,从ACID到BASE

          本文链接:https://www.haomeiwen.com/subject/sniudftx.html