缓存与redis
缓存
1 缓存漂移
本地缓存在集群多节点场景下数据不一致
2 集中式缓存
解决集群内多个节点间执行写操作之后,各节点本地缓存不一致的问题;扩大内存缓存容量限制
Redis的单次请求处理性能极高,甚至可以达到微秒级别的响应速度
对集中式缓存的过分滥用导致频繁的IO交互,IO请求,线程阻塞和唤醒,使得系统在线程上下文切换层面浪费巨大,应该尝试降低对集中式缓存(Redis)的请求数量,那么应该将本地缓存和集中式缓存结合起来使用
3 多级缓存策略
变更频率高的数据采用集中式缓存,极少变更的数据或者对短期一致性要求不高的数据采用本地缓存
缓存举例
ARP协议,基于ARP缓存表进行IP与终端硬件MAC地址之间的缓存映射;Mybatis的一级缓存和二级缓存;CPU与内存之间的临时存储器(高速缓存)
4 使用缓存场景
- 降低自身CPU的消耗(字段冗备,中间结果存储)
- 减少对外IO的交互(从自身性能出发,降低响应延迟;从对端稳定性考虑,调用方可以缓存结果减少IO)
- 从自身可靠性出发,远程服务结果缓存起来,起到兜底作用,提升自身的抗风险能力(nacos为例,nacos客户端从服务端拉取配置和服务注册信息,然后缓存到客户端本地使用,即使服务端宕机,也能保证业务侧正常使用)
- 前端APP或H5,降低用户流量消耗,提升页面渲染速度,减少出现白屏情况
5 缓存的架构模式
- 旁路型缓存:业务自行实现与缓存和数据库的读写操作,需要注意高并发下的数据一致性问题,可能会出现缓存击穿,缓存穿透,缓存雪崩
- 穿透型缓存:业务模块不会和数据库直接交互,只知道缓存接口
- 异步行缓存:读写只和缓存相关,异步将数据同步到DB中,适用于对数据一致性不高的场景
6 缓存的实践
- 缓存必须可删除重建
- 有兜底屏障,关注缓存量超过承受范围的处理策略,定好数据的淘汰机制
- 避免缓存集中失效,比如批量加载数据到缓存的时候随机打散过期时间,避免同一时间大批量缓存失效引发缓存雪崩问题
- 有效地冷数据预热加载机制,以及热点数据防过期机制,避免出现大量对冷数据的请求无法命中缓存或者热点数据突然失效,导致缓存击穿问题
- 合理的防身自保手段,比如采用布隆过滤器机制,避免被恶意请求攻陷,导致缓存穿透类的问题
7 数据淘汰机制(有损自保的降级策略)
- 直接拒绝
- 随机剔除
- 基于LRU,最久未被使用的剔除
- 提前过期,按过期时间排序,最快过期的先剔除
- 根据创建时间、修改时间、优先级、访问次数等维度剔除
- redis提供6种淘汰机制
- noeviction:不接受任何新数据写入
- allkeys-lru和volatile-lru基于lru策略进行丢弃
- allkeys-random和volatile-random基于随机策略进行丢弃
- volatile-ttl快过期先淘汰
8 缓存雪崩
- 设置相同失效时间,导致大量缓存数据在短时间内集体失效,大量请求无法命中缓存而直接流转到了数据库,数据库承受不住压力,导致系统瘫痪(缓存雪崩)
- 将过期时间在一个固定时间段内以毫秒级别进行随机打散
- 在分布式微服务化的系统中,每个服务有自己独立的缓存服务,如果某个服务的缓存出现了雪崩,通过服务降级,服务熔断,接口限流等操作将问题阻断在单个业务模块中,避免殃及其他模块
9 缓存击穿
- 缓存击穿和前面提到的缓存雪崩产生的原因其实很相似
- 缓存击穿是少量缓存失效(热点数据)遭遇大并发量的请求,导致这些请求全部涌入数据库中
- 为热点数据设置过期时间续期操作
- 利用分布式锁,第一个持锁线程先从数据库查询写入到缓存中,其余请求尝试先从缓存中获取数据
- 如果因为某种原因,冷数据突然被很多人访问,也会造成缓存击穿,这种需要考虑对冷数据加热处理,比如指定时间段内如果对冷数据访问超过了某个阈值,单独加到热点数据缓存中,设置一个合适的过期时间
10 缓存穿透
- 大量无效的请求没有命中缓存,全部请求到DB,DB中也没有命中,请求过多导致DB承受太大压力(外部爬虫,黑客攻击等)
- 做一个类似白名单,布隆过滤器,构建一些反爬策略,请求签名校验机制,添加IP访问限制策略
11 数据一致性
- 缓存删除+数据库更新
缓存删除成功,数据库更新失败情况下,再次读取的时候,会从DB里将旧数据加到缓存,保证一致。但是如果在并发情况下,还有一个线程的查询在两个操作中间,则会出现数据不一致的情况
- 数据库更新+缓存删除
数据库更新成功,缓存删除失败情况下,采用数据库事务回滚,在非并发条件下,保证一致,注意事务粒度不能过大,避免事务成为阻塞系统性能的瓶颈,如果对并发性能要求极高,可以尝试重试机制和异步补偿机制和设置缓存过期时间
- 数据库更新+缓存更新
并发情况下,另一线程的操作在两个操作中间,则会出现数据不一致的情况,也可以用数据库事务来保证,需要保证使用的事务隔离级别为Serializable或者Repeatable Read级别,以此来保证并发更新的场景下不会出现数据不一致问题,但这也降低了并发效率,提高数据库的CPU负载,读多写少、写操作并发竞争不是特别激烈且对一致性要求不是特别高的情况下,可以采用事务(高隔离级别) + 先更新数据库再更新缓存的方式来达到数据一致
Guava cache
- 支持基于创建时间和访问时间设置过期时间
- 淘汰机制支持根据缓存条数和每条缓存的权重判断是否到达阈值,支持FIFO和LRU
- 支持集成数据源
- 自带并发锁定,同一时刻仅允许一个请求去回源获取数据并回填到缓存中,而其余请求则阻塞等待,不会造成数据源的压力过大,防止缓存击穿
- 提供缓存监控统计,stat统计日志,支持查看缓存数据的加载或者命中情况统计
- 支持使用concurrencyLevel来指定并发量
- 适用场景:读多写少对一致性要求不高,对性能要求严苛,分散redis压力,减少网络开销
Caffeine
- 异步策略,触发淘汰操作,会将清理任务添加到独立的线程池中,回溯操作也支持异步操作
- 底层ConcurrentHashMap版本的优化,java7以前是分段锁,数组+链表,java8之后是数组+红黑树(链表元素超过8个转变为红黑树),分段锁升级为synchronized+CAS锁(乐观锁)
- 淘汰算法,一般采用LRU或LFU(最近少频率),caffeine采用W-TinyLFU算法,有效的解决了LRU以及LFU存在的弊端
Ehcache
支持多级缓存
堆内缓存(heap)内存缓存,被JVM托管的内存,容量有限无法持久话,创建缓存的时候可以指定使用堆内缓存,并限制缓存大小,速度快,JVM负责回收和清理
- 堆外缓存(off-heap)是相对JVM的堆外,堆内占用数据越大,GC压力越大,为了降低GC回收的影响,可以使用堆外缓存;offheap需要大于heap的容量大小(前提是heap大小设定的是字节数而非Entity数),offheap大小必须1M以上,读写前需要序列化和反序列化
- 磁盘缓存(disk)磁盘设置大小一定要大于heap和offheap
- 集群缓存(Cluster)
- 多级缓存中必须有堆内缓存,必须按照堆内缓存 < 堆外缓存 < 磁盘缓存 < 集群缓存的顺序进行组合;多级缓存中的容量设定必须遵循堆内缓存 < 堆外缓存 < 磁盘缓存 < 集群缓存的原则;多级缓存中不允许磁盘缓存与集群缓存同时出现
- 支持缓存持久化,支持变身分布式缓存;RMI组播方式,JMS消息方式,Cache server模式,JGroup方式,Terracotta方式
- 支持为容器中每一条缓存记录设定独立过期时间,Caffeine与Guava Cache,仅允许为设定缓存容器级别统一的过期时间
- 支持JCache和SpringCache规范
- Hibernate默认缓存策略,一级缓存是session级别的,二级缓存是Ehcache
- xml配置文件中,ttl和tti只允许设置一个
- 节点中,要放在后面
- RMI组播:
- 对象要实现Serializable,可序列化和反序列化,P2P两两连接
- 向对应地址发送UDP组播包,即时消息模式,可能导致同步消息的丢失,对可靠性和一致性要求更高的的场景慎选
- JMS消息
- 异步通信API,生产者消费者模式(发布订阅模式),核心是一个消息队列
- 不需要保证所有节点都在线,当节点宕机后,重启也会收到订阅的消息
- Ehcache默认使用Activemq,也可以切换成Kafka和Rabbitmq
- Cache sever模式
- 独立进程进行部署,多个独立进程组成分布式集群,对外提供resuful和soap接口,相对于是独立的集中式缓存,类似redis
- 可以通过http访问缓存数据,3.x版本不再有cache server的介绍,完全可以用redis或Terracotta取代
- Jgroup方式
- 工作模式基于IP组播,可靠性高
- 对所有接收者的消息的无丢失传输(通过丢失消息的重发)
- 大消息的分割传输和重组
- 消息的顺序发送和接收
- 保证原子性,消息要么被所有接收者接收,要么所有接收者都收不到
- Terracotta方式
- JVM层专门负责做分布式节点间协同处理的平台框架
- 热点数据存储在进程本地,然后根据热度进行优化存储,热度高的会优先存储在更快的位置(比如heap中),存储在其中一台应用节点上的缓存数据,可以被集群中其它节点访问到
Caffeine和Ehcache和Redis区别
- Caffeine轻量级,增强版的HashMap,本地简单使用少量缓存的时候用
- Redis集中缓存,集群化,分布式,保证缓存一致性,与网络交互,性能上有损失,多节点使用的,多个服务对缓存依赖重
- Ehcache折中,内存+磁盘,持久化集群化,数据量大,独立过期时间
Redis
1 数据类型
string,list链表,set无序集合不允许重复,hash无序的key-value键值对集合,zset含有member和score,类似key-value,区别点在于score是固定的double类型的value
2 应用
- 分布式锁
redis够快,提供setnx + expire机制,Redission客户端的流行
数据库抗压层(利用redis扛住高并发的请求)
登录验证码存储
用户登录信息存储
全局ID生成&全局限流
incrby原子命令,保证各个节点之间的唯一ID不会冲突,全局请求量统计计数,结合expire实现定时重置计数器,实现限流
- bitmap方式存储每日签到数据
Redis的bitmap数据最终存储的是string类型,但是Redis为Bitmap操作提供了配套的操作接口
- 榜单
基于zset结构,score排序
3 redis淘汰策略
- noeviction:淘汰新进入的数据,即拒绝新内容写入
- allkeys-lru:将内存中已有的key按照LRU淘汰掉
- volatile-lru:从设定过期的key按照LRU淘汰掉
- allkeys-random:将内存中已有的key随机淘汰掉
- volatile-random:从设定过期的key随机淘汰掉
- volatile-ttl:从设定过期的key里面按照ttl淘汰掉
4 提问
- redis处理快是因为单线程?redis进程中只有一个线程吗?
- 查询redis中有多少个以"user_"开头的记录数量?keys和scan
- 有一批机器,单机内存小于整体待缓存的数据量,该怎么设计?
- redis是用来抗压的,如果大部分数据可以存储在单机上,允许部分数据访问数据库
- 集群扩展,以集群的力量缓存全部的数据量
- redis集群是如何决定一个记录应该保存到哪个节点的?
- 如何打破redis缓存容量受限于机器单机内存大小的问题?
- 如何使得redis能够扛住多方过来的请求压力?
- 如何保证redis不会成为单点故障源?
5 redis过期
- setex命令实现插入的时候同步指定过期时间
- 实现缓存续期--通过expire命令对已有记录重新设定过期时间
- redis过期时间的设定是基于当前命令执行时刻开始的相对过期时间
6 持久化
- RDB全量持久化
- 分为SAVE和BGSAVE
- SAVE是由redis命令执行线程按照普通命令执行
- BGSAVE是fork出一个新的进程,在新的独立到的进程中执行SAVE操作
- SAVE触发的操作只有两个场景:客户端手动发送SAVE请求,redis在shutdown的时候自动执行
- BGSAVE触发的操作两个场景:客户端手动命令出发BGSAVE操作,redis配置定时任务触发(支持间隔时间+变更数据量双重维度综合判断,达到任一条件则触发)
- 主从部署的场景中还支持仅由slave节点触发bgsave操作,在fork子进程的时候需要将redis主进程中内存所有数据都复制一份到子进程中,在执行期间对机器的剩余内存有较高要求
- AOF增量同步
- AOF更像是记录住Redis的每一次写请求执行命令,将每次执行的写操作命令记录存储到磁盘上,然后通过一种类似命令重放执行的方式,来实现数据的恢复
- always:每一条redis写请求执行的时候会触发一次磁盘写入操作,且只有在磁盘写入完成之后,请求的响应才会返回,
- every sec:异步执行,子线程异步方式每秒一次将执行命令分批写入文件中,相比always方式在异常情况下可能会丢失最后1s的执行记录
- no:redis不控制落盘时间,由操作系统去决定什么时候该往磁盘flush
- 缺点:时间久了之后会占据大量磁盘空间,易造成磁盘满的问题,需要从AOF文件回放重新构建缓存内容时,可能会耗时较久
- RDB+AOF
- 从4.0版本开始,Redis支持了RDB + AOF的混合持久化方式,通过rewrite机制来实现。需要在redis的配置文件中开启对应开关:aof-use-rdb-preamble yes
- 先通过AOF的方式记录命令,达到门槛的时候才执行rewrite操作生成RDB,最大限度降低了RDB执行频率,降低了对redis业务命令处理过程的影响。
- 通过RDB的方式替代了前期大量的AOF命令存储,有效的降低了磁盘占用。
- 通过RDB + AOF的方式,系统重建缓存的时候,先加载RDB文件完成主体数据的重建,然后在此基础上重放AOF增量命令,大大降低了启动时AOF重放的耗时
7 部署方案
- 主从(master-replica)
- 一主两从是常见的搭配,主节点(Master)同时对外提供读和写操作,从节点(Slave)通过replicate同步的方式,从主节点复制数据,保持自身数据与主节点一致,从节点只能对外提供读操作
- 对于读多写少的操作,可以采用一主多从的方式
- redis支持从节点分发数据,即从节点从从节点同步数据
- 主从模式利用从节点分担读取操作的压力
- 在容错恢复等可靠性层面欠缺明显,不具备自动的故障转移与恢复能力
- 如果master主节点宕机,则redis功能受损,无法继续提供写服务,直到手动修复master节点方可恢复,也可以手动将其中一个从节点切换为新的master节点来恢复故障。而原先的master节点恢复后,需要手动将其降级为slave节点,对外提供只读服务
- 哨兵(sentinel)
- 在主从模式的基础上,额外部署若干独立的哨兵进程,通过哨兵进程去监视者Redis主从节点的状态,一旦发现主节点宕机,则哨兵可以重新从剩余slave节点中推选一个新的节点并将其升级为master节点,比较典型的是“一主二从三哨兵”
- master宕机,sentinel选举,sentinel节点个数必须是奇数
- 哨兵模式有效的解决了高可用的问题,保证了主节点的自动切换操作
- 并没有解决分布式场景下对于集中缓存容量的焦虑
- 集群(cluster)
- 去中心化的集群部署模式,集群内所有Redis节点之间两两连接
- 一个集群有多个分区
- 同一分区内Redis节点之间的数据完全一样
- 不同分区的数据不同,每个分区内都有主从
- 最少需要部署6个redis节点
8 redis sharding(数据分片)
- 解决数据分发到各个分区的问题
- 根据key的hash值进行取模,确定最终归属的节点
- 当集群内数据分区个数出现变化的时候,比如集群扩容的时候,会导致请求被分发到错误节点上,导致缓存*命中率降低,*需要对原先扩容前已经存储的数据重新进行一次hash计算和取模操作
- 将全部的数据重新分发到新的正确节点上进行存储
- 这个操作被称为重新Sharding
- 重新sharding期间服务不可用,可能会对业务造成影响
9 一致性Hash
- 所有的存储节点排列在首尾相接的Hash环上,当有新的分区节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续一个节点
- 如果Hash圆环上的分区节点数太少,可能会出现数据在各个分片中分布不均衡的情况,也即出现数据倾斜
- 引入了虚拟节点的机制,通过增加虚拟节点,来实现数据尽可能的均匀分布在各个节点上
10 Hash槽
- 如何才能实现扩展或者收缩节点的时候,保持已有数据不丢失呢?
- 数据查询的时候,先根据key的Hash值进行计算,确定应该落入哪个Hash槽,进而根据映射关系,确定负责此Hash槽数据存储的redis分区节点是哪个
- 数据节点增加的时候,需要手动执行下处理,为新的节点分配新其负责的Hash槽位区间段;调整已有的节点的Hash槽位负责区间段;将调整到新节点上的hash槽位区间段对应的数据分片文件拷贝到新的节点上
11 Bitmap降低redis存储容量压力
- BitMap能力的支持,其实是对String数据结构的一种扩展
- 签到这个场景,我们可以每天设定一个key,然后存储的时候,我们可以将数字格式的userId表示在BitMap中具体的位置信息,而BitMap中此位置对应的bit值为1则表示该用户已签到
- 模拟构造10亿用户数据量进行压测统计,结果如下:BitMap格式: 150M,key-value格式: 41G
12 Pipeline管道批处理(客户端行为)
- 涉及到同时去执行好多条redis命令的操作,比如系统启动的时候需要将DB中存量的数据全部加载到Redis中重建缓存的时候。如果业务流程需要频繁的与Redis交互并提交命令,可能会导致在网络IO交互层面消耗太大,导致整体的性能降低。这种情况下,可以使用pipeline将各个具体的请求分批次提交到Redis服务器进行处理
- 使用pipeline的方式,可以减少客户端与redis服务端之间的网络交互频次,但是pipeline也只是负责将原本需要多次网络交互的请求封装一起提交到redis上,在redis层面其执行命令的时候依旧是逐个去执行,并不会保证这一批次的所有请求一定是连贯被执行,其中可能会被插入其余的执行请求,不具备原子性
12 使用multi实现请求的事务(服务端行为)
- 客户端请求执行了multi命令之后,也即开启了事务
- 服务端会将这个客户端记录为一个特殊的状态,之后这个客户端发送到服务器上的命令,都会被临时缓存起来而不会执行
- 只有当收到此客户端发送exec命令的时候,redis才会将缓存的所有命令一起逐条的执行并且保证这一批命令被按照发送的顺序执行、执行期间不会被其他命令插入打断,redis事务不支持回滚
13 多级缓存机制
- 将缓存数据分为了2种类型,一种是不常变更的数据,比如系统配置信息等,这种数据直接系统启动的时候从DB中加载并缓存到进程内存中,然后业务运行过程中需要使用时候直接从内存读取。而对于其他可能会经常变更的业务层面的数据,则缓存到Redis中
- 享受到本地缓存的速度优势
- 降低了客户端与远端集中式缓存服务器之间的IO交互,带宽占用,服务器压力
- 提升了业务的可靠性,本地缓存实际上也是一种额外的副本备份
- 一般业务集群部署的节点都是无状态的,只需要负载均衡对外提供请求
- redis是有状态的,数据和进程都分布在不同节点上,但是对外是一个整体
https://juejin.cn/column/7140852038258147358