Redis Data types
아래와 같은 타입들이 있다.
- Bitmap
- Geospatial
- Hash
- List
- Set
- Sorted Set
- String
Java + Redis
자바에서 Redis를 순수하게 사용해보면서 각 데이터 타입별로 사용 방법을 익혀보자.
우선, 자바에서 Redis를 사용하려면, Jedis 라는 라이브러리를 내려받으면 된다.
implementation 'redis.clients:jedis:5.2.0'
String
package cwchoiit.redis.datatypes.string;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;
public class SetGetMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
jedis.set("users:300:email", "kim@noreply.com");
jedis.set("users:300:name", "kim");
jedis.set("users:300:age", "30");
String userEmail = jedis.get("users:300:email");
System.out.println(userEmail);
// MGET : 여러개 데이터를 한번에 조회
List<String> userInfo = jedis.mget(
"users:300:email",
"users:300:name",
"users:300:age"
);
userInfo.forEach(System.out::println);
}
}
}
- 단순 문자열을 다룰때 Redis에서는 SET, GET 명령어를 사용하면 된다.
- SET Key Value 형식으로 데이터를 넣을 수 있고, GET Key 명령어로 데이터를 조회할 수 있다.
- MGET은 Multiple GET의 약자로 여러 데이터를 한번에 조회할 때 사용하는 명령어이다.
- 당연히 MSET도 있다.
실행 결과
kim@noreply.com
kim@noreply.com
kim
30
INCR, INCRBY, DECR, DECRBY
이건, 특정 Key에 대해 값을 증가하는 명령어이다. 다음 코드를 보자.
package cwchoiit.redis.datatypes.string;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class IncrDecrMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
long counter1 = jedis.incr("counter"); // counter 에 1 증가
System.out.println("counter1 = " + counter1);
long counter2 = jedis.incrBy("counter", 10L); // counter 에 10 증가
System.out.println("counter2 = " + counter2);
long counter3 = jedis.decr("counter"); // counter 에 1 감소
System.out.println("counter3 = " + counter3);
long counter4 = jedis.decrBy("counter", 10L); // counter 에 10 감소
System.out.println("counter4 = " + counter4);
}
}
}
- 뒤에 BY가 붙으면 한번에 증가 또는 감소시킬 값을 지정할 수 있다.
실행 결과
counter1 = 1
counter2 = 11
counter3 = 10
counter4 = 0
Pipeline
파이프라인은 말 그대로, 이어 실행할 수 있는, 즉 세 개의 요청이 있다면 세 개의 요청을 단건으로 실행하는 게 아니라 모아서 실행할 수 있는 방법이다.
package cwchoiit.redis.datatypes.string;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import java.util.List;
public class PipelinedMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
Pipeline pipelined = jedis.pipelined(); // 요청을 한번에 처리할 수 있는 파이프라인
pipelined.set("users:400:email", "greg@np.com");
pipelined.set("users:400:name", "greg");
pipelined.set("users:400:age", "40");
List<Object> objects = pipelined.syncAndReturnAll();
objects.forEach(System.out::println);
}
}
}
- 먼저 Pipeline 객체를 받은 후에 이 객체를 통해 어떤 작업을 수행한다. 이건 문자열 타입뿐 아니라 어떤 타입이든 가능하다.
- 그리고 Sync 명령어를 날리면, 이 파이프라인에서 지정한 명령들을 한번에 보낸다.
실행 결과
OK
OK
OK
Set
Set은 자바에서 Set 자료구조와 같은 느낌으로 생각하면 된다.
package cwchoiit.redis.datatypes.set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Set;
public class SetMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
long insertCount = jedis.sadd("users:500:follow", "100", "200", "300");
System.out.println("insertCount = " + insertCount);
// Set 자료구조이기 때문에 아무리 동일한 값을 여러번 넣어도 더 들어가지 않는다.
jedis.sadd("users:500:follow", "100", "200", "300");
jedis.sadd("users:500:follow", "100", "200", "300");
jedis.sadd("users:500:follow", "100", "200", "300");
Set<String> members = jedis.smembers("users:500:follow");
members.forEach(System.out::println);
long totalCount = jedis.scard("users:500:follow");
System.out.println("totalCount = " + totalCount);
boolean isContains = jedis.sismember("users:500:follow", "100");
System.out.println("isContains = " + isContains);
boolean isContains2 = jedis.sismember("users:500:follow", "600");
System.out.println("isContains2 = " + isContains2);
// --- SINTER 확인해보기 --- //
jedis.sadd("users:600:follow", "100", "400", "500");
// SINTER = 두 Set 자료구조가 공통으로 가지고 있는 값을 가져옴
Set<String> sInter = jedis.sinter("users:600:follow", "users:500:follow");
sInter.forEach(System.out::println);
jedis.srem("users:600:follow", "100");
}
}
}
- SADD 명령어는 Set 자료 구조에 값을 넣는 명령어이다. SADD Key ...Values 형식으로 사용하면 된다.
- Set 자료 구조이기 때문에 동일한 값을 넣는다고 중복으로 들어가지 않는다.
- Set 자료 구조이기 때문에 순서를 보장하지도 않는다.
- SMEMBERS 명령어는 특정 키에 포함된 값들을 전부 가져오는 명령어이다.
- SCARD 명령어는 Set Cardinality 의 약자로, 해당 키에 속한 값들의 개수를 가져온다.
- SISMEMBER는 특정 키에 지정한 값이 포함됐는지 판단하는 명령어이다.
- SINTER는 두 Set 자료구조에 공통으로 가지고 있는 값들을 반환한다.
- SREM은 지정한 키에 지정한 값을 제거한다.
실행 결과
insertCount = 3
100
200
300
totalCount = 3
isContains = true
isContains2 = false
100
List
List 자료구조는 굉장히 유연함을 가지고 있는데, Stack, Queue, BlockQueue, BlockStack 어떤 형태로든 사용이 가능하다.
package cwchoiit.redis.datatypes.list;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;
public class StackMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
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();
System.out.println(jedis.rpop("stack1"));
System.out.println(jedis.rpop("stack1"));
System.out.println(jedis.rpop("stack1"));
}
}
}
- Stack은 가장 마지막에 넣은 데이터가 가장 먼저 나오는 구조이다. 그래서 Redis 에서는 Stack 형태의 자료구조를 List로 구현할 수 있는데 이때 RPUSH, RPOP을 사용하면 된다.
- RPUSH 명령어는 리스트의 오른쪽에 값을 넣는다는 의미이다. [1, 2] 이런 리스트가 있을 때 RPUSH 리스트 3을 하게 되면 [1, 2, 3]이 된다.
- RPOP은 리스트의 오른쪽에서 꺼낸다는 의미가 된다. [1, 2, 3]에서 RPOP을 하면 3이 나오고 리스트는 [1, 2]가 된다.
- LRANGE는 특정 리스트의 범위를 지정해서 값을 가져오는 것이다. LRANGE Key Start Stop 형태로 사용할 수 있고, 위에서 사용한것처럼 0, -1을 범위로 지정하면 처음부터 끝까지를 의미한다.
실행 결과
aaaa
bbbb
cccc
cccc
bbbb
aaaa
package cwchoiit.redis.datatypes.list;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;
public class QueueMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
jedis.rpush("queue1", "aaaa");
jedis.rpush("queue1", "bbbb");
jedis.rpush("queue1", "cccc");
List<String> queue1 = jedis.lrange("queue1", 0, -1);
queue1.forEach(System.out::println);
System.out.println();
System.out.println(jedis.lpop("queue1"));
System.out.println(jedis.lpop("queue1"));
System.out.println(jedis.lpop("queue1"));
}
}
}
- Queue는 가장 먼저 넣은 데이터가 가장 빨리 나오는 구조이다. 이 또한 Redis는 List 자료구조로 구현이 가능하다.
- RPUSH로 순차적으로 값을 넣고, LPOP으로 값을 꺼내면 된다.
실행 결과
aaaa
bbbb
cccc
aaaa
bbbb
cccc
package cwchoiit.redis.datatypes.list;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;
public class BlockQueueOrStackMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
// BlockLeftPop 은 지정한 키의 데이터가 있는 경우, 바로 왼쪽(가장 오래된 것 = Queue)에서 데이터를 꺼내오고,
// 데이터가 없는 경우 주어진 timeout 만큼 대기한다. 대기하는 시간이 지나도 없는 경우 nil 을 반환한다.
List<String> blockedLeft = jedis.blpop(3, "queue:blocking");
if (blockedLeft != null) {
blockedLeft.forEach(System.out::println);
}
// BlockRightPop 도 같은 맥락으로 오른쪽(가장 최신것 = Stack)에서 꺼낸다고 보면 된다.
List<String> blockedRight = jedis.brpop(3, "queue:blocking");
if (blockedRight != null) {
blockedRight.forEach(System.out::println);
}
}
}
}
- BLPOP, BRPOP은 왼쪽에서 값을 꺼내거나 오른쪽에서 값을 꺼내는데, 값이 있다면 바로 가져오고 값이 없다면 값을 정해진 시간만큼 기다린 후 가져오거나 실패한다.
- 말 그대로 Blocking 이다.
Hash
Hash는 자바에서 HashMap을 생각하면 된다.
package cwchoiit.redis.datatypes.hash;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Map;
// Redis Hash 는 자바의 HashMap 으로 생각하면 된다.
public class HashMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
// 단일값 추가
jedis.hset("users:2:info", "visits", "0");
// Map 전체 추가
jedis.hset("users:2:info", Map.of(
"name", "moon",
"phone", "010-1234-5678",
"email", "moon@cwchoiit.com")
);
// 전체값 가져오기
Map<String, String> user2Info = jedis.hgetAll("users:2:info");
System.out.println(user2Info);
// 특정 필드 삭제
jedis.hdel("users:2:info", "visits");
Map<String, String> user2Info2 = jedis.hgetAll("users:2:info");
System.out.println(user2Info2);
// 단일값 가져오기
String email = jedis.hget("users:2:info", "email");
System.out.println("email = " + email);
jedis.hset("users:2:info", "visits", "0");
// 단일값 카운트 증가
jedis.hincrBy("users:2:info", "visits", 1);
String visits = jedis.hget("users:2:info", "visits");
System.out.println("visits = " + visits);
}
}
}
- HSET은 Hash 자료 구조에 값을 추가하는 것이다. HashMap을 생각하면 된다고 했으니 값을 넣을때 Key:Value를 넣으면 된다.
- HGETALL은 특정 키로 만들어진 Hash 자료구조의 모든 값을 가져오는 명령어이다.
- HDEL은 특정 키로 만들어진 Hash 자료구조의 특정 Field를 삭제하는 명령어이다.
Sorted Sets
Sorted Set은 가장 쉽게 생각하려면, 인기글이나 게임에서 랭킹 순위같은 것을 생각해보면 된다.
package cwchoiit.redis.datatypes.sortedset;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.resps.Tuple;
import java.util.List;
import java.util.Map;
/**
* Sorted Set - 인기글 순위, 게임 랭킹 순위 등 점수와 해당 점수에 대한 유저와 같이 score 저장이 가능한 자료구조
* Key Score Member
* ------------|-----------|-----------|
* | 10 | user1 |
* | 20 | user2 |
* game1:scores| 70 | user3 |
* | 100 | user4 |
* | 1 | user5 |
*/
public class SortedSetMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
// 단일값 저장
jedis.zadd("game2:scores", 10.0, "user1");
// 여러값 한번에 저장
jedis.zadd("game2:scores", Map.of(
"user2", 50.0,
"user3", 100.0,
"user4", 2.0,
"user5", 15.0)
);
// 기본 오름차순 - 즉, 점수 가장 낮은 놈이 젤 먼저 나옴
List<String> zRange = jedis.zrange("game2:scores", 0, Long.MAX_VALUE);
zRange.forEach(System.out::println);
// Score 같이 가져오기
List<Tuple> tuples = jedis.zrangeWithScores("game2:scores", 0, Long.MAX_VALUE);
tuples.forEach(System.out::println);
// 전체 개수
System.out.println(jedis.zcard("game2:scores"));
// 특정 멤버의 Score 변경
jedis.zincrby("game2:scores", 100.0, "user1");
List<Tuple> tuples2 = jedis.zrangeWithScores("game2:scores", 0, Long.MAX_VALUE);
tuples2.forEach(System.out::println);
System.out.println();
// 내림차순
List<Tuple> tuple3 = jedis.zrevrangeByScoreWithScores("game2:scores", Long.MAX_VALUE, 0);
tuple3.forEach(System.out::println);
}
}
}
- 특정 키에 Score와 Member를 집어넣으면 해당 키에 점수를 가지는 각 멤버를 관리할 수 있다.
- Sorted Set은 ZADD, ZRANGE, ZCARD 와 같이 앞에 Z를 붙인다.
- ZADD는 특정 키에 Score, Member를 추가하는 명령어이다.
- ZRANGE는 특정 키에서 주어진 범위만큼의 데이터를 가져온다. 기본은 오름차순이다.
- ZRANGE만 사용하면 멤버만 가져오고 점수까지 가져오고 싶으면 WITHSCORES 명령어를 추가할 수 있다.
- ZCARD는 전체 개수를 가져오는 명령어이다.
- Sorted Set도 INCR, INCRBY, DECR, DECRBY가 가능하다. 어떤 타입이든 다 가능하다.
Geospatial
Geospatial은 말 그대로, GEO 정보를 저장하는 자료구조이다. Latitude, Longitude를 저장할 수 있다.
package cwchoiit.redis.datatypes.geospatial;
import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.args.GeoUnit;
import redis.clients.jedis.params.GeoSearchParam;
import redis.clients.jedis.resps.GeoRadiusResponse;
import java.util.List;
public class GeoMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
// GEO ADD
jedis.geoadd("stores2:geo", 127.020123124123, 37.488888991241, "store1");
jedis.geoadd("stores2:geo", 127.020123124529, 37.488998991245, "store2");
// GEO DIST (두 지점간 거리 (단위:M))
Double geoDist = jedis.geodist("stores2:geo", "store1", "store2", GeoUnit.M);
System.out.println("geoDist = " + geoDist);
// GEO SEARCH (주어진 LON, LAT 안에 store2:geo 키에 저장된 장소가 반경 100M 안에 있는지)
List<GeoRadiusResponse> geoSearch = jedis.geosearch(
"stores2:geo",
new GeoCoordinate(127.0201, 37.4889),
500,
GeoUnit.M
);
geoSearch.forEach(geoRadiusResponse -> System.out.println("geoRadiusResponse.getMemberByString() = " + geoRadiusResponse.getMemberByString()));
// GEO SEARCH (요 녀석은 결과로 나온 녀석들의 Coordinate 정보나 Distance 정보 이런것들도 다 가져올 수 있는 방식)
List<GeoRadiusResponse> geoSearch2 = jedis.geosearch("stores2:geo",
new GeoSearchParam()
.fromLonLat(new GeoCoordinate(127.0201, 37.4889))
.byRadius(500, GeoUnit.M)
.withCoord()
.withDist()
);
geoSearch2.forEach(geoRes -> {
System.out.println("geoRes.getMemberByString() = " + geoRes.getMemberByString());
System.out.println("geoRes.getCoordinate().getLatitude() = " + geoRes.getCoordinate().getLatitude());
System.out.println("geoRes.getCoordinate().getLongitude() = " + geoRes.getCoordinate().getLongitude());
System.out.println("geoRes.getDistance() = " + geoRes.getDistance());
});
// unlink 는 del 과 같이 삭제하는 명령이지만 비동기적으로 수행되는 방법
jedis.unlink("stores2:geo");
}
}
}
- 이게 꽤 재밌는 타입인게, 두 지점간 거리나 주어진 범위 안에 특정 값이 존재하는지 등 여러 Geo 정보를 구할 수 있다.
- GEOADD는 값을 추가하는 명령어이다.
- GEODIST는 특정 키에 존재하는 장소들간 거리를 구해준다.
- GEOSEARCH는 주어진 LON, LAT 안에 특정 키안에 저장된 장소들이 있다면 그 장소들을 가져와준다.
- UNLINK는 DEL과 유사한 삭제 명령어인데 UNLINK는 DEL과 달리 비동기적으로 수행된다.
Bitmap
Bitmap은 오로지 0과 1로만 이루어진 자료구조이다.
package cwchoiit.redis.datatypes.bitmap;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import java.util.stream.IntStream;
public class BitMapMain {
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("127.0.0.1", 6379)) {
Jedis jedis = jedisPool.getResource();
// bitmap 은 0 또는 1 만 저장하는 자료구조인데, 어디에 쓰이냐? 대표적으로 특정 페이지에 어떤 사용자가 방문을 했냐?와 같은 정보를 저장할때 유용하다.
// offset 100 이 유저 ID 100을 의미한다고 가정하고 true 로 설정하면 이 유저가 해당 페이지에 방문을 했다고 판단할 수 있다.
// 근데 Set DataType 으로도 그냥 사용가능한데 왜 이걸 쓰냐? 메모리 사용 측면에서 이 bitmap 이 훨씬 더 유리
jedis.setbit("request-somepage2-20230305", 100, true);
jedis.setbit("request-somepage2-20230305", 200, true);
jedis.setbit("request-somepage2-20230305", 300, true);
System.out.println(jedis.getbit("request-somepage2-20230305", 100)); // true
System.out.println(jedis.getbit("request-somepage2-20230305", 50)); // false
System.out.println(jedis.bitcount("request-somepage2-20230305")); // 3
// bitmap vs Set
Pipeline pipelined = jedis.pipelined();
IntStream.range(0, 1000000)
.forEach(i -> {
pipelined.sadd("request-somepage-set-20250306", String.valueOf(i));
pipelined.setbit("request-somepage-bit-20250306", i, true);
if (i % 1000 == 0) {
pipelined.sync();
}
});
pipelined.sync();
// 이렇게 값을 집어넣은 다음에, redis-cli 에서 "memory usage request-somepage-set-20250306", "memory usage request-somepage-bit-20250306"
// 각각 실행해보면 차이를 알 수 있다.
// 나의 경우, Set = 40388736 | Bit = 131144
System.out.println("Set Memory USAGE: " + jedis.memoryUsage("request-somepage-set-20250306"));
System.out.println("Bit Memory USAGE: " + jedis.memoryUsage("request-somepage-bit-20250306"));
}
}
}
- 어디에 사용될까한다면, 특정 페이지에 누군가 접속을 했는지와 같은 정보를 저장하는데 나름 유용하다.
- SETBIT, GETBIT으로 저장하고 가져올 수 있다.
- 근데 이런 정보는 Set 자료구조로도 가능한데 왜 Bitmap을 사용할까? Set도 특정 키에 유저 정보를 저장해서 해당 키(페이지)에 접속했는지 알 수 있을텐데 말이다. 그 이유는 성능적으로 꽤나 차이가 있기 때문이다.