1. 缓存设计
在使用Redis场景中,最常见的问题就是缓存雪崩、缓存穿透和缓存击穿,后果都是由于各种情况导致大量请求直接访问DB,使得DB压力巨增,从而DB服务瘫痪,导致整体服务有问题。(参见新浪微博明星、大V的推送)
1.1 缓存雪崩
缓存雪崩指的是大批量缓存在同一时间失效或者是缓存层支撑不住宕机,导致流量直接涌入数据库中,会造成数据库压力过大甚至挂掉。
1.1.1 解决方案
- 把每个key的失效时间都加个随机值,保证数据不在同一时间大面积失效。
- 保证缓存层服务高可用
- 依靠隔离组件为后端限流熔断并降级
- 提前演练缓存层宕机后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定
1.2 缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
1.2.1 解决方案
- 缓存空对象
- 接口增加校验,避免不合法的参数传递进来
- 采用布隆过滤器
1.3 缓存击穿
缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
1.3.1 解决方案
- 热点数据永不过期
- 重建热点key时,加互斥锁
2. 性能优化
2.1 缓存与数据库双写不一致
解决方案如下:
- 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
- 就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期 时间依然可以解决大部分业务对于缓存的要求。
- 如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相 当于无锁。
- 也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加 了系统的复杂度。
2.2 键值设计
2.2.1 key名设计
- 【建议】: 可读性和可管理性,以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
trade:order:1
- 【建议】:简洁性,保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:
user:{uid}:friends:messages:{mid} 简化为 u:{uid}:fr:m:{mid}
- 【强制】:不要包含特殊字符
2.2.2 value设计
- 【推荐】:选择适合的数据类型,例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能之间的平衡)
- 控制key的生命周期,redis不是垃圾桶。建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)。
- 【强制】:拒绝bigkey(防止网卡流量、慢查询)
2.2.2.1 bigkey
在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存 储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是bigkey。
- 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。
- 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。 一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
bigkey的危害:
- 导致Redis阻塞
- 网络阻塞
- 过期删除有可能阻塞Redis
bigkey的产生:
一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:
(1) 社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。
(2) 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。
(3) 缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需 要注意,第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了 图方便把相关数据都存一个key下,产生bigkey。
如何优化bigkey:
- 拆,big list: list1、list2、...listN
big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成 200个key,每个key下面存放5000个用户数据 - 如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要 hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。
2.2.3 命令使用
- 【推荐】 O(N)命令关注N的数量,例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有 遍历的需求可以使用hscan、sscan、zscan代替。
- 【推荐】:禁用命令,禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的 方式渐进式处理。
- 【推荐】合理使用select,redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还 是单线程处理,会有干扰。
- 【推荐】使用批量操作提高效率,但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
- 【建议】Redis事务功能较弱,不建议过多使用,可以用lua替代
2.2.4 客户端使用
- 【推荐】避免多个应用使用一个Redis实例,正例:不相干的业务拆分,公共数据做服务化。
- 【推荐】使用带有连接池的数据库,可以有效控制连接,同时提高效率。
- 【建议】高并发下建议客户端添加熔断功能(例如sentinel、hystrix)
- 【推荐】设置合理的密码,如有必要可以使用SSL加密访问
JedisPool连接池的优化建议:
- maxTotal:最大连接数,早期的版本叫maxActive 实际上这个是一个很难回答的问题,考虑的因素比较多:
- 业务希望Redis并发量
- 客户端执行命令时间
- Redis资源:例如 nodes(例如应用个数) * maxTotal 是不能超过redis的最大连接数 maxclients。
- 资源开销:例如虽然希望控制空闲连接(连接池此刻可马上使用的连接),但是不希望因 为连接池的频繁释放创建连接造成不必靠开销。
以一个例子说明,假设:
- 一次命令时间(borrow|return resource + Jedis执行命令(含网络) )的平均耗时约为 1ms,一个连接的QPS大约是1000
- 业务期望的QPS是50000 那么理论上需要的资源池大小是50000 / 1000 = 50个。但事实上这是个理论值,还要考虑到要 比理论值预留一些资源,通常来讲maxTotal可以比理论值大一些。
- maxIdle和minIdle
maxIdle实际上才是业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置 过小,否则会有new Jedis(新连接)开销。
连接池的最佳性能是maxTotal = maxIdle,这样就避免连接池伸缩带来的性能干扰。但是如果 并发量不大或者maxTotal设置过高,会导致不必要的连接资源浪费。一般推荐maxIdle可以设置 为按上面的业务期望QPS计算出来的理论连接数,maxTotal可以再放大一倍。 minIdle(最小空闲连接数),与其说是最小空闲连接数,不如说是"至少需要保持的空闲连接 数",在使用连接的过程中,如果连接数超过了minIdle,那么继续建立连接,如果超过了maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉。 如果系统启动完马上就会有很多的请求过来,那么可以给redis连接池做预热,比如快速的创建一 些redis连接,执行简单命令,类似ping(),快速的将连接池里的空闲连接提升到minIdle的数 量。