多线程优化API请求:CountDownLatch与PriorityBlockingQueue的应用
目录
前言
CountDownLatch是什么?
PriorityBlockingQueue是什么?
场景描述
解决方案
定义统一工厂制造类
定义制造厂
定义客户请求实现
定义控制器
定义启动类
结果呈现
启动项目
请求制造操作
总结
前言
写这篇文章的缘由是因为之前在面试期间经常被提到的一个场景题,“前端向后端发起一个API请求,该API需要处理复杂的业务逻辑,涉及多个相互独立的业务模块。每个业务模块都需要执行特定的操作,且这些操作彼此之间没有依赖关系。然而,每个模块的处理都需要一定的时间,导致整体的接口响应时间较长,请给出优化接口的方案,而且结果必须通过当前接口返回”。或许大家立马想到的都是通过多线程或者通过队列异步来完成,结果延迟返回,问题的难点在于怎么能在当前接口返回最终的结果呢?在学习完RocketMq源码后我找到了最佳方案。多线程(Runnable)结合CountDownLatch以及PriorityBlockingQueue就是答案。
CountDownLatch是什么?
CountDownLatch 是 Java 并发工具库中的一个类,用于同步一个或多个线程,确保某些操作在其他操作完成之前不会继续执行。它能够使一个线程等待其他线程完成各自的工作后再继续执行。
读完
CountDownLatch的概述我们大概能猜出它在方案中的作用了吧,描述得也很清晰了。就是在场景是主线程启动了多个工作线程,并等待所有工作线程完成工作后再继续。
主要方法
void await():使当前线程等待,直到计数到达零释放。boolean await(long timeout, TimeUnit unit):使当前线程等待,直到计数到达零或者等待超时才释放。void countDown():递减计数,如果计数到达零,则释放所有等待的线程。long getCount():返回当前计数。
PriorityBlockingQueue是什么?
PriorityBlockingQueue 是 Java 并发包 (java.util.concurrent) 提供的一个线程安全的无界优先级队列。它结合了优先级队列和阻塞队列的特点,在多线程环境下非常有用。默认使用元素的自然顺序(通过实现 Comparable 接口的 compareTo 方法)。你也可以通过提供自定义的 Comparator 实现定制排序逻辑。
在本次方案中就是作为一个内存队列,通知其他独立的业务模块执行操作。
主要方法
boolean add(E e)/boolean offer(E e): 插入指定元素,返回true表示插入成功。E take(): 检索并移除队列的头部元素,如果队列为空,则等待直到有元素可用。E poll(long timeout, TimeUnit unit): 检索并移除队列的头部元素,如果队列为空,则等待指定的时间。E peek(): 检索但不移除队列的头部元素,如果队列为空,则返回null。int size(): 返回队列中的元素数量。
场景描述
根据上面问题,我们模拟一个场景:一个客户订购了一架飞机、一艘轮船、一辆汽车,但是飞机、轮船、汽车各自的工厂都相隔很远,客户想以最短的时间同时拥有它们,但生性多疑的他,为了防止制造厂商偷工减料他提出想亲自监工这三个交通工具的制造。请问客户与制造厂该怎么配合才能在客户亲自监工的情况下又能最快时间的同时拥有汽车、轮船和飞机呢?
- 一架飞机的制造时间为4s
- 一艘轮船的制造时间为3s
- 一辆汽车的制造时间为2s
方案一:传统方案就是,客户先去飞机厂通知开始制造飞机并现场监督,飞机制造完成后再去轮船厂其次再去汽车厂(顺序任意),抛出中间路程的时间,总共用时至少得 4+3+2=9s,时间太长客户不接受。

方案二:客户通过手机同时给飞机、轮船以及汽车厂发生消息通知他们开始制造,并且在三个厂商制造间安装监控,客户通过同时监督它们制造,因为飞机制造时间最长,那么当飞机制造完成,轮船和汽车肯定也已经完成了,那么总共用时最快4s,完美解决。

