互联网营销常用网站,怎么用图片做网站背景图,网站的建设哪家好,网站权重批量查询目录
1 为啥要缓存捏#xff1f;
2 基本流程#xff08;以查询商铺信息为例#xff09;
3 实现数据库与缓存双写一致
3.1 内存淘汰
3.2 超时剔除#xff08;半自动#xff09;
3.3 主动更新#xff08;手动#xff09;
3.3.1 双写方案
3.3.2 读写穿透方案
3.3.…目录
1 为啥要缓存捏
2 基本流程以查询商铺信息为例
3 实现数据库与缓存双写一致
3.1 内存淘汰
3.2 超时剔除半自动
3.3 主动更新手动
3.3.1 双写方案
3.3.2 读写穿透方案
3.3.3 写回方案
4 缓存穿透的解决方案
1缓存空对象
2布隆过滤
缓存空对象解决缓存穿透问题流程 5 缓存雪崩解决方案 6 缓存击穿的解决方案
6.1 基于互斥锁解决缓存击穿问题
6.2 基于逻辑过期解决缓存击穿问题 7 缓存工具封装 1 为啥要缓存捏
速度快,好用
缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力
实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为避震器,系统是几乎撑不住的,所以企业会大量运用到缓存技术 2 基本流程以查询商铺信息为例 3 实现数据库与缓存双写一致
首先我们需要明确数据一致性问题的主要原因是什么从主要原因入手才是解决问题的关键数据一致性的根本原因是 缓存和数据库中的数据不同步那么我们该如何让 缓存 和 数据库 中的数据尽可能的即时同步这就需要选择一个比较好的缓存更新策略了
常见的缓存更新策略 3.1 内存淘汰
利用Redis的内存淘汰机制实现缓存更新Redis的内存淘汰机制是当Redis发现内存不足时会根据一定的策略自动淘汰部分数据
这种策略模型优点在于没有维护成本但是内存不足这种无法预定的情况就导致了缓存中会有很多旧的数据数据一致性差。
Redis中常见的淘汰策略
1 noeviction默认当达到内存限制并且客户端尝试执行写入操作时Redis 会返回错误信息拒绝新数据的写入保证数据完整性和一致性 2 allkeys-lru从所有的键中选择最近最少使用Least Recently UsedLRU的数据进行淘汰。即优先淘汰最长时间未被访问的数据 3 allkeys-random从所有的键中随机选择数据进行淘汰 4 volatile-lru从设置了过期时间的键中选择最近最少使用的数据进行淘汰 5 volatile-random从设置了过期时间的键中随机选择数据进行淘汰 6 volatile-ttl从设置了过期时间的键中选择剩余生存时间Time To LiveTTL最短的数据进行淘汰
3.2 超时剔除半自动
手动给缓存数据添加TTL到期后Redis自动删除缓存
这种策略数据一致性一般维护成本有但是较低一般用于兜底方案~
3.3 主动更新手动
手动编码实现缓存更新在修改数据库的同时更新缓存
这种策略数据一致性就是最高的毕竟自己动手丰衣足食但同时维护成本也是最高的。
3.3.1 双写方案
1读取Read当需要读取数据时首先检查缓存是否存在该数据。如果缓存中存在直接返回缓存中的数据。如果缓存中不存在则从底层数据存储如数据库中获取数据并将数据存储到缓存中以便以后的读取操作可以更快地访问该数据。
2写入Write当进行数据写入操作时首先更新底层数据存储中的数据。然后根据具体情况可以选择直接更新缓存中的数据使缓存与底层数据存储保持同步或者是简单地将缓存中与修改数据相关的条目标记为无效状态缓存失效以便下一次读取时重新加载最新数据
在更新数据的情况下 优先选择删除缓存模式 其次是更新缓存模式
问题操作时先操作数据库还是先操作缓存捏
答案先操作数据库再删缓存
如果先操作缓存先删缓存再更新数据库
当线程1删除缓存到更新数据库之间的时间段会有其它线程进来查询数据由于没有加锁且前面的线程将缓存删除了这就导致请求会直接打到数据库上给数据库带来巨大压力。这个事件发生的概率很大因为缓存的读写速度块而数据库的读写较慢。
这种方式的不足之处存在缓存击穿问题且概率较大
如果先操作数据库先更新数据库再删缓存
当线程1在查询缓存且未命中此时线程1查询数据查询完准备写入缓存时由于没有加锁线程2乘虚而入线程2在这期间对数据库进行了更新此时线程1将旧数据返回了出现了脏读这个事件发生的概率很低因为先是需要满足缓存未命中且在写入缓存的那段时间内有一个线程进行更新操作缓存的读写和查询很快这段空隙时间很小所以出现脏读现象的概率也很低
这种方式的不足之处存在脏读现象但概率较小
要保证两个操作同时操作 在单体项目中可以放在同一个事务中
3.3.2 读写穿透方案
将读取和写入操作首先在缓存中执行然后再传播到数据存储
1读取穿透Read Through当进行读取请求时首先检查缓存。如果所请求的数据在缓存中找到直接返回数据。如果缓存中没有找到数据则将请求转发给数据存储以获取数据。获取到的数据随后存储在缓存中然后返回给调用者。
2写入穿透Write Through当进行写入请求时首先将数据写入缓存。缓存立即将写操作传播到数据存储确保缓存和数据存储之间的数据保持一致。这样保证了后续的读取请求从缓存中返回更新后的数据。
3.3.3 写回方案
调用者只操作缓存其他线程去异步处理数据库实现最终一致
1读取Read先检查缓存中是否存在数据如果不存在则从底层数据存储中获取数据并将数据存储到缓存中。
2写入Write先更新底层数据存储然后将待写入的数据放入一个缓存队列中。在适当的时机通过批量操作或异步处理将缓存队列中的数据写入底层数据存储 主动更新策略中三种方案的应用场景 双写方案 较适用于读多写少的场景数据的一致性由应用程序主动管理读写穿透方案 适用于数据实时性要求较高、对一致性要求严格的场景写回方案 适用于追求写入性能的场景对数据的实时性要求相对较低、可靠性也相对低 更新策略的应用场景 对于低一致性需求可以使用内存淘汰机制。例如店铺类型数据的查询缓存对于高一致性需求可以采用主动更新策略并以超时剔除作为兜底方案。例如店铺详情数据查询的缓存
4 缓存穿透的解决方案
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在这样缓存永远不会生效这些请求都会打到数据库。
常见解决缓存穿透的解决方案
1缓存空对象
就是给redis缓存一个空对象并设置TTL存活时间
优点实现简单维护方便
缺点额外的内存消耗可能造成短期的不一致
2布隆过滤
通俗的说就是中间件~
优点内存占用较少没有多余key
缺点实现复杂存在误判可能有穿透的风险无法删除数据 上面两种方式都是被动的解决缓存穿透方案此外我们还可以采用主动的方案预防缓存穿透比如增强id的复杂度避免被猜测id规律、做好数据的基础格式校验、加强用户权限校验、做好热点参数的限流
缓存空对象解决缓存穿透问题流程 5 缓存雪崩解决方案
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机导致大量请求到达数据库带来巨大压力。
就是说一群设置了有效期的key同时消失了或者说redis罢工了导致所有的或者说大量的请求会给数据库带来巨大压力叫做缓存雪崩~
缓存雪崩的常见解决方案 给不同的Key的TTL添加随机值利用Redis集群提高服务的可用性给缓存业务添加降级限流策略比如快速失败机制让请求尽可能打不到数据库上给业务添加多级缓存 6 缓存击穿的解决方案
缓存击穿问题也叫热点Key问题就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了无数的请求访问会在瞬间给数据库带来巨大的冲击。
大概击穿流程:
第一个线程查询redis发现未命中然后去数据库查询并重建缓存这个时候因为在缓存重建业务较为复杂的情况下重建时间较久又因为高并发的环境下在线程1重建缓存的时间内会有其他的大量的其他线程进来发现查找缓存仍未命中导致继续重建如此死循环。 缓存击穿的常见解决方案
互斥锁时间换空间 优点内存占用小一致性高实现简单 缺点性能较低容易出现死锁 逻辑过期空间换时间 优点性能高 缺点内存占用较大容易出现脏读 两者相比较互斥锁更加易于实现但是容易发生死锁且锁导致并行变成串行导致系统性能下降逻辑过期实现起来相较复杂且需要耗费额外的内存但是通过开启子线程重建缓存使原来的同步阻塞变成异步提高系统的响应速度但是容易出现脏读
6.1 基于互斥锁解决缓存击穿问题
就是当线程查询缓存未命中时尝试去获取互斥锁然后在重建缓存数据在这段时间里其他线程也会去尝试获取互斥锁如果失败就休眠一段时间并继续不断重试等到数据重建成功其他线程就可以命中数据了。这样就不会导致缓存击穿。这个方案数据一致性是绝对的但是相对来说会牺牲性能。
这里我们获取互斥锁可以使用redis中string类型中的setnx方法 因为setnx方法是在key不存在的情况下才可以创建成功的所以我们重建缓存时使用setnx来将锁的数据加入到redis中并且通过判断这个锁的key是否存在如果存在就是获取锁成功失败就是获取失败这样刚好可以实现互斥锁的效果。
释放锁就更简单了直接删除我们存入的锁的key来释放锁。 //获取锁public Boolean tryLock(String key){Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}//释放锁方法public void unlock(String key){stringRedisTemplate.delete(key);} 6.2 基于逻辑过期解决缓存击穿问题
给redis缓存字段中添加一个过期时间然后当线程查询缓存的时候先判断是否已经过期如果过期就获取互斥锁并开启一个子线程进行缓存重建任务直到子线程完成任务后释放锁。在这段时间内其他线程获取互斥锁失败后并不是继续等待重试而是直接返回旧数据。这个方法虽然性能较好但也牺牲了数据一致性。
所谓的逻辑过期类似于逻辑删除并不是真正意义上的过期而是新增一个字段用来标记key的过期时间这样能能够避免key过期而被自动删除这样数据就永不过期了从根本上解决因为热点key过期导致的缓存击穿。一般搞活动时比如抢优惠券秒杀等场景请求量比较大就可以使用逻辑过期等活动一过就手动删除逻辑过期的数据
逻辑过期一定要先进行数据预热将我们热点数据加载到缓存中
逻辑过期时间根据具体业务而定逻辑过期过长会造成缓存数据的堆积浪费内存过短造成频繁缓存重建降低性能所以设置逻辑过期时间时需要实际测试和评估不同参数下的性能和资源消耗情况可以通过观察系统的表现在业务需求和性能要求之间找到一个平衡点 7 缓存工具封装
调用者 /*** 根据id查询商铺数据** param id* return*/Overridepublic Result queryById(Long id) {// 调用解决缓存穿透的方法
// Shop shop cacheClient.handleCachePenetration(CACHE_SHOP_KEY, id, Shop.class,
// this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// if (Objects.isNull(shop)){
// return Result.fail(店铺不存在);
// }// 调用解决缓存击穿的方法Shop shop cacheClient.handleCacheBreakdown(CACHE_SHOP_KEY, id, Shop.class,this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);if (Objects.isNull(shop)) {return Result.fail(店铺不存在);}return Result.ok(shop);}工具类
Component
Slf4j
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate stringRedisTemplate;}/*** 将数据加入Redis并设置有效期** param key* param value* param timeout* param unit*/public void set(String key, Object value, Long timeout, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit);}/*** 将数据加入Redis并设置逻辑过期时间** param key* param value* param timeout* param unit*/public void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) {RedisData redisData new RedisData();redisData.setData(value);// unit.toSeconds()是为了确保计时单位是秒redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit);}/*** 根据id查询数据处理缓存穿透** param keyPrefix key前缀* param id 查询id* param type 查询的数据类型* param dbFallback 根据id查询数据的函数* param timeout 有效期* param unit 有效期的时间单位* param T* param ID* return*/public T, ID T handleCachePenetration(String keyPrefix, ID id, ClassT type,FunctionID, T dbFallback, Long timeout, TimeUnit unit) {String key keyPrefix id;// 1、从Redis中查询店铺数据String jsonStr stringRedisTemplate.opsForValue().get(key);T t null;// 2、判断缓存是否命中if (StrUtil.isNotBlank(jsonStr)) {// 2.1 缓存命中直接返回店铺数据t JSONUtil.toBean(jsonStr, type);return t;}// 2.2 缓存未命中判断缓存中查询的数据是否是空字符串(isNotBlank把null和空字符串给排除了)if (Objects.nonNull(jsonStr)) {// 2.2.1 当前数据是空字符串说明该数据是之前缓存的空对象直接返回失败信息return null;}// 2.2.2 当前数据是null则从数据库中查询店铺数据t dbFallback.apply(id);// 4、判断数据库是否存在店铺数据if (Objects.isNull(t)) {// 4.1 数据库中不存在缓存空对象解决缓存穿透返回失败信息this.set(key, , CACHE_NULL_TTL, TimeUnit.SECONDS);return null;}// 4.2 数据库中存在重建缓存并返回店铺数据this.set(key, t, timeout, unit);return t;}/*** 缓存重建线程池*/public static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10);/*** 根据id查询数据处理缓存击穿** param keyPrefix key前缀* param id 查询id* param type 查询的数据类型* param dbFallback 根据id查询数据的函数* param timeout 有效期* param unit 有效期的时间单位* param T* param ID* return*/public T, ID T handleCacheBreakdown(String keyPrefix, ID id, ClassT type,FunctionID, T dbFallback, Long timeout, TimeUnit unit) {String key keyPrefix id;// 1、从Redis中查询店铺数据并判断缓存是否命中String jsonStr stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(jsonStr)) {// 1.1 缓存未命中直接返回失败信息return null;}// 1.2 缓存命中将JSON字符串反序列化未对象并判断缓存数据是否逻辑过期RedisData redisData JSONUtil.toBean(jsonStr, RedisData.class);// 这里需要先转成JSONObject再转成反序列化否则可能无法正确映射Shop的字段JSONObject data (JSONObject) redisData.getData();T t JSONUtil.toBean(data, type);LocalDateTime expireTime redisData.getExpireTime();if (expireTime.isAfter(LocalDateTime.now())) {// 当前缓存数据未过期直接返回return t;}// 2、缓存数据已过期获取互斥锁并且重建缓存String lockKey LOCK_SHOP_KEY id;boolean isLock tryLock(lockKey);if (isLock) {// 获取锁成功开启一个子线程去重建缓存CACHE_REBUILD_EXECUTOR.submit(() - {try {// 查询数据库T t1 dbFallback.apply(id);// 将查询到的数据保存到Redisthis.setWithLogicalExpire(key, t1, timeout, unit);} finally {unlock(lockKey);}});}// 3、获取锁失败再次查询缓存判断缓存是否重建这里双检是有必要的jsonStr stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(jsonStr)) {// 3.1 缓存未命中直接返回失败信息return null;}// 3.2 缓存命中将JSON字符串反序列化未对象并判断缓存数据是否逻辑过期redisData JSONUtil.toBean(jsonStr, RedisData.class);// 这里需要先转成JSONObject再转成反序列化否则可能无法正确映射Shop的字段data (JSONObject) redisData.getData();t JSONUtil.toBean(data, type);expireTime redisData.getExpireTime();if (expireTime.isAfter(LocalDateTime.now())) {// 当前缓存数据未过期直接返回return t;}// 4、返回过期数据return t;}/*** 获取锁** param key* return*/private boolean tryLock(String key) {Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS);// 拆箱要判空防止NPEreturn BooleanUtil.isTrue(flag);}/*** 释放锁** param key*/private void unlock(String key) {stringRedisTemplate.delete(key);}
}