mysql事务

事务隔离级别

读未提交:一个事务还没提交时, 它做的变更就能被别的事务看到。

读提交:一个事务提交之后, 它做的变更才会被其他事务看到。

可重复读: 一个事务执行过程中看到的数据, 总是跟这个事务在启动时看到的数据是一致的。 当然在可重复读隔离级别下, 未提交变更对其他事务也是不可见的。

串行化:顾名思义是对于同一行记录, “写”会加“写锁”, “读”会加“读锁”。 当出现读写锁冲突的时候, 后访问的事务必须等前一个事务执行完成, 才能继续执行。

原理

  • 数据库里面会创建一个视图, 访问的时候以视图的逻辑结果为准
  • 在“可重复读”隔离级别下, 这个视图是在事务启动时创建的, 整个事务存在期间都用这个视图
  • 在“读提交”隔离级别下, 这个视图是在每个SQL语句开始执行的时候创建
  • 在“读未提交”隔离级别下直接返回记录上的最新值, 没有视图概念
  • 在“串行化”隔离级别下直接用加锁的方式来避免并行访问
  • 事务的原子性是通过 undo log来实现的(回滚需要undo log)
  • 事务的持久性性是通过 redo log来实现的(故障后恢复)
  • 事务的隔离性是通过 (读写锁+MVCC)来实现的(读写分离,读读并行,读写并行)
  • 而事务的一致性是通过原子性,持久性,隔离性来实现的

幻读解决

RR级别通过next-key解决幻读问题,通过锁定范围内的数据行防止其它事务插入或删除范围内的数据。

间隙锁

间隙锁(Gap Lock),顾名思义,它会封锁索引记录中的“缝隙”,不让其他事务在“缝隙”中插入数据。 它锁定的是一个不包含索引本身的开区间范围 (index1,index2)。间隙锁是封锁索引记录之间的间隙,或者封锁第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。说白了,间隙锁的目的就是为了防止索引间隔被其他事务的 “插入”。 间隙锁是InnoDB权衡性能和并发性后出来的特性。间隙锁只发生在事务隔离级别为RR(Repeatable Read)的情况下,它用于在隔离级别为RR时,阻止幻读(phantom row)的发生;隔离级别为RC时,搜索和索引扫描时,Gap锁是被禁用的,只在外键约束检查和 重复key检查时Gap锁才有效,正是因为此,RC时会幻读问题。

ps:比如某个事务执行select * from table where id between 10 and 20 for update;语句,当其他事务往表里 插入 id在(10,20)之间的值时,就会被(10,20)的间隙锁给阻塞。

临键锁

临键锁(Next-Key Lock)其实并不能算是一把新的行锁,其实际就是记录锁(Record Lock)和间隙锁(Gap Lock)的组合,既封锁了"缝隙",又封锁了索引本身,既组合起来构成了一个半开闭区间的锁范围。既临键锁锁的是索引记录本身,以及索引记录之前的间隙(index1, index2]

比如某表有4行数据,主键分别为10,11,13,20。那么该表可能存在的临键锁锁住聚簇索引的区间如下

(negative infinity, 10] (10, 11] (11, 13] (13, 20] (20, positive infinity)

每两个索引项之间就是一个next key锁区域,通常规则是左开右闭 ,除了最后一个锁区间,是全开区间,代表锁住索引最大项之后的区域

ps:记录锁,间隙锁,临键锁的关系

InnoDB的行锁默认是基于B+Tree的。所以行锁依赖的索引是有序的。记录锁就是单纯意义上的行锁,锁的就是单行数据,该数据是真实存在的;

间隙锁则是锁住一个区间中多行数据,但这些多行的数据实际是并不存在的。既只锁住真实数据对应索引项之间的一个空间范围;

临键锁就是记录锁+间隙锁的组合。只要把记录锁和间隙锁组合在一起,就是临键锁,既锁住索引项本身的真实数据,又锁住两两索引之间没有数据的空间范围。

重复读解决

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。但是不能解决写-写更新丢失问题。

ps:

当前读:就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

快照度:像不加锁的select * from 操作就是快照读,即不加锁的非阻塞读,不涉及其他锁之间的冲突(快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读)

在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。

多版本并发控制(MVCC)为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 MVCC模型在MySQL中的具体实现则是由 3个隐式字段,undo日志 ,Read View 等去完成的。

隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

DB_TRX_ID:6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID

DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)

DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

undo日志

undo log日志主要分为两种:

        insert undo log:代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

        update undo log:事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读(select,当读的过程中有写的事务开始和提交,会造成读数据的脏读、不可重复读、幻读等)时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。

purge:更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

update过程:

一、 比如一个有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

在事务1修改该行(记录)数据时,数据库会先对该行加排他锁。然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本。拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它。事务提交后,释放锁。

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

