Redis笔记_3
传统缓存的问题
传统缓存策略是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库:
- 请求经过
Tomcat处理,性能成为整个系统的瓶颈 Redis缓存失效时,对数据库产生冲击
多级缓存就是充分利用请求处理每个缓环节,分别添加缓存,减轻Tomcat压力,提升服务性能。
缓存分类
- 分布式缓存比如
Redis:- 优点: 存储容量更大,可靠性更好,可以在集群间共享
- 缺点: 访问缓存有网络开销
- 场景: 缓存数据量较大,可靠性要求较高,需要在集群间共享
- 进程本地缓存,比如
HashMap、GuavaCache:- 优点: 读取本地内存,没有网络开销,速度更快
- 缺点: 存储容量有限,可靠性较低,无法共享
- 场景: 性能要求较高,缓存数据量较小
Caffeine
1 | Cache<String, String> cache = Caffeine.newBuilder().build(); |
缓存驱逐策略(三种)
- 基于容量: 设置缓存数量上限
Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1).build();设置缓存大小上限为1 - 基于时间: 设置缓存的有效时间
Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(10)).build();设置缓存有效期为10秒,从最后一次写入开始计时 - 基于引用: 设置缓存为软引用或弱引用,利用
GC来回收缓存数据,性能较差,不建议使用- 默认情况下,当一个缓存元素过期时,
Caffeine不会自动立即清理和驱逐,而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
- 默认情况下,当一个缓存元素过期时,
冷启动与缓存预热
- 冷启动: 服务刚刚启动时,
Redis没有缓存,如果数据都在第一次查询时添加缓存,可能会给数据库带来较大的压力 - 缓存预热: 实际开发中,可以利用大数据统计用户访问的热点数据,项目启动时将这些热点数据提前查询并保存到
Redis中。如果数据量较少,就直接在启动时将所有的数据都放入Redis
缓存同步策略
| 同步策略 | 设置有效期 | 同步双写 | 异步通知 |
|---|---|---|---|
| 含义 | 给缓存设置有效期,到期自动删除,再次查询时更新 | 修改数据库的同时,直接修改缓存(使用同一个事务实现) | 修改数据库时发送事件通知,相关服务监听通知后修改缓存数据 (常用) (MQ, Canal) |
| 优点 | 简单,方便 | 时效性强,缓存和数据库强一致性 | 低耦合,可以同时通知多个缓存服务 |
| 缺点 | 时效性差,缓存过期之前可能数据不一致 | 有代码侵入风险,耦合度高 | 时效性一般,可能存在中间不一致状态 |
| 使用场景 | 更新频率较低,适合时效性要求低的业务 | 对一致性、时效性要求较高的缓存数据 | 时效性要求一般,有多个服务需要同步 |
基于MQ实现异步通知
基于Canal实现异步通知
- 基于
Canal,比MQ更好一些,时效性更强,监听效率更高,基于主从同步实现 Canal把自己伪装成MySQL的一个slave,监听master的binlog变化,再把得到的变化信息通知给Canal客户端,完成对其他数据库的同步
Redis设计
key设计
- 遵循基本格式:
[业务名称]:[数据名]:[id] - 长度不要超过44Bytes
- 不包含特殊字符
- 比如登录业务,保存用户信息,设计
key就是:login:user:10 - 长度越小占用的空间越少,所以
key满足可读性,应该尽可能短
好处:
- 可读性强
- 避免
key冲突 - 方便管理
- 更节省内存,
key是string,底层编码包含int、embstr、raw,其中embstr是一块连续空间,占用空间更少,更加紧凑。但是embstr需要在key小于44B使用
value设计
BigKey,通常以Key的大小和Key中成员的数量来总和判定,比如:Key本身数据量过大,一个String类型的Key,它的值为5MBKey中的成员数目过多,一个ZSET类型的Key,它的成员数量为10000个Key中的成员数据量过大,一个Hash类型的Key,成员数量虽然只有1000个,但是这些Value的总大小为100MB
-
BigKey危害:- 网络阻塞: 对
BigKey读请求,少量的QPS就会导致带宽使用率被占满,导致Redis实例,乃至物理机变慢 - 数据倾斜:
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡 Redis阻塞: 对元素较多的hash, list, zset等做运算会耗时较久,使主线程阻塞CPU压力: 对BigKey的数据序列化和反序列化都会导致CPU的使用率飙升,影响Redis实例和本机其他应用
- 网络阻塞: 对
-
如何发现
BigKey:redis-cli --bigkeys: 利用redis-cli提供的--bigkeys,分析所有的key,返回Key整体统计信息与每个数据最大的Big Keyscan扫描: 自己编程利用scan扫描Redis中所有的Key,利用strlen, hlen等命令判断长度,不建议使用memory usage,因为这个命令对CPU占用较高- 第三方工具,比如
Redis-Rdb-Tools分析RDB快照,全面分析内存使用 - 网络监控: 自定义工具,监控进入
Redis网络数据,超出预警值主动告警
- 推荐做法
- 单个
key的value小于10KB - 对于集合类型的
Key,建议元素数量小于1000 - 使用
unlink异步删除BIGKEY
- 单个
恰当的数据类型
存储一个User对象,有三种方法:
-
Json字符串user:1 {"name": "Jack", "age": 21} - 优点: 实现简单粗暴
- 缺点: 数据耦合不够灵活
-
字段打散
user:1:name Jack user:1:age 21 - 优点: 可以灵活访问对象任意字段
- 缺点: 占用空间大,没有办法做统一控制
-
hash
user:1 name age jack 21 - 优点: 底层使用
ziplist,占用空间小,可以灵活访问对象的任意字段 - 缺点: 代码相对复杂
- 优点: 底层使用
- 假如有
hash类型的key,其中有100万对field, value,其中field是自增id,这个key有什么问题,如何优化?- 存在的问题:
hash的entry数量超过500时,会使用哈希表,而不是ZipList,内存占用较多
- 优化:
- 可以通过
hash-max-ziplist-entries配置entry上限,但是如果entry过多就会导致BigKey问题 - 拆分为
string类型: 但是string底层没有太多内存优化,内存占用较多;想要批量获取这些数据比较麻烦 - 拆分为小的
hash,将id/100作为key,将id % 100作为field,这样每一百个元素就是一个hash
- 可以通过
- 存在的问题:
- 推荐做法
- 合理拆分数据,拒绝
BigKey - 选择合适的数据结构
Hash结构的entry数量不要超过1000- 设置合理的超时时间
- 合理拆分数据,拒绝
批处理
-
Redis的处理速度非常快,消耗的时间绝大部分都是在网络传输中消耗。 -
所以可以一次性传输多条数据,但是不能传输太多命令,否则这样单次命令占用的带宽过多,会导致网络阻塞
-
mset只能处理string类型的,hmset,sadd都只能处理相同的key,所以有缺陷,因此需要实现pipeline -
pipeline可以处理复杂类型,但是pipeline的多个命令之间不具备原子性 -
如果
mset或者pipeline这样的批处理需要在一次请求中携带多条命令,并且此时Redis是一个集群,那么批处理的多个key必须在同一个slot中,否则执行失败。
| 串行命令 | 串行slot | 并行slot | hash_tag | |
|---|---|---|---|---|
| 实现思路 | for循环遍历,依次执行每个命令 | 在客户端计算每个key的slot,将slot一致分为一组,每组都利用pipeline批处理,串行执行各组命令 | 在客户算计算每个key的slot,每组都利用pipeline批处理,并行执行各组命令 | 将所有的key都设置相同的hash_tag,则所有key的slot都一定相同 |
| 耗时 | N次网络耗时+N次命令耗时 | m次网络耗时+N次命令耗时,m = key的slot个数 | 1次网络耗时+N次命令耗时 | 1次网络耗时+N次命令耗时 |
| 优点 | 实现简单 | 耗时较短 | 耗时非常短 | 耗时非常短、实现简单 |
| 缺点 | 耗时非常久 | 实现稍复杂,slot越多,耗时越久 | 实现复杂(用的较多) | 容易出现数据倾斜(所以不怎么用,虽然性能好) |
持久化配置
- 保证数据安全,但是会带来额外的开销
- 推荐做法
-
用来做缓存的
Redis实例尽量不要开启持久化功能,放在一个单独实例里面,就不要开启持久化了 -
建议关闭
RDB持久化功能,使用AOF持久化,因为AOF每秒刷盘 -
利用脚本定期在
slave节点做RDB,实现数据备份。(也不建议频繁做) -
设置合理的
rewrite阈值,避免频繁的bgrewrite -
配置
no-appendfsync-on-rewrite=yes,禁止在rewrite期间做AOF,避免因为AOF引起的阻塞。但是这部分期间,没有做AOF,所以有可能有数据的丢失,这需要看自己是关注可用性还是持久性 -
Redis实例的物理机需要留足够的内存,应对fork和rewrite -
单个
Redis实例内存上限不要太大,比如4G或8G,可以加快fork的速度,减少主从同步,数据迁移压力 -
不要与CPU密集型应用部署在一起,比如
ES -
不要与高硬盘负载应用部署在一起,比如数据库,消息队列
-
慢查询
- 只要执行时间超时了,不管是是不是查询语句,都是慢查询
slowlog-log-slower-than: 慢查询阈值,单位是微秒,默认是10000,建议配置成1000- 慢查询会放入慢查询日志中,日志长度有上限,可以通过配置指定
slowlog-max-len,本质是一个队列的长度,默认是129,建议配置成1000,并且需要及时处理 slowlog len: 查询慢查询日志长度slowlog get[n]: 读取n条慢查询日志slowlog reset: 清空慢查询列表
安全性问题
Redis一定要设置密码- 禁止线上使用
keys , flushall, flushdb, config set命令,可以利用rename-command禁用 bind: 限制网卡,禁止外网网卡访问- 开启防火墙
- 不要使用
root账户启动Redis - 尽量不是默认的端口
内存配置
-
内存不足会导致
Key频繁被删除,响应时间变长,QPS不稳定,当内存使用率达到90%以上就需要警惕了,并快速定位到内存占用的原因内存占用 说明 数据内存 是Redis最主要的部分,存储Redis键值信息,主要是BigKey问题,内存碎片问题 进程内存 Redis本身运行肯定占用内存,比如代码,常量池,大约几兆,大多数生产环境中与Redis数据占用的内存相比可以忽略 缓冲区内存 一般包括客户端缓冲区,AOF缓冲区,复制缓冲区等,客户端缓冲区又包括输入缓冲区和输出缓冲区。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出 -
复制缓冲区: 主从复制的
repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能,通过repl-backlog-size设置,默认1M -
AOF缓冲区: AOF刷盘之前的缓冲区,执行
rewrite的缓冲区,无法设置容量上限 -
客户端缓冲区: 分为输入缓冲区和输出缓冲区,输入缓冲区最大1G不能设置,输出缓冲区可以设置
集群
完整性配置
Redis配置中,发现任意一个插槽不可用,则整个集群都停止对外服务。- 为了保证高可用性,建议将
cluster-require-full-coverage配置为no,默认是yes
带宽问题
-
集群节点之间会不断的互相
ping来确定集群中其他节点的状态,每次ping携带的信息至少包括- 插槽信息
- 集群状态信息
-
集群中节点越多,集群状态信息数据量也就越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高
-
解决办法:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则需要建立多个集群
- 避免在单个物理机中运行太多
Redis实例 - 配置合适的
cluster-node-timeout
集群问题
- 完整性问题
- 带宽问题
- 数据倾斜问题
- 客户端性能问题
- 命令的集群兼容性问题
- lua和事务问题(集群模式下没有办法使用lua和事务)
- 满足需求的前提下,能不使用集群就不用集群。因为单体主从
Redis已经能达到万级QPS,已经能满足大部分需求了


