/** * @param keys must not be {@literal null}. * @return */ public static boolean isSameSlotForAllKeys(byte[]... keys) { Assert.notNull(keys, "Keys must not be null!"); if (keys.length <= 1) { return true; } int slot = calculateSlot(keys[0]); for (int i = 1; i < keys.length; i++) { if (slot != calculateSlot(keys[i])) { return false; } } return true; }
/** * Calculate the slot from the given key. * * @param key must not be {@literal null}. * @return */ public static int calculateSlot(byte[] key) { Assert.notNull(key, "Key must not be null!"); byte[] finalKey = key; int start = indexOf(key, SUBKEY_START); if (start != -1) { int end = indexOf(key, start + 1, SUBKEY_END); if (end != -1 && end != start + 1) { finalKey = new byte[end - (start + 1)]; System.arraycopy(key, start + 1, finalKey, 0, finalKey.length); } } return crc16(finalKey) % SLOT_COUNT; }
@Override public Flux<ReactiveRedisConnection.BooleanResponse<MSetCommand>> mSetNX(Publisher<MSetCommand> commands) { return getConnection().execute(cmd -> Flux.from(commands).concatMap(command -> { if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeyValuePairs().keySet())) { return super.mSetNX(Mono.just(command)); } return Mono .error(new InvalidDataAccessApiUsageException("All keys must map to the same slot for MSETNX command.")); })); } }
@Override public Flux<NumericResponse<SUnionStoreCommand, Long>> sUnionStore(Publisher<SUnionStoreCommand> commands) { return getConnection().execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKeys(), "Source keys must not be null!"); Assert.notNull(command.getKey(), "Destination key must not be null!"); List<ByteBuffer> keys = new ArrayList<>(command.getKeys()); keys.add(command.getKey()); if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { return super.sUnionStore(Mono.just(command)); } return sUnion(Mono.just(SUnionCommand.keys(command.getKeys()))).next().flatMap(values -> { Mono<Long> result = cmd.sadd(command.getKey(), values.getOutput().toStream().toArray(ByteBuffer[]::new)); return result.map(value -> new NumericResponse<>(command, value)); }); })); }
@Override public Long zInterStore(byte[] destKey, byte[]... sets) { Assert.notNull(destKey, "Destination key must not be null!"); Assert.notNull(sets, "Source sets must not be null!"); Assert.noNullElements(sets, "Source sets must not contain null elements!"); byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { return connection.getCluster().zinterstore(destKey, sets); } catch (Exception ex) { throw convertJedisAccessException(ex); } } throw new InvalidDataAccessApiUsageException("ZINTERSTORE can only be executed when all keys map to the same slot"); }
@Override public Long zUnionStore(byte[] destKey, byte[]... sets) { Assert.notNull(destKey, "Destination key must not be null!"); Assert.notNull(sets, "Source sets must not be null!"); Assert.noNullElements(sets, "Source sets must not contain null elements!"); byte[][] allKeys = ByteUtils.mergeArrays(destKey, sets); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { return connection.getCluster().zunionstore(destKey, sets); } catch (Exception ex) { throw convertJedisAccessException(ex); } } throw new InvalidDataAccessApiUsageException("ZUNIONSTORE can only be executed when all keys map to the same slot"); }
@Override public Flux<BooleanResponse<RenameCommand>> rename(Publisher<RenameCommand> commands) { return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "key must not be null."); Assert.notNull(command.getNewName(), "NewName must not be null!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKey(), command.getNewName())) { return super.rename(Mono.just(command)); } Mono<Boolean> result = cmd.dump(command.getKey()) .switchIfEmpty(Mono.error(new RedisSystemException("Cannot rename key that does not exist", new RedisException("ERR no such key.")))) .flatMap(value -> cmd.restore(command.getNewName(), 0, value).flatMap(res -> cmd.del(command.getKey()))) .map(LettuceConverters.longToBooleanConverter()::convert); return result.map(val -> new BooleanResponse<>(command, val)); })); }
@Override public Flux<CommandResponse<SUnionCommand, Flux<ByteBuffer>>> sUnion(Publisher<SUnionCommand> commands) { return getConnection().execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKeys(), "Keys must not be null!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeys())) { return super.sUnion(Mono.just(command)); } Flux<ByteBuffer> result = Flux.merge(command.getKeys().stream().map(cmd::smembers).collect(Collectors.toList())) .distinct(); return Mono.just(new CommandResponse<>(command, result)); })); }
/** * @param keys must not be {@literal null}. * @return * @since 2.0 */ public static boolean isSameSlotForAllKeys(ByteBuffer... keys) { Assert.notNull(keys, "Keys must not be null!"); return isSameSlotForAllKeys(Arrays.asList(keys)); }
@Override public Flux<CommandResponse<SInterCommand, Flux<ByteBuffer>>> sInter(Publisher<SInterCommand> commands) { return getConnection().execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKeys(), "Keys must not be null!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeys())) { return super.sInter(Mono.just(command)); } Mono<List<ByteBuffer>> sourceSet = cmd.smembers(command.getKeys().get(0)).distinct().collectList(); List<Mono<List<ByteBuffer>>> intersectingSets = new ArrayList<>(); for (int i = 1; i < command.getKeys().size(); i++) { intersectingSets.add(cmd.smembers(command.getKeys().get(i)).distinct().collectList()); } Flux<List<ByteBuffer>> result = Flux.zip(sourceSet, Flux.merge(intersectingSets).collectList(), (source, intersectings) -> { for (List<ByteBuffer> intersecting : intersectings) { source.retainAll(intersecting); } return source; }); return Mono.just(new CommandResponse<>(command, result.concatMap(v -> Flux.fromStream(v.stream())))); })); }
@Override public List<byte[]> mGet(byte[]... keys) { Assert.notNull(keys, "Keys must not be null!"); Assert.noNullElements(keys, "Keys must not contain null elements!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { return connection.getCluster().mget(keys); } return connection.getClusterCommandExecutor() .executeMultiKeyCommand((JedisMultiKeyClusterCommandCallback<byte[]>) BinaryJedis::get, Arrays.asList(keys)) .resultsAsListSortBy(keys); }
@Override public Long bitOp(BitOperation op, byte[] destination, byte[]... keys) { Assert.notNull(op, "BitOperation must not be null!"); Assert.notNull(destination, "Destination key must not be null!"); byte[][] allKeys = ByteUtils.mergeArrays(destination, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { return connection.getCluster().bitop(JedisConverters.toBitOp(op), destination, keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } throw new InvalidDataAccessApiUsageException("BITOP is only supported for same slot keys in cluster mode."); }
@Nullable @Override public Long exists(byte[]... keys) { Assert.notNull(keys, "Keys must not be null!"); Assert.noNullElements(keys, "Keys must not contain null elements!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { try { return connection.getCluster().exists(keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } return connection.getClusterCommandExecutor() .executeMultiKeyCommand((JedisMultiKeyClusterCommandCallback<Boolean>) BinaryJedis::exists, Arrays.asList(keys)) .resultsAsList().stream().mapToLong(val -> ObjectUtils.nullSafeEquals(val, Boolean.TRUE) ? 1 : 0).sum(); }
@Override public Long del(byte[]... keys) { Assert.notNull(keys, "Keys must not be null!"); Assert.noNullElements(keys, "Keys must not contain null elements!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { try { return connection.getCluster().del(keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } return (long) connection.getClusterCommandExecutor() .executeMultiKeyCommand((JedisMultiKeyClusterCommandCallback<Long>) (client, key) -> client.del(key), Arrays.asList(keys)) .resultsAsList().size(); }
@Override public List<byte[]> bLPop(int timeout, byte[]... keys) { Assert.notNull(keys, "Key must not be null!"); Assert.noNullElements(keys, "Keys must not contain null elements!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { try { return connection.getCluster().blpop(timeout, keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } return connection.getClusterCommandExecutor() .executeMultiKeyCommand( (JedisMultiKeyClusterCommandCallback<List<byte[]>>) (client, key) -> client.blpop(timeout, key), Arrays.asList(keys)) .getFirstNonNullNotEmptyOrDefault(Collections.<byte[]> emptyList()); }
@Override public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { Assert.notNull(destinationKey, "Destination key must not be null"); Assert.notNull(sourceKeys, "Source keys must not be null"); Assert.noNullElements(sourceKeys, "Keys for PFMERGE must not contain 'null'."); byte[][] allKeys = ByteUtils.mergeArrays(destinationKey, sourceKeys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { connection.getCluster().pfmerge(destinationKey, sourceKeys); return; } catch (Exception ex) { throw convertJedisAccessException(ex); } } throw new InvalidDataAccessApiUsageException("All keys must map to same slot for pfmerge in cluster mode."); }
@Override public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { Assert.notNull(srcKey, "Source key must not be null!"); Assert.notNull(dstKey, "Destination key must not be null!"); if (ClusterSlotHashUtil.isSameSlotForAllKeys(srcKey, dstKey)) { try { return connection.getCluster().rpoplpush(srcKey, dstKey); } catch (Exception ex) { throw convertJedisAccessException(ex); } } byte[] val = rPop(srcKey); lPush(dstKey, val); return val; }
@Override public Long sInterStore(byte[] destKey, byte[]... keys) { Assert.notNull(destKey, "Destination key must not be null!"); Assert.notNull(keys, "Source keys must not be null!"); Assert.noNullElements(keys, "Source keys must not contain null elements!"); byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { return connection.getCluster().sinterstore(destKey, keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } Set<byte[]> result = sInter(keys); if (result.isEmpty()) { return 0L; } return sAdd(destKey, result.toArray(new byte[result.size()][])); }
@Override public Long sDiffStore(byte[] destKey, byte[]... keys) { Assert.notNull(destKey, "Destination key must not be null!"); Assert.notNull(keys, "Source keys must not be null!"); Assert.noNullElements(keys, "Source keys must not contain null elements!"); byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { return connection.getCluster().sdiffstore(destKey, keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } Set<byte[]> diff = sDiff(keys); if (diff.isEmpty()) { return 0L; } return sAdd(destKey, diff.toArray(new byte[diff.size()][])); }
@Override public Long sUnionStore(byte[] destKey, byte[]... keys) { Assert.notNull(destKey, "Destination key must not be null!"); Assert.notNull(keys, "Source keys must not be null!"); Assert.noNullElements(keys, "Source keys must not contain null elements!"); byte[][] allKeys = ByteUtils.mergeArrays(destKey, keys); if (ClusterSlotHashUtil.isSameSlotForAllKeys(allKeys)) { try { return connection.getCluster().sunionstore(destKey, keys); } catch (Exception ex) { throw convertJedisAccessException(ex); } } Set<byte[]> result = sUnion(keys); if (result.isEmpty()) { return 0L; } return sAdd(destKey, result.toArray(new byte[result.size()][])); }