MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis在缓存系统中的应用与实践

2022-01-301.7k 阅读

Redis 基础概述

Redis 是什么

Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。这种多数据结构的支持使得 Redis 能够适应各种不同的应用场景。

例如,字符串类型可以简单地存储一个键值对,像存储用户的基本信息中的某个字段:

import redis

r = redis.Redis(host='localhost', port=6379, db=0)
r.set('user:1:name', 'John')
print(r.get('user:1:name')) 

在上述 Python 代码中,通过 redis - py 库连接到本地 Redis 实例,并设置了一个键为 user:1:name,值为 John 的字符串数据,然后获取该值并打印。

Redis 的优势

  1. 高性能:Redis 将数据存储在内存中,内存的读写速度远远高于传统的磁盘存储,这使得 Redis 能够达到极高的读写性能。例如,在简单的键值对读写场景下,Redis 可以轻松达到每秒数万次甚至数十万次的操作频率。
  2. 丰富的数据结构:如前文所述,Redis 支持多种数据结构。以哈希结构为例,它非常适合存储对象数据。假设我们要存储一个用户的详细信息:
user_info = {
    'name': 'Jane',
    'age': 25,
    'email': 'jane@example.com'
}
r.hmset('user:2', user_info)
print(r.hgetall('user:2')) 

这里通过 hmset 方法将一个用户的信息以哈希结构存储到 Redis 中,键为 user:2,每个字段和值构成哈希的成员。 3. 持久化:Redis 提供了两种持久化机制,RDB(Redis Database)和 AOF(Append - Only - File)。RDB 是一种快照式的持久化方式,它将内存中的数据以二进制的形式保存到磁盘上,适合用于大规模数据的恢复。AOF 则是将 Redis 执行的写命令追加到文件末尾,通过重放这些命令来恢复数据,这种方式的优点是数据的完整性更好,即使 Redis 发生故障,也只会丢失最后一次持久化后的部分数据。

Redis 在缓存系统中的应用原理

缓存的基本概念

缓存是一种介于应用程序和数据源(如数据库)之间的数据存储层,它的作用是存储经常被访问的数据副本。当应用程序需要获取数据时,首先会检查缓存中是否存在该数据。如果存在,直接从缓存中读取,这大大减少了对数据源的访问次数,从而提高了系统的响应速度。

例如,在一个新闻网站中,文章的内容可能被频繁访问。如果每次用户请求文章都从数据库中读取,数据库的压力会很大。而如果将文章内容缓存起来,大部分请求可以直接从缓存中获取,只有在缓存中不存在或者缓存过期时才去数据库读取。

Redis 作为缓存的工作流程

  1. 缓存读取:应用程序向 Redis 发送读请求,请求获取特定键的数据。Redis 首先检查自身内存中是否存在该键对应的数据。如果存在,直接将数据返回给应用程序。例如:
Jedis jedis = new Jedis("localhost", 6379);
String value = jedis.get("article:1");
if (value != null) {
    // 从缓存中获取到数据,直接使用
    System.out.println("从缓存中获取到文章内容: " + value);
} else {
    // 缓存中不存在,从数据库读取
}
jedis.close();

在这段 Java 代码中,通过 Jedis 库连接到 Redis,尝试获取键为 article:1 的数据。如果获取到值,说明数据在缓存中,直接使用。 2. 缓存写入:当应用程序从数据源(如数据库)获取到数据后,会将数据写入 Redis 缓存中,以便后续的请求可以直接从缓存中获取。例如:

# 假设从数据库获取到文章内容
article_content = "这是一篇精彩的文章..."
r.set('article:1', article_content)

这里 Python 代码从数据库获取到文章内容后,将其以键 article:1 存储到 Redis 缓存中。 3. 缓存更新:当数据源中的数据发生变化时,需要相应地更新 Redis 缓存中的数据,以保证缓存数据的一致性。一种常见的更新策略是在数据更新到数据源后,立即更新 Redis 缓存。例如:

$redis = new Redis();
$redis->connect('localhost', 6379);
// 假设更新了数据库中文章的内容
// 同时更新 Redis 缓存
$newArticleContent = "文章更新后的内容";
$redis->set('article:1', $newArticleContent);

在这段 PHP 代码中,假设数据库中文章内容更新后,立即更新 Redis 缓存中对应键的数据。 4. 缓存删除:有时候,为了保证数据的一致性,需要删除 Redis 缓存中的数据。比如当数据库中的某条记录被删除时,对应的 Redis 缓存数据也应该被删除。例如:

const redis = require('redis');
const client = redis.createClient(6379, 'localhost');
// 假设数据库中 article:1 的记录被删除
client.del('article:1', function (err, reply) {
    if (!err) {
        console.log('缓存数据已成功删除');
    }
});
client.quit();

在这段 Node.js 代码中,通过 del 方法删除 Redis 中键为 article:1 的缓存数据。

Redis 缓存策略

缓存过期策略

  1. 定时过期:对每个设置了过期时间的键,Redis 会创建一个定时器,当过期时间到达时,立即删除该键对应的数据。这种策略的优点是可以精确控制数据的过期时间,缺点是会占用大量的 CPU 资源,尤其是在有大量键设置过期时间的情况下。例如:
r.setex('temp_key', 60, '临时数据') 

这里通过 setex 方法设置了一个键为 temp_key,值为 临时数据,过期时间为 60 秒的数据。60 秒后,该键值对会被自动删除。 2. 惰性过期:当应用程序请求获取某个键的数据时,Redis 会检查该键是否过期。如果过期,才会删除该键对应的数据,并返回空值。这种策略的优点是不会占用额外的 CPU 资源来处理过期键,缺点是可能会导致过期键长时间占用内存,直到被访问时才被删除。例如,在获取数据的代码中可能会有如下逻辑:

String value = jedis.get("可能过期的键");
if (value == null) {
    // 说明键可能过期或不存在
}
  1. 定期过期:Redis 会定期随机抽取一部分设置了过期时间的键进行检查,删除其中过期的键。这种策略是一种折中的方案,既不会像定时过期那样占用大量 CPU 资源,也不会像惰性过期那样让过期键长时间占用内存。Redis 会根据配置的 hz 参数(默认为 10,表示每秒执行 10 次过期检查)来控制检查的频率。

缓存淘汰策略

当 Redis 的内存使用达到设置的最大内存限制时,需要根据一定的策略淘汰部分数据,以保证新的数据能够被写入。

  1. no - eviction:不淘汰任何数据,当内存不足时,新增操作会报错。这种策略适用于需要确保数据完整性,不允许数据丢失的场景,但可能会导致应用程序因为无法写入数据而出现故障。
  2. allkeys - lru:从所有键中,选择最近最少使用(Least Recently Used,LRU)的键进行淘汰。这种策略基于一个假设,即最近最少使用的键在未来被使用的可能性也较低。例如:
redis - cli config set maxmemory - policy allkeys - lru

通过上述命令可以将 Redis 的缓存淘汰策略设置为 allkeys - lru。 3. volatile - lru:从设置了过期时间的键中,选择最近最少使用的键进行淘汰。与 allkeys - lru 的区别在于,它只考虑有过期时间的键,适用于希望保留一些永久数据(没有设置过期时间)的场景。 4. allkeys - random:从所有键中随机选择键进行淘汰,这种策略比较简单粗暴,没有考虑键的使用频率等因素,可能会淘汰掉经常使用的键,一般不推荐在生产环境中使用。 5. volatile - random:从设置了过期时间的键中随机选择键进行淘汰。 6. volatile - ttl:从设置了过期时间的键中,选择剩余过期时间最短的键进行淘汰。这种策略适用于希望优先淘汰即将过期的数据,以尽快释放内存的场景。

Redis 在不同缓存场景中的实践

单应用缓存

  1. Web 应用页面缓存:在一个简单的 Web 应用中,经常会有一些静态页面或者动态页面中不经常变化的部分。可以将这些页面片段缓存到 Redis 中。例如,一个电商网站的商品分类导航栏,其内容可能不会频繁更新。
from flask import Flask, Response
import redis

app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, db=0)


@app.route('/category - nav')
def category_nav():
    nav = r.get('category - nav - cache')
    if nav:
        return Response(nav, mimetype='text/html')
    else:
        # 从数据库或其他数据源获取导航栏内容
        nav_content = "<ul><li>分类 1</li><li>分类 2</li></ul>"
        r.set('category - nav - cache', nav_content)
        return Response(nav_content, mimetype='text/html')


if __name__ == '__main__':
    app.run(debug=True)

在这段 Flask 应用代码中,每次请求 /category - nav 时,先检查 Redis 缓存中是否有商品分类导航栏的内容。如果有,直接返回;如果没有,从数据源获取后缓存到 Redis 并返回。 2. 用户会话缓存:在 Web 应用中,用户的会话信息(如登录状态、用户设置等)可以缓存到 Redis 中。以 Java Web 应用为例,使用 Servlet 和 Jedis:

import redis.clients.jedis.Jedis;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/user - session")
public class UserSessionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Jedis jedis = new Jedis("localhost", 6379);
        String userId = request.getParameter("userId");
        String sessionInfo = jedis.get("user:session:" + userId);
        if (sessionInfo != null) {
            response.getWriter().println("从缓存中获取到用户会话信息: " + sessionInfo);
        } else {
            // 从数据库获取用户会话信息
            String newSessionInfo = "用户登录状态: 已登录";
            jedis.set("user:session:" + userId, newSessionInfo);
            response.getWriter().println("从数据库获取并缓存用户会话信息: " + newSessionInfo);
        }
        jedis.close();
    }
}

在上述代码中,根据用户 ID 从 Redis 缓存中获取用户会话信息,如果不存在则从数据库获取并缓存。

分布式缓存

  1. 分布式系统中的数据缓存:在一个分布式电商系统中,多个服务可能需要访问商品的详细信息。可以使用 Redis 作为分布式缓存来存储商品数据。例如,商品服务、订单服务等都可能需要获取商品的价格、库存等信息。
import redis

