2024.04.30 - [Spring/대용량 트래픽] - Redis 다양한 데이터 타입 알아보기
이전까지 Redis CLI를 통해서 다양한 데이터 타입을 알아보고 명령어들도 알아봤다.
이번 글은 Java에서는 어떻게 사용하는지 알아보겠다.
Jedis
Java + Redis로 줄여서 Jedis라고 부른다.
우선 Java로 실습하기 위해서 개발환경툴인 InteliJ를 사용해서 새로운 프로젝트를 만들고 시작해보겠다.
https://redis.io/docs/latest/develop/connect/clients/java/jedis/
Jedis에 대한 기본 가이드라인이다. 처음이라면 한 번 읽어보는 걸 추천한다.
Jedis를 사용하기 위해서 build.gradle에 의존성을 추가해주자!
implementation 'redis.clients:jedis:5.1.2'
main.java
JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);
jedisPool.close();
Ip와 Port로 Jedis풀을 만들어주고, 이런 객체들의 연결을 close 해줘야한다.
try(var jedisPool = new JedisPool("127.0.0.1", 6379)){
}
이렇듯 try 문을 사용하게 되면 close가 필요없고, 간결해진다!
String에 대해서 테스트를 해보겠다.
String
Redis의 방식과 상당히 유사하게 코딩할수 있다.
잘 작동하는 걸 확인할 수있다.
이제부터는 monitor 명령어를 활용해서 IteliJ에서 코딩을 함에 따른 변화를 CLI로 살펴보겠다.
그렇다면 인텔리제이 출력화면에서도 잘 나오는지 확인해봐야한다.
- MGET
- COUNTER
값도 잘 나온다.(참고로 원래값은 10이었다.)
decr, decrBy 또한 감소하는 것을 제외하고 마찬가지여서 생략하도록 한다.
위에서 set으로 여러 키값들에 대해서 Value를 지정해줄때처럼, 엄청나게 많은 키값들에 대해 설정하는 일이 일어나면 각각 따로 요청해야 하므로 비용이 많이 들게된다. 따라서 Jedis에서는 여러개의 명령 묶음을 하나의 명령으로 batch 처리할때 유용한 Redis pipelining을 제공한다.
Jedis에서 plpelining 활용
- syncAndReturnAll() : Pipleline객체로 요청한 명령을 하나로 묶어서 요청해준다.
- 반환형은 Object형
OK가 출력으로 나오는 것은, 제대로 설정이 되었는지 확인 여부를 객체로 반환하기 때문이다.
List
- Stack
- RPOP + RPUSH를 통해 구현
try(JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)){
try(Jedis jedis = jedisPool.getResource()){
//list
//1. stack
jedis.rpush("stack1", "aaa");
jedis.rpush("stack1", "bbb");
jedis.rpush("stack1", "ccc");
System.out.println(jedis.rpop("stack1"));
System.out.println(jedis.rpop("stack1"));
System.out.println(jedis.rpop("stack1"));
- Queue
- LPOP + RPUSH를 통해 구현
jedis.rpush("queue2", "aaa");
jedis.rpush("queue2", "bbb");
jedis.rpush("queue2", "ccc");
System.out.println(jedis.lpop("queue2"));
System.out.println(jedis.lpop("queue2"));
System.out.println(jedis.lpop("queue2"));
Block 명령어
POP을 할때 값이 없는 경우, 값이 생길때 까지 대기 → 메세지 큐로 활용가능
단, 인메모리 DB이고, 가용성이 높지 않기 때문에 RDB, AOF 같은 백업기능이 반드시 필요!
리스트에서만 사용가능하다, KEY뒤에 기다리는 시간까지 설정할 수 있다.
그렇다면 여러곳에서 Block명령을 사용하면 어떻게 될까? 먼저 실행한 명령부터 순서대로 실행된다.
- BLPOP
List<String> blpop = jedis.blpop(10, "queue:blocking");
if(blpop != null){
blpop.forEach(System.out::println);
}
이렇게 10초안에 KEY 리스트에 값을 집어 넣으면 삭제를 지연시킬 수 있다.
위 코드를 while문으로 감싼다면 메세지큐로 활용이 가능하다.
- BRPOP
※ LTRIM KEY [시작인덱스] [끝인덱스] : 시작~끝 인덱스까지만으로 대체한다.
이를 활용하면
- 최신값 유지 : LPUSH + LTRIM
- 오래된값 유지 : RPUSH + LTRIM
Set
- SADD, SREM, SMEMBERS
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);
- SISMEMBER
System.out.println(jedis.sismember("users:500:follow", "200"));
System.out.println(jedis.sismember("users:500:follow", "120"));
- SINTER
반환형은 SET !
Set<String> sinter = jedis.sinter("users:500:follow", "users:100:follow");
sinter.forEach(System.out::println);
Hash
이미 Redis자체가 Key - Value 로 구성되어 있어서 헷갈릴수도 있는데,
Key - Field(자료구조 해시에서의 Key로 생각) - Value 이렇게 정리해서 보면 좋을것 같다.
여러값을 저장할때는 Field - value Field - value Field - value . . . 이런식으로 해주면 된다.
그러면 많이 저장 할때는 저걸 다해야한다고? 너무 번거로운데,,? 레디스가 다 준비해놨다 ㅎㅎ Field - Value 형식은 데이터양이 적을때 하는 거고, 많을 때는 인자에 MAP자체를 입력으로 저장해도 된다!
- HGETALL : 반환형이 HASHMAP으로 되어있다.
jedis.hset("users:2:info", "name", "kim");
HashMap<String, String> userInfo = new HashMap<>();
userInfo.put("email", "kim@naver.com");
userInfo.put("phone", "010-xxxx-xxxx");
jedis.hset("users:2:info", userInfo);
jedis.hdel("users:2:info", "phone");
System.out.println(jedis.hget("users:2:info", "email"));
Map<String, String> user2Info = jedis.hgetAll("users:2:info");
user2Info.forEach((k, v) -> System.out.println(k + ' ' + v));
jedis.hincrBy("users:2:info", "visits",31);
Sorted Set
기존 Set에서 값에의해 순위를 가질수 있는 자료구조이다.
리더보드 같은 점수기반에 적합하다.
Hash와 마찬가지로 여러가지 값을 한번에 저장 하기 위해서 score member score member . . . 반복 보단 HashMap 자료구조를 ZADD에 집어 넣을 수있다.
HashMap<String, Double> scores = new HashMap<>();
scores.put("user1", 100.0);
scores.put("user2", 30.0);
scores.put("user3", 50.0);
scores.put("user4", 80.0);
scores.put("user5", 15.0);
jedis.zadd("game:2:scores", scores);
List<String> zrange = jedis.zrange("game:2:scores", 0, Long.MAX_VALUE);
zrange.forEach(System.out::println);
List<Tuple> tuples = jedis.zrangeWithScores("game:2:scores", 0, Long.MAX_VALUE);
tuples.forEach(System.out::println);
System.out.println(jedis.zcard("game:2:scores"));
jedis.zincrby("game:2:scores", 100.0, "user5");
Redis CLI 에서는 ZRANGE에서 0 +inf라고 했었는데, java에서는 +inf 예약어는 없으므로, Long.MAX_VALUE를 썼다.
Geospatial
- 2차원 지도상 위/경도 좌표
KEY 하나에 대해서 여러가지 위치 정보를 저장 할 수있고, 위치도 단위별로 표기할 수있다.
- GEOSEARCH
- 특정 멤버를 기준으로 찾기
- 특정 좌표를 기준으로 찾기
- GEOPOS : 저장한 좌표를 조회
List<GeoRadiusResponse> geosearch = jedis.geosearch(
"stores:1:geo",
new GeoCoordinate(127.031, 37.495),
100,
GeoUnit.M
);
좌표를 기준으로 찾기 위해 위 코드로 짰지만,
geosearch.forEach(geo -> {
System.out.println(geo.getMemberByString()
+ ' '
+ geo.getCoordinate().getLatitude()
+ ' '
+ geo.getCoordinate().getLongitude());
});
여기서 위/경도 정보를 넘겨 받지 못해서 예외가 발생한다.
따라서 간단하게 이름정보만 알고 싶다면 위의 코드처럼 해도 되지만, 위/경도 정보까지 알고싶을때는 GeoSearchParam() 함수로 정보를 넣어줘야한다.
** 참고로 del, unlink 명령은 어느 자료구조든 Redis에서 사용할 수 있음을 알고있자!
jedis.geoadd("stores:1:geo", 127.02985530619755, 37.49911212874, "some1");
jedis.geoadd("stores:1:geo", 127.0333352287619, 37.491921163986234, "some2");
Double geodist = jedis.geodist("stores:1:geo", "some1", "some2");
System.out.println(geodist);
/*List<GeoRadiusResponse> geosearch = jedis.geosearch(
"stores:1:geo",
new GeoCoordinate(127.031, 37.495),
100,
GeoUnit.M
);
*/
List<GeoRadiusResponse> geosearch = jedis.geosearch(
"stores:1:geo",
new GeoSearchParam()
.fromLonLat(new GeoCoordinate(127.031, 37.495))
.byRadius(500, GeoUnit.M)
.withCoord()
);
geosearch.forEach(geo -> {
System.out.println(geo.getMemberByString()
+ ' '
+ geo.getCoordinate().getLatitude()
+ ' '
+ geo.getCoordinate().getLongitude());
});
jedis.unlink("stores:1:geo");
BitMap
- 기존 Set에서 데이터 양이 많고, 단순 집계만 한다면 BitMap으로 자료구조를 변환해볼수 있다.
jedis.setbit("request-somepage-20240503", 100, true);
jedis.setbit("request-somepage-20240503", 200, true);
jedis.setbit("request-somepage-20240503", 300, true);
System.out.println(jedis.getbit("request-somepage-20240503", 100));
System.out.println(jedis.getbit("request-somepage-20240503", 50));
System.out.println(jedis.bitcount("request-somepage-20240503"));
//bitmap vs set 메모리 사용률
Pipeline pipelined = jedis.pipelined();
IntStream.rangeClosed(0, 100000).forEach(i ->{
pipelined.sadd("request-somepage-20240502", String.valueOf(i), "1");
pipelined.setbit("request-somepage-bit-20240502", i, true);
if(i == 1000){
pipelined.sync();
}
});
pipelined.sync();
여기서 데이터 처리 속도에 대해 명확히 느꼈다.
우선 10만 정도의 데이터에 대한 작업을 해주고, set 과 bitmap의 메모리 사용률을 비교해줬다.
그리고 pipeline처리가 얼마나 중요한지 알았는데, pipeline을 안쓰고 그냥 jedis로 실행해주면 이게 언제끝나나,, 싶을정도로 처리속도가 굉장히 느리다. 하지만 pipeline을 쓰면 이거 잘못 실행된게 아닌가 싶을정도로 빠르게 끝난다. 데이터양이 10만보다 훨씬 많아진다면, 당연히 더 큰 성능차이가 날것이다.
그리고 위 코드를 실행시킨뒤 redis-cli에서 성능 비교를 했다.
단순 set처리는 그냥 bitmap 쓰자,,,!
2024.05.07 - [Spring/대용량 트래픽] - Redis Transactions
'Spring > 대용량 트래픽' 카테고리의 다른 글
Redis Key, Scan 명령어 (0) | 2024.05.07 |
---|---|
Redis Transactions (0) | 2024.05.07 |
Redis 다양한 데이터 타입 알아보기 (0) | 2024.04.30 |
Redis CLI 실습 (0) | 2024.04.05 |
Redis,Docker 설치하기와 수많은 에러 (0) | 2024.04.05 |