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

Java基于数据库的分布式可重入锁(带等待时间和过期时间)

文章目录

  • 技术背景介绍
  • 代码实现
    • 数据库表结构
    • 尝试获取锁
    • 续约
    • 阻塞式获取锁
    • 解锁
    • 检查锁是否过期或者释放
  • 使用示例
  • 优化方案

项目代码

技术背景介绍

一般分布式锁使用最方便的就是使用redis实现,因为他自带超时过期机制、发布订阅模式、高吞吐高性能的优势,但是有些项目里只有mysql数据库,很多数据库都是没有数据超时过期机制和发布订阅模式的,当然也不是所有的,这里我只针对mysql数据库作为基础组件。

代码实现

数据库表结构

DROP TABLE IF EXISTS `distributed_lock`;
CREATE TABLE `distributed_lock` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',`lock_name` varchar(255) NOT NULL COMMENT '锁名',`machine_id` varchar(255) DEFAULT NULL COMMENT '服务器id',`expire_time` datetime DEFAULT NULL COMMENT '过期时间,服务里会有一个看门狗续期,如果过期了就说明服务挂了,解锁会设置为空',`is_locked` tinyint(4) NOT NULL DEFAULT '0' COMMENT '当前是否锁定状态',`state` int(11) NOT NULL DEFAULT '0' COMMENT '锁标记位 类似次数',`thread_id` varchar(255) DEFAULT NULL COMMENT '当前获得锁的线程id',`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',`is_deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE,UNIQUE KEY `idx_lock_name` (`lock_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

尝试获取锁

使用乐观锁模式更新锁记录。如果获取失败,则加入订阅列表中,等待被唤醒或者到达超时时间自动唤醒,待获取到锁后再从订阅列表中移除。他的具体等待时间取决于用户输入的等待时间和锁超时过期的时间,这里使用JUC的Semaphore来实现等待功能。

