Redis 공식문서: https://redis.io/docs/latest/
Docs
redis.io
REDIS(REmote DIctionary Server)
💡 key-value 형식의 NoSQL 인메모리 데이터베이스
Redis를 사용하는 이유
- MySQL 같은 관계형 데이터베이스는 파일시스템(SSD, HDD)에 데이터를 저장해서 서비스가 종료되어도 영구적으로 데이터를 유지할 수 있다.
- 하지만, 데이터가 영구적으로 저장될 필요가 없고 일시적으로 저장되어야 할 상황이 있다면?
- 세션이나 로그인 정보, 장바구니에 상품을 담는 기능 등… 사용자의 행동에 따라 데이터의 수정이 빈번하고 영구적으로 반영될 필요가 없다.
- 기존의 관계형 데이터베이스는 파일시스템에 데이터를 저장하고 변경하므로 속도가 느리다. 반면, 메모리의 데이터에 접근하고 변경하는 건 상대적으로 빠르다.
- 컴퓨터 구조적으로 CPU ↔ Cache ↔ Memory(RAM) ↔ Disk(SSD, HDD) 이기 때문에!
- 이러한 상황에서 자주 사용되는 것이 redis다.
- 즉, 데이터 변경이 잦은 기능을 다룰 때 ex) 리더보드, 방문자 트래킹, 캐싱, 세션 클러스터링 등 특정 상황에서 redis를 사용하여 효율을 높일 수 있다.
NoSQL 데이터베이스: SQL을 사용하지 않는다. (Not only SQL)
예를 들어,
MongoDB → Document 형식 데이터 (json, xml …)
Cassandra → Column Family (wide-col)
Redis 설치 및 연동
도커를 사용해서 redis를 설치하자. 내 컴퓨터에 설치하는 것은 여러모로 관리가 귀찮다.
도커 없인 못살아
- docker-compose.yml
services:
redis-stack:
image: redis/redis-stack
container_name: redis-stack-compose
restart: always
environment:
REDIS_ARGS: "--requirepass systempass"
ports:
- 6379:6379
- 8001:8001
이미지는 docker hub에서 redis를 검색하니 redis, redis-stack, redis-stack-server 등등 다양했다.
- redis args의
--requirepass systempass
는 비밀번호가 systempass라는 의미이다.- default / systempass
- docker-compose 파일이 있는 위치 어디에서나
docker compose up -d
를 실행하면 redis 이미지를 받아올 수 있다. - localhost:8001로 접속을 하게 되면 웹에서 redis insight(redis 사용을 편리하게 해주는 IDE)를 사용할 수 있다.
Redis의 Data Type
1) String
Redis가 Java의 Map<String, String>
처럼 동작한다고 생각하자.
저장할 수 있는 최대 크기는 512MB
GET, SET 명령어
SET user:email jwj68254@poppin.com
GET user:email
SET <key> <value>
→key
에value
문자열 데이터를 저장,"
으로 공백 구분GET <key>
→key
에 저장된 문자열 반환
INCR, DECR 명령어
저장된 데이터가 정수형이면 데이터 증감이 가능하다.
SET user:count 1
INCR user:count
DECR user:count
INCR key
→key
에 저장된 데이터를 1 증가DECR key
→key
에 저장된 데이터를 1 감소
MSET, MGET
여러 Key - Value를 한번에 다룰 수 있다.
MSET user:name wonjun user:email wonjun@naver.com
MGET user:name user:email
MSET key value [key value …]
→key value
의 형태로 주어진 인자들을 각key
에value
를 저장MGET key [key]
→ 주어진 모든key
에 해당하는 데이터를 반환
문자열 데이터이므로 이미지, 음성, 영상, 파일 등도 보관이 가능하다.
2) List
여러 문자열 데이터를 Linked List의 형태로 보관한다. Linked List이기 때문에 양 끝에 데이터를 삽입 or 삭제할 수 있는 스택 또는 큐 처럼 사용할 수 있다.
Key - Value 형태로 저장하므로 Map<String, List<String>>
의 형태로 사용한다.
LPUSH, RPUSH, LPOP, RPOP
PUSH, POP을 L, R과 조합하여, 왼쪽 또는 오른쪽에 데이터를 삽입 / 삭제할 수 있다.
LPUSH user:list alex # [alex]
LPUSH user:list brad # [brad, alex]
RPUSH user:list chad # [brad, alex, chad]
RPUSH user:list dave # [brad, alex, chad, dave]
LPOP user:list # brad
RPOP user:list # chad
LPUSH key value
→key
에 저장된 리스트의 앞쪽에value
를 저장RPUSH key value
→key
에 저장된 리스트의 뒤쪽에value
를 저장LPOP key
→key
에 저장된 리스트의 앞쪽에서 값을 반환 및 제거RPOP key
→key
에 저장된 리스트의 뒤쪽에서 값을 반환 및 제거
LLEN, LRANGE
길이 구하기, 범위 내 원소 반환하기 등의 기능
LLEN user:list
LRANGE user:list 0 3 # 0, 1, 2, 3: 4개 원소 반환
LRANGE user:list 0 100000
# 뒤에서부터 반환
LRANGE user:list 0 -1
LRANGE user:list 0 -2
# 빈 리스트 반환
LRANGE user:list 1 0
LLEN key
→key
에 저장된 리스트의 길이를 반환LRANGE key start end
→key
의start
부터end
까지 원소들을 반환end
가 실제 길이를 벗어나도 오류가 발생하진 않는다.start
>end
일 경우 빈 결과가 반환된다.- 음수의 경우 리스트의 뒤에서부터 데이터를 가져온다.
3) Set
문자열의 집합으로 중복값을 제거하며, 순서가 존재하지 않는다.
SADD, SREM, SMEMBERS, SISMEMBER, SCARD
SADD user:java alex # [alex]
SADD user:java brad # [alex, brad]
SADD user:java chad # [alex, brad, chad]
SREM user:java alex # [brad, chad]
SMEMBERS user:java # [alex, brad, chad]
SISMEMBER user:java brad # true
SISMEMBER user:java dave # false
SCARD user:java # 3
SADD key value
→key
에 저장된 집합에value
를 추가SREM key value
→key
에 저장된 집합의value
를 제거SMEMBERS key
→key
에 저장된 집합의 모든 원소를 반환SISMEMBER key value
→key
에 저장된 집합에value
가 존재하는지 반환SCARD key
→key
에 저장된 집합의 크기를 반환
SINTER, SUNION, SINTERCARD
복수의 집합이 있다면, 교집합, 합집합 등의 기능을 사용할 수 있다. 결과 집합 자체를 반환하기도, 결과 집합의 크기를 반환하기도 합니다.
# 다른 Set에 추가한 뒤,
SADD user:python alex
SADD user:python dave
SINTER user:java user:python # [alex]
SUNION user:java user:python # [alex, brad, chad, dave]
SINTERCARD 2 user:java user:python # 1
SINTER key1 key2
→key1
과key2
에 저장된 집합들의 교집합의 원소들을 반환SUNION key1 key2
→key1
과key2
에 저장된 집합들의 합집합의 원소들을 반환SINTERCARD number key1 [key2 ...]
→number
개의key
에 저장된 집합들의 교집합의 크기를 반환
중복을 허용하지 않으며, 어떤 데이터의 존재 여부를 확인하는 SISMEMBER
같은 경우 O(1)의 시간복잡도를 가진다.
4) ⭐ Hash ⭐
Field - Value 쌍으로 이뤄진 자료형
Hash 데이터를 가져오기 위해 Key를 사용하고, 이후 다시 Key에 저장된 Hash 데이터에 Field - Value 쌍을 넣어주는 형식으로 동작한다.
즉, Redis 전체가 Map
이라면 Hash는 Map<String, Map<String, String>>
의 형식
HSET, HGET, HMGET, HGETALL, HKEYS, HLEN
HSET user:alex name alex age 25 major CSE
HGET user:alex name
HGET user:alex major
HMGET user:alex age major name
HGETALL user:alex
HKEYS user:alex
HLEN user:alex
HSET key field value [field value]
→key
의 Hash에field
에value
를 넣는다. 한번에 여러field
-value
쌍을 넣어줄 수 있다.HGET key field
→key
에 저장된 Hash의field
에 저장된value
를 반환. 없는field
의 경우null
.HMGET key field [field]
→key
에 저장된 Hash에서 복수의field
에 저장된value
를 반환.HGETALL key
→key
에 저장된 Hash에 저장된field
-value
를 전부 반환.HKEYS key
→key
에 저장된 Hash에 저장된field
를 전부 반환HLEN key
→key
에 저장된 Hash에 저장된field
의 갯수를 반환
Hash는 본래 하나의 키에 복잡한 데이터 (객체의 데이터)를 하나의 키에 저장하는 용도로 주로 활용되고, 공식 문서에서도 여러 Key에 걸쳐 객체의 데이터를 표현하기 보단 Hash를 자주 활용할 것을 권장한다.
ex) 장바구니 같은 기능은 사용자별로, 어떤 물품이 몇개나 담겨있는지와 같은 정보가 포함되어야 한다. 사용자 마다 Hash 데이터를 생성하고, 물품 - 갯수 형식으로 데이터를 저장하면 사용자별 장바구니를 쉽게 저장할 수 있다.
5) Sorted Set
정렬된 집합. 기본적으로 Set과 동일하게, 유일한 값들만 유지하지만 추가적으로 각 값들에 score라고하는 실수를 함께 보관한다.
데이터를 가져올 때, score를 바탕으로 정렬하여 값들을 가져온다.
ZADD, ZINCRBY, ZRANK, ZRANGE, ZREVRANK, ZREVRANGE
ZADD user:ranks 10 alex 9 brad 11 chad 8 dave
ZINCRBY user:ranks 2 alex
ZRANK user:ranks alex
ZRANGE user:ranks 0 3
ZREVRANK user:ranks alex
ZREVRANGE user:ranks 0 3
ZADD key score member [score member ...]
→key
의 Sorted Set에score
를 점수로 가진member
를 추가, 이미 있는member
의 경우 새로운score
를 설정ZINCRBY key increment member
→key
의 Sorted Set의member
의score
를increment
만큼 증가 (음수를 전달하면 감소)ZRANK key member
→key
의 Sorted Set의member
의 순위를 오름차순 기준으로 0에서 부터 세서 반환ZRANGE key start stop
→key
의 Sorted Set의member
들을start
부터stop
순위까지 오름차순 기준으로 반환ZREVRANK key member
→key
의 Sorted Set의member
의 순위를 내림차순 기준으로 0에서 부터 세서 반환ZREVRANGE key start stop
→key
의 Sorted Set의member
들을start
부터stop
순위까지 내림차순 기준으로 반환
ex) 대표적으로 리더보드나 Rate Limiter, 즉 순위와 관련된 기능을 만드는데 사용된다.
6) 그 외 공용 명령
Key를 제거하기 위한 DEL
, 만료시각 설정을 위한 EXPIRE
DEL, EXPIRE, EXPIRETIME
SET somekey "to be deleted"
DEL somekey
SET expirekey "to be expired"
EXPIRE expirekey 5
EXPIRETIME expirekey
DEL key
→key
(와 저장된 데이터)를 제거EXPIRE key seconds
→key
의 TTL(유효시각)을seconds
로 설정,seconds
초가 지나면key
제거EXPIRETIME key
→key
가 만료되는 시각을 Unix Timestamp로 반환
저장된 모든 Key를 확인
KEYS *
glob 패턴 형식을 전달하여, 패턴에 일치하는 키를 반환하는 메서드
모든 Key를 제거
FLUSHDB
Spring boot 3 + Redis 사용해보기
Redis 개념을 학습했으니 spring boot에 적용해보자.
Spring initializer에서의 dependency는 다음과 같다.
yml 파일 추가
spring:
data:
redis:
host: localhost
port: 6379
username: default
password: systempass
레디스를 스프링 프레임워크에서 사용하는 데 크게 두 가지 방법이 있다.
1. CRUD Repository에서 사용
Item.java
package com.example.redis;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.io.Serializable;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
// @Entity 대신 @RedisHash를 사용하여 Redis에 저장할 수 있도록 설정
@RedisHash("item")
public class Item implements Serializable {
@Id
// id를 String으로 설정하면 UUID가 자동으로 배정된다.
private String id;
private String name;
private String description;
private Integer price;
}
💡 Entity의 id를 Long이 아닌 String으로 설정하면 자동으로 UUID가 생성된다.
ItemRepository.java
public interface ItemRepository extends CrudRepository<Item, String> {}
RedisRepositoryTests.java
package com.example.redis;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class RedisRepositoryTests {
@Autowired
private ItemRepository itemRepository;
@Test
void createTest() {
Item item = Item.builder()
.name("keyboard")
.description("Very Expensive Keyboard")
.price(100000)
.build();
itemRepository.save(item);
}
@Test
public void readOneTest() {
Item item = itemRepository.findById("80a0b63f-cec2-484c-8f8f-5882603d3657").orElseThrow();
System.out.println(item.getDescription());
}
@Test
public void updateTest() {
Item item = itemRepository.findById("80a0b63f-cec2-484c-8f8f-5882603d3657").orElseThrow();
item.setDescription("on sale!!!");
itemRepository.save(item);
item = itemRepository.save(item);
System.out.println(item.getDescription());
}
@Test
public void deleteTest() {
itemRepository.deleteById("80a0b63f-cec2-484c-8f8f-5882603d3657");
}
}
- createTest 수행 후 레디스 조회
- updateTest 수행 후 레디스 조회
- deleteTest 수행 (해시 테이블 Item:
80a0b63f-cec2-484c-8f8f-5882603d3657
이 사라졌다.)
RDB에서 CRUD 하듯이 비슷하게 코드를 작성하면 된다.
Entity에서 @Entity
어노테이션 대신 @RedisHash
를 사용하고,
Repository에서 JpaRepository 대신 CrudRepository를 상속받아 구현하면 된다.
2. RedisTemplate 사용
StringRedisTemplate: java와 redis의 소통
RedisConfig.java
package com.example.redis;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, ItemDto> itemRedisTemplate(
RedisConnectionFactory redisConnectionFactory
) {
RedisTemplate<String, ItemDto> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
}
전역적으로 사용할 수 있게 스프링 bean 등록을 해준다.
ItemDto.java
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ItemDto implements Serializable {
private String name;
private String description;
private Integer price;
}
RedisTemplateTests.java
package com.example.redis;
@SpringBootTest
public class RedisTemplateTests {
@Autowired
// StringRedisTemplate인 이유는 자바에서 쓰는 타입이 String이라서.
private StringRedisTemplate stringRedisTemplate;
@Test
public void stringOpsTests() {
// 자바에서 레디스의 문자열 조작을 위한 클래스
// redis template에 설정된 타입을 바탕으로 redis의 문자열 조작을 한다.
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("simple key", "simple values");
// key: simple key, value: simple vales
System.out.println(ops.get("simple key"));
// 자바에서 레디스의 집합 조작을 위한 클래스
SetOperations<String, String> setOps = stringRedisTemplate.opsForSet();
setOps.add("hobbies", "games");
setOps.add("hobbies", "coding", "alcohol", "games");
System.out.println(setOps.size("hobbies")); // 3
stringRedisTemplate.expire("hobbies", 10, TimeUnit.SECONDS); // 10초 뒤에 레디스에서 삭제
stringRedisTemplate.delete("simple key"); // simple key를 key로 가지는 데이터 삭제
}
@Autowired
private RedisTemplate<String, ItemDto> itemRedisTemplate;
@Test
public void itemRedisTemplateTests() {
ValueOperations<String, ItemDto> ops = itemRedisTemplate.opsForValue();
ops.set("my:keyboard", ItemDto.builder()
.name("Mechanical keyboard")
.description("very expensive keyboard")
.price(300000)
.build());
System.out.println(ops.get("my:keyboard"));
}
}
stringOpsTests
를 통해 simple key를 key로 가진 데이터가 삭제되고, hobbies를 key로 가지는 집합 데이터가 생성되었다.또한, 10초 뒤에 삭제하도록 expire를 걸어놨으므로 새로고침해보니 hobbies도 삭제되었다.
itemRedisTemplateTests
를 통해 RedisConfig에서 bean 등록한RedisTemplate<String, ItemDto> itemRedisTemplate
을 주입받아와opsForValue
메서드를 사용하여 my:keyboard를 key로, ItemDto를 value로 하는 데이터를 생성했다.redisTemplate.setValueSerializer(RedisSerializer.json());
를 통해 ItemDto 객체가 json으로 직렬화된 것을 확인할 수 있다.
StringRedisTemplate 클래스와 RedisTemplate 클래스 일부 코드를 봐보자.
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
this.setKeySerializer(RedisSerializer.string());
this.setValueSerializer(RedisSerializer.string());
this.setHashKeySerializer(RedisSerializer.string());
this.setHashValueSerializer(RedisSerializer.string());
}
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private boolean enableTransactionSupport = false;
private boolean exposeConnection = false;
private boolean initialized = false;
private boolean enableDefaultSerializer = true;
💡 StringRedisTemplate는 RedisTemplate<String, String>을 상속받도록 구현되어 있다. 즉, RedisTemplate<K, V> 형태의 타입 파라미터를 정의할 수 있다면, 더 복잡한 객체 데이터를 주고 받을 수 있는 것이다.
Redis로 블로그 글 별 조회수 기능 구현 (Java 코드 X, 설계)
- 내 블로그 글 별 조회수를 Redis로 확인하고 싶다.
- 블로그 URL의 PATH는
/articles/{id}
형식이다. - 로그인 여부와 상관없이 새로고침 될때마다 조회수가 하나 증가한다.
- 블로그 URL의 PATH는
-- INCR articles:{id}
INCR articles:1
INCR articles:2
INCR articles:3
-- 오늘의 조회수를 따로 관리
INCR articles:1:today
RENAME articles:1:today articles:2024-xx-xx
- 블로그에 로그인한 사람들의 조회수(중복 X)와 가장 많은 조회수를 기록한 글을 Redis로 확인하고 싶다.
- 블로그 URL의 PATH는
/articles/{id}
형식이다. - 로그인 한 사람들의 계정은 영문으로만 이뤄져 있다.
- 블로그 URL의 PATH는
-- Set -> 중복 X
SADD articles:1 wonjun -- 처음 실행할 때 1 반환, 그 후 실행 시 0 반환
SADD articles:1 test
SCARD articles:1
SADD articles:2 wonjun
SADD articles:2 test
SADD articles:2 who
SCARD articles:2
SADD articles:1 a b c
-- SADD의 결과에 따라 분기처리한다.
-- 0일 때 skip
-- 1일 때 sorted set에 추가
-- ZADD articles:ranks 1 articles:1 -- 처음 실행할 때 1 반환, 그 후 실행 시 0 반환
ZINCRBY articles:ranks 1 articles:1 -- articles:1 계속 1 씩 증가
ZINCRBY articles:ranks 1 articles:2 -- articles:2 계속 1 씩 증가
-- 첫번째 데이터만 반환
-- 리더보드에 가장 높은 socre를 가진 데이터 or 가장 많은 조회수의 글을 보고 싶다
ZREVRANGE articles:ranks 0 0
ZRANGE articles:ranks 0 0 REV
- 주문 ID, 판매 물품, 갯수, 총액, 결제 여부에 대한 데이터를 지정하기 위한
ItemOrder
클래스를RedisHash
로 만들고,- 주문 ID - String
- 판매 물품 - String
- 갯수 - Integer
- 총액 - Long
- 주문 상태 - String
- 주문에 대한 CRUD를 진행하는 기능을 만들어보자.
ItemOrder
의 속성값들을 ID를 제외하고 클라이언트에서 전달해준다.- 성공하면 저장된
ItemOrder
를 사용자에게 응답해준다.
- 실제 Entity 등은 만들지 않고, Redis에 데이터만 저장해보자.
ItemOrder.java
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@RedisHash("order")
public class ItemOrder {
@Id
private String id;
private String item;
private Integer count;
private Long totalPrice;
private String status;
}
OrderRepository.java
public interface OrderRepository extends CrudRepository<ItemOrder, String> { }
OrderController.java
package com.example.redis;
@RestController
@RequiredArgsConstructor
@RequestMapping("orders")
public class OrderController {
private final OrderRepository orderRepository;
@PostMapping
public ItemOrder create(
@RequestBody
ItemOrder order
) {
return orderRepository.save(order);
}
@GetMapping
public List<ItemOrder> readAll() {
List<ItemOrder> orders = new ArrayList<>();
orderRepository.findAll()
.forEach(orders::add);
return orders;
}
@GetMapping("{id}")
public ItemOrder readOne(
@PathVariable("id")
String id
) {
return orderRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
@PutMapping("{id}")
public ItemOrder update(
@PathVariable("id")
String id,
@RequestBody
ItemOrder order
) {
ItemOrder target = orderRepository
.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
target.setItem(order.getItem());
target.setCount(order.getCount());
target.setTotalPrice(order.getTotalPrice());
target.setStatus(order.getStatus());
return orderRepository.save(target);
}
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(
@PathVariable("id")
String id
) {
orderRepository.deleteById(id);
}
}
- JpaRepository를 사용할 때와 큰 차이가 없다.
Redis로 블로그 글 별 조회수 기능 구현 (Java 코드 O)
Redisconfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Integer> articleTemplate(
RedisConnectionFactory redisConnectionFactory
) {
RedisTemplate<String, Integer> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
return redisTemplate;
}
}
ArticleController.java
package com.example.redis;
@RestController
@RequestMapping("articles")
public class ArticleController {
private final ValueOperations<String, Integer> ops;
public ArticleController(
RedisTemplate<String, Integer> articleTemplate
) {
ops = articleTemplate.opsForValue();
}
@GetMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void read(
@PathVariable("id")
Long id
) {
ops.increment("articles:%d".formatted(id));
}
}
'데이터베이스 > Redis' 카테고리의 다른 글
Redis 응용 - 검색 결과 캐싱 (0) | 2024.08.16 |
---|---|
캐싱 전략을 활용한 Redis와 Spring boot (0) | 2024.08.15 |
Spring Boot와 Redis로 Session Clustering 간단 구현 (0) | 2024.08.14 |