架构设计
组件选择/多级
缓存设计要分多个层次,在不同层次上选择不同的缓存
JVM缓存
即本地缓存,设计在应用服务器中
通常可以采用Ehcache和Guava_Cache,在互联网应用中,由于要处理高并发,通常选择GuavaCache
适用场景
1. 对性能有非常高的要求
2. 不经常变化
3. 占用内存不大
4. 有访问整个集合的需求
5. 数据允许不实时一致
文件缓存
指基于http协议的文件缓存,一般放在Nginx中
因为静态文件中,很多都是不经常更新的,Nginx使用Proxy_cache将用户的请求缓存到本地一个目录,下一个相同请求可以直接调取缓存文件
Redis缓存
分布式缓存,采用主从+哨兵或RedisCluster的方式缓存数据库的数据
在实际开发中
作为数据库使用,数据要完整
作为缓存使用
缓存大小
GuavaCache设置
cacheBuilder.newBuilder().maximumSize(num)
Redis设置
maxmemory=512mb
maxmemory-policy allkeys lru
缓存淘汰策略选择
allkeys-random
希望请求符合平均分布
禁止驱逐,用作DB时,不设置maxmemory
Key数量
Redis单例能处理Key:2.5亿个
一个Key/Value大小最大是512M
读写峰值
Redis采用基于内存的,单进程单线程模型的KV数据库,由C语言编写,官方提供的数据是可以达到10w+QPS
命中率
说明
命中:可以直接通过缓存获取到需要的数据
不命中:无法直接在缓存获取想要的数据,需要再次查询数据库或执行其它操作
原因可能是由于缓存中根本不存在,或缓存已过期
通常来说,缓存命中率越高说明缓存收益越高,应用性能越好,抗并发能力越强
一个缓存失效机制和过期时间设计良好的系统,命中率可以做到95%以上
影响命中率的因素
1. 缓存数量越少命中率越高,比如缓存单个对象的命中率要高于缓存集合
2. 过期时间越长命中率越高
3. 缓存越大缓存对象越多,命中越多
性能监控指标
利用info查看
connected_clients
连接的客户端数量
used_memory_rss_human
系统给redis分配的内存
used_memory_peak_human
内存使用的峰值大小
total_connections_received
服务器已接受的连接请求数量
instantaneous_ops_per_sec
服务器每秒执行的命令数量
instantaneous_input_kbps
redis网络入口kps
instantaneous_output_kbps
redis网络出口kps
rejected_connections
因为最大客户端数量限制而被拒绝的连接请求数量
expired_keys
因为过期而被自动删除的数据库键数量
evicted_keys
因为最大内存容量限制而被驱逐的键数量
keyspace_hits
查找数据库键成功的次数
keyspace_misses
查询数据库键失败的次数
监控平台
grafana、prometheus、redis_exporter
缓存预热
指系统启动前,提前将相关缓存数据直接加载到缓存系统,避免在用户请求时,先查询数据库,然后再将数据缓存的问题
加载缓存思路
数据量不大,可以在项目启动时自动加载
利用定时任务刷新缓存,将数据库的数据刷新到缓存中
缓存问题
缓存穿透
一般缓存系统都是按照key去缓存查询,如果不存在对应value,就应该去后端系统查找(DB)
缓存穿透指在高并发下查询Key不存在的数据,会穿过缓存查询数据库,导致数据库压力过大而宕机
解决方案
对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或该key对应的数据insert之后清理缓存
会导致缓存太多空值占用更多空间
使用布隆过滤器,在缓存前加一层布隆过滤器,查询时先去布隆过滤器查询key是否存在,如果不存在就直接返回,存在再查询缓存和DB
缓存雪崩
当缓存服务器重启或大量缓存集中在某一个时间段失效,这样情况下,会给后端系统带来很大压力
突然大量key失效或redis重启,大量访问数据库,造成数据库崩溃
解决方案
1. key失效期分散开,不同key设置不同有效期
2. 设置二级缓存(数据不一定一致)
3. 高可用(脏读)
缓存击穿
对于一些设置了过期时间的key,如果这些key可能在某个时间点被超高并发地访问,是一种非常“热点”的数据
和雪崩对比,雪崩针对是的很多key,击穿针对的是某一个热key
缓存在某个时间点过期时,恰好此时对这个key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并会刷到缓存,这时的大并发请求可能会瞬间把后端DB压垮
解决方案
1. 用分布式锁控制访问的线程
使用Redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库
2. 不设超时时间,volatile-lru,但会造成写一致问题
当数据库数据发生更新时,缓存中的数据不会即使更新,这样会造成数据库中的数据与缓存中的数据不一致,应用会从缓存中读取到脏数据
可以采用延时双删策略
数据不一致
缓存和DB的数据不一致的根源:数据源不一样
保证数据最终一致性(延迟双删)
1. 先更新数据库同时删除缓存项,等读的时候再填充缓存
2. 2秒后再删除一个缓存项(key)
3. 设置缓存过期时间,如10s,1h
4. 将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除
升级方案
通过数据库的binlog来异步淘汰key,利用工具(canal)将binlig日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存
数据并发竞争
指多个Redis的client同事set同一个key引起的并发问题
方案一:分布式锁+时间戳
1. 整体技术方案
准备一个分布式锁,大家去抢锁,抢到锁就做Set操作
加锁目的是为了把并行读写改为串行读写的方式,从而来避免资源竞争
2. Redis分布式锁的实现
主要用到Redis的setnx(),用SetNx实现分布式锁
使用时间戳判断set顺序
方案二:利用消息队列
并发量过大的情况下,可以通过消息中间件进行处理,把并行度写进行串行化
把Redis的Set操作放在队列中使其串行化,必须一个一个执行
HotKey
当由大量请求访问某个Redis某个Key时,由于流量集中达到网络上限,从而导致这个Redis的服务器宕机,造成缓存击穿,接下来对这个Key的访问将直接访问数据库造成数据库崩溃,或者访问数据库回填Redis再访问Redis,继续崩溃
发现HotKey
1. 预估,如秒杀、热点新闻等
3. 如果是Proxy,如Codis,可在Proxy端收集
4. 利用Redis自带命令
monitor,hotkeys
执行缓存,不建议使用
5. 利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计
如Storm、Spark、Streaming、Flink
发现热点数据后可写入Zookeeper中
处理HotKey
1. 变分布式缓存为本地缓存
发现HotKey后,把缓存数据取出,直接加载到本地缓存中
可采用Ehcache、GuavaCache
这样系统在访问HotKey时可以直接访问自己的缓存了
2. 在每个Redis节点上备份HotKey数据
读取是可以采用随机读取的方式,将访问压力负载到每个Redis上
3. 利用对热点数据访问的限流熔断保护措施
每个系统实例每秒最多请求缓存集群读操作不超过400次,超过即可熔断
用户稍后自行刷新页面即可
问题:首页不行,体验不好
通过系统层自己直接加限流熔断措施,可以很好的保护后面的缓存集群
分支主题
BigKey
指存储的值(Value)非常大,常见
热门话题的讨论
大V的粉丝列表
序列化后的图片
......
影响
大Key会占用大量内存,在集群中无法均衡
Redis性能下降,主从复制异常
在主动删除或过期删除时会操作时间过长而引起服务阻塞
发现
1. Redis-cli --bigkeys
可以找到某个实例5种数据类型的最大Key(String、Hash、List、Set、Zset)
如果Redis的Key比较多,执行会比较缓慢
2. 获取生产的RDB文件
通过rdbtools分析生成csv文件,再导入MySQL或其他数据库进行统计分析
根据size_in_bytes统计
处理
优化原则就是String减少字符串长度,其他减少成员数
1. String类型的BigKey
尽量不要存入Redis中,可以使用MongoDB或缓存到CDN上
如果必须用Redis,最好单独存储,不要和其他Key一起存储
2. 单个简单的Key存储Value很大
可以尝试将对象分拆为几个Key-value,使用mget获取值
分拆意义在于分拆单次操作的压力,降低对Redis的IO影响
Hash、Set、Zset、List中存储过多的元素,可以将这些元素分拆
3. 删除大Key时不要使用Del
因为del是阻塞命令,会影响性能
使用lazy delete(unlink命令)
删除指定key(s),不存在则跳过
相比del的阻塞,该命令会在另一个线程中回收内存
仅将keys从key空间中删除,真正的数据删除会在后续异步操作
缓存与数据库一致性
缓存更新策略
利用Redis的缓存淘汰策略被动更新 LRU、LFU
一致性最差,维护成本低
在更新数据库时主动更新
先更新数据库再删缓存-----延时双删
一致性较强,维护成本高
整合MyBatis
可以使用Redis做MyBatis的二级缓存,在分布式环境下使用
分布式锁
Watch
乐观锁基于CAS(Compare_and_swap)思想,是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应
可以利用Redis来实现乐观锁
1. 利用Redis的watch功能,监控这个RedisKey的状态值
2. 获取RedisKey的值
3. 创建Redis事务
4. 给这个Key的值+1
5. 然后执行这个事务,如果Key的值被修改过则回滚,Key不加1
SetNX
实现原理
共享资源互斥,共享资源串行化
分布式锁是控制分布式系统之间同步访问共享资源的一种方式
利用Redis的单线程特性对共享资源进行串行化处理
实现方式
获取锁
1. 通过set key value [EX seconds] [PX milliseconds] [NX|XX]
推荐
能保证原子性操作
2. 通过setnx
现设置锁,然后设置过期
并发会产生问题
释放锁
2. 通过redis+lua实现
推荐
能保证原子性操作
存在问题
单机:无法保证高可用性
主从:无法保证数据强一致性,在主机宕机时会造成所得重复获得
无法续租,过期之后不能继续使用
本质分析
CAP,分布式锁是CP模型,Redis集群是AP模型
Redis集群不能保证数据的实时一致性,只能保证最终一致性
业务不需要数据强一致性时,可以使用Redis分布式锁
业务需要强一致性,即不允许重复获得锁,就不适用,而用CP模型实现,如Zookeeper,Etcd
Redission
它是架设在Redis基础上的一个Java驻内存数据网格
在基于NIO的Netty框架上,生产环境使用分布式锁
实现原理
加锁机制
如果客户端面对一个Redis集群,它首先会根据Hash节点选择一台机器,发送Lua脚本到Redis服务器上
Lua的作用是保证其中脚本业务的逻辑执行原子性
自动延时机制
只要某客户端加锁成功,就会启动一个watchdog,它是一个后台线程,每10s检查一下,如果客户端还持有锁key,就会不断延长锁key 的生存时间
分布式锁特性
互斥性
任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁
同一性
锁只能被持有该锁的客户端删除,不能由其他客户端删除
可重入性
持有某个锁的客户端可继续对该锁加锁,实现锁的续租
容错性
锁失效后自动释放锁,其他客户端可以继续获得该锁,防止死锁
Session分离
传统的Session由单个Web服务器自行维护管理,但是对于集群或分布式环境,传统模式会需要在各个web服务器间通过网络和IO进行复制,极大的影响了系统性能
可以将登录成功后的Session信息,存放在Redis中,这样多个服务器可以共享Session信息
利用Spring-session-data-redis,可以实现基于Redis的Session分离