缓存的使用与设计
1.受益
加速读写
CPU L1/L2/L3 Cache、浏览器缓存、Ehcache缓存数据库结果
降低后端负载
后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL的负载
2.成本
数据不一致:缓存层和数据层有时间窗口不一致问题,和更新策略有关
代码维护成本:多了一层缓存逻辑
运维成本:例如Redis Cluster
3.使用场景
降低后端负载
对高消耗的SQL:join结果集/分组统计结果缓存
加速请求响应
利用Redis/Memcache优化IO响应时间
大量写合并为批量写
入计数器先Redis累加再批量写DB
缓存的更新策略
LRU等算法剔除:例如 maxmemory-policy
2.超时剔除:例如expire
3.主动更新:开发控制生命周期
4.两条建议
低一致性数据:最大内存和淘汰策略
高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底
缓存粒度问题
通用性:全量属性更好
占用空间:部分属性更好
代码维护:表面上全量属性更好
缓存穿透优化
查询一个不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询
产生原因
业务代码自身问题
恶意攻击、爬虫
发现问题
业务的响应时间,受到恶意攻击时,普遍请求被打到存储层,必会引起响应时间提高,可通过监控发现
业务本身问题
相关指标:总调用数、缓存层命中数、存储层命中数
解决方法1:缓存空对象(设置过期时间)
当存储层查询不到数据后,往cache层中存储一个null,后期再被查询时,可以通过cache返回null。
缺点
cache层需要存储更多的key
缓存层和数据层数据“短期”不一致
1 public String getPassThrough(String key) { 2 String cacheValue = cache.get(key); 3 if( StringUtils.isEmpty(cacheValue) ) { 4 String storageValue = storage.get(key); 5 cache.set(key , storageValue); 6 if( StringUtils.isEmpty(storageValue) ) { 7 cache.expire(key , 60 * 5 ); 8 } 9 return storageValue; 10 } else { 11 return cacheValue; 12 } 13 }
解决方法2:布隆过滤器拦截(适合固定的数据)
将所有可能存在的数据哈希到一个足够大的bitmap中,一个不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
缓存雪崩优化
由于cache服务器承载大量的请求,当cache服务异常脱机,流量直接压向后端组件,造成级联故障。或者缓存集中在一段时间内失效,发生大量的缓存穿透
解决方法1:保证缓存高可用性
做到缓存多节点、多机器、甚至多机房。
Redis Cluster、Redis Sentinel
做二级缓存
解决方法2:依赖隔离组件为后端限流
使用Hystrix做服务降级
解决方法3:提前演练(压力测试)
解决方法4:对不同的key随机设置过期时间
无底洞问题
添加机器时,客户端的性能不但没提升,反而下降
关键点
更多的机器 != 更高的性能
更多的机器 = 数据增长与水平扩展
批量接口需求:一次mget随着机器增多,网络节点访问次数更多。网络节点的时间复杂度由O(1) -> O(node)
优化IO的方法
命令本身优化:减少慢查询命令:keys、hgetall、查询bigKey并进行优化
减少网络通信次数
mget由O(keys),升级为O(node),O(max_slow(node)) , 甚至是O(1)
降低接入成本:例如客户端长连接/连接池、NIO等
热点Key的重建优化
热点Key(访问量比较大) + 较长的重建时间(重建过程中的API或者接口比较费时间)
导致的问题:有大量的线程会去查询数据源并重建缓存,对存储层造成了巨大的压力,响应时间会变得很慢
1.三个目标
减少重建缓存的次数
数据尽可能一致
减少潜在危险:例如死锁、线程池大量被hang住(悬挂)
2.两种解决方案
互斥锁(分布式锁)
第一个线程需要重建时候,对这个Key的重建加入分布式锁,重建完成后进行解锁
这个方法避免了大量的缓存重建与存储层的压力,但是还是会有大量线程的阻塞
jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime) String get(String key) { String SET_IF_NOT_EXIST = "NX"; String SET_WITH_EXPIRE_TIME = "PX"; String value = jedis.get(key); if( null == value ) { String lockKey = "lockKey:" + key; if( "OK".equals(jedis.set(lockKey , "1" , SET_IF_NOT_EXIST , SET_WITH_EXPIRE_TIME , 180)) ) { value = db.get(key); jedis.set(key , value); jedis.delete(lockKey); } else { Thread.sleep(50); get(key); } } return value; }
永远不过期
缓存层面:不设置过期时间(不使用expire)
功能层面:为每个value添加逻辑过期时间,单发现超过逻辑过期时间后,会使用单独的线程去重建缓存。
还存在一个数据不一致的情况。可以将逻辑过期时间相对实际过期时间相对减小
Redis规模化的困扰
发布构建繁琐,私搭乱盖
节点&机器等运维成本
监控报警比较低级
CacheCloud的主要功能
一键开启Redis(单点、Sentinel、Cluster)
机器、应用、实例监控和报警
客户端:透明使用、性能上报
可视化运维:配置、扩容、Failover、机器/应用/实例上下线
已存在Redis直接移入和数据迁移
布隆过滤器引出
问题引出:现有50亿个电话号码,有10万个电话号码,要快速准确判断这些电话号码是否已经存在
1. 通过数据库查询:实现快速有点难
2. 数据预先放在内存集合中:50亿 * 8字节 = 40GB(***内存***浪费或不够)
3. hyperloglog:准确有点难
布隆过滤器原理
需要参数
m个二进制向量(m个二进制位数)
n个预备数据(类似于问题中的50亿个电话号码)
k个hash函数(每个数据,逐个进行hash,将指定向量标识为1)
构建布隆过滤器
将n个数据逐个走一遍hash流程
判断元素是否存在
将元素逐个hash进行执行
如果得到的hash结果再向量中全都是1,则表明存在,反之当前元素不存在
布隆过滤器误差率
对的数据的返回结果必然是对的,但是错误的数据也可能是对的
直观因素
m / n 的比例:比例越大,误差率越小
hash函数的个数:个数越多,误差率越小
本地布隆过滤器
实现类库:Guava
1 Funnel<Integer> funnel = Funnels.integerFunnel(); 2 int size = 1000_000; 3 double errorChance = 0.001; //错误率 4 BloomFilter<Integer> filter = BloomFilter.create(funnel , size , errorChance); 5 for(int i = 0 ; i < size ; i++ ) { 6 filter.put(i); 7 } 8 for(int i = 0 ; i < size ; i++ ) { 9 if( !filter.mightContain(i) ) { 10 System.out.println("发现不存在的数据 : " + i); 11 } 12 }
布隆过滤器的问题
容量受到限制
多个应用存在多个布隆过滤器,构建同步过滤器复杂
Redis单机布隆过滤器
基于bitmap实现,利用setbit、getbit命令实现
hash函数:
MD5
murmur3_128
murmur3_32
sha1
sha256
sha512
基于Redis单机实现存在的问题
速度慢:与本机比,输在网络
解决方法1:单独部署、与应用同机房甚至同机架部署
解决方法2:使用pipeline
容量限制:Redis最大字符串为512MB、Redis单机容量
解决方法:基于RedisCluster实现
Redis Cluster实现布隆过滤器
多个布隆过滤器:二次路由
基于pipeline提高效率
内存管理
Redis内存消耗
内存使用统计
info memory
2.内存消耗划分
used_memory的组成部分
自身内存(800K左右)
缓冲内存
客户端缓冲区
复制缓冲区
AOF缓冲区
对象内存
Key对象
Value对象
Lua内存
内存碎片 = used_memory_rss - used_memory(申请内存的会超过使用内存,内存预留与内存暂未释放)
3.子进程内存消耗
redis在bgsave与bgrewriteaof时候都会使用fork创建出子进程,fork是copy-on-write的,当发生写操作时,就会复制出一份内存
优化方式
去除THP特性:在kernel 2.6.38中新增的特性。这个特性可以加快fork的速度,但是在复制内存页的时候,会比原来的内存页扩大了512倍。例如原来是4K,扩大后为2M。当写入量比较大时,会造成不必要的阻塞与内存的暴增。
overcommit_memory=1 可以保证fork顺利完成
客户端缓冲区
#查看已连接客户端的基本信息
info clients
#查看所有Redis-Server的所有客户端的详细信息
client list
输入缓冲区
各个客户端执行的Redis命令会存储在Redis的输入缓冲区中,由单线程排队执行
- 注意:输入缓冲区最大1GB,超过后会被强制断开,不可动态配置
输出缓冲区
1 client-output-buffer-limit ${class} ${hard limit} ${soft limit} ${soft seconds} 2 ## class 客户端类型 3 ## normal 普通客户端 4 ## slave 从节点用于复制,伪装成客户端 5 ## pubsub 发布订阅客户端 6 ## hardLimit 如果客户端使用的输出缓冲区大于hardLimit,客户端会被立即关闭 7 ## softLimit 如果客户端使用的输出缓冲区超过了softLimit并且持续了softSeconds秒,客户端会立即关闭 8 ## 其中hardLimit与softLimit为0时不做任何限制
1.普通客户端
默认配置:client-output-buffer-limit normal 0 0 0
注意:需要防止大命令或者monitor,这两种情况会导致输出缓冲区暴增
2.slave客户端
默认配置:client-output-buffer-limit slave 256mb 64mb 60
建议调大:可能主从延迟较高,或者从节点数量过多时,主从复制容易发生阻塞,缓冲区会快速打满。最终导致全量复制
注意:在主从网络中,从节点不要超过2个
3.pubsub客户端
默认配置:client-output-buffer-limit pubsub 32mb 8mb 60
阻塞原因:生产速度大于消费速度
注意:需要根据实际情况进行调试
缓冲内存
1.复制缓冲区
此部分内存独享,默认1MB,考虑部分复制,可以设置更大,可以设置为100MB,牺牲部分内存避免全量复制。
2.AOF缓冲区
无论使用always、everysecs还是no的策略,都会进行刷盘,刷盘前的数据存放在AOF缓冲区中。
另外在AOF重写期间,会分配一块AOF重写缓冲区,重写时的写数据会存放在重写缓冲区当中。
为了避免数据不一致的发生,在重写完成后,会将AOF重写缓冲区的内容,同步到AOF缓冲区中。
AOF相关的缓冲区没有容量限制。
对象内存
Key:不要过长,量大不容忽视,建议控制在39字节之内。(embstr)
Value:尽量控制其内部编码为压缩编码。
内存碎片
在jemalloc中必然存在内存碎片
原因
与jemalloc有关,jemalloc会将内存空间划分为小、大、巨大三个范围;每个范围又划分了许多小的内存块单位;存储数据的时候,会选择大小最合适的内存块进行存储。
redis作者为了更好的性能,在redis中实现了自己的内存分配器来管理内存,不用的内存不会马上返还给OS,从而实现提高性能。
修改cache的值,且修改后的value与原先的value大小差异较大。
优化方法
重启redis服务,
redis4.0以上可以设置自动清理 config set activedefrag yes,也可以通过memory purge手动清理,配置监控使用性能最佳。
修改分配器(不推荐,需要对各个分配器十分了解)
避免频繁更新操作
内存回收策略
1.删除过期键值
惰性删除
1. 访问key
2. expired dict
3. del key
定时删除:每秒运行10次,采样删除
2.maxmemory-policy
Noevition:默认策略,不会删除任何数据,拒绝所有写入操作并返回错误信息。
volatile-lru:根据lru算法删除设置了超时属性的key,直到腾出空间为止。如果没有可删除的key,则将退回到noevition。
allkeys-lru:根据lru算法删除key,不管数据有没有设置超时属性,直到腾出足够空间为止。
allkeys-random:随机删除所有键,直到腾出足够空间为止。
volatile-random:随机删除过期键,直到腾出足够空间为止。
volatile-ttl:根据键值对象的ttl属性,删除最近将要过期的数据。如果没有,回退到noevicition。
客户端缓冲区优化
案例:一次线上事故,主从节点配置的maxmemory都是4GB,发现主节点的使用内存达到了4GB即内存已打满。而从节点只有2GB。
思考方向
考虑Redis的内存自身组成
主从节点之前数据传输不一致(会导致对象内存不一致)
dbsize
查看info replication中的slave_repl_offset
查看缓冲区方面
通过info clients查看最大的缓冲区占用
通过client list查看所有客户端的详细信息
问题发现:
存在一个monitor客户端对命令进行监听,由于Redis的QPS极高,monitor客户端无法及时处理,占用缓冲区。
问题预防
运维层面:线上禁用monitor(rename monitor "")
运维层面:适度限制缓冲区大小
开发层面:理解monitor原理,可以短暂寻找热点key
开发层面:使用CacheCloud可以直接监控到相关信息
内存优化其他建议
不要忽视key的长度:1个亿的key,每个字节都是节省
序列化和压缩方法:拒绝Java原生序列化,可以采用Protobuf、kryo等
不建议使用Redis的场景
数据:大且冷的数据
功能性:关系型查询、消息队列
完
原文地址:https://www.cnblogs.com/quyangyang/p/11375187.html