当前位置: 首页 > news >正文

【云岚到家】-day03-门户缓存方案选择

【云岚到家】-day03-门户缓存方案选择

1.门户常用的技术方案

什么是门户

说到门户马上会想到门户网站,中国比较早的门户网站有新浪、网易、搜狐、腾讯等,门户网站为用户提供一个集中的、易于访问的平台,使他们能够方便地获取各种信息和服务

这里我们说的门户是指一个网站或应用程序的主页,它是用户进入这个网站或系统的入口,主页上通常聚合很多的信息,包括内容导航、热点信息等,比如:门户网站的首页、新闻网站的首页、小程序的首页等

在这里插入图片描述

1.1 常用的技术方案

基于两个需求去分析:

1.门户上的信息是动态的:门户上的信息会按照一定的时间周期去更新,比如一个新闻网站不可能一直显示一样的新闻

2.门户作为入口其访问频率非常高:对于访问频率高的界面其加载速度是至关重要的,因为它直接影响用户的体验和留存率。一般来说门户网站的首页应该在2至3秒内加载完成,这被认为是一个合理的加载时间目标

常见的两类门户是:web门户和移动应用门户

1.1.1 Web门户

web门户是最常见的门户类型,比如:新浪、百度新闻等,它们通过PC浏览器访问,用户可以通过桌面电脑、笔记本电脑、平板电脑和智能手机等设备访问。Web门户通常运行在Web浏览器上,用户可以通过输入网址或通过搜索引擎访问

web门户是通过浏览器访问html网页,虽然html网页上的内容是动态的但是考虑门户作为入口其访问频率非常高所以就需要提高它的加载速度,如果网页上的数据是通过实时查询数据库得到是无法满足要求的,所以针对web门户提高性能的关键是如何提高html文件的访问性能,如何提高查询数据的性能

1.将门户页面生成静态网页发布到CDN服务器

纯静态网页通过Nginx加载要比去Tomcat加载快很多。

我们可以使用模板引擎技术将动态数据静态化生成html文件,并通过CDN分发到边缘服务器,可以提高访问效率。

Java模板引擎技术有很多,比如:freemarker、velocity等

什么是CDN?

CDN 是构建在数据网络上的一种分布式的内容分发网,旨在提高用户访问网站或应用时的性能(存放静态资源),通过CDN将内容分发到各个城市的CDN节点上,北京的网民请求北京的服务即可拿到资源,提高访问速度

在这里插入图片描述

2.html文件上的静态资源比如:图片、视频、CSS、Js等也全部放到CDN服务

3.html上的动态数据(更新频率高的)通过异步请求后端缓存服务器加载,不要直接查询数据库,通过Redis缓存提高查询速度

在这里插入图片描述

4.使用负载均衡,通过部署多个Nginx服务器共同提供服务,不仅保证系统的可用性,还可以提高系统的访问性能(三个人同时干活,一个出了问题,还有另外两个)

在这里插入图片描述

5.在前端也做一部分缓存

不仅服务端可以做缓存,前端也可以做缓存,前端可以把缓存信息存储到

LocalStorage: 提供了持久化存储,可以存储大量数据

SessionStorage: 与 LocalStorage 类似,但数据只在当前会话中有效,当用户关闭标签页或浏览器时清空

Cookie: 存储在用户计算机上的小型文本文件,可以在客户端和服务器之间传递数据

浏览器缓存:通过 HTTP 头部控制,比如:Cache-Control头部提供了更灵活的缓存控制选项,可以定义缓存的最大有效时间

1.1.2 移动应用门户

移动应用门户是专为移动设备(如智能手机和平板电脑)设计的应用程序,比如:小程序、APP等,用户可以通过应用商店下载并安装。这些应用程序提供了更好的用户体验,通常具有更高的性能和交互性,可以直接从设备主屏幕启动

对于移动应用提高访问效率方法通常有:

静态资源要走CDN服务器

对所有请求进行负载均衡

在前端及服务端缓存门户上显示的动态数据

根据上边的分析,对于Java程序员需要关注的是缓存服务的开发,主流的缓存服务器是Redis,所以我们接下来的工作重点是使用Redis为门户开发缓存服务接口

1.2 缓存技术方案

1.2.1 需求分析

目标:明确本项目门户有哪些信息需要缓存

1.界面原型

了解了门户的技术方案,下边通过门户界面原型分析本项目门户包括哪些部分,本项目小程序门户首页如下图:

在这里插入图片描述

2.缓存需求

根据门户的技术方案,为了提供效率门户显示的信息从缓存查询,根据上边的界面原型分析有哪些信息需要缓存?

1.定位界面上的已开通区域列表

在这里插入图片描述

2.服务搜索

通过Elasticsearch查询,es自带缓存

3.首页服务列表

4.热门服务列表

5.服务信息

点击某一个服务打开服务详情页面,如下图:

在这里插入图片描述

服务的详细信息一部分是服务介绍的图片和内容等,一部分是区域服务信息(比如价格)

6.服务分类

在全部服务界面左侧显示服务分类,服务分类下的服务项通过Elasticsearch去查询

在这里插入图片描述

小结

1.首页服务列表,包括两个服务分类及每个分类下的四个服务项

2.热门服务列表

3.服务类型列表

4.开通城市列表

5.服务详细信息,内容包括服务项信息、服务信息

1.3 Spring Cache入门

