Redis
- インメモリデータベース
- メモリ上(RAM)にデータを保存する
- メリット
- 低レイテンシ
- 高スループット
- デメリット
- RAMが高価
- ストレージサイズに限界がある
- NoSQL
Spring BootでRedisを用いたセッション管理をしてみる
ソースコードは以下。
Redisの準備
RedisはDockerで起動することにする。
version: '3'
services:
redis:
image: redis
ports:
- 6379:6379
compose.ymlからコンテナ起動し、docker compose exec redis bash
でコンテナ内に入り、redis-cli
を実行する。
$ docker compose up -d
$ docker compose exec redis bash
root@4229d9d7221f:/data# redis-cli
127.0.0.1:6379>
set key value
でデータ登録し、keys *
で登録されたデータ一覧を参照できる。
127.0.0.1:6379> set aaa bbb
OK
127.0.0.1:6379> keys *
1) "aaa"
127.0.0.1:6379> get aaa
"bbb"
後述するJavaコードでセッションをRedisに登録するが、セッションはシリアライズされるためgetでは参照できない。
代わりに、hgetall
で取得する。
127.0.0.1:6379> get spring:session:sessions:e8aa9a26-1c93-4b22-9733-b364586ffa52
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> hgetall spring:session:sessions:e8aa9a26-1c93-4b22-9733-b364586ffa52
1) "creationTime"
2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x8d\xe3[j3"
3) "maxInactiveInterval"
4) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
5) "sessionAttr:hoge"
6) "\xac\xed\x00\x05t\x00\x04fuga"
7) "lastAccessedTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x8d\xe3[j4"
Javaコード
特に凝ったことはしていない。
spring:
data:
redis:
host: localhost
port: 6379
password: null
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.session:spring-session-data-redis'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
package io.github.tttol.redis.controller;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@Slf4j
@RequestMapping("redis")
@RequiredArgsConstructor
public class RedisController {
private final HttpSession session;
@GetMapping
public String hello() {
return "Hello, Redis!";
}
@SuppressWarnings("null")
@GetMapping("simple/put")
public String put() {
var connectionFactory = new LettuceConnectionFactory();
connectionFactory.afterPropertiesSet();
var template = new RedisTemplate<String, String>();
template.setConnectionFactory(connectionFactory);
template.setDefaultSerializer(StringRedisSerializer.UTF_8);
template.afterPropertiesSet();
template.opsForValue().set("foo", "bar");
log.info("Value at foo:" + template.opsForValue().get("foo"));
connectionFactory.destroy();
return "put foo=bar to redis";
}
@GetMapping("session/put/{key}/{value}")
public String putSession(@PathVariable String key, @PathVariable String value) {
session.setAttribute(key, value);
return "put %s=%s to session".formatted(key, value);
}
}
putSession
メソッドをcurlで呼んでみる。
$ curl -v http://localhost:8080/redis/session/put/hoge/fuga
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /redis/session/put/hoge/fuga HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: SESSION=ZThhYTlhMjYtMWM5My00YjIyLTk3MzMtYjM2NDU4NmZmYTUy; Path=/; HttpOnly; SameSite=Lax
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 24
< Date: Mon, 26 Feb 2024 03:00:36 GMT
<
* Connection #0 to host localhost left intact
put hoge=fuga to session%
< Set-Cookie: SESSION=ZThhYTlhMjYtMWM5My00YjIyLTk3MzMtYjM2NDU4NmZmYTUy;
より、セッション登録されてることがわかる。
Redisで確認してみる。
127.0.0.1:6379> keys spring*
1) "spring:session:sessions:e8aa9a26-1c93-4b22-9733-b364586ffa52"
127.0.0.1:6379> hgetall spring:session:sessions:e8aa9a26-1c93-4b22-9733-b364586ffa52
1) "creationTime"
2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x8d\xe3[j3"
3) "maxInactiveInterval"
4) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
5) "sessionAttr:hoge"
6) "\xac\xed\x00\x05t\x00\x04fuga"
7) "lastAccessedTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x8d\xe3[j4"
セッションIDが一致しなくない?
レスポンスヘッダとRedisでセッションのIDが違うように見える。
SESSION=ZThhYTlhMjYtMWM5My00YjIyLTk3MzMtYjM2NDU4NmZmYTUy;
1) "spring:session:sessions:e8aa9a26-1c93-4b22-9733-b364586ffa52"
これはSpirng SessionがRedisへ登録する際にIDをbase64エンコードしてるらしく、値が異なって見えているもよう。
デコードすると同じ値になる。
import base64
encoded_session_id = "ZThhYTlhMjYtMWM5My00YjIyLTk3MzMtYjM2NDU4NmZmYTUy"
decoded_session_id = base64.urlsafe_b64decode(encoded_session_id).decode("utf-8")
print(decoded_session_id) # Output: e8aa9a26-1c93-4b22-9733-b364586ffa52
Spring Sessionの公式リファレンスからはその記載は見つけることができていないが、ソースコードを見るとたしかにCookieから値を取得する際にbase64エンコードしていた。
private String getValue(CookieValue cookieValue) {
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = requestedCookieValue;
if (this.jvmRoute != null) {
actualCookieValue = requestedCookieValue + this.jvmRoute;
}
if (this.useBase64Encoding) {
actualCookieValue = base64Encode(actualCookieValue);
}
return actualCookieValue;
}
参考