MySQL锁与事务隔离

锁分类

读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁。
表锁:每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲 突的概率最高,并发度最低。

  • 加表锁:lock table 表名称 read(当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待)、write(当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞,包括不能查询);
  • 查看所有表锁:show open tables;
  • 解开加过的表锁:unlock tables;

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改 操作前,会自动给涉及的表加写锁,串行化的。也可以打破这个限定,MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。

  • 当concurrent_insert设置为0时,不允许并发插入。
  • 当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。
  • 当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。

可以利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。

行锁:每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁 冲突的概率最低,并发度最高。

事务属性

ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为 事务的ACID属性。

  • 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执 行,要么全都不执行。
  • 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意 味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束 时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
  • 隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并 发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是 不可见的,反之亦然。
  • 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系 统故障也能够保持。

并发事务处理带来的问题

  • 更新丢失(Lost Update) 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每 个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其 他事务所做的更新。
  • 脏读(Dirty Reads) 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数 据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控 制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提 交的数据依赖关系。这种现象被形象的叫做“脏读”。 一句话:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基 础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。
  • 不可重读(Non-Repeatable Reads)
    一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现 其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不 可重复读”。 一句话:事务A读取到了事务B已经提交的修改数据,不符合隔离性 。
  • 幻读(Phantom Reads) 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插 入了满足其查询条件的新数据,这种现象就称为“幻读”。 一句话:事务A读取到了事务B提交的新增数据,不符合隔离性。select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

事务隔离级别

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数 据库提供一定的事务隔离机制来解决。 数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔 离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。 同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用 对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。

  • 常看当前数据库的事务隔离级别: show variables like ‘tx_isolation’;
  • 设置事务隔离级别:set tx_isolation=‘REPEATABLE-READ’;
事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

如何读取行当前数据

我们都知道mysql为了节省随机写磁盘的IO消耗(转为顺序写,即只写redo log与binlog就完成写操作,通过两阶段提交保证事务一致性),那么读取的时候怎么办?

  • 内存里存在行对应的的数据页,那么就是行当前数据。
  • 内存里没有对应数据页,那么肯定数据库数据文件上是行当前数据。

为了保证上面的需求,就要在淘汰内存中脏页时进行刷盘(flush:即将脏页数据存储到数据文件,将对应redo log删除。redo log慢了、数据库内存不足要淘汰脏页、数据库闲着、正常关闭mysql(加快启动速度)都会触发刷脏页)。

change buff:为了节约随机读磁盘的IO消耗(需要把数据页读到内存再做修改),少占用内存。在不影响一致性前提下,如果数据页不在内存,用change buffer记录更新操作,并按策略持久化到系统表空间(ibdata1)。适用于读多写少场景如账单类、日志类系统。写入后马上查就非常不适合。
唯一索引不能使用,因为唯一索引的更新操作要判断是否违反唯一性约束,必须将数据页读入内存
如果是带change buff的,那么就要先将数据页从磁盘读到内存后,应用操作日志生成当前行数据。此过程称为merge
merge流程:
1.磁盘内数据页读到内存
2.找出change buff 中关于数据页的记录,依次应用得到新版数据页。
3.写redo log,包含数据的变更和changebuffer的变更(为防止change buffer操作丢失,异常重启后查询数据页中数据被change buffer应用)。
merge虽然结束了,但磁盘数据页与change buffer对应磁盘位置还没修改,属于脏页。

MVCC(multi-version concurrency control)应用解析

mvcc只有在可重复读与读已提交模式下使用,要搞定mvcc如何使用的就要了解undo log的简单原理。

undo log:如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。它是逻辑日志,记录修改日志,也是mvcc的基础。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

  • undo log是采用段(segment)的方式来记录的(rollback segment回滚段),每个回滚段中有1024个undo log segment。每个undo操作在记录的时候占用一个undo log segment。rollback segment,默认值为128,可通过变量 innodb_undo_logs设置。undo log默认存放在共享表空间中
  • undo log也会产生redo log,因为undo log也要实现持久性保护。当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间,即系统里有没有比undo段更早的read-view。
  • 通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已,只对本事务可见,其他事物读到了只要不是读未提交状态,都是不可见的)
    delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
    update分为两种情况:update的列是否是主键列。
    如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
    如果是主键列,update分两部执行:先删除该行,再插入一行目标行。
    简单的undo log 日志格式如下,其中包括,指向下一条日志记录地址、当前事务ID、操作的表、表的唯一主键(如果没有设置唯一主键会自动设置一个,用select _rowid from table来查询)
    在这里插入图片描述
    在这里插入图片描述

经典例子
在这里插入图片描述
在这里插入图片描述
read view分析法: 获取当前所有未提交事务ID的数组与当前最大的事务ID。当查询一条行数据时,拿当前行信息与一致性视图,拿undo log中历史版本信息对照一直性视图进行处理。
如果行历史版本事务ID与当前事务一致,不用判断了,就是我们期望的结果。

