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

Redis缓存入门与实战

2022-06-087.2k 阅读

1. Redis 简介

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

Redis 的主要优势在于其高性能,由于数据存储在内存中,读写速度极快。它还支持数据的持久化,能够将内存中的数据保存到磁盘,以便在重启后恢复数据。此外,Redis 具备高可用性和分布式特性,通过主从复制和集群模式可以满足大规模应用的需求。

2. Redis 安装与基本使用

2.1 安装 Redis

在 Linux 系统上,可以通过包管理器安装 Redis。例如,在 Ubuntu 上,可以使用以下命令安装:

sudo apt update
sudo apt install redis-server

安装完成后,Redis 服务会自动启动。可以通过以下命令检查 Redis 服务状态:

sudo systemctl status redis-server

如果需要停止或重启 Redis 服务,可以使用以下命令:

sudo systemctl stop redis-server
sudo systemctl restart redis-server

在 Windows 系统上,可以从 Redis 官方网站下载 Windows 版本的安装包进行安装。安装完成后,在命令行中进入 Redis 安装目录,启动 Redis 服务器:

redis-server.exe

2.2 Redis 客户端连接

安装 Redis 后,会自带一个 Redis 客户端 redis-cli。在 Linux 或 Windows 上,都可以通过该客户端连接到 Redis 服务器。 在 Linux 上,直接在命令行输入 redis-cli 即可连接到本地 Redis 服务器:

redis-cli

在 Windows 上,进入 Redis 安装目录后,执行以下命令连接到本地 Redis 服务器:

redis-cli.exe

连接成功后,可以执行各种 Redis 命令。例如,设置一个键值对:

set mykey "Hello, Redis!"

获取键对应的值:

get mykey

3. Redis 数据结构与应用场景

3.1 字符串(String)

字符串是 Redis 最基本的数据结构,一个键可以对应一个字符串值。字符串类型可以存储任何类型的数据,如文本、数字等。在实际应用中,常用于缓存简单数据,如用户信息、配置参数等。 代码示例(使用 Python 的 redis - py 库)

import redis

# 连接 Redis 服务器
r = redis.Redis(host='localhost', port=6379, db = 0)

# 设置字符串值
r.set('user:1:name', 'John')

# 获取字符串值
name = r.get('user:1:name')
print(name.decode('utf - 8'))

3.2 哈希(Hash)

哈希类型用于存储对象,它将一个键映射到一个字段 - 值的映射表。适用于存储和管理复杂对象,如用户详细信息、商品信息等。每个哈希可以包含多个字段,每个字段都有一个对应的值。 代码示例

# 设置哈希值
r.hset('user:1', 'age', 30)
r.hset('user:1', 'email', 'john@example.com')

# 获取哈希中的字段值
age = r.hget('user:1', 'age')
email = r.hget('user:1', 'email')
print(int(age), email.decode('utf - 8'))

# 获取整个哈希
user = r.hgetall('user:1')
print(user)

3.3 列表(List)

列表是一个有序的字符串元素集合。可以从列表的两端进行插入和删除操作。常用于实现消息队列、任务队列等,也可用于存储日志记录等有序数据。 代码示例

# 向列表中添加元素
r.rpush('task:queue', 'task1')
r.rpush('task:queue', 'task2')

# 获取列表中的所有元素
tasks = r.lrange('task:queue', 0, -1)
for task in tasks:
    print(task.decode('utf - 8'))

# 从列表左侧弹出一个元素
task = r.lpop('task:queue')
print(task.decode('utf - 8'))

3.4 集合(Set)

集合是一个无序的、不重复的字符串元素集合。支持添加、删除元素,以及集合的交集、并集、差集等操作。常用于标签管理、好友关系管理等场景,例如找出共同关注的人。 代码示例

# 向集合中添加元素
r.sadd('user:1:tags', 'python')
r.sadd('user:1:tags', 'backend')

# 获取集合中的所有元素
tags = r.smembers('user:1:tags')
for tag in tags:
    print(tag.decode('utf - 8'))

# 集合的交集操作
r.sadd('user:2:tags', 'python')
r.sadd('user:2:tags', 'frontend')
common_tags = r.sinter('user:1:tags', 'user:2:tags')
for tag in common_tags:
    print(tag.decode('utf - 8'))

3.5 有序集合(Sorted Set)

有序集合和集合类似,但每个元素都会关联一个分数(score),通过分数来对元素进行排序。常用于排行榜、带权重的任务队列等场景,如游戏玩家的排行榜。 代码示例

# 向有序集合中添加元素
r.zadd('game:rank', {'player1': 100, 'player2': 200})

# 获取有序集合中按分数排序的元素
rank = r.zrange('game:rank', 0, -1, withscores=True)
for player, score in rank:
    print(player.decode('utf - 8'), score)

# 获取分数大于某个值的元素
high_scores = r.zrangebyscore('game:rank', 150, +inf)
for player in high_scores:
    print(player.decode('utf - 8'))

