DEV Community

Davide Santangelo
Davide Santangelo

Posted on

Mastering Kredis in Ruby: Your Essential Guide

Introduction

In the ever-evolving world of web development, caching mechanisms have become an indispensable tool for enhancing application performance and scalability. Ruby, a dynamic and expressive language, has gained immense popularity among developers worldwide, thanks to its simplicity and elegance. However, when it comes to caching, Ruby's built-in caching solutions often fall short of meeting the demands of high-traffic applications.

This is where Kredis, a Redis client for Ruby, comes into play, offering a powerful and efficient caching solution.

What is Kredis?

Kredis is a versatile Ruby client for Redis, an open-source, in-memory data structure store. Redis is renowned for its lightning-fast performance, making it an ideal choice for caching and other data-intensive operations. Kredis provides a seamless interface for interacting with Redis, enabling developers to leverage its full potential within their Ruby applications.

Setting up Kredis

Before diving into the code examples, let's set up Kredis in your Ruby project. First, ensure that you have Redis installed on your system. If not, you can install it using your preferred package manager or by following the instructions on the official Redis website (https://redis.io/download).

Next, add the kredis gem to your project's Gemfile:

gem 'kredis' 
Enter fullscreen mode Exit fullscreen mode

Then, run bundle install to install the gem and its dependencies.

Kredis Typed Scalars

Kredis provides typed scalars for strings, integers, decimals, floats, booleans, datetimes, and JSON hashes:

# String my_string = Kredis.string("mystring") my_string.value = "hello world!" # => SET mystring "hello world!" "hello world!" == my_string.value # => GET mystring # Integer my_integer = Kredis.integer("myinteger") my_integer.value = 7 # => SET myinteger "7" 7 == my_integer.value # => GET myinteger # Decimal my_decimal = Kredis.decimal("mydecimal") # precision! my_decimal.value = "%.47f" % (1.0 / 7) # => SET mydecimal "0.14285714285714284921269268124888185411691665649414" "0.14285714285714284921269268124888185411691665649414" == my_decimal.value # => GET mydecimal # Float my_float = Kredis.float("myfloat") # speed! my_float.value = 1.0 / 7 # => SET myfloat "0.14285714285714285" 0.14285714285714285 == my_float.value # => GET myfloat # Boolean my_boolean = Kredis.boolean("myboolean") my_boolean.value = false # => SET myboolean "f" false == my_boolean.value # => GET myboolean # Datetime my_datetime = Kredis.datetime("mydatetime") memoized_midnight = Time.zone.now.midnight my_datetime.value = memoized_midnight # SET mydatetime "2024-04-09T00:00:00.000000000Z" memoized_midnight == my_datetime.value # => GET mydatetime # JSON my_json = Kredis.json("myjson") my_json.value = {"one" => 1, "two" => "2"} # => SET myjson "{\"one\":1,\"two\":\"2\"}" {"one" => 1, "two" => "2"} == my_json.value # => GET myjson 
Enter fullscreen mode Exit fullscreen mode

There are data structures for counters, enums, flags, lists, unique lists, sets, and slots:

