数据库相关-事务[2]

171 阅读11分钟

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

隔离级别

  • 隔离级别是用来针对数据不一致的情况进行修复的。
  • 隔离效果越好,并行执行效率也就越差。(要做各种检查以及锁定)

读未提交(Read Uncommitted)

Read Uncommitted是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。

读未提交,可以称为最基本的隔离级别,并没有什么隔离能力。

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用。因为采用这种隔离级别只能防止第一类更新丢失问题,不能解决脏读,不可重复读及幻读问题。

读已提交

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别可以防止脏读问题,但会出现不可重复读及幻读问题。

可重复读

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这种隔离级别可以防止除幻读外的其他问题。

串行化

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读、第二类更新丢失问题。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争,通常数据库不会用这个隔离级别,我们需要其他的机制来解决这些问题:间隙锁,乐观锁,悲观锁。

从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。

Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。

MVCC

//todo

查看隔离级别

查看当前数据库全局设置的隔离级别:

select @@global.tx_isolation;

主流的互联网公司(包括目前的公司),使用的都是读已提交(Read Commited)

隔离级别为RC的原因

zhuanlan.zhihu.com/p/59061106

笔记
  • MySQL的默认隔离级别为什么是RR?

    • 主从复制:基于binlog进行复制

      • binlog:简单理解为binlog是一个记录数据库更改的文件 //todo

        • binlog格式

          • statement:记录的是修改SQL语句
          • row:记录的是每行实际数据的变更
          • mixed:statement和row模式的混合
      • MySQL在5.0之前binlog只支持STATEMENT格式

      • 而这种格式在读已提交(Read Commited) 这个隔离级别下主从复制是有bug的

        • img

          此时在主(master)库上执行下列语句

          select * from test;
          

          输出如下

          +---+| b |+---+| 3 |
          ​
          ​
          +---+
          ​
          ​
          1 row in set
          

          但是,你在此时在从(slave)库上执行该语句,得出输出如下

          Empty set
          

          这样,你就出现了主从不一致性的问题!原因其实很简单,就是在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删!(也就是说binlog记录实际上是按照事务提交顺序先后来的,这里session2先提交,所以binlog先记录执行插入的SQL语句,然后才是session1),从(slave)库同步的是binlog,因此从机执行的顺序和主机不一致!就会出现主从不一致! 如何解决? 解决方案有两种! (1)隔离级别设为可重复读(Repeatable Read) ,在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住! (2)将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read) ,保证主从复制不出问题!

      • 因此Mysql将可重复读(Repeatable Read) 作为默认的隔离级别。

    • 选RC不选RR的原因:

      • 缘由一:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多
      • 缘由二:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行
      • 缘由三:在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性 //todo 半一致读
    • 在RC级别下,不可重复读问题需要解决么?

      • 不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!
    • 在RC级别下,主从复制用什么binlog格式?

      • 在该隔离级别下,用的binlog为row格式,是基于行的复制。

分布式事务

相关未涉及名词:JTA XA事务

落地实践:

分布式的CAP理论

C(Consistency,一致性):在一个分布式的系统中,同一个数据所有备份,在同一时刻是否有相同的值。也就是,对于同一个数据的读写,是否立刻对于所有副本都能看到一致的结果。一种比较常见的强一致性实现就是,在看到一致的结果之前,写请求不返回,读请求阻塞或者超时。

A(Availability,可用性):在集群中一些节点故障时,集群还可以响应读写请求

P(Partition-tolerance,分区容忍性):分布式系统具有多个节点,如果节点间网络中断,就会造成分区

参考链接:

juejin.cn/post/693188…

www.cnblogs.com/mayundalao/…

zhuanlan.zhihu.com/p/183753774

note:在第三篇文章里,redis是没有事务的。那么,redis真的没有事务吗? //todo

两阶段提交(2PC)

两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。

二阶段提交是一种强一致性设计

自己使用过的例子:

  • 礼品更新

    • 活动-礼品-前台配置,活动和礼品是同个库里,前台配置在另一个库中,前台的配置依赖活动和礼品的ID。

      • 先更新礼品和活动,更新成功了再通过feign调用前台配置,如果失败,feign代理类手动抛出异常,通过Transactional注解进行事务的代理回滚。

运行过程

  1. 准备
  • 协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
  1. 提交
  • 如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
  • 需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

存在的问题

2.1 同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。

2.2 单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。

2.3 数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。

2.4 太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

总结

2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。2PC 适用于数据库层面的分布式事务场景.

三阶段提交(3PC)

相较于2PC,多了一步:预先确认资源。

补偿事务(TCC)

所谓的TCC即是:

  • Try 阶段主要是对业务系统做检测及资源预留。
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

TCC和2PC不同点在于,2PC的回滚是DB层面的(基于DB层面提供的事务功能),TCC的补偿机制是通过服务的补偿代码实现的,二者实现的层级不同。

本地消息表

本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。

本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。

然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。

如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。

这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。

可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。

即:

  1. 用持久化存储,将信息存储和本地事务的执行放在一个事务中,保证本地事务和forward的调用日志是统一的(即:forward日志的前提一定会是,本地事务已经执行成功了)。
  2. 本地事务执行完毕,往前继续调用其他服务的事务执行。
  3. 调用失败的情况:不修改forward消息发送表,通过job系统定期进行重试。如果超过一定次数,进行报警通知人工干预。
  4. 实现的是最终一致性

消息事务

流程(RocketMQ)

  1. 向broker发送一条半消息(定义:消费者不可见,可以定义为预发送),发送成功后,再执行本地事务。

  2. 根据本地事务执行结果(成功/失败),向broker发送消息(commit/rollback),同时根据这个事务的执行情况,向broker提供提供查询事务状态的接口(成功/执行中/失败)。

    • 超时情况:如果一段时间内半消息没有收到任何操作请求zhuanlan.zhihu.com/p/183753774原话),broker会根据2中提供的接口,手动查询成功失败,根据状态进行相关操作。
  3. broker将消息commit或rollback。后续的就和正常的消息队列流程一样了。

最大努力通知

最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。

\