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:taskPHP实现:
// 生产者
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主从复制的过程:
- 从节点连接主节点,发送SYNC命令
- 主节点执行BGSAVE,生成RDB快照,同时记录新的写命令
- 主节点将RDB文件发送给从节点,从节点加载RDB
- 主节点将缓存的写命令发送给从节点,从节点执行
- 之后,主节点每收到写命令,就异步发送给从节点
优点:实现读写分离,提升读性能;数据备份,主节点宕机后从节点有数据。 缺点:主节点单点故障,主节点宕机后需要手动切换从节点为主节点。
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-entries、hash-max-ziplist-value、list-max-ziplist-entries等。 - 使用共享对象:小整数(0-9999)在Redis中是共享的,不重复创建对象。
- 避免大key:大key(如String超过10KB,Hash/List/Set元素过多)会影响性能,建议拆分。
- 设置过期时间:不需要永久存储的数据设置过期时间,自动释放内存。
- 使用内存淘汰策略:配置
maxmemory和maxmemory-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。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录