> 데이터 베이스 > MySQL 튜토리얼 > MySQL 잠금 메커니즘에 대한 자세한 소개

MySQL 잠금 메커니즘에 대한 자세한 소개

零下一度
풀어 주다: 2017-07-17 10:01:34
원래의
1883명이 탐색했습니다.

1. 격리 수준

(1) 커밋되지 않은 읽기(RU)

이 격리 수준에서는 트랜잭션이 전혀 격리되지 않고 더티 읽기가 발생하며 커밋되지 않은 레코드를 읽을 수 있습니다. 실제 상황에서는 사용되지 않습니다.

(2) 읽기 커미트(RC)

이 격리 수준에서는 소위 팬텀 읽기가 발생하는 레코드만 읽을 수 있습니다. 트랜잭션에서 동일한 쿼리가 여러 번 실행되면 반환되는 레코드가 정확히 동일하지 않습니다. 팬텀 읽기의 근본 원인은 RC 격리 수준에서 각 문이 제출된 트랜잭션의 업데이트를 읽는 것입니다. 두 쿼리 사이에 다른 트랜잭션이 제출되면 두 쿼리의 결과가 일치하지 않게 됩니다. 그럼에도 불구하고 읽기-커밋 격리 수준은 프로덕션 환경에서 널리 사용됩니다.

(3) 반복 읽기(RR)

반복 읽기 격리 수준은 반복 불가능 읽기 문제를 해결하지만 여전히 팬텀 읽기 문제는 해결하지 못합니다. 그렇다면 반복 불가능한 읽기와 팬텀 읽기의 차이점은 무엇입니까? 비반복 읽기는 수정, 즉 두 번 읽은 값이 다른 것에 초점을 맞춘 반면, 팬텀 읽기는 레코드 수[삽입 및 삭제]의 변화에 ​​초점을 맞춥니다. 일반적으로 교과서에서는 팬텀 읽기 문제가 직렬화 격리 수준에 도달한 경우에만 해결될 수 있다고 설명하지만 MySQL의 innodb는 주로 GAP 잠금을 통해 팬텀 읽기 문제를 해결합니다. 또한 모든 데이터베이스가 이 격리 수준을 구현하는 것은 아닙니다. 나중에 mysql이 반복 읽기 격리 수준을 구현하는 방법을 간략하게 소개하겠습니다.

(4) 직렬화 가능

직렬 격리 모드에서는 더티 읽기 및 팬텀이 제거되지만 트랜잭션 동시성은 급격히 떨어지며 트랜잭션의 격리 수준은 트랜잭션 동시성에 반비례합니다. 격리 수준이 높을수록 트랜잭션 동시성이 낮아집니다. 실제 프로덕션 환경에서 DBA는 동시성과 비즈니스 요구 사항 충족 사이에서 균형을 이루고 적절한 격리 수준을 선택합니다.

2. 동시 스케줄링 방법

격리 수준과 밀접하게 관련된 또 다른 사항은 격리 수준을 달성하는 동시 스케줄링입니다. 동시 예약의 경우 데이터베이스 공급업체마다 구현 메커니즘이 다르지만 기본 원칙은 모두 잠금을 사용하여 여러 트랜잭션에 의해 동시에 수정되지 않도록 보호합니다. 기존의 잠금 기반 동시성 제어와 비교하여 MVCC(다중 버전 동시성 제어)의 주요 특징은 읽기에 대해 잠그지 않는다는 것입니다. 이 기능은 읽기가 많고 쓰기가 적은 시나리오에서 시스템의 동시성을 크게 향상시킵니다. 따라서 대부분의 관계형 데이터베이스는 모두 MVCC를 구현합니다.

3. 2단계 잠금 프로토콜

2단계 잠금 프로토콜의 의미는 거래가 두 단계로 나뉘며 첫 번째 단계는 봉쇄를 획득하고 두 번째 단계는 해제하는 것입니다. 봉쇄. 2단계 차단은 동시 스케줄링의 정확성을 보장합니다. 1단계 잠금(트랜잭션에 필요한 모든 잠금을 한 번에 획득)과 비교하여 2단계 잠금은 동시성을 향상시키지만 교착 상태의 가능성도 가져옵니다.

