基于Spring的事务、分布式事务以及基于redis、zookeeper的分布式锁

一、事务

事务是逻辑上的一组操作,要么都执行,要么都不执行

事务有以下四个特性:

  原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用

  一致性:执行事务前后,数据保持一致

  隔离性:事务不能被其它事务干扰

  持久性:一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响

Spring中事务的5个隔离级别:

  1、Default:跟随数据库的隔离级别

  2、未提交读:事务可以读取其它事务未提交的数据。会造成脏读。

  3、提交读:事务可以读其他事务已提交的数据。会造成不可重复读和幻读。

  4、可重复读:保证事务多次读同一个字段的值相同。会造成幻读。

  5、序列化执行:事务按序列化逐个执行。完全解决脏读、不可重复读、幻读,但是对性能影响很大,通常不使用。

Spring中事务的7个传播特性:

  1、外层有其他事务在执行,就运行在外层的事务中,如果外层没有事务,也可以不运行在事务中(就写不写一样)

  2、外层有其他事务在执行,就运行在外层的事务中,如果外层没有事务,则自己新建一个事务运行

  3、外层有其他事务在执行,就嵌套运行在外层的事务中,如果外层没有事务,则自己新建一个事务运行(一个事务嵌套另一个事务,外层事务实执行失败则所有事务回滚,内部事务执行失败则只有内部事务回滚)

  4、外层有其他事务在执行,就新建一个事务运行,不运行在外层事务中(两个事务互不影响)

  5、标注本方法必须运行在事务中

  6、标注本方法必须不运行在事务中

  7、标注该方法不运行在事务中,但如果外层有事务在运行,将事务挂起

二、分布式事务

  普通事务只运行在一个server上,而当有多个节点需要组合运行事务,需要保证事务的 AICD 特性时,则使用分布式事务。

分布式事务解决方案:

  1、两阶段提交:协调者加入。向各个节点发送预执行命令(让各个节点启动事务,预执行sql语句,而不提交)----》各个节点执行完后把执行结果发送给协调者----》协调者再根据各个节点执行的结果决定事务是否提交----》把决定下发给各个节点

  2、三阶段提交:在两阶段的基础上增加询问环节(先询问各个节点是否能正常工作),增加超时时间

  3、利用消息中间件实现最终一致性。

  4、利用阿里开源项目seata。

  5、MySQL XA 次方式性能损耗过大,不做介绍

分析:

  1、两阶段和三阶段存在很多问题:代码侵入性强,阻塞,容错,单点故障,数据不一致等问题

  2、利用消息中间件实现最终一致性:能解决两阶段提交的所有问题,但是不能保证消息的时效性。代码0侵入,流程步骤解耦

    实现原理:将事务才分为多个小事务,创建事务表来记录事务,如工行向农行转账:

      工行:转账发起,在自己的服务中开启单机事务,保证转账发起扣钱和向数据库事务表中提交一条事务记录;创建定时任务读事务表新增的记录,读到新增的记录后,开启单机事务,保证把事务记录放入消息列队中,并更改事务记录状态为已提交到消息列队。

      农行:创建定时任务消费消息列队中的消息,当有新消息时,开启单机事务,保证把工行发过来的消息插入事务表中,并成功消费消息(否则等下次重新消费);创建定时任务读取事务表中的事务记录,当读取到新记录时,开启单机事务,保证收款账户余额增加,并更新事务表的事务记录状态为已处理。

  3、利用阿里开源项目seata:去seata官网,拉一个使用与当前框架的demo,根据demo的readme去配置seate server以及seata client即可。seata目前是分布式事务最好的解决方案,一个注解搞定。

三、seata原理(摘抄自官网)

前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

总结:

  seata自己根据生成分布式事务ID维护了一个全局锁,如A,B两个事务同时运行,A先拿到本地锁,做预处理(写undo_log),准备提交数据(此时B事务则等待A事务的本地锁);A再去seata申请全局锁,如果申请到了就提交数据,没申请到就等待,超时就回滚,全当A事务没来过;A申请到了全局锁,本地数据提交,在本地分支事务表中添加undo_log日志记录(用于回滚),然后释放本地锁(此时B事务就拿到本地锁,开始准备提交,但也要申请到全局锁才能提交;此时其他节点的B事务也不能提交,等全局锁);A事务全部执行完成,要么回滚要么成功(成功后删除本地分支事务表的undo_log),释放全局锁,这时B事务才能拿到全局锁,正在提交数据。

  seata回滚是有可能发生脏写:如果一个事务不经过seata,也就不受全局锁控制,直接修改数据,当seata事务回滚的时候,可能会发生脏写。所以用seata维护分布式事务,最好把涉及到seata事务的表的所有事务都交给seata来处理。

四、分布式锁

  1、利用zookeeper目录树节点的特性来做分布式锁,session创建的节点会随着session销毁,一个节点下创建相同名称的子节点时,zookeeper会为子节点维护一个序列号。当并发时,所有线程都去某个节点下创建session子节点,谁的序列号小,锁就归谁,而没有得到锁的线程就监听自己前面一个序列号,当前一个序列号失效时,在监听的回调方法中,认为已经获得锁做处理

  2、redis做锁,利用redis的setnx命令特性,setnx只有key不存在时,才能新建成功,所以只有一个线程会成功,其他都会失败,在创建key的时候应该要设置过期时间,避免死锁

原文地址:https://www.cnblogs.com/bigdatadiary/p/13538943.html