redis 典型缓存架构设计问题及性能优化总结:
缓存穿透
查询一个根本不存在的数据,缓存层和存储层都不会命中。通常出于容错的考虑,如果从存储层查不到数据,则不写入缓存层。
原因:
自身业务代码或数据有问题
恶意攻击等造成大量空命中
解决方案1:缓存空对象
解决方案2:布隆过滤器
当布隆过滤哭喊 说某个值存在时,这个值可能不存在。当说它不存在时,那就肯定不存在。
对于不存在的数据布隆过滤器一般都能过滤掉,不再让请求再往后端发送。
布隆过滤器就是一个大型的位数组和几个不一样的无偏hash 函数,所谓无偏就是能够把元素的hash 值算得比较均匀。
这种方法适用于数据命中不高、数据相对稳定、实时性低的应用场景,通常是数据集较大,代码维护较为复杂,但是缓存空间占用较少。
缓存击穿
大量缓存同时失效导致请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大挂掉。最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。
int expireTime - new Random().nextInt(300) + 300;
缓存血崩
如果缓存架构设计得不好,大量请求访问bigkey,导致缓存能支撑的并发急剧下降,大量请求都会打到存储层,造成存储层也会级联宕机的情况。 解决问题:
1 保证缓存层服务高可用性,比如使用redis Sentinel 或 redis Cluster
2 依赖隔离组件为后端限流熔断并降级。比如使用 Sentinel 或 Hystrix 限流降级组件。
我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据,如商品属性,用户信息等,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据,如商品库存,仍然允许查询缓存,如果缓存缺失,可以继续通过数据库读取。
3 做好数据容灾。提前演练,并做一些预案。
热点数据缓存优化
使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是这种策略存在的问题,对应用却是致命的。
当前key 是一个热点key, 如热门活动,并发量非常大
重建缓存不能在短时间完成,可能是一个复杂计算,例如繁杂的SQL,多次IO, 多个依赖等,在缓存操作新选的瞬间,有大量纯种来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
解决这个问题,就是要避免大量纯种同时重建缓存。
可以利用互斥锁,此方法只允许一个纯种重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据。
String get(String key) {
// 从redis 中获取数据 String value = redis.get(key); // 如果value 为空,则重构缓存 if(null == value){ // 只允许一个线程重建缓存,使用nx, 并设置过期时间ex String mutexKey = "mutext:key:" + key; if(redis.set(mutexKey,"1","ex 180",nx)){ // 从数据库中取数据 value = db.get(key); // 设置过期时间 redis.setex(key,timeout,value); // 删除key_mutex redis.delete(mutexKey); } else { Thread.sleep(50); get(key); } } return value;
}
缓存数据库读写不一致
1 双写不一致
2 读写不一致
解决方案:
1 对于并发几率很小的数据,如个人维度的订单数据,用户数据等,这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
2 就算并发很高,如果业务上能容忍短时间的缓存数据不一致,如商品名称,商品分类菜单等,缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
3 如果不能容忍缓存数据不一致,可以通过读写锁保证并发读写或写写的时间按顺序排好队,读读的时间相当于无锁。
4 也可以用阿里开源的canal 通过监听数据库的binlog 日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。
小结:一般针对是读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存了,可以直接操作数据库。如果数据库抗不住压力,还可以把缓存作为数据读写的主存储,异步将数据同步到数据库,数据库只是作为数据的备份。
加入缓存的数据库应该是对实时性、一致性要求不是很高的数据,切记不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统的复杂性。
开发规范
key 名设计
1 建议:可读性和可管理性
以业务名或数据库名库前缀,防止key 冲突,用冒号分隔。如 业务名:表名:id
2 建议:简洁性
保证语义的前提下,控制key 长度,当key 较多时,内存占用也不容忽视。
3 强制:不要包含特殊字符
反例:空格,换行,单双引号以及其他转义字符
value 设计
1 强制:拒绝bigkey ,防止网卡流量,慢查询
在redis 中,一个字符串最大512M, 一个二级数据结构可以存储大约40亿(2^32 -1)个元素,但是实际中如果有下面两种情况,我们认为它是bigkey。
1 字符串类型:它的big 体现在单个value 值很大,一般认为超过10KB 就是bigkey。
2 非字符串类型:hash, list,set, zset,它们的big 体现在元素个数太多。
一般来说,String 类型控制在10kb 以内, hash, list, set, zset 元素个数不要超过5000。非字符串的bigkey,不要使用del 删除,使用hscan,sscan,zscan 方式浙进式删除,同时要注意防止bigkey 过期时间自动删除问题。(如一个200w 的zset 设置一个小时过期,会触发 del 操作,造成阻塞)
bigkey 性能优化
1 bigkey 的产生
一般来说,bigkey 的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的。
1 社交类:大V 粉丝列表,如果设计不当,必是bigkey
2 统计类:如按天存储某项功能或者网站的用户集合,除非没人用,否则必是bigkey
3 缓存类:将数据从数据库中load 出来序列化放在redis 中,但是要注意1-是不是有必要把所有字段都缓存;2-有没有相关关联的数据,为图方便而产生关联数据, 产生bigkey.
2 如何优化
1 拆
big list : list1, list2, ... , listN
big hash :可以将数据分段存储,比如一个大的key, 假设存了100w 的用户数据,可以拆分成200个key, 每个key 下面5000个用户数据。
2 推荐:选择适合的数据类型
举例:
// 正例:
hmset user:1 name tom age 20 favor swimming
// 反例:
set user 1 : name tom
set user 1 : age 20
set user 1 : favor swimming
3 推荐:控制key 的生命周期
建议使用expire 设置过期时间,同时过期时间要随机,防止集中过期。