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

经验分享:用一张表解决并发冲突!数据库事务锁的核心实现逻辑

背景

对于一些内部使用的管理系统来说,可能没有引入Redis,又想基于现有的基础设施处理并发问题,而数据库是每个应用都避不开的基础设施之一,因此分享个我曾经维护过的一个系统中,使用数据库表来实现事务锁的方式。

之前在文章Java业务功能并发问题处理中实现了使用MySQL行锁、Redis分布式锁来处理业务并发问题,这次来填坑了,如果想了解其他并发问题处理方式和区别,可以看看文章Java业务功能并发问题处理哈。

业务流程说明

业务加锁流程图

方案分析

适用场景

  1. 应用服务有多个实例,但是数据库是单实例;
  2. 没有用上Redis的应用服务,想通过现有的基础设施解决并发数据问题

待改进措施

  1. 设置超时机制:当出现锁无法及时释放时需要手动删除表数据,可以设置逻辑删除字段或者定时器删除过期数据;
  2. 重试获取锁机制:设置一定的循环次数,当获取不到锁时休眠200毫秒再次获取,直到循环次数用尽后再返回失败;
  3. 锁重入支持:通过增加加锁次数字段让当同一个线程可以重复获取锁

程序实现过程

框架及工具说明

  • 技术框架:SpringBootMyBatisMaven
  • 数据库:MySQL
  • 测试工具:Apifox
  • 表设计及代码说明:
    1. 唯一索引:需要有一个用于判断唯一的字段,在数据库表中通过指定唯一索引来实现;
    2. 加锁的线程号:避免A线程加的锁,被B线程删除;
    3. 锁的可见性要单独事务:添加事务锁的逻辑应在我们执行业务逻辑的事务之前,且不能跟业务逻辑的事务在一块,否则在事务提交前其他线程根本看不到这个锁,也就达不到我们锁的目的了;
    4. 为了我们的锁更方便使用,也可以将加锁逻辑抽到注解中实现,注解的实现流程:
      • 在pom文件中引入spring-boot-starter-aop
      • 编写自定义注解ConcurrencyLock
      • 实现切面类(Aspect)逻辑

代码展示

为了能让大家更关注加解锁逻辑,本文只保留主要代码,参考链接处会放置码云(gitee)的源码地址(或者点击此处跳转);
另外,本文就不展示注解方式的使用了,以免占用篇幅。

代码结构图

代码结构图

实体类

/*** 并发锁实体类*/
public class ConcurrencyLockBean {/*** 数据库主键*/private Long id;/*** 操作节点*/private String businessNode;/*** 订单唯一编号*/private String businessUniqueNo;/*** 线程ID*/private Long threadId;/*** 创建日期*/private Date creationDate;
}/*** 订单实体类*/
@Setter
@Getter
@ToString
public class OrderInfoBean {/*** 自增长主键*/private int id;/*** 订单号*/private String orderNo;/*** 物料数量*/private Integer itemQty;
}

ConcurrencyLockServiceImpl.java

@Slf4j
@Service
public class ConcurrencyLockServiceImpl implements ConcurrencyLockService {ConcurrencyLockMapper mapper;/*** service类注入*/@AutowiredConcurrencyLockServiceImpl(ConcurrencyLockMapper mapper) {this.mapper = mapper;}@Overridepublic Boolean tryLock(String businessNode, String businessUniqueNo) {long threadId = Thread.currentThread().getId();ConcurrencyLockBean concurrencyLock = mapper.selectConcurrencyLock(businessNode, businessUniqueNo);if (concurrencyLock != null) {log.info("{}数据正在操作中,请稍后", threadId);return false;}ConcurrencyLockBean lock = new ConcurrencyLockBean();lock.setBusinessNode(businessNode);lock.setBusinessUniqueNo(businessUniqueNo);lock.setThreadId(threadId);try {int insertCount = mapper.insertConcurrencyLock(lock);if (insertCount == 0) {log.info("{}获取锁失败,请稍后重试", threadId);return false;}} catch (Exception e) {log.info("{}获取锁异常,请稍后重试", threadId);return false;}log.info("{}完成锁表插入", threadId);return true;}@Overridepublic void unLock(String businessNode, String businessUniqueNo) {ConcurrencyLockBean lock = new ConcurrencyLockBean();long threadId = Thread.currentThread().getId();lock.setThreadId(threadId);lock.setBusinessNode(businessNode);lock.setBusinessUniqueNo(businessUniqueNo);mapper.deleteConcurrencyLock(lock);log.info("{}执行解锁完毕", threadId);}
}