1.3.1 入门程序

目标:学会使用SpringCache查询缓存注解并理解它的原理

Redis访问工具

面试官:你们项目中用的redis客户端是什么?缓存用的什么框架

本项目使用Redis存储缓存数据,如何通过Java去访问Redis?

常用的有Jedis和Lettuce两个访问redis的客户端库,其中Lettuce的性能和并发性要好一些,SpringBoot 默认使用的是 Lettuce 作为 Redis 的客户端

本项目集成了Spring data redis框架,在项目中可以通过RedisTemplate访问Redis,RedisTemplate提供了方便访问redis的模板方法

RedisTemplate和Lettuce 是什么关系?

RedisTemplate进行 Redis 操作时,实际上是通过 Lettuce客户端与 Redis 服务器进行通信

本项目也集成了Spring Cache,Spring Cache是spring的缓存框架,可以集成各种缓存中间件,比如:EhCache(缓存服务)、Caffeine(缓存服务)、redis

Spring Cache最终也是通过Lettuce 去访问redis

使用Spring Cache的方法很简单,只需要在方法上添加注解即可实现将方法返回数据存入缓存,以及清理缓存等注解的使用

RedisTemplate适用于灵活操作redis的场景,通过RedisTemplate的API灵活访问Redis

面试官:你们项目使用redis用的什么框架?

Spring Cache、spring data redis(RedisTemplate调用方法)

Spring Cache基本介绍

Spring Cache是Spring提供的一个缓存框架,基于AOP原理,实现了基于注解的缓存功能,只需要简单地加一个注解就能实现缓存功能,对业务代码的侵入性很小

基于SpringBoot使用Spring Cache非常简单,首先加入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId><version>2.7.10</version>
</dependency>

本项目在jzo2o-framework下的jzo2o-redis工程引入此依赖,其它服务只需要引入jzo2o-redis的依赖即可

在这里插入图片描述

简单认识它的常用注解:

@EnableCaching:开启缓存注解功能(启动类上使用)

@Cacheable:查询数据时缓存,将方法的返回值进行缓存
@CacheEvict:用于删除缓存,将一条或多条数据从缓存中删除
@CachePut:用于更新缓存,将方法的返回值放到缓存中
@Caching:组合多个缓存注解
@CacheConfig:统一配置@Cacheable中的value值

搭建环境

在jzo2o-foundations工程创建新分支dev_02并切换到该分支,在jzo2o-foundations工程引入jzo2o-redis依赖

<dependency><groupId>com.jzo2o</groupId><artifactId>jzo2o-redis</artifactId>
</dependency>

在jzo2o-foundations工程的bootstrap.yml中引入redis的配置文件,如下图:

在这里插入图片描述

在nacos配置shared-redis-cluster.yaml,开发环境使用redis单机,注意配置redis的IP地址、端口和密码

在这里插入图片描述

4.查询数据时缓存

下边使用Cacheable注解实现查询服务信息时对服务信息进行缓存,它的执行流程是:第一次查询服务信息缓存中没有该服务的信息此时去查询数据库,查询数据库拿到服务信息并进行缓存,第二次再去查询该服务信息发现缓存中有该服务的信息则直接查询缓存不再去数据库查询

流程如下:重要

在这里插入图片描述

首先在工程启动类中添加@EnableCaching注解,它表示开启Spring cache缓存组件

在这里插入图片描述

下边实现对区域服务信息查询时进行缓存。

首先找到区域服务信息的service,为了不和原来的getById(Serializable id)查询方法混淆,单独定义查询区域服务信息缓存的方法,如下:

在IServeService接口中定义如下接口:

public interface IServeService extends IService<Serve> {/*** 查询区域服务信息并进行缓存* @param id 对应serve表的主键* @return 区域服务信息*/Serve queryServeByIdCache(Long id);
}

在接口实现类中定义如下方法,此时该方法还是查询数据库

@Service
public class ServeServiceImpl extends ServiceImpl<ServeMapper, Serve> implements IServeService {@Overridepublic Serve queryServeByIdCache(Long id) {Serve serve = baseMapper.selectById(id);return serve;}
}

下边在方法中添加Cacheable注解

@Service
public class ServeServiceImpl extends ServiceImpl<ServeMapper, Serve> implements IServeService {//@Cacheable(value = "JZ_CACHE:SERVE_RECORD",key = "#id")@Cacheable(value = RedisConstants.CacheName.SERVE,key = "#id")//缓存到redis,常量就是JZ_CACHE:SERVE_RECORD@Overridepublic Serve queryServeByIdCache(Long id) {Serve serve = baseMapper.selectById(id);return serve;}
}

Cacheable注解配置的两项参数说明:

value:缓存的名称,缓存名称作为缓存key的前缀

key: 缓存key,支持SpEL表达式,上述代码表示取参数id的值作为key,最终缓存key为:缓存名称+“::”+key,例如:上述代码id为123,最终的key为:JZ_CACHE:SERVE_RECORD::123

SpEL是一种在 Spring 框架中用于处理字符串表达式的强大工具,它可以实现获取对象的属性,调用对象的方法操作

keyGenerator:指定一个自定义的键生成器(实现 org.springframework.cache.interceptor.KeyGenerator 接口的类),用于生成缓存的键。与 key 属性互斥,二者只能选其一

测试

对queryServeByIdCache方法进行测试,编写单元测试方法

