Redis是2016年最流行的内存数据库(Key-Value Store)之一,由Salvatore Sanfilippo(antirez)开发。它以高性能、丰富的数据结构、灵活的应用场景著称,被广泛应用于缓存、消息队列、排行榜、计数器、分布式锁、实时系统等场景。

与传统的关系型数据库(MySQL、PostgreSQL)不同,Redis将数据存储在内存中,读写速度极快(每秒可以处理10万+次操作)。同时,Redis支持多种数据结构(String、Hash、List、Set、Sorted Set等),不仅仅是简单的键值存储,这让Redis可以应对各种复杂的应用场景。

作为一个后端开发者,我在项目中大量使用Redis,从缓存到队列,从排行榜到分布式锁,Redis几乎无处不在。今天就来系统地讲解Redis实战,从核心数据结构到典型应用场景,从持久化到高可用,帮助你在项目中用好Redis。

一、Redis简介与安装

1. 什么是Redis

Redis(Remote Dictionary Server)是一个开源的、基于内存的、高性能的键值对存储数据库。它支持多种数据结构,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、位图(Bitmap)、HyperLogLog、地理位置(Geo)等。

Redis的核心特点:

  • 高性能:基于内存操作,读写速度极快,QPS可达10万+
  • 丰富的数据结构:支持多种数据结构,应对各种场景
  • 持久化:支持RDB和AOF两种持久化方式,数据不会丢失
  • 高可用:支持主从复制、哨兵(Sentinel)、集群(Cluster)
  • 原子操作:所有操作都是原子性的,支持事务
  • 发布订阅:支持消息的发布和订阅
  • 过期策略:支持设置键的过期时间,自动删除

2. Redis安装

在Linux上安装Redis:

# 下载Redis(2016年稳定版为3.2.x)
wget http://download.redis.io/releases/redis-3.2.0.tar.gz
tar xzf redis-3.2.0.tar.gz
cd redis-3.2.0
make

# 安装
make install

# 启动Redis服务
redis-server

# 启动Redis客户端
redis-cli

在Windows上,可以使用Microsoft Open Tech团队维护的Windows版本,或者使用WSL(Windows Subsystem for Linux)。

3. Redis配置

Redis的配置文件是redis.conf,常用配置项:

# 绑定地址
bind 127.0.0.1

# 端口
port 6379

# 密码
requirepass yourpassword

# 数据库数量
databases 16

# 内存限制
maxmemory 256mb

# 内存淘汰策略
maxmemory-policy allkeys-lru

# RDB持久化
save 900 1
save 300 10
save 60 10000

# AOF持久化
appendonly yes
appendfsync everysec

二、Redis核心数据结构

1. String(字符串)

String是Redis最基本的数据类型,一个键对应一个值。String是二进制安全的,可以存储任何数据(字符串、数字、图片、序列化对象等),最大支持512MB。

常用命令:

# 设置值
SET key value
# 获取值
GET key
# 删除键
DEL key
# 判断键是否存在
EXISTS key
# 设置过期时间(秒)
EXPIRE key seconds
# 设置值并指定过期时间
SETEX key seconds value
# 自增(整数)
INCR key
# 自减
DECR key
# 增加指定值
INCRBY key increment
# 追加内容
APPEND key value
# 获取字符串长度
STRLEN key

应用场景:缓存、计数器、分布式锁、存储对象(序列化后)。

2. Hash(哈希)

Hash是一个键值对集合,适合存储对象。一个Hash可以包含多个field-value对,最多支持2^32-1个field。

常用命令:

# 设置field的值
HSET key field value
# 获取field的值
HGET key field
# 设置多个field
HMSET key field1 value1 field2 value2
# 获取多个field
HMGET key field1 field2
# 获取所有field和value
HGETALL key
# 获取所有field
HKEYS key
# 获取所有value
HVALS key
# 删除field
HDEL key field
# 判断field是否存在
HEXISTS key field
# field自增
HINCRBY key field increment
# 获取field数量
HLEN key

应用场景:存储用户信息、商品信息、配置信息等对象数据。

3. List(列表)