4. 교착상태

소위 교착상태란 두 개 이상의 트랜잭션이 상대방이 획득할 것으로 기대하는 자원을 각각 점유하여 대기 루프를 형성하고, 서로 계속할 수 없는 상태를 말합니다. 실행하다.

5. 잠금 유형

잠금 유형에 따라 공유 잠금, 전용 잠금, 의도 공유 잠금, 의도 전용 잠금으로 나눌 수 있습니다. 잠금의 세분성에 따라 행 잠금과 테이블 잠금으로 나눌 수 있습니다. mysql의 경우 트랜잭션 메커니즘은 기본 스토리지 엔진에 의해 더 많이 구현됩니다. 따라서 mysql 레벨에는 테이블 잠금만 있는 반면, 트랜잭션을 지원하는 innodb 스토리지 엔진은 행 잠금(레코드 잠금), 갭 잠금 및 다음 키 잠금을 구현합니다. . Innodb는 인덱스로 구성된 테이블이기 때문에 MySQL의 레코드 잠금은 본질적으로 인덱스 레코드의 잠금입니다. 갭 잠금은 다음 키 잠금에서만 유효한 인덱스 레코드 갭의 잠금입니다. 레코드 잠금과 레코드 잠금 조합 앞의 간격입니다. mysql은 갭 잠금 및 다음 키 잠금을 통해 RR 격리 수준을 구현합니다.

설명:

업링크 잠금은 인덱스를 통해서만 가능합니다. 그렇지 않으면 클러스터형 인덱스의 각 행에 쓰기 잠금이 설정됩니다. 이는 실제로 다음과 같습니다. 테이블에 쓰기 잠금이 있습니다.

여러 물리적 레코드가 동일한 인덱스에 해당하고 동시에 액세스하는 경우 잠금 충돌도 발생합니다.

테이블에 여러 인덱스가 있으면 서로 다른 인덱스를 사용하여 서로 다른 행을 잠글 수 있습니다. 또한 Innodb는 행 잠금을 사용하여 데이터 레코드(클러스터형 인덱스)를 잠급니다.

MVCC 동시성 제어 메커니즘에서는 읽기 작업이 잠겨 있지 않기 때문에 어떤 작업도 읽기 작업을 차단하지 않으며 읽기 작업도 어떤 작업도 차단하지 않습니다.


RocksDB는 오픈 소스 스토리지 엔진으로서 트랜잭션의 ACID 특성을 지원합니다. ACID에서 I(격리)를 지원하려면 동시성 제어가 필수적입니다. 이 기사에서는 주로 RocksDB의 잠금 메커니즘 구현에 대해 설명합니다. 이 글을 통해 독자들이 RocksDB 동시성 제어의 원리를 심도 있게 이해할 수 있기를 바랍니다. 이 기사는 주로 다음 네 가지 측면에서 시작됩니다. 먼저 RocksDB 잠금의 기본 구조를 소개하고 RocksDB 행 잠금 데이터 구조의 잠금 공간 오버헤드를 소개합니다. 그런 다음 몇 가지 일반적인 잠금 프로세스를 소개합니다. 마지막으로 메커니즘의 필수 교착 상태 감지 메커니즘을 소개하겠습니다.