ConcurrencyLockMapper.java

import org.apache.ibatis.annotations.Param;public interface ConcurrencyLockMapper {/*** 根据业务节点和唯一业务号查询锁*/ConcurrencyLockBean selectConcurrencyLock(@Param("businessNode") String businessNode, @Param("businessUniqueNo") String businessUniqueNo);/*** 插入锁*/int insertConcurrencyLock(ConcurrencyLockBean lock);/*** 删除锁*/int deleteConcurrencyLock(ConcurrencyLockBean lock);
}

ConcurrencyLockMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.runningrookie.mapper.ConcurrencyLockMapper"><select id="selectConcurrencyLock" resultType="com.runningrookie.domain.ConcurrencyLockBean">SELECTTHREAD_ID,BUSINESS_NODE,BUSINESS_UNIQUE_NO,CREATION_DATEFROM concurrency_lockWHERE BUSINESS_UNIQUE_NO = #{businessUniqueNo}AND BUSINESS_NODE = #{businessNode}</select><insert id="insertConcurrencyLock" useGeneratedKeys="true" keyProperty="id">INSERT INTO concurrency_lock (THREAD_ID,BUSINESS_NODE,BUSINESS_UNIQUE_NO,CREATION_DATE)VALUES(#{threadId}, #{businessNode}, #{businessUniqueNo}, NOW());</insert><delete id="deleteConcurrencyLock">DELETE FROM concurrency_lockWHERE THREAD_ID = #{threadId}and BUSINESS_NODE = #{businessNode}and BUSINESS_UNIQUE_NO = #{businessUniqueNo}</delete>
</mapper>

ConcurrencyLock.java注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConcurrencyLock {String businessNode();String businessUniqueNoKey();
}

ConcurrencyLockAspect.java注解类

@Aspect
@Component
@Slf4j
public class ConcurrencyLockAspect {ConcurrencyLockService concurrencyLockService;@AutowiredConcurrencyLockAspect(ConcurrencyLockService concurrencyLockService) {this.concurrencyLockService = concurrencyLockService;}// 环绕切面@Around("@annotation(concurrencyLock)")public Object around(ProceedingJoinPoint joinPoint, ConcurrencyLock concurrencyLock) throws Throwable {long threadId = Thread.currentThread().getId();Object[] args = joinPoint.getArgs();if (args.length == 0) {return joinPoint.proceed();}// 通过反射获取值String invokeMethodName = "get" + concurrencyLock.businessUniqueNoKey().substring(0, 1).toUpperCase() + concurrencyLock.businessUniqueNoKey().substring(1);// 获取Order类的Class对象Class<?> clazz = args[0].getClass();// 获取getOrderNo方法的Method对象Method method = clazz.getMethod(invokeMethodName);// 调用getOrderNo方法并获取返回值String businessUniqueNo = method.invoke(args[0]).toString();Boolean isSuccessLock = concurrencyLockService.tryLock(concurrencyLock.businessNode(), businessUniqueNo);if (!isSuccessLock) {log.info("{}加锁失败,请稍后重试", threadId);// 生成与切点方法相同的返回对象return AjaxResult.error("加锁失败,请稍后重试");}try {log.info("{}开始执行业务逻辑", threadId);joinPoint.proceed();} finally {concurrencyLockService.unLock(concurrencyLock.businessNode(), businessUniqueNo);}return joinPoint.proceed();}
}

OrderInfoController.java