List是一个有序的字符串列表,按照插入顺序排序。可以在列表的头部(left)或尾部(right)添加元素,最多支持2^32-1个元素。

常用命令:

# 从左边添加
LPUSH key value1 value2
# 从右边添加
RPUSH key value1 value2
# 从左边弹出
LPOP key
# 从右边弹出
RPOP key
# 获取列表长度
LLEN key
# 获取指定范围的元素
LRANGE key start stop
# 通过索引设置元素
LSET key index value
# 通过索引获取元素
LINDEX key index
# 删除元素
LREM key count value
# 阻塞式弹出(等待元素)
BLPOP key timeout
BRPOP key timeout

应用场景:消息队列、最新列表、排行榜(简单版)、任务队列。

4. Set(集合)

Set是一个无序的字符串集合,元素不重复。最多支持2^32-1个元素。

常用命令:

# 添加元素
SADD key member1 member2
# 删除元素
SREM key member
# 获取所有元素
SMEMBERS key
# 判断元素是否存在
SISMEMBER key member
# 获取集合大小
SCARD key
# 交集
SINTER key1 key2
# 并集
SUNION key1 key2
# 差集
SDIFF key1 key2
# 随机获取一个元素
SRANDMEMBER key
# 随机弹出一个元素
SPOP key

应用场景:标签系统、共同好友、抽奖、去重。

5. Sorted Set(有序集合)

Sorted Set(ZSet)是一个有序的字符串集合,每个元素关联一个分数(score),按照分数排序。元素不重复,分数可以重复。

常用命令:

# 添加元素
ZADD key score1 member1 score2 member2
# 删除元素
ZREM key member
# 获取元素分数
ZSCORE key member
# 获取元素排名(从小到大)
ZRANK key member
# 获取元素排名(从大到小)
ZREVRANK key member
# 获取指定排名范围的元素(从小到大)
ZRANGE key start stop [WITHSCORES]
# 获取指定排名范围的元素(从大到小)
ZREVRANGE key start stop [WITHSCORES]
# 获取指定分数范围的元素
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
# 增加元素分数
ZINCRBY key increment member
# 获取集合大小
ZCARD key
# 获取指定分数范围的元素数量
ZCOUNT key min max
# 删除指定排名范围的元素
ZREMRANGEBYRANK key start stop
# 删除指定分数范围的元素
ZREMRANGEBYSCORE key min max

应用场景:排行榜、积分系统、延时队列、范围查找。

三、Redis缓存应用

缓存是Redis最常见的应用场景。通过将热点数据存储在Redis中,可以大幅减少数据库的访问压力,提升系统响应速度。

1. 缓存策略

常见的缓存策略:

  • Cache-Aside(旁路缓存):应用先查缓存,缓存命中则返回;缓存未命中则查数据库,然后将数据写入缓存。这是最常用的缓存策略。
  • Read-Through(读穿透):应用只查缓存,缓存未命中时由缓存层自动查数据库并写入缓存。
  • Write-Through(写穿透):应用写数据时,同时写缓存和数据库,缓存层保证数据一致性。
  • Write-Behind(异步写):应用写数据时只写缓存,由缓存层异步写入数据库,性能高但有数据丢失风险。

最常用的是Cache-Aside模式:

function getUser($id) {
    $key = "user:{$id}";
    $user = $redis->get($key);
    if ($user === false) {
        // 缓存未命中,查数据库
        $user = $db->query("SELECT * FROM users WHERE id = ?", [$id]);
        if ($user) {
            // 写入缓存,过期时间1小时
            $redis->setex($key, 3600, json_encode($user));
        }
    } else {
        $user = json_decode($user, true);
    }
    return $user;
}

2. 缓存穿透

缓存穿透是指查询一个不存在的数据,缓存中没有,数据库中也没有。每次请求都会穿透到数据库,导致数据库压力增大。

解决方案:

  • 缓存空值:查询不存在的数据时,将空值(如null或特殊标记)也缓存起来,设置较短的过期时间。
  • 布隆过滤器(Bloom Filter):将所有可能存在的数据哈希到一个足够大的bitmap中,查询时先通过布隆过滤器判断数据是否可能存在,如果不存在则直接返回,不查数据库。
