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

embedded/2025/1/17 23:15:29/

【云岚到家】-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) {......

小结

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

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


http://www.ppmy.cn/embedded/154790.html

相关文章

java day04-面向对象基础(内存 封装 继承 修饰符 工具类 )

1.对象内存图 1.1 Java 内存分配 1.2 堆和栈 栈:所有局部变量都会在栈内存中创建 局部变量&#xff1a;定义在方法中的变量或者方法声明上的变量 方法执行都会加载到栈中进行 -----------------------------------------------------------------------------------------…

工业视觉5-工业视觉选型

工业视觉5-工业视觉选型 任务分析三、知识准备问答四、相机选型五、总结 任务分析 重点明确任务要求 例子&#xff1a; 检测任务类型 外观检测&#xff1a;检查产品表面是否有划痕、污渍、缺陷等。例如&#xff0c;在电子元件生产中&#xff0c;需要检测芯片表面的瑕疵&…

【数据结构】第二章:线性表

本篇笔记课程来源&#xff1a;王道计算机考研 数据结构 【数据结构】第二章&#xff1a;线性表 一、线性表的定义和基本操作1. 定义2. 基本操作 二、顺序表1. 顺序表的定义2. 顺序表的实现3. 顺序表的特点4. 顺序表的插入5. 顺序表的删除6. 顺序表的查找 三、单链表1. 单链表的…

如何安装cnpm

今天尝试用npm install安装一个项目的依赖&#xff0c;但是无论如何都不能完成&#xff0c;等待时间非常久&#xff0c;所以同事推荐了cnpm&#xff0c;确实非常好用&#xff0c;所以推荐了出来&#xff0c;希望能给大家带来帮助。 cnpm 是中国淘宝团队提供的一个 npm 镜像工具…

Linux安装docker,安装配置xrdp远程桌面

Linux安装docker&#xff0c;安装配置xrdp远程桌面。 1、卸载旧版本docker 卸载旧版本docker命令 yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine现在就是没有旧版本的d…

告别虚拟机!WSL2安装配置教程!!!

作者&#xff1a;SkyXZ CSDN&#xff1a;SkyXZ&#xff5e;-CSDN博客 博客园&#xff1a;SkyXZ - 博客园 由于Linux的系统的稳定以及在环境管理方面的优越性&#xff0c;同时Linux对于ROS系统的独占&#xff0c;很多时候我们都乐意在Linux系统下开发我们机器人的算法&#xff0…

使用 Kubernetes 实现负载均衡

使用 Kubernetes 实现负载均衡&#xff0c;可以通过 Kubernetes 的内置服务&#xff08;Service&#xff09;资源&#xff0c;配合负载均衡器&#xff08;如云平台提供的负载均衡器或 Ingress 控制器&#xff09;来完成。以下是详细的步骤和调优案例。 一、Kubernetes 负载均衡…

FPGA随记——时钟时序一些基本知识

原文链接&#xff1a;跨时钟域设计-CSDN博客 前言 CDC&#xff08;clock domain crossing&#xff09;检查&#xff08;跨时钟域的检查&#xff09;是对电路设计中同步电路设计的检查。非同步时钟没有固定的相位关系&#xff0c;这样Setup/Hold不满足而产生了亚稳态是无法避免…