# List shopping_list = Kredis.list("shopping_list") shopping_list.append("apple") # => RPUSH shopping_list "apple" ["apple"] == shopping_list.elements # => LRANGE shopping_list 0, -1 # Integer List count_list = Kredis.list("count_list", typed: :integer, default: [1, 2, 3]) # => EXISTS? count_list, RPUSH count_list "1" "2" "3" count_list.append([4, 5, 6]) # => RPUSH count_list "4" "5" "6" count_list << 7 # => RPUSH count_list "7" [1, 2, 3, 4, 5, 6, 7] == count_list.elements # => LRANGE count_list 0 -1 # Unique List unique_shopping_list = Kredis.unique_list("unique_shopping_list") unique_shopping_list.append(%w[milk bread eggs]) # => LREM unique_shopping_list 0, "milk" + LREM unique_shopping_list 0, "bread" + LREM unique_shopping_list 0, "eggs" + RPUSH unique_shopping_list "milk", "bread", "eggs" unique_shopping_list.prepend(%w[toothpaste shampoo soap]) # => LREM unique_shopping_list 0, "toothpaste" + LREM unique_shopping_list 0, "shampoo" + LREM unique_shopping_list 0, "soap" + LPUSH unique_shopping_list "toothpaste", "shampoo", "soap" unique_shopping_list.append([]) unique_shopping_list << "cookies" # => LREM unique_shopping_list 0, "cookies" + RPUSH unique_shopping_list "cookies" unique_shopping_list.remove("bread") # => LREM unique_shopping_list 0, "bread" ["toothpaste", "shampoo", "soap", "milk", "eggs", "cookies"] == unique_shopping_list.elements # => LRANGE unique_shopping_list 0, -1 # Ordered Set book_ratings = Kredis.ordered_set("book_ratings") book_ratings.append(["1984", "Brave New World", "To Kill a Mockingbird"]) # => ZADD book_ratings 1646131025.4953232 "1984" 1646131025.495326 "Brave New World" 1646131025.4953272 "To Kill a Mockingbird" book_ratings.prepend(["The Catcher in the Rye", "Animal Farm", "The Great Gatsby"]) # => ZADD book_ratings -1646131025.4957051 "The Catcher in the Rye" -1646131025.495707 "Animal Farm" -1646131025.4957082 "The Great Gatsby" book_ratings.append([]) book_ratings << "Pride and Prejudice" # => ZADD book_ratings 1646131025.4960442 "Pride and Prejudice" book_ratings.remove("Brave New World") # => ZREM book_ratings "Brave New World" ["The Great Gatsby", "Animal Farm", "The Catcher in the Rye", "1984", "To Kill a Mockingbird", "Pride and Prejudice"] == book_ratings.elements # => ZRANGE book_ratings 0 -1 # Set appointment_dates = Kredis.set("appointment_dates", typed: :datetime) appointment_dates.add(DateTime.tomorrow, DateTime.yesterday) # => SADD appointment_dates "2021-02-03 00:00:00 +0100" "2021-02-01 00:00:00 +0100" appointment_dates << DateTime.tomorrow # => SADD appointment_dates "2021-02-03 00:00:00 +0100" 2 == appointment_dates.size # => SCARD appointment_dates [DateTime.tomorrow, DateTime.yesterday] == appointment_dates.members # => SMEMBERS appointment_dates # Hash person_details = Kredis.hash("person_details") person_details.update("name" => "Davide", "age" => "38") # => HSET person_details "name", "Davide", "age", "38" {"name" => "Davide", "age" => "38"} == person_details.to_h # => HGETALL person_details "30" == person_details["age"] # => HMGET person_details "age" %w[name age] == person_details.keys # => HKEYS person_details %w[Davide 38] == person_details.values # => HVALS person_details person_details.remove # => DEL person_details # Hash with Typed Values product_prices = Kredis.hash("product_prices", typed: :integer) product_prices.update(apple: 2, banana: 1) # HSET product_prices "apple", "2", "banana", "1" %w[apple banana] == product_prices.keys # HKEYS product_prices [2, 1] == product_prices.values # HVALS product_prices {"apple" => 2, "banana" => 1} == product_prices.to_h # HGETALL product_prices # Counter message_count = Kredis.counter("message_count") 0 == message_count.value # => GET "message_count" message_count.increment # => SET message_count 0 NX + INCRBY message_count 1 message_count.increment # => SET message_count 0 NX + INCRBY message_count 1 message_count.decrement # => SET message_count 0 NX + DECRBY message_count 1 1 == message_count.value # => GET "message_count" # Expiring Counter usage_counter = Kredis.counter("usage_counter", expires_in: 5.seconds) usage_counter.increment(by: 2) # => SET usage_counter 0 EX 5 NX + INCRBY "usage_counter" 2 2 == usage_counter.value # => GET "usage_counter" sleep 6.seconds 0 == usage_counter.value # => GET "usage_counter" # Cycle color_cycle = Kredis.cycle("color_cycle", values: %i[red blue green]) :red == color_cycle.value # => GET color_cycle color_cycle.next # => GET color_cycle + SET color_cycle 1 :blue == color_cycle.value # => GET color_cycle color_cycle.next # => GET color_cycle + SET color_cycle 2 :green == color_cycle.value # => GET color_cycle color_cycle.next # => GET color_cycle + SET color_cycle 0 :red == color_cycle.value # => GET color_cycle # Enum week_days = Kredis.enum("week_days", values: %w[Monday Tuesday Wednesday], default: "Monday") "Monday" == week_days.value # => GET week_days true == week_days.monday? # => GET week_days week_days.value = "Tuesday" # => SET week_days "Tuesday" "Tuesday" == week_days.value # => GET week_days week_days.wednesday! # => SET week_days "Wednesday" "Wednesday" == week_days.value # => GET week_days week_days.value = "Friday" "Wednesday" == week_days.value # => GET week_days week_days.reset # => DEL week_days "Monday" == week_days.value # => GET week_days # Slot parking_slots = Kredis.slots("parking_slots", available: 10) true == parking_slots.available? # => GET parking_slots parking_slots.reserve # => INCR parking_slots true == parking_slots.available? # => GET parking_slots parking_slots.reserve # => INCR parking_slots true == parking_slots.available? # => GET parking_slots parking_slots.reserve # => INCR parking_slots false == parking_slots.available? # => GET parking_slots parking_slots.reserve # => INCR parking_slots + DECR parking_slots false == parking_slots.available? # => GET parking_slots parking_slots.release # => DECR parking_slots true == parking_slots.available? # => GET parking_slots parking_slots.reset # => DEL parking_slots # Flag newsletter_flag = Kredis.flag("newsletter_flag") false == newsletter_flag.marked? # => EXISTS newsletter_flag newsletter_flag.mark # => SET newsletter_flag 1 true == newsletter_flag.marked? # => EXISTS newsletter_flag newsletter_flag.remove # => DEL newsletter_flag false == newsletter_flag.marked? # => EXISTS newsletter_flag # Expiring Flag newsletter_flag.mark(expires_in: 1.second, force: false) # => SET newsletter_flag 1 EX 1 NX false == newsletter_flag.mark(expires_in: 10.seconds, force: false) # => SET newsletter_flag 10 EX 1 NX true == newsletter_flag.marked? # => EXISTS newsletter_flag sleep 0.5.seconds true == newsletter_flag.marked? # => EXISTS newsletter_flag sleep 0.6.seconds false == newsletter_flag.marked? # => EXISTS newsletter_flag # Limiter request_limiter = Kredis.limiter("request_limiter", limit: 100, expires_in: 5.seconds) 0 == request_limiter.value # => GET "request_limiter" request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 false == request_limiter.exceeded? # => GET "request_limiter" request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 true == request_limiter.exceeded? # => GET "request_limiter" sleep 6 request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 request_limiter.poke # => SET request_limiter 0 NX + INCRBY request_limiter 1 false == request_limiter.exceeded? # => GET "request_limiter" 
Enter fullscreen mode Exit fullscreen mode