@SpringBootTest
@Slf4j
class IServeServiceTest {@Resourceprivate IServeService serveService;//区域服务查询@Testpublic void test_queryServeByIdCache(){Serve serve = serveService.queryServeByIdCache(1693815623867506689L);Assert.notNull(serve,"服务为空");}
}

运行测试方法,观察redis,数据缓存成功

在这里插入图片描述

缓存key:JZ_CACHE:SERVE_RECORD::1693815623867506689

缓存value:serve表的记录

TTL缓存过期时间:-1,表示永不过期

调整缓存过期时间

虽然数据被成功缓存,如果想调整缓存过期时间怎么做呢?

在@Cacheable注解中有一个属性为cacheManager,表示缓存管理器,通过缓存管理器可以设置缓存过期时间。

所有缓存相关的基础类都在jzo2o-redis工程,在jzo2o-redis工程定义spring cache需要的缓存管理器

在这里插入图片描述

上图中共包括三个缓存管理器:

缓存时间为30分钟、一天、永久,分别对应的bean的名称为:cacheManager30Minutes、cacheManagerOneDay、cacheManagerForever

下边我们在@Cacheable注解中指定缓存管理器为cacheManagerOneDay,即缓存时间为一天

@Cacheable(value = RedisConstants.CacheName.SERVE,key="#id",cacheManager = RedisConstants.CacheManager.ONE_DAY)
public Serve queryServeByIdCache(Long id) {Serve serve = baseMapper.selectById(id);return serve;
}

重新运行单元测试方法,我们发现缓存的过期时间没有改变,这是为什么?

原因是根据前边的缓存流程:先查询缓存,如果缓存存在则直接查询缓存返回数据,不再向缓存存储数据

在这里插入图片描述

所以我们需要删除缓存,重新运行测试方法,测试通过,观察redis中的缓存,过期时间已经改变,这说明我们设置的缓存管理器生效

在这里插入图片描述

由于缓存时间加了随机数,缓存一天的时间为90000秒左右

工作原理

Spring Cache是基于AOP原理,对添加注解@Cacheable的类生成代理对象,在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用源方法获取数据返回,并缓存起来,下边跟踪Spring Cache的切面类CacheAspectSupport.java中的private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts)方法

在这里插入图片描述

下边打断点调试

在这里插入图片描述

分别测试命中缓存和未命中缓存的情况:第一次查询(redis中没有缓存)时执行方法查询数据库,第二次命中缓存直接查询redis不再执行方法

测试Spring Cache

目标:学会使用@CacheEvict和@CachePut注解

在Spring Cache入门中使用了@Cacheable 注解,它实现的是查询时进行缓存

下边测试另外两个常用的注解,如下:

@CacheEvict:用于删除缓存,将一条或多条数据从缓存中删除
@CachePut:用于更新缓存,将方法的返回值放到缓存中

测试@CachePut

CachePut注解实现的是将方法的返回值放到缓存中

在服务上架后会将区域服务的信息写入缓存,服务下架会从缓存删除,下边我们实现服务上架将服务写入缓存

找到服务上架的方法,在方法上添加@CachePut注解

@Override
@Transactional
@CachePut(value = RedisConstants.CacheName.SERVE, key = "#id",  cacheManager = RedisConstants.CacheManager.ONE_DAY)
public Serve onSale(Long id){
....
}

上边代码同样指定了缓存名称、缓存key及缓存管理器(缓存过期时间为一天)

测试@CacheEvict

找到服务下架的方法,添加@CacheEvict注解

@Override
@Transactional
@CacheEvict(value = RedisConstants.CacheName.SERVE, key = "#id")
public Serve offSale(Long id){
....
} 

小结

Spring Cache有哪些常用的注解,都有什么用?

@EnableCaching:开启缓存注解功能

@Cacheable:查询数据时缓存,将方法的返回值进行缓存
@CacheEvict:用于删除缓存,将一条或多条数据从缓存中删除
@CachePut:用于更新缓存,将方法的返回值放到缓存中
@Caching:组合多个缓存注解

1.4 缓存常见问题

1.4.1 缓存穿透问题

在使用缓存时特别是在高并发场景下会遇到很多问题,常用的问题有缓存穿透、缓存击穿、缓存雪崩以及缓存一致性问题

1.什么是缓存穿透问题

缓存穿透是指请求(访问)一个不存在的数据,缓存层和数据库层都没有这个数据,这种请求会穿透缓存直接到数据库进行查询。它通常发生在一些恶意用户可能故意发起不存在的请求,试图让系统陷入这种情况,以耗尽数据库连接资源或者造成性能问题

在这里插入图片描述

比如:在快速入门程序中,查询一个缓存中不存在的数据将会执行方法查询数据库,数据库也不存在此数据,查询完数据库也没有缓存数据,缓存没有起到作用

2.解决方案

1.对请求增加校验机制

比如:查询的Id是长整型并且是19位,如果发来的不是长整型或不符合位数则直接返回不再查询数据库

2.缓存空值或特殊值

当查询数据库得到的数据不存在,此时我们仍然去缓存数据,缓存一个空值或一个特殊值的数据,避免每次都会查询数据库,避免缓存穿透

在这里插入图片描述

3.使用布隆过滤器

什么是布隆过滤器?

布隆过滤器是一种数据结构,用于快速判断一个元素是否属于一个集合中

