IE盒子

搜索
查看: 83|回复: 1

中间件

[复制链接]

4

主题

10

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2023-2-7 13:48:19 | 显示全部楼层 |阅读模式
ACID

MySQL 事务有四大特性:ACID

  • 原子性(Atomicity)
    A transaction must function as a single indivisible unit of work so that the entire transaction is either applied or rolled back;事务必须作为一个整体,要么都成功提交,要么同时失败(回滚)
    原子性,是由 undo log 来保证的
  • 一致性 (Consistency)
    The database should always move from one consistent state to the next
    依赖:1. 事务的其他 3 个特性;2. 业务代码逻辑正确,比如:库存为负数,就不可以再下单
  • 隔离性(Isolation)
    The results of a transaction are usually invisible to other transactions until the transaction is complete
    由 MySQL 的锁机制和 MVCC 机制来保证
  • 持久性(Durability)
    Once committed, a transaction’s changes are permanent
    This means the changes must be recorded such that data won’t be lost in a system crash
    持久性由 redo log 来保证
本文先从事务隔离性切入,只针对 InnoDB,有些概念会在后续的文章深入补充
隔离级别

事务并发过程中,会引入很多问题:脏读,不可重复读,幻读,更新丢失(脏写);隔离级别就是旨在解决这些问题
MySQL 定义了 4 个隔离级别:隔离级别越高,能解决更多的并发问题,但是性能越差

  • Read Uncommited:读未提交
    顾名思义,事务并发的情况下,事务 A 可以读取事务 B 更新但未提交的数据,那么如果事务 B 最终回滚了,那么 A 读到的就是脏数据(脏读)
    这个隔离级别,会有严重的并发问题,基本不用


  • Read Committed (简写为 RC): 读已提交
    这个隔离级别,解决了脏读的问题
    普通读操作的策略,是去读取当下最新已提交的数据,结果在不同的时间点是在动态变化的

    无法解决不可重复读、幻读问题
    不可重复读,对应这样一种场景:事务 A 在执行过程中多次读取同一条数据,但是在两次读的间隔时间中,有别的事务对这条数据作了修改并成功提交,那么事务 A 多次读取同一数据的结果是不相同的


  • Repeatable Read (简写为 RR):可重复读
    这个隔离级别,主要解决了上面提到的不可重复读问题
    普通读操作的策略,总是读取事务开始时(第一次查询)那个时间点所对应的最新已提交数据;目前可以简单地理解为对当时的数据库所有表打了一个快照,后面读的数据全都出自这个快照,对其他表也生效;当然实际肯定不是用快照方式实现的,会在后续文章中介绍

    但还会存在幻读问题:在一个事务的两次相同查询中,得到的数据条数不一致;
    举个例子:事务 A 在执行过程中需要多次读取同一个范围的数据,在两次读的间隔时间中,有其他事务在这个范围中插入或删除数据,导致事务 A 前后读到的范围中的数据条数不相同

    A phantom read can happen when you select some range of rows, another transaction inserts a new row into the range, and then you select the same range again; you will then see the new “phantom” row


  • Serializable:串行
    能解决所有事务并发问题,但是性能较差,基本不用
    实现原理:在所有读操作的时候,都会加上读锁 (lock in share mode);因为读锁和写锁是互斥的,所以读和写一定是串行的
    补充一点:如果你在其他隔离级别,手动给所有的读操作加上读锁,也能达到串行隔离级别的效果
最后再解释一下更新丢失(Lost Update)
实际上,串行之前的 3 个隔离级别,都会出现丢失更新的问题
一个典型的例子是对数据进行 `+=` 操作:比如在程序中,先用普通 select 语句读到一个值,比如 0,然后用代码作运算(比如 0 + 100),最后直接 "update table set value = 100 where id = XX;"。这样做的话,如果在 select 到 update 之间,有其他事务更新提交了同一条数据,那么当前事务提交 update 后,这个值一定会变成 100,其他事务所做的累加操作全都被覆盖了、丢掉了
推荐的做法是不要在程序中作运算,而是在更新时直接 "update table set value = value + 100 where id = XX;"。这里解释一下,MySQL 在执行写操作 (update / insert / delete) 时,需要对行加锁,会保证读取最新数据(原因参见下文),然后才进行运算、更新。这样就可以避免更新丢失问题
底层原理

本文先简单描述一下,详细分析会放在 MVCC 机制的文章中
MySQL 对写操作默认会加锁,如果对读操作也加锁,那就变成串行级别了,所以 MySQL 希望实现非阻塞并发读,对读操作不加锁,这样可以极大地提升并发性能
MySQL 对每一行记录都会维护 undo log 链,如下图所示。只有写操作会增加 undo log,先对该行加写锁(能加上写锁,表明之前其他事务的写操作全都结束了,要么提交、要么回滚,所以数据一定是最新的;同一时间只有一个事务可以进行更新,事务结束后才会释放锁),其次在修改前拷贝一份行数据追加到 undo log 中,然后再修改当前行数据、以及隐藏列:事务 ID (trx_id)、回滚指针(roll_ptr,指向上一步写到 undo log 的条目,作为回滚版本)


4 个隔离级别,在写操作部分的行为是一样的,区别在于读操作的策略:

  • 读未提交、串行:前者,直接读取索引中当前行的数据,哪怕事务未提交;后者,直接尝试对当前行加读锁。只用当前行就足够了,不需要再去扫描 undo log
  • RC、RR:会从下往上依次扫描当前行、undo log 这条链中的记录(每条记录都是一个版本),根据可见性算法,找到隔离级别所期望的版本数据。RC,读取最近已提交的版本;RR,读取事务开始时对应的版本。实现细节会在后面的文章中更新
另外,还要补充一点:如果当前行的事务 ID 就是自己,代表事务本身对这条数据进行了写操作,那么后续的读操作就会直接返回这个结果,因为是自己修改的;所以只有在读当前事务没有写过的数据时,才会执行上面的逻辑
补充问答

1. RC、RR 这两种事务隔离级别,如何选择使用哪一种?

RC 级别,更适合高并发场景,性能更高。而且 RC 不太依赖事务,还可以使用乐观锁
RR 级别,适合需要保证多个读查询在时间点上是统一的场景,比如报表统计;而且要注意必须在事务中,才能发挥出支持可重复读的特性,同时由于可重复读的特性,不能获取最新的版本号,所以无法使用乐观锁
2. RR 是否解决了幻读问题?

综合本文上面的内容,幻读是范围查询前后的数量不一致,MySQL 目前的机制,在不考虑写操作(即只有快照读)的情况下,实际上已经解决了幻读问题。
特例在于写操作,总是会读取最新数据(当前读),同时事务本身对数据的修改,在后续的读操作中总是直接作为结果返回,相当于覆盖了快照读中的结果。举一个例子:事务 A 先开始执行,然后事务 B 插入了一条 id = 10 的数据并提交,事务 A 在 RR 级别本来是看不到这条数据的,但是如果故意对自己看不到的条目进行更新,比如 "update table set value = new_value where id = 10;",那么就可以把别的事务新增的条目强行加入到事务 A 中,从而造成幻读问题
引用声明

本系列文章知识体系来源于图灵教育架构师课程,由本人在学习之后结合自己的理解、积累创作而成,已取得引用许可
回复

使用道具 举报

1

主题

10

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2025-6-24 06:09:36 | 显示全部楼层
呵呵,低调,低调!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表