4. Redis 缓存原理

4.1 缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中没有,所以会直接查询数据库,若数据库中也没有,每次请求都会打到数据库,造成数据库压力增大。 解决方案

  • 布隆过滤器:在查询数据库之前,先通过布隆过滤器判断数据是否存在。布隆过滤器是一种概率型数据结构,它可以快速判断一个元素是否在集合中,虽然存在一定的误判率,但可以有效减少对数据库的无效查询。
  • 空值缓存:当查询数据库发现数据不存在时,将空值也缓存起来,并设置一个较短的过期时间,这样下次查询相同数据时,直接从缓存中获取空值,避免再次查询数据库。

4.2 缓存雪崩

缓存雪崩是指在同一时刻大量的缓存数据同时过期,导致大量请求直接打到数据库,造成数据库压力过大甚至崩溃。 解决方案

  • 随机过期时间:在设置缓存过期时间时,采用随机值,使缓存过期时间分散开,避免大量缓存同时过期。
  • 加锁排队:在缓存失效时,使用分布式锁(如 Redis 自带的 SETNX 命令实现)来保证只有一个请求能查询数据库并更新缓存,其他请求等待,从而避免大量请求同时查询数据库。

4.3 缓存击穿

缓存击穿是指一个热点 key 在缓存过期的瞬间,大量请求同时访问,导致这些请求全部打到数据库。 解决方案

  • 永不过期:对于热点数据,不设置过期时间,同时在更新数据时,采用先更新数据库,再更新缓存的方式,确保缓存数据的一致性。
  • 互斥锁:和缓存雪崩中的加锁排队类似,在缓存过期时,使用互斥锁保证只有一个请求能查询数据库并更新缓存,其他请求等待。

5. Redis 缓存实战

5.1 Web 应用中的缓存

在 Web 应用中,缓存可以显著提高系统性能。以一个简单的用户信息查询接口为例,假设接口根据用户 ID 查询用户详细信息。 后端代码(使用 Node.js 和 Express 框架,结合 ioredis 库)

const express = require('express');
const Redis = require('ioredis');

const app = express();
const redis = new Redis();

app.get('/user/:id', async (req, res) => {
    const userId = req.params.id;
    let user = await redis.get(`user:${userId}`);

    if (user) {
        user = JSON.parse(user);
        res.json(user);
    } else {
        // 模拟从数据库查询用户信息
        const userFromDB = { id: userId, name: 'User Name', age: 25 };

        // 将用户信息存入缓存
        await redis.set(`user:${userId}`, JSON.stringify(userFromDB));

        res.json(userFromDB);
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

5.2 分布式缓存

在分布式系统中,多个应用实例可能需要共享缓存数据。Redis 提供了主从复制和集群模式来实现分布式缓存。 主从复制:主节点负责写操作,从节点复制主节点的数据。当主节点数据发生变化时,会将变化同步给从节点。这种方式可以提高读性能,因为读请求可以分摊到多个从节点上。 集群模式:Redis 集群是一个由多个节点组成的分布式系统,数据分布在不同的节点上。每个节点负责一部分数据的存储和读写。集群模式可以提供高可用性和扩展性,适用于大规模的分布式应用。 代码示例(使用 Java 的 Jedis 库连接 Redis 集群)

import redis.clients.jedis.*;
import java.util.HashSet;
import java.util.Set;

public class RedisClusterExample {
    public static void main(String[] args) {
        Set<HostAndPort> jedisClusterNodes = new HashSet<>();
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7000));
        jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7001));
        // 添加更多节点

        JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes);

        jedisCluster.set("key", "value");
        String value = jedisCluster.get("key");
        System.out.println("Value from Redis Cluster: " + value);

        jedisCluster.close();
    }
}

6. Redis 缓存的优化与管理

6.1 缓存容量管理

合理设置 Redis 的缓存容量非常重要。如果缓存容量过小,可能导致频繁的缓存淘汰,影响性能;如果缓存容量过大,会浪费内存资源。可以通过 Redis 配置文件中的 maxmemory 参数来设置最大内存。 当达到最大内存时,Redis 会根据设置的 maxmemory - policy 策略进行缓存淘汰。常见的淘汰策略有:

  • noeviction:不淘汰任何数据,当内存不足时,执行写操作会报错。
  • volatile - lru:从设置了过期时间的键中,淘汰最近最少使用的键。
  • allkeys - lru:从所有键中,淘汰最近最少使用的键。
  • volatile - ttl:从设置了过期时间的键中,淘汰即将过期的键。
  • volatile - random:从设置了过期时间的键中,随机淘汰键。
  • allkeys - random:从所有键中,随机淘汰键。

6.2 缓存预热