Basic Kredis Operations

Kredis offers a straightforward API for performing various operations on Redis. Here's an example of connecting to a Redis instance and performing some basic operations:

require 'kredis' # Connect to Redis redis = Kredis.new # Set a key-value pair redis.set('name', 'Davide Santangelo') # Get the value of a key puts redis.get('name') # Output: Davide Santangelo # Set a key-value pair with an expiration time (in seconds) redis.setex('counter', 60, 0) # Increment the value of a key redis.incr('counter') puts redis.get('counter') # Output: 1 
Enter fullscreen mode Exit fullscreen mode

In this example, we import the kredis gem, create a new instance of Kredis, and perform basic operations like setting and getting key-value pairs. We also demonstrate how to set an expiration time for a key and increment its value.

Caching with Kredis

One of the primary use cases for Kredis is caching. Redis's in-memory data store provides lightning-fast access to cached data, significantly improving application performance. Here's an example of implementing a simple cache using Kredis:

require 'kredis' # Connect to Redis redis = Kredis.new # Define a cache helper def get_cached_data(key) cached_data = redis.get(key) return cached_data if cached_data # Fetch data from a slow source (e.g., database, API) fresh_data = fetch_data_from_slow_source # Cache the fresh data redis.set(key, fresh_data) fresh_data end # Fetch data from a slow source (simulated) def fetch_data_from_slow_source sleep(3) # Simulate a slow operation 'Fresh data from slow source' end # Use the cache helper puts get_cached_data('cache_key') # Output: Fresh data from slow source (first call, cache miss) puts get_cached_data('cache_key') # Output: Fresh data from slow source (cached value) 
Enter fullscreen mode Exit fullscreen mode

