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 Key
scan
扫描: 自己编程利用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,已经能满足大部分需求了