先说明一下,Redis 7.4在本文写作时尚未正式发布(预计2024年中发布),标题说"用了三年"是夸张的说法。实际上我用Redis有三年多了,从Redis 6.x用到7.x,最近在测试7.4的开发版。这篇文章基于我这三年使用Redis的经验,结合7.x系列的新特性,分享一些踩过的坑和总结的道理。

Redis是我日常开发中用得最多的中间件之一,缓存、队列、排行榜、分布式锁、计数器,几乎每个项目都在用。但用了三年,踩了不少坑,也走了不少弯路,才慢慢明白一些道理。

这篇文章就来分享一下我用Redis三年总结的经验和道理,希望能帮到正在用Redis的朋友。

道理一:缓存不是银弹,该查数据库还是要查

最开始用Redis的时候,我觉得缓存是万能的,什么数据都往Redis里放,觉得加了缓存就快了,就不用查数据库了。

结果踩了很多坑。

第一个坑是缓存一致性问题。数据更新了,缓存没更新,导致用户看到的是旧数据。比如用户修改了昵称,数据库里改了,但缓存里还是旧的,用户看到的还是旧昵称。一开始我用的是先更新数据库再更新缓存的策略,后来发现并发情况下会有问题,改成了先更新数据库再删除缓存,还是有小概率的不一致。最后用了延迟双删,才基本解决了这个问题。

第二个坑是缓存雪崩。大量缓存同时过期,导致请求全部打到数据库,数据库压力骤增,甚至宕机。有一次我们做活动,大量商品的缓存在同一时间设置了相同的过期时间,活动开始后缓存同时过期,数据库直接被打挂了,服务不可用了半个多小时。后来我们给过期时间加了随机值,才避免了这个问题。

第三个坑是缓存穿透。查询一个不存在的数据,缓存里没有,每次都查数据库,被恶意攻击的话,数据库会被打挂。后来用了布隆过滤器和缓存空值,才解决了这个问题。

第四个坑是缓存击穿。某个热点key过期的瞬间,大量并发请求同时查数据库,导致数据库压力骤增。后来用了互斥锁和逻辑过期,才解决了这个问题。

踩了这些坑之后我才明白,缓存不是银弹,它能提升性能,但也带来了很多复杂性。不是所有数据都适合缓存,也不是加了缓存就万事大吉了。该查数据库的时候还是要查,该做的容错和降级还是要做。

现在我用缓存的原则是:只有读多写少、热点数据、对一致性要求不高的数据,才用缓存。而且一定要考虑缓存一致性、缓存雪崩、缓存穿透、缓存击穿这些问题,做好容错和降级,确保缓存出问题的时候,数据库和服务还能正常运行。

道理二:选对数据结构,比写对代码更重要

Redis有五种基本数据结构:String、Hash、List、Set、Sorted Set,还有HyperLogLog、Bitmap、Geo、Stream等高级数据结构。

最开始用Redis的时候,我什么都用String。存用户信息,用String序列化整个对象;存列表,用String存JSON数组;存排行榜,用String存排序后的数组。结果就是,数据量一大,性能很差,修改也不方便,每次修改都要把整个对象读出来,修改,再写回去,并发情况下还会有覆盖问题。

后来慢慢学会了根据场景选择合适的数据结构,才发现Redis的数据结构设计得真的很巧妙,选对了数据结构,性能和可维护性都大大提升。

比如存用户信息,用Hash比String好。Hash可以单独修改某个字段,不需要把整个对象读出来,也不会有并发覆盖问题。而且Hash的小对象压缩(ziplist/listpack)很省内存。

比如存消息列表、最新动态,用List比String好。List可以用LPUSH和LRANGE实现最新列表,用LTRIM保留最新的N条,很方便。

比如存标签、共同好友、去重,用Set比String好。Set自带去重,支持交集、并集、差集运算,实现共同好友、共同标签等功能很方便。

比如存排行榜、热门文章,用Sorted Set比String好。Sorted Set自带排序,支持按分数范围查询、按排名查询,实现排行榜功能非常方便,性能也很好。

比如存基数统计(UV统计),用HyperLogLog比Set好。HyperLogLog用很少的内存就能统计很大的基数,虽然有一定误差,但对于UV统计这种不需要精确值的场景,非常合适。

比如存签到、状态标记,用Bitmap比String好。Bitmap用一个bit表示一个状态,非常省内存,1亿个状态只需要12.5MB,而且支持位运算,统计很方便。

