做Web开发,数据库性能是永远的痛点。MySQL虽然功能强大,但在高并发场景下,磁盘IO成为瓶颈,查询速度跟不上。这时候,缓存就成了必备的优化手段——把热点数据存到内存里,读取时直接从内存取,速度能提升几个数量级。
最早我用文件做缓存,把序列化的数据存到文件里,读取时反序列化。但文件缓存有很多问题:磁盘IO慢、不支持过期自动删除、不支持复杂的数据结构、多进程并发读写有问题。后来我了解到了Redis,用了之后就再也回不去了。
Redis是一个开源的、基于内存的键值存储数据库,被称为"数据结构服务器"。它的特点是:第一,性能极高,所有数据存在内存里,读写速度能达到每秒十万次以上;第二,数据结构丰富,支持字符串、列表、哈希、集合、有序集合五种基本数据结构,能满足各种场景;第三,支持持久化,可以把内存数据保存到磁盘,重启后不丢失;第四,支持过期时间、发布订阅、事务、Lua脚本等高级功能。
Redis的应用场景非常广泛:缓存、消息队列、排行榜、计数器、分布式锁、会话存储、实时系统等等。今天就来分享我在项目中使用Redis的入门经验。
安装和基本使用
Redis的安装很简单。Ubuntu/Debian下用apt-get安装:
sudo apt-get install redis-serverCentOS下用yum安装:
sudo yum install redis安装完成后,redis-server会自动启动,默认监听6379端口。用redis-cli命令行工具连接:
redis-cli连接成功后,就可以输入Redis命令了。
Redis最基本的数据结构是字符串(String),用SET和GET命令读写:
SET name "张三"
GET name字符串可以是普通文本,也可以是数字,Redis支持对数字进行自增自减:
SET count 10
INCR count # 自增1,返回11
INCRBY count 5 # 自增5,返回16
DECR count # 自减1,返回15字符串还可以设置过期时间,用EXPIRE命令,或者在SET时直接指定:
SET token "abc123" EX 3600 # 1小时后过期
EXPIRE name 60 # 60秒后过期
TTL name # 查看剩余过期时间这是Redis做缓存的基础——把数据序列化成字符串存进去,设置过期时间,读取时反序列化。PHP里用Predis或phpredis扩展操作Redis:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 写缓存
$data = ["id" => 1, "name" => "张三"];
$redis->setex("user:1", 3600, json_encode($data));
// 读缓存
$cached = $redis->get("user:1");
if ($cached) {
$user = json_decode($cached, true);
} else {
// 缓存未命中,从数据库查
$user = $db->query("SELECT * FROM users WHERE id = 1")->fetch();
$redis->setex("user:1", 3600, json_encode($user));
}这就是最经典的缓存模式:先查缓存,命中就返回,未命中就查数据库并写入缓存。
列表:做消息队列
Redis的列表(List)是一个双向链表,可以在头部或尾部插入/删除元素,非常适合做消息队列。
生产者用LPUSH或RPUSH往列表里放消息,消费者用LPOP或RPOP从列表里取消息:
# 生产者:往队列尾部放消息
RPUSH task_queue "task1"
RPUSH task_queue "task2"
RPUSH task_queue "task3"
# 消费者:从队列头部取消息
LPOP task_queue # 返回"task1"
LPOP task_queue # 返回"task2"但普通的POP命令是非阻塞的,如果队列里没有消息,会立即返回nil。消费者需要不断轮询,浪费CPU。Redis提供了阻塞式POP命令BLPOP和BRPOP,如果队列里没有消息,会阻塞等待,直到有消息或超时:
BLPOP task_queue 30 # 阻塞等待消息,最多等30秒这样消费者就不需要轮询了,有消息时立即返回,没消息时阻塞等待,效率很高。
用Redis做消息队列,适合简单的异步任务场景,比如发送邮件、生成缩略图、数据同步等。比RabbitMQ、Kafka这些专业的消息队列轻量很多,部署简单,性能也足够。但功能相对简单,不支持消息确认、持久化保证、复杂路由等,复杂场景还是需要专业消息队列。
有序集合:做排行榜
Redis的有序集合(Sorted Set)是一种非常强大的数据结构,它类似于集合,但每个元素都关联一个分数(score),元素按分数排序。非常适合做排行榜、优先级队列等场景。
用ZADD添加元素,ZRANGE按分数从小到大查询,ZREVRANGE按分数从大到小查询:
# 添加文章阅读量
ZADD article_views 1500 "article:1"
ZADD article_views 2300 "article:2"
ZADD article_views 800 "article:3"
ZADD article_views 3100 "article:4"
# 查询阅读量最高的前3篇文章
ZREVRANGE article_views 0 2 WITHSCORES
# 返回:article:4 3100, article:2 2300, article:1 1500
# 查询某篇文章的排名
ZREVRANK article_views "article:2" # 返回1(从0开始,第二名)
# 增加某篇文章的阅读量
ZINCRBY article_views 1 "article:1" # article:1的分数+1用有序集合做排行榜,性能非常高,插入、删除、查询排名都是O(log N)的时间复杂度,即使有几百万个元素也很快。比用MySQL的ORDER BY+LIMIT高效得多,尤其是在数据量大、更新频繁的场景下。
哈希:存储对象
Redis的哈希(Hash)类似于PHP的关联数组,适合存储对象。一个哈希可以有多个字段,每个字段有对应的值:
# 存储用户信息
HSET user:1 name "张三"
HSET user:1 age 25
HSET user:1 email "zhangsan@example.com"
# 获取用户信息
HGET user:1 name # 返回"张三"
HGETALL user:1 # 返回所有字段和值
# 修改某个字段
HSET user:1 age 26
# 自增某个字段
HINCRBY user:1 age 1用哈希存储对象,比把对象序列化成字符串存储更灵活——可以单独读取、修改某个字段,不需要读取整个对象再反序列化、修改、再序列化存回去。而且哈希的存储效率也很高,小的哈希会用压缩列表存储,占用内存很少。
缓存的注意事项
用Redis做缓存,有几个常见的问题需要注意:
第一,缓存穿透。查询一个不存在的数据,缓存未命中,请求直接打到数据库。如果被恶意攻击,大量查询不存在的数据,数据库压力会很大。解决方案是:把不存在的查询也缓存起来(存一个空值,设置较短的过期时间),或者用布隆过滤器过滤掉不存在的查询。
第二,缓存雪崩。大量缓存同时过期,或者Redis宕机,导致所有请求同时打到数据库,数据库压力骤增。解决方案是:给缓存的过期时间加一个随机值,避免同时过期;Redis做高可用(主从复制、哨兵、集群);做服务降级,数据库压力大时返回默认值或错误页。
第三,缓存一致性。缓存的数据和数据库的数据不一致。比如更新了数据库,但缓存没更新,用户读到的还是旧数据。解决方案是:更新数据库后删除缓存(而不是更新缓存),下次读取时重新从数据库加载并缓存;或者用消息队列异步更新缓存。
总结
Redis是一个非常强大的工具,从最简单的字符串缓存,到列表做消息队列,到有序集合做排行榜,到哈希存储对象,它能做的事情远超想象。而且性能极高、部署简单、使用方便,是Web开发的必备工具。
这篇文章只是Redis的入门,Redis还有很多高级功能:发布订阅、事务、Lua脚本、持久化(RDB和AOF)、主从复制、哨兵、集群等。随着项目的发展,这些功能都会用到。建议大家深入学习Redis,它会成为你技术工具箱里最锋利的武器之一。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录