在事务2修改该行数据时,数据库也先为该行加锁。然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面。修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录。

事务提交,释放锁。

Read View(读视图)

Read View,说白了Read View就是事务进行快照读(select * from)操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成事务系统当前的一个快照,记录并维护系统当前活跃事务(未提交事务)的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

ReadView中主要包含4个比较重要的内容:

1. m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。

2. min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。

3. max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。

4. creator_trx_id:表示生成该ReadView的快照读操作产生的事务id。

ps:read view中活跃就是指未提交的事务, 注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1, 2, 3这三个事务,之后id为3的事务提交了。

那么一个新的读事务在生成ReadView时, m_ids就包括1和2, min_trx_id的值就是1,max_trx_id的值就是4。

查询时主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。

在访问某条记录时,按照下边的步骤判断记录的某个版本是否可见:

1)如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

2)如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。

3)如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。

4)如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

注:在RC(读已提交)隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR(可重复读)隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。



对于当前事务的启动瞬间来说, 一个数据版本的row trx_id, 有以下几种可能:

1. 如果落在绿色部分, 表示这个版本是已提交的事务或者是当前事务自己生成的, 这个数据是可见的;

2. 如果落在红色部分, 表示这个版本是由将来启动的事务生成的, 是肯定不可见的;

3. 如果落在黄色部分, 那就包括两种情况

a. 若 row trx_id在数组中, 表示这个版本是由还没提交的事务生成的, 不可见;

b. 若 row trx_id不在数组中, 表示这个版本是已经提交了的事务生成的, 可见。

1. 事务A开始前, 系统里面只有一个活跃事务ID是99;

2. 事务A、 B、 C的版本号分别是100、 101、 102, 且当前系统里只有这四个事务;

3. 三个事务开始前, (1,1) 这一行数据的row trx_id是90。

这样, 事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是[99,100,101,102]。

从图中可以看到, 第一个有效更新是事务C, 把数据从(1,1)改成了(1,2)。 这时候, 这个数据的最新版本的row trx_id是102, 而90这个版本已经成为了历史版本。

第二个有效更新是事务B, 把数据从(1,2)改成了(1,3)。 这时候, 这个数据的最新版本(即rowtrx_id) 是101, 而102又成为了历史版本。

你可能注意到了, 在事务A查询的时候, 其实事务B还没有提交, 但是它生成的(1,3)这个版本已经变成当前版本了。 但这个版本对事务A必须是不可见的, 否则就变成脏读了。

好, 现在事务A要来读数据了, 它的视图数组是[99,100]。 当然了, 读数据都是从当前版本读起的。

所以, 事务A查询语句的读数据流程是这样的:

找到(1,3)的时候, 判断出row trx_id=101, 比高水位大, 处于红色区域, 不可见;

接着, 找到上一个历史版本, 一看row trx_id=102, 比高水位大, 处于红色区域, 不可见;

再往前找, 终于找到了(1,1), 它的row trx_id=90, 比低水位小, 处于绿色区域, 可见。

这样执行下来, 虽然期间这一行数据被修改过, 但是事务A不论在什么时候查询, 看到这行数据的结果都是一致的, 所以我们称之为一致性读。

事务B在更新之前查询一次数据, 这个查询返回的k的值确实是1。但是, 当它要去更新数据的时候, 就不能再在历史版本上更新了, 否则事务C的更新就丢失了。

因此, 事务B此时的set k=k+1是在(1,2) 的基础上进行的操作。所以, 这里就用到了这样一条规则: 更新数据都是先读后写的, 而这个读, 只能读当前的

值, 称为“当前读”( current read) 。因此, 在更新的时候, 当前读拿到的数据是(1,2), 更新后生成了新版本的数据(1,3), 这个新版本的row trx_id是101。

所以, 在执行事务B查询语句的时候, 一看自己的版本号是101, 最新数据的版本号也是101, 是自己的更新, 可以直接使用, 所以查询得到的k的值是3。

这里我们提到了一个概念, 叫作当前读。 其实, 除了update语句外, select语句如果加锁, 也是当前读。

所以, 如果把事务A的查询语句select * from t where id=1修改一下, 加上lock in share mode 或for update, 也都可以读到版本号是101的数据, 返回的k的值是3。


事务C’没提交, 也就是说(1,2)这个版本上的写锁还没释放。 而事务B是当前读, 必须要读最新版本, 而且必须加锁, 因此就被

锁住了, 必须等到事务C’释放这个锁, 才能继续它的当前读。

参考:

《Mysql45讲》 

正确的理解MySQL的MVCC及实现原理

【MySQL笔记】正确的理解MySQL的MVCC及实现原理

互联网项目中mysql应该选什么事务隔离级别

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值