redis常见问题解决方案和使用规范
一、Redis 说明:
- 一款开源高性能的 key-value 数据库,通常被用作缓存
- 工作模式:单进程/线程多路复用(epoll) - redis6.0 支持多线程(只是 I/O 多线程,业务处理还是单线程)
- 单线程高性能的原因:
- 纯内存操作本来就很快;
- redis 使用 epoll 支持 io 多路复用,天生支持高并发请求;
- redis 将耗时的操作分多次处理,保证每次处理的时间都很短,保证了读写性能(redis 不建议保存太长的数据,数据很长的话处理时间就会变长);
二、部署与集群
这里并不就部署进行详细展开,简单就可选的集群模式及其特点说明如下:
- Twemproxy:
- 无法平滑的扩容或者缩容,甚至修改配置都需要重启服务;
- 很难运维,甚至没有 Dashboard;
- Redis Cluster(官方方案):
- 无中心化设计;
- 程序难以编写,代码量多的吓人,最佳实践很少;
- 整个系统高度耦合,升级困难;
- 不支持 database
- codis:
- 部分原生命令不支持
- redis-sentinel 模式:
- 不是严格意义上的集群,只是高可用方案的一种;
- 需要 cli 客户端适配,不是透明连接的;
三、redis 数据淘汰策略
Redis 一般用来缓存热点数据,为了更好的利用内存,Redis 设计了相应的内存淘汰机制(也叫做缓存淘汰机制),分为两类(被动删除和主动删除),说明如下:
- 被动删除:设置了失效的 keys 被访问时,如果已经超时,则被删除并返回空
- 主动删除:100ms 运行一次,随机删除持续 25ms(类似 Cron),或者内存使用超过 maxmemory,触发主动清理策略(配置项名称:maxmemory-policy),可选策略列表:
- volatile-lru:只从设置失效(expire set)的 keys 中选择最近最不经常使用的 key 进行删除,用以保存新数据
- volatile-ttl:只从设置失效(expire set)的 keys 中,选出存活时间(TTL)最短的 key 进行删除,用以保存新数据
- volatile-random:只从设置失效(expire set)的 keys 中,选择一些 key 进行删除,用以保存新数据
- allkeys-lru:优先删除掉最近最不经常使用的 key,用以保存新数据
- allkeys-random:随机从 all-keys 中选择一些 key 进行删除,用以保存新数据
- noeviction:不进行置换,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回 error
- 如果不设置 maxmemory,Redis 将一直使用内存,直到触发操作系统的 OOM-KILLER。
四、redis 常见问题与解决方案
- 缓存一致性:缓存和数据库数据保持一致):
- 解决方式:旁路缓存(Cache Aside Pattern)
- 读请求:先读 cache,再读 db
- 变更操作:先操作数据库,再 淘汰 缓存
- 解决方式:旁路缓存(Cache Aside Pattern)
- 缓存击穿(影响轻微):高流量下 大量请求读取一个失效的 Key -> Redis Miss -> 穿透到 DB
- 解决方式:采用分布式锁,只有拿到锁的第一个线程去请求数据库,然后插入缓存
- 缓存穿透(影响一般):访问一个不存在的 Key(恶意攻击)-> Redis Miss -> 穿透到 DB
- 解决方式:
- 给相应的 Key 设置一个 Null 值,放在缓存中
- BloomFilter 预先判断
- 解决方式:
- 缓存雪崩(影响严重):大量 Key 同时失效或者 Redis 宕机 -> Redis Miss -> 压力打到 DB
- 解决方式:
- 给失效时间加上相对的随机数
- 保证 Redis 的高可用
- 解决方式:
- 缓存污染(影响一般):有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,继续留存在缓存中白白占用缓存空间。
- 解决方式:
- 在明确知道数据被再次访问的情况下,volatile-ttl 可以有效避免缓存污染
- 解决方式:
- hot key(影响严重):突然有大量请求去访问 redis 上的某个特定 key,过于集中的流量达到物理网卡上限,从而导致这台 redis 的服务器宕机;
- 提前发现 hot key:
- 凭借业务经验,进行预估哪些是热 key
- 在操作 redis 之前,加入一行代码进行数据统计
- 用 redis 自带命令:monitor 或者 redis-cli + hotkeys 参数(redis 4.0.3 以上)
- 解决方式:
- 利用二级缓存:比如利用 ehcache,或者一个 HashMap 都可以。在你发现热 key 以后,把热 key 加载到系统的 JVM 中。;针对这种热 key 请求,会直接从 jvm 中取,而不会走到 redis 层。
- 备份热 key:将 HotKey 与一个随机数组合生成一个新 key,分散到多个 redis 实例中;
- 提前发现 hot key:
- redis 大 key(影响严重)可能导致如下问题 - 解决方式:禁止使用大 key
- 集群中各节点内存空间不均衡
- 超时阻塞:由于 Redis 单线程的特性,操作 bigkey 比较耗时,也就意味着阻塞 Redis 的可能性增大;
- 网络阻塞:每次获取 bigkey 产生的网络流量较大,假设一个 bigkey 为 1MB,每次访问量为 1000,那么每秒产生 1000MB 的流量,对于普通的千兆网卡(按照字节算是 128MB/s)的服务器简直是灭顶之灾;
- Redis 阻塞(影响严重)- 解决方式:提前做好规划,及时定位处理;
- 可能导致 Redis 阻塞的原因(根据实际情况单独处理):
- 慢查询
- bigkey 大对象
- swap
- fork 子进程
- AOF 刷盘阻塞
- Redis 输入、输出缓冲区导致的阻塞
- 网络问题
- 可能导致 Redis 阻塞的原因(根据实际情况单独处理):
五、redis 使用规范
- 冷热数据区分:只存储热数据 (如 QPS 超过 5k) 的数据加载到 Redis,低频数据放到 Mysql 或者 ES 中
- 业务数据分离:根据数据业务关联性分多个 Redis 实例使用,可避免业务相互影响、避免单实例膨胀,并降低故障影响面;
- 使用连接池且避免频繁创建销毁:连接池可以降低操作连接的资源开销,请确保使用了正确的 Redis 客户端连接池配置,避免在使用了连接池的情况下仍然存在连接频繁创建和销毁的情况
- 消息大小限制(避免大 key 和 hot key):Redis 是单线程服务,消息过大会阻塞并拖慢其他操作、也可能导致持久化到磁盘时的 I/O 问题,保持消息体在 1kb 以下(最大不过 10kb,可以使用 snappy、msgpack 等压缩)
- 缓存 Key 设置失效时间:作为缓存使用的 Key,必须要设置失效时间(注意失效时间的单位:有的是秒,有的是毫秒)
- 缓存不能有中间态:缓存应该仅作缓存用,去掉后业务逻辑不应发生改变,万不可切入到业务里,徒增增加业务风险和维护成本;
- 严禁使用 Keys:属于 O(N)操作,会阻塞其他正常命令,且效率极低;建议 rename 此命令,从根源禁用;
- 严禁使用 Flush:flush 命令会清空所有数据,属于高危操作,应该 rename 此命令,从根源禁用;
- 严禁作为消息队列使用:Redis 当作消息队列使用,会有容量、网络、效率、功能方面的多种问题;
- 严禁不设置范围的批量操作:避免执行批量操作函数,如不设范围的 zset 操作、对大数据量 Key 使用 HGETALL、Redis Cluster 集群的 mget 操作、sunion, sinter, sdiff 等一些聚合操作;
- 禁止事务操作:redis 本身已经很快了,如无大的必要,建议捕获异常进行回滚,不要使用事务函数
- 禁止 lua 脚本扩展:和 SQL 的存储过程类似,redis 中使用 lua 会引入性能和一些难以维护的问题;
- 禁止长时间 monitor:monitor 函数可以快速看到当前 redis 正在执行的数据流,但是长时间阻塞在 monitor 命令上,会严重影响 redis 的性能(尽量避免高峰时间 monitor)
- 禁止分 database 使用(禁用 select 函数):容易误操作且 cluster 集群模式也不支持多个 database;
- 建议不使用高级数据结构:使用基本的 5 种:string(字符串)、list(列表)、hash(字典)、set(集合) 和 zset(有序集合)
- 建议扩展方式首选客户端 hash 而不是扩容集群规格(比如上 M/S 或者 Cluster):集群越大,在状态同步和持久化方面的性能越差,优先使用客户端 hash 进行集群拆分,将数据落到不同的 redis 实例中;
参考资料:
- https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&mid=2650519808&idx=1&sn=78d404047311bdc83a3a9f650e045f06
- https://mp.weixin.qq.com/s/5zi0ib0SZ3Qv3LTzF9dfKg