出现的现象:
这个问题只在 AWS elastic cache版本会重现
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR Error running script (call to f_e9f69f2beb755be68b5e456ee2ce9aadfbc4ebf4): @user_script:1: @user_script: 1: -READONLY You can't write against a read only replica. at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54) at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52) at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:275) at org.springframework.data.redis.connection.lettuce.LettuceScriptingCommands.convertLettuceAccessException(LettuceScriptingCommands.java:236) at org.springframework.data.redis.connection.lettuce.LettuceScriptingCommands.evalSha(LettuceScriptingCommands.java:195) at org.springframework.data.redis.connection.DefaultedRedisConnection.evalSha(DefaultedRedisConnection.java:1502) at jdk.internal.reflect.GeneratedMethodAccessor225.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:61) at com.sun.proxy.$Proxy248.evalSha(Unknown Source) at org.springframework.data.redis.core.script.DefaultScriptExecutor.eval(DefaultScriptExecutor.java:77) at org.springframework.data.redis.core.script.DefaultScriptExecutor.lambda$execute$0(DefaultScriptExecutor.java:68) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:175) at org.springframework.data.redis.core.script.DefaultScriptExecutor.execute(DefaultScriptExecutor.java:58) at org.springframework.data.redis.core.script.DefaultScriptExecutor.execute(DefaultScriptExecutor.java:52) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:350)
官方关于这个bug的描述:
environment
- Lettuce version(s): [5.0.3.RELEASE, 5.0.4.RELEASE,5.1.0.M1]
- Redis version: AWS 6.0.5
官方case:
https://github.com/lettuce-io/lettuce-core/issues/804
问题原因:
ReadFrom类详情配置:https://lettuce.io/core/6.1.4.RELEASE/api/index.html
Modifier and Type Field and Description static ReadFrom ANY Setting to read from any node. static ReadFrom ANY_REPLICA Setting to read from any replica node. static ReadFrom MASTER Setting to read from the upstream only. static ReadFrom MASTER_PREFERRED Setting to read preferred from the upstream and fall back to a replica if the master is not available. static ReadFrom NEAREST Setting to read from the nearest node. static ReadFrom REPLICA Setting to read from the replica only. static ReadFrom REPLICA_PREFERRED Setting to read preferred from replica and fall back to upstream if no replica is not available. static ReadFrom SLAV EDeprecated. renamed to REPLICA. static ReadFrom SLAVE_PREFERRED Deprecated. Renamed to REPLICA_PREFERRED. static ReadFrom UPSTREAM Setting to read from the upstream only. static ReadFrom UPSTREAM_PREFERRED Setting to read preferred from the upstream and fall back to a replica if the upstream is not available.
查看读写分离的读取节点
io.lettuce.core.masterslave.MasterSlaveConnectionProvider public CompletableFuture<StatefulRedisConnection<K, V>> getConnectionAsync(MasterSlaveConnectionProvider.Intent intent) { if (this.debugEnabled) { logger.debug("getConnectionAsync(" + intent + ")"); } if (this.readFrom != null && intent == MasterSlaveConnectionProvider.Intent.READ) { List<RedisNodeDescription> selection = this.readFrom.select(new Nodes() { public List<RedisNodeDescription> getNodes() { return MasterSlaveConnectionProvider.this.knownNodes; } public Iterator<RedisNodeDescription> iterator() { return MasterSlaveConnectionProvider.this.knownNodes.iterator(); } }); if (selection.isEmpty()) { throw new RedisException(String.format("Cannot determine a node to read (Known nodes: %s) with setting %s", this.knownNodes, this.readFrom)); } else { try { Flux<StatefulRedisConnection<K, V>> connections = Flux.empty(); RedisNodeDescription node; for(Iterator var4 = selection.iterator(); var4.hasNext(); connections = connections.concatWith(Mono.fromFuture(this.getConnection(node)))) { node = (RedisNodeDescription)var4.next(); } return !OrderingReadFromAccessor.isOrderSensitive(this.readFrom) && selection.size() != 1 ? connections.filter(StatefulConnection::isOpen).collectList().map((it) -> { int index = ThreadLocalRandom.current().nextInt(it.size()); return (StatefulRedisConnection)it.get(index); }).switchIfEmpty(connections.next()).toFuture() : connections.filter(StatefulConnection::isOpen).next().switchIfEmpty(connections.next()).toFuture(); } catch (RuntimeException var6) { throw Exceptions.bubble(var6); } } } else { return this.getConnection(this.getMaster()); } }
rredisTemplate.execute方法在执行lua脚本时,使用的是redis的evalsha方法,默认算读操作(io.lettuce.core.masterslave.ReadOnlyCommands.isReadOnlyCommand方法中有体现),
所以被分配到了slave节点,而lua脚本一般读写并存操作,比如以上代码释放redis锁,而redis的slave节点不能进行write操作,所以报以下错误
// eval 和 evalsha是redis执行脚本的方法,
EVAL,EVALSHA
解决方案:
1、重写lettuce底层源码ReadOnlyCommands中CommandName枚举类,将 EVAL, EVALSHA从此类删除
- 找到你所要重写类,查看其中的路径;io.lettuce.core.masterslave.ReadOnlyCommands
- 在我们的 src 目录下新建一个同包名同类名的类;
- 将 jar 包中的重写方法所在类的所有代码复制到我们新建的同包名同类名的类中;
- 在CommandName枚举中删除EVAL, EVALSHA属性
- 程序会优先使用我们 src 下面的类,这样就覆盖了 jar 包的方法 。
2、升级lettuce-core到最新的版本,新版本已经解决。