解决方案
GitHub源码地址:点击获取
首先通过idea创建一个springboot项目
定义统一工厂制造类
每个工厂都有制造方法FactoryService
public interface FactoryService {//制造boolean factoryOfManufacture();
}
定义制造厂
飞机、轮船、汽车都是独立的制造厂家,所以需要三个独立的线程来处理.
定义公共抽象线程类ServiceThread并实现多线程Runnablel类的启动方法start()
@Slf4j
public abstract class ServiceThread implements Runnable{protected Thread thread;protected boolean isDaemon = false;//Make it able to restart the threadprivate final AtomicBoolean started = new AtomicBoolean(false);public ServiceThread() {}//获取线程名称public abstract String getServiceName();//线程启动方法public void start() {log.info("Try to start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread);if (!started.compareAndSet(false, true)) {return;}this.thread = new Thread(this, getServiceName());this.thread.setDaemon(isDaemon);this.thread.start();log.info("Start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread);}
}
定义飞机厂类AirPlaneService基础线程类ServiceThread以及实现工厂类FactoryService
@Service
@Slf4j
public class AirPlaneService extends ServiceThread implements FactoryService {public static ConcurrentMap<String, AirplaneRequest> requestTable =new ConcurrentHashMap<>();//接收制造请求通知public static PriorityBlockingQueue<AirplaneRequest> requestQueue =new PriorityBlockingQueue<>();//请求通知静态内部类对象public static class AirplaneRequest implements Comparable<AirplaneRequest> {//线程完成设置为0private CountDownLatch countDownLatch = new CountDownLatch(1);//用户idprivate String userId;public CountDownLatch getCountDownLatch() {return countDownLatch;}public void setCountDownLatch(CountDownLatch countDownLatch) {this.countDownLatch = countDownLatch;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}@Overridepublic int compareTo(AirplaneRequest o) {return 0;}}//获取当前线程名称并设置线程名称@Overridepublic String getServiceName() {return AirPlaneService.class.getSimpleName();}//执行线程@Overridepublic void run() {log.info("飞机工厂启动-----------");//循环处理不同的请求通知while (this.factoryOfManufacture()) ;}public boolean factoryOfManufacture() {boolean isSuccess = false;AirplaneRequest airplaneRequest = null;try {//等待飞机制造请求airplaneRequest = requestQueue.take();log.info("开始飞机制造-----------");//校验数据是否合法AirplaneRequest expectedRequest = this.requestTable.get(airplaneRequest.getUserId());if (null == expectedRequest) {log.warn("this mmap request expired, maybe cause timeout " + airplaneRequest.getUserId());return true;}if (expectedRequest != airplaneRequest) {log.warn("never expected here, maybe cause timeout " + airplaneRequest.getUserId());return true;}//...业务处理Thread.sleep(4000);//...isSuccess = true;} catch (InterruptedException e) {log.warn(this.getServiceName() + " interrupted, possibly by shutdown.");return false;} finally {if (airplaneRequest != null && isSuccess) {log.info("飞机制造完成啦-----------");airplaneRequest.getCountDownLatch().countDown();}}return true;}
}
定义轮船厂类ShipService基础线程类ServiceThread以及实现工厂类FactoryService
@Service
@Slf4j
public class ShipService extends ServiceThread implements FactoryService {public static ConcurrentMap<String, ShipRequest> requestTable =new ConcurrentHashMap<>();public static PriorityBlockingQueue<ShipRequest> requestQueue =new PriorityBlockingQueue<>();public static class ShipRequest implements Comparable<ShipRequest> {private CountDownLatch countDownLatch = new CountDownLatch(1);private String userId;public CountDownLatch getCountDownLatch() {return countDownLatch;}public void setCountDownLatch(CountDownLatch countDownLatch) {this.countDownLatch = countDownLatch;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}@Overridepublic int compareTo(ShipRequest o) {return 0;}}@Overridepublic String getServiceName() {return ShipService.class.getSimpleName();}@Overridepublic void run() {log.info("轮船工厂启动-----------");while (this.factoryOfManufacture()) ;}@Overridepublic boolean factoryOfManufacture() {boolean isSuccess = false;ShipRequest shipRequest = null;try {shipRequest = requestQueue.take();log.info("开始制造轮船-----------");//校验数据是否合法ShipRequest expectedRequest = this.requestTable.get(shipRequest.getUserId());if (null == expectedRequest) {log.warn("this mmap request expired, maybe cause timeout " + shipRequest.getUserId());return true;}if (expectedRequest != shipRequest) {log.warn("never expected here, maybe cause timeout " + shipRequest.getUserId());return true;}//...业务处理Thread.sleep(3000);//...isSuccess = true;} catch (InterruptedException e) {log.warn(this.getServiceName() + " interrupted, possibly by shutdown.");return false;} finally {if (shipRequest != null && isSuccess) {log.info("轮船制造完成啦-----------");shipRequest.getCountDownLatch().countDown();}}return true;}
}
定义汽车厂类CarService基础线程类ServiceThread以及实现工厂类FactoryService
@Service
@Slf4j
public class CarService extends ServiceThread implements FactoryService {public static ConcurrentMap<String, CarRequest> requestTable =new ConcurrentHashMap<>();public static PriorityBlockingQueue<CarRequest> requestQueue =new PriorityBlockingQueue<>();public static class CarRequest implements Comparable<CarRequest> {private CountDownLatch countDownLatch = new CountDownLatch(1);private String userId;public CountDownLatch getCountDownLatch() {return countDownLatch;}public void setCountDownLatch(CountDownLatch countDownLatch) {this.countDownLatch = countDownLatch;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}@Overridepublic int compareTo(CarRequest o) {return 0;}}@Overridepublic String getServiceName() {return CarService.class.getSimpleName();}@Overridepublic void run() {log.info("汽车工厂启动-----------");while (this.factoryOfManufacture());}@Overridepublic boolean factoryOfManufacture() {boolean isSuccess = false;CarRequest carRequest = null;try {carRequest = requestQueue.take();log.info("开始汽车制造-----------");//校验数据是否合法CarRequest expectedRequest = this.requestTable.get(carRequest.getUserId());if (null == expectedRequest) {log.warn("this mmap request expired, maybe cause timeout " + carRequest.getUserId());return true;}if (expectedRequest != carRequest) {log.warn("never expected here, maybe cause timeout " + carRequest.getUserId());return true;}//...业务处理Thread.sleep(2000);//...isSuccess = true;} catch (InterruptedException e) {log.warn(this.getServiceName() + " interrupted, possibly by shutdown.");return false;} finally {if (carRequest != null && isSuccess) {log.info("汽车制造完成啦-----------");carRequest.getCountDownLatch().countDown();}}return true;}
}
定义客户请求实现
@Service
public interface PurchaseService {//请求制造Boolean manufacturing(String idCard);
}
实现逻辑类
@Service
@Slf4j
public class PurchaseServiceImpl implements PurchaseService {//超时时间private static int waitTimeOut = 1000 * 5;@Autowiredprivate AirPlaneService airPlaneService;@Autowiredprivate CarService carService;@Autowiredprivate ShipService shipService;@Overridepublic Boolean manufacturing(String userId) {long startTime = System.currentTimeMillis();//通知飞机厂计算飞机制造AirPlaneService.AirplaneRequest airplaneRequest = new AirPlaneService.AirplaneRequest();airplaneRequest.setUserId(userId);airPlaneService.requestTable.put(userId, airplaneRequest);airPlaneService.requestQueue.offer(airplaneRequest);//通知汽车厂计算汽车制造CarService.CarRequest carRequest = new CarService.CarRequest();carRequest.setUserId(userId);carService.requestTable.put(userId, carRequest);carService.requestQueue.offer(carRequest);//通知轮船厂计算轮船制造ShipService.ShipRequest shipRequest = new ShipService.ShipRequest();shipRequest.setUserId(userId);shipService.requestTable.put(userId, shipRequest);shipService.requestQueue.offer(shipRequest);//获取飞机制造AirPlaneService.AirplaneRequest airplaneOK = airPlaneService.requestTable.get(userId);//获取飞机制造CarService.CarRequest carOK = carService.requestTable.get(userId);//获取轮船制造ShipService.ShipRequest shipOK = shipService.requestTable.get(userId);try {//等待获取制造结果if (airplaneOK != null && carOK!= null && shipOK!= null) {boolean waitOK = airplaneOK.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);boolean waitOK2 = carOK.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);boolean waitOK3 = shipOK.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);//如果都成功了,返回trueif (waitOK && waitOK2 && waitOK3) {log.info("总共用时:"+(System.currentTimeMillis() - startTime));airPlaneService.requestTable.remove(userId);carService.requestTable.remove(userId);shipService.requestTable.remove(userId);return true;}}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException(e);}System.out.println("失败了总共用时:"+(System.currentTimeMillis() - startTime));return false;}
}
定义控制器
@RestController
public class UserController {@Autowiredprivate PurchaseService purchaseService;@GetMapping("/manufacturing")public Boolean manufacturing(String userId) {return purchaseService.manufacturing(userId);}
}
定义启动类
同时启动飞机、轮船以及汽车的厂家线程
@SpringBootApplication
public class AsyncOneApplication {public static void main(String[] args) {SpringApplication.run(AsyncOneApplication.class, args);//启动飞机制造线程AirPlaneService airPlaneService = new AirPlaneService();airPlaneService.start();//启动汽车制造线程CarService carService = new CarService();carService.start();//启动轮船制造线程ShipService shipService = new ShipService();shipService.start();}
}
结果呈现
启动项目

可以看到飞机、汽车以及轮船各自的线程都启动了并成功设置了对应的线程名称

并且第一条日志输出成功,因为内存队列requestQueue中目前没有数据,所以线程阻塞。其他两个同理。
请求制造操作
请求接口返回true

查看日志

可以发现,飞机、轮船、汽车的制造都完成了,并且和前面说的一样用时4s左右,完美实现。
可能有人会发出疑问,为何每个服务中的请求对象中都要单独定义CountDownLatch,难道不可以定义一个全局的CountDownLatch数字设置为3就可以了,每个线程完成了就减1就可以了,其实这种方式也可以,但考虑到实际业务中3个服务线程并不一定会被同时投递,或者小明只单独订购飞机呢是吧,所以对每个线程的请求对象单独进行维护CountDownLatch。
总结
在本文中,我们深入探讨了如何优化一个复杂的API请求,涉及多个独立业务模块且需要尽快返回结果的问题。通过结合多线程(Runnable)、CountDownLatch以及PriorityBlockingQueue,我们实现了高效的并行处理和有序的任务管理,成功地在当前接口返回了最终的结果。这一解决方案不仅提高了系统性能,还确保了结果的及时返回,具有很强的实用性和可操作性。希望本文提供的思路和实践能为大家在类似场景中提供有效的解决方案。
相关文章:
多线程优化API请求:CountDownLatch与PriorityBlockingQueue的应用
目录 前言 CountDownLatch是什么? PriorityBlockingQueue是什么? 场景描述 解决方案 定义统一工厂制造类 定义制造厂 定义客户请求实现 定义控制器 定义启动类 结果呈现 启动项目 请求制造操作 总结 前言 写这篇文章的缘由是因为之前在面…...
谷粒商城实战笔记-54-商品服务-API-三级分类-拖拽效果
文章目录 一,54-商品服务-API-三级分类-修改-拖拽效果1,el-tree控件加上允许拖拽的属性2,是否允许拖拽3,完整代码 一,54-商品服务-API-三级分类-修改-拖拽效果 本节的主要内容是给三级分类树形结构加上拖拽功能&#…...
AI大模型学习必备十大网站
随着人工智能技术的快速发展,AI大模型(如GPT-3、BERT等)在自然语言处理、计算机视觉等领域取得了显著的成果。对于希望深入学习AI大模型的开发者和研究者来说,找到合适的学习资源至关重要。本文将为大家推荐十大必备网站ÿ…...
Elasticsearch:Golang ECS 日志记录 - zap
ECS 记录器是你最喜欢的日志库的格式化程序/编码器插件。它们可让你轻松地将日志格式化为与 ECS 兼容的 JSON。 编码器以 JSON 格式记录日志,并在可能的情况下依赖默认的 zapcore/json_encoder。它还处理 ECS 错误格式的错误字段记录。 默认情况下,会添…...
关于线性代数(考研)
1.AE的特征值的问题 若λ是A的特征值,对应的特征向量是x,则Axλx,所以(AE)xAxExλxx(λ1)x,所以λ1是AE的特征值。所以若A的特征值是1,1,0,则AE的特征值就是11,11,01&am…...
【java基础】spring springMVC springboot 的区别
Spring, Spring MVC, 和 Spring Boot 是三个紧密相关的技术,它们都是由 Pivotal 团队(原SpringSource)开发的,主要用于构建企业级的Java应用程序。尽管它们在功能上有所交集,但各自也有独特的定位和用途。 Spring Fra…...
【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 开源项目热度排行榜(100分) - 三语言AC题解(Python/Java/Cpp)
🍭 大家好这里是清隆Coding ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线评测,专栏文章质量平均 93 分 最新华为OD机试目录…...
大模型算法面试题(十一)
本系列收纳各种大模型面试题及答案。 1、说一下目前主流或前沿的预训练模型,包括nlp(百度ERNIE3.0,华为NEZHA,openAI gpt-3,nvidia MegatronLM,macrosoft T5)和cv(我只知道CLIP&…...
CSS 基础知识
CSS(级联样式表)是设置 Web 内容样式的代码。CSS 基础知识将介绍入门所需的内容。我们将回答以下问题:如何将文本设置为红色?如何使内容显示在(网页)布局中的某个位置?如何用背景图片和颜色装饰我的网页? 什么是CSS? 像HTML一样,CSS不是一种编程语言。它也不是一种标…...
IntelliJ IDEA 和 Eclipse的区别
IntelliJ IDEA 和 Eclipse 是两个非常流行的 Java 集成开发环境(IDE),它们各自具有不同的特点和优势。下面是它们之间的一些主要对比: 性能和资源使用 IntelliJ IDEA 被认为在某些方面更加智能,能够提供更好的代码分…...
Ansible之playbook剧本编写(二)
tags 模块 可以在一个playbook中为某个或某些任务定义“标签”,在执行此playbook时通过ansible-playbook命令使用--tags选项能实现仅运行指定的tasks。 playbook还提供了一个特殊的tags为always。作用就是当使用always作为tags的task时,无论执行哪一个t…...
力扣第二十九题——两数相除
内容介绍 给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求 不使用 乘法、除法和取余运算。 整数除法应该向零截断,也就是截去(truncate)其小数部分。例如,8.345 将被截断为 8 ,-…...
解析三款热门的文献翻译工具:优势与使用指南
今儿咱们来聊聊那些让咱们头疼又不得不面对的事儿——文献翻译。在浩瀚的学术海洋里遨游时,遇到外文文献那是家常便饭,但语言障碍就像海上的迷雾,一不小心就能让你偏离航向。别担心,我这不就带着几款亲测好用的文献翻译神器来了嘛…...
git 过滤LFS文件下载
git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f" git config --global filter.lfs.process "git-lfs filter-process --skip" 恢复下载 git config --global filter.lfs.smudge "git-lfs smudge -- %f" git config --g…...
内存泄漏详解
文章目录 什么是内存泄漏内存泄漏的原因排查及解决内存泄漏避免内存泄漏及时释放资源设置合理的变量作用域及时清理不需要的对象避免无限增长避免内部类持有外部类引用使用弱引用 什么是内存泄漏 内存泄漏是指不使用的对象持续占有内存使得内存得不到释放,从而造成…...
多角度解析高防CDN防御DDOS及CC攻击
网络攻击的形式也日益多样化,其中DDoS(分布式拒绝服务)和CC(Challenge Collapsar)攻击尤为突出,给网站和企业带来了巨大的安全威胁。高防CDN(Content Delivery Network)作为一种专业…...
(7) cmake 编译C++程序(二)
文章目录 概要整体代码结构整体代码小结 概要 在ubuntu下,通过cmake编译一个稍微复杂的管理程序 整体代码结构 整体代码 boss.cpp #include "boss.h"Boss::Boss(int id, string name, int dId) {this->Id id;this->Name name;this->DeptId …...
C语言系统调用linux文件系统
在C语言中,open、write和read函数是系统调用(system calls),它们直接由操作系统提供,用于底层的文件操作。这些函数是UNIX和类UNIX系统(如Linux)中的标准接口,不同于C标准库中的文件…...
LeetCode142 环形链表 II
前言 题目: 142. 环形链表 II 文档: 代码随想录——环形链表 II 编程语言: C 解题状态: 思路错误,链表不允许被修改 思路 两步走,第一步,判断有没有环,第二步,判断入环口…...
逆向案例二十八——某高考志愿网异步请求头参数加密,以及webpack
网址:aHR0cDovL3d3dy54aW5nYW9rYW90Yi5jb20vY29sbGVnZXMvc2VhcmNo 抓包分析,发现请求头有参数u-sign是加密的,载荷没有进行加密,直接跟栈分析。 进入第二个栈,打上断点,分析有没有加密位置。 可以看到参数…...
AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
