redis初心者です。以下は https://docs.spring.io/spring-data-redis/docs/current/reference/html/#pipeline の抄訳。
5.11. Pipelining
Redisにはパイプライン(https://redis.io/topics/pipelining)があり、これはリプライを待たずにサーバへ複数コマンドを送信し、単一ステップでリプライを読み込みます。パイプラインにより、たとえば同一Listに多数の要素を追加するなど、一度に複数コマンドを送信する場合にパフォーマンス向上が見込めます。
Spring Data Redisはパイプラインでコマンド実行のためのRedisTemplateメソッドをいくつか用意しています。パイプラインの処理結果が不要な場合、executeのpipeline引数にtrueを渡します。executePipelined
ではRedisCallback
かSessionCallback
でパイプライン処理を渡します。
// キューから指定アイテム数popする List<Object> results = stringRedisTemplate.executePipelined( new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisConnection stringRedisConn = (StringRedisConnection)connection; for(int i=0; i< batchSize; i++) { stringRedisConn.rPop("myqueue"); } return null; } });
上の例はパイプラインでキューから指定アイテム数のright popをバルク処理します。results
のList
にはpopした全アイテムが入っています。RedisTemplate
は、return前に全結果をデシリアライズするためシリアライザに値・hash key・hash valueを渡すので、上の例では戻される値はStringです。パイプライン結果用のカスタムシリアライザを渡せるexecutePipelined
もあります。
なお、RedisCallback
の戻り値はnullにして下さい。この値はパイプライン処理の結果を返す際に無視します。
抜粋終了。なるほどね? ということで実際に試してみる。とりあえず、愚直にループset
するのとpipeline使用との速度を比較する。
ソースコード
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>
以下はapplication.yml。
spring: redis: host: localhost timeout: 10000000
spring.redis.host.timeout: 10000000
だが、これが無いとウチの環境だと400万件あたりから以下のような例外スローするようになる。なので、とりあえずの措置としてタイムアウトは事実上発生しない数値を設定している。
Caused by: io.lettuce.core.RedisException: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s) at io.lettuce.core.LettuceFutures.awaitAll(LettuceFutures.java:88) ~[lettuce-core-5.1.8.RELEASE.jar:na] at org.springframework.data.redis.connection.lettuce.LettuceConnection.closePipeline(LettuceConnection.java:528) ~[spring-data-redis-2.1.10.RELEASE.jar:2.1.10.RELEASE] ... 21 common frames omitted
愚直なset
for (int i = 0; i < MAX; i++) { redis.boundValueOps("" + i).set("hogeValue"); }
pipeline使用。
var list = redis.executePipelined(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisConnection stringRedisConn = (StringRedisConnection) connection; for (int i = 0; i < MAX; i++) { stringRedisConn.set("" + i, "hogeValue"); } return null; } }); System.out.println(list.size());
上記処理ブロックの開始・終了のSystem.currentTimeMillis()
の差を計測する。
結果
計測結果
面倒くさくてそれぞれ1回しか計測してない。なので信頼度としては微妙だが、今回は両者の相対的な差が重要なんで、そこらへんは無視する。
あと下表を見ればわかるが愚直なループは10万件で約43秒と、それ以上件数増やして計測する気になれなかったのでやっていない。
件数 | pipeline | 愚直 |
---|---|---|
1,000 | 995 | 1630 |
10,000 | 1266 | 5999 |
100,000 | 3297 | 43070 |
1,000,000 | 15245 | - |
2,000,000 | 42208 | - |
3,000,000 | 58512 | - |
4,000,000 | 77789 | - |
感想とか
Redis初心者なんであーだこーだ考えたことダラダラと書いてます。なので、以下は技術的におかしなこと書いてる可能性あります。
10万件で約3秒・約43秒とかなりの差がついた。この環境において数万件以上を連続set
したい場合、pipelineの使用が有効らしい。逆に、1万件以下、1000件だと差が0.5秒とかそんなもんで、そもそも両者とも1秒付近。この程度だと、pipelineをやる価値は微妙、といった所だろうか。数百万件以上だとpipelineを使わないと話にならない速度差になるので、pipelineの本懐はむしろ数百万以上にある、と考えられる。
RDBのいわゆる「コミット間隔」が念頭にあったので、pipelineは一度に数百万単位が有効な世界、と分かった時は結構面食らった。pipelineの「リプライを待たずにコマンドを送信し続け、最後にまとめてコマンド結果を受け取る」の理解が要なのだろう。