缓存预热是指在系统上线前或缓存失效后,提前将热点数据加载到缓存中,避免在系统运行初期由于大量缓存未命中而导致数据库压力过大。可以通过脚本批量将数据加载到 Redis 中。 例如,在 Python 中可以这样实现:

import redis

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

# 假设从数据库获取热点数据的函数
def get_hot_data_from_db():
    # 实际逻辑从数据库查询数据
    return [('user:1', {'name': 'John', 'age': 30}), ('user:2', {'name': 'Jane', 'age': 25})]

hot_data = get_hot_data_from_db()
for key, value in hot_data:
    r.set(key, str(value))

6.3 缓存监控与维护

可以使用 Redis 提供的 INFO 命令获取 Redis 服务器的运行状态信息,包括内存使用情况、客户端连接数、缓存命中率等。通过监控这些指标,可以及时发现性能问题并进行优化。 在 Linux 上,可以通过 redis - cli info 命令获取信息。在代码中,也可以通过相应的 Redis 客户端库获取这些信息。例如,在 Python 中:

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
info = r.info()
print(info)

定期清理无效的缓存数据也是维护缓存性能的重要工作。可以通过设置合理的过期时间,让 Redis 自动淘汰过期数据。同时,对于一些不再使用的缓存键,也可以手动删除。

7. 与其他技术的结合

7.1 Redis 与关系型数据库

在大多数应用中,Redis 通常作为关系型数据库(如 MySQL、PostgreSQL)的缓存层。数据的持久化存储在关系型数据库中,而频繁读取的数据则缓存在 Redis 中。 在更新数据时,需要注意保持缓存和数据库的一致性。常见的策略有:

  • 先更新数据库,再更新缓存:这种方式简单直接,但在高并发场景下可能会出现缓存更新失败导致数据不一致的问题。
  • 先删除缓存,再更新数据库:在更新数据库前先删除缓存,下次读取时会重新从数据库加载数据并更新缓存。但在删除缓存后、更新数据库前,如果有读请求,会导致脏数据被返回。
  • 延时双删:先删除缓存,更新数据库,然后延迟一段时间再次删除缓存。这段延迟时间要足够长,确保数据库的更新操作已经完成,其他读请求不会读到脏数据。

7.2 Redis 与消息队列

Redis 本身可以作为简单的消息队列使用,通过列表(List)数据结构的 rpushlpop 操作实现消息的入队和出队。但在一些复杂的场景下,可能会结合专业的消息队列系统,如 RabbitMQ、Kafka 等。 Redis 与消息队列结合时,可以将消息队列中的消息作为缓存的更新信号。例如,当消息队列接收到一条数据更新的消息时,触发 Redis 缓存的更新操作,确保缓存数据的实时性。

7.3 Redis 与分布式系统框架

在分布式系统框架中,如 Spring Cloud,Redis 可以用于实现分布式缓存、分布式锁等功能。Spring Cloud 提供了集成 Redis 的相关组件,使得在 Spring Boot 应用中使用 Redis 更加方便。 例如,在 Spring Boot 应用中配置 Redis 缓存:

spring:
  cache:
    cache - names: userCache
    redis:
      host: localhost
      port: 6379

在服务代码中使用缓存注解:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable("userCache")
    public String getUserById(String id) {
        // 模拟从数据库获取用户信息
        return "User Information for " + id;
    }
}

8. 常见问题与解决方法

8.1 Redis 连接问题

在连接 Redis 服务器时,可能会遇到连接失败的问题。常见原因有:

  • Redis 服务器未启动:检查 Redis 服务是否已经启动,使用相应的命令启动 Redis 服务。
  • 端口号错误:确认配置的 Redis 端口号是否正确,默认端口号为 6379。
  • 防火墙阻挡:如果服务器开启了防火墙,需要确保 Redis 服务端口已开放。在 Linux 上,可以使用 iptablesfirewalld 命令开放端口。

8.2 数据一致性问题

如前文所述,在缓存和数据库结合使用时,可能会出现数据一致性问题。除了采用前面提到的更新策略外,还可以通过以下方式解决:

  • 使用事务:在 Redis 中,可以使用 MULTIEXEC 命令组成事务,确保多个操作的原子性,避免在更新缓存时出现部分操作失败导致的数据不一致。
  • 监控与修复:定期检查缓存和数据库中的数据一致性,通过比对关键数据,发现不一致时及时进行修复。

8.3 性能问题

Redis 通常性能很高,但在某些情况下可能会出现性能下降的问题。可能的原因和解决方法如下:

  • 内存不足:检查 Redis 的内存使用情况,如果内存不足,调整 maxmemory 参数或采用合适的缓存淘汰策略。
  • 网络问题:网络延迟或带宽不足可能影响 Redis 的性能。检查网络连接,确保网络稳定,并考虑优化网络配置。
  • 大量小键值对:如果 Redis 中存储了大量的小键值对,会增加内存碎片,影响性能。可以考虑将相关的小键值对合并成一个哈希类型存储。