// 缓存空值方案
function getUser($id) {
    $key = "user:{$id}";
    $user = $redis->get($key);
    if ($user === false) {
        $user = $db->query("SELECT * FROM users WHERE id = ?", [$id]);
        if ($user) {
            $redis->setex($key, 3600, json_encode($user));
        } else {
            // 缓存空值,过期时间5分钟
            $redis->setex($key, 300, json_encode(null));
        }
    } else {
        $user = json_decode($user, true);
    }
    return $user;
}

3. 缓存击穿

缓存击穿是指一个热点key在缓存过期的瞬间,大量并发请求同时穿透到数据库,导致数据库压力骤增。

解决方案:

  • 互斥锁(Mutex Lock):缓存失效时,只允许一个请求去查数据库并更新缓存,其他请求等待。
  • 永不过期:热点数据不设置过期时间,由后台异步更新缓存。
  • 提前更新:在缓存过期前,提前更新缓存。
// 互斥锁方案
function getHotData($id) {
    $key = "hot:{$id}";
    $data = $redis->get($key);
    if ($data === false) {
        // 尝试获取锁
        $lockKey = "lock:{$key}";
        $locked = $redis->set($lockKey, 1, ['nx', 'ex' => 10]);
        if ($locked) {
            // 获取锁成功,查数据库并更新缓存
            $data = $db->query("SELECT * FROM hot_data WHERE id = ?", [$id]);
            $redis->setex($key, 3600, json_encode($data));
            // 释放锁
            $redis->del($lockKey);
        } else {
            // 获取锁失败,等待后重试
            usleep(50000); // 等待50ms
            return getHotData($id); // 递归重试
        }
    } else {
        $data = json_decode($data, true);
    }
    return $data;
}

4. 缓存雪崩

缓存雪崩是指大量缓存同时过期,或者Redis宕机,导致大量请求同时穿透到数据库,导致数据库压力骤增甚至宕机。

解决方案:

  • 过期时间加随机值:在设置缓存过期时间时,加上一个随机值,避免大量缓存同时过期。
  • 多级缓存:使用本地缓存(如Caffeine、Guava Cache)+ Redis缓存,Redis失效时本地缓存还能顶一阵。
  • Redis高可用:使用Redis主从复制、哨兵、集群,保证Redis的高可用,避免单点故障。
  • 限流降级:在数据库前加限流,超过阈值则降级返回默认值或错误,保护数据库。
  • 缓存预热:系统启动时,提前将热点数据加载到缓存中。
// 过期时间加随机值
function setCache($key, $value, $baseTtl = 3600) {
    // 基础过期时间 + 0-300秒随机值
    $ttl = $baseTtl + rand(0, 300);
    $redis->setex($key, $ttl, $value);
}

5. 缓存更新策略

更新数据时,如何保证缓存和数据库的一致性?常见策略:

  • 先更新数据库,再删除缓存:这是最常用的策略。更新数据库后,删除缓存,下次查询时重新从数据库加载并写入缓存。
  • 先删除缓存,再更新数据库:删除缓存后更新数据库,但是在更新数据库的过程中,如果有读请求,会把旧数据写入缓存,导致数据不一致。可以通过延迟双删解决。
  • 更新数据库的同时更新缓存:写操作同时更新数据库和缓存,但是并发写可能导致缓存被旧值覆盖。

推荐使用"先更新数据库,再删除缓存"的策略:

function updateUser($id, $data) {
    // 1. 更新数据库
    $db->execute("UPDATE users SET ... WHERE id = ?", [$id]);
    // 2. 删除缓存
    $redis->del("user:{$id}");
}

6. 分布式锁

Redis可以实现分布式锁,用于在分布式环境中保证同一资源的互斥访问。

分布式锁的实现要点:

  • 互斥性:同一时间只有一个客户端持有锁。
  • 防死锁:锁必须有过期时间,即使客户端宕机,锁也能自动释放。
  • 解铃还须系铃人:加锁和解锁必须是同一个客户端,不能释放别人的锁。
  • 可重入性:同一个客户端可以多次获取同一把锁。