@RestController
@RequestMapping("/orderInfo")
public class OrderInfoController {OrderInfoService orderInfoService;@Autowiredprivate OrderInfoController(OrderInfoService orderInfoService) {this.orderInfoService = orderInfoService;}@PostMappingpublic AjaxResult saveOrderInfo(@RequestBody OrderInfoBean bean) {return orderInfoService.saveOrderInfo(bean);}
}

OrderServiceImpl.java

/*** 订单逻辑代码*/
@Slf4j
@Service
public class OrderInfoServiceImpl implements OrderInfoService {ConcurrencyLockService concurrencyLockService;/*** service类注入*/@AutowiredOrderInfoServiceImpl(ConcurrencyLockService concurrencyLockService) {this.concurrencyLockService = concurrencyLockService;}@Overridepublic AjaxResult saveOrderInfo(OrderInfoBean bean) {long threadId = Thread.currentThread().getId();final String businessNode = "插入";Boolean isSuccessLock = concurrencyLockService.tryLock(businessNode, bean.getOrderNo());if (!isSuccessLock) {return AjaxResult.error("加锁失败,请稍后重试");}try {log.info("{}开始执行业务逻辑", threadId);// TODO:模拟业务逻辑耗时Thread.sleep(1500);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {concurrencyLockService.unLock(businessNode, bean.getOrderNo());}return AjaxResult.success();}@Override@ConcurrencyLock(businessNode = "插入", businessUniqueNoKey = "orderNo")@Transactionalpublic AjaxResult saveOrderInfoByAnnotation(OrderInfoBean bean) {// TODO:模拟业务逻辑耗时Thread.sleep(1500);return AjaxResult.success();}
}

pom.xml相关依赖

dependencies中添加下列依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>

事务处理表的表结构

CREATE TABLE `concurrency_lock` (`ID` int NOT NULL AUTO_INCREMENT COMMENT '主键',`THREAD_ID` int DEFAULT NULL COMMENT '线程号',`BUSINESS_NODE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作节点',`BUSINESS_UNIQUE_NO` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '单据号',`CREATION_DATE` datetime DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`ID`),UNIQUE KEY `uni_business_no` (`BUSINESS_UNIQUE_NO`,`BUSINESS_NODE`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

测试输出结果

使用Apifox并发发送5次请求,可以看到实际成功获取到锁并执行的只有一个线程

17:08:00.449 [http-nio-8080-exec-1] c.r.service.impl.ConcurrencyLockServiceImpl - 40完成锁表插入
17:08:00.462 [http-nio-8080-exec-1] c.runningrookie.service.impl.OrderInfoServiceImpl - 40开始执行业务逻辑
17:08:00.573 [http-nio-8080-exec-5] c.r.service.impl.ConcurrencyLockServiceImpl - 44获取锁异常,请稍后重试
17:08:00.573 [http-nio-8080-exec-4] c.r.service.impl.ConcurrencyLockServiceImpl - 43获取锁异常,请稍后重试
17:08:00.573 [http-nio-8080-exec-3] c.r.service.impl.ConcurrencyLockServiceImpl - 42获取锁异常,请稍后重试
17:08:00.573 [http-nio-8080-exec-2] c.r.service.impl.ConcurrencyLockServiceImpl - 41获取锁异常,请稍后重试
17:08:00.574 [http-nio-8080-exec-1] c.r.service.impl.ConcurrencyLockServiceImpl - 40执行解锁完毕

参考链接

gitee代码仓库地址:数据库并发锁

相关文章:

经验分享:用一张表解决并发冲突!数据库事务锁的核心实现逻辑

背景 对于一些内部使用的管理系统来说&#xff0c;可能没有引入Redis&#xff0c;又想基于现有的基础设施处理并发问题&#xff0c;而数据库是每个应用都避不开的基础设施之一&#xff0c;因此分享个我曾经维护过的一个系统中&#xff0c;使用数据库表来实现事务锁的方式。 之…...

嵌入式学习前要了解的基础知识

一、电压和电流 在嵌入式开发中&#xff0c;电压和电流是两个基本的电气概念&#xff0c;对于理解和设计电子电路至关重要。它们直接影响到嵌入式系统的性能、功耗、可靠性和安全性。 电压&#xff08;Voltage&#xff09; 电压是电场力推动电荷移动的能力&#xff0c;通常以…...

RTC、直播、点播技术对比|腾讯云/即构/声网如何选型 — 2025 版

前言 作为一个有多年实战经验的开发者&#xff0c;在音视频技术领域我深刻体会到 RTC&#xff08;实时通信&#xff09;、直播和点播三者的不同。虽然它们的核心都涉及音视频内容的传输&#xff0c;但在实际应用中&#xff0c;它们的技术实现、使用场景以及所面临的挑战各不相…...

《白帽子讲 Web 安全》之文件操作安全

目录 引言 &#xff08;一&#xff09;文件上传与下载漏洞概述 1.文件上传的常见安全隐患 1.1前端校验的脆弱性与服务端脚本执行危机在文件上传流程中&#xff0c;部分开发者可能会在前端使用 JavaScript 代码对文件后缀名进行简单校验&#xff0c;试图以此阻止非法文件上传…...

yolov8训练模型、测试视频

yolov8先训练生成best.pt文件&#xff0c;用这个生成的模型进行视频的测试 因为本来用的代码生成的测试视频打不开&#xff0c;格式应该是损坏了&#xff0c;或者部分帧没有正常保存吧。 修改了一下代码&#xff0c;现状可以正常打开生成的视频了。 1、训练代码train.py im…...

03.网络编程套接字(二)

文章目录 简单的TCP网络程序 服务端创建套接字 服务端绑定 服务端监听 服务端获取连接 服务端处理请求 客户端创建套接字 客户端发起请求 服务器测试 单执行流服务器的弊端 多进程版的TCP网络程序 线程池版的TCP网络程序 简单的TCP网络程序 服务端创建套接字 我…...

一周学会Flask3 Python Web开发-Flask3之表单处理WTForms安装与定义WTForms表单类

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 我们平时开发项目&#xff0c;都会用到表单&#xff0c;编写表单&#xff0c;提交表单&#xff0c;验证表单&#xff0c;如果…...

Git基本命令索引

GIT基本命令索引 创建代码库修改和提交代码日志管理远程操作操作分支 创建代码库 操作指令初始化仓库git init克隆远程仓库git clone 修改和提交代码 操作指令查看文件状态git status文件暂存git add文件比较git diff文件提交git commit回滚版本git reset重命名或者移动工作…...

【论文阅读笔记】SL-YOLO(2025/1/13) | 小目标检测 | HEPAN、C2fDCB轻量化模块

目录 摘要 1 引言 2 相关工作 3 方法 3.1 为小目标检测增加一个头 3.2 优化网络结构 3.3 改进轻量化模块 3.3.1 C2fDCB 3.3.2 SCDown 4 实验 4.1 数据集 4.2 实验环境 4.3 与其他模型的比较 4.4 消融研究 ▲不同网络结构的分析 ▲不同模块的分析 ▲不同降采样…...

MySQL SQL 优化专题

MySQL SQL 优化专题 1. 插入数据优化 -- 普通插入&#xff08;不推荐&#xff09; INSERT INTO tb_user VALUES(1,tom); INSERT INTO tb_user VALUES(2,cat); INSERT INTO tb_user VALUES(3,jerry);-- 优化方案1&#xff1a;批量插入&#xff08;推荐&#xff0c;不建议超过1…...

Mac上安装Pycharm

说明&#xff1a;仅供参考&#xff0c;是自己的安装流程&#xff0c;以免以后自己想不起来来看看的笔记 官网地址&#xff1a;https://www.jetbrains.com/pycharm/ 1、点击Download&#xff0c;跳转到下一个页面 2、MAC&#xff0c;选择Mac OS&#xff0c;在Pycharm Professio…...

flask框架基础入门学习教程

文章目录 前言1. 环境搭建1.1Python安装1.2选择Python开发环境1.3 创建虚拟环境&#xff08;可选但推荐&#xff09;1.4 安装 Flask 2. 第一个 Flask 应用3. 路由和视图函数3.1 基本路由3.2 动态路由3.3 路由参数类型 4. 请求和响应4.1 获取请求数据4.2 响应对象 5. 模板渲染6.…...

Qt显示一个hello world

一、显示思路 思路一&#xff1a;通过图形化方式&#xff0c;界面上创建出一个控件显示。 思路二&#xff1a;通过编写C代码在界面上创建控件显示。 二、思路一实现 点开 Froms 的 widget.ui&#xff0c;拖拽 label 控件&#xff0c;显示 hello world 即可。 qmake 基于 .…...

MySQL快速搭建主从复制

一、基于位点的主从复制部署流程 确定主库Binlog是否开启修改主从server_id主库导出数据从库导入数据确定主库备份时的位点在从库配置主库信息查看复制状态并测试数据是否同步 二、准备阶段(主库和从库配置都需要修改&#xff09; 1、确定主库Binlog是否开启 2、修改主从se…...

力扣-动态规划-674 最长连续递增序列

思路 dp数组定义&#xff1a;以i为结尾的最长连续递增序列递推公式&#xff1a; if(nums[i-1] < nums[i]) dp[i] dp[i-1] 1; dp数组初始化&#xff1a;都为1遍历顺序&#xff1a;顺序时间复杂度&#xff1a; 代码 class Solution { public:int findLengthOfLCIS(v…...

在笔记本电脑上用DeepSeek搭建个人知识库

最近DeepSeek爆火&#xff0c;试用DeepSeek的企业和个人越来越多。最常见的应用场景就是知识库和知识问答。所以本人也试用了一下&#xff0c;在笔记本电脑上部署DeepSeek并使用开源工具搭建一套知识库&#xff0c;实现完全在本地环境下使用本地文档搭建个人知识库。操作过程共…...

leetcode 94. 二叉树的中序遍历

题目如下 做了那么多道难题&#xff0c;给自己放放松。通过代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int …...

YOLOv12:目标检测新时代的破局者

目录 一、YOLOv12 横空出世二、YOLOv12 的性能飞跃2.1 多规模优势2.2 对比超越 三、技术创新与原理剖析3.1 区域注意力模块&#xff08;Area Attention&#xff0c;A2&#xff09;3.2 残差高效层聚合网络&#xff08;R-ELAN&#xff09;3.3 架构优化细节 四、实验验证与结果分析…...

基于OFDR的层压陆相页岩油储层中非对称裂缝群传播的分布式光纤监测

关键词&#xff1a;OFDR、分布式光纤传感、裂缝传播 一. 概述 四川盆地凉高山组优质页岩油储层存在复杂的垂直重叠岩性&#xff0c;大陆页岩油储层存在发育层理&#xff0c;薄层和天然裂缝&#xff0c;对水平井多级压裂技术的裂缝网络形态控制和监测构成挑战。本研究提出了一…...

Linux虚拟机网络配置-桥接网络配置

简介 本文档旨在指导用户如何在虚拟环境中配置Linux系统的桥接网络&#xff0c;以实现虚拟机与物理主机以及外部网络的直接通信。桥接网络允许虚拟机如同一台独立的物理机一样直接连接到物理网络&#xff0c;从而可以被分配一个独立的IP地址&#xff0c;并能够与网络中的其他设…...

Chrome-Charset:3步彻底解决网页乱码问题,告别天书般的浏览体验![特殊字符]

Chrome-Charset&#xff1a;3步彻底解决网页乱码问题&#xff0c;告别天书般的浏览体验&#xff01;&#x1f680; 【免费下载链接】Chrome-Charset An extension used to modify the page default encoding for Chromium 55 based browsers. 项目地址: https://gitcode.com/…...

ElevenLabs甘肃话语音合成技术解析(西北方言TTS工程化白皮书)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;ElevenLabs甘肃话语音合成技术概览 ElevenLabs 是全球领先的语音合成平台&#xff0c;原生支持英语、西班牙语、法语等数十种主流语言&#xff0c;但**不直接内置甘肃话&#xff08;属中原官话秦陇片&a…...

【仅剩最后47份】盐印相风格训练数据集泄露报告(含原始Agfa APX 400扫描底片参数+Midjourney反向蒸馏权重)

更多请点击&#xff1a; https://codechina.net 第一章&#xff1a;盐印相风格的视觉基因与数字重生 盐印相&#xff08;Salted Paper Print&#xff09;作为19世纪早期摄影术的奠基性工艺&#xff0c;其独特颗粒质感、柔和影调过渡与温润泛黄基底&#xff0c;构成了不可复制的…...

深入WCH USB主机IP:对比CH58x与CH32系列寄存器差异及CherryUSB适配心得

深入解析WCH USB主机IP&#xff1a;寄存器差异与CherryUSB适配实战 1. WCH USB主机IP架构概览 沁恒微电子&#xff08;WCH&#xff09;的CH58x/CH57x与CH32V/CH32F系列微控制器虽然采用不同的USB主机IP设计&#xff0c;但在协议栈层面保持了高度兼容性。这种设计哲学体现了硬件…...

从暗房到云端:一位古籍修复师用Midjourney蓝晒法重制《天工开物》插图的72小时实录(含失败21次原始日志)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;从暗房到云端&#xff1a;一场古籍图像的跨时空显影 在胶片时代&#xff0c;古籍修复师需在幽暗的暗房中&#xff0c;借助红光灯与显影液&#xff0c;一帧一帧唤醒沉睡于纸背的墨痕&#xff1b;而今&am…...

闲鱼客户端三重动态签名机制解析:x-sign、x-mini-wua与x-umt

1. 这不是“爬虫教程”&#xff0c;而是一次对闲鱼客户端通信机制的解剖式复盘你有没有遇到过这样的情况&#xff1a;用 Python 写了个闲鱼商品监控脚本&#xff0c;本地跑得好好的&#xff0c;一上服务器就频繁 403&#xff1b;或者用 Postman 模拟请求&#xff0c;Headers 里…...

Windows RTMP流媒体服务器搭建完整指南:nginx-rtmp-win32终极教程

Windows RTMP流媒体服务器搭建完整指南&#xff1a;nginx-rtmp-win32终极教程 【免费下载链接】nginx-rtmp-win32 Nginx-rtmp-module Windows builds. 项目地址: https://gitcode.com/gh_mirrors/ng/nginx-rtmp-win32 想要在Windows系统上快速搭建自己的RTMP直播服务器…...

如何验证代理IP纯净度?2026年IP检测与优化指南

一个“脏”IP&#xff0c;如同一个有问题的身份证&#xff0c;它可能会让你的账户面临高风险&#xff0c;甚至被平台封禁。为了避免这种情况&#xff0c;验证和优化代理IP的纯净度成为了不可忽视的环节。本文将为你提供一套2026年最新的代理IP纯净度检测与优化方案&#xff0c;…...

Box64终极指南:如何在ARM设备上轻松运行x86程序?三个简单步骤解锁无限可能

Box64终极指南&#xff1a;如何在ARM设备上轻松运行x86程序&#xff1f;三个简单步骤解锁无限可能 【免费下载链接】box64 Box64 - Linux Userspace x86_64 Emulator with a twist, targeted at ARM64, RV64 and LoongArch Linux devices 项目地址: https://gitcode.com/gh_m…...

如何实现百度网盘效率革命?BaiduPanFilesTransfers智能管理工具深度解析

如何实现百度网盘效率革命&#xff1f;BaiduPanFilesTransfers智能管理工具深度解析 【免费下载链接】BaiduPanFilesTransfers 百度网盘批量转存、分享和检测工具 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduPanFilesTransfers 还在为海量网盘资源的手动整理而消…...