选对数据结构,真的比写对代码更重要。数据结构选对了,代码简单,性能好,可维护性高;数据结构选错了,代码复杂,性能差,还容易出问题。

现在我用Redis之前,都会先想一下,这个场景适合用什么数据结构,有没有更合适的数据结构,而不是上来就用String。

道理三:持久化不是备份,该备份还是要备份

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

最开始用Redis的时候,我觉得开了持久化就安全了,数据不会丢了。结果有一次,服务器磁盘坏了,RDB和AOF文件都在坏的磁盘上,数据全丢了,损失很大。

后来我才明白,持久化不是备份。持久化只是把数据从内存写到磁盘上,防止Redis重启后数据丢失。但如果磁盘坏了、服务器挂了、机房出问题了,持久化文件也会丢。真正的备份,是把数据备份到其他地方,比如其他服务器、云存储、异地机房。

现在我用Redis的备份策略是:

第一,开启AOF持久化,用everysec策略,每秒刷一次盘,兼顾性能和安全性。同时也开启RDB,作为辅助,方便快速恢复。

第二,定期备份RDB文件。每天凌晨,用cron任务把RDB文件复制到备份服务器,同时上传到云存储。保留最近7天的备份,重要数据保留30天。

第三,异地备份。把备份文件同步到另一个城市的服务器,防止机房级别的故障。

第四,定期恢复测试。每个月做一次恢复测试,把备份文件恢复到测试环境,确认备份是可用的。很多人只备份不恢复测试,真出问题的时候才发现备份不可用,那就晚了。

还有,不是所有数据都需要备份。Redis里很多数据是缓存,丢了可以从数据库重新加载,不需要备份。只有那些不能丢的数据,比如分布式锁的状态、计数器、队列数据,才需要备份。备份之前要想清楚,哪些数据需要备份,哪些不需要,不要什么都备份,浪费存储空间和恢复时间。

道理四:集群不是越复杂越好,适合自己的才是最好的

Redis有几种部署方式:单机、主从、哨兵、集群。

最开始用Redis的时候,我觉得集群越复杂越好,越高端越好。一上来就搞Redis Cluster,三主三从,还要跨机房部署。结果维护起来很麻烦,问题也很多,比如数据迁移、扩容缩容、故障转移、跨机房延迟,搞得焦头烂额。

后来项目规模没那么大,其实用不上集群,改成了哨兵模式,一主两从,三个哨兵节点,维护简单,稳定性也高,完全够用。

现在我选择Redis部署方式的原则是:根据业务规模和需求来选,不要盲目追求复杂和高端。

如果是个人项目、小项目,QPS不高,数据量不大,对可用性要求不高,单机就够了。简单,维护成本低。

如果对可用性有一定要求,不能接受长时间停机,用主从或者哨兵。主从简单,有一个主节点多个从节点,主节点挂了需要手动切换;哨兵在主从的基础上增加了自动故障转移,主节点挂了自动选一个从节点升级为主,可用性更高。大部分中小项目,哨兵模式就够用了。

如果数据量很大,单台机器存不下,或者QPS很高,单台机器扛不住,才需要用Redis Cluster。Cluster把数据分片到多个节点,水平扩展,能支持更大的数据量和更高的QPS。但Cluster的维护成本也更高,需要处理数据迁移、扩容缩容、热点key等问题。

还有,不要盲目跨机房部署。跨机房部署虽然能提高容灾能力,但网络延迟大,数据同步慢,问题也更多。如果不是对容灾要求特别高,单机房部署就够了,做好备份就行。

适合自己的才是最好的。不要为了用技术而用技术,要根据实际需求来选。

道理五:性能优化要靠数据,不要靠感觉

用Redis,性能优化是常事。但最开始我优化性能,都是靠感觉,觉得哪里慢就优化哪里,结果经常优化了半天,没什么效果,甚至越优化越差。

后来学会了用数据说话,先监控,再分析,最后优化,效果就好多了。

Redis的性能监控,主要看这几个指标:

第一,延迟。用redis-cli --latency或者redis-cli --latency-history监控Redis的响应延迟。延迟高了,说明有问题。

第二,QPS。用info stats里的instantaneousopsper_sec看每秒操作数。QPS接近机器的极限,就需要扩容了。

第三,内存。用info memory看内存使用情况。内存用满了,会触发淘汰策略,甚至OOM。