In this example, we define a get_cached_data helper function that first checks if the requested data is available in the Redis cache. If not, it fetches the data from a slow source (simulated by a 3-second delay), caches the fresh data in Redis, and returns it. Subsequent calls to get_cached_data with the same key will retrieve the cached value, significantly improving response times.

Advanced Kredis Features

Kredis provides a wide range of advanced features to enhance your caching and data management capabilities. Here are a few examples:

Pipelining

Kredis supports pipelining, which allows you to send multiple commands to Redis in a single request, reducing the overhead of network round-trips.

redis.pipelined do redis.set('key1', 'value1') redis.set('key2', 'value2') redis.get('key1') redis.get('key2') end 
Enter fullscreen mode Exit fullscreen mode

Transactions

Redis transactions ensure atomic operations, preventing race conditions and data corruption when multiple clients are modifying the same data.

redis.multi do redis.incr('counter') redis.incr('counter') end 
Enter fullscreen mode Exit fullscreen mode

Pub/Sub

Redis supports a publish/subscribe messaging system, which can be useful for building real-time applications or implementing event-driven architectures.

# Subscribe to a channel redis.subscribe('channel_name') do |on| on.message do |channel, message| puts "Received message '#{message}' on channel '#{channel}'" end end # Publish a message to the channel redis.publish('channel_name', 'Hello, world!') 
Enter fullscreen mode Exit fullscreen mode

Scripting

Kredis allows you to execute Lua scripts directly on the Redis server, enabling complex data manipulations and atomic operations.

script = "redis.call('set', KEYS[1], ARGV[1])" redis.eval(script, keys: ['key'], argv: ['value']) 
Enter fullscreen mode Exit fullscreen mode

These are just a few examples of the advanced features offered by Kredis. With its extensive documentation and active community support, you can explore and leverage many more features to build robust and high-performance applications.

Using Kredis with Ruby on Rails

Ruby on Rails, provides seamless integration with Kredis, making it a powerful combination for building high-performance web applications. With Rails' built-in caching mechanisms and Kredis' efficient Redis client, developers can leverage the best of both worlds to achieve optimal performance and scalability.

Here's an example of using Kredis as a caching store in a Rails application:

# config/environments/production.rb config.cache_store = :redis_store, { redis: { driver: :kredis, url: ENV['REDIS_URL'], connect_timeout: 30, # Defaults to 20 seconds read_timeout: 0.2, # Defaults to 1 second write_timeout: 0.2, # Defaults to 1 second reconnect_attempts: 1 # Defaults to 0 }, namespace: 'cache' } 
Enter fullscreen mode Exit fullscreen mode

In this configuration, we set the cache store for the Rails production environment to use redis_store with Kredis as the Redis driver. We specify the Redis connection URL and configure various timeouts and reconnection attempts to suit our application's needs.

Once configured, you can take advantage of Rails' built-in caching mechanisms, such as fragment caching, action caching, and low-level caching, which will all utilize the Kredis-powered Redis store for caching data.

# app/controllers/posts_controller.rb def show @post = Post.find(params[:id]) fresh_when(@post.updated_at) end # app/views/posts/show.html.erb <% cache @post do %> <%= @post.title %> <%= @post.content %> <% end %> 
Enter fullscreen mode Exit fullscreen mode

In this example, we fetch a Post object and cache the rendered view using the cache helper, which leverages the configured Redis store powered by Kredis. The fresh_when helper ensures that the cache is invalidated when the Post object is updated.

By combining the power of Kredis and Rails' caching mechanisms, developers can significantly improve the performance and scalability of their web applications, ensuring a smooth and responsive user experience, even under heavy load.

Conclusion

Kredis is a powerful Redis client for Ruby that unlocks the full potential of the Redis in-memory data store. By providing a seamless interface and a wide range of features, Kredis empowers developers to implement efficient caching strategies, handle real-time data updates, and build scalable and high-performance applications. Whether you're working on a small project or a large-scale enterprise application, leveraging Kredis can significantly enhance your application's performance and reliability.

Top comments (0)