diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 2095d2c1ca3e154681778627fe1d09dc4f1839d3..754d31a99b2dcaa75b89d2322ac19955f3a13b4b 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -3703,7 +3703,6 @@ Layout/LineLength:
     - 'spec/lib/gitlab/import_export/uploads_manager_spec.rb'
     - 'spec/lib/gitlab/import_export/version_checker_spec.rb'
     - 'spec/lib/gitlab/import_sources_spec.rb'
-    - 'spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb'
     - 'spec/lib/gitlab/issuable_metadata_spec.rb'
     - 'spec/lib/gitlab/issues/rebalancing/state_spec.rb'
     - 'spec/lib/gitlab/jira/dvcs_spec.rb'
diff --git a/.rubocop_todo/lint/ambiguous_operator_precedence.yml b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
index 5b352842a870d6ed54a4e78abfafc4ccc180a6a7..a7cac8214c08c6721a2bf1fcfd312e294de10cd0 100644
--- a/.rubocop_todo/lint/ambiguous_operator_precedence.yml
+++ b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
@@ -102,7 +102,6 @@ Lint/AmbiguousOperatorPrecedence:
     - 'spec/lib/gitlab/database/batch_count_spec.rb'
     - 'spec/lib/gitlab/database/consistency_checker_spec.rb'
     - 'spec/lib/gitlab/graphql/tracers/metrics_tracer_spec.rb'
-    - 'spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb'
     - 'spec/lib/gitlab/issues/rebalancing/state_spec.rb'
     - 'spec/lib/gitlab/kroki_spec.rb'
     - 'spec/lib/gitlab/memory/instrumentation_spec.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index e497e2199abd29ae4b3122a57dd1513165ee217d..7ff9f8b091f7bd0e2d3f7e59982bc1969f86f1c3 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -3588,7 +3588,6 @@ RSpec/FeatureCategory:
     - 'spec/lib/gitlab/instrumentation/rate_limiting_gates_spec.rb'
     - 'spec/lib/gitlab/instrumentation/redis_base_spec.rb'
     - 'spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb'
-    - 'spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb'
     - 'spec/lib/gitlab/instrumentation/redis_spec.rb'
     - 'spec/lib/gitlab/internal_post_receive/response_spec.rb'
     - 'spec/lib/gitlab/issuable/clone/attributes_rewriter_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index 4e4aa25c4176fc9c79697caeb7b284c9033271e4..7d7755db97417379e03140837cd321bd39b21874 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -2334,7 +2334,6 @@ RSpec/NamedSubject:
     - 'spec/lib/gitlab/rack_attack_spec.rb'
     - 'spec/lib/gitlab/reactive_cache_set_cache_spec.rb'
     - 'spec/lib/gitlab/redis/boolean_spec.rb'
-    - 'spec/lib/gitlab/redis/cross_slot_spec.rb'
     - 'spec/lib/gitlab/redis/db_load_balancing_spec.rb'
     - 'spec/lib/gitlab/redis/multi_store_spec.rb'
     - 'spec/lib/gitlab/redis/queues_spec.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index 4bf891cd5c33cde41cd390d958bca3ad847b7395..31f59f51bf7c6d7ead47159da0da68dddcc0c798 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -2517,7 +2517,6 @@ Style/InlineDisableAnnotation:
     - 'lib/gitlab/import_export/project/relation_factory.rb'
     - 'lib/gitlab/import_sources.rb'
     - 'lib/gitlab/instrumentation/redis_cluster_validator.rb'
-    - 'lib/gitlab/instrumentation/redis_interceptor.rb'
     - 'lib/gitlab/internal_events.rb'
     - 'lib/gitlab/issuable/clone/copy_resource_events_service.rb'
     - 'lib/gitlab/issues/rebalancing/state.rb'
@@ -2556,7 +2555,6 @@ Style/InlineDisableAnnotation:
     - 'lib/gitlab/pagination/offset_pagination.rb'
     - 'lib/gitlab/pagination_delegate.rb'
     - 'lib/gitlab/patch/action_cable_subscription_adapter_identifier.rb'
-    - 'lib/gitlab/patch/node_loader.rb'
     - 'lib/gitlab/patch/prependable.rb'
     - 'lib/gitlab/patch/redis_cache_store.rb'
     - 'lib/gitlab/patch/sidekiq_cron_poller.rb'
@@ -2573,7 +2571,6 @@ Style/InlineDisableAnnotation:
     - 'lib/gitlab/rack_attack.rb'
     - 'lib/gitlab/rack_attack/request.rb'
     - 'lib/gitlab/rack_attack/store.rb'
-    - 'lib/gitlab/redis/cross_slot.rb'
     - 'lib/gitlab/redis/hll.rb'
     - 'lib/gitlab/redis/multi_store.rb'
     - 'lib/gitlab/reference_extractor.rb'
@@ -2942,9 +2939,7 @@ Style/InlineDisableAnnotation:
     - 'spec/lib/gitlab/pagination/keyset/order_spec.rb'
     - 'spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb'
     - 'spec/lib/gitlab/patch/database_config_spec.rb'
-    - 'spec/lib/gitlab/patch/node_loader_spec.rb'
     - 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
-    - 'spec/lib/gitlab/redis/cross_slot_spec.rb'
     - 'spec/lib/gitlab/redis/multi_store_spec.rb'
     - 'spec/lib/gitlab/search/abuse_detection_spec.rb'
     - 'spec/lib/gitlab/shard_health_cache_spec.rb'
diff --git a/Gemfile b/Gemfile
index 6c452d26358a99ff1c0893ab2e9a6ac0cebd9d13..506904a1d7bf787a49af591f726edf35924a1911 100644
--- a/Gemfile
+++ b/Gemfile
@@ -288,8 +288,9 @@ gem 'js_regex', '~> 3.8' # rubocop:todo Gemfile/MissingFeatureCategory
 gem 'device_detector' # rubocop:todo Gemfile/MissingFeatureCategory
 
 # Redis
-gem 'redis', '~> 4.8.0' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'redis-namespace', '~> 1.10.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'redis-namespace', '~> 1.10.0', feature_category: :redis
+gem 'redis', '~> 5.0.0', feature_category: :redis
+gem 'redis-clustering', '~> 5.0.0', feature_category: :redis
 gem 'connection_pool', '~> 2.4' # rubocop:todo Gemfile/MissingFeatureCategory
 
 # Redis session store
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 6edc3cfb8f1473216a421c30784708acddf63e8c..ae18a1d7c833b0b6916387cb9067392e6e77e8be 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -523,9 +523,11 @@
 {"name":"recaptcha","version":"5.12.3","platform":"ruby","checksum":"37d1894add9e70a54d0c6c7f0ecbeedffbfa7d075acfbd4c509818dfdebdb7ee"},
 {"name":"recursive-open-struct","version":"1.1.3","platform":"ruby","checksum":"a3538a72552fcebcd0ada657bdff313641a4a5fbc482c08cfb9a65acb1c9de5a"},
 {"name":"redcarpet","version":"3.6.0","platform":"ruby","checksum":"8ad1889c0355ff4c47174af14edd06d62f45a326da1da6e8a121d59bdcd2e9e9"},
-{"name":"redis","version":"4.8.0","platform":"ruby","checksum":"2000cf5014669c9dc821704b6d322a35a9a33852a95208911d9175d63b448a44"},
+{"name":"redis","version":"5.0.8","platform":"ruby","checksum":"3b770ea597850b26d6a9718fa184241e53e6c8a7ae0486ee8bfaefd29f26f3d8"},
 {"name":"redis-actionpack","version":"5.4.0","platform":"ruby","checksum":"f10cf649ab05914716d63334d7f709221ecc883b87cf348f90ecfe0c35ea3540"},
 {"name":"redis-client","version":"0.19.0","platform":"ruby","checksum":"6ed9af23ff5aa87cf4d59439db77082b4cae5a0abbdd114ec5420bd63456324d"},