第四,慢查询。用slowlog get看慢查询,找出执行时间长的命令,优化它们。

第五,连接数。用info clients看连接数。连接数过多,可能是连接泄漏,或者客户端配置有问题。

第六,持久化。用info persistence看RDB和AOF的状态,持久化是否正常,有没有阻塞。

监控到数据之后,再分析问题出在哪里,针对性优化。

常见的Redis性能问题和优化方法:

第一,大key。大key是Redis性能的杀手,一个key的value太大,会导致读写慢、阻塞、内存碎片、迁移困难。用redis-cli --bigkeys扫描大key,然后拆分。比如把一个大的Hash拆成多个小Hash,把一个大的List拆成多个小List,或者用Hash的field来拆分。

第二,热key。某个key访问量特别大,导致单个节点压力过大。可以用本地缓存(Caffeine、Guava)减轻Redis压力,或者把热key复制多份,分散到不同节点。

第三,慢命令。某些命令执行时间长,比如keys *、flushall、hgetall大Hash、smembers大Set、lrange大List。避免在生产环境用这些命令,用scan代替keys,用hscan、sscan、zscan代替hgetall、smembers、zrange,大的集合要分页查询。

第四,连接池配置不合理。连接池太小,导致等待连接;连接池太大,导致连接数过多。根据QPS和延迟调整连接池大小,一般连接数 = QPS * 平均延迟 / 1000,再留一点余量。

第五,持久化阻塞。RDB fork子进程的时候,如果内存很大,fork时间长,会阻塞Redis。AOF rewrite的时候也会有IO压力。可以优化持久化配置,比如用无盘复制、调整AOF刷盘策略、在低峰期做持久化。

第六,内存碎片。频繁的增删改会导致内存碎片,内存占用比实际数据大很多。用info memory看memfragmentationratio,大于1.5说明碎片严重。可以开启activedefrag自动碎片整理,或者重启Redis整理碎片。

性能优化一定要靠数据,不要靠感觉。先监控,找到瓶颈,再针对性优化,优化之后再看数据验证效果。不要盲目优化,更不要过度优化。

道理六:安全问题不能忽视

Redis的安全问题,很容易被忽视。最开始我用Redis,就是默认配置,直接绑0.0.0.0,没设密码,觉得在内网里,安全没问题。

结果有一次,服务器被黑客扫到了,Redis被入侵,数据被删,还被植入了挖矿程序,服务器CPU跑满,服务全部不可用。花了好几天才恢复,损失很大。

从那以后,我才重视Redis的安全问题。

Redis的安全配置,主要有这几点:

第一,设置密码。在redis.conf里设置requirepass,客户端连接需要密码。密码要复杂,不要用弱密码。

第二,绑定内网IP。不要绑0.0.0.0,只绑内网IP,比如bind 127.0.0.1 192.168.1.100,只允许内网访问。如果需要公网访问,一定要用防火墙限制来源IP,或者用SSH隧道、VPN。

第三,禁用危险命令。在redis.conf里用rename-command禁用或者重命名危险命令,比如flushall、flushdb、keys、config、debug、shutdown、save等。防止误操作或者黑客利用这些命令搞破坏。

第四,不要用root用户运行Redis。用普通用户运行,限制权限,即使被入侵,也不会拿到root权限。

第五,开启保护模式。Redis默认开启protected-mode,在没有设置密码和绑定IP的情况下,只允许本地访问。不要关掉保护模式。

第六,定期更新Redis版本。Redis会定期发布安全补丁,要及时更新,修补已知漏洞。

第七,监控异常访问。监控Redis的连接和操作,发现异常IP或者异常命令,及时告警和处理。

安全问题不能忽视,不要觉得在内网就安全。黑客的扫描工具24小时在扫,只要有漏洞,迟早会被扫到。做好安全配置,防患于未然。

道理七:不要在Redis里做不适合的事

Redis是一个很强大的工具,但它不是万能的,不是什么都适合放在Redis里。

最开始我觉得Redis快,什么都往Redis里放。用Redis做消息队列,用Redis做搜索引擎,用Redis做关系数据库,用Redis做文件存储。结果踩了很多坑,性能和可靠性都不好。

后来才明白,每个工具都有它适合的场景,不要在Redis里做不适合的事。

Redis适合做什么?缓存、分布式锁、计数器、排行榜、限流、会话存储、轻量级消息队列(List/Stream)、地理位置(Geo)、基数统计(HyperLogLog)、位图(Bitmap)。这些场景,Redis做得很好。

