사용자에게 맞춤 상품을 보여주는 api(라우터)를 개발할 일이 생겼다.
해당 사용자의 구매 이력과 전체 사용자의 구매 이력들을 통해 맞춤 상품을 보여줘야 하는데, 해당 사용자의 구매 이력을 보여주기 위해 참조하는 테이블은 이미 너무 많은 api에서 접근하기 때문에 부하가 큰 상황이다.
심지어 해당 사용자에게 맞춤 상품을 노출하는 곳이 사용자가 많이 들어가는 화면이다.
db 부하를 최소화하기 위해 캐시 서버인 redis를 사용하게 되어 공부를 시작했다.
redis의 특징을 정리하기 전에 in-memory DB와 cache가 무엇인지 알아야 한다.
in-memory DB는 disk-based DB와 달리 메모리에 데이터를 저장하는 DB 를 말하며 일반적으로 key-value 방식(dictionary / json과 같은 구조)을 많이 사용한다.
OS 수업 등에서 배웠다시피 디스크에 대한 접근은 메모리 <-> 디스크 간 병목(bottleneck)이 있기 때문에 메모리에 대한 접근보다 훨씬 느리다.
병목 현상이 생기는 이유는 disk-based DB는 데이터를 페이지 단위로 읽어오기 때문에 본인이 원하는 데이터가 지금 메모리에 있는 페이지에 없다면 다른 페이지를 요청해야되기 때문이다.
하지만 in-memory DB에도 단점은 있다.
일반적으로 생각하는 DB와 달리 휘발성 이라는 것이다.
에러가 나서 갑자기 프로세스가 죽는다면 데이터가 전부 사라진다.
또한 메모리에 데이터를 저장하기 때문에 저장 공간이 상대적으로 매우 작다.
한번 읽은 데이터를 임의의 공간에 저장하여 다음에 읽을 때는 빠르게 결과를 받을 수 있도록 도와주는 저장공간이다.
캐시 서버에는 Look aside cache, Write Back 두 가지 패턴이 존재한다.
memcached와 비슷한 '캐시 시스템'으로 in-memory DB의 하나로 NoSQL이다.
아마존 공식 문서에 따르면 redis는 다음과 같은 특징을 가지고 있다.
하지만 이를 넘어서는 가장 큰 특징은 아래 사진과 같이 String, Bitmap, Bit field 등 다양한 자료구조를 지원한다는 것이다.
이렇게 다양한 자료구조를 지원하면 개발의 편의성이 높아지고 난이도는 낮아지는 장점이 있다.
예를 들어, 어떤 데이터를 정렬할 때 DBMS를 이용한다면 DB를 데이터에 저장하고, 저장하는 데이터를 정렬하여 다시 읽어오는 과정에서 오버헤드가 더 들 수 있다.
하지만 redis의 Sorted-Set을 이용하면 빠르고 간단하게 데이터를 정렬할 수 있다.
AWS에서는 Amazon ElasticCache라는 이름으로 사용된다.
MacOS: brew install redis
Linux: sudo apt install redis-server
현재는 이미 있는 서버를 사용하므로 클라이언트 측에 대한 지식만 있으면 된다. 추후에 서버를 사용할 일이 있다면 추가할 예정이다.
2 와 같은 명령어를 사용하여 설치한다.
`redis-cli -h ${접속할 호스트} -p ${접속할 포트} -a ${비밀번호}
자세한 명령어는 https://freeblogger.tistory.com/10 참조
cf1) redis-cli에서 :
(콜론)은 namespace를 의미하며, redis 내에서 표현 형식이라고 생각해도 된다.
cf2) 삽질한 이유1. redis는 한 번 key 값이 설정되면 다른 타입으로 바꿀 수 없다. list 타입이라고 결정되면 lpush, llen 등 해당 key에 대해서는 관련된 명령어만 쓸 수 있다.
cf3) 삽질한 이유2. hget(hset)과 get(set)의 인자의 개수가 다르다. hxxx가 hash table 인자를 하나 더 받는다.
cf4) 삽질한 이유3. object 타입은 json 형태 그대로 들어가는 것이 아니라 "object object" 이런 식으로 들어간다. 따라서 JSON.stringify로 변환한 후에 캐시에 저장해야 한다.
Node.js에서 가장 많이 사용하는 클라이언트는 ioredis이다.
공식문서 에 따르면 다음과 같은 특징을 가지고 있다.
설치: npm install ioredis && npm install -D @types/node(for typescript)
////// 간단한 사용법(공식 문서의 코드)
//// connection
new Redis(); // Connect to 127.0.0.1:6379
new Redis(6380); // 127.0.0.1:6380
new Redis(6379, "192.168.1.1"); // 192.168.1.1:6379
new Redis("/tmp/redis.sock");
new Redis({
port: 6379, // Redis port
host: "127.0.0.1", // Redis host
username: "default", // needs Redis >= 6
password: "my-top-secret",
db: 0, // Defaults to 0
});
// Connect to 127.0.0.1:6380, db 4, using password "authpassword":
new Redis("redis://:authpassword@127.0.0.1:6380/4");
// Username can also be passed via URI.
new Redis("redis://username:authpassword@127.0.0.1:6380/4");
//// data get, set
// Import ioredis.
// You can also use `import Redis from "ioredis"` if your project is an ESM module or a TypeScript project.
const Redis = require("ioredis");
// Create a Redis instance.
// By default, it will connect to localhost:6379.
// We are going to cover how to specify connection options soon.
const redis = new Redis();
redis.set("mykey", "value"); // Returns a promise which resolves to "OK" when the command succeeds.
// ioredis supports the node.js callback style
redis.get("mykey", (err, result) => {
if (err) {
console.error(err);
} else {
console.log(result); // Prints "value"
}
});
// Or ioredis returns a promise if the last argument isn't a function
redis.get("mykey").then((result) => {
console.log(result); // Prints "value"
});
redis.zadd("sortedSet", 1, "one", 2, "dos", 4, "quatro", 3, "three");
redis.zrange("sortedSet", 0, 2, "WITHSCORES").then((elements) => {
// ["one", "1", "dos", "2", "three", "3"] as if the command was `redis> ZRANGE sortedSet 0 2 WITHSCORES`
console.log(elements);
});
// All arguments are passed directly to the redis server,
// so technically ioredis supports all Redis commands.
// The format is: redis[SOME_REDIS_COMMAND_IN_LOWERCASE](ARGUMENTS_ARE_JOINED_INTO_COMMAND_STRING)
// so the following statement is equivalent to the CLI: `redis> SET mykey hello EX 10`
redis.set("mykey", "hello", "EX", 10);
아래는 redis 자료 구조에 따른 사용법이다.
// import redis from 'redis';
// client = redis.createClient();
// string: 가장 기본적인 형태의 key-value
// string를 이용하여 구현한다면 set {메인 Key}:{서브 Key} value의 형태로 데이터를 저장
client.set('key', 'value');
client.get('key');
// (sorted) set: 순서가 없는 문자열. 유일한 요소만 저장됨.
client.sadd('student', '이름1');
client.sadd('student', '이름2');
client.smembers('student', (err, data) => {
// 무언가
})
// list: 중복값 허용. 순서 저장. 메모리가 허용하는 한 많이 저장
client.lpush('listName', 'value');
client.lrange('listName', 'startIndex', 'lastIndex' /* -1 이면 모두 */, (err, items) => {
// 무언가
})
// hash set
// hash를 이용해 구현한다면 hset {메인 Key} {서브 Key} value의 형태로 저장. 다만 TTL 등을 사용할 수 없음
// import Redis from 'ioredis';
// const redis = new Redis({ port: 1234, host: 'github.com/mochang2', password: '1234' }) // options
await redis.hset('hash table', 'key', 'value); // 단일 key-value 쌍
await redis.hget('key', 'value');
await redis.hmset('hash table', 'key1', 'value1', 'key2', 'value2');
// 또는
await redis.hmset('hash table', {
'key1': 'value1',
'key2': 'value2',
}); // 여러 개의 key를 토대로 값을 set
await redis.hmget('hash table', 'key1', 'key2'); // 여러 개의 key를 토대로 값을 get
await redis.hget('hash table'); // key를 명시하지 않으면 모든 value를 가져옴
await redis.hexists('hash table', 'key1'); // 존재하는지 확인
await redis.hdel('hash table', 'key1', 'key2'...); // 삭제
await redis.hincrby('hash table', 'key1', value); // hash increment by. 얼마만큼 값을 증가시킬지 명시. 음수의 value가 들어갈 수 있음
await redis.hkeys('hash table'); // 모든 key를 가져옴
await redis.hlen('hash table'); // key의 개수를
// 내가 작성한 예시
// https://pjc0247.tistory.com/45 참고. string으로 저장 vs hash로 저장
const workFunc = async (argvKey, list) => {
const redisPipe = redisClient.pipeline(); // list이기 때문에 추가할 때마다 response를 받는 오버헤드를 줄이기 위해 pipeline 사용
for (const value of list) {
redisPipe.rpush(
`table:${argvKey}`,
JSON.stringify(makeJson(value)) // makeJson은 별도의 함수
);
}
redisPipe.expire(`table:${argvKey}`, 86400).exec(); // 캐싱기간: 1일
};
user // 로그인 여부에 따라
? await workFunc(user.id, array.slice(0, RECOMMEND_NUMBER)) // 일정 개수만 추천
: await workFunc("anonymous", array.slice(0, RECOMMEND_NUMBER));
https://wildeveloperetrain.tistory.com/21
https://aws.amazon.com/ko/elasticache/what-is-redis/
https://devlog-wjdrbs96.tistory.com/374
https://www.npmjs.com/package/ioredis
https://sabarada.tistory.com/135
https://freeblogger.tistory.com/10