目录
意向共享锁(IS)
表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
意向排他锁(IX)
表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
记录锁(Record Locks)
锁定指定行的索引项
间隙锁(Gap Locks)
锁定某一个范围内(以(主键列、辅助索引列)为间隙点,两个间隙点之间的数据区域加锁)的索引,但不包括记录本身;防止幻读、防止间隙内有新数据插入、防止已存在的数据更新为间隙内的数据。
间隙锁定(Next-Key Locks)
锁定一个范围内的索引,并且锁定记录本身: Next-Key Locks = Record Locks + Gap Locks
死锁
TODO
锁模式 | 排他锁(X) | 意向排他锁(IX) | 共享锁(S) | 意向共享锁(IS) |
---|---|---|---|---|
排他锁(X) | N | N | N | N |
意向排他锁(IX) | N | Y | N | Y |
共享锁(S) | N | N | Y | Y |
意向共享锁(IS) | N | Y | Y | Y |
X和所有锁冲突;IX和IX,IS兼容;S和S,IS兼容;IS和S,IS,IX兼容
Dirty page:脏页
一般业务运行过程中,当业务需要对某张的某行数据进行修改的时候,innodb会先将该数据从磁盘读取到缓存中去,然后在缓存中对这条数据进行修改,这样缓存中的数据就和磁盘的数据不一致了,这个时候缓存中的数据就称为dirty page,只有当脏页统一刷新到磁盘中才会是clean page
控制线程:用来表示一个工作线程,主要是关联AP,TM,RM三者的一个线程,也就是事务上下文环境。简单的说,就是需要标识一个全局事务以及分支事务的关系
原子性(Atomicity)
事务执行的最小单元;一个事务内的CRUD操作要么全部执行完成,要么全部不执行。MySQL是通过redo log实现原子性的。
一致性(Consistency)
数据状态总是要保持一致性,从一个一致性状态转为另一个一致性状态。MySQL是通过undo log实现一致性的。
隔离性(Isolation)
事务之间相互隔离。MySQL是通过锁来实现隔离性的。
持久性(Durability)
指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。MySQL是通过redo log 实现的持久性。
数据库存在立即修改和延迟修改,所以在事务执行过程中可能存在以下情况
问题 | 描述 |
---|---|
更新丢失(Lost update) | 两个事务都同时更新一行数据但是第二个事务却中途失败退出(回滚)导致对数据两个修改都失效了。 |
脏读取(Dirty Reads) | 一个事务开始读取了某行数据但是另外一个事务已经更新了此数据但没有能够及时提交。可能所有操作都被回滚。 |
不可重复读取(Non-repeatable Reads) | 一个事务对同一行数据重复读取两次但是却得到了不同结果。是因为另一个事务在T1两次查询之间更新了数据,导致T1的第二次查询不一致。 |
两次更新问题(Second lost updates problem) | 有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交这就会造成第一次写操作失效。不可重复读取特例。 |
幻读(Phantom Reads) | 事务在操作过程中进行两次查询,第二次查询结果包含了第一次查询中未出现的数据。产生幻读的原因是事务一在进行范围查询的时候没有增加范围锁。对应的是insert操作。 |
时间 | 事务A | 事务B | 时间 | 事务A | 事务B |
---|---|---|---|---|---|
T1 | 开始事务 | T1 | 开始事务 | ||
T2 | 开始事务 | T2 | 开始事务 | ||
T3 | 查询余额500 | T3 | 查询余额500 | ||
T4 | 查询余额500 | T4 | 增加100,余额600 | ||
T5 | 增加100,余额600 | T5 | 查询余额600(脏读) | ||
T6 | 增加100,余额600 | T6 | 回滚事务,余额500 | ||
T7 | 提交事务 | T7 | 增加100,余额700 | ||
T8 | 回滚事务,余额500(更新丢失) | T8 | 提交事务 |
时间 | 事务A | 事务B | 时间 | 事务A | 事务B |
---|---|---|---|---|---|
T1 | 开始事务 | T1 | 开始事务 | ||
T2 | 开始事务 | T2 | 开始事务 | ||
T3 | 查询余额500 | T3 | 查询余额500 | ||
T4 | 查询余额500 | T4 | 查询余额500 | ||
T5 | 增加100,余额600 | T5 | 增加200,余额600 | ||
T6 | 提交事务 | T6 | 提交事务 | ||
T7 | 查询余额600(不可重复读取) | T7 | 减少100,余额400 | ||
T8 | 提交事务 | T8 | 提交事务(两次更新问题) |
时间 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计出100条 | |
T4 | 插入一条记录 | |
T5 | 提交事务 | |
T6 | 统计出101条(幻读) | |
T7 | 提交事务 | |
T8 |
隔离级别 | 描述 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|---|
未提交读Read Uncommitted | 一个事务可以读到另外一个事务未提交的数据 | 是 | 是 | 是 | 是 |
已提交读Read Committed | 一个事务修改数据过程中,如果事务还没提交,其他事务不能读该数据 | 是 | 否 | 是 | 是 |
可重复读Repeatable Read | 在一个事务内两次读取同一条记录结果却不一致。 | 是 | 否 | 否 | 是 |
可串行化Serializable | 事务间顺序执行 | 是 | 否 | 否 | 否 |
隔离级别 | 实现原理 | 描述 |
---|---|---|
未提交读 | 事务在读数据时候并未对数据加锁;事务在更新数据时,对其加 行级排它锁,写完释放 | T1读r时,T2可读r也可写r(S); T1写r时(S),T2可读r不可写r(S)。不能同时更新 |
已提交读 | 事务在读取数据时,对其加 行级共享锁,读完释放;事务在更新数据时,对其加 行级排他锁,事务结束时释放 | T1读r(S),T2可读r(S)不可写r(X),T1读完释放后可写(X); T1写r时(X),T2不可读r(S)不可写r(X) |
可重复读 | 事务在读取数据时,对其加 行级共享锁,事务结束时释放;事务在更新数据时,对其加 行级排他锁,事务结束时释放 | T1读r时(S),T2可读r(S)不可写r(X);T1写r时(X),T2不可读r(S)不可写r(X) |
可串行化 | 事务在读取数据时,对其加 表级共享锁,事务结束时释放;事务在更新数据时,对其加 表级排他锁,事务结束时释放 | T1读r时(S),T2可读r(S)不可写r(X);T1写r时(X),T2不可读r(S)不可写r(X) |
隔离级别 | 操作 | 锁 | 生命周期 | 操作 | 锁 | 生命周期 |
---|---|---|---|---|---|---|
未提交读 | 读 | 写 | 行级排它锁 | 立即释放 | ||
已提交读 | 读 | 行级共享锁 | 立即释放 | 写 | 行级排它锁 | 事务结束 |
可重复读 | 读 | 行级共享锁 | 事务结束 | 写 | 行级排它锁 | 事务结束 |
可串行化 | 读 | 表级共享锁 | 事务结束 | 写 | 表级排他锁 | 事务结束 |
内容
逻辑日志,记录了对MySQL数据库执行更改的所有操作。保存了事务发生之前的数据的一个版本。
作用
提供回滚和多个行版本控制(MVCC)。确保事务的一致性。在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。
什么时候产生
事务开始之前,将当前是的版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性
什么时候释放
当事务提交之后,undo log并不能立马被删除,
而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。
内容
物理日志,记录的是数据页的物理修改后的值,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
作用
确保事务的持久性+原子性。防止在发生故障的时间点的时候,尚有脏页未写入磁盘。在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
什么时候产生
在事务开始时就产生Redo log,日志会先被写入内存中的日志缓冲区(redo log buffer);在满足某条件时,日志被写入磁盘上的重做日志文件(redo log file)。
什么时候释放
当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。
内容
逻辑格日志,可以简单认为就是执行过的事务中的sql语句。但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息,
也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。
什么时候产生:
事务提交的时候,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。这里与redo log很明显的差异就是redo log并不一定是在事务提交的时候刷新到磁盘,redo log是在事务开始之后就开始逐步写入磁盘。
因此对于事务的提交,即便是较大的事务,提交(commit)都是很快的,但是在开启了bin_log的情况下,对于较大事务的提交,可能会变得比较慢一些。这是因为binlog是在事务提交的时候一次性写入的造成的,这些可以通过测试验证。
什么时候释放:
binlog的默认是保持时间由参数expire_logs_days配置,也就是说对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后,会被自动删除。
对应的物理文件:
配置文件的路径为log_bin_basename,binlog日志文件按照指定大小,当日志文件达到指定的最大的大小之后,进行滚动更新,生成新的日志文件。
对于每个binlog日志文件,通过一个统一的index文件来组织。
一般所说的log file并不是磁盘上的物理日志文件,而是操作系统缓存中的log file,官方手册上的意思也是如此。所以在本文后续内容中都以os buffer或者file system buffer来表示官方手册中所说的Log file,然后log file则表示磁盘上的物理日志文件,即log file on disk。另外,之所以要经过一层os buffer,是因为open日志文件的时候,open没有使用O_DIRECT标志位,该标志位意味着绕过操作系统层的os buffer,IO直写到底层存储设备。不使用该标志位意味着将日志进行缓冲,缓冲到了一定容量,或者显式fsync()才会将缓冲中的刷到存储设备。使用该标志位意味着每次都要发起系统调用。比如写abcde,不使用o_direct将只发起一次系统调用,使用o_object将发起5次系统调用。
MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。
事务其实就是并发控制的基本单位;相信我们都知道,事务是一个序列操作,其中的操作要么都执行,要么都不执行,它是一个不可分割的工作单位;数据库事务的 ACID 四大特性是事务的基础,了解了 ACID 是如何实现的,我们也就清除了事务的实现,接下来我们将依次介绍数据库是如何实现这四个特性的。
原子性的定义就是一个事务内的一系列操作,要么全部成功,要么全部不成功。所以保证全部成功的日志文件就是redo log,它会使所有操作记录持久化到磁盘;如果又一部分操作失败了,则需要将全部操作回滚,能完成这个功能的就是undo log,undo log会记录操作前的版本,可以将此次操作回滚到之前的版本状态。
持久性的实现也是通过重做日志(redo log)。重做日志在生成的时候分两部分,一部分会先写入内存中的日志缓冲区,再满足某条件时,将缓冲区的日志持久化到磁盘。
数据库为了保证事务之间的隔离性,提出了四个隔离级别,数据库对于隔离级别的实现就是使用并发控制机制对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新,而最重要也最常见的并发控制机制,在这里我们将简单介绍三种最重要的并发控制器机制的工作原理。
快照隔离是多版本并发控制(mvcc)的一种实现方式。
由于快照隔离导致事务看不到其他事务对数据项的更新,为了避免出现丢失更新问题,可以采用以下两种方案避免:
事务间可能冲突的操作通过数据项的不同版本的快照相互隔离,到真正要写入数据库时才进行冲突检测。因而这也是一种乐观并发控制。
全局事务类似于在本地事务基础上做了一层嵌套,实际上多次事务提交和回滚对性能影响较大,占用的资源也比较多。而且全局事务遇到mysql_connection.commit后宕机,尽管oracle_connection提交失败需要回滚,此时已经无法回滚mysql_connection,存在数据不一致的风险。
use order_db;
## 分别开启事务
mysql_connection.begin;
oracle_connection.begin;
## 分别执行操作
update mysql_orders set order_money = order_money + 0.01 where id = 171517987004616228;
update oracle_orders set order_money = order_money + 0.01 where id = 171517987004616228;
## 分别提交,如果有一个失败,则分别进行rollback操作
mysql_connection.commit;
oracle_connection.commit;
TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖资源管理器(RM)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
TCC方案其实是两阶段提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。
事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。
对应用的侵入性强。
业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
实现难度较大。
需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
- 1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
- 2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
- 3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
- 1)协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
- 2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
- 3)参与者节点向协调者节点发送”完成”消息。
- 4)协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
- 1)协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
- 2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
- 3)参与者节点向协调者节点发送”回滚完成”消息。
- 4)协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
1、同步阻塞问题。
执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。
由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。
在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
4、二阶段无法解决的问题
协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
与两阶段提交不同的是,三阶段提交有两个改动点。
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
1.事务询问
协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
2.响应反馈
参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
消息一致性方案是通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。
消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本较高。
同步事件
可靠事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。首先能想到的也是最简单的就是同步事件通知,业务处理与消息发送同步执行,实现逻辑见下方代码及时序图。
public void trans() {
try {
// 1. 操作数据库
bool result = dao.update(data);// 操作数据库失败,会抛出异常
// 2. 如果数据库操作成功则发送消息
if(result){
mq.send(data);// 如果方法执行失败,会抛出异常
}
} catch (Exception e) {
roolback();// 如果发生异常,就回滚
}
}
上面的逻辑看上去天衣无缝,如果数据库操作失败则直接退出,不发送消息;如果发送消息失败,则数据库回滚;如果数据库操作成功且消息发送成功,则业务成功,消息发送给下游消费。然后仔细思考后,同步消息通知其实有两点不足的地方。
最大努力通知模式
相比可靠事件通知模式,最大努力通知模式就容易理解多了。最大努力通知型的特点是,业务服务在提交事务后,进行有限次数(设置最大次数限制)的消息发送,比如发送三次消息,若三次消息发送都失败,则不予继续发送。所以有可能导致消息的丢失。同时,主业务方需要提供查询接口给从业务服务,用来恢复丢失消息。最大努力通知型对于时效性保证比较差(既可能会出现较长时间的软状态),所以对于数据一致性的时效性要求比较高的系统无法使用。这种模式通常使用在不同业务平台服务或者对于第三方业务服务的通知,如银行通知、商户通知等,这里不再展开。
业务补偿模式
接下来介绍两种补偿模式,补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。首先介绍业务补偿模式,业务补偿模式是一种纯补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。举个例子,小明从杭州出发,去往美国纽约出差,现在他需要定从杭州去往上海的火车票,以及从上海飞往纽约的飞机票。如果小明成功购买了火车票之后发现那天的飞机票已经售空了,那么与其在上海再多待一天,小明还不如取消去上海的火车票,选择飞往北京再转机纽约,所以小明就取消了去上海的火车票。这个例子中购买杭州到上海的火车票是服务a,购买上海到纽约的飞机票是服务b,业务补偿模式就是在服务b失败的时候,对服务a进行补偿操作,在例子中就是取消杭州到上海的火车票。
补偿模式要求每个服务都提供补偿借口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。
业务补偿模式最大的缺点是软状态的时间比较长,既数据一致性的时效性很低,多个服务常常可能处于数据不一致的情况。
原文:https://www.cnblogs.com/shiyusen/p/10644474.html