Redis不适合做什么?

第一,不适合做关系型数据库。Redis没有关系模型,没有SQL,没有事务(只有简单的事务),没有复杂查询,不适合存储需要复杂关联查询的数据。关系型数据还是要用MySQL、PostgreSQL。

第二,不适合做搜索引擎。Redis的搜索功能很弱,只有简单的模糊匹配,没有全文检索,没有分词,没有相关性排序。全文搜索还是要用Elasticsearch、OpenSearch。

第三,不适合做大数据存储。Redis是内存数据库,数据都存在内存里,成本高。大量冷数据不适合放在Redis里,应该放在磁盘数据库或者对象存储里。

第四,不适合做高可靠的消息队列。Redis的List和Stream可以做消息队列,但可靠性不如专业的消息队列,比如RabbitMQ、Kafka、RocketMQ。如果对消息可靠性要求很高,还是用专业的消息队列。

第五,不适合做复杂的计算。Redis的计算能力有限,不适合做复杂的数据分析和计算。复杂计算还是要用Spark、Flink等大数据处理框架。

用对工具,事半功倍;用错工具,事倍功半。不要因为Redis快,就什么都往Redis里放。要根据场景选择合适的工具,Redis只做它擅长的事。

道理八:监控和告警是必须的,不是可选的

最后一个道理,也是最重要的:监控和告警是必须的,不是可选的。

最开始用Redis,我没有做监控和告警,觉得Redis很稳定,不会出问题。结果有一次,Redis内存满了,触发了淘汰策略,缓存数据被大量淘汰,导致数据库压力骤增,服务变慢,用户投诉了才发现。如果有监控和告警,提前就能发现内存快满了,及时扩容,就不会出这个问题。

从那以后,我给每个Redis实例都做了监控和告警。

Redis的监控,主要监控这几个指标:

第一,存活状态。Redis是否在运行,能不能正常连接。

第二,延迟。响应延迟是否正常,有没有突然升高。

第三,QPS。每秒操作数是否正常,有没有突增或者突降。

第四,内存。内存使用率,有没有接近上限,淘汰策略有没有触发。

第五,连接数。连接数是否正常,有没有连接泄漏。

第六,持久化。RDB和AOF是否正常,有没有持久化失败。

第七,慢查询。有没有慢查询,慢查询的频率和耗时。

第八,主从同步。主从同步是否正常,有没有延迟或者断开。

第九,集群状态。如果是集群,集群节点是否正常,有没有节点掉线,数据分片是否均衡。

告警的阈值要合理,不要太灵敏(频繁告警导致麻木),也不要太迟钝(出问题了才告警)。一般设置两级告警:警告(warning)和严重(critical)。比如内存使用率超过70%发警告,超过85%发严重;延迟超过10ms发警告,超过50ms发严重。

告警渠道要可靠,比如短信、电话、邮件、企业微信/钉钉。严重告警一定要能及时通知到人,不能只发个邮件没人看。

除了监控和告警,还要有应急预案。Redis挂了怎么办?主从切换失败怎么办?数据丢了怎么办?内存满了怎么办?这些都要提前想好预案,出了问题才能快速处理,减少损失。

监控和告警是必须的,不是可选的。不要等出了问题才后悔没做监控。

写在最后

用了三年Redis,踩了很多坑,也总结了很多道理。这些道理,都是用线上故障和加班换来的,希望能帮到大家,少踩坑。

总结一下这八个道理:

  1. 缓存不是银弹,该查数据库还是要查,做好缓存一致性和容错降级。
  2. 选对数据结构,比写对代码更重要,根据场景选择合适的数据结构。
  3. 持久化不是备份,该备份还是要备份,定期做恢复测试。
  4. 集群不是越复杂越好,适合自己的才是最好的,根据业务规模选择部署方式。
  5. 性能优化要靠数据,不要靠感觉,先监控再分析最后优化。
  6. 安全问题不能忽视,做好密码、IP绑定、危险命令禁用等安全配置。
  7. 不要在Redis里做不适合的事,用对工具,事半功倍。
  8. 监控和告警是必须的,不是可选的,做好监控告警和应急预案。

Redis是一个很优秀的工具,但要用好它,需要学习和实践。希望这篇文章能帮你更好地使用Redis。

有什么问题欢迎交流,一起学习进步。