+{"name":"redis-cluster-client","version":"0.7.5","platform":"ruby","checksum":"12fd1c9eda17157a5cd2ce46afba13a024c28d24922092299a8daa9f46e4e78a"},
+{"name":"redis-clustering","version":"5.0.8","platform":"ruby","checksum":"8e2f3de3b1a700668eeac59125636e01be6ecd985e635a4d5649c47d71f6e166"},
 {"name":"redis-namespace","version":"1.10.0","platform":"ruby","checksum":"2c1c6ea7c6c5e343e75b9bee3aa4c265e364a5b9966507397467af2bb3758d94"},
 {"name":"redis-rack","version":"3.0.0","platform":"ruby","checksum":"abb50b82ae10ad4d11ca2e4901bfc2b98256cdafbbd95f80c86fc9e001478380"},
 {"name":"redis-store","version":"1.10.0","platform":"ruby","checksum":"f258894f9f7e82834308a3d86242294f0cff2c9db9ae66e5cb4c553a5ec8b09e"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 087da2cb58d566b531bddfe639a4962a851fbd69..1429f0c39dabbe562a91218ade8f3ebdb9627568 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1385,13 +1385,19 @@ GEM
       json
     recursive-open-struct (1.1.3)
     redcarpet (3.6.0)
-    redis (4.8.0)
+    redis (5.0.8)
+      redis-client (>= 0.17.0)
     redis-actionpack (5.4.0)
       actionpack (>= 5, < 8)
       redis-rack (>= 2.1.0, < 4)
       redis-store (>= 1.1.0, < 2)
     redis-client (0.19.0)
       connection_pool
+    redis-cluster-client (0.7.5)
+      redis-client (~> 0.12)
+    redis-clustering (5.0.8)
+      redis (= 5.0.8)
+      redis-cluster-client (>= 0.7.0)
     redis-namespace (1.10.0)
       redis (>= 4)
     redis-rack (3.0.0)
@@ -2069,8 +2075,9 @@ DEPENDENCIES
   rbtrace (~> 0.4)
   re2 (= 2.7.0)
   recaptcha (~> 5.12)
-  redis (~> 4.8.0)
+  redis (~> 5.0.0)
   redis-actionpack (~> 5.4.0)
+  redis-clustering (~> 5.0.0)
   redis-namespace (~> 1.10.0)
   request_store (~> 1.5.1)
   responders (~> 3.0)
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 2eb9c9bca7f24730c3de04caf629006261b51368..756e0b7fbf9cddf3ca8f58d75503f479e54496bf 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -84,7 +84,7 @@ def self.set(user, request)
       )
 
       Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-        Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+        redis.pipelined do |pipeline|
           pipeline.setex(
             key_name(user.id, session_private_id),
             expiry,
diff --git a/app/workers/concerns/limited_capacity/job_tracker.rb b/app/workers/concerns/limited_capacity/job_tracker.rb
index b4d884f914df027af9aa0c87a29205b1246b9959..2ac3c5e991bfb79b3faab1acb160839de8f4f040 100644
--- a/app/workers/concerns/limited_capacity/job_tracker.rb
+++ b/app/workers/concerns/limited_capacity/job_tracker.rb
@@ -21,7 +21,7 @@ def initialize(namespace)
 
     def register(jid, max_jids)
       with_redis do |redis|
-        redis.eval(LUA_REGISTER_SCRIPT, keys: [counter_key], argv: [jid, max_jids])
+        redis.eval(LUA_REGISTER_SCRIPT, keys: [counter_key], argv: [jid.to_s, max_jids.to_i])
       end.present?
     end
 
@@ -59,7 +59,7 @@ def counter_key
     end
 
     def remove_job_keys(redis, keys)
-      redis.srem?(counter_key, keys)
+      redis.srem?(counter_key, keys) if keys.present?
     end
 
     def with_redis(&block)
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
index fbd2cbaabad64847720c12be3f8ea4cdea434876..c7385612602a02f16defc4a878dcb06bdfd5a71f 100644
--- a/config/initializers/7_redis.rb
+++ b/config/initializers/7_redis.rb
@@ -21,12 +21,6 @@
 # :nocov:
 # rubocop:enable Gitlab/NoCodeCoverageComment
 
-Redis::Client.prepend(Gitlab::Instrumentation::RedisInterceptor)
-Redis::Cluster::NodeLoader.prepend(Gitlab::Patch::NodeLoader)
-Redis::Cluster::SlotLoader.prepend(Gitlab::Patch::SlotLoader)
-Redis::Cluster::CommandLoader.prepend(Gitlab::Patch::CommandLoader)
-Redis::Cluster.prepend(Gitlab::Patch::RedisCluster)
-
 # this only instruments `RedisClient` used in `Sidekiq.redis`
 RedisClient.register(Gitlab::Instrumentation::RedisClientMiddleware)
 RedisClient.prepend(Gitlab::Patch::RedisClient)
diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb
index dc69a108f56e5f3386b3b67f7c50a5b560ca47ce..1d22c774443068e6981af83fe6cf7b5f5fa00cf0 100644
--- a/config/initializers/action_cable.rb
+++ b/config/initializers/action_cable.rb
@@ -25,7 +25,17 @@
 # https://github.com/rails/rails/blob/bb5ac1623e8de08c1b7b62b1368758f0d3bb6379/actioncable/lib/action_cable/subscription_adapter/redis.rb#L18
 ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda do |config|
   args = config.except(:adapter, :channel_prefix)
-    .merge(instrumentation_class: ::Gitlab::Instrumentation::Redis::ActionCable)
+    .merge(custom: { instrumentation_class: "ActionCable" })
+
+  if args[:url]
+    # cable.yml uses the url key but unix sockets needs to be passed into Redis
+    # under the `path` key. We do a simple reassignment to resolve that.
+    uri = URI.parse(args[:url])
+    if uri.scheme == 'unix'
+      args[:path] = uri.path
+      args.delete(:url)
+    end
+  end
 
   ::Redis.new(args)
 end
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index e1c59851fb1a95f26d6134639e15cea3066668ad..72e7d3f10f98b69bece9a7809006d8bd95001ed8 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -6,7 +6,7 @@
 Peek.singleton_class.prepend ::Gitlab::PerformanceBar::WithTopLevelWarnings
 
 Rails.application.config.peek.adapter = :redis, {
-  client: ::Redis.new(Gitlab::Redis::Cache.params),
+  client: Gitlab::Redis::Cache.redis,
   expires_in: 5.minutes
 }
 
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 7f410d7bf7bbd97b2be9e74130d799746657cbc7..70bd3d012d096f5e72e0c272adc1d03813161af1 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -29,12 +29,12 @@
                "_gitlab_session"
              end
 
-store = Gitlab::Redis::Sessions.store(namespace: Gitlab::Redis::Sessions::SESSION_NAMESPACE)
+::Redis::Store::Factory.prepend(Gitlab::Patch::RedisStoreFactory)
 
 Rails.application.configure do
   config.session_store(
     :redis_store, # Using the cookie_store would enable session replay attacks.
-    redis_store: store,
+    redis_server: Gitlab::Redis::Sessions.params.merge(namespace: Gitlab::Redis::Sessions::SESSION_NAMESPACE),
     key: cookie_key,
     secure: Gitlab.config.gitlab.https,
     httponly: true,
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 9b7233dbd14e2cf051b5fac14594cc5bc9fcfcc4..401d18df790d8d8ee66f3ed804e79766c5d5f6c7 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -28,7 +28,7 @@ def enable_semi_reliable_fetch_mode?
 end
 
 # Custom Queues configuration
-queues_config_hash = Gitlab::Redis::Queues.redis_client_params
+queues_config_hash = Gitlab::Redis::Queues.params
 
 enable_json_logs = Gitlab.config.sidekiq.log_format != 'text'
 
diff --git a/doc/administration/redis/troubleshooting.md b/doc/administration/redis/troubleshooting.md
index aebb3004223566bea7d81e27c031b3964b29463d..7ec6414bf45ed7e346c774f04053f29e76a48295 100644
--- a/doc/administration/redis/troubleshooting.md
+++ b/doc/administration/redis/troubleshooting.md
@@ -124,7 +124,7 @@ To make sure your configuration is correct:
 1. Run in the console:
 
    ```ruby
-   redis = Redis.new(Gitlab::Redis::SharedState.params)
+   redis = Gitlab::Redis::SharedState.redis
    redis.info
    ```
 
diff --git a/doc/development/redis.md b/doc/development/redis.md
index f9afe5e071890ea7bb5370229334cc22fbb899ca..368f6709367929ea9be10bfc1cb645e3c126aa32 100644
--- a/doc/development/redis.md
+++ b/doc/development/redis.md
@@ -81,10 +81,10 @@ Developers are highly encouraged to use [hash-tags](https://redis.io/docs/refere
 where appropriate to facilitate future adoption of Redis Cluster in more Redis types. For example, the Namespace model uses hash-tags
 for its [config cache keys](https://gitlab.com/gitlab-org/gitlab/-/blob/1a12337058f260d38405886d82da5e8bb5d8da0b/app/models/namespace.rb#L786).
 
-To perform multi-key commands, developers may use the [`Gitlab::Redis::CrossSlot::Pipeline`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/redis/cross_slot.rb) wrapper.
+To perform multi-key commands, developers may use the [`.pipelined`](https://github.com/redis-rb/redis-cluster-client#interfaces) method which splits and sends commands to each node and aggregates replies.
 However, this does not work for [transactions](https://redis.io/docs/interact/transactions/) as Redis Cluster does not support cross-slot transactions.
 
-For `Rails.cache`, we handle the `MGET` command found in `read_multi_get` by [patching it](https://gitlab.com/gitlab-org/gitlab/-/blob/c2bad2aac25e2f2778897bd4759506a72b118b15/lib/gitlab/patch/redis_cache_store.rb#L10) to use the `Gitlab::Redis::CrossSlot::Pipeline` wrapper.
+For `Rails.cache`, we handle the `MGET` command found in `read_multi_get` by [patching it](https://gitlab.com/gitlab-org/gitlab/-/blob/c2bad2aac25e2f2778897bd4759506a72b118b15/lib/gitlab/patch/redis_cache_store.rb#L10) to use the `.pipelined` method.
 The minimum size of the pipeline is set to 1000 commands and it can be adjusted by using the `GITLAB_REDIS_CLUSTER_PIPELINE_BATCH_LIMIT` environment variable.
 
 ## Redis in structured logging
diff --git a/doc/development/redis/new_redis_instance.md b/doc/development/redis/new_redis_instance.md
index ff5394cef8f22e1fadf8f6003e6b7848234c320d..054364fce5e44c07dd8f8dc65accad20f77c91ba 100644
--- a/doc/development/redis/new_redis_instance.md
+++ b/doc/development/redis/new_redis_instance.md
@@ -147,8 +147,8 @@ module Gitlab
         # Don't use multistore if redis.foo configuration is not provided
         return super if config_fallback?
 
-        primary_store = ::Redis.new(params)
-        secondary_store = ::Redis.new(config_fallback.params)
+        primary_store = init_redis(params)
+        secondary_store = init_redis(config_fallback.params)
 
         MultiStore.new(primary_store, secondary_store, store_name)
       end
diff --git a/ee/app/services/elastic/indexing_control_service.rb b/ee/app/services/elastic/indexing_control_service.rb
index 50ececd4710628ba472a4a35ad08e87b0dbdbcef..1b82c2f3a76a82385393ccb5af8a5bed2576e3de 100644
--- a/ee/app/services/elastic/indexing_control_service.rb
+++ b/ee/app/services/elastic/indexing_control_service.rb
@@ -68,7 +68,7 @@ def resume_processing!
 
         Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
           if queue_size == 0
-            Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |p|
+            redis.pipelined do |p|
               p.del(redis_score_key)
               p.del(redis_set_key)
             end
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
index 341edbed9c2d683d68d281a7c9a9dee3689301b3..0c4a5147a384835d836c4989942d7128bf4d644b 100644
--- a/lib/gitlab/auth/unique_ips_limiter.rb
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -31,7 +31,7 @@ def update_and_return_ips_count(user_id, ip)
 
           Gitlab::Redis::SharedState.with do |redis|
             redis.multi do |r|
-              r.zadd(key, time, ip)
+              r.zadd(key, time, ip) if ip
               r.zremrangebyscore(key, 0, time - config.unique_ips_limit_time_window)
               r.zcard(key)
             end.last
diff --git a/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl.rb b/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl.rb
index 2672498b6271f6b14f56ace976d02699bf0d787f..7886abdb6d40800c6456b165413d7e3d20e003f9 100644
--- a/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl.rb
+++ b/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl.rb
@@ -14,7 +14,7 @@ def perform(keys)
           ttl_jitter = 2.hours.to_i
 
           Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-            Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+            redis.pipelined do |pipeline|
               keys.each { |key| pipeline.expire(key, ttl_duration + rand(-ttl_jitter..ttl_jitter)) }
             end
           end
@@ -25,7 +25,7 @@ def scan_match_pattern
         end
 
         def redis
-          @redis ||= ::Redis.new(Gitlab::Redis::Cache.params)
+          @redis ||= Gitlab::Redis::Cache.redis
         end
       end
     end
diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
index 4beb8f54abf034bc2912e55cd1a39db1968bd82b..9320233501f168f61af8538920f6c9445e38ab94 100644
--- a/lib/gitlab/cache/ci/project_pipeline_status.rb
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -99,7 +99,7 @@ def load_from_cache
         def store_in_cache
           with_redis do |redis|
             redis.pipelined do |p|
-              p.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
+              p.mapped_hmset(cache_key, { sha: sha.to_s, status: status.to_s, ref: ref.to_s })
               p.expire(cache_key, STATUS_KEY_TTL)
             end
           end
diff --git a/lib/gitlab/cache/import/caching.rb b/lib/gitlab/cache/import/caching.rb
index c538e5162c7b3036c62495ea91ef30552f568141..0b24137076dd662d6962516e43c376e8823f90a5 100644
--- a/lib/gitlab/cache/import/caching.rb
+++ b/lib/gitlab/cache/import/caching.rb
@@ -139,7 +139,7 @@ def self.set_includes?(raw_key, value)
           key = cache_key_for(raw_key)
 
           with_redis do |redis|
-            redis.sismember(key, value)
+            redis.sismember(key, value || value.to_s)
           end
         end
 
@@ -162,7 +162,7 @@ def self.values_from_set(raw_key)
         def self.write_multiple(mapping, key_prefix: nil, timeout: TIMEOUT)
           with_redis do |redis|
             Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-              Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+              redis.pipelined do |pipeline|
                 mapping.each do |raw_key, value|
                   key = cache_key_for("#{key_prefix}#{raw_key}")
 
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index dc5f4e1b3243247bb25b8b68f8ec8ff87803e364..0d5a66175f8e520f9de489336a48e6af5ec62018 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -197,7 +197,9 @@ def read_cache
         record_hit_ratio(results)
 
         results.map! do |result|
-          Gitlab::Json.parse(gzip_decompress(result), symbolize_names: true) unless result.nil?
+          unless result.nil?
+            Gitlab::Json.parse(gzip_decompress(result.force_encoding(Encoding::UTF_8)), symbolize_names: true)
+          end
         end
 
         file_paths.zip(results).to_h
diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb
index 18ff7c28e174b6f29a4db719877a42bdf2241f70..dbdc8c3c95c7ca6f618cdf0749ea8dcfecf1238e 100644
--- a/lib/gitlab/discussions_diff/highlight_cache.rb
+++ b/lib/gitlab/discussions_diff/highlight_cache.rb
@@ -16,7 +16,7 @@ class << self
         def write_multiple(mapping)
           with_redis do |redis|
             Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-              Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipelined|
+              redis.pipelined do |pipelined|
                 mapping.each do |raw_key, value|
                   key = cache_key_for(raw_key)
 
@@ -42,7 +42,7 @@ def read_multiple(raw_keys)
             with_redis do |redis|
               Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
                 if Gitlab::Redis::ClusterUtil.cluster?(redis)
-                  Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+                  redis.pipelined do |pipeline|
                     keys.each { |key| pipeline.get(key) }
                   end
                 else
@@ -54,7 +54,7 @@ def read_multiple(raw_keys)
           content.map! do |lines|
             next unless lines
 
-            Gitlab::Json.parse(gzip_decompress(lines)).map! do |line|
+            Gitlab::Json.parse(gzip_decompress(lines.force_encoding(Encoding::UTF_8))).map! do |line|
               Gitlab::Diff::Line.safe_init_from_hash(line)
             end
           end
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
index 5fdf5ac9436275ffa5721545658598e949bb2dd7..57c160c9607e74b9ea58c437c4e35097af2c2bcd 100644
--- a/lib/gitlab/etag_caching/store.rb
+++ b/lib/gitlab/etag_caching/store.rb
@@ -17,7 +17,7 @@ def touch(*keys, only_if_missing: false)
 
         Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
           with_redis do |redis|
-            Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+            redis.pipelined do |pipeline|
               keys.each_with_index do |key, i|
                 pipeline.set(redis_shared_state_key(key), etags[i], ex: EXPIRY_TIME, nx: only_if_missing)
               end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 0b18a33770701353fb385d29408e9eb22f7560c3..bae1bf061433e24093174fbdedd8dbfbd4dfd0fd 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -60,6 +60,7 @@ def self.throttle(key, group: nil, period: 1.hour, count: 1, &block)
 
     def self.cancel(key, uuid)
       return unless key.present?
+      return unless uuid.present?
 
       Gitlab::Redis::SharedState.with do |redis|
         redis.eval(LUA_CANCEL_SCRIPT, keys: [ensure_prefixed_key(key)], argv: [uuid])
@@ -110,7 +111,7 @@ def waiting?
     # false if the lease is taken by a different UUID or inexistent.
     def renew
       Gitlab::Redis::SharedState.with do |redis|
-        result = redis.eval(LUA_RENEW_SCRIPT, keys: [@redis_shared_state_key], argv: [@uuid, @timeout])
+        result = redis.eval(LUA_RENEW_SCRIPT, keys: [@redis_shared_state_key], argv: [@uuid, @timeout.to_i])
         result == @uuid
       end
     end
diff --git a/lib/gitlab/instrumentation/redis_cluster_validator.rb b/lib/gitlab/instrumentation/redis_cluster_validator.rb
index 948132e6edddb973e1ad926884f73486efe64014..5c3655f8f803f0d203585e2e292aa730800d1c12 100644
--- a/lib/gitlab/instrumentation/redis_cluster_validator.rb
+++ b/lib/gitlab/instrumentation/redis_cluster_validator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 require 'rails'
-require 'redis'
+require 'redis-clustering'
 
 module Gitlab
   module Instrumentation
@@ -230,7 +230,7 @@ def has_cross_slot_keys?(keys)
         end
 
         def key_slot(key)
-          ::Redis::Cluster::KeySlotConverter.convert(extract_hash_tag(key))
+          ::RedisClient::Cluster::KeySlotConverter.convert(extract_hash_tag(key))
         end
 
         # This is almost identical to Redis::Cluster::Command#extract_hash_tag,
diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb
deleted file mode 100644
index 9c89af6a0dc5814b1a156faab19171be0b3dde5f..0000000000000000000000000000000000000000
--- a/lib/gitlab/instrumentation/redis_interceptor.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module Instrumentation
-    module RedisInterceptor
-      include RedisHelper
-
-      def call(command)
-        instrument_call([command], instrumentation_class) do
-          super
-        end
-      end
-
-      def call_pipeline(pipeline)
-        instrument_call(pipeline.commands, instrumentation_class, true) do
-          super
-        end
-      end
-
-      def write(command)
-        measure_write_size(command, instrumentation_class) if ::RequestStore.active?
-        super
-      end
-
-      def read
-        result = super
-        measure_read_size(result, instrumentation_class) if ::RequestStore.active?
-        result
-      end
-
-      def ensure_connected
-        super do
-          instrument_reconnection_errors do
-            yield
-          end
-        end
-      end
-
-      def instrument_reconnection_errors
-        yield
-      rescue ::Redis::BaseConnectionError => ex
-        instrumentation_class.instance_count_connection_exception(ex)
-
-        raise ex
-      end
-
-      # That's required so it knows which GitLab Redis instance
-      # it's interacting with in order to categorize accordingly.
-      #
-      def instrumentation_class
-        @options[:instrumentation_class] # rubocop:disable Gitlab/ModuleWithInstanceVariables
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/issues/rebalancing/state.rb b/lib/gitlab/issues/rebalancing/state.rb
index c60dac6f571e5e393bfa6438af86c309bb84bf59..12cc5f6e5ddf4d7dde5c242914e2831a0e76cd24 100644
--- a/lib/gitlab/issues/rebalancing/state.rb
+++ b/lib/gitlab/issues/rebalancing/state.rb
@@ -100,7 +100,7 @@ def remove_current_project_id_cache
         def refresh_keys_expiration
           with_redis do |redis|
             Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-              Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+              redis.pipelined do |pipeline|
                 pipeline.expire(issue_ids_key, REDIS_EXPIRY_TIME)
                 pipeline.expire(current_index_key, REDIS_EXPIRY_TIME)
                 pipeline.expire(current_project_key, REDIS_EXPIRY_TIME)
diff --git a/lib/gitlab/markdown_cache/redis/store.rb b/lib/gitlab/markdown_cache/redis/store.rb
index af9098c3300dea7564c3a4ea9239ead7a35cfe62..85aeaba92c4a5aee21e6b3c415903b4cb78dd2b3 100644
--- a/lib/gitlab/markdown_cache/redis/store.rb
+++ b/lib/gitlab/markdown_cache/redis/store.rb
@@ -11,7 +11,7 @@ def self.bulk_read(subjects)
 
           data = Gitlab::Redis::Cache.with do |r|
             Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-              Gitlab::Redis::CrossSlot::Pipeline.new(r).pipelined do |pipeline|
+              r.pipelined do |pipeline|
                 subjects.each do |subject|
                   new(subject).read(pipeline)
                 end
diff --git a/lib/gitlab/patch/command_loader.rb b/lib/gitlab/patch/command_loader.rb
deleted file mode 100644
index 357b6270b0dd597db0be6012580a3204789a88f6..0000000000000000000000000000000000000000
--- a/lib/gitlab/patch/command_loader.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module Patch
-    module CommandLoader
-      extend ActiveSupport::Concern
-
-      class_methods do
-        # Shuffle the node list to spread out initial connection creation amongst all nodes
-        #
-        # The input is a Redis::Cluster::Node instance which is an Enumerable.
-        #  `super` receives an Array of Redis::Client instead of a Redis::Cluster::Node
-        def load(nodes)
-          super(nodes.to_a.shuffle)
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/patch/node_loader.rb b/lib/gitlab/patch/node_loader.rb
deleted file mode 100644
index 85237abc1376b9340eee3df8ad0087049ea1d543..0000000000000000000000000000000000000000
--- a/lib/gitlab/patch/node_loader.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-# Patch to address https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2212#note_1287996694
-# It uses hostname instead of IP address if the former is present in `CLUSTER NODES` output.
-if Gem::Version.new(Redis::VERSION) > Gem::Version.new('4.8.1')
-  raise 'New version of redis detected, please remove or update this patch'
-end
-
-module Gitlab
-  module Patch
-    module NodeLoader
-      extend ActiveSupport::Concern
-
-      class_methods do
-        # Shuffle the node list to spread out initial connection creation amongst all nodes
-        #
-        # The input is a Redis::Cluster::Node instance which is an Enumerable.
-        #  `super` receives an Array of Redis::Client instead of a Redis::Cluster::Node
-        def load_flags(nodes)
-          super(nodes.to_a.shuffle)
-        end
-      end
-
-      def self.prepended(base)
-        base.class_eval do
-          # monkey-patches https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/cluster/node_loader.rb#L23
-          def self.fetch_node_info(node)
-            node.call(%i[cluster nodes]).split("\n").map(&:split).to_h do |arr|
-              [
-                extract_host_identifier(arr[1]),
-                (arr[2].split(',') & %w[master slave]).first # rubocop:disable Naming/InclusiveLanguage
-              ]
-            end
-          end
-
-          # Since `CLUSTER SLOT` uses the preferred endpoint determined by
-          # the `cluster-preferred-endpoint-type` config value, we will prefer hostname over IP address.
-          # See https://redis.io/commands/cluster-nodes/ for details on the output format.
-          #
-          # @param [String] Address info matching fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
-          def self.extract_host_identifier(node_address)
-            ip_chunk, hostname, _auxiliaries = node_address.split(',')
-            return ip_chunk.split('@').first if hostname.blank?
-
-            port = ip_chunk.split('@').first.split(':')[1]
-            "#{hostname}:#{port}"
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/patch/redis_cache_store.rb b/lib/gitlab/patch/redis_cache_store.rb
index 96729056ce5f765ca5c9a5ca1c7c00806e5b3776..35c617ba72b1ce671bdc723b5dbf425b2e351b70 100644
--- a/lib/gitlab/patch/redis_cache_store.rb
+++ b/lib/gitlab/patch/redis_cache_store.rb
@@ -20,7 +20,7 @@ def delete_multi_entries(entries, **options)
         delete_count = 0
         redis.with do |conn|
           entries.each_slice(pipeline_batch_size) do |subset|
-            delete_count += Gitlab::Redis::CrossSlot::Pipeline.new(conn).pipelined do |pipeline|
+            delete_count += conn.pipelined do |pipeline|
               subset.each { |entry| pipeline.del(entry) }
             end.sum
           end
@@ -58,7 +58,7 @@ def patched_read_multi_mget(*names)
 
       def pipeline_mget(conn, keys)
         keys.each_slice(pipeline_batch_size).flat_map do |subset|
-          Gitlab::Redis::CrossSlot::Pipeline.new(conn).pipelined do |p|
+          conn.pipelined do |p|
             subset.each { |key| p.get(key) }
           end
         end
diff --git a/lib/gitlab/patch/redis_cluster.rb b/lib/gitlab/patch/redis_cluster.rb
deleted file mode 100644
index 145ce35a31736165daba94c40bc2597debdb0a5f..0000000000000000000000000000000000000000
--- a/lib/gitlab/patch/redis_cluster.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# Patch to expose `find_node_key` method for cross-slot pipelining
-# In redis v5.0.x, cross-slot pipelining is implemented via redis-cluster-client.
-# This patch should be removed since there is no need for it.
-# Gitlab::Redis::CrossSlot and its usage should be removed as well.
-if Gem::Version.new(Redis::VERSION) != Gem::Version.new('4.8.0')
-  raise 'New version of redis detected, please remove or update this patch'
-end
-
-module Gitlab
-  module Patch
-    module RedisCluster
-      # _find_node_key exposes a private function of the same name in Redis::Cluster.
-      # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/cluster.rb#L282
-      def _find_node_key(command)
-        find_node_key(command)
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/patch/redis_store_factory.rb b/lib/gitlab/patch/redis_store_factory.rb
new file mode 100644
index 0000000000000000000000000000000000000000..81636ed665dc5e62662e08d0889f5eb7db2e009e
--- /dev/null
+++ b/lib/gitlab/patch/redis_store_factory.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Patch
+    module RedisStoreFactory
+      def create
+        # rubocop:disable Gitlab/ModuleWithInstanceVariables -- patched code references @options in redis-store
+        opt = @options
+        # rubocop:enable Gitlab/ModuleWithInstanceVariables
+        return Gitlab::Redis::ClusterStore.new(opt) if opt[:nodes]
+
+        super
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/patch/slot_loader.rb b/lib/gitlab/patch/slot_loader.rb
deleted file mode 100644
index e302d844078d7d2b9a4d8195ca14dc9b9a486992..0000000000000000000000000000000000000000
--- a/lib/gitlab/patch/slot_loader.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module Patch
-    module SlotLoader
-      extend ActiveSupport::Concern
-
-      class_methods do
-        # Shuffle the node list to spread out initial connection creation amongst all nodes
-        #
-        # The input is a Redis::Cluster::Node instance which is an Enumerable.
-        #  `super` receives an Array of Redis::Client instead of a Redis::Cluster::Node
-        def load(nodes)
-          super(nodes.to_a.shuffle)
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/redis/cluster_store.rb b/lib/gitlab/redis/cluster_store.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d660c782d4d0199a889316560ac0742786e09580
--- /dev/null
+++ b/lib/gitlab/redis/cluster_store.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'redis-clustering'
+require 'redis/store/ttl'
+require 'redis/store/interface'
+require 'redis/store/namespace'
+require 'redis/store/serialization'
+
+module Gitlab
+  module Redis
+    class ClusterStore < ::Redis::Cluster
+      include ::Redis::Store::Interface
+
+      def initialize(options = {})
+        orig_options = options.dup
+
+        @serializer = orig_options.key?(:serializer) ? orig_options.delete(:serializer) : Marshal
+
+        unless orig_options[:marshalling].nil?
+          # `marshalling` only used here, might not be supported in `super`
+          @serializer = orig_options.delete(:marshalling) ? Marshal : nil
+        end
+
+        _remove_unsupported_options(options)
+        super(options)
+
+        _extend_marshalling
+        _extend_namespace orig_options
+      end
+
+      # copies ::Redis::Store::Ttl implementation in a redis-v5 compatible manner
+      def set(key, value, options = nil)
+        ttl = get_ttl(options)
+        if ttl
+          setex(key, ttl.to_i, value, raw: true)
+        else
+          super(key, value)
+        end
+      end
+
+      # copies ::Redis::Store::Ttl implementation in a redis-v5 compatible manner
+      def setnx(key, value, options = nil)
+        ttl = get_ttl(options)
+        if ttl
+          multi do |m|
+            m.setnx(key, value)
+            m.expire(key, ttl)
+          end
+        else
+          super(key, value)
+        end
+      end
+
+      private
+
+      def get_ttl(options)
+        # https://github.com/redis-store/redis-store/blob/v1.10.0/lib/redis/store/ttl.rb#L37
+        options[:expire_after] || options[:expires_in] || options[:expire_in] if options
+      end
+
+      def _remove_unsupported_options(options)
+        # Unsupported keywords should be removed to avoid errors
+        # https://github.com/redis-rb/redis-client/blob/v0.13.0/lib/redis_client/config.rb#L21
+        options.delete(:raw)
+        options.delete(:serializer)
+        options.delete(:marshalling)
+        options.delete(:namespace)
+        options.delete(:scheme)
+      end
+
+      def _extend_marshalling
+        extend ::Redis::Store::Serialization unless @serializer.nil?
+      end
+
+      def _extend_namespace(options)
+        @namespace = options[:namespace]
+        extend ::Redis::Store::Namespace
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/redis/cluster_util.rb b/lib/gitlab/redis/cluster_util.rb
index 9e307940de3d17141e18e985226cf3e50661982e..99f337749d0d8bdf9970c1c5cf9470fc9cb57b14 100644
--- a/lib/gitlab/redis/cluster_util.rb
+++ b/lib/gitlab/redis/cluster_util.rb
@@ -13,14 +13,14 @@ def cluster?(obj)
           if obj.is_a?(MultiStore)
             cluster?(obj.primary_store) || cluster?(obj.secondary_store)
           else
-            obj.respond_to?(:_client) && obj._client.is_a?(::Redis::Cluster)
+            obj.is_a?(::Redis::Cluster)
           end
         end
 
         def batch_unlink(keys, redis)
           expired_count = 0
           keys.each_slice(1000) do |subset|
-            expired_count += Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+            expired_count += redis.pipelined do |pipeline|
               subset.each { |key| pipeline.unlink(key) }
             end.sum
           end
@@ -30,7 +30,7 @@ def batch_unlink(keys, redis)
         # Redis cluster alternative to mget
         def batch_get(keys, redis)
           keys.each_slice(1000).flat_map do |subset|
-            Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
+            redis.pipelined do |pipeline|
               subset.map { |key| pipeline.get(key) }
             end
           end
diff --git a/lib/gitlab/redis/command_builder.rb b/lib/gitlab/redis/command_builder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99d26760b3d3786edc0fc06ce951843271b827c7
--- /dev/null
+++ b/lib/gitlab/redis/command_builder.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Redis
+    module CommandBuilder
+      extend self
+
+      # Ref: https://github.com/redis-rb/redis-client/blob/v0.19.1/lib/redis_client/command_builder.rb
+      # we modify the command builder to convert nil to strings as this behaviour was present in
+      # https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/connection/command_helper.rb#L20
+      #
+      # Note that we only adopt the Ruby3.x-compatible logic in .generate.
+      # Symbol.method_defined?(:name) is true in Ruby 3
+      def generate(args, kwargs = nil)
+        command = args.flat_map do |element|
+          case element
+          when Hash
+            element.flatten
+          else
+            element
+          end
+        end
+
+        kwargs&.each do |key, value|
+          if value
+            if value == true
+              command << key.name
+            else
+              command << key.name << value
+            end
+          end
+        end
+
+        command.map! do |element|
+          case element
+          when String
+            element
+          when Symbol
+            element.name
+          when Integer, Float, NilClass
+            element.to_s
+          else
+            raise TypeError, "Unsupported command argument type: #{element.class}"
+          end
+        end
+
+        raise ArgumentError, "can't issue an empty redis command" if command.empty?
+
+        command
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/redis/cross_slot.rb b/lib/gitlab/redis/cross_slot.rb
deleted file mode 100644
index e5aa6d9ce7246d36db668e54c71c25f118860589..0000000000000000000000000000000000000000
--- a/lib/gitlab/redis/cross_slot.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
-  module Redis
-    module CrossSlot
-      class Router
-        attr_reader :node_mapping, :futures, :node_sequence, :cmd_queue
-
-        delegate :respond_to_missing?, to: :@redis
-
-        # This map contains redis-rb methods which does not map directly
-        # to a standard Redis command. It is used transform unsupported commands to standard commands
-        # to find the node key for unsupported commands.
-        #
-        # Redis::Cluster::Command only contains details of commands which the Redis Server
-        # returns. Hence, commands like mapped_hmget and hscan_each internally will call the
-        # base command, hmget and hscan respectively.
-        #
-        # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/cluster/command.rb
-        UNSUPPORTED_CMD_MAPPING = {
-          # Internally, redis-rb calls the supported Redis command and transforms the output.
-          # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/commands/hashes.rb#L104
-          mapped_hmget: :hmget
-        }.freeze
-
-        # Initializes the CrossSlot::Router
-        # @param {::Redis}
-        def initialize(redis)
-          @redis = redis
-          @node_mapping = {}
-          @futures = {}
-          @node_sequence = []
-          @cmd_queue = []
-        end
-
-        # For now we intercept every redis.call and return a Gitlab-Future object.
-        # This method groups every commands to a node for fan-out. Commands are grouped using the first key.
-        #
-        # rubocop:disable Style/MissingRespondToMissing
-        def method_missing(cmd, *args, **kwargs, &blk)
-          # Note that we can re-map the command without affecting execution as it is
-          # solely for finding the node key. The original cmd will be executed.
-          node = @redis._client._find_node_key([UNSUPPORTED_CMD_MAPPING.fetch(cmd, cmd)] + args)
-
-          @node_mapping[node] ||= []
-          @futures[node] ||= []
-
-          @node_sequence << node
-          @node_mapping[node] << [cmd, args, kwargs || {}, blk]
-          f = Future.new
-          @futures[node] << f
-          @cmd_queue << [f, cmd, args, kwargs || {}, blk]
-          f
-        end
-        # rubocop:enable Style/MissingRespondToMissing
-      end
-
-      # Wraps over redis-rb's Future in
-      # https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis/pipeline.rb#L244
-      class Future
-        def set(future, is_val = false)
-          @redis_future = future
-          @is_val = is_val
-        end
-
-        def value
-          return @redis_val if @is_val
-
-          @redis_future.value
-        end
-      end
-
-      # Pipeline allows cross-slot pipelined to be called. The fan-out logic is implemented in
-      # https://github.com/redis-rb/redis-cluster-client/blob/master/lib/redis_client/cluster/pipeline.rb
-      # which is available in redis-rb v5.0.
-      #
-      # This file can be deprecated after redis-rb v4.8.0 is upgraded to v5.0
-      class Pipeline
-        # Initializes the CrossSlot::Pipeline
-        # @param {::Redis}
-        def initialize(redis)
-          @redis = redis
-        end
-
-        # pipelined is used in place of ::Redis `.pipelined` when running in a cluster context
-        # where cross-slot operations may happen.
-        def pipelined(&block)
-          # Directly call .pipelined and defer the pipeline execution to MultiStore.
-          # MultiStore could wrap over 0, 1, or 2 Redis Cluster clients, handling it here
-          # will not work for 2 clients since the key-slot topology can differ.
-          if use_cross_slot_pipelining?
-            router = Router.new(@redis)
-            yield router
-            execute_commands(router)
-          else
-            # use redis-rb's pipelined method
-            @redis.pipelined(&block)
-          end
-        end
-
-        private
-
-        def use_cross_slot_pipelining?
-          !@redis.instance_of?(::Gitlab::Redis::MultiStore) && @redis._client.instance_of?(::Redis::Cluster)
-        end
-
-        def execute_commands(router)
-          router.node_mapping.each do |node_key, commands|
-            # TODO possibly use Threads to speed up but for now `n` is 3-5 which is small.
-            @redis.pipelined do |p|
-              commands.each_with_index do |command, idx|
-                future = router.futures[node_key][idx]
-                cmd, args, kwargs, blk = command
-                future.set(p.public_send(cmd, *args, **kwargs, &blk)) # rubocop:disable GitlabSecurity/PublicSend
-              end
-            end
-          end
-
-          router.node_sequence.map do |node_key|
-            router.futures[node_key].shift.value
-          end
-        rescue ::Redis::CommandError => err
-          if err.message.start_with?('MOVED', 'ASK')
-            Gitlab::ErrorTracking.log_exception(err)
-            return execute_commands_sequentially(router)
-          end
-
-          raise
-        end
-
-        def execute_commands_sequentially(router)
-          router.cmd_queue.map do |command|
-            future, cmd, args, kwargs, blk = command
-            future.set(@redis.public_send(cmd, *args, **kwargs, &blk), true) # rubocop:disable GitlabSecurity/PublicSend
-            future.value
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/redis/multi_store.rb b/lib/gitlab/redis/multi_store.rb
index e6262f7f61b516ec17e3f25316d47c8d6d97d42b..476077cb96f0dcbc248022b5db9abdb0b7183dbf 100644
--- a/lib/gitlab/redis/multi_store.rb
+++ b/lib/gitlab/redis/multi_store.rb
@@ -432,16 +432,6 @@ def same_redis_store?
 
       # rubocop:disable GitlabSecurity/PublicSend
       def send_command(redis_instance, command_name, *args, **kwargs, &block)
-        # Run wrapped pipeline for each instance individually so that the fan-out is distinct.
-        # If both primary and secondary are Redis Clusters, the slot-node distribution could
-        # be different.
-        #
-        # We ignore args and kwargs since `pipelined` does not accept arguments
-        # See https://github.com/redis/redis-rb/blob/v4.8.0/lib/redis.rb#L164
-        if command_name.to_s == 'pipelined' && redis_instance._client.instance_of?(::Redis::Cluster)
-          return Gitlab::Redis::CrossSlot::Pipeline.new(redis_instance).pipelined(&block)
-        end
-
         if block
           # Make sure that block is wrapped and executed only on the redis instance that is executing the block
           redis_instance.send(command_name, *args, **kwargs) do |*params|
@@ -462,7 +452,7 @@ def with_instance(instance, *params)
       end
 
       def redis_store?(pool)
-        pool.with { |c| c.instance_of?(Gitlab::Redis::MultiStore) || c.is_a?(::Redis) }
+        pool.with { |c| c.instance_of?(Gitlab::Redis::MultiStore) || c.is_a?(::Redis) || c.is_a?(::Redis::Cluster) }
       end
 
       def validate_stores!
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index a106ed3884ef34b3a7a469525018ddaaa8fa5db9..f7a7e703d90e7de08aca82ba94c3bd06dc916566 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -20,7 +20,7 @@ class Wrapper
       CommandExecutionError = Class.new(StandardError)
 
       class << self
-        delegate :params, :url, :store, :encrypted_secrets, :redis_client_params, to: :new
+        delegate :params, :url, :store, :encrypted_secrets, to: :new
 
         def with
           pool.with { |redis| yield redis }
@@ -90,7 +90,17 @@ def instrumentation_class
         end
 
         def redis
-          ::Redis.new(params)
+          init_redis(params)
+        end
+
+        private
+
+        def init_redis(config)
+          if config[:nodes].present?
+            ::Redis::Cluster.new(config.merge({ concurrency: { model: :none } }))
+          else
+            ::Redis.new(config)
+          end
         end
       end
 
@@ -99,13 +109,8 @@ def initialize(rails_env = nil)
       end
 
       def params
-        redis_store_options
-      end
-
-      # redis_client_params modifies redis_store_options to be compatible with redis-client
-      # TODO: when redis-rb is updated to v5, there is no need to support 2 types of config format
-      def redis_client_params
         options = redis_store_options
+        options[:command_builder] = CommandBuilder
 
         # avoid passing classes into options as Sidekiq scrubs the options with Marshal.dump + Marshal.load
         # ref https://github.com/sidekiq/sidekiq/blob/v7.1.6/lib/sidekiq/redis_connection.rb#L37
@@ -114,14 +119,14 @@ def redis_client_params
         # we use strings to look up Gitlab::Instrumentation::Redis.storage_hash as a bypass
         options[:custom] = { instrumentation_class: self.class.store_name }
 
-        # TODO: add support for cluster when upgrading to redis-rb v5.y.z we do not need cluster support
-        # as Sidekiq workload should not and does not run in a Redis Cluster
-        # support to be added in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134862
         if options[:sentinels]
           # name is required in RedisClient::SentinelConfig
           # https://github.com/redis-rb/redis-client/blob/1ab081c1d0e47df5d55e011c9390c70b2eef6731/lib/redis_client/sentinel_config.rb#L17
           options[:name] = options[:host]
           options.except(:scheme, :instrumentation_class, :host, :port)
+        elsif options[:cluster]
+          options[:nodes] = options[:cluster].map { |c| c.except(:scheme) }
+          options.except(:scheme, :instrumentation_class, :cluster)
         else
           # remove disallowed keys as seen in
           # https://github.com/redis-rb/redis-client/blob/1ab081c1d0e47df5d55e011c9390c70b2eef6731/lib/redis_client/config.rb#L21
@@ -134,7 +139,7 @@ def url
       end
 
       def db
-        redis_store_options[:db]
+        redis_store_options[:db] || 0
       end
 
       def sentinels
@@ -156,7 +161,7 @@ def sentinels?
       end
 
       def store(extras = {})
-        ::Redis::Store::Factory.create(redis_store_options.merge(extras))
+        ::Redis::Store::Factory.create(params.merge(extras))
       end
 
       def encrypted_secrets
@@ -182,7 +187,11 @@ def redis_store_options
         final_config = parse_extra_config(decrypted_config)
 
         result = if final_config[:cluster].present?
-                   final_config[:db] = 0 # Redis Cluster only supports db 0
+                   final_config[:cluster] = final_config[:cluster].map do |node|
+                     next node unless node.is_a?(String)
+
+                     ::Redis::Store::Factory.extract_host_options_from_uri(node)
+                   end
                    final_config
                  else
                    parse_redis_url(final_config)
diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb
index fab0e9e09e8dae73ce14061908b5b36152085896..6da5556833f2ecd81d6c8e7342214a841bc9ae09 100644
--- a/lib/gitlab/repository_hash_cache.rb
+++ b/lib/gitlab/repository_hash_cache.rb
@@ -86,6 +86,8 @@ def write(key, hash)
 
       full_key = cache_key(key)
 
+      hash = standardize_hash(hash)
+
       with do |redis|
         results = redis.pipelined do |pipeline|
           # Set each hash key to the provided value
diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb
index eb73a0a3d31eba41e0f9d26c579964b977219871..967a139e7fb3c1e109f6aa6352de4c352f8c4ef1 100644
--- a/lib/gitlab/set_cache.rb
+++ b/lib/gitlab/set_cache.rb
@@ -62,7 +62,7 @@ def try_include?(key, value)
 
       with do |redis|
         redis.multi do |multi|
-          multi.sismember(full_key, value)
+          multi.sismember(full_key, value.to_s)
           multi.exists?(full_key) # rubocop:disable CodeReuse/ActiveRecord
         end
       end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 883e1ba0558dd65b82852fffb0e0ff7995b77048..c40f125db40d4230a12f063ea7f5c7ddeadcb58a 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -75,7 +75,7 @@ def update_latest_wal_location!
           argv = []
           job_wal_locations.each do |connection_name, location|
             diff = pg_wal_lsn_diff(connection_name)
-            argv += [connection_name, diff || '', location]
+            argv += [connection_name, diff ? diff.to_f : '', location]
           end
 
           with_redis { |r| r.eval(UPDATE_WAL_COOKIE_SCRIPT, keys: [cookie_key], argv: argv) }
diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb
index 7b48782be989e438840e02b4b4ca5532a8c14c29..247be5429a1df4076f8e033eb293ea861c1eb00e 100644
--- a/spec/controllers/concerns/product_analytics_tracking_spec.rb
+++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb
@@ -109,7 +109,8 @@ def expect_no_internal_tracking
       end
 
       it 'tracks total Redis counters' do
-        expect(Gitlab::Usage::Metrics::Instrumentations::TotalCountMetric).to receive(:redis_key).twice # total and 7d
+        expect(Gitlab::Usage::Metrics::Instrumentations::TotalCountMetric).to receive(:redis_key)
+          .twice.and_call_original # total and 7d
 
         get :index
       end
diff --git a/spec/fixtures/config/redis_new_format_host_standalone.yml b/spec/fixtures/config/redis_new_format_host_standalone.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f5836586247389bc74ca40cab1a21d1284671fac
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_host_standalone.yml
@@ -0,0 +1,8 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development:
+  url: redis://:mynewpassword@development-host:6379/99
+test:
+  url: redis://:mynewpassword@test-host:6379/99
+production:
+  url: redis://:mynewpassword@production-host:6379/99
diff --git a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
index cf82fd751ddea57ebd694267227670f136e5d4de..d89ad21ca82bd57574854736b521842cd727bb45 100644
--- a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
+++ b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb
@@ -27,7 +27,7 @@
 
         sub = ActionCable.server.pubsub.send(:redis_connection)
 
-        expect(sub.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
+        expect(sub.connection[:id]).to eq('/home/localuser/redis/redis.socket/0')
         expect(ActionCable.server.config.cable[:id]).to be_nil
       end
     end
diff --git a/spec/initializers/session_store_spec.rb b/spec/initializers/session_store_spec.rb
index c9333d022dd31a6450b2bdc7582cb36f4492cc56..bd943830f6e879315a9bb38d4530096c185e117f 100644
--- a/spec/initializers/session_store_spec.rb
+++ b/spec/initializers/session_store_spec.rb
@@ -15,7 +15,7 @@
 
   describe 'config#session_store' do
     it 'initialized as a redis_store with a proper servers configuration' do
-      expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
+      expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_server: Gitlab::Redis::Sessions.params.merge(namespace: Gitlab::Redis::Sessions::SESSION_NAMESPACE)))
 
       load_session_store
     end
diff --git a/spec/lib/click_house/migration_support/exclusive_lock_spec.rb b/spec/lib/click_house/migration_support/exclusive_lock_spec.rb
index 5176cc752666addf7eaef212f137ee18d22b784b..17e58a4ddef60f6df3b98eec9198ead3717cf8ce 100644
--- a/spec/lib/click_house/migration_support/exclusive_lock_spec.rb
+++ b/spec/lib/click_house/migration_support/exclusive_lock_spec.rb
@@ -5,6 +5,8 @@
 RSpec.describe ClickHouse::MigrationSupport::ExclusiveLock, feature_category: :database do
   include ExclusiveLeaseHelpers
 
+  let(:worker_id) { 1 }
+
   let(:worker_class) do
     # This worker will be active longer than the ClickHouse worker TTL
     Class.new do
@@ -81,7 +83,7 @@ def migration
       end
 
       around do |example|
-        described_class.register_running_worker(worker_class, anything) do
+        described_class.register_running_worker(worker_class, worker_id) do
           example.run
         end
       end
diff --git a/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb b/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb
index c52d1b4c9f2a593f45bf90b3517ef0d24b312504..a55ae5711a8d040f39e57d0ce884a8112cddb8a0 100644
--- a/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb
+++ b/spec/lib/gitlab/background_migration/redis/backfill_project_pipeline_status_ttl_spec.rb
@@ -4,7 +4,7 @@
 
 RSpec.describe Gitlab::BackgroundMigration::Redis::BackfillProjectPipelineStatusTtl,
   :clean_gitlab_redis_cache, feature_category: :redis do
-  let(:redis) { ::Redis.new(::Gitlab::Redis::Cache.params) }
+  let(:redis) { ::Gitlab::Redis::Cache.redis }
   let(:keys) { ["cache:gitlab:project:1:pipeline_status", "cache:gitlab:project:2:pipeline_status"] }
   let(:invalid_keys) { ["cache:gitlab:project:pipeline_status:1", "cache:gitlab:project:pipeline_status:2"] }
 
diff --git a/spec/lib/gitlab/database/migrations/runner_backoff/communicator_spec.rb b/spec/lib/gitlab/database/migrations/runner_backoff/communicator_spec.rb
index cfc3fb398e24eaa82ee130678c84389444e9dd80..9f1ade529b1822dac2f9b04fb77c7ba0f8d41eac 100644
--- a/spec/lib/gitlab/database/migrations/runner_backoff/communicator_spec.rb
+++ b/spec/lib/gitlab/database/migrations/runner_backoff/communicator_spec.rb
@@ -28,7 +28,7 @@
 
     it 'reads from Redis' do
       recorder = RedisCommands::Recorder.new { subject }
-      expect(recorder.log).to include([:exists, 'gitlab:exclusive_lease:gitlab/database/migration/runner/backoff'])
+      expect(recorder.log).to include(['exists', 'gitlab:exclusive_lease:gitlab/database/migration/runner/backoff'])
     end
 
     context 'with runner_migrations_backoff disabled' do
diff --git a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
index 30981e4bd7d1185f7bbd12110319e425f5448804..8068be798d392fed31a05fe045d4a8788acf0368 100644
--- a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
@@ -33,7 +33,7 @@ def fake_file(offset)
 
       mapping.each do |key, value|
         full_key = described_class.cache_key_for(key)
-        found_key = Gitlab::Redis::Cache.with { |r| r.get(full_key) }
+        found_key = Gitlab::Redis::Cache.with { |r| r.get(full_key).force_encoding("UTF-8") }
 
         expect(described_class.gzip_decompress(found_key)).to eq(value.to_json)
       end
diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb
index 186bf7d7ec15f1e49448ca556421a5f9191814bc..3dc23422e4edc03dd86ded7a406b7deb291a0d8a 100644
--- a/spec/lib/gitlab/external_authorization/cache_spec.rb
+++ b/spec/lib/gitlab/external_authorization/cache_spec.rb
@@ -23,9 +23,9 @@ def set_in_redis(key, value)
   describe '#load' do
     it 'reads stored info from redis' do
       freeze_time do
-        set_in_redis(:access, false)
+        set_in_redis(:access, false.to_s)
         set_in_redis(:reason, 'Access denied for now')
-        set_in_redis(:refreshed_at, Time.now)
+        set_in_redis(:refreshed_at, Time.now.to_s)
 
         access, reason, refreshed_at = cache.load
 
diff --git a/spec/lib/gitlab/instrumentation/redis_client_middleware_spec.rb b/spec/lib/gitlab/instrumentation/redis_client_middleware_spec.rb
index a8bded69696e656992f22dcbeffb2141c595e81c..2eb77add2ed6997542fa009acf2c708949d624d8 100644
--- a/spec/lib/gitlab/instrumentation/redis_client_middleware_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_client_middleware_spec.rb
@@ -9,10 +9,9 @@
   include RedisHelpers
 
   let_it_be(:redis_store_class) { define_helper_redis_store_class }
-  let_it_be(:redis_client) { RedisClient.new(redis_store_class.redis_client_params) }
 
   before do
-    redis_client.call("flushdb")
+    redis_store_class.with(&:flushdb)
   end
 
   describe 'read and write' do
@@ -24,27 +23,30 @@
       # The response is 1001, so 4 bytes. Exercise counting an integer reply.
       [[:set, 'foobar', 1000]] | [:incr, 'foobar'] | (4 + 6) | 4
 
-      # Exercise counting empty multi bulk reply. Returns an empty hash `{}`
-      [] | [:hgetall, 'foobar'] | (7 + 6) | 2
+      # Exercise counting empty multi bulk reply.
+      [] | [:hgetall, 'foobar'] | (7 + 6) | 0
 
       # Hgetall response length is combined length of keys and values in the
       # hash. Exercises counting of a multi bulk reply
-      # Returns `{"field"=>"hello world"}`, 5 for field, 11 for hello world, 8 for {, }, 4 "s, =, >
-      [[:hset, 'myhash', 'field', 'hello world']] | [:hgetall, 'myhash'] | (7 + 6) | (5 + 11 + 8)
+      [[:hset, 'myhash', 'field', 'hello world']] | [:hgetall, 'myhash'] | (7 + 6) | (5 + 11)
 
       # Exercise counting of a bulk reply
       [[:set, 'foo', 'bar' * 100]] | [:get, 'foo'] | (3 + 3) | (3 * 100)
 
-      # Nested array response: [['foo', 0.0], ['bar', 1.0]]. Returns scores as float.
+      # Nested array response: [['foo', 0], ['bar', 1.1000000000000001]] due to Redis precision
+      # See https://github.com/redis/redis/issues/1499
       [[:zadd, 'myset', 0, 'foo'],
-        [:zadd, 'myset', 1, 'bar']] | [:zrange, 'myset', 0, -1, 'withscores'] | (6 + 5 + 1 + 2 + 10) | (3 + 3 + 3 + 3)
+        [:zadd, 'myset', 1.1,
+          'bar']] | [:zrange, 'myset', 0, -1, 'withscores'] | (6 + 5 + 1 + 2 + 10) | (3 + 1 + 3 + 18)
     end
 
     with_them do
       it 'counts bytes read and written' do
-        setup.each { |cmd| redis_client.call(*cmd) }
-        RequestStore.clear!
-        redis_client.call(*command)
+        redis_store_class.with do |redis|
+          setup.each { |cmd| redis.call(cmd) }
+          RequestStore.clear!
+          redis.call(command)
+        end
 
         expect(Gitlab::Instrumentation::Redis.read_bytes).to eq(expect_read)
         expect(Gitlab::Instrumentation::Redis.write_bytes).to eq(expect_write)
@@ -58,35 +60,48 @@
     it 'counts successful requests' do
       expect(instrumentation_class).to receive(:instance_count_request).with(1).and_call_original
 
-      redis_client.call(:get, 'foobar')
+      redis_store_class.with { |redis| redis.call(:get, 'foobar') }
     end
 
     it 'counts successful pipelined requests' do
       expect(instrumentation_class).to receive(:instance_count_request).with(2).and_call_original
       expect(instrumentation_class).to receive(:instance_count_pipelined_request).with(2).and_call_original
 
-      redis_client.pipelined do |pipeline|
-        pipeline.call(:get, '{foobar}buz')
-        pipeline.call(:get, '{foobar}baz')
+      redis_store_class.with do |redis|
+        redis.pipelined do |pipeline|
+          pipeline.call(:get, '{foobar}buz')
+          pipeline.call(:get, '{foobar}baz')
+        end
       end
     end
 
     context 'when encountering exceptions' do
-      before do
-        allow(redis_client.instance_variable_get(:@raw_connection)).to receive(:call).and_raise(
-          RedisClient::Error)
+      where(:case_name, :exception, :exception_counter) do
+        'generic exception' | Redis::CommandError.new                             | :instance_count_exception
+        'moved redirection' | Redis::CommandError.new("MOVED 123 127.0.0.1:6380") | :instance_count_cluster_redirection
+        'ask redirection'   | Redis::CommandError.new("ASK 123 127.0.0.1:6380")   | :instance_count_cluster_redirection
       end
 
-      it 'counts exception' do
-        expect(instrumentation_class).to receive(:instance_count_exception)
-                                           .with(instance_of(RedisClient::Error)).and_call_original
-        expect(instrumentation_class).to receive(:log_exception)
-                                           .with(instance_of(RedisClient::Error)).and_call_original
-        expect(instrumentation_class).to receive(:instance_count_request).and_call_original
+      with_them do
+        before do
+          redis_store_class.with do |redis|
+            # We need to go 1 layer deeper to stub _client as we monkey-patch Redis::Client
+            # with the interceptor. Stubbing `redis` will skip the instrumentation_class.
+            allow(redis._client.instance_variable_get(:@raw_connection)).to receive(:call).and_raise(exception)
+          end
+        end
+
+        it 'counts exception' do
+          expect(instrumentation_class).to receive(exception_counter)
+                                             .with(instance_of(Redis::CommandError)).and_call_original
+          expect(instrumentation_class).to receive(:log_exception)
+                                             .with(instance_of(Redis::CommandError)).and_call_original
+          expect(instrumentation_class).to receive(:instance_count_request).and_call_original
 
-        expect do
-          redis_client.call(:auth, 'foo', 'bar')
-        end.to raise_error(RedisClient::Error)
+          expect do
+            redis_store_class.with { |redis| redis.call(:auth, 'foo', 'bar') }
+          end.to raise_exception(Redis::CommandError)
+        end
       end
     end
 
@@ -99,7 +114,7 @@
         expect(instrumentation_class).to receive(:increment_cross_slot_request_count).and_call_original
         expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
 
-        redis_client.call(:mget, 'foo', 'bar')
+        redis_store_class.with { |redis| redis.call(:mget, 'foo', 'bar') }
       end
 
       it 'does not count allowed cross-slot requests' do
@@ -107,7 +122,7 @@
         expect(instrumentation_class).to receive(:increment_allowed_cross_slot_request_count).and_call_original
 
         Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-          redis_client.call(:mget, 'foo', 'bar')
+          redis_store_class.with { |redis| redis.call(:mget, 'foo', 'bar') }
         end
       end
 
@@ -116,7 +131,7 @@
         expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
 
         Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-          redis_client.call(:mget, 'bar')
+          redis_store_class.with { |redis| redis.call(:get, 'bar') }
         end
       end
 
@@ -124,7 +139,7 @@
         expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
         expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
 
-        redis_client.call(:mget, '{foo}bar', '{foo}baz')
+        redis_store_class.with { |redis| redis.call(:mget, '{foo}bar', '{foo}baz') }
       end
     end
 
@@ -135,7 +150,7 @@
 
       it 'still runs cross-slot validation' do
         expect do
-          redis_client.call('mget', 'foo', 'bar')
+          redis_store_class.with { |redis| redis.mget('foo', 'bar') }
         end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
       end
     end
@@ -157,7 +172,7 @@
           expect(instrumentation_class).to receive(:instance_observe_duration).with(a_value > 0)
             .and_call_original
 
-          redis_client.call(*command)
+          redis_store_class.with { |redis| redis.call(*command) }
         end
       end
 
@@ -166,17 +181,21 @@
           expect(instrumentation_class).to receive(:instance_observe_duration).twice.with(a_value > 0)
             .and_call_original
 
-          redis_client.pipelined do |pipeline|
-            pipeline.call(:get, '{foobar}buz')
-            pipeline.call(:get, '{foobar}baz')
+          redis_store_class.with do |redis|
+            redis.pipelined do |pipeline|
+              pipeline.call(:get, '{foobar}buz')
+              pipeline.call(:get, '{foobar}baz')
+            end
           end
         end
 
         it 'raises error when keys are not from the same slot' do
           expect do
-            redis_client.pipelined do |pipeline|
-              pipeline.call(:get, 'foo')
-              pipeline.call(:get, 'bar')
+            redis_store_class.with do |redis|
+              redis.pipelined do |pipeline|
+                pipeline.call(:get, 'foo')
+                pipeline.call(:get, 'bar')
+              end
             end
           end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
         end
@@ -200,11 +219,11 @@
 
       with_them do
         it 'skips requests we do not want in the apdex' do
-          setup.each { |cmd| redis_client.call(*cmd) }
+          setup.each { |cmd| redis_store_class.with { |redis| redis.call(*cmd) } }
 
           expect(instrumentation_class).not_to receive(:instance_observe_duration)
 
-          redis_client.call(*command)
+          redis_store_class.with { |redis| redis.call(*command) }
         end
       end
 
@@ -212,10 +231,12 @@
         it 'skips requests that have blocking commands' do
           expect(instrumentation_class).not_to receive(:instance_observe_duration)
 
-          redis_client.pipelined do |pipeline|
-            pipeline.call(:get, '{foobar}buz')
-            pipeline.call(:rpush, '{foobar}baz', 1)
-            pipeline.call(:brpop, '{foobar}baz', 0)
+          redis_store_class.with do |redis|
+            redis.pipelined do |pipeline|
+              pipeline.call(:get, '{foobar}buz')
+              pipeline.call(:rpush, '{foobar}baz', 1)
+              pipeline.call(:brpop, '{foobar}baz', 0)
+            end
           end
         end
       end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
deleted file mode 100644
index 2a160a9d31662644ce4211fc0f4e2b43072be7ae..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ /dev/null
@@ -1,261 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require 'rspec-parameterized'
-require 'support/helpers/rails_helpers'
-
-RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :request_store, feature_category: :scalability do
-  using RSpec::Parameterized::TableSyntax
-  include RedisHelpers
-
-  let_it_be(:redis_store_class) { define_helper_redis_store_class }
-
-  before do
-    redis_store_class.with(&:flushdb)
-  end
-
-  describe 'read and write' do
-    where(:setup, :command, :expect_write, :expect_read) do
-      # The response is 'OK', the request size is the combined size of array
-      # elements. Exercise counting of a status reply.
-      [] | [:set, 'foo', 'bar'] | 3 + 3 + 3 | 2
-
-      # The response is 1001, so 4 bytes. Exercise counting an integer reply.
-      [[:set, 'foobar', 1000]] | [:incr, 'foobar'] | 4 + 6 | 4
-
-      # Exercise counting empty multi bulk reply
-      [] | [:hgetall, 'foobar'] | 7 + 6 | 0
-
-      # Hgetall response length is combined length of keys and values in the
-      # hash. Exercises counting of a multi bulk reply
-      [[:hset, 'myhash', 'field', 'hello world']] | [:hgetall, 'myhash'] | 7 + 6 | 5 + 11
-
-      # Exercise counting of a bulk reply
-      [[:set, 'foo', 'bar' * 100]] | [:get, 'foo'] | 3 + 3 | 3 * 100
-
-      # Nested array response: [['foo', 0], ['bar', 1]]
-      [[:zadd, 'myset', 0, 'foo'], [:zadd, 'myset', 1, 'bar']] | [:zrange, 'myset', 0, -1, 'withscores'] | 6 + 5 + 1 + 2 + 10 | 3 + 1 + 3 + 1
-    end
-
-    with_them do
-      it 'counts bytes read and written' do
-        redis_store_class.with do |redis|
-          setup.each { |cmd| redis.call(cmd) }
-          RequestStore.clear!
-          redis.call(command)
-        end
-
-        expect(Gitlab::Instrumentation::Redis.read_bytes).to eq(expect_read)
-        expect(Gitlab::Instrumentation::Redis.write_bytes).to eq(expect_write)
-      end
-    end
-  end
-
-  describe 'counting' do
-    let(:instrumentation_class) { redis_store_class.instrumentation_class }
-
-    it 'counts successful requests' do
-      expect(instrumentation_class).to receive(:instance_count_request).with(1).and_call_original
-
-      redis_store_class.with { |redis| redis.call(:get, 'foobar') }
-    end
-
-    it 'counts successful pipelined requests' do
-      expect(instrumentation_class).to receive(:instance_count_request).with(2).and_call_original
-      expect(instrumentation_class).to receive(:instance_count_pipelined_request).with(2).and_call_original
-
-      redis_store_class.with do |redis|
-        redis.pipelined do |pipeline|
-          pipeline.call(:get, '{foobar}buz')
-          pipeline.call(:get, '{foobar}baz')
-        end
-      end
-    end
-
-    context 'when encountering connection exceptions within process' do
-      before do
-        redis_store_class.with do |redis|
-          allow(redis._client).to receive(:write).and_call_original
-        end
-      end
-
-      it 'counts connection exceptions' do
-        redis_store_class.with do |redis|
-          expect(redis._client).to receive(:write).with([:get, 'foobar']).and_raise(::Redis::ConnectionError)
-        end
-
-        expect(instrumentation_class).to receive(:instance_count_connection_exception)
-                                             .with(instance_of(Redis::ConnectionError)).and_call_original
-
-        redis_store_class.with { |redis| redis.call(:get, 'foobar') }
-      end
-    end
-
-    context 'when encountering exceptions' do
-      where(:case_name, :exception, :exception_counter) do
-        'generic exception' | Redis::CommandError                                 | :instance_count_exception
-        'moved redirection' | Redis::CommandError.new("MOVED 123 127.0.0.1:6380") | :instance_count_cluster_redirection
-        'ask redirection'   | Redis::CommandError.new("ASK 123 127.0.0.1:6380")   | :instance_count_cluster_redirection
-      end
-
-      with_them do
-        before do
-          redis_store_class.with do |redis|
-            # We need to go 1 layer deeper to stub _client as we monkey-patch Redis::Client
-            # with the interceptor. Stubbing `redis` will skip the instrumentation_class.
-            allow(redis._client).to receive(:process).and_raise(exception)
-          end
-        end
-
-        it 'counts exception' do
-          expect(instrumentation_class).to receive(exception_counter)
-                                             .with(instance_of(Redis::CommandError)).and_call_original
-          expect(instrumentation_class).to receive(:log_exception)
-                                             .with(instance_of(Redis::CommandError)).and_call_original
-          expect(instrumentation_class).to receive(:instance_count_request).and_call_original
-
-          expect do
-            redis_store_class.with { |redis| redis.call(:auth, 'foo', 'bar') }
-          end.to raise_exception(Redis::CommandError)
-
-          expect(Thread.current[:redis_client_error_count]).to eq(0)
-        end
-      end
-    end
-
-    context 'in production environment' do
-      before do
-        stub_rails_env('production') # to avoid raising CrossSlotError
-      end
-
-      it 'counts disallowed cross-slot requests' do
-        expect(instrumentation_class).to receive(:increment_cross_slot_request_count).and_call_original
-        expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
-
-        redis_store_class.with { |redis| redis.call(:mget, 'foo', 'bar') }
-      end
-
-      it 'does not count allowed cross-slot requests' do
-        expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
-        expect(instrumentation_class).to receive(:increment_allowed_cross_slot_request_count).and_call_original
-
-        Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-          redis_store_class.with { |redis| redis.call(:mget, 'foo', 'bar') }
-        end
-      end
-
-      it 'does not count allowed non-cross-slot requests' do
-        expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
-        expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
-
-        Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-          redis_store_class.with { |redis| redis.call(:get, 'bar') }
-        end
-      end
-
-      it 'skips count for non-cross-slot requests' do
-        expect(instrumentation_class).not_to receive(:increment_cross_slot_request_count).and_call_original
-        expect(instrumentation_class).not_to receive(:increment_allowed_cross_slot_request_count).and_call_original
-
-        redis_store_class.with { |redis| redis.call(:mget, '{foo}bar', '{foo}baz') }
-      end
-    end
-
-    context 'without active RequestStore' do
-      before do
-        ::RequestStore.end!
-      end
-
-      it 'still runs cross-slot validation' do
-        expect do
-          redis_store_class.with { |redis| redis.mget('foo', 'bar') }
-        end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
-      end
-    end
-  end
-
-  describe 'latency' do
-    let(:instrumentation_class) { redis_store_class.instrumentation_class }
-
-    describe 'commands in the apdex' do
-      where(:command) do
-        [
-          [[:get, 'foobar']],
-          [%w[GET foobar]]
-        ]
-      end
-
-      with_them do
-        it 'measures requests we want in the apdex' do
-          expect(instrumentation_class).to receive(:instance_observe_duration).with(a_value > 0)
-            .and_call_original
-
-          redis_store_class.with { |redis| redis.call(*command) }
-        end
-      end
-
-      context 'with pipelined commands' do
-        it 'measures requests that do not have blocking commands' do
-          expect(instrumentation_class).to receive(:instance_observe_duration).twice.with(a_value > 0)
-            .and_call_original
-
-          redis_store_class.with do |redis|
-            redis.pipelined do |pipeline|
-              pipeline.call(:get, '{foobar}buz')
-              pipeline.call(:get, '{foobar}baz')
-            end
-          end
-        end
-
-        it 'raises error when keys are not from the same slot' do
-          expect do
-            redis_store_class.with do |redis|
-              redis.pipelined do |pipeline|
-                pipeline.call(:get, 'foo')
-                pipeline.call(:get, 'bar')
-              end
-            end
-          end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
-        end
-      end
-    end
-
-    describe 'commands not in the apdex' do
-      where(:setup, :command) do
-        [['rpush', 'foobar', 1]] | ['brpop', 'foobar', 0]
-        [['rpush', 'foobar', 1]] | ['blpop', 'foobar', 0]
-        [['rpush', '{abc}foobar', 1]] | ['brpoplpush', '{abc}foobar', '{abc}bazqux', 0]
-        [['rpush', '{abc}foobar', 1]] | ['brpoplpush', '{abc}foobar', '{abc}bazqux', 0]
-        [['zadd', 'foobar', 1, 'a']] | ['bzpopmin', 'foobar', 0]
-        [['zadd', 'foobar', 1, 'a']] | ['bzpopmax', 'foobar', 0]
-        [['xadd', 'mystream', 1, 'myfield', 'mydata']] | ['xread', 'block', 1, 'streams', 'mystream', '0-0']
-        [['xadd', 'foobar', 1, 'myfield', 'mydata'], ['xgroup', 'create', 'foobar', 'mygroup', 0]] | ['xreadgroup', 'group', 'mygroup', 'myconsumer', 'block', 1, 'streams', 'foobar', '0-0']
-        [] | ['command']
-      end
-
-      with_them do
-        it 'skips requests we do not want in the apdex' do
-          redis_store_class.with { |redis| setup.each { |cmd| redis.call(*cmd) } }
-
-          expect(instrumentation_class).not_to receive(:instance_observe_duration)
-
-          redis_store_class.with { |redis| redis.call(*command) }
-        end
-      end
-
-      context 'with pipelined commands' do
-        it 'skips requests that have blocking commands' do
-          expect(instrumentation_class).not_to receive(:instance_observe_duration)
-
-          redis_store_class.with do |redis|
-            redis.pipelined do |pipeline|
-              pipeline.call(:get, '{foobar}buz')
-              pipeline.call(:rpush, '{foobar}baz', 1)
-              pipeline.call(:brpop, '{foobar}baz', 0)
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/patch/node_loader_spec.rb b/spec/lib/gitlab/patch/node_loader_spec.rb
deleted file mode 100644
index 000083fc6d06ba65665667fce0eb47e9398c6ee6..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/patch/node_loader_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Patch::NodeLoader, feature_category: :redis do
-  using RSpec::Parameterized::TableSyntax
-
-  describe '#fetch_node_info' do
-    let(:redis) { double(:redis) } # rubocop:disable RSpec/VerifiedDoubles
-
-    # rubocop:disable Naming/InclusiveLanguage
-    where(:case_name, :args, :value) do
-      [
-        [
-          'when only ip address is present',
-          "07c37df 127.0.0.1:30004@31004 slave e7d1eec 0 1426238317239 4 connected
-67ed2db 127.0.0.1:30002@31002 master - 0 1426238316232 2 connected 5461-10922
-292f8b3 127.0.0.1:30003@31003 master - 0 1426238318243 3 connected 10923-16383
-6ec2392 127.0.0.1:30005@31005 slave 67ed2db 0 1426238316232 5 connected
-824fe11 127.0.0.1:30006@31006 slave 292f8b3 0 1426238317741 6 connected
-e7d1eec 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-5460",
-          {
-            '127.0.0.1:30004' => 'slave', '127.0.0.1:30002' => 'master', '127.0.0.1:30003' => 'master',
-            '127.0.0.1:30005' => 'slave', '127.0.0.1:30006' => 'slave', '127.0.0.1:30001' => 'master'
-          }
-        ],
-        [
-          'when hostname is present',
-          "07c37df 127.0.0.1:30004@31004,host1 slave e7d1eec 0 1426238317239 4 connected
-67ed2db 127.0.0.1:30002@31002,host2 master - 0 1426238316232 2 connected 5461-10922
-292f8b3 127.0.0.1:30003@31003,host3 master - 0 1426238318243 3 connected 10923-16383
-6ec2392 127.0.0.1:30005@31005,host4 slave 67ed2db 0 1426238316232 5 connected
-824fe11 127.0.0.1:30006@31006,host5 slave 292f8b3 0 1426238317741 6 connected
-e7d1eec 127.0.0.1:30001@31001,host6 myself,master - 0 0 1 connected 0-5460",
-          {
-            'host1:30004' => 'slave', 'host2:30002' => 'master', 'host3:30003' => 'master',
-            'host4:30005' => 'slave', 'host5:30006' => 'slave', 'host6:30001' => 'master'
-          }
-        ],
-        [
-          'when auxiliary fields are present',
-          "07c37df 127.0.0.1:30004@31004,,shard-id=69bc slave e7d1eec 0 1426238317239 4 connected
-67ed2db 127.0.0.1:30002@31002,,shard-id=114f master - 0 1426238316232 2 connected 5461-10922
-292f8b3 127.0.0.1:30003@31003,,shard-id=fdb3 master - 0 1426238318243 3 connected 10923-16383
-6ec2392 127.0.0.1:30005@31005,,shard-id=114f slave 67ed2db 0 1426238316232 5 connected
-824fe11 127.0.0.1:30006@31006,,shard-id=fdb3 slave 292f8b3 0 1426238317741 6 connected
-e7d1eec 127.0.0.1:30001@31001,,shard-id=69bc myself,master - 0 0 1 connected 0-5460",
-          {
-            '127.0.0.1:30004' => 'slave', '127.0.0.1:30002' => 'master', '127.0.0.1:30003' => 'master',
-            '127.0.0.1:30005' => 'slave', '127.0.0.1:30006' => 'slave', '127.0.0.1:30001' => 'master'
-          }
-        ],
-        [
-          'when hostname and auxiliary fields are present',
-          "07c37df 127.0.0.1:30004@31004,host1,shard-id=69bc slave e7d1eec 0 1426238317239 4 connected
-67ed2db 127.0.0.1:30002@31002,host2,shard-id=114f master - 0 1426238316232 2 connected 5461-10922
-292f8b3 127.0.0.1:30003@31003,host3,shard-id=fdb3 master - 0 1426238318243 3 connected 10923-16383
-6ec2392 127.0.0.1:30005@31005,host4,shard-id=114f slave 67ed2db 0 1426238316232 5 connected
-824fe11 127.0.0.1:30006@31006,host5,shard-id=fdb3 slave 292f8b3 0 1426238317741 6 connected
-e7d1eec 127.0.0.1:30001@31001,host6,shard-id=69bc myself,master - 0 0 1 connected 0-5460",
-          {
-            'host1:30004' => 'slave', 'host2:30002' => 'master', 'host3:30003' => 'master',
-            'host4:30005' => 'slave', 'host5:30006' => 'slave', 'host6:30001' => 'master'
-          }
-        ]
-      ]
-    end
-    # rubocop:enable Naming/InclusiveLanguage
-
-    with_them do
-      before do
-        allow(redis).to receive(:call).with([:cluster, :nodes]).and_return(args)
-      end
-
-      it do
-        expect(Redis::Cluster::NodeLoader.load_flags([redis])).to eq(value)
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/patch/redis_cache_store_spec.rb b/spec/lib/gitlab/patch/redis_cache_store_spec.rb
index 21c256fdbbee3b36430903a98089d74b944e3ec8..ae9c31e0c513a55c49545be50c3b5a6f7fe96b42 100644
--- a/spec/lib/gitlab/patch/redis_cache_store_spec.rb
+++ b/spec/lib/gitlab/patch/redis_cache_store_spec.rb
@@ -13,6 +13,8 @@
     cache.write('{user1}:x', 1)
     cache.write('{user1}:y', 2)
     cache.write('{user1}:z', 3)
+
+    cache.instance_variable_set(:@pipeline_batch_size, nil)
   end
 
   describe '#read_multi_mget' do
@@ -34,7 +36,7 @@
       end
 
       context 'when reading large amount of keys' do
-        let(:input_size) { 2000 }
+        let(:input_size) { 2100 }
         let(:chunk_size) { 1000 }
 
         shared_examples 'read large amount of keys' do
@@ -45,10 +47,11 @@
                 ::Gitlab::Redis::ClusterUtil.cluster?(redis.default_store)
 
               if normal_cluster || multistore_cluster
-                expect_next_instances_of(Gitlab::Redis::CrossSlot::Pipeline, 2) do |pipeline|
-                  obj = instance_double(::Redis)
-                  expect(pipeline).to receive(:pipelined).and_yield(obj)
-                  expect(obj).to receive(:get).exactly(chunk_size).times
+                times = (input_size.to_f / chunk_size).ceil
+                expect(redis).to receive(:pipelined).exactly(times).times.and_call_original
+
+                expect_next_instances_of(::Redis::PipelinedConnection, times) do |p|
+                  expect(p).to receive(:get).at_most(chunk_size).times
                 end
               else
                 expect(redis).to receive(:mget).and_call_original
diff --git a/spec/lib/gitlab/patch/redis_client_spec.rb b/spec/lib/gitlab/patch/redis_client_spec.rb
index af094e9e0d25185adc913d8b08d0296b335235af..3e2d46dd19c8c3cbb17992aa278248832b132a89 100644
--- a/spec/lib/gitlab/patch/redis_client_spec.rb
+++ b/spec/lib/gitlab/patch/redis_client_spec.rb
@@ -6,7 +6,7 @@
   include RedisHelpers
 
   let_it_be(:redis_store_class) { define_helper_redis_store_class }
-  let_it_be(:redis_client) { RedisClient.new(redis_store_class.redis_client_params) }
+  let_it_be(:redis_client) { RedisClient.new(redis_store_class.params) }
 
   before do
     Thread.current[:redis_client_error_count] = 1
diff --git a/spec/lib/gitlab/patch/redis_store_factory_spec.rb b/spec/lib/gitlab/patch/redis_store_factory_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be37a044ac154fd8c4dde9ba7005d84dcfe9ec67
--- /dev/null
+++ b/spec/lib/gitlab/patch/redis_store_factory_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Patch::RedisStoreFactory, feature_category: :redis do
+  describe '#create' do
+    let(:params) { { host: 'localhost' } }
+
+    subject(:factory_create) { ::Redis::Store::Factory.create(params) } # rubocop:disable Rails/SaveBang -- redis-store does not implement create!
+
+    context 'when using standalone Redis' do
+      it 'does not create ClusterStore' do
+        expect(Gitlab::Redis::ClusterStore).not_to receive(:new)
+
+        factory_create
+      end
+    end
+
+    context 'when using a Redis Cluster' do
+      let(:params) { { nodes: ["redis://localhost:6001", "redis://localhost:6002"] } }
+
+      it 'creates a ClusterStore' do
+        expect(Gitlab::Redis::ClusterStore).to receive(:new).with(params.merge({ raw: false }))
+
+        factory_create
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/rack_attack/store_spec.rb b/spec/lib/gitlab/rack_attack/store_spec.rb
index 19b3f239d915f8c42c8cbeff428a5447d35fa415..efe6f9382f954e93d0249fe63f75e33920f15e09 100644
--- a/spec/lib/gitlab/rack_attack/store_spec.rb
+++ b/spec/lib/gitlab/rack_attack/store_spec.rb
@@ -102,7 +102,7 @@ def with_redis(&block)
       before do
         broken_redis = Redis.new(
           url: 'redis://127.0.0.0:0',
-          instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class
+          custom: { instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class }
         )
         allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(broken_redis)
       end
diff --git a/spec/lib/gitlab/redis/cluster_store_spec.rb b/spec/lib/gitlab/redis/cluster_store_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..aaeee6156a0d882c536d766fb0f39da734bd6aae
--- /dev/null
+++ b/spec/lib/gitlab/redis/cluster_store_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# This spec only runs if a Redis Cluster is configured for Gitlab::Redis::Cache.
+# ::Redis::Cluster fetches the cluster details from the server on `initialize` and will raise
+# an error if the cluster is not found.
+#
+# An example would be the following in config/redis.yml assuming gdk is set up with redis-cluster.
+# test:
+#   cache
+#     cluster:
+#       - "redis://127.0.0.1:6003"
+#       - "redis://127.0.0.1:6004"
+#       - "redis://127.0.0.1:6005"
+RSpec.describe Gitlab::Redis::ClusterStore, :clean_gitlab_redis_cache,
+  feature_category: :redis, if: ::Gitlab::Redis::Cache.params[:nodes] do
+  let(:params) { ::Gitlab::Redis::Cache.params }
+
+  subject(:store) { ::Redis::Store::Factory.create(params) } # rubocop:disable Rails/SaveBang -- not a rails method
+
+  describe '.new' do
+    it 'initialises a cluster store' do
+      expect(store).to be_instance_of(::Gitlab::Redis::ClusterStore)
+    end
+
+    it 'extends Serialization by default' do
+      expect(store.is_a?(::Redis::Store::Serialization)).to eq(true)
+    end
+
+    it 'sets a default serializer when left empty' do
+      expect(store.instance_variable_get(:@serializer)).to eq(Marshal)
+    end
+
+    context 'when serializer field is defined' do
+      let(:params) { ::Gitlab::Redis::Cache.params.merge(serializer: Class) }
+
+      it 'sets serializer according to the options' do
+        expect(store.instance_variable_get(:@serializer)).to eq(Class)
+      end
+    end
+
+    context 'when marshalling field is defined' do
+      let(:params) { ::Gitlab::Redis::Cache.params.merge(marshalling: true, serializer: Class) }
+
+      it 'overrides serializer with Marshal' do
+        expect(store.instance_variable_get(:@serializer)).to eq(Marshal)
+      end
+    end
+
+    context 'when marshalling field is false' do
+      let(:params) { ::Gitlab::Redis::Cache.params.merge(marshalling: false, serializer: Class) }
+
+      it 'overrides serializer with Marshal' do
+        expect(store.instance_variable_get(:@serializer)).to eq(nil)
+      end
+    end
+
+    context 'when namespace is defined' do
+      let(:params) { ::Gitlab::Redis::Cache.params.merge(namespace: 'testing') }
+
+      it 'extends namespace' do
+        expect(store.is_a?(::Redis::Store::Namespace)).to eq(true)
+      end
+
+      it 'write keys with namespace' do
+        store.set('testkey', 1)
+
+        ::Gitlab::Redis::Cache.with do |conn|
+          expect(conn.exists('testing:testkey')).to eq(1)
+        end
+      end
+    end
+  end
+
+  describe '#set' do
+    context 'when ttl is added' do
+      it 'writes the key and sets a ttl' do
+        expect(store.set('test', 1, expire_after: 100)).to eq('OK')
+
+        expect(store.ttl('test')).to be > 95
+        expect(store.get('test')).to eq(1)
+      end
+    end
+
+    context 'when there is no ttl' do
+      it 'sets the key' do
+        expect(store.set('test', 1)).to eq('OK')
+
+        expect(store.get('test')).to eq(1)
+        expect(store.ttl('test')).to eq(-1)
+      end
+    end
+  end
+
+  describe '#setnx' do
+    context 'when ttl is added' do
+      it 'writes the key if not exists and sets a ttl' do
+        expect(store.setnx('test', 1, expire_after: 100)).to eq([true, true])
+        expect(store.ttl('test')).to be > 95
+        expect(store.get('test')).to eq(1)
+        expect(store.setnx('test', 1, expire_after: 100)).to eq([false, true])
+      end
+    end
+
+    context 'when there is no ttl' do
+      it 'writes the key if not exists' do
+        expect(store.setnx('test', 1)).to eq(true)
+        expect(store.setnx('test', 1)).to eq(false)
+
+        expect(store.get('test')).to eq(1)
+        expect(store.ttl('test')).to eq(-1)
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/redis/cluster_util_spec.rb b/spec/lib/gitlab/redis/cluster_util_spec.rb
index a517545014508012798a37abb3df9013367ff9ad..1cd6a450584bb1542fe157724b7aeb1dd95cc9c2 100644
--- a/spec/lib/gitlab/redis/cluster_util_spec.rb
+++ b/spec/lib/gitlab/redis/cluster_util_spec.rb
@@ -5,10 +5,14 @@
 RSpec.describe Gitlab::Redis::ClusterUtil, feature_category: :scalability do
   using RSpec::Parameterized::TableSyntax
 
+  let(:router_stub) { instance_double(::RedisClient::Cluster::Router) }
+
+  before do
+    allow(::RedisClient::Cluster::Router).to receive(:new).and_return(router_stub)
+  end
+
   describe '.cluster?' do
     context 'when MultiStore' do
-      let(:redis_cluster) { instance_double(::Redis::Cluster) }
-
       where(:pri_store, :sec_store, :expected_val) do
         :cluster | :cluster | true
         :cluster | :single  | true
@@ -17,10 +21,7 @@
       end
 
       before do
-        # stub all initialiser steps in Redis::Cluster.new to avoid connecting to a Redis Cluster node
-        allow(::Redis::Cluster).to receive(:new).and_return(redis_cluster)
-        allow(redis_cluster).to receive(:is_a?).with(::Redis::Cluster).and_return(true)
-        allow(redis_cluster).to receive(:id).and_return(1)
+        allow(router_stub).to receive(:node_keys).and_return([])
 
         allow(Gitlab::Redis::MultiStore).to receive(:same_redis_store?).and_return(false)
         skip_default_enabled_yaml_check
@@ -28,8 +29,8 @@
 
       with_them do
         it 'returns expected value' do
-          primary_redis = pri_store == :cluster ? ::Redis.new(cluster: ['redis://localhost:6000']) : ::Redis.new
-          secondary_redis = sec_store == :cluster ? ::Redis.new(cluster: ['redis://localhost:6000']) : ::Redis.new
+          primary_redis = pri_store == :cluster ? Redis::Cluster.new(nodes: ['redis://localhost:6000']) : Redis.new
+          secondary_redis = sec_store == :cluster ? Redis::Cluster.new(nodes: ['redis://localhost:6000']) : Redis.new
           primary_pool = ConnectionPool.new { primary_redis }
           secondary_pool = ConnectionPool.new { secondary_redis }
           multistore = Gitlab::Redis::MultiStore.new(primary_pool, secondary_pool, 'teststore')
@@ -48,16 +49,8 @@
     end
 
     context 'when is Redis::Cluster' do
-      let(:redis_cluster) { instance_double(::Redis::Cluster) }
-
-      before do
-        # stub all initialiser steps in Redis::Cluster.new to avoid connecting to a Redis Cluster node
-        allow(::Redis::Cluster).to receive(:new).and_return(redis_cluster)
-        allow(redis_cluster).to receive(:is_a?).with(::Redis::Cluster).and_return(true)
-      end
-
       it 'returns true' do
-        expect(described_class.cluster?(::Redis.new(cluster: ['redis://localhost:6000']))).to be_truthy
+        expect(described_class.cluster?(Redis::Cluster.new(nodes: ['redis://localhost:6000']))).to be_truthy
       end
     end
   end
diff --git a/spec/lib/gitlab/redis/command_builder_spec.rb b/spec/lib/gitlab/redis/command_builder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7ebb0228d2d7a9fd027b6383870d8c06808f6bcd
--- /dev/null
+++ b/spec/lib/gitlab/redis/command_builder_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# references specs in https://github.com/redis-rb/redis-client/blob/master/test/redis_client/command_builder_test.rb
+# we add `handles nil arguments` to test our own added logic
+RSpec.describe Gitlab::Redis::CommandBuilder, feature_category: :redis do
+  describe '.generate' do
+    def call(*args, **kwargs)
+      described_class.generate(args, kwargs)
+    end
+
+    it 'handles nil arguments' do
+      expect(call("a", nil)).to eq(["a", ""])
+    end
+
+    it 'handles positional arguments' do
+      expect(call("a", "b", "c")).to eq(%w[a b c])
+    end
+
+    it 'handles arrays' do
+      expect(call("a", %w[b c])).to eq(%w[a b c])
+    end
+
+    it 'handles hashes' do
+      expect(call("a", { "b" => "c" })).to eq(%w[a b c])
+    end
+
+    it 'handles symbols' do
+      expect(call(:a, { b: :c }, :d)).to eq(%w[a b c d])
+    end
+
+    it 'handles numerics' do
+      expect(call(1, 2.3)).to eq(["1", "2.3"])
+    end
+
+    it 'handles kwargs booleans' do
+      expect(call(ttl: nil, ex: false, withscores: true)).to eq(["withscores"])
+    end
+
+    it 'handles kwargs values' do
+      expect(call(ttl: 42)).to eq(%w[ttl 42])
+    end
+
+    it 'handles nil kwargs' do
+      expect(call(%i[a b c])).to eq(%w[a b c])
+    end
+
+    it 'raises error on unsupported types' do
+      expect { call(hash: {}) }.to raise_error(TypeError)
+    end
+
+    it 'raises error on empty commands' do
+      expect { call }.to raise_error(ArgumentError)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/redis/cross_slot_spec.rb b/spec/lib/gitlab/redis/cross_slot_spec.rb
deleted file mode 100644
index 4e9830f4110a2bcc6bfb9e2dd8e314725c7071c3..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/redis/cross_slot_spec.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::CrossSlot, feature_category: :redis do
-  include RedisHelpers
-
-  let_it_be(:redis_store_class) { define_helper_redis_store_class }
-
-  before do
-    redis_store_class.with(&:flushdb)
-  end
-
-  describe '.pipelined' do
-    context 'when using redis client' do
-      before do
-        redis_store_class.with { |redis| redis.set('a', 1) }
-      end
-
-      it 'performs redis-rb pipelined' do
-        expect(Gitlab::Redis::CrossSlot::Router).not_to receive(:new)
-
-        expect(
-          Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-            redis_store_class.with do |redis|
-              described_class::Pipeline.new(redis).pipelined do |p|
-                p.get('a')
-                p.set('b', 1)
-              end
-            end
-          end
-        ).to eq(%w[1 OK])
-      end
-    end
-
-    context 'when using with MultiStore' do
-      let_it_be(:primary_db) { 1 }
-      let_it_be(:secondary_db) { 2 }
-      let_it_be(:primary_store) { create_redis_store(redis_store_class.params, db: primary_db, serializer: nil) }
-      let_it_be(:secondary_store) { create_redis_store(redis_store_class.params, db: secondary_db, serializer: nil) }
-      let_it_be(:primary_pool) { ConnectionPool.new { primary_store } }
-      let_it_be(:secondary_pool) { ConnectionPool.new { secondary_store } }
-      let_it_be(:multistore) { Gitlab::Redis::MultiStore.new(primary_pool, secondary_pool, 'testing') }
-
-      before do
-        primary_store.set('a', 1)
-        secondary_store.set('a', 1)
-        skip_default_enabled_yaml_check
-      end
-
-      it 'performs multistore pipelined' do
-        expect(Gitlab::Redis::CrossSlot::Router).not_to receive(:new)
-
-        expect(
-          Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
-            multistore.with_borrowed_connection do
-              described_class::Pipeline.new(multistore).pipelined do |p|
-                p.get('a')
-                p.set('b', 1)
-              end
-            end
-          end
-        ).to eq(%w[1 OK])
-      end
-    end
-
-    context 'when using Redis::Cluster' do
-      # Only stub redis client internals since the CI pipeline does not run a Redis Cluster
-      let(:redis) { double(:redis) } # rubocop:disable RSpec/VerifiedDoubles
-      let(:client) { double(:client) } # rubocop:disable RSpec/VerifiedDoubles
-      let(:pipeline) { double(:pipeline) } # rubocop:disable RSpec/VerifiedDoubles
-
-      let(:arguments) { %w[a b c d] }
-
-      subject do
-        described_class::Pipeline.new(redis).pipelined do |p|
-          arguments.each { |key| p.get(key) }
-        end
-      end
-
-      before do
-        allow(redis).to receive(:_client).and_return(client)
-        allow(redis).to receive(:pipelined).and_yield(pipeline)
-        allow(client).to receive(:instance_of?).with(::Redis::Cluster).and_return(true)
-      end
-
-      it 'fan-out and fan-in commands to separate shards' do
-        # simulate fan-out to 3 shards with random order
-        expect(client).to receive(:_find_node_key).exactly(4).times.and_return(3, 2, 1, 3)
-
-        arguments.each do |key|
-          f = double('future') # rubocop:disable RSpec/VerifiedDoubles
-          expect(pipeline).to receive(:get).with(key).and_return(f)
-          expect(f).to receive(:value).and_return(key)
-        end
-
-        expect(subject).to eq(arguments)
-      end
-
-      shared_examples 'fallback on cross-slot' do |redirection|
-        context 'when redis cluster undergoing slot migration' do
-          before do
-            allow(pipeline).to receive(:get).and_raise(::Redis::CommandError.new("#{redirection} 1 127.0.0.1:7001"))
-          end
-
-          it 'logs error and executes sequentially' do
-            expect(client).to receive(:_find_node_key).exactly(4).times.and_return(3, 2, 1, 3)
-            expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(::Redis::CommandError))
-
-            arguments.each do |key|
-              expect(redis).to receive(:get).with(key).and_return(key)
-            end
-
-            subject
-          end
-        end
-      end
-
-      it_behaves_like 'fallback on cross-slot', 'MOVED'
-      it_behaves_like 'fallback on cross-slot', 'ASK'
-
-      context 'when receiving non-MOVED/ASK command errors' do
-        before do
-          allow(pipeline).to receive(:get).and_raise(::Redis::CommandError.new)
-          allow(client).to receive(:_find_node_key).exactly(4).times.and_return(3, 2, 1, 3)
-        end
-
-        it 'raises error' do
-          expect { subject }.to raise_error(::Redis::CommandError)
-        end
-      end
-    end
-  end
-end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 348215d553c03e1b141c75396e0026fc1131fb64..0d841be4e3c642e363b06eea22c4daeeaa37ab9d 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -58,6 +58,7 @@
   context 'when primary_store is not a ::Redis instance' do
     before do
       allow(primary_store).to receive(:is_a?).with(::Redis).and_return(false)
+      allow(primary_store).to receive(:is_a?).with(::Redis::Cluster).and_return(false)
     end
 
     it 'fails with exception' do
@@ -69,6 +70,7 @@
   context 'when secondary_store is not a ::Redis instance' do
     before do
       allow(secondary_store).to receive(:is_a?).with(::Redis).and_return(false)
+      allow(secondary_store).to receive(:is_a?).with(::Redis::Cluster).and_return(false)
     end
 
     it 'fails with exception' do
@@ -618,35 +620,6 @@
         end
       end
 
-      context 'when either store is a an instance of ::Redis::Cluster' do
-        let(:pipeline) { double }
-        let(:client) { double }
-
-        before do
-          allow(client).to receive(:instance_of?).with(::Redis::Cluster).and_return(true)
-          allow(pipeline).to receive(:pipelined)
-          multi_store.with_borrowed_connection do
-            allow(multi_store.default_store).to receive(:_client).and_return(client)
-          end
-        end
-
-        it 'calls cross-slot pipeline within multistore' do
-          if name == :pipelined
-            # we intentionally exclude `.and_call_original` since primary_store/secondary_store
-            # may not be running on a proper Redis Cluster.
-            multi_store.with_borrowed_connection do
-              expect(Gitlab::Redis::CrossSlot::Pipeline).to receive(:new)
-                                                              .with(multi_store.default_store)
-                                                              .exactly(:once)
-                                                              .and_return(pipeline)
-              expect(Gitlab::Redis::CrossSlot::Pipeline).not_to receive(:new).with(multi_store.non_default_store)
-            end
-          end
-
-          subject
-        end
-      end
-
       context 'when with_readonly_pipeline is used' do
         it 'calls the default store only' do
           expect(primary_store).to receive(:send).and_call_original
diff --git a/spec/lib/gitlab/redis/sessions_spec.rb b/spec/lib/gitlab/redis/sessions_spec.rb
index 874822e3e6accba692a27f7f6724eff776bf35d3..e822b7399b741c0ac924355d0abe33278f88b936 100644
--- a/spec/lib/gitlab/redis/sessions_spec.rb
+++ b/spec/lib/gitlab/redis/sessions_spec.rb
@@ -8,9 +8,9 @@
   describe '#store' do
     subject(:store) { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
 
-    # Check that Gitlab::Redis::Sessions is configured as RedisStore.
+    # Check that Gitlab::Redis::Sessions is configured as RedisStore or ClusterStore
     it 'instantiates an instance of Redis::Store' do
-      expect(store).to be_instance_of(::Redis::Store)
+      expect([::Redis::Store, ::Gitlab::Redis::ClusterStore].include?(store.class)).to eq(true)
     end
   end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 581fd3dfb380ef2aae1adbdc87237466178325e3..9bfb61077a0c8472b3502c7db088c83250f83480 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -539,15 +539,15 @@ def initialize_from_file_path(path)
 
 Rack::Test::UploadedFile.prepend(TouchRackUploadedFile)
 
-# Monkey-patch to enable ActiveSupport::Notifications for Redis commands
+# Inject middleware to enable ActiveSupport::Notifications for Redis commands
 module RedisCommands
   module Instrumentation
-    def process(commands, &block)
-      ActiveSupport::Notifications.instrument('redis.process_commands', commands: commands) do
-        super(commands, &block)
+    def call(command, redis_config)
+      ActiveSupport::Notifications.instrument('redis.process_commands', commands: command) do
+        super(command, redis_config)
       end
     end
   end
 end
 
-Redis::Client.prepend(RedisCommands::Instrumentation)
+RedisClient.register(RedisCommands::Instrumentation)
diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb
index 0250e4326099ef33df060ef251d89dd3eabab246..27310c7e04c962178df67b91537889d701a2321d 100644
--- a/spec/support/helpers/dns_helpers.rb
+++ b/spec/support/helpers/dns_helpers.rb
@@ -60,8 +60,8 @@ def db_hosts
   def permit_redis!
     # https://github.com/redis-rb/redis-client/blob/v0.11.2/lib/redis_client/ruby_connection.rb#L51 uses Socket.tcp that
     # calls Addrinfo.getaddrinfo internally.
-    hosts = Gitlab::Redis::ALL_CLASSES.map do |redis_instance|
-      redis_instance.redis_client_params[:host]
+    hosts = Gitlab::Redis::ALL_CLASSES.flat_map do |redis_instance|
+      redis_instance.params[:host] || redis_instance.params[:nodes]&.map { |n| n[:host] }
     end.uniq.compact
 
     hosts.each do |host|
diff --git a/spec/support/matchers/exceed_redis_call_limit.rb b/spec/support/matchers/exceed_redis_call_limit.rb
index 2b1e1ebad23a88b3ae9b972095644025228f2bf8..2cb92089723a90fc0f2b75bc11f307ccb6435c72 100644
--- a/spec/support/matchers/exceed_redis_call_limit.rb
+++ b/spec/support/matchers/exceed_redis_call_limit.rb
@@ -14,7 +14,7 @@ def verify_count(expected, block)
   end
 
   def verify_commands_count(command, expected, block)
-    @actual = build_recorder(block).by_command(command).count
+    @actual = build_recorder(block).by_command(command.to_s).count
 
     @actual > expected
   end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 22d8871b71877a83cc26c1d27764b4cbb5872210..3a9216e7aca94547ea80ca8018015e791c5feb90 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -6297,7 +6297,6 @@
 - './spec/lib/gitlab/instrumentation/rate_limiting_gates_spec.rb'
 - './spec/lib/gitlab/instrumentation/redis_base_spec.rb'
 - './spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb'
-- './spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb'
 - './spec/lib/gitlab/instrumentation/redis_spec.rb'
 - './spec/lib/gitlab/internal_post_receive/response_spec.rb'
 - './spec/lib/gitlab/issuable/clone/attributes_rewriter_spec.rb'
diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb
index 3dbe43d822f47556141797d5adf8fc35cb97a0fa..55ce520caab5b7d8e240cf829517748526d8de69 100644
--- a/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb
@@ -25,7 +25,7 @@
 
         expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything)
 
-        worker.perform(project_id, {}, waiter_key)
+        worker.class.perform_inline(project_id, {}, waiter_key)
       end
     end
 
@@ -49,7 +49,7 @@
         expect(Gitlab::BitbucketImport::Logger).to receive(:info).twice
         expect_next(worker.importer_class, project, kind_of(Hash)).to receive(:execute)
 
-        worker.perform(project_id, {}, waiter_key)
+        worker.class.perform_inline(project_id, {}, waiter_key)
       end
 
       it_behaves_like 'notifies the waiter'
@@ -62,7 +62,7 @@
         it 'tracks the error' do
           expect(Gitlab::Import::ImportFailureService).to receive(:track).once
 
-          worker.perform(project_id, {}, waiter_key)
+          worker.class.perform_inline(project_id, {}, waiter_key)
         end
       end
 
@@ -74,7 +74,7 @@
         it 'tracks the error and raises the error' do
           expect(Gitlab::Import::ImportFailureService).to receive(:track).once
 
-          expect { worker.perform(project_id, {}, waiter_key) }.to raise_error(StandardError)
+          expect { worker.class.perform_inline(project_id, {}, waiter_key) }.to raise_error(StandardError)
         end
       end
     end
@@ -85,7 +85,7 @@
       it 'does not call the importer' do
         expect_next(worker.importer_class).not_to receive(:execute)
 
-        worker.perform(project_id, {}, waiter_key)
+        worker.class.perform_inline(project_id, {}, waiter_key)
       end
 
       it_behaves_like 'notifies the waiter'
diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb
index 45248f57683b7b5cc59e5f1d08327315b756039d..89eec531b87bcba4f19a3d68126bf18eccaec359 100644
--- a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb
@@ -19,6 +19,10 @@
     let(:project_id) { project_id }
     let(:waiter_key) { 'key' }
 
+    before do
+      allow(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything, ttl: Gitlab::Import::JOB_WAITER_TTL)
+    end
+
     shared_examples 'notifies the waiter' do
       specify do
         allow_next(worker.importer_class).to receive(:execute)
diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb
index 1c153b7c31b62785f26ac20dae71badef6fb6fc7..2f387528740b6363305791dfa07685b1f5d45046 100644
--- a/spec/support/shared_examples/redis/redis_shared_examples.rb
+++ b/spec/support/shared_examples/redis/redis_shared_examples.rb
@@ -80,18 +80,17 @@
 
     context 'with new format' do
       it_behaves_like 'redis store' do
-        let(:config_file_name) { config_new_format_host }
+        # use new format host without sentinel details as `.to_s` checks `config` which
+        # tries to resolve master/replica details with an actual sentinel instance.
+        # https://github.com/redis-rb/redis-client/blob/v0.18.0/lib/redis_client/sentinel_config.rb#L128
+        let(:config_file_name) { "spec/fixtures/config/redis_new_format_host_standalone.yml" }
         let(:host) { "development-host:#{redis_port}" }
       end
     end
   end
 
-  describe '.redis_client_params' do
-    # .redis_client_params wraps over `.redis_store_options` by modifying its outputs
-    # to be compatible with `RedisClient`. We test for compatibility in this block while
-    # the contents of redis_store_options are tested in the `.params` block.
-
-    subject { described_class.new(rails_env).redis_client_params }
+  describe '.params' do
+    subject { described_class.new(rails_env).params }
 
     let(:rails_env) { 'development' }
     let(:config_file_name) { config_old_format_socket }
@@ -103,56 +102,6 @@
       end
     end
 
-    context 'when url is host based' do
-      context 'with old format' do
-        let(:config_file_name) { config_old_format_host }
-
-        it 'does not raise ArgumentError for invalid keywords' do
-          expect { RedisClient.config(**subject) }.not_to raise_error
-        end
-
-        it_behaves_like 'instrumentation_class in custom key'
-      end
-
-      context 'with new format' do
-        let(:config_file_name) { config_new_format_host }
-
-        where(:rails_env, :host) do
-          [
-            %w[development development-host],
-            %w[test test-host],
-            %w[production production-host]
-          ]
-        end
-
-        with_them do
-          it 'does not raise ArgumentError for invalid keywords in SentinelConfig' do
-            expect(subject[:name]).to eq(host)
-            expect { RedisClient.sentinel(**subject) }.not_to raise_error
-          end
-
-          it_behaves_like 'instrumentation_class in custom key'
-        end
-      end
-    end
-
-    context 'when url contains unix socket reference' do
-      let(:config_file_name) { config_old_format_socket }
-
-      it 'does not raise ArgumentError for invalid keywords' do
-        expect { RedisClient.config(**subject) }.not_to raise_error
-      end
-
-      it_behaves_like 'instrumentation_class in custom key'
-    end
-  end
-
-  describe '.params' do
-    subject { described_class.new(rails_env).params }
-
-    let(:rails_env) { 'development' }
-    let(:config_file_name) { config_old_format_socket }
-
     it 'withstands mutation' do
       params1 = described_class.params
       params2 = described_class.params
@@ -251,10 +200,16 @@
 
         with_them do
           it 'returns hash with host, port, db, and password' do
-            is_expected.to include(host: host, password: 'mynewpassword', port: redis_port, db: redis_database)
+            is_expected.to include(name: host, password: 'mynewpassword', db: redis_database)
             is_expected.not_to have_key(:url)
           end
+
+          it 'does not raise ArgumentError for invalid keywords in SentinelConfig' do
+            expect { RedisClient.sentinel(**subject) }.not_to raise_error
+          end
         end
+
+        it_behaves_like 'instrumentation_class in custom key'
       end
 
       context 'with redis cluster format' do
@@ -272,13 +227,19 @@
           it 'returns hash with cluster and password' do
             is_expected.to include(
               password: 'myclusterpassword',
-              cluster: [
+              nodes: [
                 { host: "#{host}1", port: redis_port },
                 { host: "#{host}2", port: redis_port }
               ]
             )
             is_expected.not_to have_key(:url)
           end
+
+          it 'does not raise ArgumentError for invalid keywords in ClusterConfig' do
+            expect { RedisClient::ClusterConfig.new(**subject) }.not_to raise_error
+          end
+
+          it_behaves_like 'instrumentation_class in custom key'
         end
       end
     end
diff --git a/spec/support_specs/helpers/redis_commands/recorder_spec.rb b/spec/support_specs/helpers/redis_commands/recorder_spec.rb
index ef46db5e29ea7071532af7498efffa0b6416853c..5c200f15bae3b7c05f18981cddcb0b18b4021b19 100644
--- a/spec/support_specs/helpers/redis_commands/recorder_spec.rb
+++ b/spec/support_specs/helpers/redis_commands/recorder_spec.rb
@@ -13,7 +13,7 @@
       it 'records Redis commands' do
         recorder = described_class.new { cache.read('key1') }
 
-        expect(recorder.log).to include([:get, 'cache:gitlab:key1'])
+        expect(recorder.log).to include(['get', 'cache:gitlab:key1'])
       end
     end
 
@@ -35,10 +35,10 @@
         cache.delete('key1')
       end
 
-      expect(recorder.log).to include([:set, 'cache:gitlab:key1', anything, anything, anything])
-      expect(recorder.log).to include([:get, 'cache:gitlab:key1'])
-      expect(recorder.log).to include([:get, 'cache:gitlab:key2'])
-      expect(recorder.log).to include([:del, 'cache:gitlab:key1'])
+      expect(recorder.log).to include(['set', 'cache:gitlab:key1', anything, anything, anything])
+      expect(recorder.log).to include(['get', 'cache:gitlab:key1'])
+      expect(recorder.log).to include(['get', 'cache:gitlab:key2'])
+      expect(recorder.log).to include(['del', 'cache:gitlab:key1'])
     end
 
     it 'does not record commands before the call' do
@@ -48,8 +48,8 @@
         cache.read('key1')
       end
 
-      expect(recorder.log).not_to include([:set, anything, anything])
-      expect(recorder.log).to include([:get, 'cache:gitlab:key1'])
+      expect(recorder.log).not_to include(['set', anything, anything])
+      expect(recorder.log).to include(['get', 'cache:gitlab:key1'])
     end
 
     it 'refreshes recording after reinitialization' do
@@ -68,15 +68,15 @@
         cache.read('key4')
       end
 
-      expect(recorder1.log).to include([:get, 'cache:gitlab:key2'])
-      expect(recorder1.log).not_to include([:get, 'cache:gitlab:key1'])
-      expect(recorder1.log).not_to include([:get, 'cache:gitlab:key3'])
-      expect(recorder1.log).not_to include([:get, 'cache:gitlab:key4'])
+      expect(recorder1.log).to include(['get', 'cache:gitlab:key2'])
+      expect(recorder1.log).not_to include(['get', 'cache:gitlab:key1'])
+      expect(recorder1.log).not_to include(['get', 'cache:gitlab:key3'])
+      expect(recorder1.log).not_to include(['get', 'cache:gitlab:key4'])
 
-      expect(recorder2.log).to include([:get, 'cache:gitlab:key4'])
-      expect(recorder2.log).not_to include([:get, 'cache:gitlab:key1'])
-      expect(recorder2.log).not_to include([:get, 'cache:gitlab:key2'])
-      expect(recorder2.log).not_to include([:get, 'cache:gitlab:key3'])
+      expect(recorder2.log).to include(['get', 'cache:gitlab:key4'])
+      expect(recorder2.log).not_to include(['get', 'cache:gitlab:key1'])
+      expect(recorder2.log).not_to include(['get', 'cache:gitlab:key2'])
+      expect(recorder2.log).not_to include(['get', 'cache:gitlab:key3'])
     end
   end
 
@@ -91,10 +91,10 @@
         cache.delete('key2')
       end
 
-      expect(recorder.log).to include([:set, 'cache:gitlab:key1', anything, anything, anything])
-      expect(recorder.log).to include([:get, 'cache:gitlab:key1'])
-      expect(recorder.log).not_to include([:get, 'cache:gitlab:key2'])
-      expect(recorder.log).not_to include([:del, 'cache:gitlab:key2'])
+      expect(recorder.log).to include(['set', 'cache:gitlab:key1', anything, anything, anything])
+      expect(recorder.log).to include(['get', 'cache:gitlab:key1'])
+      expect(recorder.log).not_to include(['get', 'cache:gitlab:key2'])
+      expect(recorder.log).not_to include(['del', 'cache:gitlab:key2'])
     end
   end
 
@@ -107,7 +107,7 @@
         cache.delete('key2')
       end
 
-      expect(recorder.by_command(:del)).to match_array([[:del, 'cache:gitlab:key2']])
+      expect(recorder.by_command('del')).to match_array([['del', 'cache:gitlab:key2']])
     end
   end
 
diff --git a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
index d11b044b093bc964a3ba4ff8d6218477203c4e92..4cb0a25f89294a86bea4b7f865234723e7470fb0 100644
--- a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
+++ b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb
@@ -108,17 +108,24 @@
 
         before do
           allow(importer).to receive(:execute).and_return(importer_result)
+          allow(Gitlab::GithubGistsImport::Representation::Gist)
+            .to receive(:from_json_hash)
+            .with(anything)
+            .and_return(gist_object)
         end
 
         it 'tracks and logs error' do
+          # use `anything` since jid is created in Sidekiq's middleware. `jid` does not exist until
+          # perform_inline is called.
           expect(Gitlab::GithubImport::Logger)
             .to receive(:error)
-            .with(log_attributes.merge('message' => 'importer failed', 'exception.message' => 'error_message'))
+            .with(log_attributes.merge('message' => 'importer failed', 'exception.message' => 'error_message',
+              'jid' => anything))
           expect(Gitlab::JobWaiter)
             .to receive(:notify)
-            .with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL)
+            .with('some_key', anything, ttl: Gitlab::Import::JOB_WAITER_TTL)
 
-          subject.perform(user.id, gist_hash, 'some_key')
+          subject.class.perform_inline(user.id, gist_hash, 'some_key') # perform_inline calls .perform
 
           expect_snowplow_event(
             category: 'Gitlab::GithubGistsImport::ImportGistWorker',
@@ -130,7 +137,7 @@
         end
 
         it 'persists failure' do
-          expect { subject.perform(user.id, gist_hash, 'some_key') }
+          expect { subject.class.perform_inline(user.id, gist_hash, 'some_key') }
             .to change { ImportFailure.where(user: user).count }.from(0).to(1)
 
           expect(ImportFailure.where(user_id: user.id).first).to have_attributes(
diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
index 3b2114ee488132607de9bbc9b6912fc1812a0ac7..c9c4c6e2d2b52a7fc319840301fdd5fc0c21f410 100644
--- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
@@ -46,7 +46,7 @@
 
       context 'when import label does not exist' do
         it 'does not record import failure' do
-          subject.perform(project.id, 123, issue_attrs, some_key)
+          subject.class.perform_inline(project.id, 123, issue_attrs, some_key)
 
           expect(label.issues.count).to eq(0)
           expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0)
@@ -57,7 +57,7 @@
         before do
           Gitlab::JiraImport.cache_import_label_id(project.id, label.id)
 
-          subject.perform(project.id, 123, issue_attrs, some_key)
+          subject.class.perform_inline(project.id, 123, issue_attrs, some_key)
         end
 
         it 'does not record import failure' do
diff --git a/spec/workers/redis_migration_worker_spec.rb b/spec/workers/redis_migration_worker_spec.rb
index 9f29c84a94895f4902f22dd356ac544f64d63db9..d8f2cacdb13e5ee3c7b785020d475b48485f9cae 100644
--- a/spec/workers/redis_migration_worker_spec.rb
+++ b/spec/workers/redis_migration_worker_spec.rb
@@ -30,7 +30,7 @@ def scan_match_pattern
         end
 
         def redis
-          ::Redis.new(::Gitlab::Redis::Cache.params)
+          ::Gitlab::Redis::Cache.redis
         end
       end
     end