📌 In-memory database
: 메모리에 데이터를 적재하여 활용하는 데이터베이스 시스템
종류 | 특징 |
---|---|
Memcached | - 단일 데이터 타입 제공 - 대규모 캐시데이터 관리에 용이 |
Redis | - 다양한 자료구조 지원 - 캐시, 세션, 지도 등 다양한 종류의 데이터 관리 가능 |
📌 Remote Dictionary Server
: in-memory, key-value 데이터를 지원하는 오픈소스
RDB(Snapshot)
: 특정 시점에 데이터베이스 전체를 디스크에 저장AOF(Append Only File)
: 데이터 쓰기 명령어를 저장 (=히스토리 쌓임) → RDB보다 복구작업에 용이multiple thread for i/o
도입# 도커 레디스 이미지 pull
docker pull redis:6.2
# 레디스 실행
docker run --rm -it -d redis:6.2
# 도커 실행 상태 확인
docker ps
# port mapping
docker run --rm -it -d -p 6379:6379 redis:6.2
# Redis 종료
docker kill <container id>
-d
옵션 : 백그라운드에서 실행. 즉 터미널 종료시에도 계속 실행됨# cli 실행
docker exec -it <컨테이너 Id> redis-cli
# 실행된 명령 실시간 출력
docker exec -it <컨테이너 Id> redis-cli monitor
# 로컬 개발환경에서 데이터타입에 따른 성능테스트 (보통 10만)
docker exec -it <컨테이너 Id> /bin/bash
redis-benchmark
- Key:Value의 가장 기본적인 타입으로 Text, Byte를 저장함
- 증가(increment), 감소(decrement)에 대한 원자적 연산 지원
- 시작되면 중간에 중단되지 않고 완전히 수행되는 연산
SET : 값 저장
SETNX : 값이 없는 경우에만 저장
GET : 조회
MGET : 여러개의 key 조회
INC : 원자적 연산 (증가)
DEC : 원자적 연산 (감소)
# SET
docker exec -it <컨테이너 Id> redis-cli
SET "users:100:name" "mallang"
SET "users:100:email" "mallang@mallang.com"
# GET
get users:100:name
"mallang"
# MGET
MGET users:100:name users:100:email
1) "mallang"
2) "mallang@mallang.com"
# SETNX
SETNX users:100:name chunsik
(integer) 0
SETNX users:101:name chunsik
(integer) 1
# INCREMENT / DECREMENT
INCR counter
(integer) 1
INCR counter
(integer) 2
DECR counter
(integer) 1
INCRBY counter 10
(integer) 11
DECRBY counter 5
(integer) 6
공식 Jedis https://redis.io/docs/latest/develop/clients/jedis/
implementation 'redis.clients:jedis:4.3.1'
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
/* Redis와 통신하기 위해 Jedis 연결을 관리하는 pool */
// 풀링 기법 -> 연결을 미리 생성하고 재사용하여 성능 최적화
// try 사용 -> 풀 close 자동화
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (Jedis jedis = jedisPool.getResource()) {
System.out.println("Redis 연결 상태: " + jedis.ping());
jedis.set("users:300:name", "mallang");
jedis.set("users:300:email", "mallang@mallang.com");
jedis.set("users:300:age", "23");
var userEmail = jedis.get("users:300:email");
System.out.println(userEmail);
List<String> userInfo = jedis.mget("users:300:name", "users:300:email", "users:300:age");
userInfo.forEach(System.out::println);
long counter = jedis.incr("counter");
System.out.println(counter);
counter = jedis.incrBy("counter", 10L);
System.out.println(counter);
counter = jedis.decr("counter");
System.out.println(counter);
counter = jedis.decrBy("counter", 20L);
System.out.println(counter);
// Jedis Pipeline 사용 -> 대량의 데이터 set 성능 최적화
Pipeline pipelined = jedis.pipelined();
pipelined.set("users:400:name", "chunsik");
pipelined.set("users:400:email", "chunsik@chunsik.com");
pipelined.set("users:400:age", "24");
List<Object> objects = pipelined.syncAndReturnAll();
objects.forEach(i -> System.out.println(i.toString()));
}
}
}
}
redis-cli -h <Redis 호스트> -p <Redis 포트>
- List 데이터타입은 순서가 있는 데이터의 컬렉션을 저장하는데 사용함
- 중복값을 허용하며 양쪽 끝에서 데이터를 추가/제거할 수 있는 유연한 구조를 제공
- Linked List (strings) → Java ArrayList
- Stack, Queue(메시지대기열) 등의 용도로 활용
- Set 데이터타입은 중복을 허용하지 않는 값들의 집합 (collection) → 같은 값이 여러번 저장되지 않음
- 중복된 값이 자동으로 제거되며, 값들의 순서가 없음
- 요소 추가/삭제가 빠름 (O1)
- 집합연산(교집합/합집합/차집합) 제공
# 해당 키 값의 리스트 자료형에 왼쪽 부분에 값을 추가
LPUSH
> LPUSH mylist "apple" "banana"
# result : banana apple
# 해당 키 값의 리스트 자료형에 오른쪽 부분에 값을 추가
RPUSH
> RPUSH mylist "cherry" "tomato"
# result : banana apple cherry tomato
# 해당 키 값의 리스트 자료형에서 왼쪽 끝의 요소를 제거하고 반환 (= 오래된 값)
LPOP
> LPOP mylist
# result : banana
# 해당 키 값의 리스트 자료형에서 오른쪽 끝의 요소를 제거하고 반환 (= 최신 값)
RPOP
> RPOP mylist
# result : tomato
# 해당 키 값의 리스트 자료형에서 리스트 요소 개수를 반환
LLEN
> LLEN mylist
# 2
# 해당 키 값의 리스트 자료형에서 지정된 범위 이외의 요소를 삭제
LTRIM
> LPUSH mylist A B C D E → E D C B A
> LTRIM mylist 1 3
# D C B
# 해당 키 값의 리스트 자료형에서 인덱스 범위를 반환
LRANGE
> LRANGE mylist 0 -1
# apple cherry
LPOP
RPOP
과 비슷하지만, 꺼낼 데이터가 없을 경우 즉시반환하는 대신 일정시간동안 대기함# Last In, Frist Out
BLPOP key1 [key2...] timeout
# First In, First Out
BRPOP key1 [key2...] timeout
implementation 'redis.clients:jedis:4.3.1'
public class Main {
public static void main(String[] args) {
System.out.println("Hello world");
/* Redis와 통신하기 위해 Jedis 연결을 관리하는 Pool */
// 풀링 기법 -> 연결을 미리 생성하고 재사용하여 성능 최적화
// try 사용 -> 풀 close 자동화
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (Jedis jedis = jedisPool.getResource()) {
/* LIST */
// 1. stack : Last In First Out
jedis.rpush("stack1", "aaaa");
jedis.rpush("stack1", "bbbb");
jedis.rpush("stack1", "cccc");
// List<String> stack1 = jedis.lrange("stack1", 0, -1);
// stack1.forEach(System.out::println);
System.out.println(jedis.rpop("stack1"));
System.out.println(jedis.rpop("stack1"));
System.out.println(jedis.rpop("stack1"));
// 2. queue : 순차적처리, First In First Out
jedis.rpush("queue1", "zzzz");
jedis.rpush("queue1", "aaaa");
jedis.rpush("queue1", "cccc");
System.out.println(jedis.lpop("queue1"));
System.out.println(jedis.lpop("queue1"));
System.out.println(jedis.lpop("queue1"));
// 3. block : BLPOP, BRPOP
List<String> blpop = jedis.blpop(10, "queue:blocking");
if (blpop != null) {
blpop.forEach(System.out::println);
}
}
}
}
}
# 요소추가 (SADD)
> SADD myset "apple" "banana" "cherry"
> SADD myset "banana" -> 중복된 값 저장 X
# 요소조회 (SMEMBERS)
> SMEMBERS myset -> ["apple", "banana", "cherry"]
# 요소개수확인 (SCARD)
> SCARD myset -> 3
# 요소삭제 (SREM)
> SREM myset "banana"
> SMEMBERS myset -> ["apple", "cherry"]
# 특정 값 포함 여부 확인 (SISMEMBER)
> SISMEMBER myset "apple" -> 1(존재함)
> SISMEMBER myset "grape" -> 0(존재하지않음)
# 집합연산_교집합 (SINTER)
> SADD set1 "a" "b" "c"
> SADD set2 "b" "c" "d"
> SINTER set1 set2 -> ["b", "c"]
public class Main {
public static void main(String[] args) {
System.out.println("Hello world");
/* Redis와 통신하기 위해 Jedis 연결을 관리하는 Pool */
// 풀링 기법 -> 연결을 미리 생성하고 재사용하여 성능 최적화
// try 사용 -> 풀 close 자동화
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (Jedis jedis = jedisPool.getResource()) {
/* SET */
jedis.sadd("users:500:follow", "100", "200", "300");
jedis.srem("users:500:follow", "100");
Set<String> smembers = jedis.smembers("users:500:follow");
smembers.forEach(System.out::println);
System.out.println(jedis.sismember("users:500:follow", "200"));
System.out.println(jedis.sismember("users:500:follow", "120"));
System.out.println(jedis.scard("users:500:follow"));
Set<String> sinter = jedis.sinter("users:500:follow", "users:100:follow");
sinter.forEach(System.out::println);
}
}
}
}
- 필드-값 (Field - Value) 쌍을 저장하는 자료구조
- Key-Value 형태로 여러개의 필드를 저장할 수 있음
- 관계형데이터베이스의 Row 처럼 사용
- 한개의 Key 아래, 여러개의 Field-Value 저장가능
# 값 저장 (HSET)
> HSET user:1001 name "mallang" age "24" city "Busan"
-> (Key : user:1001), (Field : name, age, city) (Value : mallang, 24, Busan)
# 특정 필드 값 조회 (HGET)
> HGET user:1001 name
-> "mallang"
# 모든 필드 값 조회 (HGETALL)
> HGETALL user:1001
-> name "mallang" age "24" city "Busan"
# 여러 필드 조회 (HMGET)
> HMGET user:1001 name age
-> "mallang", "24"
# 필드 삭제 (HDEL)
> HDEL user:1001 city
> HGETALL user:1001
-> name "mallang" age "24"
implementation 'redis.clients:jedis:4.3.1'
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
/* Redis와 통신하기 위해 Jedis 연결을 관리하는 Pool */
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (Jedis jedis = jedisPool.getResource()) {
/*
데이터를 단건으로 저장 할 때
hset(String key, String field, String value)
데이터를 다건으로 저장 할 때 -> Map 형식 사용
hset(String key, Map<String, String> hash)
*/
// hset 값 저장
jedis.hset("users:1:info", "name", "mallang");
var userInfo = new HashMap<String, String>();
userInfo.put("email", "mallang@mallang.com");
userInfo.put("phone", "010-0000-0000");
jedis.hset("users:2:info", userInfo);
// hdel 값 삭제
jedis.hdel("users:2:info", "phone");
// hget, hgetall
System.out.println(jedis.hget("users:2:info", "email"));
Map<String, String> user2Info = jedis.hgetAll("users:2:info");
user2Info.forEach((k, v) -> System.out.printf("%s %s%n", k, v));
// hincr 숫자 값 증가
jedis.hincrBy("users:2:info", "visits", 30);
}
}
}
}
- (ZSET)은 중복이 없는 유니크한 값(member)을 저장하면서, 각 값에 점수(score)를 부여하여 자동 정렬하는 자료구조
- 값이 중복되지 않음 (Set과 동일)
- 점수를 기준으로 자동 정렬 됨 (Set은 정렬 불가)
- 점수를 기준으로 순위 조회 가능
# 값 추가 ZADD
> ZADD leaderboard 100 " mallang" 200 "chunsik" 300 "ruddy"
-> 점수 부여
# 값 삭제 ZREM
> ZREM leaderboard "ruddy"
# 값 조회 ZRANGE
> ZRANGE leaderboard 0 -1 WITHSCORES
-> 낮은 점수부터 정렬된 결과조회
> ZREVRANGE leaderboard 0 -1 WITHSCORES
-> 높은 점수부터 정렬된 결과조회
# 전체 개수 확인 ZCARD
> ZCARD leaderboard
-> 저장된 값(멤버)의 개수 반환
# 순위조회 ZRANK
> ZRANK leaderboard "mallang"
-> 0 (0부터 시작, 낮은점수가 먼저)
# 점수증가 ZINCRBY
> ZINCRBY leaderboard 200 "mallang"
-> mallang의 점수를 10 증가
implementation 'redis.clients:jedis:4.3.1'
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (var jedis = jedisPool.getResource()) {
// sorted set
var scores = new HashMap<String, Double>();
scores.put("users1", 100.0);
scores.put("users2", 30.0);
scores.put("users3", 50.0);
scores.put("users4", 80.0);
scores.put("users5", 15.0);
// zadd 값추가
jedis.zadd("game:scores", scores);
// zrange 값조회 (asc)
List<String> zrange = jedis.zrange("game:scores", 0, Long.MAX_VALUE);
zrange.forEach(System.out::println);
// zcard 전체 개수 확인
System.out.println(jedis.zcard("game:scores"));
// zrange with scores 점수와 함께 출력
List<Tuple> tuples = jedis.zrangeWithScores("game:scores", 0, Long.MAX_VALUE);
tuples.forEach(i -> System.out.println("%s %f".formatted(i.getElement(), i.getScore())));
}
}
}
}
- 위도(latitude)와 경도(longitude) 좌표를 기반으로 위치데이터를 저장하고, 거리 계산 및 근접 검색을 수행하는 기능
- 위치저장 + 거리계산 + 반경 내 위치검색 + 좌표변환
# 위치데이터 저장 GEOADD
> GEOADD places 129.0756 35.1796
-> 부산좌표
# 위치데이터 검색 GEOSEARCH
> GEOSEARCH key FROMMEMBER(특정 멤버(위치)를 기준) member
> GEOSEARCH key FROMLONLAT(지정한좌표) longitude latitude BYRADIUS(반경) radius unit
> GEOSEARCH key BYBOX(가로/세로 크기로 사각형범위 지정) width height unit ASC(or DESC)
> GEOSEARCH key WITHDIST(검색된 위치와 기준점 간 거리포함) WITHCOORD(검색된 위치의 위/경도 포함) WITHHASH(GeoHash값 포함) COUNT cnt
# 거리계산 GEODIST
GEODIST key "Seoul" "busan" km
-> m, km, mi, ft 단위 지원
# 좌표조회 GEOPOS
GEOPOS key "Seoul"
implementation 'redis.clients:jedis:4.3.1'
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (var jedis = jedisPool.getResource()) {
// geo add
jedis.geoadd("store:geo", 127.02985530619755, 37.49911212874, "some1");
jedis.geoadd("store:geo", 127.0333352287619, 37.491921163986234, "some2");
// geo dist
Double geodist = jedis.geodist("store:geo", "some1", "some2");
System.out.println(geodist);
// geo search
List<GeoRadiusResponse> radiusResponseList = jedis.geosearch (
"store:geo"
, new GeoCoordinate(127.033, 37.495) // 기준좌표
, 500 // 반경 500m
, GeoUnit.M // 단위 m
);
// geo search (GeoSearchParam 활용)
List<GeoRadiusResponse> radiusResponseList1 = jedis.geosearch (
"store:geo"
, new GeoSearchParam ()
.fromLonLat(new GeoCoordinate(127.033, 37.495)) // 기준좌표
.byRadius(500, GeoUnit.M) // 반경 500 단위 m
.withCoord() // 검색 결과에 좌표정보 포함
);
radiusResponseList1.forEach (response ->
System.out.println("%s %f %f".formatted(
response.getMemberByString()
, response.getCoordinate().getLatitude()
, response.getCoordinate().getLongitude()
))
);
// geo 삭제
jedis.unlink("stores:geo");
}
}
}
}
- 0 또는 1의 값으로 이루어진 비트열
- 메모리를 적게 사용하여 대량의 데이터 저장에 유용함
# 지정된 위치에 값 설정 SETBIT
> SETBIT key offset(index) value
# 지정된 위치에 비트 값 가져오기 GETBIT
> GETBIT key offset(index)
# 1의 개수 세기(참여횟수 등) BITCOUNT
> BITCOUNT key
implementation 'redis.clients:jedis:4.3.1'
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
try (var jedisPool = new JedisPool("127.0.0.1", 6379)) {
try (var jedis = jedisPool.getResource()) {
jedis.setbit("request-somepage-20250301", 100, true);
jedis.setbit("request-somepage-20250301", 200, true);
jedis.setbit("request-somepage-20250301", 300, true);
System.out.println(jedis.getbit("request-somepage-20250301", 100));
System.out.println(jedis.getbit("request-somepage-20250301", 50));
System.out.println(jedis.bitcount("request-somepage-20250301"));
// bitmap vs set (메모리사용률 비교)
Pipeline pipelined = jedis.pipelined(); // 여러개의 redis 명령어를 한번에 전송
IntStream.rangeClosed(0, 100000).forEach(i -> {
pipelined.sadd("request-somepage-set-20250302", String.valueOf(i), "1");
pipelined.setbit("request-somepage-bit-20250302", 1, true);
if (1 == 1000) {
pipelined.sync();
}
});
pipelined.sync();
}
}
}
}