它使用多个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点,将Bit array理解为一个二进制数组,数组元素是0或1

当一个元素加入集合时,通过N个散列函数将这个元素映射到一个Bit array中的N个点,把它们设置为1

执行三个函数,判断有没有结果是0,是0就代表这个不存在,就不查询数据库

在这里插入图片描述

检索某个元素时再通过这N个散列函数对这个元素进行映射,根据映射找到具体位置的元素,如果这些位置有任何一个0,则该元素一定不存在,如果都是1很可能存在误判

哈希函数的基本特性

同一个数使用同一个哈希函数计算哈希值,其哈希值总是一样的

对不同的数用相同的哈希函数计算哈希值,其哈希值可能一样,这称为哈希冲突

哈希函数通常是单向的不可逆的,即从哈希值不能逆向推导出原始输入。这使得哈希函数适用于加密和安全应用

为什么会存在误判?

主要原因是哈希冲突。布隆过滤器使用多个哈希函数将输入的元素映射到位数组中的多个位置,当多个不同的元素通过不同的哈希函数映射到相同的位数组位置时就发生了哈希冲突

由于哈希函数的有限性,不同的元素可能会映射到相同的位置上,这种情况下即使元素不在布隆过滤器中可能产生误判,即布隆过滤器判断元素在集合中

如何降低误判率?

增加Bit array空间,减少哈希冲突,优化散列函数,使用更多的散列函数

如何使用布隆过滤器?

将要查询的元素通过N个散列函数提前全部映射到Bit array中,比如:查询服务信息,需要将全部服务的id提前映射到Bit array中,当去查询元素是否在数据库存在时从布隆过滤器查询即可,如果哈希函数返回0则表示肯定不存在

使用布隆过滤器就是把数据库的服务信息全部同步到Bit array

在这里插入图片描述

布隆过滤器的优点是:二进制数组占用空间少,插入和查询效率高效

缺点是存在误判率,并且删除困难,因为同一个位置由于哈希冲突可能存在多个元素,删除某个元素可能删除了其它元素

布隆过滤器的应用场景?

1.海量数据去重,比如URL去重,搜索引擎爬虫抓取网页,使用布隆过滤器可以快速判定一个URL是否已经被爬取过,避免重复爬取

2.垃圾邮件过滤:使用布隆过滤器可以用于快速判断一个邮件地址是否是垃圾邮件发送者,对于海量的邮件地址,布隆过滤器可以提供高效的判定

3.安全领域:在网络安全中,布隆过滤器可以用于检查一个输入值是否在黑名单中,用于快速拦截一些潜在的恶意请求

4.避免缓存穿透:通过布隆过滤器判断是否不存在,如果不存在则直接返回

如何代码实现布隆过滤器?

使用redit的bitmap位图结构实现

使用redisson实现

使用google的Guava库实现

下边举例说明:

引入依赖

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>28.2-jre</version>
</dependency>

测试代码:

package com.jzo2o.foundations.service;import java.nio.charset.Charset;public class BloomFilterExample {public static void main(String[] args) {// 创建一个布隆过滤器,预期元素数量为1000,误判率为0.01BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000, 0.01);// 添加元素到布隆过滤器bloomFilter.put("example1");bloomFilter.put("example2");bloomFilter.put("example3");// 测试元素是否在布隆过滤器中System.out.println(bloomFilter.mightContain("example1")); // trueSystem.out.println(bloomFilter.mightContain("example4")); // false}
}

在上述代码中,我们创建了一个预期包含1000个元素、误判率为0.01的布隆过滤器。然后,我们向布隆过滤器中添加了三个元素(“example1”、“example2” 和 “example3”),并测试了几个元素是否在布隆过滤器中

请注意,误判率是你可以调整的一个参数。较低的误判率通常需要更多的空间和计算资源

3.小结

什么是缓存穿透?如何解决缓存穿透?

什么是布隆过滤器?如何使用布隆过滤器?

本项目使用缓存空值或特殊值的方法去解决缓存穿透

1.4.2 缓存击穿问题

1.什么是缓存击穿

缓存击穿发生在访问热点数据,大量请求访问同一个热点数据,当热点数据失效后同时去请求数据库,瞬间耗尽数据库资源,导致数据库无法使用

比如某手机新品发布,当缓存失效时有大量并发到来导致同时去访问数据库

2.解决方案

1.使用锁

单体架构下(单进程内)可以使用同步锁控制查询数据库的代码,只允许有一个线程去查询数据库,查询得到数据库存入缓存

synchronized(obj){//查询数据库//存入缓存
}

分布式架构下(多个进程之间)可以使用分布式锁进行控制

// 获取分布式锁对象
RLock lock = redisson.getLock("myLock");
try {// 尝试加锁,最多等待100秒,加锁后自动解锁时间为30秒boolean isLocked = lock.tryLock(100, 30, java.util.concurrent.TimeUnit.SECONDS);if (isLocked) {//查询数据库//存入缓存} else {System.out.println("获取锁失败,可能有其他线程持有锁");}
} catch (InterruptedException e) {e.printStackTrace();
} finally {// 释放锁lock.unlock();System.out.println("释放锁...");
}

2.热点数据不过期

可以由后台程序提前将热点数据加入缓存,缓存过期时间不过期,由后台程序做好缓存同步

例如:当服务上架后将服务信息缓存到redis且永不过期,此时需要使用put注解

3.缓存预热

分为提前预热、定时预热

提前预热就是提前写入缓存

定时预热是使用定时程序去更新缓存

4.热点数据查询降级处理

对热点数据查询定义单独的接口,当缓存中不存在时走降级方法避免查询数据库

小结

什么是缓存击穿?如何解决缓存击穿?

本项目对热点数据定时预热,使用定时任务刷新缓存保证缓存永不过期,解决缓存穿透问题

1.4.3 缓存雪崩问题

1.什么是缓存雪崩?

缓存雪崩是缓存中大量key失效后,当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用

比如对某信息设置缓存过期时间为30分钟,在大量请求同时查询该类信息时,此时就会有大量的同类信息存在相同的过期时间,一旦失效将同时失效,造成雪崩问题

2.解决方案

1.使用锁进行控制

思路同缓存击穿

2.对同一类型信息的key设置不同的过期时间

通常对一类信息的key设置的过期时间是相同的,这里可以在原有固定时间的基础上加上一个随机时间使它们的过期时间都不相同

具体实现:在framework工程中定义缓存管理器指定过期时间加上随机数

在这里插入图片描述

3.缓存定时预热

不用等到请求到来再去查询数据库存入缓存,可以提前将数据存入缓存。使用缓存预热机制通常有专门的后台程序去将数据库的数据同步到缓存

小结

什么是缓存雪崩?如何解决缓存雪崩?

本项目对key设置不同的过期时间解决缓存雪崩问题

1.4.4 缓存不一致问题

1.什么是缓存不一致问题

缓存不一致问题是指当发生数据变更后该数据在数据库和缓存中是不一致的,此时查询缓存得到的并不是与数据库一致的数据

**缓存不一致会导致什么后果?**比如:查看商品信息的价格与真实价格不一致,影响用户体验,如果直接使用缓存中的价格去计算订单金额更会导致计算结果错误

造成缓存不一致的原因可能是在写数据库和写缓存两步存在异常,也可能是并发所导致

写数据库和写缓存导致不一致称为双写不一致,比如:先更新数据库成功了,更新缓存时失败了,最终导致不一致

并发导致缓存不一致举例如下:

在这里插入图片描述

执行流程:

线程1先写入数据库X,当去写入缓存X时网络卡顿

线程2先写入数据库Y

线程2再写入缓存Y

线程1 写入缓存旧值X覆盖了新值Y

即使先写入缓存再写数据在并发环境也可能存在问题

在这里插入图片描述

流程:

线程1先写入缓存X,当去写入数据库X时网络卡顿

线程2先写入缓存Y

线程2再写入数据库Y

线程1 写入数据库旧值X覆盖了新值Y

2.解决方案

如何解决并发环境下双写不一致的问题?

1 使用分布式式锁

在这里插入图片描述

流程:

线程1申请分布式锁,拿到锁。此时其它线程无法获取同一把锁

线程1写数据库,写缓存,操作完成释放锁

线程2申请分布锁成功,写数据库,写缓存

对双写的操作每个线程顺序执行

对操作异常问题仍需要解决:写数据库成功写缓存失败了,数据库需要回滚,此时就需要使用分布式事务组件

使用分布式锁解决双写一致性不仅性能低下,复杂度增加

2 延迟双删

既然双写操作存在不一致,我们把写缓存改为删除缓存呢?

先写数据库再删除缓存,如果删除缓存失败了缓存也就不一致了,那我们改为:先删除缓存再写数据库

在这里插入图片描述

执行流程:

线程1删除缓存

线程2读缓存发现没有数据此时查询数据库拿到旧数据写入缓存

线程1写入数据库

即使线程1删除缓存、写数据库操作后线程2再去查询缓存也可能存在问题

在这里插入图片描述

线程1向主数据库写,线程2向从数据库查询,流程如下:

线程1删除缓存

线程1向主数据库写,数据向从数据库同步

线程2查询缓存没有数据,查询从数据库,得到旧数据

线程2将旧数据写入缓存

解决上边的问题采用延迟双删:

线程1先删除缓存,再写入主数据库,延迟一定时间再删除缓存

在这里插入图片描述

上图线程1的动作简化为下图:

在这里插入图片描述

延迟多长时间呢?

延迟主数据向从数据库同步的时间间隔,如果延迟时间设置不合理也会导致数据不一致

3 异步同步

延迟双删的目的也是为了保证最终一致性,即允许缓存短暂不一致,最终保证一致性

保证最终一致性的方案有很多,比如:通过MQ、Canal、定时任务都可以实现

Canal是一个数据同步工具,读取MySQL的binlog日志拿到更新的数据,再通过MQ发送给异步同步程序,最终由异步同步程序写到redis。此方案适用于对数据实时性有一定要求的场景

通过Canal加MQ异步任务方式流程如下:

在这里插入图片描述

流程如下:

线程1写数据库

canal读取binlog日志,将数据变化日志写入mq

同步程序监听mq接收到数据变化的消息

同步程序解析消息内容写入redis,写入redis成功正常消费完成,消息从mq删除

定时任务方式流程如下:

专门启动一个数据同步任务定时读取数据同步到redis,此方式适用于对数据实时性要求不强更新不频繁的数据

在这里插入图片描述

线程1写入数据库(业务数据表,变化日志表)

同步程序读取数据库(变化日志表),根据变化日志内容写入redis,同步完成删除变化日志

小结

如何保证缓存一致性?

2.缓存实现

2.1 开通区域列表缓存实现

目标:

能说出项目中的缓存方案

实现开通区域列表缓存(完成查询缓存、删除缓存)

2.1.1 缓存方案分析

根据本项目门户的需求可知共有以下几块信息需要缓存

在这里插入图片描述

下边分析第一个开通区域列表的缓存方案:

查询缓存:查询已开通区域列表,如果没有缓存则查询数据库并将查询结果进行缓存,如果存在缓存则直接返回

启用区域:删除开通区域信息缓存(再次查询将缓存新的开通区域列表)。

禁用区域:删除开通区域信息缓存,删除该区域下的其它缓存信息,包括:首页服务列表,服务类型列表,热门服务列表。

定时任务:每天凌晨缓存已开通区域列表

2.1.2 查询缓存实现

下边我们先实现开通区域列表查询缓存

首先把测试环境准备好:

启动jzo2o-gateway、jzo2o-publics、jzo2o-customer、jzo2o-foundations,打开小程序开发工具

打开小程序,点击首页左上角的地址进入服务地址城市选择页面

在这里插入图片描述

在定位界面显示已开通城市列表,已开通城市是指在区域管理中所有启用的区域信息

在这里插入图片描述

跟踪Network找到开通区域列表的URL:/foundations/consumer/region/activeRegionList

在这里插入图片描述

打开jzo2o-foundations工程,根据接口地址找到具体的代码

找到接口:

package com.jzo2o.foundations.controller.consumer;/*** 区域表 前端控制器*/
@RestController("consumerRegionController")
@RequestMapping("/consumer/region")
@Api(tags = "用户端 - 区域相关接口")
public class RegionController {@Resourceprivate IRegionService regionService;@GetMapping("/activeRegionList")@ApiOperation("已开通服务区域列表")public List<RegionSimpleResDTO> activeRegionList() {return regionService.queryActiveRegionListCache();}}

找到service方法如下:

在这里插入图片描述

在service方法上添加Spring cache注解:

@Override
@Cacheable(value = RedisConstants.CacheName.JZ_CACHE, key = "'ACTIVE_REGIONS'", cacheManager = RedisConstants.CacheManager.FOREVER) //永不过期
public List<RegionSimpleResDTO> queryActiveRegionListCache() {return queryActiveRegionList();
}

说明:

key: 当key用一个固定字符串时需要在双引号中用单引号括起来,如下所示:key = “‘ACTIVE_REGIONS’”

cacheManager :RedisConstants.CacheManager.FOREVER设置了缓存永不过期

重启jzo2o-foundations工程进行测试,观察redis成功缓存开通区域列表

在这里插入图片描述

3.启用区域

启用一个新区域已经开通区域列表需要变更,该如何实现呢?

启用区域后删除已开通区域列表缓存,当去查询开通区域列表时重新缓存最新的开通区域列表

接口的代码如下:

在这里插入图片描述

找到service代码,修改如下:

/*** 区域管理**/
@Service
public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> implements IRegionService {/*** 区域启用* @param id 区域id*/
@Override
@CacheEvict(value = RedisConstants.CacheName.JZ_CACHE, key = "'ACTIVE_REGIONS'")
public void active(Long id) {
......
}

4.禁用区域

如果是禁用一个区域则需要删除开通区域列表缓存

禁用区域除了删除开通区域列表还需要删除首页服务列表、热门服务列表等,所以这里使用@Caching注解

找到禁用区域的代码,修改如下:

/*** 区域管理**/
@Service
public class RegionServiceImpl extends ServiceImpl<RegionMapper, Region> implements IRegionService {@Override@Caching(evict = {@CacheEvict(value = RedisConstants.CacheName.JZ_CACHE, key = "'ACTIVE_REGIONS'")
//            todo:删除首页服务列表缓存})public void deactivate(Long id) {......

小结

项目哪里进行了缓存,缓存方案是什么?

项目中如何保证缓存的一致性?

相关文章:

【云岚到家】-day03-门户缓存方案选择

【云岚到家】-day03-门户缓存方案选择 1.门户常用的技术方案 什么是门户 说到门户马上会想到门户网站&#xff0c;中国比较早的门户网站有新浪、网易、搜狐、腾讯等&#xff0c;门户网站为用户提供一个集中的、易于访问的平台&#xff0c;使他们能够方便地获取各种信息和服务…...

在IDEA中使用通义灵码插件:全面提升开发效率的智能助手

在IDEA中使用通义灵码插件&#xff1a;全面提升开发效率的智能助手 随着软件开发行业对效率和质量要求的不断提高&#xff0c;开发者们一直在寻找能够简化工作流程、提升代码质量的工具。阿里云推出的通义灵码插件正是这样一个旨在帮助开发者更高效地编写高质量代码的强大工具…...

【正则表达式】从0开始学习正则表达式

正则表达式&#xff08;英语&#xff1a;Regular Expression&#xff0c;在代码中常简写为regex、regexp或RE&#xff09; 一、推荐学习网站 正则表达式 – 语法 | 菜鸟教程 正则表达式30分钟入门教程 | 菜鸟教程 编程胶囊-打造学习编程的最好系统 二、必知必记 2.1 元字符…...

PHP智慧小区物业管理小程序

&#x1f31f;智慧小区物业管理小程序&#xff1a;重塑社区生活&#xff0c;开启便捷高效新篇章 &#x1f31f; 智慧小区物业管理小程序是一款基于PHPUniApp精心雕琢的智慧小区物业管理小程序&#xff0c;它犹如一股清新的科技之风&#xff0c;吹进了现代智慧小区的每一个角落…...

Linux安装Docker教程(详解)

如果想要系统学习docker,建议进入官方文档中学习&#xff1a;docker官方文档 一. 基本概念 Docker Desktop 和 Docker Engine 有什么区别&#xff1f; Docker Desktop for Linux 提供用户友好的图形界面&#xff0c;可简化容器和服务的管理。它包括 Docker Engine&#xff0c…...

开源AI微调指南:入门级简单训练,初探AI之路

112&#xff0c;如何让 113&#xff1f; 简单的微调你的 AI&#xff0c; 微调前的效果&#xff0c;怎么调教它都是 112. 要对其进行微调&#xff08;比如训练113&#xff09;&#xff0c;可以按以下步骤进行。 确保你已经安装了以下工具和库&#xff1a; ollamallama3.2Pyt…...

Leetcode 91. 解码方法 动态规划

原题链接&#xff1a;Leetcode 91. 解码方法 自己写的代码&#xff1a; class Solution { public:int numDecodings(string s) {int ns.size();vector<int> dp(n,1);if(s[n-1]0) dp[n-1]0;for(int in-2;i>0;i--){if(s[i]!0){string ts.substr(i,2);int tmpatoi(t.c…...

ASP .NET Core 学习(.NET9)配置接口访问路由

新创建的 ASP .NET Core Web API项目中Controller进行请求时&#xff0c;是在地址:端口/Controller名称进行访问的&#xff0c;这个时候Controller的默认路由配置如下 访问接口时&#xff0c;是通过请求方法&#xff08;GET、Post、Put、Delete&#xff09;进行接口区分的&…...

将 AzureBlob 的日志通过 Azure Event Hubs 发给 Elasticsearch(2 换掉付费的Event Hubs)

前情回顾&#xff1a; 将 AzureBlob 的日志通过 Azure Event Hubs 发给 Elasticsearch&#xff08;1&#xff09;-CSDN博客 前边的方案是挺好的&#xff0c;但 Azure Event Hubs 是付费服务&#xff0c;我这里只是一个获取日志进行必要的分析&#xff0c;并且不要求实时性&am…...

idea 如何安装 github copilot

idea 如何安装 github copilot 要在 IntelliJ IDEA 中安装 GitHub Copilot&#xff0c;可以按照以下步骤操作&#xff1a; 打开 IntelliJ IDEA: 启动 IntelliJ IDEA。 打开插件管理器: 点击菜单栏中的 File。 选择 Settings&#xff08;Windows/Linux&#xff09;或 Prefere…...

1.17学习

crypto nssctf-[SWPUCTF 2021 新生赛]crypto8 不太认识这是什么编码&#xff0c;搜索一下发现是一个UUENCODE编码&#xff0c;用在线工具UUENCODE解码计算器—LZL在线工具解码就好 misc buuctf-文件中的秘密 下载附件打开后发现是一个图片&#xff0c;应该是一个图片隐写&…...

Redis系列之底层数据结构整数集IntSet

Redis系列之底层数据结构整数集IntSet 什么是IntSet IntSet&#xff0c;整数集合&#xff0c;是Redis集合类型的一种底层数据结构&#xff0c;当一个集合只包含整数值元素&#xff0c;并且这个集合的元素数量不多时&#xff0c;redis就会选用intset作为底层实现。 IntSet的数…...

外包公司名单一览表(成都)

大家好&#xff0c;我是苍何。 之前写了一篇武汉的外包公司名单&#xff0c;评论区做了个简单统计&#xff0c;很多人说&#xff0c;在外包的日子很煎熬&#xff0c;不再想去了。 有小伙伴留言说有些外包会强制离职&#xff0c;不行就转岗&#xff0c;让人极度没有安全感。 这…...

个人vue3-学习笔记

声明:这只是我个人的学习笔记(黑马),供以后复习用 。一天学一点,随时学随时更新。明天会更好的! 这里只给代码,不给运行结果,看不出来代码的作用我也该进厂了。。。。。 Day1 使用create-vue创建项目。 1.检查版本。 node -v 2.创建项目 npm init vue@latest 可…...

STM32 FreeRTOS消息队列

队列简介 队列是任务间通信的主要形式。 它们可以用于在任务之间以及中断和任务之间发送消息。 队列是线程安全的数据结构&#xff0c;任务可以通过队列在彼此之间传递数据。有以下关键特点&#xff1a; FIFO顺序&#xff1a;队列采用先进先出 (FIFO) 的顺序&#xff0c;即先…...

Datawhale-self-llm-Phi-4 Langchain接入教程

本项目是一个围绕开源大模型、针对国内初学者、基于 AutoDL 平台的中国宝宝专属大模型教程&#xff0c;针对各类开源大模型提供包括环境配置、本地部署、高效微调等技能在内的全流程指导&#xff0c;简化开源大模型的部署、使用和应用流程&#xff0c;让更多的普通学生、研究者…...

窥探QCC518x/308x系列与手机之间的蓝牙HCI记录与分析 - 手机篇

今天要介绍给大家的是, 当我们在开发高通耳机时如果遇到与手机之间相容性问题, 通常会用Frontline或Ellisys的Bluetooth Analyzer来截取资料分析, 如果手边没有这样的仪器, 要如何窥探Bluetooth的HCI log.这次介绍的是手机篇. 这次跟QCC518x/QCC308x测试的手机是Samsung S23 U…...

Golang Gin系列-1:Gin 框架总体概述

本文介绍了Gin框架&#xff0c;探索了它的关键特性&#xff0c;并建立了简单入门的应用程序。在这系列教程里&#xff0c;我们会探索Gin的主要特性&#xff0c;如路由、中间件、数据库集成等&#xff0c;最终能使用Gin框架构建健壮的web应用程序。 总体概述 Gin是Go编程语言的…...

CF986 div2 ABCD补题

//***不知道在不在进步 A 注意点&#xff1a;其实这个暴力就行&#xff0c;但有个限制&#xff0c;就是最多走100遍如果不到那就一定到不了。其实我感觉10遍就可以了&#xff0c;但WA了。不管怎么说&#xff0c;100遍不超时而且稳对。 代码&#xff1a; #include<bits/s…...

Ubuntu 22.04 上安装和使用 ComfyUI

在 Ubuntu 22.04 上安装和使用 ComfyUI可以按照以下步骤进行&#xff1a; 安装前的准备 确保系统更新到最新 打开终端并运行&#xff1a; sudo apt update sudo apt upgrade安装 Python 3 和 pip 如果没有安装 Python 3 和 pip&#xff0c;可以通过以下命令进行安装&#xff1…...

用户中心项目教程(一)--Ant design pro初始化的学习和使用

文章目录 1.项目定位2.项目开发流程3.需求分析4.技术选型5.Ant design pro初始化5.1快速使用5.2初始化过程 6.项目依赖的报错处理6.1项目出现的问题6.2怎么查看问题6.3怎么解决报错6.4关于pnpm的安装 7.项目启动和运行7.1项目如何启动7.2双击跳转7.3登录和注册7.4页面分析7.5关…...

分频器code

理论学习 数字电路中时钟占有非常重要的地位。时间的计算都依靠时钟信号作为基本单元。一般而言&#xff0c;一块板子只有一个晶振&#xff0c;即只有一种频率的时钟&#xff0c;但是数字系统中&#xff0c;经常需要对基准时钟进行不同倍数的分频&#xff0c;进而得到各模块所需…...

C#中字符串方法

字符串属性&#xff1a;Lenght 长度比最大索引大1 string str "frerfgd"; 1.可以通过索引&#xff0c;获取字符串中的某一个字符&#xff0c;下标“0&#xff0c;1.......” Console.WriteLine(str[0]);//f Console.WriteLine(str[1]);//r //Console.WriteLine(s…...

Python毕业设计选题:基于django+vue的二手电子设备交易平台设计与开发

开发语言&#xff1a;Python框架&#xff1a;djangoPython版本&#xff1a;python3.7.7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 管理员登录 管理员功能界面 用户管理 设备类型管理 设备信息管理 系统首页 设备信息…...

【愚公系列】《微信小程序与云开发从入门到实践》059-迷你商城小程序的开发(加入购物车与创建订单功能开发)

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…...

Mac Android studio :gradle 配置、代理设置、及各种环境变量配置;

mac 安装 AS&#xff0c;最烦人的就是这些配置了&#xff08;吐槽一下&#xff1a;这软件真是垃圾的一批&#xff0c;同样的代码换了电脑就没法用&#xff0c;比 vscode 甚至比低评分的xcode还差劲&#xff01;&#xff09; --------------------- 一、 gradle 下载及环境变量…...

unity——Preject3——开始界面拼面板

目录 1.创建panel&#xff0c;去掉panel自带的image&#xff0c;自己加一个image&#xff0c;使用锚点分配好 2.锚点&#xff08;快捷键点击后 ALTShift&#xff09; 锚点是什么&#xff1f; 锚点的实际例子 例子1&#xff1a;固定在父容器的中心 例子2&#xff1a;对齐到…...

【达梦数据库(Oracle模式)】如何将视图中的数据导出

在某些情况下&#xff0c;我们需要将生产环境某个模式下的数据导入到开发电脑中&#xff0c;因为正式环境无法连接外网数据。 方式一&#xff1a;将视图查询出来&#xff0c;然后右键导出所有查询结果&#xff08;不推荐&#xff09; 优点&#xff1a;方便快捷 缺点&#xff1…...

GB44495-2024 汽车整车信息安全技术要求 - V2X部分前置要求

背景 GB 44495-2024《汽车整车信息安全技术要求》中关于V2X&#xff08;车与外界通信&#xff09;的部分&#xff0c;主要关注于通信安全要求&#xff0c;旨在确保车辆在与外部设备进行数据交互时的信息安全。其测试大致可分为消息层&#xff08;数据无异常&#xff09;、应用…...

FastAPI 应用的容器化与 Docker 部署:提升性能与可扩展性

FastAPI 应用的容器化与 Docker 部署&#xff1a;提升性能与可扩展性 目录 &#x1f433; 使用 Docker 容器化 FastAPI 应用⚙️ 使用 Docker Compose 管理多个服务的部署&#x1f680; 在 Docker 容器中部署与运行 FastAPI 应用 1. &#x1f433; 使用 Docker 容器化 FastAPI…...