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

手写分布式配置中心(四)增加实时刷新功能(长轮询)

上一篇文章中实现了短轮询,不过短轮询的弊端也很明显,如果请求的频率较高,那么就会导致服务端压力大(并发高);如果请求的频率放低,那么客户端感知变更的及时性就会降低。所以我们来看另一种轮询方式,长轮询。
长轮询就是客户端发起请求,如果服务端的数据没有发生变更,那么就hold住请求,直到服务端的数据发生了变更,或者达到了一定的时间就会返回。这样就减少了客户端和服务端不断频繁连接和传递数据的过程,并且不会消耗服务端太多资源,而且客户端感知变更的及时性也会大大提高

代码在https://gitee.com/summer-cat001/config-center​​​​​​​

原理

要实现服务端长时间hold请求,就要利用到servlet异步的特性,因为web服务器会有一个线程池,每一个请求来了之后会提交给这个线程池去处理请求,如果一个任务很长时间都没完成的话就会一直占有这个线程,那么其他请求来了会发现线程池里没有可用的线程就会一直等,直到有空闲的线程,这样就会导致并发性大大的减少。所以需要采用异步响应的方式去实现,而比较方便实现异步http的方式就是Servlet3.0提供的AsyncContext 机制。asyncContext是为了把主线程返回给web服务器的线程池,不影响服务对其他客户端请求。会有线程专门处理这个长轮询,但并不是说每一个长轮询的http请求都要用一个线程阻塞在那。而是把长轮询的request的引用在一个集合中存起来,用一个或几个线程专门处理一批客户端的长轮询请求,这样就不需要为每一个长轮询单独分配线程阻塞在那了,从而大大降低了资源的消耗。注意,异步不是非阻塞,响应数据时还是要阻塞的。

服务端

服务端增加一个长轮询的接口

@PostMapping("/change/get/long")public Result<Void> getLongChangeConfig(@RequestBody Map<Long, Integer> configIdMap, HttpServletRequest request, HttpServletResponse response) {if (configIdMap == null || configIdMap.isEmpty()) {return Result.fail("配置参数错误");}response.setContentType("application/json;charset=UTF-8");AsyncContext asyncContext = request.startAsync();asyncContext.setTimeout(0);ConfigPolingTask configPolingTask = new ConfigPolingTask();configPolingTask.setAsyncContext(asyncContext);configPolingTask.setConfigPolingDataMap(configIdMap);configPolingTask.setEndTime(System.currentTimeMillis() + 28 * 1000);configService.configListener(configPolingTask);return null;}

主要就是把请求的配置id和版本的map、超时时间、asyncContext对象组装成一个任务,添加到任务池里,如有更新了配置,会去任务池里找是否有该配置id的任务,如果版本号大于任务的版本号,就将新配置返回给客户端。于此同时会有1个定时线程每1秒访问一下任务池,找到过期的任务,返回给客户端。客户端的请求过期时间是30秒,服务端过期时间定的是28秒,也就是配置没有改变的情况下,会hold请求28秒才返回,提前2秒返回是为了防止返回传输时间导致超过30秒,客户端断开链接。
 