1. 행 잠금 데이터 구조
RocksDB의 최소 잠금 단위는 행입니다. KV 저장소의 경우 잠금 개체가 키이고 각 키는 LockInfo 구조에 해당합니다. 모든 키는 해시 테이블을 통해 관리되며, 잠금을 찾을 때 해시 테이블을 통해 직접 찾아 키가 잠겨 있는지 확인할 수 있습니다. 그러나 전역적으로 해시 테이블이 하나만 있는 경우 이 해시 테이블에 액세스할 때 많은 충돌이 발생하여 동시성 성능에 영향을 미칩니다. RocksDB는 먼저 Columnfamily로 분할됩니다. 각 Columnfamily의 잠금은 LockMap으로 관리되며 각 LockMap은 LockMapStripe로 분할되고 해시 테이블(std::unordered_map관련 데이터 구조는 다음과 같습니다:

struct LockInfo {
bool exclusive; //排它锁或是共享锁
autovector<TransactionID> txn_ids; //事务列表,对于共享锁而言,同一个key可以对应多个事务

// Transaction locks are not valid after this time in us
uint64_t expiration_time;
}

struct LockMapStripe { 
// Mutex must be held before modifying keys map
std::shared_ptr<TransactionDBMutex> stripe_mutex;

// Condition Variable per stripe for waiting on a lock
std::shared_ptr<TransactionDBCondVar> stripe_cv;

// Locked keys mapped to the info about the transactions that locked them.
std::unordered_map<std::string, LockInfo> keys;
}

struct LockMap {
const size_t num_stripes_; //分片个数
std::atomic<int64_t> lock_cnt{0}; //锁数目
std::vector<LockMapStripe*> lock_map_stripes_; //锁分片
}

class TransactionLockMgr {
using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>;
LockMaps lock_maps_;

// Thread-local cache of entries in lock_maps_. This is an optimization
// to avoid acquiring a mutex in order to look up a LockMap
std::unique_ptr<ThreadLocalPtr> lock_maps_cache_;
}
로그인 후 복사

2.行锁空间代价
由于锁信息是常驻内存,我们简单分析下RocksDB锁占用的内存。每个锁实际上是unordered_map中的一个元素,则锁占用的内存为key_length+8+8+1,假设key为bigint,占8个字节,则100w行记录,需要消耗大约22M内存。但是由于内存与key_length正相关,导致RocksDB的内存消耗不可控。我们可以简单算算RocksDB作为MySQL存储引擎时,key_length的范围。对于单列索引,最大值为2048个字节,具体可以参考max_supported_key_part_length实现;对于复合索引,索引最大长度为3072个字节,具体可以参考max_supported_key_length实现。假设最坏的情况,key_length=3072,则100w行记录,需要消耗3G内存,如果是锁1亿行记录,则需要消耗300G内存,这种情况下内存会有撑爆的风险。因此RocksDB提供参数配置max_row_locks,确保内存可控,默认RDB_MAX_ROW_LOCKS设置为1G,对于大部分key为bigint场景,极端情况下,也需要消耗22G内存。而在这方面,InnoDB则比较友好,hash表的key是(space_id, page_no),所以无论key有多大,key部分的内存消耗都是恒定的。前面我也提到了InnoDB在一个事务需要锁大量记录场景下是有优化的,多个记录可以公用一把锁,这样也间接可以减少内存。

3.上锁流程分析
前面简单了解了RocksDB锁数据结构的设计以及锁对内存资源的消耗。这节主要介绍几种典型场景下,RocksDB是如何加锁的。与InnoDB一样,RocksDB也支持MVCC,读不上锁,为了方便,下面的讨论基于RocksDB作为MySQL的一个引擎来展开,主要包括三类,基于主键的更新,基于二级索引的更新,基于主键的范围更新等。在展开讨论之前,有一点需要说明的是,RocksDB与InnoDB不同,RocksDB的更新也是基于快照的,而InnoDB的更新基于当前读,这种差异也使得在实际应用中,相同隔离级别下,表现有所不一样。对于RocksDB而言,在RC隔离级别下,每个语句开始都会重新获取一次快照;在RR隔离级别下,整个事务中只在第一个语句开始时获取一次快照,所有语句共用这个快照,直到事务结束。

3.1.基于主键的更新
这里主要接口是TransactionBaseImpl::GetForUpdate
1).尝试对key加锁,如果锁被其它事务持有,则需要等待
2).创建snapshot
3).调用ValidateSnapshot,Get key,通过比较Sequence判断key是否被更新过
4).由于是加锁后,再获取snapshot,所以检查一定成功。
5).执行更新操作
这里有一个延迟获取快照的机制,实际上在语句开始时,需要调用acquire_snapshot获取快照,但为了避免冲突导致的重试,在对key加锁后,再获取snapshot,这就保证了在基于主键更新的场景下,不会存在ValidateSnapshot失败的场景。

堆栈如下:

1-myrocks::ha_rocksdb::get_row_by_rowid
2-myrocks::ha_rocksdb::get_for_update
3-myrocks::Rdb_transaction_impl::get_for_update
4-rocksdb::TransactionBaseImpl::GetForUpdate
{
//加锁
5-rocksdb::TransactionImpl::TryLock
  6-rocksdb::TransactionDBImpl::TryLock
    7-rocksdb::TransactionLockMgr::TryLock 

 //延迟获取快照,与acquire_snapshot配合使用
 6-SetSnapshotIfNeeded()

 //检查key对应快照是否过期
 6-ValidateSnapshot
  7-rocksdb::TransactionUtil::CheckKeyForConflict
    8-rocksdb::TransactionUtil::CheckKey
      9-rocksdb::DBImpl::GetLatestSequenceForKey //第一次读取

//读取key
5-rocksdb::TransactionBaseImpl::Get
  6-rocksdb::WriteBatchWithIndex::GetFromBatchAndDB
    7-rocksdb::DB::Get
      8-rocksdb::DBImpl::Get
        9-rocksdb::DBImpl::GetImpl //第二次读取
}
로그인 후 복사

3.2.基于主键的范围更新
1).创建Snapshot,基于迭代器扫描主键
2).通过get_row_by_rowid,尝试对key加锁
3).调用ValidateSnapshot,Get key,通过比较Sequence判断key是否被更新过
4).如果key被其它事务更新过(key对应的SequenceNumber比Snapshot要新),触发重试
5).重试情况下,会释放老的快照并释放锁,通过tx->acquire_snapshot(false),延迟获取快照(加锁后,再拿snapshot)
5).再次调用get_for_update,由于此时key已经被加锁,重试一定可以成功。
6).执行更新操作
7).跳转到1,继续执行,直到主键不符合条件时,则结束。

3.3.基于二级索引的更新
这种场景与3.2类似,只不过多一步从二级索引定位主键过程。
1).创建Snapshot,基于迭代器扫描二级索引
2).根据二级索引反向找到主键,实际上也是调用get_row_by_rowid,这个过程就会尝试对key加锁
3).继续根据二级索引遍历下一个主键,尝试加锁
4).当返回的二级索引不符合条件时,则结束

4.死锁检测算法
死锁检测采用DFS((Depth First Search,深度优先算法),基本思路根据加入等待关系,继续查找被等待者的等待关系,如果发现成环,则认为发生了死锁,当然在大并发系统下,锁等待关系非常复杂,为了将死锁检测带来的资源消耗控制在一定范围,可以通过设置deadlock_detect_depth来控制死锁检测搜索的深度,或者在特定业务场景下,认为一定不会发生死锁,则关闭死锁检测,这样在一定程度上有利于系统并发的提升。需要说明的是,如果关闭死锁,最好配套将锁等待超时时间设置较小,避免系统真发生死锁时,事务长时间hang住。死锁检测基本流程如下:
1.定位到具体某个分片,获取mutex
2.调用AcquireLocked尝试加锁
3.若上锁失败,则触发进行死锁检测
4.调用IncrementWaiters增加一个等待者
5.如果等待者不在被等待者map里面,则肯定不会存在死锁,返回
6.对于被等待者,沿着wait_txn_map_向下检查等待关系,看看是否成环
7.若发现成环,则将调用DecrementWaitersImpl将新加入的等待关系解除,并报死锁错误。

相关的数据结构:

class TransactionLockMgr {
// Must be held when modifying wait_txn_map_ and rev_wait_txn_map_.
std::mutex wait_txn_map_mutex_;

// Maps from waitee -> number of waiters.
HashMap<TransactionID, int> rev_wait_txn_map_;

// Maps from waiter -> waitee.
HashMap<TransactionID, autovector<TransactionID>> wait_txn_map_;

DecrementWaiters //
IncrementWaiters //
}
struct TransactionOptions {
bool deadlock_detect = false; //是否检测死锁
int64_t deadlock_detect_depth = 50; //死锁检测的深度
int64_t lock_timeout = -1; //等待锁时间,线上一般设置为5s
int64_t expiration = -1; //持有锁时间,
}
로그인 후 복사

위 내용은 MySQL 잠금 메커니즘에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