当前位置:首页 > 问答 > 正文

Redis集成中那些坑和错误,真是让人头大又难搞清楚

说到Redis,很多人都觉得它简单,就是个内存缓存,用起来能有多难?但真到项目里集成进去,尤其是在高并发或者复杂业务场景下,各种稀奇古怪的问题就冒出来了,很多问题还不是一下子就能定位到的,真是让人头大。

第一大坑:连接数被打满,服务突然瘫痪。 这是最经典也是最吓人的一个问题,Redis默认的最大连接数(在redis.conf里是maxclients)是10000,听起来很多对吧?但如果你在代码里没有正确管理连接,比如用了连接池但配置不当,或者更糟糕——在方法内部频繁创建和关闭连接,那么在高并发下,连接数瞬间就能飙到上限,结果就是,新的请求再也无法连接到Redis,所有依赖缓存的业务功能全部挂掉,整个应用可能都会雪崩,这个问题在(Stack Overflow上关于“Redis max number of clients reached”的讨论)和很多开发者的故障复盘报告中频繁出现,关键是,它平时没事,流量一上来就爆发,让人措手不及。

Redis集成中那些坑和错误,真是让人头大又难搞清楚

第二大坑:错误的键过期策略导致缓存“击穿”和“雪崩”。 这个听起来有点专业,但说白了就是设置过期时间(TTL)的时机和方式不对,缓存击穿”,指的是某一个热点key(比如首页头条新闻)在失效的瞬间,突然有巨大的并发请求过来,而这个key此时在Redis里不存在了,这下好了,所有请求都绕开缓存,直接砸到后端的数据库上,数据库可能瞬间就被压垮了,另一种是“缓存雪崩”,指的是在某个时间点,大量的key同时过期,导致请求同样全部落到数据库,造成巨大压力,很多人设置过期时间时,习惯性地都设成一样的,比如都设成1小时,那么1小时后,这些key就可能同时失效,避免的办法也简单,比如给热点key设置永不过期,或者通过后台任务定时更新;对于雪崩,可以给批量key的过期时间加上一个随机值,让它们错开失效。

第三大坑:胡乱使用KEYS命令,导致Redis服务卡死。 这个命令是用于查找所有符合给定模式的key,比如KEYS user:*,在开发测试环境,数据量小,用着很爽,但一旦上了生产环境,数据量达到百万甚至千万级别,这个命令就是一场灾难,因为KEYS命令是阻塞式的,它会遍历整个数据库的所有key,执行期间Redis无法处理其他任何命令,CPU会瞬间飙升到100%,服务基本就不可用了,线上环境绝对禁止使用KEYS命令,那想遍历key怎么办?应该使用SCAN命令,它是非阻塞的、渐进式的,虽然可能会重复遍历到某些key,但不会影响Redis的正常服务,这个坑是无数运维用血泪教训换来的,在(Redis官方文档对KEYS和SCAN命令的警告)中也被明确强调。

Redis集成中那些坑和错误,真是让人头大又难搞清楚

第四大坑:忽略了持久化机制带来的影响,导致系统间歇性变慢。 Redis为了数据不丢,提供了RDB和AOF两种持久化方式,RDB是打快照,AOF是记录每一条写命令,但这俩如果配置不当,就会成为性能杀手,RDB在执行bgsave后台保存时,会fork一个子进程,如果此时Redis内存占用很大(比如20G),fork操作本身可能会很耗时,导致主进程短暂停顿,应用就会感觉到延迟,AOF如果设置为每次写入都同步(appendfsync always),那确实最安全,但性能也是最差的,每条写命令都要刷盘,如果设置为每秒同步一次(appendfsync everysec),这是默认值,也是推荐值,但在服务器故障时可能会丢失1秒的数据,很多人不了解这些区别,要么为了性能关了持久化导致数据丢失,要么为了安全配置不当导致服务变慢。

第五大坑:数据类型使用不当,白白浪费内存和性能。 Redis不是简单地把Java对象序列化成JSON字符串存进去就完事了,它有丰富的数据结构,比如String, Hash, List, Set, Sorted Set,如果用错了地方,效果天差地别,要存储一个对象的多个字段(比如用户信息:name, age, email),很多人会存成多个String类型的key(user:123:name, user:123:age...),或者存成一个大的JSON字符串,其实更好的方式是用Hash结构,它可以在一个key里管理多个field-value,不仅内存利用更高效(特别是使用了ziplist编码时),而且可以单独操作某个字段,非常方便,再比如,要用一个列表来记录最新的100条动态,用List结构配合LTRIM命令就非常合适,如果用String存然后再解析,就既低效又麻烦,这种设计上的坑,不会立刻导致故障,但会随着数据量增长,慢慢拖慢系统,增加不必要的成本。

第六大坑:网络问题导致的超时和读写错误。 在分布式环境下,应用服务器和Redis服务器之间的网络并非绝对可靠,可能会因为物理故障、带宽打满、防火墙规则变动等原因,出现网络延迟升高甚至瞬断的情况,如果你的客户端没有设置合理的超时时间,或者没有重试机制,那么一次网络抖动就可能导致应用线程被长时间阻塞,进而引发连锁反应,在某些云服务商那里,Redis实例可能会有流量限制,如果短时间内读写流量过大,可能会被限流,表现就是大量的超时错误,这些问题排查起来很麻烦,因为问题不在应用代码也不在Redis本身,而在“虚无缥缈”的网络层面。

第七大坑:盲目相信Redis的“原子性”,写出非原子操作。 Redis的每个命令确实是原子执行的,但当你需要把多个命令组合成一个逻辑操作时,如果你只是简单地一个个命令顺序执行,那这个组合操作就不是原子的,比如经典的“检查再设置”(check-and-set)场景:先GET一个key的值,判断一下,然后SET一个新值,在并发环境下,两个客户端可能同时GET到旧值,然后都去SET,结果就错了,解决这个问题必须使用Redis的事务(MULTI/EXEC)或者更推荐的Lua脚本,Lua脚本在Redis中是原子执行的,能确保脚本里的所有命令被一次性、不被打断地完成,很多人因为不了解这个特性,写出了有并发bug的代码,在低并发时发现不了,一到生产环境就出诡异的数据错乱。

Redis就像一把锋利的瑞士军刀,用好了能极大提升系统性能,但用不好反而容易伤到自己,这些坑都不是什么高深的原理问题,但每一个都需要在实际踩过或者有充分意识的情况下才能有效避免,集成的时候,多想想数据一致性、性能极限、异常处理,才能让它真正成为系统的助力,而不是故障的源头。

Redis集成中那些坑和错误,真是让人头大又难搞清楚