[Back-end] Redis와 Data types

말랑이·2025년 2월 22일
0

SpringBoot

목록 보기
27/30

1 In-memory database

📌 In-memory database : 메모리에 데이터를 적재하여 활용하는 데이터베이스 시스템

  • 메모리를 데이터베이스로 사용하므로 디스크 대비 빠른 응답속도 보장 가능 (밀리초 단위)
  • 메모리를 사용하기에 데이터가 휘발성임
  • 다양한 데이터 타입 제공
종류특징
Memcached- 단일 데이터 타입 제공
- 대규모 캐시데이터 관리에 용이
Redis- 다양한 자료구조 지원
- 캐시, 세션, 지도 등 다양한 종류의 데이터 관리 가능

2 Redis

📌 Remote Dictionary Server : in-memory, key-value 데이터를 지원하는 오픈소스

1️⃣ Persisten on Disk

  • 메모리에 있는 데이터를 디스크에 저장하여 사용 가능 (백업용)
    • RDB(Snapshot) : 특정 시점에 데이터베이스 전체를 디스크에 저장
    • AOF(Append Only File) : 데이터 쓰기 명령어를 저장 (=히스토리 쌓임) → RDB보다 복구작업에 용이

2️⃣ Single Thread

  • 클라이언트로부터 전달받은 명령어를 Single Thread로 처리함
  • 외부로부터 여러개의 명령을 한번에 받아도 순차적으로 처리
    → 데이터 일관성 유지 가능, 보통 초당 10만건까지 커버
  • Single Thread 처리로 인한 지연문제 발생으로 Redis 6.0부터는 multiple thread for i/o 도입

3 Redis 구현사례

1️⃣ Cache

  • 자주 사용하거나 잘 변하지 않는 데이터에 대해 Redis에 적재 후 활용
  • 반복되는 DB 쿼리 질의를 줄임 → 데이터베이스 리소스 부담이 줄어듬 + 빠른 응답속도 보장 가능

2️⃣ Session Store

  • 웹어플리케이션에서 세션 저장이 필요할 경우 Redis를 분산 저장소로 사용
  • 설정 시간 후, 오래된 세션 데이터는 자동 삭제됨

3️⃣ Pub/Sub

  • publish(메시지 발행), subscribe(메시지 구독)
  • 비동기처리 목적으로 사용
  • 메시지 중계를 위한 용도

4️⃣ Message Queue

  • Redis에 메시지를 저장 후 메시지를 필요로 하는 서버에 요청 순서대로 전달

5️⃣ leader board

  • 순위를 매김

4 Redis 실행

# 도커 레디스 이미지 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 옵션 : 백그라운드에서 실행. 즉 터미널 종료시에도 계속 실행됨
  • local 6379에서 도커 6379로 연결

5 Redis CLI

# cli 실행
docker exec -it <컨테이너 Id> redis-cli

# 실행된 명령 실시간 출력
docker exec -it <컨테이너 Id> redis-cli monitor

# 로컬 개발환경에서 데이터타입에 따른 성능테스트 (보통 10만)
docker exec -it <컨테이너 Id> /bin/bash
redis-benchmark


6 DataTypes

1️⃣ String

  • 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

Java 코드 실습

공식 Jedis https://redis.io/docs/latest/develop/clients/jedis/

  • 개발환경
    • 언어 : Java
    • JDK : Java 17
    • build : Gradle
    • 의존성 : 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에서 SET한 값 확인
redis-cli -h <Redis 호스트> -p <Redis 포트>

2️⃣ List, Set

  • List 데이터타입은 순서가 있는 데이터의 컬렉션을 저장하는데 사용함
  • 중복값을 허용하며 양쪽 끝에서 데이터를 추가/제거할 수 있는 유연한 구조를 제공
  • Linked List (strings) → Java ArrayList
  • Stack, Queue(메시지대기열) 등의 용도로 활용
  • Set 데이터타입은 중복을 허용하지 않는 값들의 집합 (collection) → 같은 값이 여러번 저장되지 않음
  • 중복된 값이 자동으로 제거되며, 값들의 순서가 없음
  • 요소 추가/삭제가 빠름 (O1)
  • 집합연산(교집합/합집합/차집합) 제공

List 명령어

# 해당 키 값의 리스트 자료형에 왼쪽 부분에 값을 추가
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

Blocking List 연산 명령어

  • Redis의 블로킹 리스트 연산 명령어
  • 리스트에서 요소를 꺼낼 때, 데이터가 없으면 대기(Blocking)하는 기능을 제공
  • LPOP RPOP과 비슷하지만, 꺼낼 데이터가 없을 경우 즉시반환하는 대신 일정시간동안 대기함
# Last In, Frist Out
BLPOP key1 [key2...] timeout

# First In, First Out
BRPOP key1 [key2...] timeout

Java 코드 실습

  • 개발환경
    • 언어 : Java
    • JDK : Java 17
    • build : Gradle
    • 의존성 : 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);
                }

                
            }
        }
    }
}

SET 명령어

# 요소추가 (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"]

Java 코드 실습

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);
                
            }
        }
    }
}

3️⃣ Hash

  • 필드-값 (Field - Value) 쌍을 저장하는 자료구조
  • Key-Value 형태로 여러개의 필드를 저장할 수 있음
  • 관계형데이터베이스의 Row 처럼 사용
  • 한개의 Key 아래, 여러개의 Field-Value 저장가능

Hash 명령어

# 값 저장 (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"

Java 실습 코드

  • 개발환경
    • 언어 : Java
    • JDK : Java 17
    • build : Gradle
    • 의존성 : 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);

            }
        }

    }
}

4️⃣ Sorted Set

  • (ZSET)은 중복이 없는 유니크한 값(member)을 저장하면서, 각 값에 점수(score)를 부여하여 자동 정렬하는 자료구조
  • 값이 중복되지 않음 (Set과 동일)
  • 점수를 기준으로 자동 정렬 됨 (Set은 정렬 불가)
  • 점수를 기준으로 순위 조회 가능

Sorted 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 증가

Java 실습 코드

  • 개발환경
    • 언어 : Java
    • JDK : Java 17
    • build : Gradle
    • 의존성 : 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())));

            }
        }

    }
}

5️⃣ Geospatial

  • 위도(latitude)와 경도(longitude) 좌표를 기반으로 위치데이터를 저장하고, 거리 계산 및 근접 검색을 수행하는 기능
  • 위치저장 + 거리계산 + 반경 내 위치검색 + 좌표변환

Geospatial 기본 문법

# 위치데이터 저장 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"

Java 실습 코드

  • 개발환경
    • 언어 : Java
    • JDK : Java 17
    • build : Gradle
    • 의존성 : 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");

            }
        }

    }
}

6️⃣ Bitmap

  • 0 또는 1의 값으로 이루어진 비트열
  • 메모리를 적게 사용하여 대량의 데이터 저장에 유용함

Bitmap 기본문법

# 지정된 위치에 값 설정 SETBIT
> SETBIT key offset(index) value

# 지정된 위치에 비트 값 가져오기 GETBIT
> GETBIT key offset(index)

# 1의 개수 세기(참여횟수 등) BITCOUNT
> BITCOUNT key

Java 실습 코드

  • 개발환경
    • 언어 : Java
    • JDK : Java 17
    • build : Gradle
    • 의존성 : 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();

            }
        }

    }
}
profile
🐰 I'm Sunyeon-Jeong, mallang

0개의 댓글