r = redis.Redis(host='redis - cluster - master', port=6379, db=0)


def get_product_info(product_id):
    product_info = r.get('product:' + str(product_id))
    if product_info:
        return product_info
    else:
        # 从数据库获取商品信息
        from database import get_product_from_db
        product = get_product_from_db(product_id)
        r.set('product:' + str(product_id), product)
        return product


在这段 Python 代码中,假设 redis - cluster - master 是 Redis 集群的主节点地址。不同的服务通过调用 get_product_info 函数来获取商品信息,首先从 Redis 缓存中查找,若不存在则从数据库获取并缓存。 2. 缓存一致性问题及解决方案:在分布式系统中,缓存一致性是一个关键问题。当数据在数据源中更新时,需要确保所有节点的缓存数据也得到相应更新。一种常见的解决方案是使用发布 - 订阅(Pub/Sub)模式。例如,当商品的价格在数据库中更新时,发布一个消息到 Redis 的某个频道:

r.publish('product - updates', 'product:1:price - updated') 

其他服务订阅该频道:

p = r.pubsub()
p.subscribe('product - updates')
for message in p.listen():
    if message['type'] =='message':
        update_key = message['data'].decode('utf - 8')
        if update_key.startswith('product:'):
            r.delete(update_key) 

在上述代码中,当有商品更新消息发布到 product - updates 频道时,订阅该频道的服务会收到消息,并删除对应的 Redis 缓存键,以保证下次获取数据时从数据源重新加载最新数据。

Redis 缓存性能优化

减少网络开销

  1. 批量操作:Redis 提供了一些批量操作的命令,如 mgetmset 等。通过这些命令,可以在一次网络请求中获取或设置多个键值对,减少网络请求次数。例如,获取多个用户的基本信息:
user_keys = ['user:1:name', 'user:2:name', 'user:3:name']
names = r.mget(user_keys)
print(names) 

这里通过 mget 方法一次获取多个用户的名字,相比多次调用 get 方法,大大减少了网络开销。 2. 合理设置连接池:在应用程序中,创建和销毁 Redis 连接是有开销的。使用连接池可以复用连接,减少连接创建和销毁的次数。以 Java 为例,使用 Jedis 连接池:

JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(10);
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);
try (Jedis jedis = jedisPool.getResource()) {
    // 使用 Jedis 执行 Redis 操作
    jedis.set("key", "value");
} catch (Exception e) {
    e.printStackTrace();
} finally {
    jedisPool.close();
}

在上述代码中,通过 JedisPool 创建了一个连接池,设置了最大连接数和最大空闲连接数,应用程序从连接池中获取连接执行 Redis 操作,操作完成后归还连接,避免了频繁创建和销毁连接。

优化内存使用

  1. 数据结构选择:根据实际需求选择合适的数据结构可以有效减少内存使用。例如,如果要存储一个用户的多个属性,使用哈希结构比使用多个字符串结构更节省内存。假设存储用户信息:
# 使用哈希结构
user_info = {
    'name': 'Bob',
    'age': 30,
    'email': 'bob@example.com'
}
r.hmset('user:3', user_info)

# 若使用字符串结构
r.set('user:3:name', 'Bob')
r.set('user:3:age', '30')
r.set('user:3:email', 'bob@example.com')

对比可以发现,哈希结构将用户的所有信息存储在一个键下,而使用字符串结构则需要多个键,在数据量较大时,哈希结构能更有效地利用内存。 2. 优化键值设计:尽量缩短键的长度,避免使用过长的键名。键名过长会占用额外的内存空间。同时,合理设计值的存储方式,例如对于较大的文本数据,可以考虑进行压缩后再存储。例如,使用 Python 的 zlib 库对文本进行压缩:

import zlib

large_text = "这是一段很长很长很长的文本..."
compressed_text = zlib.compress(large_text.encode('utf - 8'))
r.set('large - text - key', compressed_text)

# 获取数据时解压缩
retrieved_compressed_text = r.get('large - text - key')
decompressed_text = zlib.decompress(retrieved_compressed_text).decode('utf - 8')

在上述代码中,将大文本压缩后存储到 Redis,获取时再解压缩,减少了内存占用。

监控与调优

  1. 使用 Redis 内置监控工具:Redis 提供了 INFO 命令,可以获取 Redis 服务器的各种运行信息,如内存使用情况、客户端连接数、操作命令统计等。通过分析这些信息,可以了解 Redis 的性能瓶颈。例如,通过 redis - cli info 命令可以在终端查看 Redis 的详细信息。
  2. 应用程序层面的监控:在应用程序中,可以记录 Redis 操作的耗时、命中率等指标。以 Python 应用为例,可以使用 time 模块记录操作耗时:
import time

start_time = time.time()
value = r.get('test - key')
end_time = time.time()
print(f"获取数据耗时: {end_time - start_time} 秒")

通过记录这些指标,可以分析 Redis 操作对应用程序性能的影响,并针对性地进行优化。同时,可以通过计算缓存命中率(缓存命中次数 / 总请求次数)来评估缓存的有效性,若命中率较低,可能需要调整缓存策略或数据结构。