你是小阿巴,刚入职的程序员。
这天,产品经理找到你:阿巴阿巴,用户吐槽咱们网站首页加载太慢,快优化!
你打开监控一看,好家伙!每秒有上万个用户在访问首页,每次都要查询 MySQL 数据库来获取热门文章。
虽然你运用毕生所学优化了数据库查询,但它还是扛不住这么高的并发。
你急得满头大汗:数据库快撑不住了,怎么办啊?
这时,你的导师 —— 号称 “后端之狗” 的鱼皮路过,淡定地说了 3 个字:Redis(瑞迪斯)。
你一脸懵:Redis?那是啥?
⭐️ 推荐观看本文对应视频:
第一阶段:认识 Redis
鱼皮:Redis 的全称是 Remote Dictionary Server(远程字典服务器),是一个 基于内存的 K/V 存储系统。
你:远程?字典?K/V?这些都是什么啊?
鱼皮:你可以把 Redis 当成一个数据库,服务器可以通过网络远程操作它写入和读取数据。K/V 就是 Key-Value 键值对,数据就像字典一样保存。
你可以通过 Key 快速查询到对应的 Value。而且因为数据存在内存中,Redis 的读写速度有时比 MySQL 快 100 倍!
你眼前一亮:那我赶紧装一个试试!
机智如你,直接打开
但是怎么操作 Redis 呢?
鱼皮:可以使用官方提供的命令行工具 Redis CLI 连接并操作 Redis。
打开终端输入下列命令:
redis-cli -h 127.0.0.1 -p 6379
连上之后,咱们试试最基础的操作,利用 SET 命令保存一个键值对:
SET name "xiaoaba"
然后通过键读取到值:
GET name
你刚敲完命令,屏幕立刻返回 "小阿巴"。
你大为震惊:秒回!这也太快了!
鱼皮:你刚刚保存的值是 Redis 的 String 字符串 类型,只是最简单的一种。
你:还有其他类型?
鱼皮:当然,Redis 能存的东西可多着呢~ 它有 5 种基本数据结构,适用于不同场景:
1)String 字符串:存简单的值,比如用户名、计数器
SET username:1 "鱼皮"
GET username:1
2)Hash 哈希表:存对象,比如用户信息 {name: "小阿巴", age: 18}
HSET user:1001 name "小阿巴" age 18
HGET user:1001 name
3)List 列表:存有序数据,比如最新的 10 条评论
LPUSH comments "太棒了!" "学到了"
LRANGE comments 0 -1
4)Set 集合:存不重复的数据,比如点赞用户列表
SADD post:1:likes user1 user2 user3
SMEMBERS post:1:likes
5)SortedSet 有序集合:存需要排序且不重复的数据,比如游戏排行榜
ZADD leaderboard 100 "玩家A" 95 "玩家B" 90 "玩家C"
ZRANGE leaderboard 0 -1 WITHSCORES
你感叹道:这用法也太多了,我要背这些命令吗?
鱼皮:千万别背!用的时候去查
而且想偷懒的话,推荐你装个 Redis 官方的可视化工具
你:哇,确实方便多了!但是我怎么用 Java 代码操作 Redis 呢?
鱼皮:主流编程语言都有操作 Redis 的客户端 SDK。
对于 Java 开发者,可以选择 Jedis、Lettuce、Spring Data Redis 和 Redisson,它们封装了很多操作 Redis 的方法,几行代码就能搞定。
时间充足的话,建议你看看某马的 Redis 教程来系统学习,我当年也看过,讲的很好,先把基础篇和实战篇看完就够了。
你:得咧,我这就看!
第二阶段:实战应用
半个月后,你已经看了不少教程,准备解决热门文章查询太慢的问题。
你实现了经典的缓存逻辑:第一次查询时,先从 Redis 中查找,如果没有找到,再从 MySQL 数据库中查询,然后将结果存入 Redis 中,并设置过期时间。后续相同的查询就能直接从 Redis 返回结果。
示例代码:
public List<Article> getHotArticles() {
// 1. 先从 Redis 查询
String cacheKey = "hot_articles";
String cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
// 缓存命中,直接返回
return JSON.parseArray(cachedData, Article.class);
}
// 2. 缓存未命中,查询数据库
List<Article> articles = articleMapper.selectHotArticles();
// 3. 将结果存入 Redis,设置 10 分钟过期
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(articles), 10, TimeUnit.MINUTES);
return articles;
}
使用 Redis 后,查询速度从 3 秒缩短到了 0.3 秒,产品经理看你的眼神都变了,你很是得意。
鱼皮:不错,不过 Redis 的作用可不仅仅是缓存,你还能用它继续优化项目么?比如之前用户反馈的登录态失效问题。
你想了想:Redis 的本质是 存储系统,也就是说,可以利用 Redis 来存储多个服务器间需要共享的数据。公司有 2 台服务器,用户第一次登录时,Session 存在服务器 A 本地;刷新页面后,请求被分配到服务器 B,就找不到 Session 了,导致丢失登录态。那我就把需要共享的 Session 数据存到 Redis 中,这样无论请求分配到哪台服务器,都能从 Redis 里拿到 Session。
鱼皮欣慰地点了点头:不错,这就是典型的 分布式 Session 问题。
类似的,分布式锁也可以利用 Redis 实现,让多个服务器都从 Redis 去获取锁资源,谁先拿到锁,谁就能操作,其他人排队等待。
你:确实,那 Redis 还有其他应用场景么?
鱼皮:那可太多了,Redis 除了 5 种基本数据结构外,还提供了很多 “高级” 数据结构,专门解决特定场景的问题。
-
GEO:存地理位置坐标,实现附近的人、附近的餐厅功能
-
Bitmap:用 1 个 bit 表示一个状态,节约内存,实现用户签到、在线状态统计
-
HyperLogLog:适用于大规模统计 UV(独立访客数),虽然存在误差,但内存占用只有 12 KB
-
布隆过滤器:快速判断数据是否存在,可用来防止无效查询打到数据库
-
Stream:消息队列功能,可以实现异步任务,比如用户下单后异步发送短信通知(但更推荐用专业的消息队列)
鱼皮:对了,还有更高级的玩法,Redis 支持编写 Lua 脚本 保证多个操作的原子性,确保要么全部成功,要么全部失败,避免数据不一致的问题。
你:这些听起来好高级,感觉学不完了……
鱼皮:想什么呢,肯定学不完啊!
Redis 是在持续更新的,去看看
Redis 是实战型技术,光看不练等于白学,一定要多动手实践。
你:好,那我就先给
第三阶段:常见问题和解决方案
一个月后的某天,凌晨 3 点,你被电话吵醒了。
运维小哥急切地说:阿巴阿巴,网站挂了!数据库查询一直超时,你快看看!
你很是疑惑:都用 Redis 缓存了,还能超时?
你赶紧去公司看了下日志,发现原来有恶意用户在疯狂查询一个不存在的文章 ID,每次 Redis 缓存中都查不到,请求就直接打到了 MySQL 数据库上!
鱼皮这时也赶到了公司:这就是经典的 缓存穿透 问题,恶意请求故意查询不存在的数据,绕过缓存,直接攻击数据库。
你汗流浃背了:那怎么办?
鱼皮:最简单的方法是,即使数据库查询结果为空,仍将这个空结果缓存到 Redis 中,并设置一个较短的过期时间。后续相同请求会直接命中缓存的空值,避免访问数据库。
你恍然大悟,赶紧写代码补充了缓存空值的逻辑。
public Article getArticleById(Long id) {
String cacheKey = "article:" + id;
String cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
// 如果是空值标识,直接返回 null
if ("__NULL__".equals(cachedData)) {
return null;
}
return JSON.parseObject(cachedData, Article.class);
}
// 查询数据库
Article article = articleMapper.selectById(id);
if (article != null) {
// 存储正常数据,10 分钟过期
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(article), 10, TimeUnit.MINUTES);
} else {
// 存储空值标识,2 分钟过期
redisTemplate.opsForValue().set(cacheKey, "NULL", 2, TimeUnit.MINUTES);
}
return article;
}
鱼皮提醒道:缓存穿透只是其中一个问题,实际项目中还会遇到 缓存击穿 和 缓存雪崩。
1)缓存击穿:热门数据突然过期,大量请求同时涌向数据库。可以用互斥锁,让请求排队一个一个来,第一个请求负责查数据库并重建缓存,其他请求等缓存重建完成后再查询。
2)缓存雪崩:大量缓存同时过期,数据库瞬间压力巨大。解决方法是给过期时间加上随机值,避免同时失效。
你感慨道:Redis 的坑还真不少!
鱼皮:这就是为什么要学习 Redis 的最佳实践,比如合理设计缓存键名、设置合适的过期时间、选择正确的数据结构等等。
鱼皮:不过这才刚开始呢,你以后还会遇到缓存和数据库的数据不一致、查询 Redis 阻塞、内存爆满等问题,都是需要学习的,以后我再给你讲吧。
你:那如果 Redis 本身也扛不住高并发怎么办?
鱼皮:那就搭建 多级缓存,比如服务器本地内存缓存 => Redis 分布式缓存 => 数据库,层层过滤。用户请求先查本地缓存,没有再查 Redis,还没有才查数据库。每一层都能拦截一部分请求,大大减少 Redis 的压力。
还有 缓存预热,比如大促活动前,提前把热门商品数据加载到 Redis 中,避免活动开始时大量请求同时打到数据库。
第四阶段:高级特性和生产部署
用了一段时间 Redis 后,你开始有点飘了。
你对着新来的实习生阿坤炫耀:Redis 我闭着眼睛都能写!什么缓存、分布式锁、多级缓存,我都搞定过!
结果第二天,老板黑着脸找到你:昨晚机房断电重启后,很多用户反馈自己的学习进度丢失了,怎么回事?!
你这才意识到问题的严重性:Redis 数据存储在内存,断电不就没了吗?
鱼皮及时出现:看来你对 Redis 的理解还停留在玩具阶段啊,生产环境的 Redis 可不是这么简单!我来问你几个问题。
第一问:怎么防止数据丢失?
你支支吾吾:做…… 做备份?
没想到,旁边的阿坤突然鸡叫起来:可以利用 Redis 提供的 2 种持久化方案!
1)RDB 快照:定期把内存数据完整保存到硬盘,像拍照一样,恢复快但可能丢失最新数据。
2)AOF 日志:把每个写操作都记录下来,像记日记,数据更安全但恢复较慢(需要重新执行所有写操作)。
第二问:Redis 服务器挂了怎么办?
你挠头:重启?
鱼皮摇头:用户等得起吗?
阿坤又鸡叫起来:要部署 主从集群,主节点负责写数据,从节点实时同步主节点的数据。这样即使主节点挂了,从节点立刻顶上,用户毫无感知。
第三问:主节点挂了,谁来决定哪个从节点升级?
你无言以对,但是那阿坤竟从容不迫:用 哨兵机制,哨兵就像监工,可以实时监控 Redis 集群健康状态。发现主节点挂了,自动选择一个从节点升级为新主节点,实现自动故障转移。
第四问:数据量太大,单台 Redis 存不下怎么办?
你眼前一亮,终于等到自己会的问题了,抢答道:删除数据!
阿坤用看流浪狗的眼神看了你一眼,回答道:首先 Redis 自身有多种淘汰策略,会自动删除不常用的数据。
还可以搭建 分片集群,把数据按照某种规则分散到多台 Redis 上,每台只存一部分。Redis 使用 哈希槽 机制来分配数据,既能存储更多数据,又能承受更高并发。
鱼皮拍了拍阿坤的肩膀:小伙子年轻有为啊!
你羞愧地抬不起头:我以为自己已经掌握了 Redis,原来只是学了个皮毛……
鱼皮:这些都是 Redis 在生产环境必须考虑的问题,大厂的 Redis 集群动辄几十上百个节点,就是为了保证 高可用、高性能、高可扩展性。小阿巴,你还要好好跟阿坤学习啊。
第五阶段:深入底层原理
被鱼皮连环拷问后,你主动找到阿坤:我想深入学习 Redis,不能只停留在会用的层面,请问你是怎么学习底层原理的呀?
阿坤有些惊讶:咦?你不背八股文的么?去
你震惊了:现在的校招生,竟然恐怖如斯!
鱼皮:阿坤你别逗他了,其实我们可以带着问题学习。比如你知道 Redis 为什么这么快 吗?
你想了想:因为数据存在内存里?
鱼皮:这只是表面原因。深层次的原因有很多:
-
高效的数据结构:Redis 底层使用了动态字符串、跳表、压缩列表等经过优化的数据结构
-
单线程模型:避免了多线程的上下文切换开销
-
IO 多路复用:一个线程就能同时处理成千上万个连接
-
内存管理:有完善的内存淘汰策略,比如 LRU 算法
你惊讶:单线程还能这么快?
鱼皮:对!这就是 Redis 设计的巧妙之处。从这些问题出发,去阅读相关的文章,或者直接像阿坤说的刷一刷 Redis 面试题,就能快速学会很多核心知识点。
如果想系统学习,可以看看《Redis 设计与实现》这本书,讲得很透彻;甚至可以看看 Redis 的开源代码。
要记住,学习底层原理不是为了炫技,而是为了更好地使用 Redis,遇到问题时能够快速定位和解决。
你:好的,我这就去学!
结尾
若干年后,你已经成为了公司的 Redis 专家。
不仅能熟练使用 Redis 解决各种业务问题,搭个 Redis 集群架构也是手拿把掐的。
你也像鱼皮当时一样,耐心地给新人分享学习 Redis 的经验,让他们谨记鱼皮的教诲 “Redis 是实战型技术,一定要多动手实践”。
再次遇到鱼皮是在一条昏暗的小巷,此时的他年过 35,灰头土脸。你什么都没说,只是给他点了个赞,投了 2 个币,不打扰,是你的温柔。