Mastering Kredis in Ruby: Your Essential Guide
Tags: ruby, rails, tutorial
Published: 2024-04-09 09:28:06

##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](https://github.com/rails/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:

```ruby
gem 'kredis'
```

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:

```ruby
# 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
```

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

```ruby
# 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"
```

## 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:

```ruby
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
```

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:

```ruby
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)
```

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.

```ruby
redis.pipelined do
  redis.set('key1', 'value1')
  redis.set('key2', 'value2')
  redis.get('key1')
  redis.get('key2')
end
```

### Transactions

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

```ruby
redis.multi do
  redis.incr('counter')
  redis.incr('counter')
end
```

### Pub/Sub

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

```ruby
# 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!')
```

### Scripting

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

```ruby
script = "redis.call('set', KEYS[1], ARGV[1])"
redis.eval(script, keys: ['key'], argv: ['value'])
```

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:

```ruby
# 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'
}
```

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.

```ruby
# 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 %>
```

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](https://apidock.com/rails/ActionController/Base/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.