class RedisLock {
    private $redis;
    private $lockValues = [];

    public function __construct($redis) {
        $this->redis = $redis;
    }

    // 加锁
    public function lock($key, $timeout = 10, $retry = 3) {
        $value = uniqid() . mt_rand(); // 唯一值,用于安全解锁
        $lockKey = "lock:{$key}";
        while ($retry-- > 0) {
            // SET NX EX:原子操作,只有key不存在时才设置,并设置过期时间
            $result = $this->redis->set($lockKey, $value, ['nx', 'ex' => $timeout]);
            if ($result) {
                $this->lockValues[$key] = $value;
                return true;
            }
            usleep(50000); // 等待50ms重试
        }
        return false;
    }

    // 解锁(使用Lua脚本保证原子性:判断value是否匹配,匹配才删除)
    public function unlock($key) {
        $lockKey = "lock:{$key}";
        $value = $this->lockValues[$key] ?? null;
        if (!$value) return false;
        $script = <<<LUA
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
LUA;
        $this->redis->eval($script, [$lockKey, $value], 1);
        unset($this->lockValues[$key]);
        return true;
    }
}

四、Redis消息队列应用

Redis可以作为轻量级的消息队列使用,适合对消息可靠性要求不高、追求简单高性能的场景。

1. List实现简单队列

使用List的LPUSH(生产者)和RPOP(消费者)实现简单队列:

# 生产者:从左边推入消息
LPUSH queue:task "task1"
LPUSH queue:task "task2"

# 消费者:从右边弹出消息
RPOP queue:task

PHP实现:

// 生产者
function pushTask($task) {
    $redis->lPush('queue:task', json_encode($task));
}

// 消费者
function processTask() {
    while (true) {
        // BRPOP:阻塞式弹出,没有消息时等待,避免空轮询
        $result = $redis->brPop('queue:task', 30);
        if ($result) {
            $task = json_decode($result[1], true);
            // 处理任务
            handleTask($task);
        }
    }
}

优点:简单、高性能、支持阻塞读取。 缺点:不支持消息确认(消费失败消息会丢失)、不支持消息持久化(Redis宕机消息丢失)、不支持多消费者组。

2. 延迟队列

使用Sorted Set实现延迟队列,将消息的执行时间作为score,消费者轮询获取到期的消息:

// 生产者:添加延迟消息
function addDelayTask($task, $delaySeconds) {
    $executeTime = time() + $delaySeconds;
    $taskId = uniqid();
    $taskData = json_encode(['id' => $taskId, 'task' => $task]);
    $redis->zAdd('queue:delay', $executeTime, $taskData);
}

// 消费者:处理到期消息
function processDelayTask() {
    while (true) {
        $now = time();
        // 获取score小于等于当前时间的消息(即到期的消息)
        $tasks = $redis->zRangeByScore('queue:delay', 0, $now, ['limit' => [0, 10]]);
        foreach ($tasks as $taskData) {
            // 先删除,再处理(避免重复消费)
            $removed = $redis->zRem('queue:delay', $taskData);
            if ($removed) {
                $task = json_decode($taskData, true);
                handleTask($task['task']);
            }
        }
        if (empty($tasks)) {
            sleep(1); // 没有到期消息,等待1秒
        }
    }
}

应用场景:订单超时取消、延迟通知、定时任务。

3. 发布订阅(Pub/Sub)

Redis支持发布订阅模式,生产者(发布者)将消息发布到频道(channel),消费者(订阅者)订阅频道,接收消息。

# 订阅者:订阅频道
SUBSCRIBE channel:news
# 订阅者:按模式订阅
PSUBSCRIBE channel:*

# 发布者:发布消息到频道
PUBLISH channel:news "Hello Redis!"

PHP实现:

// 发布者
function publishMessage($channel, $message) {
    $redis->publish($channel, $message);
}

// 订阅者(需要在CLI模式下运行,长连接)
$redis->subscribe(['channel:news'], function($redis, $channel, $message) {
    echo "Received from {$channel}: {$message}\n";
    // 处理消息
});

