SQLite 공유 캐시 모드
SQLite 공유 캐시 모드 (Shared-Cache Mode)
SQLite는 버전 3.3.0(2006-01-11)부터 임베디드 서버에서 사용하기 위한 특별한 "공유 캐시"(shared-cache) 모드(기본적으로 비활성화)를 포함해요. 공유 캐시 모드가 활성화되고 한 스레드가 같은 데이터베이스에 여러 연결을 설정하면, 그 연결들은 단일 데이터 및 스키마 캐시를 공유해요. 이는 시스템에 필요한 메모리와 IO의 양을 크게 줄일 수 있어요.
버전 3.5.0(2007-09-04)에서 공유 캐시 모드는 같은 캐시가 단일 스레드 내에서뿐 아니라 전체 프로세스에 걸쳐 공유될 수 있도록 수정됐어요. 이 변경 이전에는 스레드 간에 데이터베이스 연결을 전달하는 데 제한이 있었어요. 그 제한은 3.5.0 업데이트에서 제거됐어요. 이 문서는 버전 3.5.0 기준의 공유 캐시 모드를 설명해요.
공유 캐시 모드는 어떤 경우에는 잠금 모델의 의미론을 바꿔요. 세부 사항은 이 문서에서 설명해요. 일반적인 SQLite 잠금 모델에 대한 기본적인 이해(자세한 내용은 SQLite 버전 3의 파일 잠금 및 동시성 참조)가 전제돼요.
출처: 문서
본문
1. 공유 캐시 사용은 권장되지 않아요 (Use of shared-cache is discouraged)
공유 캐시 모드는 더 이상 사용되지 않는(obsolete) 기능이에요. 공유 캐시 모드의 사용은 권장되지 않아요. 공유 캐시의 대부분의 사용 사례는 WAL 모드로 더 잘 해결돼요.
공유 캐시 모드는 2006년 Symbian 개발자들의 요청으로 발명됐어요. 그들의 문제는 휴대전화의 연락처 데이터베이스가 동기화되고 있으면 그 데이터베이스 파일이 잠겨 버린다는 것이었어요. 그런데 전화가 오면 데이터베이스 잠금 때문에 수신 전화에 적절한 벨소리나 화면에 보여줄 발신자 사진 등을 찾기 위해 연락처 데이터베이스를 질의할 수 없었어요. WAL 모드(약 2010년)는 트랜잭션 격리를 깨지 않으면서 동시 접근을 허용하므로 이 문제에 대한 더 나은 해결책이에요.
소스 코드에서 SQLite를 직접 빌드하는 애플리케이션은 -DSQLITE_OMIT_SHARED_CACHE 컴파일 타임 옵션을 사용하는 것이 권장돼요. 결과 바이너리가 더 작고 더 빠르기 때문이에요.
여기에 설명된 공유 캐시 인터페이스는 완전한 이전 버전 호환성을 보장하기 위해 SQLite에서 계속 지원될 거예요. 하지만 공유 캐시의 사용은 권장되지 않아요.
공유 캐시 잠금 모델 (Shared-Cache Locking Model)
외부에서, 즉 다른 프로세스나 스레드의 관점에서 보면 공유 캐시를 사용하는 둘 이상의 데이터베이스 연결은 하나의 연결처럼 보여요. 여러 공유 캐시 또는 일반 데이터베이스 사용자 사이를 중재하는 데 사용되는 잠금 프로토콜은 다른 곳에 설명돼 있어요.
Figure 1은 세 개의 데이터베이스 연결이 설정된 예시 런타임 구성을 나타내요. 연결 1은 일반 SQLite 데이터베이스 연결이에요. 연결 2와 3은 캐시를 공유해요. 일반 잠금 프로토콜이 연결 1과 공유 캐시 사이의 데이터베이스 접근을 직렬화해요. 연결 2와 3에 의한 공유 캐시 접근을 직렬화(또는 아래 "Read-Uncommitted 격리 모드" 참조, 직렬화하지 않음)하는 데 사용되는 내부 프로토콜은 이 절의 나머지에서 설명해요.
공유 캐시 잠금 모델에는 세 가지 수준이 있어요. 트랜잭션 수준 잠금, 테이블 수준 잠금, 스키마 수준 잠금이에요. 이들은 다음 세 개의 하위 절에서 설명돼요.
1. 트랜잭션 수준 잠금 (Transaction Level Locking)
SQLite 연결은 읽기 트랜잭션과 쓰기 트랜잭션의 두 종류의 트랜잭션을 열 수 있어요. 이것은 명시적으로 수행되지 않아요. 트랜잭션은 처음으로 데이터베이스 테이블에 쓸 때까지 암묵적으로 읽기 트랜잭션이며, 그 시점에 쓰기 트랜잭션이 돼요.
한 번에 단일 공유 캐시에 대한 연결 중 최대 하나만 쓰기 트랜잭션을 열 수 있어요. 이것은 어떤 수의 읽기 트랜잭션과도 공존할 수 있어요.
2. 테이블 수준 잠금 (Table Level Locking)
둘 이상의 연결이 공유 캐시를 사용할 때, 잠금은 테이블 단위로 동시 접근 시도를 직렬화하는 데 사용돼요. 테이블은 "읽기 잠금"(read-lock)과 "쓰기 잠금"(write-lock)의 두 종류의 잠금을 지원해요. 잠금은 연결에 부여돼요. 각 데이터베이스 연결은 어떤 시점에 각 데이터베이스 테이블에 대해 읽기 잠금, 쓰기 잠금, 또는 잠금 없음 중 하나를 가져요.
한 번에 단일 테이블은 얼마든지 많은 수의 활성 읽기 잠금이나 단일 활성 쓰기 잠금을 가질 수 있어요. 테이블에서 데이터를 읽으려면 연결은 먼저 읽기 잠금을 얻어야 해요. 테이블에 쓰려면 연결은 그 테이블에 대한 쓰기 잠금을 얻어야 해요. 필요한 테이블 잠금을 얻을 수 없으면 쿼리는 실패하고 호출자에게 SQLITE_LOCKED가 반환돼요.
연결이 테이블 잠금을 얻으면 현재 트랜잭션(읽기 또는 쓰기)이 끝날 때까지 해제되지 않아요.
2.1. Read-Uncommitted 격리 모드 (Read-Uncommitted Isolation Mode)
위에서 설명한 동작은 read_uncommitted pragma를 사용해 격리 수준을 직렬화(기본값)에서 read-uncommitted로 변경함으로써 약간 수정될 수 있어요.
read-uncommitted 모드의 데이터베이스 연결은 위에서 설명한 대로 데이터베이스 테이블에서 읽기 전에 읽기 잠금을 얻으려고 시도하지 않아요. 이는 다른 데이터베이스 연결이 읽히고 있는 동안 테이블을 수정하면 일관되지 않은 쿼리 결과를 초래할 수 있지만, read-uncommitted 모드의 연결이 연 읽기 트랜잭션은 다른 어떤 연결에 의해서도 차단되거나 차단할 수 없다는 것을 의미하기도 해요.
Read-uncommitted 모드는 데이터베이스 테이블에 쓰는 데 필요한 잠금에는 영향을 주지 않아요. (즉, read-uncommitted 연결은 여전히 쓰기 잠금을 얻어야 하므로 데이터베이스 쓰기는 여전히 차단되거나 차단될 수 있어요.) 또한 read-uncommitted 모드는 아래 나열된 규칙에 필요한 sqlite_schema 잠금에도 영향을 주지 않아요. ("스키마(sqlite_schema) 수준 잠금" 절 참조)
/* Set the value of the read-uncommitted flag:
**
** True -> Set the connection to read-uncommitted mode.
** False -> Set the connection to serialized (the default) mode.
*/
PRAGMA read_uncommitted = boolean;
/* Retrieve the current value of the read-uncommitted flag */
PRAGMA read_uncommitted;
3. 스키마(sqlite_schema) 수준 잠금 (Schema (sqlite_schema) Level Locking)
sqlite_schema 테이블은 다른 모든 데이터베이스 테이블과 같은 방식으로 공유 캐시 읽기 및 쓰기 잠금을 지원해요(위 설명 참조). 다음 특별 규칙도 적용돼요.
- 연결은 데이터베이스 테이블에 접근하거나 다른 읽기·쓰기 잠금을 얻기 전에 sqlite_schema에 대한 읽기 잠금을 얻어야 해요.
- 데이터베이스 스키마를 수정하는 문(예: CREATE 또는 DROP TABLE)을 실행하기 전에 연결은 sqlite_schema에 대한 쓰기 잠금을 얻어야 해요.
- 다른 연결이 어떤 첨부된 데이터베이스(기본 데이터베이스 "main" 포함)의 sqlite_schema 테이블에 대한 쓰기 잠금을 보유하고 있으면 연결은 SQL 문을 컴파일할 수 없어요.
스레드 관련 문제 (Thread Related Issues)
공유 캐시 모드가 활성화된 SQLite 버전 3.3.0부터 3.4.2에서는 데이터베이스 연결이 sqlite3_open()을 호출해 그 연결을 만든 스레드에 의해서만 사용될 수 있었어요. 그리고 연결은 같은 스레드의 다른 연결과만 캐시를 공유할 수 있었어요. 이러한 제한은 SQLite 버전 3.5.0(2007-09-04)부터 제거됐어요.
공유 캐시와 가상 테이블 (Shared Cache And Virtual Tables)
이전 SQLite 버전에서는 공유 캐시 모드를 가상 테이블과 함께 사용할 수 없었어요. 이 제한은 SQLite 버전 3.6.17(2009-08-10)에서 제거됐어요.
공유 캐시 모드 활성화 (Enabling Shared-Cache Mode)
공유 캐시 모드는 프로세스 단위로 활성화돼요. C 인터페이스를 사용하면 다음 API로 공유 캐시 모드를 전역적으로 활성화하거나 비활성화할 수 있어요.
int sqlite3_enable_shared_cache(int);
sqlite3_enable_shared_cache()에 대한 각 호출은 sqlite3_open(), sqlite3_open16(), sqlite3_open_v2()로 생성된 이후의 데이터베이스 연결에 영향을 줘요. 이미 존재하는 데이터베이스 연결은 영향을 받지 않아요. sqlite3_enable_shared_cache()에 대한 각 호출은 같은 프로세스 내의 이전 모든 호출을 덮어써요.
sqlite3_open_v2()로 생성된 개별 데이터베이스 연결은 세 번째 매개변수에 SQLITE_OPEN_SHAREDCACHE 또는 SQLITE_OPEN_PRIVATECACHE 플래그를 사용해 공유 캐시 모드에 참여할지 여부를 선택할 수 있어요. 이 두 플래그 중 하나를 사용하면 sqlite3_enable_shared_cache()가 설정한 전역 공유 캐시 모드 설정을 덮어써요. 두 플래그 중 하나만 사용해야 해요. sqlite3_open_v2()의 세 번째 인수에 SQLITE_OPEN_SHAREDCACHE와 SQLITE_OPEN_PRIVATECACHE 두 플래그를 모두 사용하면 동작은 정의되지 않아요.
URI 파일 이름을 사용하면 "cache" 쿼리 매개변수로 데이터베이스가 공유 캐시를 사용할지 여부를 지정할 수 있어요. "cache=shared"로 공유 캐시를 활성화하고 "cache=private"로 공유 캐시를 비활성화해요. URI 쿼리 매개변수로 데이터베이스 연결의 캐시 공유 동작을 지정할 수 있는 능력은 ATTACH 문에서 캐시 공유를 제어할 수 있게 해줘요. 예를 들어:
ATTACH 'file:aux.db?cache=shared' AS aux;
공유 캐시와 인메모리 데이터베이스 (Shared Cache And In-Memory Databases)
SQLite 버전 3.7.13(2012-06-11)부터 공유 캐시는 URI 파일 이름으로 생성된 인메모리 데이터베이스에서 사용될 수 있어요. 이전 버전과의 호환성을 위해, 장식되지 않은 이름 ":memory:"로 데이터베이스를 열면 인메모리 데이터베이스에 대해 공유 캐시는 항상 비활성화돼요. 버전 3.7.13 이전에는 사용된 데이터베이스 이름, 현재 시스템 공유 캐시 설정, 쿼리 매개변수나 플래그와 무관하게 인메모리 데이터베이스에 대해 공유 캐시가 항상 비활성화됐어요.
인메모리 데이터베이스에 공유 캐시를 활성화하면 같은 프로세스의 둘 이상의 데이터베이스 연결이 같은 인메모리 데이터베이스에 접근할 수 있어요. 공유 캐시의 인메모리 데이터베이스는 그 데이터베이스에 대한 마지막 연결이 닫힐 때 자동으로 삭제되고 메모리가 회수돼요.