InnoDB는 MySQL에서 사용할 수 있는 스토리지 엔진 중 거의 유일하게 레코드 기반의 잠금을 제공해 높은 동시성 처리가 가능하고, 안정적이며 성능이 뛰어나다. InnoDB의 개략적인 구조는 아래 그림과 같다.

InnoDB 스토리지 엔진의 주요 특징은 다음과 같다.
InnoDB의 모든 테이블은 기본적으로 Primary Key를 기준으로 클러스터링돼 저장된다. 즉 Primary Key 값의 순서대로 디스크에 저장되며, 모든 Secondary Index는 레코드의 주소 대신 Primary Key의 값을 논리적인 주소로 사용한다. Primary Key가 클러스터링 인덱스이기 때문에 Primary Key를 이용한 범위 검색은 상당히 빨리 처리될 수 있다. 이로 인해 쿼리 실행 계획에서 Primary Key는 다른 Secondary Index에 비해 비중이 높게 설정된다.
Foregin Key에 대한 지원은 InnoDB 스토리지 엔진에서 지원하기 때문에 MyISAM이나 Memory 테이블에서는 사용할 수 없다. InnoDB에서 Foregin Key는 부모 테이블과 자식 테이블 모두 해당 칼럼에 대한 인덱스 생성이 필요하고, 변경 시 반드시 부모 테이블이나 자식 테이블에 데이터가 있는지 체크하는 작업이 필요하기 때문에 잠금이 여러 테이블로 전파되고, 이로 인해 데드락이 발생할 때가 많아 개발 시 주의하는 것이 좋다.
Foregin Key 사용 유무에 따른 성능 변화에 대해서 궁금하면, martin-son님의 블로그를 참고해보자.
MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하기 위함이다. InnoDB는 Undo Log를 이용해 MVCC를 구현한다. MVCC는 하나의 레코드에 대해 여러 개의 버젼을 동시에 관리한다.
이해를 위해, 트랜잭션 격리 수준이 READ_COMMITTED인 MySQL에서 InnoDB 테이블의 데이터 변경을 처리하는 과정을 살펴보자.
테이블 구조는 아래와 같다.
CREATE TABLE users(
id INT NOT NULL,
name VARCHAR(20) NOT NULL,
PRIMARY KEY (id)
);
이제 레코드 하나를 생성해보자.
INSERT INTO users (id, name) VALUES (1, '신찬규');
COMMIT;
그러면 데이터베이스 상태는 아래와 같게 된다.

해당 레코드를 UPDATE를 실행하면 다음과 같게 된다.
UPDATE users SET name = '유지훈' WHERE id = 1;

UPDATE가 실행되면, COMMIT 실행과 상관 없이 InnoDB 버퍼 풀은 새로운 값으로 바뀌고, Undo Log에는 이전 값이 복사되어 있다. 이때 레코드 전체를 복사하는 것이 아닌, Primary Key와 변경된 값만이 복사된다. 디스크에는 지연된 쓰기에 의해 InnoDB의 쓰기 쓰레드가 값을 반영했을 수도 있고 아닐 수도 있다. 다만, InnoDB는 ACID를 보장하기 때문에 동일한 상태라 보아도 무방하다.
아직 COMMIT을 하지 않았는데, 이때 다른 사용자가 아래와 같이 해당 레코즈를 조회하게 되면
SELECT * FROM users WHERE users.id = 1;
설정된 격리 수준에 따라 InnoDB 버퍼 풀의 데이터를 반환할 수도 있고 Undo Log의 데이터를 반환할 수도 있다. 현재 설정된 격리 수준은 READ_COMMITED이고, COMMIT을 하기 전이기 때문에 Undo Log에 있는 데이터가 반환된다. 격리 수준이 READ_COMMITED 이상이고 COMMIT 또는 ROLLBACK 이전이면 Undo Log의 데이터를 반환하는 것이다.
이러한 과정을 MVCC라고 하고, 하나의 레코드에 대해 2개의 버젼이 유지되며 필요에 따라 알맞은 버젼의 데이터를 반환하는 구조다.
이 상태에서 COMMIT을 하게 되면 현재 상태를 영구적으로 유지하고, ROLLBACK을 실행하면 Undo Log의 데이터를 InnoDB 버퍼 풀로 복구한다. Undo Log의 데이터는 해당 데이터를 필요하는 트랜잭션이 없을 때 삭제된다. 그래서 트랜잭션이 길어지면 Undo Log에서 관리하는 데이터가 많아지기 때문에 Undo Log 영역의 메모리 공간이 늘어나게 될 수 있으니 조심해야 한다. MySQL 5.5까지는 Undo Log 영역의 메모리 공간이 늘어나면 새로 구축하지 않는 이상 줄일 수 없었는데, MySQL 5.7부터 이를 해결했다.
MVCC를 사용하면 잠금을 걸지 않고 읽기 작업을 수행할 수 있다. 격리 수준이 SERIALIZABLE이 아니라면 순수한 SELECT 쿼리는 다른 트랜잭션의 변경 작업과 상관 없이 항상 잠금을 대기하지 않고 바로 실행된다. 이를 잠금 없는 일간된 읽기(Non-Locking Consistent Read)라고 한다.
InnoDB는 MVCC를 통해 잠금을 걸지 않고 읽기 작업을 수행할 수 있다. 트랜잭션 격리 수준이 SERIALIZABLE이 아닐 때, 순수한 읽기 작업은 다른 트랜잭션이 가지고 있더라도 잠금을 대기하지 않고 읽기 작업을 수행한다. 이를 "잠금 없는 일관된 읽기"라고 한다.
InnoDB는 내부적으로 잠금이 교착 상태에 빠지지 않도록 잠금 대기 목록을 그래프로 관리하고, 데드락 감지 쓰레드를 사용해 이를 주기적으로 검사를 해 교착 상태에 빠진 트랜잭션 중 일반적으로 Undo Log의 레코드가 더 적은 트랜잭션을 종료시킨다. 이는 Undo Log 크기가 작을 수록 Rollback에 대한 비용이 적기 때문이다.
그러나 각 트랜잭션이 가진 잠금 개수가 많아지면 그래프 크기가 커지기 때문에 데드락 감지 쓰레드가 느려진다. 이는 데드락 감지를 하는 도중 그래프가 변하지 않도록, 즉 잠금 상태가 변하지 않도록 그래프에 새로운 잠금을 걸기 때문인데, 그동안 나머지 쓰레드가 잠금을 얻지 못하고 대기하게 되기 때문이다.
이는 innodb_deadlock_detect를 OFF하여 자동 데드락 감지를 끄면 해결할 수 있는데, 그렇게 되면 교착 상태가 발생해도 해결되지 않게 된다. 이때는 데드락 상황에서 일정 시간이 지나면 요청이 자동으로 실패되도록 innodb_lock_wait_timeout을 활성화하여 타임아웃을 설정하면 된다. 자동 데드락 감지를 끌때는 타임아웃을 기본값인 50보다 훨씬 낮은 시간으로 변경하는 것이 좋다.
InnoDB는 예상치 못한 오류로부터 데이터를 보호하기 위해 여러 가지 기능을 갖추고 있다. 이를 통해 MySQL이 시작될 때 완료되지 못한 트랜잭션이나 디스크에 일부만 기록된 데이터 페이지 등에 대한 복구 작업이 자동으로 진행된다.