注意:Redis的发布订阅是"发后即忘"(fire and forget),如果订阅者不在线,消息会丢失。不适合对消息可靠性要求高的场景,适合实时通知、广播等场景。

五、Redis持久化

Redis提供两种持久化方式:RDB和AOF。

1. RDB(Redis Database)

RDB是在指定的时间间隔内,将内存中的数据快照写入磁盘。恢复时直接将RDB文件加载到内存。

配置:

# 900秒内至少1个key变化,触发快照
save 900 1
# 300秒内至少10个key变化,触发快照
save 300 10
# 60秒内至少10000个key变化,触发快照
save 60 10000

# RDB文件名
dbfilename dump.rdb
# RDB文件目录
dir /var/lib/redis

优点:

  • RDB文件紧凑,适合备份和恢复
  • 恢复速度快(直接加载到内存)
  • 对性能影响小(fork子进程进行持久化)

缺点:

  • 可能丢失最后一次快照之后的数据
  • fork子进程时,如果数据量大,可能耗时较长,导致服务暂停

2. AOF(Append Only File)

AOF是将每一个写命令追加到文件末尾。恢复时重新执行AOF文件中的所有命令,恢复数据。

配置:

# 开启AOF
appendonly yes
# AOF文件名
appendfilename "appendonly.aof"
# 同步策略:always(每次写都同步,最安全最慢)、everysec(每秒同步,推荐)、no(由操作系统决定,最快最不安全)
appendfsync everysec
# AOF重写时是否停止同步
no-appendfsync-on-rewrite no
# AOF重写触发条件:文件大小增长100%且大于64MB
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

优点:

  • 数据安全性高,最多丢失1秒的数据(everysec模式)
  • AOF文件是文本格式,可读性好,可手动修复

缺点:

  • AOF文件通常比RDB文件大
  • 恢复速度比RDB慢(需要重新执行所有命令)
  • 可能存在Bug(极少数命令在重写时可能有问题)

3. 混合持久化(Redis 4.0+)

Redis 4.0开始支持混合持久化,结合RDB和AOF的优点:AOF重写时,将当前数据以RDB格式写入AOF文件开头,之后的写命令以AOF格式追加。恢复时先加载RDB部分,再执行AOF部分,兼顾恢复速度和数据安全。

注意:2016年Redis稳定版为3.2.x,混合持久化是4.0的特性,2016年还没有。

六、Redis高可用

1. 主从复制

Redis主从复制,一个主节点(master)负责写,多个从节点(slave)负责读。主节点将数据同步到从节点,实现读写分离和数据备份。

配置:

# 从节点配置(redis.conf)
slaveof 192.168.1.100 6379  # 主节点地址和端口
# 从节点只读
slave-read-only yes

主从复制的过程:

  1. 从节点连接主节点,发送SYNC命令
  2. 主节点执行BGSAVE,生成RDB快照,同时记录新的写命令
  3. 主节点将RDB文件发送给从节点,从节点加载RDB
  4. 主节点将缓存的写命令发送给从节点,从节点执行
  5. 之后,主节点每收到写命令,就异步发送给从节点

优点:实现读写分离,提升读性能;数据备份,主节点宕机后从节点有数据。 缺点:主节点单点故障,主节点宕机后需要手动切换从节点为主节点。

2. 哨兵(Sentinel)

Redis Sentinel是Redis官方的高可用方案,用于监控Redis主从集群,在主节点宕机时自动进行故障转移,将一个从节点提升为主节点。

Sentinel的功能:

  • 监控:持续监控主节点和从节点是否正常
  • 通知:节点故障时通知管理员
  • 自动故障转移:主节点故障时,自动将一个从节点提升为主节点,并更新其他从节点的主节点地址
  • 配置提供者:客户端连接Sentinel,获取当前主节点地址

Sentinel配置(sentinel.conf):

# 端口
port 26379
# 监控主节点:mymaster是主节点名称,192.168.1.100 6379是主节点地址,2是法定人数(至少2个Sentinel同意才进行故障转移)
sentinel monitor mymaster 192.168.1.100 6379 2
# 主节点多久无响应判定为故障(毫秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间
sentinel failover-timeout mymaster 180000
# 同时同步新主节点的从节点数量
sentinel parallel-syncs mymaster 1

