本地事务总结
GOAL:ACID
Atomicity
Transactions are often composed of multiple statements. Atomicity guarantees that each transaction is treated as a single "unit", which either succeeds completely or fails completely: if any of the statements constituting a transaction fails to complete, the entire transaction fails and the database is left unchanged.
Consistency
Consistency ensures that a transaction can only bring the database from one consistent state to another, preserving database invariants
Isolation
Transactions are often executed concurrently (e.g., multiple transactions reading and writing to a table at the same time). Isolation ensures that concurrent execution of transactions leaves the database in the same state that would have been obtained if the transactions were executed sequentially.
Durability
Durability guarantees that once a transaction has been committed, it will remain committed even in the case of a system failure (e.g., power outage or crash.
Local Transaction
定义
本地事务/局部事务,是基础的事务解决方案。单个服务使用单个数据源。
依赖数据库本身的事务能力,比如开启,终止,提交,回滚,隔离级别等,如果数据库本身不具备,则无法提供相应的能力,比如MyISAM没有rollback
ARIES原理
基于语义的恢复与隔离算法,实现原子性(A),持久性(D),提供隔离性(I)
A和D
原因
数据需要写入磁盘等持久化存储,如果突然断电或者宕机,会造成数据丢失,即Crash
写入磁盘本身存在未写入,写入中,写入完成等状态,其中写入中为中间状态
- 未提交事务,写入后崩溃:修改3条数据,2条已经写入磁盘,剩余1条未写入。需要回滚,保证原子性。
- 已提交事务,写入前崩溃:修改3条数据,均未写入磁盘,崩溃。需要能够重新写入,保证持久性。
解决
由于Crash是不可避免的,所以需要Crash Recovery进行补偿
Commit Logging
提交日志:修改数据的操作需要的全部信息以日志的形式,顺序追加记录。成功提交的记录,追加日志(提交记录,Commit Record),数据库根据提交记录进行数据存储操作,完成后,追加日志(记录结束,End Record)。
- 有提交记录,没有记录结束: 持久性,顺序执行数据存储
- 有操作记录,无提交记录:原子性,执行数据回滚
性能问题:事务提交之前,不允许修改磁盘数据,如果修改数据量大,占用大量缓存区,不利于提升数据库性能。
Write Ahead Logging(ARIES)
提前写入:允许事物提交之前,写入变动数据
写入时机:
- FORCE:日志与磁盘写入同时完成。默认NO-FORCE,基于日志,可以随时写入
- STEAL:允许提前写入,充分利用磁盘IO
CommitLogging 允许no-force,但是不需要steal
WAL 增加额外的Undo Log,Redo Log,实现no-force + steal
- UNDO LOG:写入磁盘前,记录什么数据,从什么修改为什么等,用于Crash时,进行数据回滚/擦除
- Redo LOG:重演数据变动,持久性
恢复步骤:
- 分析阶段:从最后一次检查点(CheckPoint)扫描日志,获取所有没有记录结束(End Record)的日志事务,形成待恢复事务集合,包括Transaction Table,Dirty Page Table
- 重做Redo:Repeat History,找出所有的Commit Record,然后顺序写入磁盘,然后追加End Record
- 回滚Undo:剩下的事物日志(未Commit的)属于待回滚的(Loser),根据Undo Log中信息,将已经写入的信息,修改回去,完成回滚。
Redo和Undo都需要支持幂等,复杂的数据结构,主要来自ARIES的论文。
从优化磁盘 I/O 的角度看,NO-FORCE 加 STEAL 组合的性能无疑是最高的;从算法实现与日志的角度看 NO-FORCE 加 STEAL 组合的复杂度无疑也是最高的
Shadow Paging
数据的变动会写到硬盘的数据中,但并不是直接就地修改原先的数据,而是先将数据复制一份副本,保留原数据,修改副本数据。
在事务过程中,被修改的数据会同时存在两份,一份是修改前的数据,一份是修改后的数据,这也是“影子”(Shadow)这个名字的由来。
当事务成功提交,所有数据的修改都成功持久化之后,最后一步是去修改数据的引用指针,将引用从原数据改为新复制出来修改后的副本,最后的“修改指针”这个操作将被认为是原子操作,现代磁盘的写操作可以认为在硬件上保证了不会出现“改了半个值”的现象。
Shadow Paging 也可以保证原子性和持久性。Shadow Paging 实现事务要比 Commit Logging 更加简单,但涉及隔离性与并发锁时,Shadow Paging 实现的事务并发能力就相对有限,因此在高性能的数据库中应用不多。
I
原因
多个事物读写同一个数据的时候,会存在并发的可能,需要保证各个事务独立,不互相影响的同时,还要保证数据使用上的一致,所以需要实现数据库事务的隔离性。
锁
- 读锁:共享锁,某个事务对数据施加读锁,其他事务只能添加读锁,但不能添加写锁,即只能读取,不能修改。当数据只有一个读锁的时候,持有读锁的事务,可以升级为写锁。
- 写锁:排他锁,某个事物对数据施加写锁,其他事物无法添加读锁或者写锁,只能等待锁释放。
- 范围锁:范围排他锁,对某个范围的数据施加排他锁,select ... where 范围 for update
隔离级别
串行化SE
- 强制事务串行,事务互相隔离,不交叉
- 性能差
可重复读RR(幻读)
- 同一事务过程中,读取到的数据始终是一致的
- 其他事务对范围条件内数据新增时,造成当前事务实际读取到的结果集数量前后不一致,即幻读
读已提交RC(不可重复读)
- 同一事务中,读取到的数据都是已提交的数据,但多次读取相同数据,不保证数据一致
- 同一事务过程中,两次读取同一主键数据,中间其他事务修改数据,造成前后两次读取数据不一致,即不可重复读取
- 事务过程中,其他事物的提交会影响数据,违反隔离
读未提交RU(脏读)
- 同一事物中,读取到的数据可能是其他事务未提交的数据,不保证是最终数据,违反一致
- 事务过程中,受其他事务回滚会影响数据,违反隔离
LBCC - 基于锁的并发控制
RU
- 执行Update等操作时,添加事务范围的写锁
- Select等操作时,不添加任何锁,直接读取
- 事务A对数据添加排他锁,执行update语句,修改数据完成,未提交,另一个事务因为排他锁,无法对数据加读写锁,但是可以直接读取数据,且数据为修改未提交的数据,此时事务A执行回滚,导致已读取数据成为脏数据
RC
- 执行Update等操作时,添加事务范围的写锁
- 执行Select操作时,尝试添加瞬时读锁,操作完成释放
- 事务A对数据添加排他锁,执行update语句,修改数据完成,未提交,另一个事务尝试添加读锁进行查询,因为排他所以失败,未读取到数据,阻塞,避免了脏读
- 事务A查询数据,执行时添加瞬时读锁,读取到数值后释放读锁,未提交,此时另一个事务尝试修改数据,成功添加事务范围写锁,修改数据完成,提交,事务A再次查询数据,添加瞬时读锁,读取到新的数值,与上一次数值不一致。导致同一事务中,同一个数据前后数值不一致,不可重复读
RR
- 执行Update/Select操作时,添加事务范围的读写锁
- 事务A查询数据,执行时对数据添加读锁,读取后不释放,未提交,此时另一个事务尝试修改数据,发现数据有读锁,添加写锁失败,阻塞,避免了不可重复读
- 事务A进行范围查询,例如日期=今天,范围结果数量为100,对这100条数据添加读写锁,未提交,此时另一个事务在相同范围内,添加新的数据记录,使符合日期=今天的结果数量为101,事务A再次进行范围查询,相同条件,查询出101条数据,导致同一事务中,前后数据集不一致,即幻读
SE
- 表锁 或 基于索引的间隙锁,均为排他锁
问题
基于锁来实现数据库的隔离机制,主要是通过排他来进行阻塞,保证数据多次查询的一致。但是加锁的代价很高,例如RC时,select操作需要添加瞬时的共享锁,加解锁频繁,效率低
MVCC - 基于版本的并发控制(Mysql)
事务版本属性
每次事务会生成一个递增的事务ID,每行数据扩展两个属性,操作事务ID,回滚指针(Undo Log)
查询:数据的操作事务ID <= 当前事务ID
新增:数据的操作事物ID -> 当前事务ID,回滚指针 -> null
删除:数据变化写入Undo Log,操作事务ID -> 当前事务ID,回滚指针 -> Undo Log,标记删除 -> 当前事务ID,purge后物理删除
更新:数据变化写入Undo Log,数据的更新,操作事务ID -> 当前事务ID,回滚指针 -> Undo Log
Read View 读视图
快照读:查询操作(Select)通过读视图进行,不加锁
当前读:新增,更新,删除操作(insert,update,delete)加锁,使用最新数据
读视图提供:
- m_ids : 当前活跃事务ID,即进行中未提交的事务
- min_id:活跃事务中的最小事务ID
- max_id:下一次新事务的ID,活跃事务最大ID+1
- creator_id:创建该视图的事务ID
版本控制判断
- 数据的操作事务ID < min_id,当前事务查询开始前已经被提交的数据
- 数据的操作事物ID >= max_id,当前事物查询开始后的产生的未知状态数据
- 数据的操作事务ID = 当前事务ID,当前事务操作中未提交的数据
- 数据的操作事务ID在m_ids中,正在被其他事务操作未提交的数据
- 数据的操作事务ID不在m_ids中,已经被提交的数据
RU
- 不需要版本控制,单纯的写锁,避免脏写
RC
- 每次查询都会生成新的ReadView
- 事务A(ID=8)启动时,生成ReadView([8],8,9,8),读取数据S(v=100,tid=7),小于mid
- 事务B(ID=9)启动,生成ReadView([8,9],8,10,9),读取数据S(v=100,tid=7),小于mid
- 事务A修改数据S,生成新的数据S(v=200,tid=8,rp=7),未提交
- 事务B再次读取数据S,发现(v=200,tid=8),生成新的ReadView([8,9],8,10,9),tid在readview中的m_ids中,是其他事务未提交数据,不使用,根据rp,向前查找,找到tid=7,小于mid,读取(v=100,tid=7)
- 事务A提交数据S
- 事务B再次读取数据S,发现(v=200,tid=8),生成新的ReadView([9],9,10,9),tid小于mid,直接使用
- 如果反向,事务B执行修改,并先于A提交,事务A第二次查询的时候,生成新的ReadView([8],8,10,8),读取数据S,发现(v=200,tid=9),min < tid < max_id,tid不在m_ids中,为其他事务已提交数据,直接使用
- 实现了提交读,但是统一事务中,前后两次数据不一致,存在不可重复读
RR
- 事务过程中使用开始时生成的ReadView进行读取,查询不更新ReadView
- 事务A(ID=8)启动时,生成ReadView([8],8,9,8),读取数据S(v=100,tid=7),小于mid
- 事务B(ID=9)启动,生成ReadView([8,9],8,10,9),读取数据S(v=100,tid=7),小于mid
- 事务A修改数据S,生成新的数据S(v=200,tid=8,rp=7),未提交
- 事务B再次读取数据S,发现(v=200,tid=8),tid在自己readview中的m_ids中,是其他事务未提交数据,不使用,根据rp,向前查找,找到tid=7,小于mid,读取(v=100,tid=7)
- 事务A提交数据S
- 事务B再次读取数据S,发现(v=200,tid=8),因为readview不更新,m_ids中不会移除已完成实物8,所以仍然认为数据是其他事务未提交数据,仍然使用tid=7的数据
- 通过控制readview的生命周期,可以实现同一事务中的可重复读,同时解决了范围查询的幻读
- 当事务B是只读场景时,执行的过程中,事务A对数据进行了修改,或者新增了查询范围内的数据,事务B并不能读取到最新版本的数据,存在单条数据的幻读,或者数据不全
当事务B是读写场景时,执行写操作的时候,会强制使用数据的最新版本,同时增加行锁,间隙锁(索引节点间加锁,R-S-T),避免其他事务进行数据操作
- 如果B的写操作在A的写操作之前,A被阻塞,阻止了数据变化,避免了幻读
- 如果B的写操作在A的写操作之后,A能够成功更新数据,依旧会发生幻读
SE
- 使用排他锁机制
本文由 Ivan Dong 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 12, 2023 at 04:01 am