From 7910819952e0583ab9572a9bc0ee41700006d44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Mon, 18 Aug 2025 16:00:44 +0800 Subject: [PATCH 01/10] Implemented read-write separation based on JedisSentineled --- pom.xml | 6 + .../jedis/DefaultJedisClientConfig.java | 18 ++ .../clients/jedis/JedisClientConfig.java | 18 ++ .../SentineledConnectionProvider.java | 214 ++++++++++++++++-- .../redis/clients/jedis/util/Commands.java | 57 +++++ .../SentineledConnectionProviderTest.java | 52 ++++- .../UnifiedJedisClientSideCacheTestBase.java | 3 +- 7 files changed, 346 insertions(+), 22 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/util/Commands.java diff --git a/pom.xml b/pom.xml index 402213f529..d0365160b2 100644 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,12 @@ ${resilience4j.version} true + + org.powermock + powermock-module-junit4 + 2.0.2 + test + io.github.resilience4j resilience4j-circuitbreaker diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java index 25a4737ec0..981242625f 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java @@ -33,6 +33,8 @@ public final class DefaultJedisClientConfig implements JedisClientConfig { private final AuthXManager authXManager; + private final boolean fallbackToMaster; + private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { this.redisProtocol = builder.redisProtocol; this.connectionTimeoutMillis = builder.connectionTimeoutMillis; @@ -50,6 +52,7 @@ private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { this.clientSetInfoConfig = builder.clientSetInfoConfig; this.readOnlyForRedisClusterReplicas = builder.readOnlyForRedisClusterReplicas; this.authXManager = builder.authXManager; + this.fallbackToMaster = builder.fallbackToMaster; } @Override @@ -143,6 +146,11 @@ public boolean isReadOnlyForRedisClusterReplicas() { return readOnlyForRedisClusterReplicas; } + @Override + public boolean isFallbackToMaster() { + return fallbackToMaster; + } + public static Builder builder() { return new Builder(); } @@ -175,6 +183,8 @@ public static class Builder { private AuthXManager authXManager = null; + private boolean fallbackToMaster = true; + private Builder() { } @@ -297,6 +307,11 @@ public Builder authXManager(AuthXManager authXManager) { return this; } + public Builder fallbackToMaster(boolean fallbackToMaster) { + this.fallbackToMaster = fallbackToMaster; + return this; + } + public Builder from(JedisClientConfig instance) { this.redisProtocol = instance.getRedisProtocol(); this.connectionTimeoutMillis = instance.getConnectionTimeoutMillis(); @@ -314,6 +329,7 @@ public Builder from(JedisClientConfig instance) { this.clientSetInfoConfig = instance.getClientSetInfoConfig(); this.readOnlyForRedisClusterReplicas = instance.isReadOnlyForRedisClusterReplicas(); this.authXManager = instance.getAuthXManager(); + this.fallbackToMaster = instance.isFallbackToMaster(); return this; } } @@ -376,6 +392,8 @@ public static DefaultJedisClientConfig copyConfig(JedisClientConfig copy) { builder.authXManager(copy.getAuthXManager()); + builder.fallbackToMaster(copy.isFallbackToMaster()); + return builder.build(); } } diff --git a/src/main/java/redis/clients/jedis/JedisClientConfig.java b/src/main/java/redis/clients/jedis/JedisClientConfig.java index ce7fd82de4..71f1671d74 100644 --- a/src/main/java/redis/clients/jedis/JedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java @@ -6,6 +6,7 @@ import javax.net.ssl.SSLSocketFactory; import redis.clients.jedis.authentication.AuthXManager; +import redis.clients.jedis.util.Commands; public interface JedisClientConfig { @@ -115,4 +116,21 @@ default boolean isReadOnlyForRedisClusterReplicas() { default ClientSetInfoConfig getClientSetInfoConfig() { return ClientSetInfoConfig.DEFAULT; } + + /** + * fallback when no replicas are healthy, default to master + * @return {@code true} - to execute command by master. {@code false} - throw exception. + */ + default boolean isFallbackToMaster() { + return true; + } + + /** + * check a Command is READONLY + * @param args + * @return + */ + default boolean isReadCommand(CommandArguments args) { + return Commands.ReadOnlyCommands.contains(args.getCommand()); + } } diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java index dedf34fb69..e9eb1643d6 100644 --- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; @@ -26,6 +27,15 @@ import redis.clients.jedis.util.IOUtils; public class SentineledConnectionProvider implements ConnectionProvider { + class PoolInfo { + public String host; + public ConnectionPool pool; + + public PoolInfo(String host, ConnectionPool pool) { + this.host = host; + this.pool = pool; + } + } private static final Logger LOG = LoggerFactory.getLogger(SentineledConnectionProvider.class); @@ -51,6 +61,10 @@ public class SentineledConnectionProvider implements ConnectionProvider { private final Lock initPoolLock = new ReentrantLock(true); + private final List slavePools = new ArrayList<>(); + + private int poolIndex; + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Set sentinels, final JedisClientConfig sentinelClientConfig) { this(masterName, masterClientConfig, null, null, sentinels, sentinelClientConfig); @@ -102,6 +116,35 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m initMaster(master); } + private Connection getSlaveResource() { + int startIdx; + synchronized (slavePools) { + poolIndex++; + if (poolIndex >= slavePools.size()) { + poolIndex = 0; + } + startIdx = poolIndex; + } + return _getSlaveResource(startIdx, 0); + } + + private Connection _getSlaveResource(int idx, int cnt) { + PoolInfo poolInfo; + synchronized (slavePools) { + if (cnt >= slavePools.size()) { + return null; + } + poolInfo = slavePools.get(idx % slavePools.size()); + } + try { + Connection jedis = poolInfo.pool.getResource(); + return jedis; + } catch (Exception e) { + LOG.error("get connection fail:", e); + return _getSlaveResource(idx + 1, cnt + 1); + } + } + @Override public Connection getConnection() { return pool.getResource(); @@ -109,6 +152,16 @@ public Connection getConnection() { @Override public Connection getConnection(CommandArguments args) { + boolean readCommand = masterClientConfig.isReadCommand(args); + if (readCommand) { + Connection slaveConn = getSlaveResource(); + if (slaveConn != null) { + return slaveConn; + } + if (!masterClientConfig.isFallbackToMaster()) { + throw new JedisException("can not get Connection, all slave is invalid"); + } + } return pool.getResource(); } @@ -117,6 +170,10 @@ public void close() { sentinelListeners.forEach(SentinelListener::shutdown); pool.close(); + + for (PoolInfo slavePool : slavePools) { + slavePool.pool.close(); + } } public HostAndPort getCurrentMaster() { @@ -167,6 +224,79 @@ private ConnectionPool createNodePool(HostAndPort master) { } } + private void initSlaves(List slaves) { + List removedSlavePools = new ArrayList<>(); + try { + synchronized (slavePools) { + Loop: + for (int i = slavePools.size()-1; i >= 0; i--) { + PoolInfo poolInfo = slavePools.get(i); + for (HostAndPort slave : slaves) { + String host = slave.toString(); + if (poolInfo.host.equals(host)) { + continue Loop; + } + } + removedSlavePools.add(slavePools.remove(i)); + } + + for (HostAndPort slave : slaves) { + addSlave(slave); + } + } + } finally { + if (!removedSlavePools.isEmpty() && clientSideCache != null) { + clientSideCache.flush(); + } + + for (PoolInfo removedSlavePool : removedSlavePools) { + removedSlavePool.pool.destroy(); + } + } + } + + private static boolean isHealthy(String flags) { + for (String flag : flags.split(",")) { + switch (flag.trim()) { + case "s_down": + case "o_down": + case "disconnected": + return false; + } + } + return true; + } + + private void addSlave(HostAndPort slave) { + String newSlaveHost = slave.toString(); + synchronized (this.slavePools) { + for (int i = 0; i < this.slavePools.size(); i++) { + PoolInfo poolInfo = this.slavePools.get(i); + if (poolInfo.host.equals(newSlaveHost)) { + return; + } + } + slavePools.add(new PoolInfo(newSlaveHost, createNodePool(slave))); + } + } + + private void removeSlave(HostAndPort slave) { + String newSlaveHost = slave.toString(); + PoolInfo removed = null; + synchronized (this.slavePools) { + for (int i = 0; i < this.slavePools.size(); i++) { + PoolInfo poolInfo = this.slavePools.get(i); + if (poolInfo.host.equals(newSlaveHost)) { + removed = slavePools.remove(i); + break; + } + } + } + if (removed != null) { + removed.pool.destroy(); + } + } + private HostAndPort initSentinels(Set sentinels) { HostAndPort master = null; @@ -262,6 +392,24 @@ public void run() { sentinelJedis = new Jedis(node, sentinelClientConfig); + List> slaveInfos = sentinelJedis.sentinelSlaves(masterName); + + List slaves = new ArrayList<>(); + + for (int i = 0; i < slaveInfos.size(); i++) { + Map slaveInfo = slaveInfos.get(i); + String flags = slaveInfo.get("flags"); + if (flags == null || !isHealthy(flags)) { + continue; + } + String ip = slaveInfo.get("ip"); + int port = Integer.parseInt(slaveInfo.get("port")); + HostAndPort slave = new HostAndPort(ip, port); + slaves.add(slave); + } + + initSlaves(slaves); + // code for active refresh List masterAddr = sentinelJedis.sentinelGetMasterAddrByName(masterName); if (masterAddr == null || masterAddr.size() != 2) { @@ -275,24 +423,58 @@ public void run() { public void onMessage(String channel, String message) { LOG.debug("Sentinel {} published: {}.", node, message); - String[] switchMasterMsg = message.split(" "); - - if (switchMasterMsg.length > 3) { - - if (masterName.equals(switchMasterMsg[0])) { - initMaster(toHostAndPort(switchMasterMsg[3], switchMasterMsg[4])); - } else { - LOG.debug( - "Ignoring message on +switch-master for master {}. Our master is {}.", - switchMasterMsg[0], masterName); - } - - } else { - LOG.error("Invalid message received on sentinel {} on channel +switch-master: {}.", - node, message); + String[] switchMsg = message.split(" "); + String slaveIp; + int slavePort; + switch (channel) { + case "+switch-master": + if (switchMsg.length > 3) { + if (masterName.equals(switchMsg[0])) { + initMaster(toHostAndPort(switchMsg[3], switchMsg[4])); + } else { + LOG.debug( + "Ignoring message on +switch-master for master {}. Our master is {}.", + switchMsg[0], masterName); + } + } else { + LOG.error("Invalid message received on sentinel {} on channel +switch-master: {}.", + node, message); + } + break; + case "+sdown": + if (switchMsg[0].equals("master")) { + return; + } + if (!masterName.equals(switchMsg[5])) { + return; + } + slaveIp = switchMsg[2]; + slavePort = Integer.parseInt(switchMsg[3]); + removeSlave(new HostAndPort(slaveIp, slavePort)); + break; + case "-sdown": + if (!masterName.equals(switchMsg[5])) { + return; + } + slaveIp = switchMsg[2]; + slavePort = Integer.parseInt(switchMsg[3]); + addSlave(new HostAndPort(slaveIp, slavePort)); + break; + case "+slave": + if (!masterName.equals(switchMsg[5])) { + return; + } + slaveIp = switchMsg[2]; + slavePort = Integer.parseInt(switchMsg[3]); + addSlave(new HostAndPort(slaveIp, slavePort)); + + String masterIp = switchMsg[6]; + int masterPort = Integer.parseInt(switchMsg[7]); + removeSlave(new HostAndPort(masterIp, masterPort)); + break; } } - }, "+switch-master"); + }, "+switch-master", "+sdown", "-sdown", "+slave"); } catch (JedisException e) { diff --git a/src/main/java/redis/clients/jedis/util/Commands.java b/src/main/java/redis/clients/jedis/util/Commands.java new file mode 100644 index 0000000000..4927dc2165 --- /dev/null +++ b/src/main/java/redis/clients/jedis/util/Commands.java @@ -0,0 +1,57 @@ +package redis.clients.jedis.util; + + +import redis.clients.jedis.Protocol.Command; +import redis.clients.jedis.bloom.RedisBloomProtocol.BloomFilterCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.CountMinSketchCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.CuckooFilterCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.TDigestCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.TopKCommand; +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.json.JsonProtocol.JsonCommand; +import redis.clients.jedis.search.SearchProtocol; +import redis.clients.jedis.search.SearchProtocol.SearchCommand; +import redis.clients.jedis.timeseries.TimeSeriesProtocol; +import redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesCommand; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class Commands { + + public static final Set ReadOnlyCommands = new HashSet(Arrays.asList( + Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, + Command.DBSIZE, Command.SELECT, Command.ECHO, Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, + Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, // <-- string + Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, // <-- bit (string) + Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, Command.HGETALL, Command.HSTRLEN, + Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, Command.HPEXPIRETIME, Command.HRANDFIELD, // <-- hash + Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, // <-- list + Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, Command.SDIFF, Command.SISMEMBER, + Command.SMISMEMBER, Command.SINTERCARD, // <-- set + Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, + Command.ZCOUNT, Command.ZUNION, Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, Command.ZRANGEBYLEX, + Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, // <-- zset + Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, Command.GEORADIUSBYMEMBER_RO, // <-- geo + Command.PFCOUNT, // <-- hyper log log + Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, Command.XPENDING, Command.XINFO, // <-- stream + Command.FCALL_RO, // <-- program + Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, Command.SSCAN, Command.ZSCAN, + Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, + Command.VGETATTR, Command.VINFO, // <-- vector set + BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, BloomFilterCommand.INFO, // <-- BloomFilterCommand + CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, CuckooFilterCommand.INFO, // <-- CuckooFilterCommand + CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, // <-- CountMinSketchCommand + TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, // <-- TopKCommand + TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, TDigestCommand.MAX, + TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, // <-- TDigestCommand + JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, JsonCommand.ARRINDEX, JsonCommand.ARRLEN, + JsonCommand.OBJKEYS, JsonCommand.OBJLEN, JsonCommand.DEBUG, JsonCommand.RESP, // <-- JsonCommand + SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, SearchCommand.AGGREGATE, + SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, SearchCommand.SUGLEN, SearchCommand.DICTDUMP, + SearchCommand.SPELLCHECK, SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, // <-- SearchCommand + TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, TimeSeriesCommand.MREVRANGE, + TimeSeriesCommand.INFO, TimeSeriesCommand.GET, TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX // <-- TimeSeriesCommand + )); +} diff --git a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java index f15f2095e5..9533c8ce00 100644 --- a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java +++ b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java @@ -1,5 +1,6 @@ package redis.clients.jedis; +import java.util.ArrayList; import java.util.HashSet; import java.util.Set; @@ -7,6 +8,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.providers.SentineledConnectionProvider; @@ -19,6 +24,8 @@ /** * @see JedisSentinelPoolTest */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({SentineledConnectionProvider.class}) public class SentineledConnectionProviderTest { private static final String MASTER_NAME = "mymaster"; @@ -28,6 +35,8 @@ public class SentineledConnectionProviderTest { protected Set sentinels = new HashSet<>(); + protected String password = "foobared"; + @BeforeEach public void setUp() throws Exception { sentinels.clear(); @@ -41,7 +50,7 @@ public void repeatedSentinelPoolInitialization() { for (int i = 0; i < 20; ++i) { try (SentineledConnectionProvider provider = new SentineledConnectionProvider(MASTER_NAME, - DefaultJedisClientConfig.builder().timeoutMillis(1000).password("foobared").database(2).build(), + DefaultJedisClientConfig.builder().timeoutMillis(1000).password(password).database(2).build(), sentinels, DefaultJedisClientConfig.builder().build())) { provider.getConnection().close(); @@ -76,7 +85,7 @@ public void checkCloseableConnections() throws Exception { GenericObjectPoolConfig config = new GenericObjectPoolConfig<>(); try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, - DefaultJedisClientConfig.builder().timeoutMillis(1000).password("foobared").database(2).build(), + DefaultJedisClientConfig.builder().timeoutMillis(1000).password(password).database(2).build(), config, sentinels, DefaultJedisClientConfig.builder().build())) { assertSame(SentineledConnectionProvider.class, jedis.provider.getClass()); jedis.set("foo", "bar"); @@ -91,7 +100,7 @@ public void checkResourceIsCloseable() { config.setBlockWhenExhausted(false); try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, - DefaultJedisClientConfig.builder().timeoutMillis(1000).password("foobared").database(2).build(), + DefaultJedisClientConfig.builder().timeoutMillis(1000).password(password).database(2).build(), config, sentinels, DefaultJedisClientConfig.builder().build())) { Connection conn = jedis.provider.getConnection(); @@ -113,7 +122,7 @@ public void checkResourceIsCloseable() { @Test public void testResetInvalidPassword() { DefaultRedisCredentialsProvider credentialsProvider - = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, "foobared")); + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) @@ -154,7 +163,7 @@ public void testResetValidPassword() { fail("Should not get resource from pool"); } catch (JedisException e) { } - credentialsProvider.setCredentials(new DefaultRedisCredentials(null, "foobared")); + credentialsProvider.setCredentials(new DefaultRedisCredentials(null, password)); try (Connection conn2 = jedis.provider.getConnection()) { new Jedis(conn2).set("foo", "bar"); @@ -162,4 +171,37 @@ public void testResetValidPassword() { } } } + + @Test + public void testReadWriteSeparation() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build())) { + + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertEquals("bar", jedis.get("foo")); + } + } + + @Test + public void testFallbackToMasterIsFalseAndNoSlave() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").fallbackToMaster(false).build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build())) { + + Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertThrows(JedisException.class, () -> jedis.get("foo")); + } + } } diff --git a/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java b/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java index fa4043799e..87e7940471 100644 --- a/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java +++ b/src/test/java/redis/clients/jedis/csc/UnifiedJedisClientSideCacheTestBase.java @@ -145,7 +145,7 @@ public void immutableCacheEntriesTest() { } @Test - public void invalidationTest() { + public void invalidationTest() throws InterruptedException { try (UnifiedJedis jedis = createCachedJedis(CacheConfig.builder().build())) { Cache cache = jedis.getCache(); jedis.set("{csc}1", "one"); @@ -161,6 +161,7 @@ public void invalidationTest() { assertEquals(0, cache.getStats().getInvalidationCount()); jedis.set("{csc}1", "new-one"); + Thread.sleep(1000); List reply2 = jedis.mget("{csc}1", "{csc}2", "{csc}3"); assertEquals(Arrays.asList("new-one", "two", "three"), reply2); From 610aac49862ce5fd3dca437eb4dc9f9cde9ca86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Mon, 25 Aug 2025 14:16:49 +0800 Subject: [PATCH 02/10] test ut --- .../redis/clients/jedis/util/Commands.java | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/Commands.java b/src/main/java/redis/clients/jedis/util/Commands.java index 4927dc2165..ac594911cf 100644 --- a/src/main/java/redis/clients/jedis/util/Commands.java +++ b/src/main/java/redis/clients/jedis/util/Commands.java @@ -9,9 +9,7 @@ import redis.clients.jedis.bloom.RedisBloomProtocol.TopKCommand; import redis.clients.jedis.commands.ProtocolCommand; import redis.clients.jedis.json.JsonProtocol.JsonCommand; -import redis.clients.jedis.search.SearchProtocol; import redis.clients.jedis.search.SearchProtocol.SearchCommand; -import redis.clients.jedis.timeseries.TimeSeriesProtocol; import redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesCommand; import java.util.Arrays; @@ -20,38 +18,24 @@ public class Commands { - public static final Set ReadOnlyCommands = new HashSet(Arrays.asList( - Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, - Command.DBSIZE, Command.SELECT, Command.ECHO, Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, - Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, // <-- string + public static final Set ReadOnlyCommands = new HashSet(Arrays.asList(Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, // <-- string Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, // <-- bit (string) - Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, Command.HGETALL, Command.HSTRLEN, - Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, Command.HPEXPIRETIME, Command.HRANDFIELD, // <-- hash + Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, Command.HPEXPIRETIME, Command.HRANDFIELD, // <-- hash Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, // <-- list - Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, Command.SDIFF, Command.SISMEMBER, - Command.SMISMEMBER, Command.SINTERCARD, // <-- set - Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, - Command.ZCOUNT, Command.ZUNION, Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, Command.ZRANGEBYLEX, - Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, // <-- zset + Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, // <-- set + Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, // <-- zset Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, Command.GEORADIUSBYMEMBER_RO, // <-- geo Command.PFCOUNT, // <-- hyper log log Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, Command.XPENDING, Command.XINFO, // <-- stream Command.FCALL_RO, // <-- program - Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, Command.SSCAN, Command.ZSCAN, - Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, - Command.VGETATTR, Command.VINFO, // <-- vector set + Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, // <-- vector set BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, BloomFilterCommand.INFO, // <-- BloomFilterCommand CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, CuckooFilterCommand.INFO, // <-- CuckooFilterCommand CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, // <-- CountMinSketchCommand TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, // <-- TopKCommand - TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, TDigestCommand.MAX, - TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, // <-- TDigestCommand - JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, JsonCommand.ARRINDEX, JsonCommand.ARRLEN, - JsonCommand.OBJKEYS, JsonCommand.OBJLEN, JsonCommand.DEBUG, JsonCommand.RESP, // <-- JsonCommand - SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, SearchCommand.AGGREGATE, - SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, SearchCommand.SUGLEN, SearchCommand.DICTDUMP, - SearchCommand.SPELLCHECK, SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, // <-- SearchCommand - TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, TimeSeriesCommand.MREVRANGE, - TimeSeriesCommand.INFO, TimeSeriesCommand.GET, TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX // <-- TimeSeriesCommand - )); + TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, // <-- TDigestCommand + JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, JsonCommand.DEBUG, JsonCommand.RESP, // <-- JsonCommand + SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, // <-- SearchCommand + TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX // <-- TimeSeriesCommand + )); } From d1b9d0b06e6ac18d78a072130ae8e5b00ea0b65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Mon, 8 Sep 2025 13:31:34 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat(read):=20=E5=A2=9E=E5=8A=A0=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E7=AD=96=E7=95=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ReadFrom 枚举类,用于配置读取策略 - 在 JedisSentineled 和 SentineledConnectionProvider 中添加读取策略相关代码 - 修改 DefaultJedisClientConfig,移除不必要的 fallbackToMaster配置 - 重构 readOnlyCommands 判断逻辑,提高可维护性 --- .../jedis/DefaultJedisClientConfig.java | 18 --- .../clients/jedis/JedisClientConfig.java | 20 +-- .../redis/clients/jedis/JedisSentineled.java | 16 +++ .../java/redis/clients/jedis/Protocol.java | 2 +- .../java/redis/clients/jedis/ReadFrom.java | 12 ++ .../SentineledConnectionProvider.java | 130 ++++++++++++++---- .../redis/clients/jedis/util/Commands.java | 41 ------ .../clients/jedis/util/ReadOnlyCommands.java | 62 +++++++++ .../redis/clients/jedis/HostAndPorts.java | 4 +- .../SentineledConnectionProviderTest.java | 27 +++- 10 files changed, 217 insertions(+), 115 deletions(-) create mode 100644 src/main/java/redis/clients/jedis/ReadFrom.java delete mode 100644 src/main/java/redis/clients/jedis/util/Commands.java create mode 100644 src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java index 981242625f..25a4737ec0 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java @@ -33,8 +33,6 @@ public final class DefaultJedisClientConfig implements JedisClientConfig { private final AuthXManager authXManager; - private final boolean fallbackToMaster; - private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { this.redisProtocol = builder.redisProtocol; this.connectionTimeoutMillis = builder.connectionTimeoutMillis; @@ -52,7 +50,6 @@ private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { this.clientSetInfoConfig = builder.clientSetInfoConfig; this.readOnlyForRedisClusterReplicas = builder.readOnlyForRedisClusterReplicas; this.authXManager = builder.authXManager; - this.fallbackToMaster = builder.fallbackToMaster; } @Override @@ -146,11 +143,6 @@ public boolean isReadOnlyForRedisClusterReplicas() { return readOnlyForRedisClusterReplicas; } - @Override - public boolean isFallbackToMaster() { - return fallbackToMaster; - } - public static Builder builder() { return new Builder(); } @@ -183,8 +175,6 @@ public static class Builder { private AuthXManager authXManager = null; - private boolean fallbackToMaster = true; - private Builder() { } @@ -307,11 +297,6 @@ public Builder authXManager(AuthXManager authXManager) { return this; } - public Builder fallbackToMaster(boolean fallbackToMaster) { - this.fallbackToMaster = fallbackToMaster; - return this; - } - public Builder from(JedisClientConfig instance) { this.redisProtocol = instance.getRedisProtocol(); this.connectionTimeoutMillis = instance.getConnectionTimeoutMillis(); @@ -329,7 +314,6 @@ public Builder from(JedisClientConfig instance) { this.clientSetInfoConfig = instance.getClientSetInfoConfig(); this.readOnlyForRedisClusterReplicas = instance.isReadOnlyForRedisClusterReplicas(); this.authXManager = instance.getAuthXManager(); - this.fallbackToMaster = instance.isFallbackToMaster(); return this; } } @@ -392,8 +376,6 @@ public static DefaultJedisClientConfig copyConfig(JedisClientConfig copy) { builder.authXManager(copy.getAuthXManager()); - builder.fallbackToMaster(copy.isFallbackToMaster()); - return builder.build(); } } diff --git a/src/main/java/redis/clients/jedis/JedisClientConfig.java b/src/main/java/redis/clients/jedis/JedisClientConfig.java index 71f1671d74..5548b36b64 100644 --- a/src/main/java/redis/clients/jedis/JedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java @@ -6,10 +6,9 @@ import javax.net.ssl.SSLSocketFactory; import redis.clients.jedis.authentication.AuthXManager; -import redis.clients.jedis.util.Commands; +import redis.clients.jedis.util.ReadOnlyCommands; public interface JedisClientConfig { - default RedisProtocol getRedisProtocol() { return null; } @@ -116,21 +115,4 @@ default boolean isReadOnlyForRedisClusterReplicas() { default ClientSetInfoConfig getClientSetInfoConfig() { return ClientSetInfoConfig.DEFAULT; } - - /** - * fallback when no replicas are healthy, default to master - * @return {@code true} - to execute command by master. {@code false} - throw exception. - */ - default boolean isFallbackToMaster() { - return true; - } - - /** - * check a Command is READONLY - * @param args - * @return - */ - default boolean isReadCommand(CommandArguments args) { - return Commands.ReadOnlyCommands.contains(args.getCommand()); - } } diff --git a/src/main/java/redis/clients/jedis/JedisSentineled.java b/src/main/java/redis/clients/jedis/JedisSentineled.java index 26f208a03b..147ac351b5 100644 --- a/src/main/java/redis/clients/jedis/JedisSentineled.java +++ b/src/main/java/redis/clients/jedis/JedisSentineled.java @@ -7,6 +7,7 @@ import redis.clients.jedis.csc.CacheConfig; import redis.clients.jedis.csc.CacheFactory; import redis.clients.jedis.providers.SentineledConnectionProvider; +import redis.clients.jedis.util.ReadOnlyCommands; public class JedisSentineled extends UnifiedJedis { @@ -37,6 +38,21 @@ public JedisSentineled(String masterName, final JedisClientConfig masterClientCo masterClientConfig.getRedisProtocol()); } + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom) { + super(new SentineledConnectionProvider(masterName, masterClientConfig, poolConfig, sentinels, sentinelClientConfig, readFrom), + masterClientConfig.getRedisProtocol()); + } + + public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom, + ReadOnlyCommands.ReadOnlyPredicate readOnlyPredicate) { + super(new SentineledConnectionProvider(masterName, masterClientConfig, poolConfig, sentinels, sentinelClientConfig, readFrom, readOnlyPredicate), + masterClientConfig.getRedisProtocol()); + } + @Experimental public JedisSentineled(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, final GenericObjectPoolConfig poolConfig, diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index daa7a3c917..f42025f304 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -23,7 +23,7 @@ public final class Protocol { public static final String DEFAULT_HOST = "127.0.0.1"; public static final int DEFAULT_PORT = 6379; - public static final int DEFAULT_SENTINEL_PORT = 26379; + public static final int DEFAULT_SENTINEL_PORT = 26378; public static final int DEFAULT_TIMEOUT = 2000; public static final int DEFAULT_DATABASE = 0; public static final int CLUSTER_HASHSLOTS = 16384; diff --git a/src/main/java/redis/clients/jedis/ReadFrom.java b/src/main/java/redis/clients/jedis/ReadFrom.java new file mode 100644 index 0000000000..12fc232bab --- /dev/null +++ b/src/main/java/redis/clients/jedis/ReadFrom.java @@ -0,0 +1,12 @@ +package redis.clients.jedis; + +public enum ReadFrom { + // read from the upstream only. + UPSTREAM, + // read from the replica only. + REPLICA, + // read preferred from the upstream and fall back to a replica if the upstream is not available. + UPSTREAM_PREFERRED, + // read preferred from replica and fall back to upstream if no replica is not available. + REPLICA_PREFERRED +} diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java index e9eb1643d6..31c07af4a6 100644 --- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java @@ -20,11 +20,13 @@ import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.ReadFrom; import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.csc.Cache; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.util.IOUtils; +import redis.clients.jedis.util.ReadOnlyCommands; public class SentineledConnectionProvider implements ConnectionProvider { class PoolInfo { @@ -59,10 +61,16 @@ public PoolInfo(String host, ConnectionPool pool) { private final long subscribeRetryWaitTimeMillis; + private final ReadFrom readFrom; + + private ReadOnlyCommands.ReadOnlyPredicate READ_ONLY_COMMANDS; + private final Lock initPoolLock = new ReentrantLock(true); private final List slavePools = new ArrayList<>(); + private final Lock slavePoolsLock = new ReentrantLock(true); + private int poolIndex; public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, @@ -83,26 +91,41 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS); } + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom) { + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, readFrom, ReadOnlyCommands.asPredicate()); + } + + public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, + final GenericObjectPoolConfig poolConfig, + Set sentinels, final JedisClientConfig sentinelClientConfig, ReadFrom readFrom, + ReadOnlyCommands.ReadOnlyPredicate readOnlyPredicate) { + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, readFrom, readOnlyPredicate); + } + @Experimental public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig) { this(masterName, masterClientConfig, clientSideCache, poolConfig, sentinels, sentinelClientConfig, - DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS); + DEFAULT_SUBSCRIBE_RETRY_WAIT_TIME_MILLIS, ReadFrom.UPSTREAM, ReadOnlyCommands.asPredicate()); } public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig, final long subscribeRetryWaitTimeMillis) { - this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, subscribeRetryWaitTimeMillis); + this(masterName, masterClientConfig, null, poolConfig, sentinels, sentinelClientConfig, subscribeRetryWaitTimeMillis, ReadFrom.UPSTREAM, ReadOnlyCommands.asPredicate()); } @Experimental public SentineledConnectionProvider(String masterName, final JedisClientConfig masterClientConfig, Cache clientSideCache, final GenericObjectPoolConfig poolConfig, Set sentinels, final JedisClientConfig sentinelClientConfig, - final long subscribeRetryWaitTimeMillis) { + final long subscribeRetryWaitTimeMillis, ReadFrom readFrom, ReadOnlyCommands.ReadOnlyPredicate readOnlyPredicate) { this.masterName = masterName; this.masterClientConfig = masterClientConfig; @@ -111,6 +134,8 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m this.sentinelClientConfig = sentinelClientConfig; this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis; + this.readFrom = readFrom; + this.READ_ONLY_COMMANDS = readOnlyPredicate; HostAndPort master = initSentinels(sentinels); initMaster(master); @@ -118,24 +143,31 @@ public SentineledConnectionProvider(String masterName, final JedisClientConfig m private Connection getSlaveResource() { int startIdx; - synchronized (slavePools) { + slavePoolsLock.lock(); + try { poolIndex++; if (poolIndex >= slavePools.size()) { poolIndex = 0; } startIdx = poolIndex; + } finally { + slavePoolsLock.unlock(); } return _getSlaveResource(startIdx, 0); } private Connection _getSlaveResource(int idx, int cnt) { PoolInfo poolInfo; - synchronized (slavePools) { + slavePoolsLock.lock(); + try { if (cnt >= slavePools.size()) { return null; } poolInfo = slavePools.get(idx % slavePools.size()); + } finally { + slavePoolsLock.unlock(); } + try { Connection jedis = poolInfo.pool.getResource(); return jedis; @@ -152,17 +184,43 @@ public Connection getConnection() { @Override public Connection getConnection(CommandArguments args) { - boolean readCommand = masterClientConfig.isReadCommand(args); - if (readCommand) { - Connection slaveConn = getSlaveResource(); - if (slaveConn != null) { - return slaveConn; - } - if (!masterClientConfig.isFallbackToMaster()) { - throw new JedisException("can not get Connection, all slave is invalid"); - } + boolean isReadCommand = READ_ONLY_COMMANDS.isReadOnly(args); + if (!isReadCommand) { + return pool.getResource(); + } + + Connection conn; + switch (readFrom) { + case REPLICA: + conn = getSlaveResource(); + if (conn == null) { + throw new JedisException("all replica is invalid"); + } + return conn; + case UPSTREAM_PREFERRED: + try { + conn = pool.getResource(); + if (conn != null) { + return conn; + } + } catch (Exception e) { + LOG.error("get master connection error", e); + } + + conn = getSlaveResource(); + if (conn == null) { + throw new JedisException("all redis instance is invalid"); + } + return conn; + case REPLICA_PREFERRED: + conn = getSlaveResource(); + if (conn != null) { + return conn; + } + return pool.getResource(); + default: + return pool.getResource(); } - return pool.getResource(); } @Override @@ -226,25 +284,28 @@ private ConnectionPool createNodePool(HostAndPort master) { private void initSlaves(List slaves) { List removedSlavePools = new ArrayList<>(); + slavePoolsLock.lock(); try { - synchronized (slavePools) { - Loop: - for (int i = slavePools.size()-1; i >= 0; i--) { - PoolInfo poolInfo = slavePools.get(i); - for (HostAndPort slave : slaves) { - String host = slave.toString(); - if (poolInfo.host.equals(host)) { - continue Loop; - } + for (int i = slavePools.size()-1; i >= 0; i--) { + PoolInfo poolInfo = slavePools.get(i); + boolean found = false; + for (HostAndPort slave : slaves) { + String host = slave.toString(); + if (poolInfo.host.equals(host)) { + found = true; + break; } + } + if (!found) { removedSlavePools.add(slavePools.remove(i)); } + } - for (HostAndPort slave : slaves) { - addSlave(slave); - } + for (HostAndPort slave : slaves) { + addSlave(slave); } } finally { + slavePoolsLock.unlock(); if (!removedSlavePools.isEmpty() && clientSideCache != null) { clientSideCache.flush(); } @@ -269,7 +330,8 @@ private static boolean isHealthy(String flags) { private void addSlave(HostAndPort slave) { String newSlaveHost = slave.toString(); - synchronized (this.slavePools) { + slavePoolsLock.lock(); + try { for (int i = 0; i < this.slavePools.size(); i++) { PoolInfo poolInfo = this.slavePools.get(i); if (poolInfo.host.equals(newSlaveHost)) { @@ -277,13 +339,16 @@ private void addSlave(HostAndPort slave) { } } slavePools.add(new PoolInfo(newSlaveHost, createNodePool(slave))); + } finally { + slavePoolsLock.unlock(); } } private void removeSlave(HostAndPort slave) { String newSlaveHost = slave.toString(); PoolInfo removed = null; - synchronized (this.slavePools) { + slavePoolsLock.lock(); + try { for (int i = 0; i < this.slavePools.size(); i++) { PoolInfo poolInfo = this.slavePools.get(i); if (poolInfo.host.equals(newSlaveHost)) { @@ -291,6 +356,8 @@ private void removeSlave(HostAndPort slave) { break; } } + } finally { + slavePoolsLock.unlock(); } if (removed != null) { removed.pool.destroy(); @@ -421,7 +488,7 @@ public void run() { sentinelJedis.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { - LOG.debug("Sentinel {} published: {}.", node, message); + LOG.debug("Sentinel {} with channel {} published: {}.", node, channel, message); String[] switchMsg = message.split(" "); String slaveIp; @@ -453,6 +520,9 @@ public void onMessage(String channel, String message) { removeSlave(new HostAndPort(slaveIp, slavePort)); break; case "-sdown": + if (switchMsg.length < 5) { + return; + } if (!masterName.equals(switchMsg[5])) { return; } diff --git a/src/main/java/redis/clients/jedis/util/Commands.java b/src/main/java/redis/clients/jedis/util/Commands.java deleted file mode 100644 index ac594911cf..0000000000 --- a/src/main/java/redis/clients/jedis/util/Commands.java +++ /dev/null @@ -1,41 +0,0 @@ -package redis.clients.jedis.util; - - -import redis.clients.jedis.Protocol.Command; -import redis.clients.jedis.bloom.RedisBloomProtocol.BloomFilterCommand; -import redis.clients.jedis.bloom.RedisBloomProtocol.CountMinSketchCommand; -import redis.clients.jedis.bloom.RedisBloomProtocol.CuckooFilterCommand; -import redis.clients.jedis.bloom.RedisBloomProtocol.TDigestCommand; -import redis.clients.jedis.bloom.RedisBloomProtocol.TopKCommand; -import redis.clients.jedis.commands.ProtocolCommand; -import redis.clients.jedis.json.JsonProtocol.JsonCommand; -import redis.clients.jedis.search.SearchProtocol.SearchCommand; -import redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesCommand; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -public class Commands { - - public static final Set ReadOnlyCommands = new HashSet(Arrays.asList(Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, // <-- string - Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, // <-- bit (string) - Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, Command.HPEXPIRETIME, Command.HRANDFIELD, // <-- hash - Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, // <-- list - Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, // <-- set - Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, // <-- zset - Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, Command.GEORADIUSBYMEMBER_RO, // <-- geo - Command.PFCOUNT, // <-- hyper log log - Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, Command.XPENDING, Command.XINFO, // <-- stream - Command.FCALL_RO, // <-- program - Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, // <-- vector set - BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, BloomFilterCommand.INFO, // <-- BloomFilterCommand - CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, CuckooFilterCommand.INFO, // <-- CuckooFilterCommand - CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, // <-- CountMinSketchCommand - TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, // <-- TopKCommand - TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, // <-- TDigestCommand - JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, JsonCommand.DEBUG, JsonCommand.RESP, // <-- JsonCommand - SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, // <-- SearchCommand - TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX // <-- TimeSeriesCommand - )); -} diff --git a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java new file mode 100644 index 0000000000..51db381501 --- /dev/null +++ b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java @@ -0,0 +1,62 @@ +package redis.clients.jedis.util; + + +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.Protocol.Command; +import redis.clients.jedis.bloom.RedisBloomProtocol.BloomFilterCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.CountMinSketchCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.CuckooFilterCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.TDigestCommand; +import redis.clients.jedis.bloom.RedisBloomProtocol.TopKCommand; +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.json.JsonProtocol.JsonCommand; +import redis.clients.jedis.search.SearchProtocol.SearchCommand; +import redis.clients.jedis.timeseries.TimeSeriesProtocol.TimeSeriesCommand; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class ReadOnlyCommands { + + private static final ReadOnlyPredicate PREDICATE = command -> isReadOnlyCommand(command); + + private static final Set READ_ONLY_COMMANDS = new HashSet(Arrays.asList(Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, // <-- string + Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, // <-- bit (string) + Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, Command.HPEXPIRETIME, Command.HRANDFIELD, // <-- hash + Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, // <-- list + Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, // <-- set + Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, // <-- zset + Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, Command.GEORADIUSBYMEMBER_RO, // <-- geo + Command.PFCOUNT, // <-- hyper log log + Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, Command.XPENDING, Command.XINFO, // <-- stream + Command.FCALL_RO, // <-- program + Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, // <-- vector set + BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, BloomFilterCommand.INFO, // <-- BloomFilterCommand + CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, CuckooFilterCommand.INFO, // <-- CuckooFilterCommand + CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, // <-- CountMinSketchCommand + TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, // <-- TopKCommand + TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, // <-- TDigestCommand + JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, JsonCommand.DEBUG, JsonCommand.RESP, // <-- JsonCommand + SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, // <-- SearchCommand + TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX // <-- TimeSeriesCommand + )); + + public static ReadOnlyPredicate asPredicate() { + return PREDICATE; + } + + public static boolean isReadOnlyCommand(CommandArguments args) { + return READ_ONLY_COMMANDS.contains(args.getCommand()); + } + + @FunctionalInterface + public interface ReadOnlyPredicate { + + /** + * @param command the input command. + * @return {@code true} if the input argument matches the predicate, otherwise {@code false} + */ + boolean isReadOnly(CommandArguments command); + } +} diff --git a/src/test/java/redis/clients/jedis/HostAndPorts.java b/src/test/java/redis/clients/jedis/HostAndPorts.java index 2a1fc5581d..3ab17de37e 100644 --- a/src/test/java/redis/clients/jedis/HostAndPorts.java +++ b/src/test/java/redis/clients/jedis/HostAndPorts.java @@ -21,9 +21,9 @@ public final class HostAndPorts { } sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT)); - sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 1)); + sentinelHostAndPortList.add(new HostAndPort("10.148.17.43", Protocol.DEFAULT_SENTINEL_PORT + 1)); sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 2)); - sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 3)); + sentinelHostAndPortList.add(new HostAndPort("10.148.17.43", Protocol.DEFAULT_SENTINEL_PORT + 3)); sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 4)); clusterHostAndPortList.add(new HostAndPort("localhost", 7379)); diff --git a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java index 9533c8ce00..ca2f0a3cd9 100644 --- a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java +++ b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java @@ -16,6 +16,7 @@ import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.providers.SentineledConnectionProvider; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -35,7 +36,8 @@ public class SentineledConnectionProviderTest { protected Set sentinels = new HashSet<>(); - protected String password = "foobared"; + protected String password = "0a2eb141353cf115"; +// protected String password = "foobared"; @BeforeEach public void setUp() throws Exception { @@ -189,14 +191,14 @@ public void testReadWriteSeparation() throws InterruptedException { } @Test - public void testFallbackToMasterIsFalseAndNoSlave() throws InterruptedException { + public void testReadFromREPLICAAndNoSlave() throws InterruptedException { DefaultRedisCredentialsProvider credentialsProvider = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) - .clientName("my_shiny_client_name").fallbackToMaster(false).build(), new ConnectionPoolConfig(), - sentinels, DefaultJedisClientConfig.builder().build())) { + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA)) { Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); jedis.set("foo", "bar"); @@ -204,4 +206,21 @@ public void testFallbackToMasterIsFalseAndNoSlave() throws InterruptedException assertThrows(JedisException.class, () -> jedis.get("foo")); } } + + @Test + public void testFallbackTOMasterWhenNOSlave() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA_PREFERRED)) { + + Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertDoesNotThrow(() -> jedis.get("foo")); + } + } } From 5063c11ad879088ecf1d8f22fa769f9ebbe17a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Mon, 8 Sep 2025 13:32:29 +0800 Subject: [PATCH 04/10] Implemented read-write separation based on JedisSentineled --- src/main/java/redis/clients/jedis/Protocol.java | 2 +- src/test/java/redis/clients/jedis/HostAndPorts.java | 4 ++-- .../redis/clients/jedis/SentineledConnectionProviderTest.java | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index f42025f304..daa7a3c917 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -23,7 +23,7 @@ public final class Protocol { public static final String DEFAULT_HOST = "127.0.0.1"; public static final int DEFAULT_PORT = 6379; - public static final int DEFAULT_SENTINEL_PORT = 26378; + public static final int DEFAULT_SENTINEL_PORT = 26379; public static final int DEFAULT_TIMEOUT = 2000; public static final int DEFAULT_DATABASE = 0; public static final int CLUSTER_HASHSLOTS = 16384; diff --git a/src/test/java/redis/clients/jedis/HostAndPorts.java b/src/test/java/redis/clients/jedis/HostAndPorts.java index 3ab17de37e..2a1fc5581d 100644 --- a/src/test/java/redis/clients/jedis/HostAndPorts.java +++ b/src/test/java/redis/clients/jedis/HostAndPorts.java @@ -21,9 +21,9 @@ public final class HostAndPorts { } sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT)); - sentinelHostAndPortList.add(new HostAndPort("10.148.17.43", Protocol.DEFAULT_SENTINEL_PORT + 1)); + sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 1)); sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 2)); - sentinelHostAndPortList.add(new HostAndPort("10.148.17.43", Protocol.DEFAULT_SENTINEL_PORT + 3)); + sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 3)); sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 4)); clusterHostAndPortList.add(new HostAndPort("localhost", 7379)); diff --git a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java index ca2f0a3cd9..cf41ed10b3 100644 --- a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java +++ b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java @@ -36,8 +36,7 @@ public class SentineledConnectionProviderTest { protected Set sentinels = new HashSet<>(); - protected String password = "0a2eb141353cf115"; -// protected String password = "foobared"; + protected String password = "foobared"; @BeforeEach public void setUp() throws Exception { From 9c0578debd60f97f7df5be6ddc13825f37739422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Mon, 8 Sep 2025 13:46:15 +0800 Subject: [PATCH 05/10] add ut --- .../SentineledConnectionProviderTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java index cf41ed10b3..79df1b6e03 100644 --- a/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java +++ b/src/test/java/redis/clients/jedis/SentineledConnectionProviderTest.java @@ -199,6 +199,7 @@ public void testReadFromREPLICAAndNoSlave() throws InterruptedException { .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA)) { + Thread.sleep(1000); Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); jedis.set("foo", "bar"); Thread.sleep(1000); @@ -216,6 +217,25 @@ public void testFallbackTOMasterWhenNOSlave() throws InterruptedException { .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA_PREFERRED)) { + Thread.sleep(1000); + Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); + jedis.set("foo", "bar"); + Thread.sleep(1000); + assertDoesNotThrow(() -> jedis.get("foo")); + } + } + + @Test + public void testAllWriteCommandsWhenNOSlave() throws InterruptedException { + DefaultRedisCredentialsProvider credentialsProvider + = new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(null, password)); + + try (JedisSentineled jedis = new JedisSentineled(MASTER_NAME, DefaultJedisClientConfig.builder() + .timeoutMillis(2000).credentialsProvider(credentialsProvider).database(2) + .clientName("my_shiny_client_name").build(), new ConnectionPoolConfig(), + sentinels, DefaultJedisClientConfig.builder().build(), ReadFrom.REPLICA_PREFERRED, command -> false)) { + + Thread.sleep(1000); Whitebox.setInternalState(jedis.provider, "slavePools", new ArrayList<>()); jedis.set("foo", "bar"); Thread.sleep(1000); From ae74a40c0c3c9f9acb6394d6e5381261f283799f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Tue, 9 Sep 2025 17:26:15 +0800 Subject: [PATCH 06/10] add ut --- .../clients/jedis/providers/SentineledConnectionProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java index 31c07af4a6..e04a23b050 100644 --- a/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java +++ b/src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java @@ -520,7 +520,7 @@ public void onMessage(String channel, String message) { removeSlave(new HostAndPort(slaveIp, slavePort)); break; case "-sdown": - if (switchMsg.length < 5) { + if (switchMsg.length < 6) { return; } if (!masterName.equals(switchMsg[5])) { From 3b91b6c69466e5c1051a9ff83a72870c505245af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Wed, 17 Sep 2025 15:33:06 +0800 Subject: [PATCH 07/10] format file --- src/main/java/redis/clients/jedis/ReadFrom.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/redis/clients/jedis/ReadFrom.java b/src/main/java/redis/clients/jedis/ReadFrom.java index 12fc232bab..5ac339009b 100644 --- a/src/main/java/redis/clients/jedis/ReadFrom.java +++ b/src/main/java/redis/clients/jedis/ReadFrom.java @@ -1,12 +1,12 @@ package redis.clients.jedis; public enum ReadFrom { - // read from the upstream only. - UPSTREAM, - // read from the replica only. - REPLICA, - // read preferred from the upstream and fall back to a replica if the upstream is not available. - UPSTREAM_PREFERRED, - // read preferred from replica and fall back to upstream if no replica is not available. - REPLICA_PREFERRED + // read from the upstream only. + UPSTREAM, + // read from the replica only. + REPLICA, + // read preferred from the upstream and fall back to a replica if the upstream is not available. + UPSTREAM_PREFERRED, + // read preferred from replica and fall back to upstream if no replica is not available. + REPLICA_PREFERRED } From da4d3777f7f04a47f557658c127b530457bd824b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Thu, 25 Sep 2025 11:07:03 +0800 Subject: [PATCH 08/10] format ReadOnlyCommands.java --- .../clients/jedis/util/ReadOnlyCommands.java | 86 ++++++++++++++----- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java index 51db381501..6d0ec0352d 100644 --- a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java +++ b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java @@ -1,6 +1,5 @@ package redis.clients.jedis.util; - import redis.clients.jedis.CommandArguments; import redis.clients.jedis.Protocol.Command; import redis.clients.jedis.bloom.RedisBloomProtocol.BloomFilterCommand; @@ -21,26 +20,71 @@ public class ReadOnlyCommands { private static final ReadOnlyPredicate PREDICATE = command -> isReadOnlyCommand(command); - private static final Set READ_ONLY_COMMANDS = new HashSet(Arrays.asList(Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, // <-- string - Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, // <-- bit (string) - Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, Command.HPEXPIRETIME, Command.HRANDFIELD, // <-- hash - Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, // <-- list - Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, // <-- set - Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, // <-- zset - Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, Command.GEORADIUSBYMEMBER_RO, // <-- geo - Command.PFCOUNT, // <-- hyper log log - Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, Command.XPENDING, Command.XINFO, // <-- stream - Command.FCALL_RO, // <-- program - Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, // <-- vector set - BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, BloomFilterCommand.INFO, // <-- BloomFilterCommand - CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, CuckooFilterCommand.INFO, // <-- CuckooFilterCommand - CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, // <-- CountMinSketchCommand - TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, // <-- TopKCommand - TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, // <-- TDigestCommand - JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, JsonCommand.DEBUG, JsonCommand.RESP, // <-- JsonCommand - SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, // <-- SearchCommand - TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX // <-- TimeSeriesCommand - )); + private static final Set READ_ONLY_COMMANDS = new HashSet(Arrays + .asList( + // string + Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, + Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, + Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, + Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, + // bit + Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, + // hash + Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, + Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, + Command.HPEXPIRETIME, Command.HRANDFIELD, + // list + Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, + // set + Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, + Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, + // zset + Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, + Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, + Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, + Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, + // geo + Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, + Command.GEORADIUSBYMEMBER_RO, + // hyper log + Command.PFCOUNT, + // stream + Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, + Command.XPENDING, Command.XINFO, + // program + Command.FCALL_RO, + // vector set + Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, + Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, + Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, + // BloomFilterCommand + BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, + BloomFilterCommand.INFO, + // CuckooFilterCommand + CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, + CuckooFilterCommand.INFO, + // CountMinSketchCommand + CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, + // TopKCommand + TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, + // TDigestCommand + TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, + TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, + TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, + // JsonCommand + JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, + JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, + JsonCommand.DEBUG, JsonCommand.RESP, + // SearchCommand + SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, + SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, + SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, + SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, + // TimeSeriesCommand + TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, + TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, + TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX + )); public static ReadOnlyPredicate asPredicate() { return PREDICATE; From ab0c82c480418583cb15426918badf5c81985423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Thu, 25 Sep 2025 11:07:26 +0800 Subject: [PATCH 09/10] format ReadOnlyCommands.java --- src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java index 6d0ec0352d..b487d54c69 100644 --- a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java +++ b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java @@ -20,8 +20,7 @@ public class ReadOnlyCommands { private static final ReadOnlyPredicate PREDICATE = command -> isReadOnlyCommand(command); - private static final Set READ_ONLY_COMMANDS = new HashSet(Arrays - .asList( + private static final Set READ_ONLY_COMMANDS = new HashSet(Arrays.asList( // string Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, From 5c34820ae1882082c3742a7535a2ea5b31d80dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=87=91=E6=9D=BE?= Date: Thu, 25 Sep 2025 11:14:45 +0800 Subject: [PATCH 10/10] format ReadOnlyCommands.java --- pom.xml | 3 +- .../clients/jedis/util/ReadOnlyCommands.java | 128 +++++++++--------- 2 files changed, 66 insertions(+), 65 deletions(-) diff --git a/pom.xml b/pom.xml index 13deca6936..3b91194d41 100644 --- a/pom.xml +++ b/pom.xml @@ -397,7 +397,7 @@ @{failsafeSuffixArgLine} ${JVM_OPTS} - + **/*IntegrationTest.java **/*IntegrationTests.java @@ -500,6 +500,7 @@ **/Health*.java src/main/java/redis/clients/jedis/MultiClusterClientConfig.java src/main/java/redis/clients/jedis/HostAndPort.java + src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java diff --git a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java index b487d54c69..7b488e1541 100644 --- a/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java +++ b/src/main/java/redis/clients/jedis/util/ReadOnlyCommands.java @@ -20,70 +20,70 @@ public class ReadOnlyCommands { private static final ReadOnlyPredicate PREDICATE = command -> isReadOnlyCommand(command); - private static final Set READ_ONLY_COMMANDS = new HashSet(Arrays.asList( - // string - Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, - Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, - Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, - Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, - // bit - Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, - // hash - Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, - Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, - Command.HPEXPIRETIME, Command.HRANDFIELD, - // list - Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, - // set - Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, - Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, - // zset - Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, - Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, - Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, - Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, - // geo - Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, - Command.GEORADIUSBYMEMBER_RO, - // hyper log - Command.PFCOUNT, - // stream - Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, - Command.XPENDING, Command.XINFO, - // program - Command.FCALL_RO, - // vector set - Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, - Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, - Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, - // BloomFilterCommand - BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, - BloomFilterCommand.INFO, - // CuckooFilterCommand - CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, - CuckooFilterCommand.INFO, - // CountMinSketchCommand - CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, - // TopKCommand - TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, - // TDigestCommand - TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, - TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, - TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, - // JsonCommand - JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, - JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, - JsonCommand.DEBUG, JsonCommand.RESP, - // SearchCommand - SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, - SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, - SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, - SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, - // TimeSeriesCommand - TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, - TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, - TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX - )); + private static final Set READ_ONLY_COMMANDS = new HashSet( + Arrays.asList( + // string + Command.PING, Command.AUTH, Command.HELLO, Command.GET, Command.EXISTS, Command.TYPE, + Command.KEYS, Command.RANDOMKEY, Command.DUMP, Command.DBSIZE, Command.SELECT, Command.ECHO, + Command.EXPIRETIME, Command.PEXPIRETIME, Command.TTL, Command.PTTL, Command.SORT_RO, + Command.INFO, Command.MONITOR, Command.LCS, Command.MGET, Command.STRLEN, Command.SUBSTR, + // bit + Command.GETBIT, Command.BITPOS, Command.GETRANGE, Command.BITCOUNT, Command.BITFIELD_RO, + // hash + Command.HGET, Command.HMGET, Command.HEXISTS, Command.HLEN, Command.HKEYS, Command.HVALS, + Command.HGETALL, Command.HSTRLEN, Command.HTTL, Command.HPTTL, Command.HEXPIRETIME, + Command.HPEXPIRETIME, Command.HRANDFIELD, + // list + Command.LLEN, Command.LRANGE, Command.LINDEX, Command.LPOS, + // set + Command.SMEMBERS, Command.SCARD, Command.SRANDMEMBER, Command.SINTER, Command.SUNION, + Command.SDIFF, Command.SISMEMBER, Command.SMISMEMBER, Command.SINTERCARD, + // zset + Command.ZDIFF, Command.ZRANGE, Command.ZRANK, Command.ZREVRANK, Command.ZREVRANGE, + Command.ZRANDMEMBER, Command.ZCARD, Command.ZSCORE, Command.ZCOUNT, Command.ZUNION, + Command.ZINTER, Command.ZRANGEBYSCORE, Command.ZREVRANGEBYSCORE, Command.ZLEXCOUNT, + Command.ZRANGEBYLEX, Command.ZREVRANGEBYLEX, Command.ZMSCORE, Command.ZINTERCARD, + // geo + Command.GEODIST, Command.GEOHASH, Command.GEOPOS, Command.GEORADIUS_RO, + Command.GEORADIUSBYMEMBER_RO, + // hyper log + Command.PFCOUNT, + // stream + Command.XLEN, Command.XRANGE, Command.XREVRANGE, Command.XREAD, Command.XREADGROUP, + Command.XPENDING, Command.XINFO, + // program + Command.FCALL_RO, + // vector set + Command.LASTSAVE, Command.ROLE, Command.OBJECT, Command.TIME, Command.SCAN, Command.HSCAN, + Command.SSCAN, Command.ZSCAN, Command.LOLWUT, Command.VSIM, Command.VDIM, Command.VCARD, + Command.VEMB, Command.VLINKS, Command.VRANDMEMBER, Command.VGETATTR, Command.VINFO, + // BloomFilterCommand + BloomFilterCommand.EXISTS, BloomFilterCommand.MEXISTS, BloomFilterCommand.CARD, + BloomFilterCommand.INFO, + // CuckooFilterCommand + CuckooFilterCommand.EXISTS, CuckooFilterCommand.MEXISTS, CuckooFilterCommand.COUNT, + CuckooFilterCommand.INFO, + // CountMinSketchCommand + CountMinSketchCommand.QUERY, CountMinSketchCommand.INFO, + // TopKCommand + TopKCommand.QUERY, TopKCommand.LIST, TopKCommand.INFO, + // TDigestCommand + TDigestCommand.INFO, TDigestCommand.CDF, TDigestCommand.QUANTILE, TDigestCommand.MIN, + TDigestCommand.MAX, TDigestCommand.TRIMMED_MEAN, TDigestCommand.RANK, + TDigestCommand.REVRANK, TDigestCommand.BYRANK, TDigestCommand.BYREVRANK, + // JsonCommand + JsonCommand.GET, JsonCommand.MGET, JsonCommand.TYPE, JsonCommand.STRLEN, + JsonCommand.ARRINDEX, JsonCommand.ARRLEN, JsonCommand.OBJKEYS, JsonCommand.OBJLEN, + JsonCommand.DEBUG, JsonCommand.RESP, + // SearchCommand + SearchCommand.INFO, SearchCommand.SEARCH, SearchCommand.EXPLAIN, SearchCommand.EXPLAINCLI, + SearchCommand.AGGREGATE, SearchCommand.CURSOR, SearchCommand.SYNDUMP, SearchCommand.SUGGET, + SearchCommand.SUGLEN, SearchCommand.DICTDUMP, SearchCommand.SPELLCHECK, + SearchCommand.TAGVALS, SearchCommand.PROFILE, SearchCommand._LIST, + // TimeSeriesCommand + TimeSeriesCommand.RANGE, TimeSeriesCommand.REVRANGE, TimeSeriesCommand.MRANGE, + TimeSeriesCommand.MREVRANGE, TimeSeriesCommand.INFO, TimeSeriesCommand.GET, + TimeSeriesCommand.MGET, TimeSeriesCommand.QUERYINDEX)); public static ReadOnlyPredicate asPredicate() { return PREDICATE;