[Daily morning study] Redis ์บ์ ์ ๋ต (Cache-Aside, Write-Through, Write-Behind)
#daily morning study
์ ์บ์๊ฐ ํ์ํ๊ฐ
DB์ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆด ๋๋ง๋ค ๋์คํฌ I/O๊ฐ ๋ฐ์ํ๋ค. ๋์ผํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ณตํด์ ์ฝ๋ ํจํด์ด ์๋ค๋ฉด, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํด๋๊ณ ๋ฐ๋ก ๋๋ ค์ฃผ๋ ๊ฒ ํจ์ฌ ๋น ๋ฅด๋ค. Redis๋ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ ์คํ ์ด๋ก, ํ๊ท ์๋ต ์๊ฐ์ด 1ms ๋ฏธ๋ง์ด๋ผ ์ด๋ฐ ์บ์ ๋ ์ด์ด๋ก ๋ง์ด ์ฐ์ธ๋ค.
์บ์๋ฅผ ๋์ ํ ๋ ํต์ฌ ๊ฒฐ์ ์ฌํญ์ ๋ ๊ฐ์ง๋ค.
- ์ฝ๊ธฐ ์ ๋ต: ์บ์์ ๋ฐ์ดํฐ๊ฐ ์์ ๋(Cache Miss) ์ด๋ป๊ฒ ์ฑ์ธ ๊ฒ์ธ๊ฐ
- ์ฐ๊ธฐ ์ ๋ต: ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๋ ์บ์์ DB๋ฅผ ์ด๋ป๊ฒ ๋๊ธฐํํ ๊ฒ์ธ๊ฐ
์ฝ๊ธฐ ์ ๋ต
Cache-Aside (Lazy Loading)
๊ฐ์ฅ ํํ๊ฒ ์ฐ์ด๋ ํจํด์ด๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ด ์บ์๋ฅผ ์ง์ ๊ด๋ฆฌํ๋ค.
1. ์ฑ โ Redis ์กฐํ
2. Cache Hit โ ๋ฐ๋ก ๋ฐํ
3. Cache Miss โ DB ์กฐํ โ ๊ฒฐ๊ณผ๋ฅผ Redis์ ์ ์ฅ โ ๋ฐํ
def get_user(user_id: str):
cached = redis.get(f"user:{user_id}")
if cached:
return json.loads(cached)
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
redis.setex(f"user:{user_id}", 3600, json.dumps(user)) # TTL 1์๊ฐ
return user
์ฅ์
- ์ค์ ๋ก ์ฝํ๋ ๋ฐ์ดํฐ๋ง ์บ์ฑ๋๋ฏ๋ก ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น๊ฐ ์ ๋ค
- Redis๊ฐ ๋ค์ด๋์ด๋ DB๋ก ํด๋ฐฑํ ์ ์์ด ์ฅ์ ๋ด์ฑ์ด ๋๋ค
๋จ์
- ์ฒ์ ์์ฒญ(Cache Miss)์ ํญ์ ๋๋ฆฌ๋ค (Cold Start)
- DB ๋ฐ์ดํฐ๊ฐ ๋ฐ๋์ด๋ TTL์ด ๋ง๋ฃ๋๊ธฐ ์ ๊น์ง ์บ์๋ ์ค๋๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ค (Stale Data ๋ฌธ์ )
Read-Through
Cache-Aside์ ๋น์ทํ์ง๋ง ์บ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ณ๋ ๋ ์ด์ด๊ฐ DB ์กฐํ๊น์ง ๋ด๋นํ๋ค. ์ฑ ์ฝ๋๋ ํญ์ ์บ์์๋ง ์์ฒญํ๋ค.
์ฑ โ ์บ์ ๋ ์ด์ด โ (Miss๋ฉด) DB ์กฐํ โ ์บ์ ์ ์ฅ โ ๋ฐํ
์ฑ ์ฝ๋๊ฐ ๋จ์ํด์ง์ง๋ง, ์บ์ ๋ ์ด์ด๊ฐ DB ์คํค๋ง๋ฅผ ์์์ผ ํ๋ฏ๋ก ๊ฒฐํฉ๋๊ฐ ๋์์ง๋ค.
์ฐ๊ธฐ ์ ๋ต
Write-Through
๋ฐ์ดํฐ๋ฅผ ์ธ ๋ ์บ์์ DB์ ๋์์ ์ ์ฅํ๋ค.
์ฑ โ Redis ์ ์ฅ โ DB ์ ์ฅ โ ์๋ฃ ์๋ต
def update_user(user_id: str, data: dict):
# ์บ์ ๋จผ์ ์
๋ฐ์ดํธ
redis.setex(f"user:{user_id}", 3600, json.dumps(data))
# DB๋ ๋ฐ๋ก ์
๋ฐ์ดํธ
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
์ฅ์
- ์บ์์ DB๊ฐ ํญ์ ๋๊ธฐํ๋์ด Stale Data ๋ฌธ์ ๊ฐ ์๋ค
- Cache-Aside์ ์ฒซ Miss ๋ฌธ์ ๊ฐ ์๋ค
๋จ์
- ์ฐ๊ธฐ๊ฐ ๋ ๋ฒ ๋ฐ์ํ๋ฏ๋ก ์ฐ๊ธฐ ๋ ์ดํด์๊ฐ ๋์ด๋๋ค
- ์ค์ ๋ก ๋ค์ ์ฝํ์ง ์๋ ๋ฐ์ดํฐ๋ ์บ์์ ์์ธ๋ค (๋ถํ์ํ ์บ์ ์ค์ผ)
Write-Behind (Write-Back)
์บ์์๋ง ๋จผ์ ์ฐ๊ณ , DB ๋ฐ์์ ๋น๋๊ธฐ๋ก ๋์ค์ ์ฒ๋ฆฌํ๋ค.
์ฑ โ Redis ์ ์ฅ โ ์๋ฃ ์๋ต
โ (๋น๋๊ธฐ)
DB ์ ์ฅ
์ฅ์
- ์ฐ๊ธฐ ์๋ต์ด ๋งค์ฐ ๋น ๋ฅด๋ค
- DB์ ์ฐ๊ธฐ๊ฐ ๋ชฐ๋ฆฌ๋ ์ํฉ(Write-Heavy)์์ ๋ถํ๋ฅผ ๋ถ์ฐํ ์ ์๋ค
๋จ์
- Redis๊ฐ DB์ ๋ฐ์ํ๊ธฐ ์ ์ ๋ค์ด๋๋ฉด ๋ฐ์ดํฐ๊ฐ ์ ์ค๋๋ค
- ๊ตฌํ ๋ณต์ก๋๊ฐ ๋๋ค (๋น๋๊ธฐ ํ, ์คํจ ์ฌ์๋ ๋ก์ง ํ์)
- ๊ธ์ต ํธ๋์ญ์ ์ฒ๋ผ ๋ฐ์ดํฐ ๋ด๊ตฌ์ฑ์ด ์ค์ํ ์์คํ ์๋ ๋ถ์ ํฉํ๋ค
Write-Around
์ฐ๊ธฐ ์ ์บ์๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๊ณ DB์๋ง ์ ์ฅํ๋ค. ์ฝ์ ๋ Cache Miss๊ฐ ๋๋ฉด ๊ทธ๋ ์บ์๋ฅผ ์ฑ์ด๋ค.
์ผํ์ฑ ๋ฐ์ดํฐ๋ ์์ฃผ ์ฝํ์ง ์๋ ๋ฐ์ดํฐ์ ์ ํฉํ๋ค. ๋์ฉ๋ ๋ก๊ทธ ์ ์ฅ, ๋ฐฐ์น ์ฒ๋ฆฌ ๊ฒฐ๊ณผ ๋ฑ์ด ์์๋ค.
์ ๋ต ๋น๊ต
| ์ ๋ต | ์ฝ๊ธฐ ์ฑ๋ฅ | ์ฐ๊ธฐ ์ฑ๋ฅ | ์ผ๊ด์ฑ | ์ฅ์ ๋ด์ฑ |
|---|---|---|---|---|
| Cache-Aside | Cache Hit ์ ๋น ๋ฆ | ๋ณดํต | ๋ฎ์ (Stale ๊ฐ๋ฅ) | ๋์ |
| Read-Through | Cache Hit ์ ๋น ๋ฆ | ๋ณดํต | ๋ฎ์ (Stale ๊ฐ๋ฅ) | ์ค๊ฐ |
| Write-Through | ๋น ๋ฆ | ๋๋ฆผ | ๋์ | ์ค๊ฐ |
| Write-Behind | ๋น ๋ฆ | ๋งค์ฐ ๋น ๋ฆ | ์ค๊ฐ | ๋ฎ์ |
| Write-Around | ์ฒซ ์ฝ๊ธฐ ๋๋ฆผ | ๋ณดํต | ๋ณดํต | ๋์ |
์บ์ ๋ฌดํจํ (Cache Invalidation)
์บ์ ์ ๋ต์์ ๊ฐ์ฅ ์ด๋ ค์ด ๋ถ๋ถ์ด ๋ฌดํจํ๋ค. ์ ๋ช ํ ๋ง์ด ์๋ค.
โThere are only two hard things in Computer Science: cache invalidation and naming things.โ
TTL (Time-To-Live)
redis.setex("key", 3600, value) # 1์๊ฐ ํ ์๋ ์ญ์
๋จ์ํ์ง๋ง TTL ๋ง๋ฃ ์ ๊น์ง๋ Stale Data๋ฅผ ์๋นํ๊ฒ ๋๋ค.
์ด๋ฒคํธ ๊ธฐ๋ฐ ๋ฌดํจํ
DB ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๋ ํด๋น ์บ์ ํค๋ฅผ ์ง์ ์ญ์ ํ๊ฑฐ๋ ์ ๋ฐ์ดํธํ๋ค.
def update_user(user_id: str, data: dict):
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
redis.delete(f"user:{user_id}") # ์บ์ ์ญ์ โ ๋ค์ ์ฝ๊ธฐ ๋ DB์์ ์ฑ์์ง
์ญ์ ํ ๋ค์ ์ฝ๊ธฐ๊ฐ Cache-Aside๋ก ์ฑ์์ง๋ ๊ตฌ์กฐ๋ค. ์ ๋ฐ์ดํธ์ ์ญ์ ์ฌ์ด์ ์งง์ ์๊ฐ ๋์ Stale Data๊ฐ ์๋น๋ ์ ์๋ค.
์บ์ ์คํฌํผ๋ (Cache Stampede) ๋ฌธ์
TTL์ด ๋ง๋ฃ๋๋ ์๊ฐ ๋์์ ์๋ฐฑ ๊ฐ์ ์์ฒญ์ด Cache Miss๋ฅผ ๋ฐ์ผ๋ฉด, ๋ชจ๋ DB๋ฅผ ๋์์ ์กฐํํ๋ค. DB์ ๊ฐ์๊ธฐ ๋ถํ๊ฐ ๋ชฐ๋ฆฌ๋ ํ์์ด๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
- ๋ฎคํ ์ค ๋ฝ: ์ฒซ ๋ฒ์งธ ์์ฒญ๋ง DB๋ฅผ ์กฐํํ๊ฒ ํ๊ณ , ๋๋จธ์ง๋ ๋๊ธฐ
def get_with_lock(key: str):
cached = redis.get(key)
if cached:
return cached
lock_key = f"lock:{key}"
if redis.set(lock_key, "1", nx=True, ex=5): # ๋ฝ ํ๋
try:
value = db.query(...)
redis.setex(key, 3600, value)
return value
finally:
redis.delete(lock_key)
else:
# ๋ฝ ํ๋ ์คํจ โ ์ ์ ๋๊ธฐ ํ ์ฌ์๋
time.sleep(0.1)
return get_with_lock(key)
- ํ๋ฅ ์ ์กฐ๊ธฐ ๊ฐฑ์ (Probabilistic Early Expiration): TTL์ด ์์ ํ ๋ง๋ฃ๋๊ธฐ ์ ์ ์ผ๋ถ ์์ฒญ์์ ๋ฏธ๋ฆฌ ๊ฐฑ์
Redis ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ ํ
์บ์ฑ ์๋๋ฆฌ์ค์ ๋ฐ๋ผ Redis ์๋ฃ๊ตฌ์กฐ๋ฅผ ์ ์ ํ ์ ํํ๋ฉด ์ฑ๋ฅ์ ๋ ๋์ด์ฌ๋ฆด ์ ์๋ค.
| ์๋๋ฆฌ์ค | Redis ์๋ฃ๊ตฌ์กฐ | ๋ช ๋ น์ด ์์ |
|---|---|---|
| ๋จ์ ํค-๊ฐ ์บ์ฑ | String | GET, SETEX |
| ์ ์ ์ธ์ , ๊ฐ์ฒด ์บ์ฑ | Hash | HGETALL, HSET |
| ์ต๊ทผ ์กฐํ ๋ชฉ๋ก | List | LPUSH, LRANGE |
| ๋ญํน, ์ ๋ ฌ๋ ๋ชฉ๋ก | Sorted Set | ZADD, ZRANGE |
| ์ค๋ณต ์ ๊ฑฐ (๋ฐฉ๋ฌธ์ ์ ๋ฑ) | Set / HyperLogLog | SADD, PFADD |
์ธ์ ์บ์ฑ์ Hash๋ฅผ ์ฐ๋ฉด ํ๋ ๋จ์๋ก ์ ๋ฐ์ดํธ๊ฐ ๊ฐ๋ฅํด์ String๋ณด๋ค ํจ์จ์ ์ด๋ค.
# String: ์ ์ฒด ๊ฐ์ฒด๋ฅผ ์ง๋ ฌํ/์ญ์ง๋ ฌํ
redis.set("session:abc", json.dumps({"user_id": 1, "role": "admin"}))
# Hash: ํ๋๋ง ์
๋ฐ์ดํธ ๊ฐ๋ฅ
redis.hset("session:abc", "role", "user") # ๋ค๋ฅธ ํ๋๋ ๊ทธ๋๋ก
์บ์ ํํธ์จ ๋ชจ๋ํฐ๋ง
์บ์๊ฐ ์ ๋๋ก ๋์ํ๋์ง ํํธ์จ(Hit Rate)์ ์ง์์ ์ผ๋ก ํ์ธํด์ผ ํ๋ค.
# Redis ํต๊ณ ํ์ธ
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
# keyspace_hits: ์บ์ ํํธ ํ์
# keyspace_misses: ์บ์ ๋ฏธ์ค ํ์
# Hit Rate = hits / (hits + misses)
์ผ๋ฐ์ ์ผ๋ก ํํธ์จ์ด 80% ๋ฏธ๋ง์ด๋ฉด ์บ์ฑ ์ ๋ต์ ์ฌ๊ฒํ ํ ํ์๊ฐ ์๋ค.
์ ๋ฆฌ
- ์ฝ๊ธฐ ์์ฃผ ์๋น์ค: Cache-Aside + TTL ๊ธฐ๋ฐ ๋ฌดํจํ๋ก ์์ํด์ ํํธ์จ ํ์ธ
- ์ฐ๊ธฐ ๋น๋๊ฐ ๋ฎ๊ณ ์ผ๊ด์ฑ์ด ์ค์: Write-Through
- ์ฐ๊ธฐ๊ฐ ๋ง๊ณ ์๋๊ฐ ์ฐ์ : Write-Behind (๋จ, ๋ฐ์ดํฐ ์ ์ค ์ํ ๊ฐ์)
- ์บ์ ์คํฌํผ๋ ๋ฐฉ์ง: ๋ฎคํ ์ค ๋ฝ ๋๋ ํ๋ฅ ์ ์กฐ๊ธฐ ๊ฐฑ์
- ๋ชจ๋ํฐ๋ง: ํํธ์จ ์ถ์ ํ์, 70~80% ๋ฏธ๋ง์ด๋ฉด ์ ๋ต ์ฌ๊ฒํ