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