缓存性能优化
以下章节我们结合一些具体建议和案例来说明如何针对缓存的使用进行性能优化。
Redis使用规范
如下的规范可以帮助我们在系统运行过程中,尽可能减少遇到redis不稳定或异常的概率, 保证系统的长稳运行。
业务使用规范
原则 |
原则说明 |
级别 |
备注 |
---|---|---|---|
就近部署业务,避免时延过大 |
如果部署位置过远(非同一个region)或者时延较大(例如业务服务器与Redis实例通过公网连接),网络延迟将极大影响读写性能。 |
强制 |
如果对于时延较为敏感,请避免创建跨AZ Redis实例。 |
冷热数据区分 |
建议将热数据加载到 Redis 中。低频数据可存储在 Mysql或者ElasticSearch中。 |
建议 |
Redis将低频数据存入内存中,并不会加速访问,且占用Redis空间。 |
业务数据分离 |
避免多个业务共用一个Redis。 |
强制 |
一方面避免业务相互影响,另一方面避免单实例膨胀,并能在故障时降低影响面,快速恢复。 |
禁止使用select功能在单Redis实例做多db区分。 |
强制 |
Redis单实例内多DB隔离性较差,Redis开源社区已经不再发展多DB特性,后续不建议依赖该特性。 |
|
设置合理的内存淘汰(逐出)策略 |
合理设置淘汰策略,可以在Redis内存意外写满的时候,仍然正常提供服务。 |
强制 |
DCS默认的逐出策略为volatile-lru,请根据业务需求选择。Redis支持的数据逐出策略 |
以缓存方式使用Redis |
Redis事务功能较弱,不建议过多使用。 |
建议 |
事务执行完后,不可回滚。 |
数据异常的情况下,支持清空缓存进行数据恢复。 |
强制 |
Redis本身没有保障数据强一致的机制和协议,业务不能强依赖Redis数据的准确性。 |
|
以缓存方式使用Redis时,所有的key需设置过期时间,不可把Redis作为数据库使用。 |
强制 |
失效时间并非越长越好,需要根据业务性质进行设置。 |
|
防止缓存击穿 |
推荐搭配本地缓存使用Redis,对于热点数据建立本地缓存。本地缓存数据使用异步方式进行刷新。 |
建议 |
- |
防止缓存穿透 |
非关键路径透传数据库,建议对访问数据库进行限流。 |
建议 |
- |
从Redis获取数据未命中时,访问只读数据库实例。可通过域名等方式对接多个只读实例。 |
建议 |
核心是未命中的缓存数据不会打到主库上。 用域名对接多个只读数据库实例,一旦出现问题,可以增加只读实例应急。 |
|
不用作消息队列 |
发布订阅场景下,不建议作为消息队列使用。 |
强制 |
|
合理选择规格 |
如果业务增长会带来Redis请求增长,请选择集群实例(Proxy集群和Cluster集群) |
强制 |
单机和主备扩容只能实现内存、带宽的扩容,无法实现计算性能扩容。 |
生产实例需要选择主备或者集群实例,不能选用单机实例 |
强制 |
- |
|
主备实例,不建议使用过大的规格。 |
建议 |
Redis在执行RewriteAOF和BGSAVE的时候,会fork一个进程,过大的内存会导致卡顿 |
|
具备降级或容灾措施 |
缓存访问失败时,具备降级措施,从DB获取数据;或者具备容灾措施,自动切换到另一个Redis使用。 |
建议 |
- |
数据设计规范
分类 |
原则 |
原则说明 |
级别 |
备注 |
---|---|---|---|---|
Key相关规范 |
使用统一的命名规范。 |
一般使用业务名(或数据库名)为前缀,用冒号分隔。Key的名称保证语义清晰。 |
建议 |
例如,业务名:子业务名:id |
控制Key名称的长度。 |
在保证语义清晰的情况下,尽量减少Key的长度。有些常用单词可使用缩写,例如,user缩写为u,messages缩写为msg。 |
建议 |
建议不要超过128字节(越短越好)。 |
|
禁止包含特殊字符(大括号“{}”除外)。 |
禁止包含特殊字符,如空格、换行、单双引号以及其他转义字符。 |
建议 |
由于大括号“{}”为Redis的hash tag语义,如果使用的是集群实例,Key名称需要正确地使用大括号避免分片不均的情况。 |
|
Value相关规范 |
设计合理的Value大小。 |
设计合理的Key中Value的大小,推荐小于10 KB。 |
建议 |
过大的Value会引发分片不均、热点Key、实例流量或CPU使用率冲高等问题,还可能导致变更规格和迁移失败。应从设计源头上避免此类问题带来的影响。 |
设计合理的Key中元素的数量。 |
对于集合和列表类的数据结构(例如Hash,Set,List等),避免其中包含过多元素,建议单Key中的元素不要超过5000个。 |
建议 |
由于某些命令(例如HGETALL)的时间复杂度直接与Key中的元素数量相关。如果频繁执行时间复杂度为O(N)及以上的命令,且Key中的子Key数量过多容易引发慢请求、分片流量不均或热点Key问题。 |
|
选择合适的数据类型。 |
合理地选择数据结构能够节省内存和带宽。 |
建议 |
例如存储用户的信息,可用使用多个key,使用set u:1:name "X"、set u:1:age 20存储,也可以使用hash数据结构,存储成1个key,设置用户属性时使用hmset一次设置多个,同时这样存储也能节省内存。 |
|
设置合理的过期时间。 |
合理设置Key的过期时间,将过期时间打散,避免大量Key在同一时间点过期。 |
建议 |
设置过期时间时,可以在基础值上增减一个随机偏移值,避免在同一个时间点大量Key过期。大量Key过期会导致CPU使用率冲高。 |
命令使用规范
原则 |
原则说明 |
级别 |
备注 |
谨慎使用O(N)复杂度的命令 |
时间复杂度为O(N)的命令,需要特别注意N的值。避免N过大,造成Redis阻塞以及CPU使用率冲高。 |
强制 |
例如:hgetall、lrange、smembers、zrange、sinter这些命令都是做全集操作,如果元素很多,会消耗大量CPU资源。可使用hscan、sscan、zscan这些分批扫描的命令替代。 |
禁用高危命令 |
禁止使用flushall、keys、hgetall等命令,或对命令进行重命名限制使用。 |
强制 |
请参考命令重命名的内容。 |
慎重使用select |
Redis多数据库支持较弱,多业务用多数据库实际还是单线程处理,会有干扰。最好是拆分使用多个Redis。 |
建议 |
- |
使用批量操作提高效率 |
如果有批量操作,可使用mget、mset或pipeline,提高效率,但要注意控制一次批量操作的元素个数。 |
建议 |
mget、mset和pipeline的区别如下:
|
避免在lua脚本中使用耗时代码 |
lua脚本的执行超时时间为5秒钟,建议不要在lua脚本中使用比较耗时的代码。 |
强制 |
比如长时间的sleep、大的循环等语句。 |
避免在lua脚本中使用随机函数 |
调用lua脚本时,建议不要使用随机函数去指定key,否则在主备节点上执行结果不一致,从而导致主备节点数据不一致。 |
强制 |
- |
遵循集群实例使用lua的限制 |
遵循集群实例使用lua的限制。 |
强制 |
|
对mget,hmget等批量命令做并行和异步IO优化 |
某些客户端对于MGET,HMGET这些命令没有做特殊处理,串行执行再合并返回,效率较低,建议做并行优化。 |
建议 |
例如Jedis对于MGET命令在集群中执行的场景就没有特殊优化,串行执行,比起lettuce中并行pipeline,异步IO的实现,性能差距可达到数十倍,该场景建议使用Jedis的客户端自行实现slot分组和pipeline的功能。 |
禁止使用del命令直接删除大Key |
使用del命令直接删除大Key(主要是集合类型)会导致节点阻塞,影响后续请求。 |
强制 |
Redis 4.0后的版本可以通过UNLINK命令安全地删除大Key,该命令是异步非阻塞的。 对于Redis 4.0之前的版本:
|