public boolean tryLock(String lockName, Long waitTime, Long leaseTime, TimeUnit timeUnit) {long startTime = System.currentTimeMillis();String threadId = getCurrentThreadId();Long ttl = tryAcquire(lockName, leaseTime, timeUnit);// lock acquiredif (ttl == null) {return true;}long time = timeUnit.toMillis(waitTime);if (waitTime != -1 && System.currentTimeMillis() - startTime < time) {//没有获取到锁,也没到等待时长,执行订阅释放锁的任务LockEntry lockEntry = subscribe(lockName, threadId, () -> {});try {while (true) {ttl = tryAcquire(lockName, leaseTime, timeUnit);// lock acquiredif (ttl == null) {return true;}long remainTtl = time - System.currentTimeMillis() + startTime;if (remainTtl < 0) {return false;}// waiting for messagelockEntry.getLatch().tryAcquire(ttl >= 0 && ttl < remainTtl ? ttl : remainTtl, TimeUnit.MILLISECONDS);}} catch (InterruptedException e) {log.error("thread interrupted", e);throw new RuntimeException(e);} finally {unsubscribe(lockEntry, lockName);}} else {return false;}}private Long tryAcquire(String lockName, long leaseTime, TimeUnit unit) {String currentThreadId = getCurrentThreadId();//设定了自动释放锁的时间if (leaseTime != -1) {return tryLockInner(leaseTime, unit, lockName, currentThreadId);}//没有设置自动过期时间,就需要在获取到之后使用看门狗续期Long remainTtl = tryLockInner(internalLockLeaseTime, TimeUnit.MILLISECONDS, lockName, currentThreadId);// lock acquiredif (remainTtl == null) {scheduleExpirationRenewal(lockName, currentThreadId);}return remainTtl;}/*** 加锁成功返回null,否则返回锁的过期时间** @param leaseTime* @param unit* @param lockName* @param threadId* @return*/private Long tryLockInner(long leaseTime, TimeUnit unit, String lockName, String threadId) {long internalLockLeaseTime = unit.toMillis(leaseTime);//查询是否存在锁LockObject existLock = lockRepository.queryLock(lockName);LockObject lockObject = new LockObject();lockObject.setLockName(lockName);lockObject.setThreadId(threadId);lockObject.setMachineId(machineId);lockObject.setIsLocked(true);lockObject.setExpireTime(new Date(System.currentTimeMillis() + internalLockLeaseTime));if (existLock == null) {//保存锁lockObject.setState(1);try {lockRepository.save(lockObject);} catch (Exception e) {//抛出数据重复异常,说明被其他线程锁定了//返回需要等待的时间log.error("lock other thread occupy", e);return reCheckTtl(leaseTime, unit, lockName, threadId);}} else {//存在的锁会判断是否是当前线程的,如果是也允许加锁成功,支持可重入//如果正好其他锁释放了,那也会抢锁,具体是否公平由各数据库的内部锁决定int updateNum = lockRepository.reentrantLock(lockObject);if (updateNum == 0) {//返回需要等待的时间return reCheckTtl(leaseTime, unit, lockName, threadId);}}//加锁成功return null;}private Long reCheckTtl(long leaseTime, TimeUnit unit, String lockName, String threadId) {Long ttl = queryLockTtl(lockName);if (ttl == null) {//如果返回null,那就是获取锁的时候失败了,但是执行查询锁的过期时间的时候释放了//就需要重新执行上锁逻辑return tryLockInner(leaseTime, unit, lockName, threadId);} else {return ttl;}}/*** 获取锁的释放时间,单位毫秒,* 如果锁不存在 或者 未上锁 或者 已过期 则返回null** @param lockName* @return*/private Long queryLockTtl(String lockName) {LockObject lockObject = lockRepository.queryLock(lockName);if (lockObject != null && lockObject.getExpireTime() != null) {long intervalTime = lockObject.getExpireTime().getTime() - System.currentTimeMillis();if (intervalTime > 0) {return intervalTime;}}return null;}
<update id="updateReentrantLock">update distributed_lock<set>is_locked   = true,machine_id   = #{machineId,jdbcType=VARCHAR},thread_id   = #{threadId,jdbcType=VARCHAR},state       = if(expire_time &lt; NOW(), 1, state + 1),expire_time = #{expireTime,jdbcType=TIMESTAMP}</set>where is_deleted = 0and lock_name = #{lockName,jdbcType=VARCHAR}and (expire_time &lt; NOW()or is_locked = falseor (machine_id = #{machineId,jdbcType=VARCHAR}and thread_id = #{threadId,jdbcType=VARCHAR}))</update>

续约

如果锁没有设置过期时间,那么就需要设置自动续期,使用过期和续期的目的也是为了防止服务宕机导致锁无法释放的问题。如果续期失败说明锁已经释放了,那么会自动停止锁的续约任务。

private void scheduleExpirationRenewal(String lockName, String threadId) {ExpirationEntry entry = new ExpirationEntry(lockName, threadId);ExpirationEntry oldEntry = expirationRenewalMap.putIfAbsent(expirationRenewalKey(lockName, threadId), entry);if (oldEntry != null) {oldEntry.addCount();} else {//只对第一次获取锁的线程续约,后面的属于重入renewExpiration(lockName, threadId);}}private void renewExpiration(String lockName, String threadId) {String keyName = expirationRenewalKey(lockName, threadId);ExpirationEntry ee = expirationRenewalMap.get(keyName);if (ee == null) {return;}//获取到锁后过1/3时间开启续约任务scheduledExecutor.schedule(() -> {ExpirationEntry ent = expirationRenewalMap.get(keyName);if (ent == null) {return;}boolean renewResult = renewExpirationLock(lockName, ent.getThreadId());if (!renewResult) {//更新失败说明锁被释放了log.error("Can't update lock " + lockName + " expiration");expirationRenewalMap.remove(keyName);return;}// reschedule itselfrenewExpiration(lockName, threadId);}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);}private void cancelExpirationRenewal(String lockName, String threadId) {String keyName = expirationRenewalKey(lockName, threadId);ExpirationEntry task = expirationRenewalMap.get(keyName);if (task == null) {return;}Integer count = task.reduceCount();if (count == 0) {expirationRenewalMap.remove(keyName);}}private String expirationRenewalKey(String lockName, String threadId) {return lockName + "_" + threadId;}/*** 续期** @param lockName* @param threadId*/private boolean renewExpirationLock(String lockName, String threadId) {LockObject lockObject = new LockObject();lockObject.setLockName(lockName);lockObject.setThreadId(threadId);lockObject.setMachineId(machineId);lockObject.setExpireTime(new Date(System.currentTimeMillis() + internalLockLeaseTime));int updateNum = lockRepository.renewExpirationLock(lockObject);return updateNum != 0;}
<update id="updateRenewExpirationLock">update distributed_lockset expire_time = #{expireTime,jdbcType=TIMESTAMP}where is_deleted = 0and is_locked = trueand lock_name = #{lockName,jdbcType=VARCHAR}and machine_id   = #{machineId,jdbcType=VARCHAR}and thread_id   = #{threadId,jdbcType=VARCHAR}and expire_time &gt; NOW()</update>

阻塞式获取锁

阻塞式获取锁和非阻塞的区别就是等待锁释放的过程,没有获取到锁的线程会一直等待下去。

public void lock(String lockName, long leaseTime, TimeUnit unit) {LockEntry lockEntry = null;try {while (true) {// 尝试获取锁Long ttl = tryAcquire(lockName, leaseTime, unit);if (ttl == null) {// 成功获取到锁,直接退出break;}// 未获取到锁,订阅锁释放通知(如果还没订阅)if (lockEntry == null) {lockEntry = subscribe(lockName, getCurrentThreadId(), () -> {});}// 等待锁释放通知,直到TTL时间结束try {lockEntry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {// 恢复线程的中断状态Thread.currentThread().interrupt();throw new RuntimeException("Thread was interrupted while waiting for the lock", e);}}} finally {// 确保在退出时释放锁并取消订阅if (lockEntry != null) {unsubscribe(lockEntry, getCurrentThreadId());}}}

解锁

获取锁的线程释放锁的时候,state会减1,直到减到0,锁才会真正的释放。这里需要移除锁续约的任务,并且唤醒等待当前锁的线程

public void unlock(String lockName) {if (releaseLock(lockName)) {//释放锁成功后去除看门狗的续期//如果解锁失败,比如自己获取到锁过期了,然后又去释放锁,因为他没有续约任务所以不需要移除cancelExpirationRenewal(lockName, getCurrentThreadId());//发送锁释放的通知// 这里只处理本机维护的等待锁的线程,其他的机器数据库没法主动发出通知,需要轮训或者由获取锁的线程下次获取锁时自行处理LockEntry lockEntry = subscribeMap.get(lockName);//要判空,因为如果没有阻塞中的线程,那么lockEntry会为空if (lockEntry != null) {Semaphore semaphore = lockEntry.getLatch();if (semaphore.hasQueuedThreads()) {semaphore.release();}}}}
<update id="updateReleaseLock">update distributed_lock<set>state       = state - 1,expire_time = if(state=0, null, expire_time),is_locked   = if(state=0, false, true),machine_id   = if(state=0, null, machine_id),thread_id   = if(state=0, null, thread_id),</set>where is_deleted = 0and lock_name = #{lockName,jdbcType=VARCHAR}and machine_id   = #{machineId,jdbcType=VARCHAR}and thread_id   = #{threadId,jdbcType=VARCHAR}and expire_time &gt; NOW()and is_locked = true</update>

检查锁是否过期或者释放

因为mysql数据库没有发布订阅的功能,所以这里采用了定时查询的模式检查锁的状态。如果检测到锁释放了,会发起唤醒等待锁线程的通知,让等待的线程重新尝试获取锁。

public void process() {scheduledExecutor.scheduleAtFixedRate(() -> {//执行本机订阅这把锁的检查任务List<String> needCheckLockNameList = subscribeMap.entrySet().stream().filter(entry -> entry.getValue().getCounter().get() != 0).map(entry -> entry.getKey()).collect(Collectors.toList());//查询已经过期或者释放的锁List<String> lockNameList = lockRepository.queryAllowObtainLockList(needCheckLockNameList);//执行对应锁的唤醒操作lockNameList.forEach(lockName -> {LockEntry lockEntry = subscribeMap.get(lockName);if (lockEntry != null) {//这里最多多唤醒一次,无非就是让等待线程多抢占一次,没什么关系,这种场景发生在tryAcquire正好过期,定时任务正好运行//多一次判断可以大幅度减少冲突时多释放的信号Semaphore semaphore = lockEntry.getLatch();if (semaphore.hasQueuedThreads()) {semaphore.release();log.info("定时任务发起唤醒等待锁的通知");}}});}, 0, 1, TimeUnit.SECONDS);}
<select id="queryAllowObtainLockList" resultType="java.lang.String">select lock_namefrom distributed_lockwhere is_deleted = 0and lock_name in<foreach collection="list" item="lockName" open="(" close=")" separator=",">#{lockName,jdbcType=VARCHAR}</foreach>and (is_locked = falseor expire_time &lt; NOW())</select>

使用示例

public static void main(String[] args) {// 第一个Spring容器,加载配置类 Config1ApplicationContext context1 = new AnnotationConfigApplicationContext(MybatisPlusConfig.class);// 第二个Spring容器,加载配置类 Config2ApplicationContext context2 = new AnnotationConfigApplicationContext(MybatisPlusConfig.class);DatabaseDistributedLock server1 = context1.getBean(DatabaseDistributedLock.class);DatabaseDistributedLock server2 = context2.getBean(DatabaseDistributedLock.class);server1.lock("test");new Thread(() -> {ThreadUtil.sleep(1, TimeUnit.SECONDS);if (server2.tryLock("test", 17L, TimeUnit.SECONDS)) {System.out.println("我执行了1");ThreadUtil.sleep(5, TimeUnit.SECONDS);server2.unlock("test");}}).start();new Thread(() -> {ThreadUtil.sleep(2, TimeUnit.SECONDS);if (server1.tryLock("test", 17L, TimeUnit.SECONDS)) {System.out.println("我执行了2");ThreadUtil.sleep(5, TimeUnit.SECONDS);server1.unlock("test");}}).start();System.out.println("我获取到了锁");ThreadUtil.sleep(15, TimeUnit.SECONDS);server1.unlock("test");ThreadUtil.sleep(100, TimeUnit.SECONDS);}

优化方案

订阅通知如果有消息队列的话,可以借助用来实现发布订阅锁通知

相关文章:

Java基于数据库的分布式可重入锁(带等待时间和过期时间)

文章目录 技术背景介绍代码实现数据库表结构尝试获取锁续约阻塞式获取锁解锁检查锁是否过期或者释放 使用示例优化方案 项目代码 技术背景介绍 一般分布式锁使用最方便的就是使用redis实现&#xff0c;因为他自带超时过期机制、发布订阅模式、高吞吐高性能的优势&#xff0c;…...

国家信息安全水平考试(NISP一级)最新题库-第十七章

目录 另外免费为大家准备了刷题小程序和docx文档&#xff0c;有需要的可以私信获取 1 受到了ARP欺骗的计算机&#xff0c;发出的数据包&#xff0c;     地址是错误的&#xff08;&#xff09; A.源IP&#xff1b;B.目的IP&#xff1b;C.源MAC&#xff1b;D.目的MAC 正…...

Java 8 新特性概览

Java 8 是 Java 语言发展史上的一个重要里程碑&#xff0c;它引入了许多革命性的特性&#xff0c;极大地提高了开发效率和程序性能。以下是 Java 8 的一些关键新特性&#xff1a; 1. Lambda 表达式 Lambda 表达式是 Java 8 中最引人注目的特性之一。它允许你以简洁的语法编写…...

pyspark==堆叠

安装环境 docker pull jupyter/all-spark-notebook 方式一 from pyspark.sql import SparkSession from pyspark.sql.functions import expr, col# 创建SparkSession spark SparkSession.builder.appName("StudentScores").getOrCreate()# 创建示例数据 data [(…...

Zypher Network Layer3 主网上线,不容错过的“宝藏方舟”活动

前言 随着 Zytron Layer3 主网的上线&#xff0c;Zypher Network 联合 Linea 共同推出了“宝藏方舟”活动&#xff0c;用户可通过参与活动&#xff0c;获得包括代币、积分、SBT 等系列奖励。 Zypher Network 是一个以 ZK 方案为核心的游戏底层堆栈&#xff0c;其提供了一个具备…...

【小白学机器学习21】 理解假设检验的关键:反证法

目录 理解假设检验的关键&#xff1a;反证法 1 假设的检验的出发点&#xff1a;H1假设&#xff0c; 1.1 为什么我们不去直接证明H1是否正确&#xff1f; 2 故意设立一个假设H1的否命题为H0 3 设定显著度α 4 总结假设检验的整个思路就是反证法 5 两类错误的关系 理解假…...

鸿蒙中富文本编辑与展示

富文本在鸿蒙系统如何展示和编辑的&#xff1f;在文章开头我们提出这个疑问&#xff0c;带着疑问来阅读这篇文章。 富文本用途可以展示图文混排的内容&#xff0c;在日常App 中非常常见&#xff0c;比如微博的发布与展示&#xff0c;朋友圈的发布与展示&#xff0c;都在使用富文…...

Python Q-learning 算法详解与应用案例

目录 Python Q-learning 算法详解与应用案例引言一、Q-learning 的基本原理1.1 强化学习基础1.2 Q值及其更新1.3 Q-learning 的特性 二、Python 中 Q-learning 的面向对象实现2.1 QTable 类的实现2.2 Environment 类的实现2.3 Agent 类的实现 三、案例分析3.1 简单环境中的 Q-l…...

解决:如何在opencv中得到与matlab立体标定一样的矫正图?(python版opencv)

目的&#xff1a;采用一样的标定参数&#xff0c;matlab中和opencv中的立体矫正图像是一样的吗&#xff1f;不一样的话怎么让它们一样&#xff1f; 结论&#xff1a;不一样。后文为解决方案。 原因&#xff1a;注意matlab的标定结果在matlab中的用法和在opencv中的用法不一样&a…...

gin入门教程(4):路由与处理器

路由与处理器 在 Gin 框架中&#xff0c;路由和处理器是核心组成部分&#xff0c;负责将 HTTP 请求映射到相应的处理逻辑。 1. 定义路由 在 cmd/main.go 中&#xff0c;您可以定义不同的路由&#xff0c;例如&#xff1a; r.GET("/ping", func(c *gin.Context) {…...

【python+Redis】hash修改

文章目录 前请详解一、关于Update1. 语法2. 代码示例 二、完整代码 前请详解 Redis库数据 keyvalue1{“id”: 1, “name”: “xxx”, “age”: “18”, “sex”: “\u7537”}2{“id”: 2, “name”: “xxx”, “age”: “18”, “sex”: “\u5973”}3{“id”: 3, “name”: “…...

MAVlink协议 部分通用消息集解析

文章目录 MAVLink是一种非常轻量级的消息传输协议, 用于地面控制终端&#xff08;地面站&#xff09;与无人机之间 (以及机载无人机组件之间) 进行通信&#xff0c; 为一种设计用于资源受限系统及带宽受限链路的二进制遥测协议。 HEARTBEAT 检测信号消息显示系统或组件存在并正…...

c++实现跳表

原理 跳表&#xff08;Skip List&#xff09; 是一种随机化数据结构&#xff0c;用于高效查找、插入和删除&#xff0c;尤其适用于有序数据集合。相比链表&#xff0c;跳表通过多层索引结构加速查找&#xff0c;期望时间复杂度接近 O(log⁡n)。跳表的主要思想是&#xff1a; …...

新探索研究生英语读写教程pdf答案(基础级)

《新探索研究生英语读写教程》的设计和编写充分考虑国内研究生人才培养目标和研究生公共英语的教学需求&#xff0c; 教学内容符合研究生认知水平&#xff0c; 学术特征突出&#xff1b;教学设计紧密围绕学术阅读、学术写作和学术研究能力培养&#xff1b;教学资源立体多元&…...

管道与共享内存

一&#xff0c;命名管道 管道的限制就是他只能在有血缘关系&#xff08;父子进程&#xff09;的进程中&#xff0c;允许互相访问&#xff0c;这是有局限性的&#xff0c;所以我们想在毫无关系的进程中允许他们相互访问&#xff0c;这就是命名管道的定义。 总结&#xff1a;命名…...

ES 自定义排序方式

es默认score是根据query的相关度进行打分的&#xff0c;具体打分机制可以参见&#xff1a;官方文档。如果召回时既希望有相关性又能根据其他信息进行排序。 例如小红书搜索的时候&#xff0c;可能既希望有召回相关度又能根据热度信息&#xff08;如果喜欢、收藏等等参数去进行召…...

在vue中,编写一个li标签同时使用v-for和v-if,谁的优先级更高

在 Vue 中&#xff0c;v-if 和 v-for 是两个常用的指令&#xff0c;但它们的优先级不同。当二者一起使用时&#xff0c;v-for 的优先级高于 v-if。这意味着&#xff0c;v-for 会先执行&#xff0c;即使列表中的某些元素不满足 v-if 条件&#xff0c;它们仍会被遍历和渲染。 由…...

Java 后端开发面试题及其答案

以下是一些常见的 Java 后端开发面试题及其答案&#xff0c;涵盖了 Java 基础、面向对象、并发、多线程、框架等多个方面&#xff1a; 1. Java 中的基本数据类型有哪些&#xff1f; 答案&#xff1a; Java 中的基本数据类型有 8 种&#xff1a; int&#xff1a;32 位整数lon…...

C++,STL 045(24.10.24)

内容 1.对set容器的大小进行操作。 2.set容器的交换操作。 运行代码 #include <iostream> #include <set>using namespace std;void printSet(set<int> &s) {for (set<int>::iterator it s.begin(); it ! s.end(); it){cout << *it <…...

二叉树习题其五【力扣】【算法学习day.12】

前言 书接上篇文章二叉树习题其四&#xff0c;这篇文章我们将基础拓展 ###我做这类文档一个重要的目的还是给正在学习的大家提供方向&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一…...

【数据库】Mysql的锁类型

Mysql中的锁机制主要是为了保证数据的一致性和完整性&#xff0c;在并发的情况下起着至关重要的作用。其中锁的类型主要是分为以下几种&#xff1a; 按照粒度分类 全局锁&#xff1a;对于整个数据库实例进行枷锁&#xff0c;加锁后整个实例就处于只读的状态。局锁通常用于需要…...

自媒体短视频制作素材下载网站推荐,让创作更简单

随着自媒体行业的火爆&#xff0c;视频质量要求也越来越高。想要找到无版权的高清视频素材并不容易&#xff0c;但别担心&#xff01;今天为大家整理了5个国内外高质量的素材网站&#xff0c;让你轻松获取自媒体短视频素材&#xff0c;快收藏起来吧&#xff01; 蛙学网 蛙学网是…...

Altium Designer 入门基础教程(五)

本文章继续接着《Altium Designer 入门基础教程&#xff08;四&#xff09;》的内容往下介绍&#xff1a; 七、AD画板的整个流程步骤 I.集成库的制作 AD元件库有2种&#xff1a;1、原理图元件库SCH.LIB 2、印刷电路板&#xff08;PCB&#xff09;元件库 PCB.LIB 印刷电路…...

Java题集练习3

Java题集练习3 1 什么时候用instanceof instanceOf关键字主要用于判断一个对象是否为某个类的子类或是接口的实例&#xff0c;通常用于类型转换和运行时类型判断的场景&#xff0c;比如继承和多态中。比如&#xff0c;创建一个Animal类及其子类Cat和Cat子类Hat&#xff0c;可…...

【部署篇】Haproxy-01安装部署(源码方式安装)

‌一、HAProxy概述‌ HAProxy是一款免费、快速且可靠的代理软件&#xff0c;提供高可用性、负载均衡&#xff0c;支持TCP和HTTP应用代理&#xff0c;HAProxy凭借其卓越的性能和灵活性&#xff0c;成为众多知名网站和系统的首选代理软件。‌ ‌核心特点‌&#xff1a; ‌高性能…...

开拓鸿蒙测试新境界,龙测科技引领自动化测试未来

在当今科技舞台上&#xff0c;鸿蒙 OS 以非凡先进性强势登场&#xff0c;打破传统操作系统格局&#xff0c;为软件测试领域带来全新机遇与艰巨挑战。 一、鸿蒙 OS 的辉煌崛起 &#xff08;一&#xff09;壮丽发展历程与卓越市场地位 鸿蒙 OS 的发展如波澜壮阔的史诗。2023 年…...

Java项目-基于springboot框架的自习室预订系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…...

调整数组奇偶数顺序

今天给大家分享一道题目&#xff0c;要求我们输入一个数组&#xff0c;将全部奇数放在偶数前面&#xff08;无需比较大小&#xff09;&#xff0c;下面是我写的代码 这个方法比使用三个数组进行数据传输要节省不少程序运行时间&#xff0c;缺点是使用了较多的while循环&#xf…...

Electron调用nodejs的cpp .node扩展【非安全】

Electron调用nodejs的cpp .node扩展【非安全】 环境&#xff1a; electron: 30.1.1 nodejs: 20.14.0前言 Electron中可以非常容易的调用nodejs的js代码&#xff0c;但是对于cpp .node扩展需要一定的配置才能调用&#xff0c;下面介绍一种最简单的cpp扩展的调用方法&#xff…...

一文了解AOSP是什么?

一文了解AOSP是什么&#xff1f; AOSP基本信息 基本定义 AOSP是Android Open Source Project的缩写&#xff0c;这是一个由Google维护的完全免费和开放的操作系统开发项目。它是Android系统的核心基础&#xff0c;提供了构建移动操作系统所需的基本组件。 主要特点 完全开源…...

上海南站网站建设公司/包头整站优化

点击蓝字关注我们什么是函数的返回值&#xff1f;C语言函数如果执行成功的话&#xff0c;返回1、返回0还是返回别的值&#xff1f;如果大家还不是很清楚&#xff0c;那么就让小橙同学来给大家介绍一下。C语言简介首先&#xff0c;简单介绍一下C语言。C 语言是一种通用的、面向过…...

批发网站/seo托管服务

docker是个轻量级的操作环境。 docker编译fuel的命令包括&#xff0c; 1 构建ubuntu-builder映像&#xff08;来自docker/Makefile) /usr/bin/docker build --rmtrue --no-cachetrue -t opnfv.org/ubuntu-builder:14.04 ubuntu-builder 从ubuntu-builder下的Dockerfile来构…...

制作一个网站平台要多钱/查询网站流量的网址

在挂载镜像文件的时候如果出现下面的问题&#xff1a; mount: you must specify the filesystem type 解决方法&#xff1a; 虚拟机&#xff0d;〉setting->cd/dvd&#xff0d;〉device status-.connected前面的勾打上 然后再重新挂载&#xff0c;如果还是出现同样的错…...

西安市住房和城乡建设局门户网站/站长网站优化公司

实体类中包含对象属性从后台传回到前台表单&#xff0c;后台提示错误&#xff1a; at com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase.serialize(AsArraySerializerBase.java:183) at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsFiel…...

有哪些建设网站公司/seo搜索排名优化公司

家里宽带安装完成后&#xff0c;总感觉网速比自己安装的网速要慢&#xff0c;这该怎么办呢&#xff1f;今天小哥哥就来教大家三个宽带测速的方法&#xff0c;自己就能测试网速啦&#xff01;(一般我们的安装师傅在安装完宽带之后&#xff0c;会现场测试网速&#xff0c;您也可以…...

夺宝网站建设/长沙有实力seo优化

C基础(6)——数据的共享与保护 变量和对象定义在不同的位置&#xff08;函数体内、类体内、函数原型参数表内、所有函数和类之外&#xff09;。 其作用域、可见性、生存期都不同。 属于整个类的数据成员——静态数据成员。 用于处理静态数据成员的函数——静态成员函数。 友元…...