マルチユーザーのデータベース駆動型アプリケーションを開発する場合の最大の難点は、一方では、同時にデータベースアクセスを最大限に活用することであり、このため、各ユーザーが一貫した方法でデータを読み取り、変更できるようにすることです。ロック機構。
6.1ロックとは
ロックメカニズムは、共有リソースへの同時アクセスを管理するために使用されます。InnoDBはテーブルデータを行レベルでロックするだけでなく、データベース内の他の複数の場所でもロックし、多くの異なるリソースへの同時アクセスを可能にします。たとえば、バッファプールでLRUリストを操作したり、モバイルLRUリストの要素を削除および追加したりします。
InnoDBロックの実装はOracleデータベースと非常に似ており、一貫した非ロック読み取り、行レベルのロックのサポートを提供します。行レベルのロックには追加のオーバーヘッドがなく、同時実行性と一貫性を得ることができます。
6.2ロックとラッチ
ラッチは、非常に短いロック時間を必要とするため、一般にラッチ(軽量)と呼ばれ、期間が長いとパフォーマンスが低下します。InnoDBでは、ラッチはミューテックスとrwlock(読み取り/書き込みロック)に分割できます。目的は、重要なリソースを操作する並行スレッドの正確性を保証することであり、通常はデッドロックの検証は行われません。
通常、lockは、テーブル、ページ、行など、データベース内のオブジェクトをロックします。また、ロックオブジェクトは、トランザクションのコミットまたはロールバック後にのみ解放されます。
ロック | ラッチ | |
対象 | 事業 | スレッド |
守る | データベースメモリ | メモリデータ構造 |
期間 | トランザクションプロセス全体 | 重要なリソース |
模様 | 行ロック、テーブルロック、インテンションロック | 読み取り/書き込みロック、ミューテックスロック |
デッドロック | グラフ待機メカニズムとタイムアウトメカニズムによるデッドロック検出と処理 | デッドロックの検出と処理のメカニズムはなく、デッドロックが発生しないことを保証するアプリケーションロックの順序のみ |
に存在する | ロックマネージャーハッシュテーブル | 各データ構造のオブジェクト |
6.3 InnoDBストレージエンジンのロック
6.3.1ロックのタイプ
InnoDBは、次の2つの標準行レベルロックを実装します。
- 共有ロックSロックにより、トランザクションはデータの行を読み取ることができます
- 排他ロック、Xロックにより、トランザクションはデータの行を削除または更新できます。
SロックとXロックはどちらも行ロックであり、互換性とは同じ行にあるレコードの互換性を指します。
InnoDBは、行レベルのロックとテーブルレベルのロックを同時にサポートするマルチグラニュラリティロックをサポートしています。つまり、インテントロック、インテントロックは、ロックされたオブジェクトを複数のレベルに分割します。インテントロックは、トランザクションがよりきめ細かいロックして。
ページのレコードrにXロックを追加する必要がある場合は、データベースA、テーブル、ページのIXを意図的にロックし、最後にrのXロックを行う必要があります。これらのパーツのいずれかが待機を引き起こす場合、操作は粗粒度ロックが完了するまで待機する必要があります。InnoDBは、比較的シンプルなインテントロックをサポートしています。インテントロックは、主にトランザクションの次の行で要求されるロックのタイプを明らかにするためのテーブルレベルのロックです。
- 意図的な共有ロック(ISロック)。トランザクションは、テーブル内の数行の共有ロックを取得する必要があります。
- 意図的排他ロック(IXロック)。トランザクションは、テーブル内の数行の排他ロックを取得したい
InnoDBは行レベルのロックをサポートしているため、インテントロックは全テーブルスキャン以外のリクエストをブロックしません。
IS | IX | S | バツ | |
IS | 対応 | アローロン | 対応 | 互換性がありません |
IX | 対応 | 対応 | 互換性がありません | 互換性がありません |
S | 対応 | 互換性がありません | 対応 | 互換性がありません |
バツ | 互換性がありません | 互換性がありません | 互換性がありません | 互換性がありません |
6.3.2一貫した非ロック読み取り
一貫性のある非ロック読み取り(一貫性のある非ロック読み取り)とは、データベース内の現在の実行時間行データを読み取るためのマルチバージョン同時実行制御(マルチバージョンの同時実行制御)によるInnoDBを指します。この時点で、読み取り行が削除または更新を実行している場合、読み取り操作は行のロックが解放されるのを待ちません。逆に、InnoDBは行のスナップショットデータを読み取ります(この実装は、元に戻すセグメントを通じて行われます) )、元に戻すセグメントはトランザクションのデータをロールバックするために使用されるため、追加のオーバーヘッドはありません。また、トランザクションが履歴データを変更する必要がないため、スナップショットデータの読み取りをロックする必要はありません。
一貫性のある非ロック読み取りは、InnoDBのデフォルトの読み取りモード(繰り返し可能な分離レベル)です。つまり、読み取りは占有せず、テーブルのロックを待機しません。
1つの行に複数のスナップショットデータが記録されます。このテクノロジーは一般に行マルチバージョン同時実行制御テクノロジーと呼ばれます。READCOMMITEDおよびREPEATABLE READのトランザクション分離レベルでは、InnoDBは非ロックの一貫した読み取りを使用します。
READ COMMITEDトランザクション分離レベルでは、スナップショットデータの場合、ロックされた行の最新のスナップショットデータが常に読み取られます。データベース理論の観点から、これはACIDの分離Iに違反しています。REPEATABLE READは、トランザクションの開始時に行データバージョンを読み取ります。
時間 | セッションA | セッションB |
1 | ベギン | |
2 | select * from t where id = 1 | |
3 | ベギン | |
4 | update t set id = 3 where id = 1 この時点で、トランザクションは送信されず、Xロックが追加され、セッションA5のクエリ、MVCCクエリ結果を使用します |
|
5 | select * from t where id = 1 | |
6 | コミット; | |
7 | select * from t where id = 1 | |
8 | コミット; |
READ COMMITED分離レベルでは、セッションBが送信された後、ID = 1のデータが3に更新され、このときのデータが最新であるため、1、5はID = 1、7はnullを返します。REPEATABLE READでは、トランザクションの開始前のデータが常に読み取られるため、1、5、7は常にid = 1のデータを返します。
6.3.3一貫したロックの読み取り
場合によっては、ユーザーはデータベースの読み取り操作を明示的にロックして、データの整合性を確保する必要があります。これには、選択クエリの場合でも、データベースがロック文をサポートする必要があります。InnoDBは、selectステートメントの2つの一貫したロック読み取り操作をサポートしています。
- 更新……を選択
- 選択……共有モードでロック
select……更新の場合、読み取り行にXロックを追加します。他のトランザクションは行にロックを追加できません。選択……共有モード読み取り行レコードにSロックを追加します。他のトランザクションは、ロックされた行にSロックを追加できますが、Xロックを追加できません。Xロックはブロックのみできます。
select……updateの場合、select……share inモードは、トランザクション、トランザクションコミット、ロック解除である必要があります。
6.3.4自己成長とロック
InnoDBのメモリ構造には、自己増分値を含む各テーブルの自己増分カウンタがあります。次のステートメントを実行して、カウンター値を取得します。
更新のためにtからMAX(auto_inc_col)を選択します。
挿入操作は、この自己増分カウンター値に従って、自己増分列に1を追加します。この実装はAUTO-INCロックと呼ばれます。この種のロックは実際には特別なテーブルロックメカニズムを使用します。挿入のパフォーマンスを向上させるために、ロックはトランザクションの完了後ではなく、自己インクリメント値の挿入のSQLステートメントが完了した後に解放されます。すぐに解放します。
AUTO-INCロックはある程度並行性を向上させますが、値が自己増加する列の同時挿入のパフォーマンスは低く、トランザクションは前の挿入の完了を待つ必要があります(トランザクションの完了を待つ必要はありません)。挿入...別のトランザクションがブロックされるため、大量の選択データを挿入すると、挿入のパフォーマンスに影響します。
MySQL 5.1.22以降、InnoDBは軽量ミューテックス用の自己拡張実装メカニズムを提供します。このバージョンから、InnoDBは、innodb_autoinc_lock_modeパラメータを提供して、セルフインクリメントモードを制御します。デフォルトは1です。
innodb_autoinc_lock_mode | 解説 |
0 | MySQL5.1.22より前の実装、つまりAUTO-INCロックモードを介した実装。新しい自己成長モードのため、この値はユーザーの設定にはなりません。 |
1 | デフォルト値。「単純挿入」の場合、この値はmutexを使用してカウンターをメモリーに蓄積します。「一括挿入」の場合、従来のAUTO-INCロック方法が引き続き使用されます。この構成では、ロールバック操作が考慮されない場合、自己追加値の増加は継続的です。このように考えると、ステートメントベースのレプリケーション方法は依然として非常にうまく機能します。 注:AUTO-INCロックを使用していて、「単純な挿入」を再度実行する必要がある場合でも、「AUTO-INCロック」のリリースを待つ必要があります。 |
2 | このモードでは、「挿入のような」自己増加値はすべて、AUTO-INCロックではなく相互排除によって生成されます。明らかに、これは最高のパフォーマンスメソッドですが、いくつかの問題が発生します。同時実行性が存在する場合、自己挿入の値は挿入ごとに連続しない可能性があり、さらに、ステートメントベースのレプリケーションに基づく問題が発生することが重要です。したがって、このモードを使用する場合は、レプリケーションのマスター/スレーブデータの同時実行性と一貫性を最大にするために、常に行ベースレプリケーションを使用する必要があります。 |
挿入タイプ | 解説 |
insert-like | 所有的插入语句,包括 insert、replace、insert……select、replace……select、load data等 |
simple inserts | 能在插入前就确定插入行数的语句,包括insert,replace等,不包含 insert …… on duplicate key update这类SQL |
bulk inserts | 在插入前不能确定插入行数的语,如 insert……select,replace……select、load data等 |
mixed-mode inserts | 插入中有一部分的值是自增长的,一部分是可以确定的,如insert into t (e1,e2) values (1,'a'),(NULL,'b'),(3,'c'),也可以是 insert …… on duplicate key update这类SQL |
另外,在 InnoDB中,自增长的列必须是索引,同时是索引的第一个列,否则MySQL会抛出异常。
6.3.5 外键和锁
外键主要用于引用完整性的约束检查,在InnoDB中,对于一个外键列,如果没有显示的对这个列添加索引,InnoDB会自动对其加索引,避免表锁。
对于外键的插入或更新,需要先查询父表,但是对于父表的查询不是使用一致性非锁定读(MVCC),因为这样会发生数据不一致的问题,使用的是 select ……lock in share mode,主动为付表加S锁,如果这时父表上已有X锁,则阻塞。
时间 | 会话A | 会话B |
1 | BEGIN | |
2 | delete from parent where id=5 | |
3 | BEGIN | |
4 | insert into child select 2,5 #第二列是外键,执行时被阻塞waiting |
6.4 锁的算法
6.4.1 行锁的三种算法
InnoDB 有三种行锁算法,分别是
- Record Lock:单个记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并锁定记录本身
Record Lock 总是锁住索引记录,如果 InnoDB在建立时没有设置任何一个索引,那么 InnoDB会使用隐士的主键来锁定。
InnoDB对于行的查询都是采用 Next-Key Lock,其目的是为了解决幻读 Phantom Problem,是谓词锁的一种改进。
当查询的索引含有唯一属性时,InnoDB 会对Next-Key Lock优化,降级为 Record Key,以此提高应用的并发性。
如前面介绍的。next-key lock 降级为record lock是在查询的列是唯一索引的情况下,若是辅助索引,则情况不同:
create table z (a int, b int , Primary key(a), key(b)) insert into z select 1,1; insert into z select 3,1; insert into z select 5,3; insert into z select 7,6; insert into z select 10,8;
其中b是辅助索引,此时执行 select * from z where b=3 for update;
此时,sql 语句通过索引列b进行查询,因此其使用传统的 next-key lock 进行加锁,并且由于有两个索引,其需要分别进行锁定。对于聚集索引,仅对列a=5的索引加record lock。而辅助索引,加的是next-key lock,锁定的是(1,3)范围,特别注意的是,InnoDB还会对辅助索引下一个键值加上 gap lock,即还有一个(3,6)范围的锁。因此,一下SQL都会被阻塞:
select * from z where a=5 lock in share mode; insert into z select 4,2; insert into z select 6,5;
从上面可以看出,gap lock的作用是为了阻止多个事务将记录插入到同一个范围内,而这会导致幻读问题的产生。用户可以通过以下两种方式来关闭 Gap Lock:
- 将事务隔离级别改为 READ COMMITED
- 将参数 innodb_locks_unsafe_for_binlog设为1
在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用 Record Lock进行锁定,需要牢记的是,上述配置破坏了事务的隔离性,并且对 replication可能会导致不一致。且从性能上看,READ COMMITED也不会优于默认的 READ REPEATABLE;
在 InnoDB中,对Insert的操作,其会检查插入记录的下一条记录是否被锁定,若已锁定,则不允许查询。对于上面的例子,会话A已经锁定了表中b=3的记录,即已锁定了(1,3)的范围,这时如果在其他会话中进行如下的插入同样会导致阻塞
insert into z select 2,2;
因为检测到辅助索引列b上插入2的记录时,发现3已经被索引,而将插入修改为如下值,可以立即执行:
insert into z select 2,0;
最后,对于唯一键值的锁定,next-key lock降级为record ke仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的一个,那么查询其实是range类型,而不是point查询,故InnoDB依然采用 next-key lock进行锁定。
6.4.2 解决 Phantom Problem
幻读指的是在同一个事务下,连续执行两次相同的SQL语句可能返回不一样的结果,第二次的SQL语句可能会返回之前不存在的行。
InnoDB采用 next-key lock 的算法解决了 Phantom Problem,对 select * from t where id > 2 for update,锁住的不单是5这个值,而是对(2,+∞)这个范围加了X锁。因此,对这个范围的插入是不允许的,从而避免幻读。
时间 | 会话A | 会话B |
1 | set session tx_isolation = 'READ-COMMITED' |
|
2 | BEGIN | |
3 | select * from t where a>2 for update; ***********1 row ************* a:4 |
|
4 | BEGIN | |
5 | insert into t select 4 | |
6 | COMMIT; | |
7 | select * from t where a>2 for update; ***********1 row ************* a:4 ***********2 row ************* a:5 |
REPEATABLE READ 采用的是 next-key locking加锁。而 READCOMMITED 采用的是 record lock .
此外,用户可以通过 InnoDB的 next-key lock在应用层面实现唯一性的检查:
select * from table where col=xxx lock in share mode; if not found any row : #unique for insert value insert into table values(……);
如果用户通过一个索引查询一个值,并对该行加上了S lock,那么即使查询的值不存在,其锁定的也是一个范围,因此若没有返回任何行,那么新插入的值一定是唯一的。
那,如果在第一步select lock in share mode时,有多个事务并发操作,那么这种唯一性检查是否会有问题,其实不会,因为会发生死锁。只有一个事务会成功,其他的事务会抛出死锁错误。
6.5 锁问题
6.5.1 脏读
脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另一个事务未提交的数据,则显然违反了数据库的隔离性。
脏读指的是在不同事务下,当前事务可以读到另外事务未提交的数据,即脏数据。
时间 | 会话A | 会话B |
1 | set @@tx_isolation = 'read-ncommited' |
|
2 | set @@tx_isolation = 'read-ncommited' |
|
3 | BEGIN |
|
4 | select * from t ; **********1 row ************* a:1 |
|
5 | insert into t select 2; | |
6 | select * from t ; **********1 row ************* a:1 **********2 row ************* a:2 |
脏读发生条件是需要事务的隔离级别为 read uncommited;目前大部分数据库至少设置为 read COMMITED;
6.5.2 不可重复读
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的是已提交的数据,但是违反了事务一致性的要求。
时间 | 会话A | 会话B |
1 | et @@tx_isolation = 'read-commited' |
|
2 | et @@tx_isolation = 'read-commited' |
|
3 | BEGIN | BEGIN |
4 | select * from t ; **********1 row ************* a:1 |
|
5 | insert into t select 2; | |
6 | COMMITED | |
7 | select * from t ; **********1 row ************* a:1 **********2 row ************* a:2 |
一般来说,不可重复读是可接受的,因为读到的是已提交的数据,本身没有带来很大问题。在 InnoDB中使用 next-key lock避免不可重复读问题,即 幻读(Phantom Problem)。在 Next-Key lock算法下,对索引的扫描,不仅是锁住扫描到的索引,还有这些索引覆盖的范围,因此在这个范围内插入是不允许的。这样则避免了另外的事务在这个范围内的插入导致不可重读的问题。
6.5.3 丢失更新
丢失更新就是一个事务的更新操作被另一个事务的更新操作覆盖,从而导致数据不一致。
- 事务1将行记录r更新为v1,但是事务未提交
- 事务2将行记录r更新为v2,事务未提交
- 事务1提交
- 事务2提交
当前数据库的任何隔离级别下,以上情况都不会导致数据库理论意义上的丢失更新问题,因为,对于行的DML操作,需要对行货其他粗粒度级别的对象加锁,步骤2,事务2并不能对记录进行更新,被阻塞,直到事务1提交。
但在生产应用中,还有一个逻辑意义的丢失更新问题,而导致该问题的不是因为数据库本身的问题,简单来说,下面情况会发生丢失更新:
- 事务T1查询一行数据,放入本地内存,返回给User1
- 事务T2查询一行数据,放入本地内存,返回给User2
- User1修改后,更新数据库提交
- User2修改后,更新数据库提交
显然,这个过程中,User1的修改操作”丢失“了。在银行操作中,尤为恐怖。要避免丢失,需要让事务串行化。
时间 | 会话A | 会话B |
1 | BEGIN | |
2 | select cash into @cash from account where user=pUser for update #加X锁 |
|
3 | select cash into @cash from account where user=pUser for update #等待,直到m提交后,锁释放 |
|
…… | …… | |
m | update account set cash = @cash - 9000 where user = pUser |
|
m+1 | commit | |
m+2 | update account set cash = @cash -1 where user = pUser |
|
m+3 | commit |
6.6 阻塞
因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源,这就是阻塞。
阻塞并不是一件坏事,其是为了确保事务可以并发且正常地运行。
需要牢记的是,默认情况下,InnoDB存储引擎不会回滚超时引发的错误异常,其实,InnoDB在大部分情况下都不会对异常进行回滚。
时间 | 会话A | 会话B |
1 | select * from t; **********3 row ************* a:1 a:2 a:4 |
|
2 | BEGIN | |
3 | select * from t where a < 4 for update; **********2 row ************* a:1 a:2 #对(2,4)上X锁 |
|
4 | BEGIN | |
5 | insert into t select 5; | |
6 | insert into t select 3; #等待超时,需要确定超时后,5的插入是否需要回滚或提交,否则这是十分危险的状态 |
6.7 死锁
死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种相互等待的现象。解决死锁的办法:
- 超时回滚
- wait-for graph 等待图的方式进行死锁检测,采用深度有限的算法,选择回滚undo量最小的事务。
死锁的概率与以下因素有关:
- 事务的数量n,数量越多死锁概率越大
- 事务操作的数量r,数量越多,死锁概率越大
- 操作数据的集合R,越小,死锁概率越大
6.8 锁升级
锁升级就是将当前锁的粒度降低,例如把行锁升级为页锁,把页锁升级为表锁。
InnoDB不存在锁升级,因为其不是根据每个记录来产生行锁的。相反,其根据每个事务访问的每个页对锁进行管理,采用的是位图的方式。因此不管一个事务锁住页中的一个记录还是多个记录,开销都是一样的。