どうもこじらです。
2024年10月末頃からHeroku Redis(Heroku Key-Value Store Addons)への接続ができなくなっていました。
原因はHeroku側でRedisアドオンのサービスの改定とRedisのバージョンアップがおこなわれたことにあります。
ただ、Heroku公式の設定方法を参考にしてもうまく接続できず、なんだかなぁといった具合で定期的にトライしてはいたんですが、作業の優先順位の都合で後回しにしちゃってました。
今日Geminiと話してたら解決方法が分かったので共有です。
エラー内容
スタックとレースはこんな感じです。
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.translateException(LettuceConnectionFactory.java:1847) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1778) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getNativeConnection(LettuceConnectionFactory.java:1580) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.lambda$getConnection$0(LettuceConnectionFactory.java:1560) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.doInLock(LettuceConnectionFactory.java:1521) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$SharedConnection.getConnection(LettuceConnectionFactory.java:1557) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getSharedConnection(LettuceConnectionFactory.java:1243) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory.getConnection(LettuceConnectionFactory.java:1049) at org.springframework.data.redis.core.RedisConnectionUtils.fetchConnection(RedisConnectionUtils.java:195) at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:144) at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:105) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:383) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:363) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:350) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) at jp.brainjuice.pokego.config.RedisAspect.logRedisOperations(RedisAspect.java:25) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:637) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:627) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720) at org.springframework.data.redis.core.RedisTemplate$$SpringCGLIB$$0.execute(<generated>) at org.springframework.data.redis.core.RedisKeyValueAdapter.getAllOf(RedisKeyValueAdapter.java:358) at org.springframework.data.redis.core.RedisKeyValueAdapter.getAllOf(RedisKeyValueAdapter.java:341) at org.springframework.data.keyvalue.core.KeyValueTemplate.lambda$findAll$2(KeyValueTemplate.java:216) at org.springframework.data.keyvalue.core.KeyValueTemplate.execute(KeyValueTemplate.java:314) at org.springframework.data.keyvalue.core.KeyValueTemplate.executeRequired(KeyValueTemplate.java:329) at org.springframework.data.keyvalue.core.KeyValueTemplate.findAll(KeyValueTemplate.java:214) at org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository.findAll(SimpleKeyValueRepository.java:108) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355) at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277) at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516) at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285) at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:173) at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:148) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) at jdk.proxy2/jdk.proxy2.$Proxy157.findAll(Unknown Source) at jp.brainjuice.pokego.cache.service.TopicListManager.createTopicPageList(TopicListManager.java:121) at jp.brainjuice.pokego.cache.service.TopicListManager.updateTopicList(TopicListManager.java:101) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:130) at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$2(ScheduledMethodRunnable.java:124) at io.micrometer.observation.Observation.observe(Observation.java:499) at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:124) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305) at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:840) Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to example.com/<unresolved>:6379 at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78) at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:56) at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:350) at io.lettuce.core.RedisClient.connect(RedisClient.java:215) at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.lambda$getConnection$1(StandaloneConnectionProvider.java:112) at java.base/java.util.Optional.orElseGet(Optional.java:364) at org.springframework.data.redis.connection.lettuce.StandaloneConnectionProvider.getConnection(StandaloneConnectionProvider.java:112) at org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory$ExceptionTranslatingConnectionProvider.getConnection(LettuceConnectionFactory.java:1776) ... 79 common frames omitted Caused by: java.net.SocketException: Connection reset at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:394) at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:426) at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:255) at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:356) at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994) at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ... 1 common frames omitted
バージョン
- Java: 17
- org.springframework.boot: 3.3.3
- Heroku Redis(Heroku Key-Value Store): 7.0.15
Heroku Redisへの接続方法(Java,Spring Boot)
結果的に接続方法はシンプルでした。
まず、公式のページにあるSpring Bootの場合の設定方法を引用し、任意のRedis設定用のConfigurationクラスに以下を設定します。
@Bean LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() { return builder -> { if (builder.build().isUseSsl()) { builder.useSsl().disablePeerVerification(); } }; }
miniプラン云々REDIS_TLS_URL云々は無視してOKです。普通にREDIS_URLで接続する認識で問題ありません。
これに加え、application.ymlに以下の設定をしてください。
spring.data.redis.url: ${REDIS_URL:}
application.properties派の人はいい感じに読み替えてください。
たぶん、Spring Bootの公式ドキュメントかどこかには当然のように設定方法書いてある内容なんだろうなとは思いますが、Herokuの記事にはREDIS_URLを設定するだけでいい感じにSpring Bootが読み込んでくれるかのように書いてあり気付けませんでした。
これだけで接続できます。
躓いたポイント
Spring Bootを使用してRedisの接続情報を設定する際には色々な設定方法があります。
@Beanでカスタマイズすれば柔軟に接続情報を設定出来ちゃいそうですが、Spring BootがSpringを一部内包していて、SpringがLettuceを内包している都合でそんなうまいことやってくれる訳ではありません。
具体的には、org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizerと、org.springframework.data.redis.connection.lettuce.LettuceConnectionFactoryは併用できません。
LettuceConnectionFactoryを@Beanで定義して設定すると、環境変数に設定したREDIS_URLからホスト名やポート、usernameやpassword等を明示的に設定していけるので扱いやすいんですが、これを設定しちゃうとLettuceClientConfigurationBuilderCustomizerで設定した定義を読んでくれません。
あ、正確にはLettuceClientConfigurationBuilderCustomizerで返却値に指定したラムダ内の処理を読んでくれません。
理由は、SpringBootがConnectionFactoryを生成する過程でLettuceClientConfigurationBuilderCustomizerの定義を読むため、LuttuceConnectionFactoryを明示的に指定した場合はそっちが優先されるようです。
まぁざっと流し見した感じはそんな感じでした。ディテールは間違ってるかも。
ちなみに、LettuceConnectionFactoryでゴリ押す場合は、UseSslをtrueに、verifyPeerをfalseに設定すればHeroku Redisに接続できるみたいです。
まぁ非推奨ですけどね。
@Bean LettuceConnectionFactory redisConnectionFactory() throws URISyntaxException { URI uri = new URI(System.getenv("REDIS_URL")); RedisStandaloneConfiguration redisConf = new RedisStandaloneConfiguration(); // redisConfにuriから取得した接続情報を設定する。 LettuceConnectionFactory factory = new LettuceConnectionFactory(redisConf); factory.setUseSsl(true); factory.setVerifyPeer(false); return factory; }
あと、LettuceConnectionFactoryのデフォルトの接続先がlocalhost:6379な点も注意してください。ローカルでHeroku Redisを接続しに行くように設定し、接続できたと思ったらlocalhost:6379に繋がっていて時間を無駄にしました。私の二の舞にはならないでくださいw
こじらでした
じゃ