Redis Transaction
목차
- Multi, Exec, Discard, Watch, Unwatch
- Pipeline, Non Transactional Pipeline
- Lua Script
요약
- Database 의 Transaction : ACID 를 보장하는 작업 단위를 말한다.
- Redis 의 Transaction : (ACID 까지는 아니지만) 독립적인 작업단위를 말한다.
- Multi + Exec : Single Isolation Operation 방식으로 처리하는 방법이다.
- Watch + Multi + Exec : Optimistic Locking 방식으로 처리하는 방법이다.
개발을 할 때, Redis의 Transaction을 다루는 경우 Database의 Transaction의 개념과 헷갈리는 부분들이 있었고, 정확한 이해를 위해 Redis와 Database 에서 말하는 Transaction이 어떤 차이가 있는지 정리합니다.
Database 에서의 Transaction
트랜잭션은 데이터베이스(Database)에서 ACID (Atomic, Consistency, Isolation, Durability)를 보장하는 작업단위를 말합니다. ACID는 각각 원자성, 일관성, 독립성, 지속성을 의미하고 Transaction이라는 한가지 작업을 처리하는데 보장하는 성질을 의미합니다. 개발자는 Database 에 반영되어야 하는 한가지 작업을 완전히 수행하기 위해, 하나 혹은 둘 이상의 SQL문(ex, Select, Insert, Delete, Update) 조합하게 됩니다. 이 때, 둘 이상의 SQL 문들의 조합으로 구성하는 경우, 원자성(Atomic)은 여러 개의 SQL 문들이 마치 하나처럼 완전히 실행되거나 실행되지 않도록 Single Operation으로 동작하게 하는 것, 일관성(Consistency)은 실행 이전과 실행 이후의 상태가 일관된 유지된다는 의미로 무결성 제약조건에 위배되지 않는 것, 독립성(Isolation)은 Transaction 을 구성하는 SQL 문들이 실행 되는 중간에 다른 Transaction 들이 끼어들지 못하는 온전히 독립된 상태로 실행되는 것으로 동시성 제어(Concurrency Control)를 위함이고, 지속성(Durability)은 실행 완료 이후 변경사항이 영구적으로 유지된다는 것으로 SSD, Disk 등에 보관된 상태로 존재한다는 것을 의미합니다. Transaction이 실제로 어떻게 사용될 수 있는지 아래의 2가지 예시를 통해 살펴봅니다.
example 1
A 테이블의 특정 Record의 Column 값에 따라 B와 C의 테이블에 한번에 반영이 되어야 한다면, A 테이블 조회를 위한 Select + B 테이블 갱신을 위한 Insert + C 테이블 갱신을 위한 Insert 를 필요로 할 것입니다. 이를 Query 문으로 작성한다면 아래와 같을 것이고 이 Query 문들은 전부 실행이 되어 반영이 되거나 혹은 전부 반영이 되지 않아야 합니다. 만약에, B 테이블은 제대로 반영이 되었는데, C 테이블은 primary key 가 중복되어 duplicate entry 상황으로 에러가 발생되었다고 해서 C 테이블에만 반영이 안되는 상황이 일어나면 안된다는 것입니다. 다시 말해서, Database System 에서 Transaction의 특징 중 전체가 실행되거나 실행되지 않아야 한다는 원자성(Atomic) 보장해주고 있기하기 때문에 이러한 처리가 가능한 것입입니다.
각 query 문을 독립적으로 실행
select data1, data2 from A
insert into B (key) values (data1 조회 값)
insert into C (key) values (data2 조회 값)
각 query 문을 하나의 transaction으로 묶어서 실행 (=transaction의 ACID 를 보장)
> begin
> select data1, data2 from A
// data1, data2 조회값 확인
> insert into B (key) values (data1 조회 값)
> insert into C (key) values (data2 조회 값)
> commit
example 2
카카오페이, 토스, 국민은행 등에서 사용하는 돈을 관리하는 프로그램을 개발한다고 생각해본다면, 그리고 이 프로그램이 사용자의 계좌 정보를 유지하기 위해 만든 테이블을 간단히 구성해본다면 아래처럼 생각해볼 수 있습니다.
account table (계좌 테이블)
id | account (계좌) | balance (금액) |
joker | 01-001-0001 | 1000 |
batman | 01-001-0002 | 2000 |
위 처럼 계좌 테이블이 구성되어 있을 때, joker가 batman 계좌로 500원을 입금한다면 joker의 계좌에서 -500, batman 계좌에서 +500이 되어야 합니다. 그리고 아래처럼 이 작업은 2개의 Select, 2개의 Update 쿼리의 조합과 쿼리문들 사이에 개발자가 작성해놓은 프로그램의 코드가 일부 작성되어 있을 것입니다. 개발자가 의도한 것은 코딩되어 있는 4개의 쿼리가 하나의 Transaction이 되어 DB에서 모두 완벽히 수행되는 것입니다.
만약, Transaction 내의 SQL문 실행 중에 다른 Transaction 의 SQL 문이 동일한 Table의 Record를 접근하려 한다면, 이를 막고 먼저 실행되고 있는 Transaction 의 처리가 완료된 이후 다음 Transaction이 처리되어야 합니다.
예를 들어, joker가 의도적으로 PC 은행앱과 스마트폰 은행앱으로 동시에 500원을 batman 으로 보내는 상황을 만든다고 할지라도, PC 은행앱에서 요청한 Transaction과 스마트폰 은행앱에서 요청한 Transaction은 각각 독립적으로 실행되어야 한다는 것입니다. 두 Transaction 내의 SQL 문들이 섞여서 처리된다면 어떤 문제가 발생할지 모르기 때문입니다. 독립적으로 실행되지 않고 섞일 수 있다면 joker의 돈은 500원만 줄었는데 batman의 돈은 1000원이 늘어나는 상황이나 joker의 돈은 1000원이 줄었는데 batman의 돈은 500원만 늘어나는 상황들이 발생할 수도 있구요. 다시 말해서, Database System 에서 Transaction의 특징 중 각 Transaction은 독립적인 환경에서 실행되어야 한다는 독립성(Isolation)을 보장해주고 있기하기 때문에 이러한 처리가 가능한 것입입니다.
각 query 문을 독립적으로 실행
> select balance from account where id = joker
// joker의 balance 조회값 확인
> select balance from account where id = batman
// batman의 balance 조회값 확인
> update account set balance = (joker의 balance 조회값 - 500) where id = joker
> update account set balance = (batman의 balance 조회값 + 500) where id = batman
각 query 문을 하나의 transaction으로 묶어서 실행 (=transaction의 ACID 를 보장)
> begin
> select balance from account where id = joker
// joker의 balance 조회값 확인
> select balance from account where id = batman
// batman의 balance 조회값 확인
> update account set balance = (joker의 balance 조회값 - 500) where id = joker
> update account set balance = (batman의 balance 조회값 + 500) where id = batman
> commit
Redis 에서의 Transaction
Redis 에서 Transaction 을 말하면 보통 Multi 와 Exec 명령의 조합으로 이루어진 Operation을 의미하며, 하나의 Single Isolation Operation 처리가 가능하도록 제공하는 것이 목적입니다.
Transaction은 (Lua script를 제외하고) 일반적으로 두가지 형태로 사용하는데, Multi + Exec 조합과 Watch + Multi + Exec 조합의 형태입니다. 각 조합에 대한 방법을 설명을 하기 이전에, 우선 Transaction 관련 명령들의 역할을 소개하면 아래와 같습니다.
- Multi 명령 : Transaction 시작을 나타내는 명령
- Exec 명령 : Transaction 작성을 마치고 내용들을 실행하는 명령
- Discard 명령 : Transaction 을 취소시키는 명령
- Watch 명령 : Transaction 이 마무리 되는 명령(Exec, Discard)이 실행되기 이전에 모니터링 중이던 key 의 변경사항이 있다면 반영하지 않도록 하는 사전에 등록해두는 명령 (=마치 Exec 명령 앞에 IF 조건문을 사용하는 형태)
- Unwatch 명령 : Transaction 이 마무리 되는 명령(Exec, Discard)이 실행되기 이전에 모니터링 중이던 key 를 모니터링하던 Watch의 처리를 중단하는 명령 (=Exec, Discard 명령은 Unwatch 명령을 포함함)
Multi + Exec or Discard
Multi 명령 이후, 각 명령들은 Atomic 하게 진행되므로 Transaction이 진행되는 동안 다른 명령이 끼어들 수 없습니다. Multi 명령 이후, Exec에 의해 한번에 실행될 때 만약 Multi ~ Exec 명령 이내의 어느 명령어가 실패하는 경우가 발생하더라도 나머지 명령은 실행될 수 있습니다. (이 말은 Database의 Transaction 처럼 Transaction내 명령들을 모두 반영하거나 모두 반영하지 않거나를 보장하지 않는다는 말입니다) Multi + Exec 명령으로 만들어진 Transaction은 일부 실패하더라도 롤백(Rollback)이 되지 않습니다. 이 점이 DB의 Transaction과 다른 점입니다. Redis는 태생자체가 롤백을 지원하지 않지만 만약, Watch 를 사용한다면 변경사항을 반영하기 이전에 처리 되는 중간에 참조하는 값에 대한 변화가 있었는지 체크하고 반영을 하지 않도록 처리하는 로직을 추가할 수는 있습니다. Watch에 대한 설명이 조금 어려운 것 같지만, Watch에 대해서는 아래에서 다시 설명합니다.
처리 순서
- Multi 명령 실행 이후의 다음 명령들은 Queue에 넣는다.
- (한번에 실행될) 명령들(ex, Get, Lpush, Set)을 Queue에 넣는다.
- 실행할 경우,
- Exec 명령 실행으로, Queue의 모든 명령들이 실행되고 Trasnaction을 종료한다.
- 실행하지 않을 경우,
- Discard 명령 실행으로 Queue는 모두 명령들을 비우고 Transaction을 종료한다.
처리 예시
MULTI
val1 = GET key1 // 코드 구현부 내용
val1 = val1 + 9 // 코드 구현부 내용
SET key1 $val1 // Multi/Exec 내에 있는 key들은 외부에서 변경이 불가능하다. (외부에서 key1 변경 불가능)
EXEC
Watch + Multi + Exec
Transaction (Multi + Exec) 처리에서 Watch는 (CAS와 유사한 방법으로) Optimistic Lock 기능이 추가됩니다. Transaction 처리에서 Watch는 (Redis에서 처리하려는 Transaction에 포함된 키의 수정을 시도함으로써) Race Condition 상황을 발생시키는 다른 Client들의 요청들은 모두 실패로 만들어버립니다.
Muti + Exec 조합에서 Watch 가 추가되면, 위 그림에서 보면 Multi + Exec 명령 이전에 Watch 명령이 추가되어 있는데, Watch A 명령은 Unwatch 명령을 만날 때까지 A 키 값의 변경사항이 있는지를 모니터링하다가, Exec 이전에 변경사항이 있다면 Transaction 실행을 하지 말고(=중단시키고) Unwatch 명령을 실행하라는 의미를 가지고 있습니다. 위 그림에서 Watch 로 인해 Transaction 처리가 어떻게 달라질 수 있는지 3가지 case를 살펴보겠습니다.
case1 은 Client X의 Transaction이 실행되기 전에 "Transaction 내에 포함된 A키 값을 변경"시키는 상황입니다. 이 경우, 먼저 실행된 Incr A 명령이 반영되고 그 뒤에 Transaction 내용은 모두 무효화됩니다.
// Client X 에서 실행한 명령
> watch a
OK
> incr a
"1"
> multi
OK
> incr a
QUEUED
> exec
(nil) // Client X의 "incr a"가 실행이 되지 않았기 때문에 (nil)이 출력됨
> get a
"1"
case2 는 Client X의 Transaction이 시작되기 전에 Client Y가 "Client X의 Transaction 내에 포함된 A키 값을 변경"시키는 상황입니다. 이 경우, 먼저 실행된 Client Y의 Incr A 명령이 반영되고 그 뒤에 Client X의 Transaction 내용은 모두 무효화됩니다.
// Client X 에서 실행한 명령
> watch a
OK
> multi // 이 명령을 실행하기 이전에 Client Y 에서 "incr a" 를 실행
OK
> incr a
QUEUED
> exec
(nil) // Client X의 "incr a"가 실행이 되지 않았기 때문에 (nil)이 출력됨
> get a
"1"
case3 은 Client X의 Transaction이 완료되기 전에 Client Y가 "Client X의 Transaction 내에 포함된 A키 값을 변경"시키는 상황입니다. 이 경우, 먼저 실행처리가 완료된 Client Y의 Incr A 명령이 반영되고 실행처리가 완료되지 않은 Client X의 Transaction 내용은 모두 무효화됩니다.
// Client X 에서 실행한 명령
> watch a
OK
> multi
OK
> incr a // 이 명령을 실행하기 이전에 Client Y 에서 "incr a" 를 실행
QUEUED
> exec
(nil) // Client X의 "incr a"가 실행이 되지 않았기 때문에 (nil)이 출력됨
> get a
"1"
처리 순서
- Watch 명령이후 특정 키 모니터링을 시작하고, Exec(Unwatch 포함) or Discard 명령(Unwatch 포함) or Unwatch 명령 이전까지 변경사항을 확인한다.
- Multi 명령 실행 이후의 다음 명령들은 Queue에 넣는다.
- (한번에 실행될) 명령들(ex, Get, Lpush, Set)을 Queue에 넣는다.
- Exec 명령 실행으로, Queue의 모든 명령들이
- Watch 명령이후 모니터링하던 키 변경사항이 있었다면, 모든 변경사항이 반영되지 않는다.
- Watch 명령이후 모니터링하던 키 변경사항이 없었다면, 모든 변경사항이 반영된다.
처리 예시
WATCH key1
val1 = GET key1 // 코드 구현부 내용
val1 = val1 + 9 // Watch~Exec 내의 명령들이 Isolation Operation 처리가 되진 않지만, 모니터링을 하고 있으므로 변경을 감지한다.
MULTI
SET key1 $val1
EXEC
Redis 에서의 Transaction Q&A
Q. Check-And-Set or Compare-And-Swap (CAS) ?
Atomic 명령이 가능하게 하는 알고리즘을 의미합니다. 멀티 스레드, 멀티 코드 등의 병렬 구조를 위한 알고리즘으로, 요약하면, 여러개의 스레드 중 하나의 스레드에 저장된 값과 메인 스레드에 저장된 값을 비교하여 일치한다면 메인 스레드의 값을 새로운 값으로 세팅(교체)하고, 틀리다면 실패 후 다시 시도함으로써 여러개의 스레드이더라도 모두 하나의 값을 참조할 수 있도록 제공하는 방법입니다.
Q. Race Condition ?
둘 이상의 프로세스, 스레드가 하나의 데이터에 접근하려는 경우를 말하며, 이 경우 먼저 집입한 스레드가 해당 데이터 접근에 Lock을 걸어버리면서 해결합니다.
Q. Optimistic Lock ?
Begin, Modify, Validate, Commit/Rollback 단계를 거쳐서 여러 Transaction 들이 각각 독립적으로 실행이 한번에 되는 것을 말합니다.
Q. Redis 공식 홈페이지에서 설명하는 Transaction ?
Redis 사이트에서는 Redis에서 말하는 Transaction이 무엇인지를 정의하고 있는데, 몇가지 내용을 발췌하면 아래와 같습니다.
"All the commands in a transaction are serialized and executed sequentially. This guarantees that the commands are executed as a single isolated operation."
위 문장은 Transaction 내의 모든 명령은 순서대로 실행되며, 고립된 상태로 처리되어 중간에 다른 명령이 끼어들 수 없음을 의미합니다.
"Either all of the commands or none are processed, so a Redis transaction is also atomic."
위 문장은 모든 명령이 실행되는 것도, 모든 명령이 실행되지 않는 것도 가능하기 때문에, Redis Transaction을 atomic 이라고 부르기도 한다는 것을 의미합니다. 아래가 모든 명령이 실행되지 않을 경우에 대한 예시코드입니다.
// 아래 명령을 실행할 경우, LRANGE에 대한 문법이 틀려서 실패했지만, LPUSH를 실행하지 않는다.
> MULTI
OK
> LPUSH p 1 2 3 4 5
QUEUED
> LRANGE 0 -1 // 정상은 LRANGE p 0 -1 이므로 에러가 발생한다.
(error) ERR wrong number of arguments for 'lrange' command
> EXEC
(error) EXECABORT Transaction discard because of previous errors.
> get p
(nil)
"It's important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands."
위 문장은 Transaction 내 명령 하나가 실패하더라도 다른 명령들이 처리되니, 이 점을 잘 염두해두고 사용해야 한다는 것을 의미합니다. 아래가 예시코드입니다.
// 아래 명령을 실행할 경우, 2개는 정상처리되고 2개는 에러가 발생한다.
> SET a abc
OK
> SET b 1
OK
> MULTI
OK
> INCR a
QUEUED
> LPOP a
QUEUED
> INCR b
QUEUED
> LPOP c
QUEUED
> EXEC
1) (error) ERR value is not an integer or out of range // a는 integer 타입이 아니라서 증가시킬 수가 없다.
2) (error) WRONGTYPE Oeration against a key holding the wrong kind of value // a는 List 구조가 아니니 lpop 명령을 사용할수 없다.
3) "2"
4) (nil)
참고
(Redis) Transaction 설명 링크, 번역링크
(Redis) Command 설명 MULTI, EXEC, DISCARD, WATCH, UNWATCH
(Redis) Redis에서 Transaction Rollback이 필요없는 이유 설명
(Blog) CAS 알고리즘 링크
(Blog) Redis Transaction - MULTI/EXEC + WATCH (Optimistic Lock) 설명 링크
(Blog) Redis Transaction - WATCH/UNWATCH 설명 링크
(Blog) Redis Transaction - UNWATCH 사용 예 코드 링크
(Blog) Redis Transaction - MULTI/EXEC, DISCARD - java 예제 코드 링크
(Stackoverflow) Redis and Data Integrity 링크
(IBM) ACID properties of transactions 링크
(PPT) Redis Transaction - Optimistic locking 링크