是否大于一致性视图中最大事务ID是否在一致性视图中所有未提交事务ID行数据是否回滚到事务开始时状态下一步操作
大于拿行历史版本信息继续对照表进行判断
小于等于拿行历史版本信息继续对照表进行判断
小于等于当前记录就是我们期望的结果

读已提交分析:事物A结果为k=2,事物B结果为k=3 。
每一个语句执行前都会重新算出一个新的视图。通过下图与read view分析法很好理解,事物A查询时创建视图时就一个未提交事务【101】,当前版本对事物A不可见,所以查询到的是行在内存中数据回滚到历史版本1状态。发现102是小于等于一致性视图中最大事务ID的,而且还不在视图中,所以就是期望结果K=2。事务B查询时创建视图【100,101】。由于行当前版本就是当前事务创建,所以直接返回结果k=3
在这里插入图片描述

可重复读实现:事物A结果为k=1,事物B结果为k=3 。
由于101、102都大于事务A的视图【99、100】最大事务ID所以不可见,由于90版小于最大事务ID并且不在视图中,所以可见K=1。虽然事务B视图为【99、100、101】,但是由于102版本后事务B又更新了行(跟新会使用排它锁,触发当前读,会获取到历史版本1),所以当前版本可见k=3.
在这里插入图片描述

间隙锁

无索引行锁会升级为表锁:锁主要是加在索引上,如果对非索引字 段更新, 行锁可能会变表锁
前文已经提到过幻读,幻读只有在串行化的事务级别下可避免,但是现实是我们基本不可能用这个隔离级别,那如何解决幻读呢,MySql为我们提供了间隙锁,但是间隙锁只有在可重复读情况下生效。让我们看一下只使用行锁后果。
语义上问题
首先看一个例子,注意查询中带有for update 是当前读哟,可重复读隔离级别下也会是这个结果。
CREATE TABLE t (
id int(11) NOT NULL,
c int(11) DEFAULT NULL,
d int(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY c (c)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
在这里插入图片描述
本来sessionA在T1时刻期望将d=5这一行锁住,继续做其他操作,但最终发现T3、T5都发现,有人新增或修改了d=5的行,就是没有锁住期望的行,但是是符合事务隔离级别。
数据一致性的问题

在这里插入图片描述
如上执行后,数据库记录变成(5,5,100),(0,5,5),(1,5,5)。但binglog(satement格式)中是按事务提交顺序存储的,拿到备份库结果就是(0,5,100)、(1,5,100)和(5,5,100)了,因为事务A最后执行。所以读可提交模式下下binlog要用row模式,可以解决数据一致性问题
只给现有记录加锁,怎么都阻止不了新插入的记录

那么间隙锁是如何解决上述问题的?
加间隙锁,包含了两个“原则”、两个“优化”和一个“bug”。
原则1:加锁的基本单位是next-key lock。希望你还记得,next-key lock是前开后闭区间。
原则2:查找过程中访问到的对象才会加锁。
优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
间隙锁(开区间)和行锁合并称 next-key lock (前开后闭)。select * from t for update要把整个表所有记录锁起来,就形成了7个next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum](如果读已提交模式是可以添加记录的)。
间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化=,在查询字段中加入索引中不存在的字段
详细例子可以参考如下文章:https://www.cnblogs.com/gaosf/p/11149600.html

主从同步问题

由于并发事务影响,从库如何并发处理relay log(从主库顺序写入的binlog)就是最大难题,也是最影响性能的点。
MySQL 5.5版本的并行复制策略":单线程处理
MySQL 5.6版本的并行复制策略:支持了并行复制,只是支持的粒度是按库并行
MariaDB的并行复制策略:
1.能够在同一组里提交的事务,一定不会修改同一行;
2.主库上可以并行执行的事务,备库上也一定是可以并行执行的。
在实现上,MariaDB是这么做的:

  • 在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
  • commit_id直接写到binlog里面;
  • 传到备库应用的时候,相同commit_id的事务分发到多个worker执行;
    -这一组全部执行完成后,coordinator再去取下一批。
    当时,这个策略出来的时候是相当惊艳的。因为,之前业界的思路都是在“分析binlog,并拆分到worker”上。而MariaDB的这个策略,目标是“模拟主库的并行模式”。

MySQL 5.7的并行复制策略:在mariaDB上优化,调整binlog的组提交参数,延迟提交是更多事务处于prepare状态,可以增加并行度。

  • 同时处于prepare状态的事务,在备库执行时是可以并行的;
  • 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的

MySQL 5.7.22的并行复制策略
基于WRITESET的并行复制。相应地,新增了一个参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。

  • COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
  • WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行。
  • WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。

当然为了唯一标识,这个hash值是通过“库名+表名+索引名+值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert语句对应的writeset就要多增加一个hash值。类似按行分发的策略。不过,MySQL官方的这个实现还是有很大的优势:由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。
因此,MySQL 5.7.22的并行复制策略在通用性上还是有保证的。当然,对于“表上没主键”和“外键约束”的场景,WRITESET策略也是没法并行的,也会暂时退化为单线程模型。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值