通常部署3个或5个Sentinel节点(奇数个),保证多数派。

3. 集群(Cluster)

Redis Cluster是Redis的分布式集群方案,将数据分散存储在多个节点上,实现数据分片和高可用。每个节点负责一部分数据(slot),节点之间互相通信。

Redis Cluster将所有key映射到16384个slot(哈希槽)中,每个节点负责一部分slot。计算slot的公式:slot = CRC16(key) mod 16384

Cluster的特点:

  • 数据分片:数据分散在多个节点,支持大数据量
  • 高可用:每个主节点可以有多个从节点,主节点故障时自动故障转移
  • 去中心化:每个节点都知道其他节点的状态,客户端可以连接任意节点
  • 不支持多key操作(跨slot的多key命令不支持,如MSET、事务)

注意:Redis Cluster在Redis 3.0正式发布,2016年(Redis 3.2)已经比较稳定。

七、Redis性能优化

1. 内存优化

  • 合理选择数据结构:小数据量使用压缩列表(ziplist),大数据量使用哈希表。Redis配置项:hash-max-ziplist-entrieshash-max-ziplist-valuelist-max-ziplist-entries等。
  • 使用共享对象:小整数(0-9999)在Redis中是共享的,不重复创建对象。
  • 避免大key:大key(如String超过10KB,Hash/List/Set元素过多)会影响性能,建议拆分。
  • 设置过期时间:不需要永久存储的数据设置过期时间,自动释放内存。
  • 使用内存淘汰策略:配置maxmemorymaxmemory-policy,内存满时按策略淘汰key。

2. 性能优化

  • 使用连接池:避免频繁创建和销毁连接,使用连接池复用连接。
  • 批量操作:使用Pipeline或MGET/MSET等批量命令,减少网络往返。
  • 避免阻塞操作:避免使用KEYS、FLUSHALL等阻塞命令,生产环境禁用或使用SCAN替代。
  • 合理设置持久化:根据业务需求选择RDB或AOF,避免持久化对性能的影响。
  • 分离读写:使用主从复制,读请求走从节点,写请求走主节点。
  • 使用Lua脚本:将多个操作封装在Lua脚本中,原子执行,减少网络往返。

3. 安全优化

  • 设置密码:配置requirepass,禁止无密码访问。
  • 绑定内网IP:配置bind,只允许内网访问,不暴露公网。
  • 禁用危险命令:使用rename-command禁用或重命名FLUSHALL、FLUSHDB、KEYS、CONFIG等危险命令。
  • 使用防火墙:通过防火墙限制访问Redis端口的IP。

总结

Redis是一个功能强大、性能优异的内存数据库,在缓存、消息队列、排行榜、分布式锁、实时系统等场景都有广泛的应用。本文从Redis核心数据结构(String、Hash、List、Set、Sorted Set)、缓存应用(缓存策略、缓存穿透/击穿/雪崩、分布式锁、缓存更新)、消息队列应用(List队列、延迟队列、发布订阅)、持久化(RDB、AOF)、高可用(主从复制、哨兵、集群)、性能优化等方面,系统讲解了Redis实战。

Redis的核心优势:

  • 高性能:基于内存操作,QPS可达10万+
  • 丰富的数据结构:支持多种数据结构,应对各种场景
  • 高可用:支持主从复制、哨兵、集群
  • 持久化:支持RDB和AOF,数据不丢失
  • 简单易用:API简洁,学习成本低

在使用Redis时,需要注意:

  • 缓存穿透、击穿、雪崩的防护
  • 缓存和数据库的一致性
  • 大key和热key的处理
  • 内存管理和淘汰策略
  • 持久化和高可用的配置
  • 安全加固(密码、绑定IP、禁用危险命令)

Redis是后端开发者的必备技能之一,掌握Redis的核心原理和实战技巧,能让你在项目中构建更高性能、更可靠的系统。希望本文能帮助你更好地理解和使用Redis。