贵阳哪家网站建设公司好,seo公司资源,wordpress调用最新留言,建设工程施工合同网站1.绪论
redis本质上就是一个缓存框架#xff0c;所以我们需要研究如何使用redis来缓存数据#xff0c;并且如何解决缓存中的常见问题#xff0c;缓存穿透#xff0c;缓存击穿#xff0c;缓存雪崩#xff0c;以及如何来解决缓存一致性问题。
2.缓存的优缺点
2.1 缓存的…1.绪论
redis本质上就是一个缓存框架所以我们需要研究如何使用redis来缓存数据并且如何解决缓存中的常见问题缓存穿透缓存击穿缓存雪崩以及如何来解决缓存一致性问题。
2.缓存的优缺点
2.1 缓存的优点
缓存可以降低数据库的负载提高读写效率和响应时间。
2.2 缓存的缺点
1.需要额外的资源消耗
2.如何保证缓存和数据库的一致性是一个问题所以要求强一致性的业务尽量不要用缓存
3.缓存一致性
为了保证缓存和数据库的一致性我们一般有三种方式来操作缓存分别是读写穿透旁路缓存和异步写回。
3.1 Cache Aside Pattern (旁路缓存
其实就是人工编码方式也是我们操作redis的常见的使用方式就是先更新完数据库再去更新缓存。
3.1.1 步骤
1.读缓存
如果命中缓存直接返回如果未命中便从数据中读取数据并且更新缓存。
2.写缓存
如果未命中缓存直接更新数据库如果命中缓存便更新数据库同时更新删除缓存。
3.1.2 更新缓存和数据库的方式
读缓存这里没有一致性问题但是在写入缓存的时候是先写入到数据库还是先写入到缓存这里就会有一致性问题。对于写缓存的操作方式主要有如下4种:写数据库写缓存写缓存写数据库写数据库删缓存写缓存删数据。
1.写缓存写数据库 先写缓存再写数据库的话如果缓存写入成功数据库写入失败数据库回滚此时缓存便会与数据库内容不一致。
2.写数据写缓存 可以看出在线程1更新数据库为1成功线程2获得时间片更新数据库和缓存为x线程1再次获得时间片更新缓存为1。这个时候缓存和数据库数据不一致。
3.删缓存更新数据库 可以看出线程1在删除缓存后线程2获得时间片读取到缓存为空会查询数据库并将该旧值写入到缓存中但是线程1会更新数据库为新值导致缓存不一致。
4.更新数据库删除缓存
假设此时因为缓存淘汰策略缓存已经失效。所以初始时缓存为null 可以看出假设缓存初始时因为淘汰策略缓存为null然后线程1查询数据库得到A1线程2获得时间片更新数据库为Ax并且删除缓存线程1得到时间片更新缓存为A1此时发生了缓存不一致。
但是上面主要有两个条件导致:
1.线程1查询缓存刚好失效需要从数据库中重新读取
2.线程1查完库后线程2立刻更新数据库并且删除缓存。
这两个条件在事件开发过程中是很难遇到的所以该方式可以作为缓存更新数据库的方式。
5.延迟双删
延迟双删其实是在第3种方式删除缓存更新数据库上面再加了一次删除。如下 前面说过在删除缓存和更新数据之间可能会有其他线程因为未命中缓存所以读取到旧数据并且更新到缓存中。所以我们就延迟一定时间尽量将这部分线程更新的缓存数据删除掉。
a) 为什么需要第一次删除
因为删除和写入数据库不是一个原子操作如果是先更新数据库然后延迟一段时间在删除缓存这样操作的话如果第二次删除失败会有不一致的问题。所以在更新数据库前引入一次删除操作这样可以尽可能的保证删除成功。
b) 为什么需要删除第二次
前面已经讲过第二次删除是为了删除掉因为第一次删除和更新期间其他线程查询数据库旧值 并写入到缓存的问题。
3.1.3 如何保证删除成功
根据前文的分析我们在实际开发中可以通过如下两种方式来更新缓存
1.更新数据库删除缓存
2.延迟双删
当时上面两种方式都需要保证删除成功才能保证缓存和数据库的一致性我们应该怎样才能保证删除缓存成功呢
1.设置超时时间
这种方式其实就是利用缓存自带的过期策略保证缓存一定会过期尽量的减少脏读。
2.重试
当删除失败的时候可以进行重试但是可能影响接口性能。
3.监听binlog
比如延迟双删可以变成如下
1.删除缓存
2.更新数据库。
3.监听binlog删除缓存。
监听binlog的中间件一般都有重试机制能够保证删除尽量成功。
3.1.4 如何保证redis和数据库的强一致性
前面说的更新缓存的方案里面推荐的更新数据库删除缓存和延迟双删都不能完全保证数据库和缓存的一致性。如果对一致性要求很高可以做成同步的方式先更新数据库再更新redis并且给这两个操作加分布式锁保证原子性。
3.2 Read/Write Through Pattern读写穿透
缓存和数据库为一个整体用户只需要操作缓存至于如何实现缓存与数据的一致性交给缓存去实现。其实我觉得这种模式叫通过缓存读通过缓存写更好理解。因为客户端基本上只更缓存打交道如果缓存没有数据需要同步数据库内容是通过缓存去更新的。更新缓存数据后同步到数据库也是通过缓存更新的。 3.2.1 读缓存
读缓存的时候如果数据存在便直接返回如果数据不存在缓存便会从数据库中拉取数据并用户返回。
3.2.2 写缓存
写缓存的时候,如果未命中缓存便更新数据库。如果命中缓存便更新缓存缓存在更新数据到数据库。注意缓存需要保证这两个动作的原子性。
这里为什么客户端更新缓存的时候是直接更新数据库而不更新缓存呢其实是因为这样可以将更新操作分摊到读写缓存中来。读缓存时同步未命中缓存的那一部分数据写缓存时同步命中缓存的那一部分数据。
3.2.3 优势
其实就是为了减少旁路缓存用户的开发工作缓存自己实现了和数据库同步的这部分工作。
3.3 Write Behind Caching Pattern(异步写回)
异步写回其实就是用户只用更新缓存中的数据然后启动一个线程异步的将缓存中的数据刷到数据库中。这个其实在很多框架中都是采用这种方式比如前面介绍的RocketMq中对MappedFile的持久化还有linux中的页缓存等都是采用这种方式。
它的优点就是只用和缓存打交道所以速度极快。并且它和读穿/写穿模式的最主要的区别是。读穿/写穿模式是更新缓存过后会同步刷新到数据库中。但是异步写回是异步的写入到库中。所以可能会有丢数据的风险。
4.缓存穿透
4.1 什么是缓存穿透
缓存穿透就是当客户端访问缓存时发现缓存中没有数据然后去访问数据库但是数据库中也没有数据。所以在读取数据的时候因为数据库中没有数据给redis缓存所以请求会一直到数据库中导致数据库压力过大。
4.2 怎么解决缓存穿透
4.2.1 缓存空对象
1.操作
当数据库中没有数据的时候可以缓存一个空对象到redis中。
2.优缺点
操作简单但是如果数据库有对象的时候并且采用的是过期淘汰的策略的话会有一段时间和数据库不一致。
3.代码
public R,ID R queryWithPassThrough(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit){String key keyPrefix id;// 1.从redis查询商铺缓存String json stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json ! null) {// 返回一个错误信息return null;}// 4.不存在根据id查询数据库R r dbFallback.apply(id);// 5.不存在返回错误if (r null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在写入redisthis.set(key, r, time, unit);return r;}
4.2.3 布隆过滤器
可以在请求前根据id到布隆过滤器中查询一下判断该数据是否存在如果不存在便直接返回。布隆过滤器时基于概率统计的判断某个元素是否存在某个位数组中的工具。
我们来看看其实现原理
布隆过滤器由一组hash函数和一个数组组成。现在假设有k个hash函数当有一个对象传入的时候这k个hash喊出会将这个字符串进行hash运算然后映射到数组的k个bit位上。在判断对象是否存在的时候根据对象上面的bit位是否都位1如果都为1的话表示对象可能存在。但为什么是可能而不是一定呢因为有hash冲突有极低的可能存在某两个元素经过k个hash函数的映射到数组中的位置是一样的。 5.缓存雪崩
5.1 什么是缓存雪崩
缓存雪崩就是在某一个时刻大量的key同时过期或者redis直接宕机导致大量请求涌入到数据库数据库压力激增。
5.2 怎么解决缓存雪崩
为了预防大量key同时过期给key的过期时间设置一个随机值
为了防止redis过期我们可以通过集群的方式保证redis服务高可用。
6.缓存击穿
6.1 什么是缓存击穿
缓存击穿就是在高并发场景下因为热点key这里热点key可以指访问频率高或者重建缓存时间长的key过期导致大量线程同时重建缓存。
6.2 怎么解决缓存击穿
6.2.1 互斥锁
1.思路
其实就是保证只有一个线程在重建缓存。当某个线程发现缓存不存在是先加互斥锁然后查询数据库构建缓存更新缓存。如果此时其他线程来获取缓存发现缓存为空重建缓存时需要先阻塞获取锁。
2.代码 public R, ID R queryWithMutex(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit) {String key keyPrefix id;// 1.从redis查询商铺缓存String shopJson stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson ! null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey LOCK_SHOP_KEY id;R r null;try {boolean isLock tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功根据id查询数据库r dbFallback.apply(id);// 5.不存在返回错误if (r null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}
6.2.2 逻辑过期
1.思路
其主要步骤如下
1.给数据设置一个逻辑过期时间并且写入到缓存中比如{name:三expireTime:1720712827}
2.线程1查询缓存发现数据已经过期单独启动一个线程进行缓存重建这里重建缓存也需要加互斥锁防止多个线程进行重建。
3.其他线程访问缓存发现缓存过期首先会获取锁如果发现数据已经过期会去获取锁进行缓存重建但是获取锁失败返回redis中的旧数据。
2.代码
public R, ID R queryWithLogicalExpire(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit) {String key keyPrefix id;// 1.从redis查询商铺缓存String json stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在这里会进行缓存预热提前将热点数据加载到redis中if (StrUtil.isBlank(json)) {// 3.存在直接返回return null;}// 4.命中需要先把json反序列化为对象RedisData redisData JSONUtil.toBean(json, RedisData.class);R r JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期直接返回店铺信息return r;}// 5.2.已过期需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey LOCK_SHOP_KEY id;boolean isLock tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 6.3.成功开启独立线程实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() - {try {// 查询数据库R newR dbFallback.apply(id);// 重建缓存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;}3.优缺点
逻辑过期不用进行锁等待但是会占用额外的空间(存储缓存过期时间)并且不能保证一致性(因为其他线程发现有线程在异步重建缓存过后会返回旧数据。
7.参考
1.Redis第12讲——缓存的三种设计模式_缓存的设计模式-CSDN博客
2.缓存一致性问题解决方案-CSDN博客
3.https://www.yuque.com/hollis666/un6qyk/tmcgo0
4. 黑马程序员Redis入门到实战教程深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目_哔哩哔哩_bilibili