目录
redis缓存
缓存:数据交换缓冲区,临时存储数据,读写性能高,例如:Cpu缓存
项目使用缓存好处:
- 降低后端负载:如果请求内容缓存中存在(命中缓存),则后端不在重新处理
- 提高读写效率:降低响应时间 缓存的存储位置性能更高且已经存在的比重新处理速度更快
缓存的成本:
- 数据一致性 :如果源数据被修改,缓存中存放的可能是旧数据,导致数据不一致
- 代码维护成本高 :添加了缓存,使得项目代码变得复杂,考虑的情况更多
- 运维成本: 添加了额外的服务器用作缓存
缓存适合不经常变动,但经常用到的数据
读写缓存
整体流程
添加店铺读写缓存
修改service实现 ,缓存数据结构采用String 类型 因此要求 将对象转为json
添加店铺类型读写缓存
使用 缓存的对象的另一种形式 hash 缓存 ,将对象转为map ,并注意对象属性非String的字段 (使用StringRedisTemplate 要求全部为String)
数据类型选择
本次数据是List类型与 以往不同
- 使用String 类型,将整个List集合看做一个vaule,并转为一个json字符串 ,形成一个K_V键值对。
- 使用Hash类型,将集合中每一条数据看做 filed(自己生成)和value
- 使用List类型,是将集合中的对象看做value。遍历数据,将对象类型转为json,再放入缓存。取出同样将集合的String类型转为对象
效果
更新缓存
更新缓存策略
redis中缓存中的数据与数据库中的数据可能更新不及时,存在缓存不一致现象
内存淘汰
:
redis 自动完成,当内存不足时,reids随机删除一些数据 。下次查询,缓存中数据不存在又会重新放入缓存- 数据一致性:redis随机删除,一致性全靠概率
- 维护成本:redis自动完成,无需维护
超时剔除
:
给键值对添加ttl过期时间,当设置的键值对过期时被redis删除。下次查询,缓存中数据不存在又会重新放入缓存- 数据一致性:如果数据库在键值对没有过期时进行修改,redis中仍保存旧数据,会导致短暂的不一致性 。不一致性时间<=TTL时间。可靠性一般
- 维护成本:只需要在设置键值对时,添加过期时间
主动更新
:
每次对数据的修改,都同时对缓存进行修改- 数据一致性:绝大数情况下一致
- 维护成本:每次修改数据库,都要手动编码修改缓存,麻烦
因此:内存淘汰结合超时剔除适用于对数据一致性要求不高,修改不频繁的数据
-
手动编码,可控性最高
问题:-
数据变动时,
删除缓存
or更新缓存
更 新 缓 存 问 题 \color{red}{更新缓存问题} 更新缓存问题 :每次数据库修改都会写入缓存,在修改过程中,如果仍然没有请求访问,那么只有最后一次请求有效,之前对缓存的多次写入导致无效写过多
因此 ,应该以读缓存时为准,在读数据时写入缓存,所以修改时,只需删除缓存。在请求到达前,无论多少此修改,都只删除一次,请求到来时,写入一次。但小问题是,第一次请求要访问数据库
-
缓 存 事 务 问 题 \color{red}{缓存事务问题} 缓存事务问题在删除缓存时,如何保证数据库与缓存之间的原子性(同时成功或同时失败)
单体应用:将缓存与数据库使用同一个事务
分布式系统:利用TCC等分布式事务方案 -
多 线 程 问 题 \color{red}{多线程问题} 多线程问题 先操作缓存还是数据库
若先删除缓存,可能存在当 把缓存删除后,数据库还未还未更新完,此时由一个请求查找不到缓存,读取了数据库旧值,并将旧值写入缓存,当更新完数据后,只有等待再次查询缓存中旧数据过期才会使用新的数据
若先操作数据库,在删除缓存:在查询数据缓存未命中时,查询数据库,正准备将数据写入缓存,这时一个线程将数据修改并删除了缓存,而写入缓存的是旧数据。同样只有等待再次查询缓存中旧数据过期才会使用新的数据
二者都可能导致旧数据新写入缓存(使用超时剔除兜底),但二者概率相差很大
第一种概率
:在更新数据库时,另一个线程完成数据库读取并写入缓存
第二种概率
:缓存未命中 在写入缓存时(时间较写入数据库短很多)这期间另一个线程完成更新数据库并删除缓存 -
-
使用第三方服务,自动维护,向上透明,无需关注一致性
-
单独开辟一个线程,异步更新缓存 。可将多次修改进行压缩,统一修改。效率比较高,但一致性不好
更新店铺缓存
在 根据id查询店铺,缓存未命中就查询数据库,并将结果写入缓存后返回
这一基础上,设置缓存的过期时间(采用超时剔除用来更新出错时兜底)
在根据id修改店铺时,先修改数据库,在将缓存删除
缓存问题
缓存穿透
请求的数据缓存中和数据库中都不存在(只有数据库存在才能放入缓存),这样缓存永远不会失效,请求一直打到服务器
如果恶意频繁发送数据库不存在的数据,大量请求穿透缓存直接到达数据库,会搞垮数据库
两种解决方案
缓存空对象
将缓存和数据库都不存在的数据,将key对应的空值返回并进redis,使得缓存能够命中(尽管返回空),减少数据库压力
由此每次从缓存中取出都要判断是否为空
优点:实现简单、维护方便
缺点:
- 大量的空值的key占用内存 (设置过期时间缓解)
- 数据短期不一致 ,如果缓存的空对象数据库后来有值了,而缓存却仍返回空(将过期时间置短缓解,或新增数据时 添加缓存覆盖)
布隆过滤
在缓存与请求之间,添加一个过滤器。该过滤器具有统计功能,先计算数据库中存在哪些数据,在请求到达缓存之前根据所需数据是否存在进行拦截 。布隆过滤器只知道数据有没有,不知道具体数据,简化存储 (不存在,一定不存在。存在,可能不存在)降低穿透概率
原理是 一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中。
根据 :两个哈希值相同但原始值不一定相同(哈希碰撞),但原始值相同哈希值一定相同
。将key转为哈希值,并将值对应bit的位置 置为1,即代表key存在。查询时,将key转为hash值,在去bitMap的对应位置比对
,判断是否为1,若为0则代表数据中一定不存在,进行拦截 。 若hash数字过长,可除bitMap设定的最大值(会增大误差),确保所有hash结果都能找到对应的bitMap中bit位。
特点:
- 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。 (只要存在就一定会将对应bit位为1,但bit位 为1 ,可能是其他hash值相同的key)
- 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。(因为一个bit位 可能代表多个key)
可用于拦截 如:缓存穿透;去重;,但存在小概率误差
优点
- 占用内存小
缺点 - 实现复杂
- 存在误判
增加url复杂度:防止被猜到规律,通过拦截,进入请求
对格式进行校验:在查询数据之前,进行一些非法过滤
加强用户权限:减少非登录用户发起恶意请机会,被拦截器拦截
对用户进行限流:例如 ,某些用户每秒最多发起几次请求,防止突然发送大量恶意请求
在读取店铺中防止缓存穿透
采用缓存空对象方式
相比之前 多了个: 当数据库中也不存在时,添加空值放入缓存 ,并在从缓存中查询时,注意判断数据是否为空
整体导图
缓存雪崩
同一时间,大量请求无法使用缓存(key集体过期,缓存服务器宕机),像雪崩一样冲向数据库
解决方案:
针对大量key过期
:
- 将key的过期时间设置不相同,例如在指定过期时间的基础上加小范围随机时间,防止过期时间相同,集体失效,数据库压力过大
针对缓存服务器宕机
: - redis采用集群方式部署,高可用
- 添加容灾处理,牺牲部分服务,确保能够运行
- 给业务添加多级缓存
整体导图
缓存击穿
热点的key过期,导致一瞬间大量访问该key的请求打到数据库 。该key有两个特征:1. 被大量请求访问 2. 缓存重建业务复杂(在重建长时间里,大量请求透过,多次进行缓存重建)
原有逻辑 如果缓存中不存在,判断是否是缓存穿透处理,从数据数查找,并放入缓存
。这样没有考虑热点key不存在且重建缓存耗时,多个请求都要查询数据库,重建缓存的情况
解决方式
互斥锁
原有逻辑 如果缓存中不存在,判断是否是缓存穿透处理,从数据数查找,并放入缓存
引入互斥后,多了一步, 当数据缓存不存在时,先去判断是否有线程正在创建缓存(根据互斥锁),若有 则等待 然后继续访问缓存,重复以上步骤,若没有直接访问数据库,放入缓存
这种方式就减少了数据库的查询,避免缓存多次重建
一个请求拿到锁后,独自进行创建缓存。在重建缓存完成之前,其他线程只能等待一会后重复以上步骤
问题:多个请求等待一个请求重构缓存,如果这个缓存构建时间多久,其余也只能等待
互斥锁的表示
jdk中提供的 synchronized
和lock
固定流程,只有拿到锁才能执行,其余只能等待。而互斥锁没有拿到锁的不只是等待(等待过后,不是向下执行,还要继续从缓存中获取)
使用redis自定义互斥锁:
利用锁的特征:不存在时才可以获取,一但获取在释放前不可再次获取
所以使用 Setnx key value
建立一个标识作为锁
添加店铺查询热点key过期问题
流程图
创建两个工具方法
将之前解决 单线程缓存以及缓存穿透问题封装成一个方法,并创建解决多线程热点key问题的方法。二者可切换
逻辑过期
不设置TTL,而是value中标识的时间,过期不是由redis自动决定,使得热点key永不过期,交给业务决定。业务判断过期一边更新缓存,一边仍然使用旧数据。
引入逻辑过期后:请求从缓存中拿到数据,先去判断是否过期,若过期则在判断是否有线程正在重建(锁),若没有开辟一个子进程去访问数据库重建缓存,父线程则直接返回旧数据;若有锁,则继续返回旧数据
添加店铺查询热点key过期问题
使用逻辑过期法
- 提前在redis写入店铺数据,并设置逻辑过期时间进行数据预热
已知shop类不含有过期时间属性 ,解决思路:- 创建一个类,继承shop类,并在本类中添加过期字段
- 创建一个类,使用包装shop类
在缓存中添加预热的key
最终效果
最大问题:
数据有不一致情况
互斥锁与逻辑删除对比
整体导图