@Slf4j
@Service
public class ConfigServiceImpl implements ConfigService {private ConfigDAO configDAO;private ConfigSyncService configSyncService;@Autowiredprivate LocalConfigDAO localConfigDAO;@Autowiredprivate LocalConfigSyncServiceImpl localConfigSyncService;@Value("${config.center.mode:0}")private int configCenterMode;private int respThreadNum;private final ExecutorService respExecutor;private final ConfigPolingTasksHolder configPolingTasksHolder;public ConfigServiceImpl() {configPolingTasksHolder = new ConfigPolingTasksHolder();//构建用于响应长轮询的线程池respExecutor = new ThreadPoolExecutor(100, 5000,0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(102400),this::newRespThread,new ThreadPoolExecutor.CallerRunsPolicy());//每1秒轮询执行一次任务超时检测ScheduledExecutorService timeoutCheckExecutor = new ScheduledThreadPoolExecutor(1, this::newCheckThread);timeoutCheckExecutor.scheduleAtFixedRate(this::responseTimeoutTask, 0, 1, TimeUnit.SECONDS);}@PostConstructpublic void init() {ConfigCenterModeEnum configCenterModeEnum = ConfigCenterModeEnum.getEnum(configCenterMode);if (configCenterModeEnum == null) {throw new IllegalArgumentException("配置config.center.mode错误");}if (configCenterModeEnum == ConfigCenterModeEnum.STANDALONE) {this.configDAO = localConfigDAO;this.configSyncService = localConfigSyncService;}}@Overridepublic Result<Void> insertConfig(ConfigBO configBO) {List<ConfigDO> configList = configDAO.getAllValidConfig();if (configList.stream().anyMatch(c -> c.getName().equals(configBO.getName()))) {return Result.fail("配置名重复");}ConfigDO configDO = new ConfigDO();configDO.setName(configBO.getName());configDO.setConfigData(configBO.getConfigData().toJSONString());configDAO.insertConfigDO(configDO);return Result.success(null);}@Overridepublic Result<Void> updateConfig(ConfigBO configBO) {ConfigDO configDO = new ConfigDO();configDO.setId(configBO.getId());configDO.setName(configBO.getName());configDO.setConfigData(configBO.getConfigData().toJSONString());configDAO.updateConfig(configDO);configSyncService.publish(configBO.getId());return Result.success(null);}@Overridepublic Result<Void> delConfig(long id, long updateUid) {configDAO.delConfig(id, updateUid);return Result.success(null);}@Overridepublic Result<List<ConfigBO>> getAllValidConfig() {List<ConfigDO> configList = configDAO.getAllValidConfig();return Result.success(configList.stream().map(this::ConfigDO2BO).collect(Collectors.toList()));}@Overridepublic void configListener(ConfigPolingTask configPolingTask) {//先将任务加到待响应列表中,然后再判断账号是否有改变,防止并发问题//如先判断再加进去,加入前如有变动,任务里无法感知到,空等到超时configPolingTasksHolder.addConfigTask(configPolingTask);List<ConfigBO> allValidConfig = getAllValidConfig().getData();List<ConfigVO> changeConfigList = getChangeConfigList(configPolingTask, allValidConfig);if (!changeConfigList.isEmpty()) {List<ConfigPolingTask> todoTask = configPolingTasksHolder.getExecuteTaskList(configPolingTask::equals);if (!todoTask.isEmpty()) {doResponseTask(configPolingTask, Result.success(changeConfigList));}}}@Overridepublic void onChangeConfigEvent(long configId) {List<ConfigPolingTask> todoTasks = configPolingTasksHolder.getExecuteTaskList(configPolingTask -> configPolingTask.getConfigPolingDataMap().containsKey(configId));if (!todoTasks.isEmpty()) {List<ConfigBO> configList = Collections.singletonList(ConfigDO2BO(configDAO.getConfig(configId)));todoTasks.forEach(todoTask -> {List<ConfigVO> changeConfigList = getChangeConfigList(todoTask, configList);respExecutor.submit(() -> doResponseTask(todoTask, Result.success(changeConfigList)));});}}private List<ConfigVO> getChangeConfigList(ConfigPolingTask configPolingTask, List<ConfigBO> configList) {Map<Long, Integer> configPolingDataMap = configPolingTask.getConfigPolingDataMap();return configList.stream().filter(configBO -> configPolingDataMap.containsKey(configBO.getId())).filter(configBO -> configBO.getVersion() > configPolingDataMap.get(configBO.getId())).map(ConfigServiceImpl::configBO2ConfigVO).collect(Collectors.toList());}private ConfigBO ConfigDO2BO(ConfigDO configDO) {ConfigBO configBO = new ConfigBO();configBO.setId(configDO.getId());configBO.setName(configDO.getName());configBO.setVersion(configDO.getVersion());configBO.setCreateTime(configDO.getCreateTime());configBO.setConfigData(JSON.parseObject(configDO.getConfigData()));return configBO;}//响应超时未改变的任务private void responseTimeoutTask() {List<ConfigPolingTask> timeoutTasks = configPolingTasksHolder.getExecuteTaskList(configPolingTask -> System.currentTimeMillis() >= configPolingTask.getEndTime());timeoutTasks.forEach(timeoutTask -> respExecutor.submit(() ->doResponseTask(timeoutTask, Result.success(new ArrayList<>()))));}private void doResponseTask(ConfigPolingTask configPolingTask, Result<?> result) {AsyncContext asyncContext = configPolingTask.getAsyncContext();try (PrintWriter writer = asyncContext.getResponse().getWriter()) {writer.write(JSON.toJSONString(result));writer.flush();} catch (Exception e) {log.error("doResponseTimeoutTask error,task:{}", configPolingTask, e);} finally {asyncContext.complete();}}private Thread newCheckThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);t.setName("ConfigLongPollingTimeoutCheckExecutor");return t;}private Thread newRespThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);t.setName("ConfigLongPollingTimeoutRespExecutor-" + respThreadNum++);return t;}public static ConfigVO configBO2ConfigVO(ConfigBO configBO) {ConfigVO configVO = new ConfigVO();configVO.setId(configBO.getId());configVO.setName(configBO.getName());configVO.setVersion(configBO.getVersion());configVO.setConfigData(configBO.getConfigData());configVO.setCreateTime(DateUtil.date2str1(configBO.getCreateTime()));return configVO;}
}
public class ConfigPolingTasksHolder {private final List<ConfigPolingTask> configPolingTasks;public ConfigPolingTasksHolder() {configPolingTasks = new ArrayList<>();}public synchronized void addConfigTask(ConfigPolingTask configPolingTask) {configPolingTasks.add(configPolingTask);}//将要处理的任务在任务列表中删除,并将其放到外面执行,防止锁的时间太长public synchronized List<ConfigPolingTask> getExecuteTaskList(Predicate<ConfigPolingTask> predicate) {List<ConfigPolingTask> resultTasks = new ArrayList<>();configPolingTasks.removeIf(configPolingTask -> {boolean res = predicate.test(configPolingTask);if (res) {resultTasks.add(configPolingTask);}return res;});return resultTasks;}
}
@Data
public class ConfigPolingTask {/*** 截止时间*/private long endTime;/*** 异步请求*/private AsyncContext asyncContext;/*** 配置轮询数据(配置id,版本)*/private Map<Long, Integer> configPolingDataMap;
}

客户端

客户端就很简单了,只要循环发一个超时时间是30秒的http请求就行

public void startLongPolling() {polling("/config/change/get/long", null, 30000);}public void polling(String uri, Runnable runnable, int readTimeout) {Thread thread = new Thread(() -> {while (!Thread.interrupted()) {try {Optional.ofNullable(runnable).ifPresent(Runnable::run);Map<Long, List<ConfigDataBO>> refreshConfigMap = new HashMap<>();configMap.values().forEach(configBO -> {Optional.ofNullable(configBO.getConfigDataList()).ifPresent(cdList -> cdList.stream().filter(cd -> cd.getRefreshFieldList() != null && !cd.getRefreshFieldList().isEmpty()).forEach(refreshConfigMap.computeIfAbsent(configBO.getId(), k1 -> new ArrayList<>())::add));});if (refreshConfigMap.isEmpty()) {return;}Map<String, Integer> configIdMap = refreshConfigMap.keySet().stream().collect(Collectors.toMap(String::valueOf, configId -> configMap.get(configId).getVersion()));HttpRespBO httpRespBO = HttpUtil.httpPostJson(url + uri, JSON.toJSONString(configIdMap), readTimeout);List<ConfigVO> configList = httpResp2ConfigVOList(httpRespBO);if (configList.isEmpty()) {continue;}configList.forEach(configVO -> {Map<String, Object> result = new HashMap<>();DataTransUtil.buildFlattenedMap(result, configVO.getConfigData(), "");ConfigBO configBO = this.configMap.get(configVO.getId());configBO.setVersion(configVO.getVersion());List<ConfigDataBO> configDataList = configBO.getConfigDataList();Map<String, ConfigDataBO> configDataMap = configDataList.stream().collect(Collectors.toMap(ConfigDataBO::getKey, Function.identity()));result.forEach((key, value) -> {ConfigDataBO configDataBO = configDataMap.get(key);if (configDataBO == null) {configDataList.add(new ConfigDataBO(key, value.toString()));} else {configDataBO.setValue(value.toString());List<RefreshFieldBO> refreshFieldList = configDataBO.getRefreshFieldList();if (refreshFieldList == null) {refreshFieldList = new ArrayList<>();configDataBO.setRefreshFieldList(refreshFieldList);}refreshFieldList.forEach(refreshFieldBO -> {try {Field field = refreshFieldBO.getField();field.setAccessible(true);field.set(refreshFieldBO.getBean(), value.toString());} catch (Exception e) {log.error("startShortPolling set Field error", e);}});}});});} catch (Exception e) {log.error("startShortPolling error", e);}}});thread.setName("startShortPolling");thread.setDaemon(true);thread.start();}private List<ConfigVO> httpResp2ConfigVOList(HttpRespBO httpRespBO) {if (!httpRespBO.success()) {throw new IllegalArgumentException("获取配置失败:code:" + httpRespBO.getCode() + ",msg:" + httpRespBO.getMessage());}if (httpRespBO.getBody() == null) {throw new IllegalArgumentException("获取配置失败 body is null:code:" + httpRespBO.getCode() + ",msg:" + httpRespBO.getMessage());}Result<?> result = JSON.parseObject(new String(httpRespBO.getBody(), StandardCharsets.UTF_8), Result.class);if (result.failed()) {throw new IllegalArgumentException("获取配置失败 result:" + result);}return JSON.parseArray(JSON.toJSONString(result.getData()), ConfigVO.class);}
public class ClientTest {private String userName;private String userAge;private List<Object> education;public ClientTest() throws NoSuchFieldException {ConfigCenterClient configCenterClient = new ConfigCenterClient("http://localhost:8088");Map<String, String> configProperty = configCenterClient.getConfigProperty();this.userName = configProperty.get("user.name");this.userAge = configProperty.get("user.age");this.education = new ArrayList<>();int i = 0;while (configProperty.containsKey("user.education[" + i + "]")) {education.add(configProperty.get("user.education[" + (i++) + "]"));}configCenterClient.addRefreshField("user.name", new RefreshFieldBO(this, ClientTest.class.getDeclaredField("userName")));configCenterClient.startLongPolling();}public String toString() {return "姓名:" + userName + ",年龄:" + userAge + ",教育经历:" + education;}public static void main(String[] args) throws NoSuchFieldException, InterruptedException {ClientTest clientTest = new ClientTest();while (!Thread.interrupted()) {System.out.println(clientTest);Thread.sleep(1000);}}
}

效果

相关文章:

手写分布式配置中心(四)增加实时刷新功能(长轮询)

上一篇文章中实现了短轮询&#xff0c;不过短轮询的弊端也很明显&#xff0c;如果请求的频率较高&#xff0c;那么就会导致服务端压力大&#xff08;并发高&#xff09;&#xff1b;如果请求的频率放低&#xff0c;那么客户端感知变更的及时性就会降低。所以我们来看另一种轮询…...

03 | 事务隔离:为什么你改了我还看不见?

提到事务&#xff0c;你肯定不陌生&#xff0c;和数据库打交道的时候&#xff0c;我们总是会用到事务。最经典的例子就是转账&#xff0c;你要给朋友小王转 100 块钱&#xff0c;而此时你的银行卡只有 100 块钱。 转账过程具体到程序里会有一系列的操作&#xff0c;比如查询余…...

Jmeter读取与使用Redis数据

Jmeter 作为当前非常受欢迎的接口测试和性能测试的工具&#xff0c;在企业中得到非常广泛的使用&#xff0c;而 Redis 作为缓存数据库&#xff0c;也在企业中得到普遍使用&#xff0c; Redis服务和客户端安装 windows下安装 默认端口&#xff1a;6379 下载地址&#xff1a; …...

flask 支持跨域访问 非常简单的方式 flask_cors

安装 pip install -U flask-cors from flask import Flask from flask_cors import CORSapp Flask(__name__) CORS(app)app.route("/") def helloWorld():return "Hello, cross-origin-world!"参考 https://www.cnblogs.com/anxminise/p/9814326.html …...

Hololens 2应用开发系列(1)——使用MRTK在Unity中设置混合现实场景并进行程序模拟

Hololens 2应用开发系列&#xff08;1&#xff09;——使用MRTK在Unity中进行程序模拟 一、前言二、创建和设置MR场景三、MRTK输入模拟的开启 一、前言 在前面的文章中&#xff0c;我介绍了Hololens 2开发环境搭建和项目生成部署等相关内容&#xff0c;使我们能生成一个简单Ho…...

Newtonsoft.Json

目录 引言 1、简单使用 1.1、官方案例 1.2、JsonConvert 2、特性 2.1、默认模式[JsonObject(MemberSerialization.OptIn/OptOut)] 2.2、序列化为集合JsonArrayAttribute/JsonDictionaryAttribute 2.3、序列化该元素JsonProperty 2.4、忽略元素JsonIgnoreAttribute 2.5、…...

速卖通平台的API返回结果有哪些数据字段?

速卖通&#xff08;AliExpress&#xff09;作为阿里巴巴旗下的国际电商平台&#xff0c;提供了API接口供开发者使用&#xff0c;以获取商品、订单、物流等各种信息。然而&#xff0c;速卖通API返回的具体数据字段可能会随着API版本、接口类型以及时间的变化而有所不同。 在编写…...

C++ 标准模板库(STL)

1、vector 动态数组&#xff0c;可随时添加删除元素&#xff0c;在堆空间开辟内存。 方法含义front() 返回第一个元素O(1) back()返回最后一个元素O(1)pop_back()删除最后一个元素O(1)push_back(ele)在末尾插入元素O(1)size()返回实际元素个数O(1)clear()清除所有元素O(N)resi…...

【Javascript】设计模式之发布订阅模式

文章目录 1、现实中的发布&#xff0d;订阅模式2、DOM 事件3、简单的发布-订阅模式4、通用的发布-订阅模式5、先发布再订阅6、小结 发布—订阅模式又叫观察者模式&#xff0c;它定义对象间的一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于…...

DataLoader

import torchvision from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter# 准备的测试数据集 数据放在了CIFAR10文件夹下test_data torchvision.datasets.CIFAR10("./CIFAR10",trainFalse, transformtorchvision.transfor…...

持续集成(CICD)- Jenkins+Git+gogs综合实战(笔记二)

文章目录 七、拉取代码方法一:直接填写命令进行拉取(不建议用这种)方法二:使用源码管理拉取代码步骤一:确认环境(检查自己是否有Git插件)步骤二:构建项目时对项目的源码管理选择 Git步骤三:输入你仓库的SSH地址或者https地址,并且添加gitee的用户名和密方法一和方法二…...

VUE:key属性的作用

在 Vue.js 中&#xff0c;key属性的主要作用是帮助 Vue 在进行 DOM 更新时&#xff0c;能够更准确地识别哪些节点可以复用。 当key值发生变化时&#xff0c;Vue 会执行以下步骤&#xff1a; 1.查找旧节点&#xff1a;Vue 会查找虚拟 DOM 中具有旧key值的节点。 2.匹配新节点…...

linux的通信方案(SYSTEM V)

文章目录 共享内存(Share Memory)信号队列&#xff08;Message Queue&#xff09;信号量(semaphore) 进程间通信的核心理念&#xff1a;让不同的进程看见同一块资源 linux下的通信方案&#xff1a; SYSTEM V 共享内存(Share Memory) 特点&#xff1a;1.共享内存是进程见通信最…...

VUE 入门及应用 ( 路由 router )

6.前端路由 router Vue Router | Vue.js 的官方路由 (vuejs.org) 官方地址 : https://router.vuejs.org/zh/ 6.1.基本配置 6.1.0.准备 MyPage.vue 创建 用于测试 vue文件 ../views/MyPage.vue <template><div><h1>MyPage</h1></div> </…...

SpringBoot集成RocketMQ

RocketMQ是一个纯Java、分布式、队列模型的开源消息中间件&#xff0c;前身是MetaQ&#xff0c;是阿里参考Kafka特点研发的一个队列模型的消息中间件&#xff0c;后开源给apache基金会成为了apache的顶级开源项目&#xff0c;具有高性能、高可靠、高实时、分布式特点。 环境搭…...

【Web】关于FastJson反序列化开始前的那些前置知识

目录 FastJson介绍 FJ序列化与反序列化方法 关于反序列化三种方式的关系与区别 FastJson反序列化漏洞原理通识 关于getter&setter FastJson介绍 FastJson&#xff08;快速JSON&#xff09;是一个Java语言编写的高性能、功能丰富且易于使用的JSON解析和序列化库。它由…...

工业镜头的重要参数之视场、放大倍率、芯片尺寸--51camera

今天来简单介绍下工业镜头中常用的参数中的三个&#xff1a; 1、视场 视场&#xff08;FOV&#xff09;也称视野,是指能被视觉系统观察到的物方可视范围。 对于镜头而言&#xff0c;可观察到的视场跟镜头放大倍率及相机芯片选择有关。因此需要根据被观察物体的尺寸&#xff…...

基于java springboot+redis网上水果超市商城设计和实现以及文档

基于java springbootredis网上水果超市商城设计和实现以及文档 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留…...

3. 在Go语言项目中使用Zap日志库

文章目录 一、介绍二、 默认的Go Logger1. 实现Go Logger2. 设置Logger3. 使用Logger4. Logger的运行5. Go Logger的优势和劣势 三、Uber-go Zap1. 为什么选择Uber-go zap2. 安装3. 配置Zap Logger4. 定制logger4.1 将日志写入文件而不是终端4.2 将JSON Encoder更改为普通的Log…...

想要节省成本,哪个品牌的https证书值得考虑?

为了确保网站数据传输安全&#xff0c;启用HTTPS加密是关键步骤。在众多SSL证书供应商中&#xff0c;如何找到价格合理且品质优良的HTTPS加密证书呢&#xff1f;本文将探讨这个问题&#xff0c;并重点关注具有高性价比优势的沃通CA。 沃通CA作为业内知名的SSL证书服务商&#x…...

R语言及其开发环境简介

R语言及其开发环境简介 R 语言历史 R 语言来自 S 语言&#xff0c;是 S 语言的一个变种。S语言由贝尔实验室研究开发&#xff0c;著名的 C 语言、Unix 系统也是贝尔实验室开发的。R 属于 GNU 开源软件&#xff0c;最初发布于1997年&#xff0c;实现了与 S 语言基本相同的功能…...

部署DNS解析服务

一、安装软件&#xff0c;关闭防火墙&#xff0c;启动服务 1.yum install -y bind bind-utils bind-chroot 2.systemctl stop firewalld && setenforce 0 3.systemctl start named 二、工作目录 /var/named/chroot/etc #存放主配置文件 /var/named/chroot/var/n…...

2024新算法:鹅算法优化VMD参数,五种适应度函数任意切换,最小包络熵、样本熵、信息熵、排列熵、排列熵/互信息熵...

本期采用鹅算法优化一下VMD参数。利用MATLAB官方自带的VMD函数。 替换为官方自带的VMD函数后&#xff0c;寻优速度真的大幅度提升&#xff01;数据量大的不妨都试试这个官方的VMD函数。当然要下载2020a以上的MATLAB才可以哦&#xff01; 同样以西储大学数据集为例&#xff0c;选…...

自定义注解校验

在日常开发中经常会用到String类型的数据当作数值进行映射&#xff0c;势必会做出数值范围的校验&#xff0c;可以通过自定义注解的办法简化代码实现&#xff0c;减少冗余代码。 Target({ElementType.FIELD}) Retention(RetentionPolicy.RUNTIME) Constraint(validatedBy St…...

由数据范围反推算法复杂度以及算法内容

一般ACM或者笔试题的时间限制是1秒或2秒。 在这种情况下&#xff0c;C代码中的操作次数控制在 1 0 7 ∼ 1 0 8 10^7\sim10^8 107∼108为最佳。 下面给出在不同数据范围下&#xff0c;代码的时间复杂度和算法该如何选择&#xff1a; n ≤ 30 n\leq30 n≤30&#xff0c;指数级别…...

js监听F11触发全屏事件

当用户使用 F11 键进行浏览器全屏时&#xff0c;由于此时并非通过浏览器提供的 Fullscreen API 进入全屏模式&#xff0c;因此无法通过 fullscreenchange 事件来监听全屏状态的变化。在这种情况下&#xff0c;可以通过监听 resize 事件来检测浏览器窗口大小的变化&#xff0c;从…...

Seata 2.x 系列【1】专栏导读

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Spring Boot 版本 3.1.0 本系列Seata 版本 2.0.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-seata-demo 文章目录 1. 背景2. 简介3. 适用人群4. 环境及版本5. 文章导航5…...

fly-barrage 前端弹幕库(3):滚动弹幕的设计与实现

项目官网地址&#xff1a;https://fly-barrage.netlify.app/&#xff1b; &#x1f451;&#x1f40b;&#x1f389;如果感觉项目还不错的话&#xff0c;还请点下 star &#x1f31f;&#x1f31f;&#x1f31f;。 Gitee&#xff1a;https://gitee.com/fei_fei27/fly-barrage&a…...

Mysql面试总结

基础 1. 数据库的三范式是什么&#xff1f; 第一范式&#xff1a;强调的是列的原子性&#xff0c;即数据库表的每一列都是不可分割的原子数据项。第二范式&#xff1a;要求实体的属性完全依赖于主关键字。所谓完全 依赖是指不能存在仅依赖主关键字一部分的属性。第三范式&…...

【深圳五兴科技】Java后端面经

本文目录 写在前面试题总览1、java集合2、创建线程的方式3、对spring的理解4、Spring Boot 和传统 Spring 框架的一些区别5、springboot如何解决循环依赖6、对mybatis的理解7、缓存三兄弟8、接口响应慢的处理思路9、http的状态码 写在前面 关于这个专栏&#xff1a; 本专栏记录…...