Redission · 可重入锁(Reentrant Lock)
前言
Redisson是一个强大的分布式Java对象和服务库,专为简化在分布式环境中的Java开发而设计。通过Redisson,开发人员可以轻松地在分布式系统中共享数据、实现分布式锁、创建分布式对象,并处理各种分布式场景的挑战。
Redisson的设计灵感来自于Redis,但它不仅仅是Redis的Java客户端,更是在分布式环境下构建分布式系统所需的一套工具。无论是分布式集合、分布式锁、还是分布式调度器,Redisson都提供了简单而强大的API,使得开发者能够专注于业务逻辑而不必担心复杂的分布式细节。
底层原理
通过提供易于使用的API和丰富的功能集,Redisson旨在帮助开发者更轻松地构建可靠的、高性能的分布式系统。
 Redisson的底层原理主要基于Redis的分布式特性和Java的高级特性。以下是一些关键的底层原理:
- Redis协议: Redisson使用Redis协议进行与Redis服务器的通信。这意味着它能够与任何遵循Redis协议的Redis服务器进行交互。通过利用Redis的分布式特性,Redisson实现了分布式对象和服务。
 - Java序列化: Redisson使用Java对象的序列化和反序列化机制将Java对象转化为Redis数据结构。这使得在Java应用程序和Redis之间传递对象变得简单。默认情况下,Redisson使用标准的Java序列化,但也支持其他序列化方式,如JSON、Jackson等。
 - 分布式锁的实现: Redisson的分布式锁是通过Redis的
SETNX(set if not exists)命令实现的。它利用了Redis的原子性操作,确保在分布式环境中只有一个客户端能够成功获取锁。 - 监听器和事件通知: Redisson通过订阅/发布机制实现事件通知。当分布式对象发生变化时,Redisson会发布相应的事件,已注册的监听器将得到通知。这基于Redis的
PUB/SUB功能,使得分布式环境下的事件通知成为可能。 - 分布式集群: 对于Redis集群,Redisson使用了Redis的集群模式。它能够识别集群中的不同节点,并根据需要进行数据分片和分布式操作。
 - 线程模型: Redisson使用异步的线程模型来处理与Redis服务器的通信。这有助于提高性能,允许多个操作同时执行而不阻塞主线程。
 
总体而言,Redisson利用了Redis强大的分布式功能,并通过Java的特性将其封装为易于使用的API。底层的实现涵盖了分布式锁、分布式对象、事件通知等方面,以满足在分布式环境中构建高性能应用程序的需求。
Redisson分布式锁类型
Redisson提供了多种类型的分布式锁,以满足不同场景的需求。以下是一些常见的Redisson分布式锁类型:
- 可重入锁(Reentrant Lock): 可以被同一个线程重复加锁的锁。同一个线程在持有锁的情况下可以再次加锁,而不会引起死锁。
 - 公平锁(Fair Lock): 公平锁按照请求加锁的顺序进行获取锁,即先来先得。这有助于避免某些线程长时间等待的问题,提高公平性。
 - 联锁(MultiLock): 可以同时获取多个锁,且在释放锁时可以选择全部释放或部分释放。适用于需要操作多个资源的场景。
 - 红锁(RedLock): RedLock是一种分布式锁算法,使用多个Redis节点来确保锁的强一致性。通过在不同的节点上创建锁,即使其中一个节点失效,其他节点依然可以工作。
 - 读写锁(ReadWrite Lock): 读写锁分为读锁和写锁,多个线程可以同时持有读锁,但只有一个线程能够持有写锁。适用于读多写少的场景。
 - 信号量(Semaphore): 类似于Java的
Semaphore,用于控制同时访问某个资源的线程数量。 - 闭锁(CountDownLatch): 用于等待多个线程完成操作后再执行下一步操作。
 - 过期锁(Lease Lock): 具有自动过期时间的锁,确保在一定时间内锁会被释放,避免锁长时间占用。
 
这些锁的类型使得Redisson适用于各种分布式场景,开发者可以根据具体的需求选择合适的锁类型来确保分布式环境下的协同和同步。
可重入锁(Reentrant Lock)
基于Redis的Redisson分布式可重入锁RLockJava对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
引入 Maven 依赖
在微服务的 pom.xml 引入 redisson 的 maven 依赖
        <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.2</version> <!-- 使用最新版本 --></dependency>
 
自定义配置类
下面的代码是单节点 Redis 的配置。
package com.example.demo.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;@Configuration
public class RedissonConfig {/*** 对 Redisson 的使用都是通过 RedissonClient 对象* @return* @throws IOException*/@Bean(destroyMethod="shutdown") // 服务停止后调用 shutdown 方法。public RedissonClient redisson() throws IOException {// 创建配置Config config = new Config();config.useSingleServer().setAddress("redis://xx.xx.x.x:6379").setPassword("123456");// 集群模式// config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");return Redisson.create(config);}
}
 
测试API
package com.example.demo.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/api/redis")
public class MyController {@Autowiredprivate ReentrantLockService reentrantLockService;@GetMapping("/locked-operation")public String performLockedOperation() {// 模拟多个线程同时调用可重入锁的操作for (int i = 1; i <= 5; i++) {final int threadNumber = i;new Thread(() -> {reentrantLockService.performLockedOperation();}).start();}return "Locked operation initiated.";}
}
 
测试类
package com.example.demo.controller;import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
@Slf4j
public class ReentrantLockService {@Autowiredprivate RedissonClient redissonClient;public void performLockedOperation() {// 获取可重入锁RLock lock = redissonClient.getLock("myReentrantLock");String threadName = Thread.currentThread().getName();try {// 尝试加锁,最多等待10秒,锁的自动释放时间为30秒boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);if (isLocked) {// 执行需要加锁的操作log.info(threadName + " - 获取锁成功,执行加锁操作...");// 模拟业务操作Thread.sleep(5000);log.info(threadName + " - 加锁操作完成。");} else {log.info(threadName + "在指定时间内无法获取锁。");}} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println(threadName + "尝试获取锁时发生中断。");} finally {// 释放锁if (lock.isHeldByCurrentThread()) {log.info(threadName + " - 释放锁");lock.unlock();}}}
}
 
输出日志
2023-11-17 15:56:13.935  INFO 12316 --- [      Thread-17] c.e.d.controller.ReentrantLockService    : Thread-17 - 获取锁成功,执行加锁操作...
2023-11-17 15:56:18.945  INFO 12316 --- [      Thread-17] c.e.d.controller.ReentrantLockService    : Thread-17 - 加锁操作完成。
2023-11-17 15:56:18.969  INFO 12316 --- [      Thread-17] c.e.d.controller.ReentrantLockService    : Thread-17 - 释放锁
2023-11-17 15:56:19.020  INFO 12316 --- [      Thread-16] c.e.d.controller.ReentrantLockService    : Thread-16 - 获取锁成功,执行加锁操作...
2023-11-17 15:56:23.901  INFO 12316 --- [      Thread-19] c.e.d.controller.ReentrantLockService    : Thread-19在指定时间内无法获取锁。
2023-11-17 15:56:23.901  INFO 12316 --- [      Thread-20] c.e.d.controller.ReentrantLockService    : Thread-20在指定时间内无法获取锁。
2023-11-17 15:56:23.909  INFO 12316 --- [      Thread-18] c.e.d.controller.ReentrantLockService    : Thread-18在指定时间内无法获取锁。
2023-11-17 15:56:24.023  INFO 12316 --- [      Thread-16] c.e.d.controller.ReentrantLockService    : Thread-16 - 加锁操作完成。
2023-11-17 15:56:24.046  INFO 12316 --- [      Thread-16] c.e.d.controller.ReentrantLockService    : Thread-16 - 释放锁
 
分析日志
这段日志展示了一个使用Redisson实现的分布式锁的情景。让我详细解释一下日志和代码的原理:
获取锁(Thread-17):
Thread-17成功获取了名为 “myReentrantLock” 的可重入锁。- 执行了一段需要锁保护的业务操作,模拟了一个长时间的操作,持有锁。
 
释放锁(Thread-17):
- `Thread-17` 完成了业务操作,释放了锁。
 
获取锁(Thread-16):
Thread-16在Thread-17释放锁之后成功获取了相同的锁。- 执行了一段需要锁保护的业务操作,然后释放了锁。
 
获取锁失败(Thread-19, Thread-20, Thread-18):
Thread-19,Thread-20, 和Thread-18在指定的等待时间内无法获取锁,因为此时Thread-16持有锁。
总结原理:
- 通过
redissonClient.getLock("myReentrantLock")创建了一个可重入锁对象。 lock.tryLock(10, 30, TimeUnit.SECONDS)尝试获取锁,在10秒内等待,锁的自动释放时间为30秒。- 如果获取锁成功,执行需要加锁的操作,然后释放锁。
 - 其他线程在获取锁时,如果超过指定时间未能成功获取,会得到相应的提示。
 
这段代码通过Redisson实现了一个可重入的分布式锁,确保在分布式环境下对共享资源的安全访问。成功获取锁的线程执行受保护的操作,其他线程则需要等待或处理获取锁失败的情况。这有助于协调分布式系统中的并发访问,防止竞争条件和数据不一致性。
阻塞与非阻塞
阻塞方式
- 在阻塞方式中,线程在尝试获取锁时,如果锁已被其他线程占用,那么当前线程会被阻塞,一直等到锁被释放后才能继续执行。在阻塞模式下,线程可能会等待相当长的时间,直到获取到锁。
 
ReentrantLock lock = new ReentrantLock();// 阻塞方式获取锁
lock.lock();
try {// 执行需要锁保护的代码
} finally {lock.unlock();
} 
非阻塞方式
- 在非阻塞方式中,线程尝试获取锁时,如果锁已被其他线程占用,当前线程不会被阻塞,而是立即返回一个结果,告知是否成功获取锁。非阻塞方式下,线程不会等待,而是可以继续执行其他操作。
 
ReentrantLock lock = new ReentrantLock();// 非阻塞方式尝试获取锁
if (lock.tryLock()) {try {// 执行需要锁保护的代码} finally {lock.unlock();}
} else {// 未获取到锁的处理逻辑
} 
看门狗Watchdog

Redisson 使用看门狗(Watchdog)机制来保持分布式锁的有效性。看门狗是一种定时任务,负责定期延长锁的过期时间,确保在业务执行时间较长或者发生异常情况时,锁不会过早释放。
下面是 Redisson 看门狗的简要原理:
- 锁的过期时间: 当获取分布式锁时,会设置锁的过期时间(通常是锁的租期)。这个过期时间是在 Redis 中设置的,表示锁在这段时间内有效。
 - 看门狗的作用: Redisson 的看门狗定期(比如每隔一定时间)检查当前线程持有的锁是否过期。如果锁的过期时间快到了,看门狗会尝试续租,延长锁的过期时间。
 - 续租操作: 续租操作是通过发送一个延长锁过期时间的命令到 Redis。如果当前线程在续租时发生了异常,比如网络异常,看门狗会尽力保证在后续的定时任务中继续尝试续租。
 - 锁的释放: 如果看门狗发现锁已经过期且无法续租,它会尝试删除锁,释放资源。这是为了防止因为业务执行时间较长或者发生异常情况导致锁一直被占用而不释放。
 - 线程关闭时的处理: Redisson 看门狗还处理了线程关闭的情况。如果获取锁的线程关闭了,看门狗会立即释放锁,以避免死锁情况。
 
通过看门狗机制,Redisson 能够确保在使用分布式锁的场景下,锁不会因为持有锁的线程异常退出或者执行时间过长而导致锁被过早释放。这提高了分布式锁的可靠性和稳定性。
源码解析

这段代码是 Redisson 中续租锁过期时间的方法。让我们逐步解析其中的关键部分:
renewExpiration 方法: 这个方法用于执行锁的过期时间续租操作。
private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}// 创建定时任务,定时执行续租操作Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {// 获取续租信息ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 异步执行续租操作RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {// 续租失败,记录错误日志,移除续租信息log.error("Can't update lock " + getRawName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// 续租成功,重新调度续租任务renewExpiration();} else {// 续租失败,取消续租任务cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 续租时间为租约时间的1/3// 将定时任务绑定到续租信息中ee.setTimeout(task);
}
 
internalLockLeaseTime 变量

定时任务创建: 使用 commandExecutor.getConnectionManager().newTimeout 创建一个定时任务,这个任务会在 internalLockLeaseTime / 3 毫秒后执行。
续租操作: 在定时任务执行时,异步执行 renewExpirationAsync 方法,该方法负责向 Redis 发送命令更新锁的过期时间。
回调处理: 在异步续租操作完成时,根据续租操作的结果,进行相应的处理。
- 如果续租成功,重新调度下一次续租任务。
 - 如果续租失败,取消续租任务,并记录错误日志。
 
这个机制通过定时任务实现了定期的锁续租,确保分布式锁在持有期间不会因为过期而被自动释放。
相关文章:
Redission · 可重入锁(Reentrant Lock)
前言 Redisson是一个强大的分布式Java对象和服务库,专为简化在分布式环境中的Java开发而设计。通过Redisson,开发人员可以轻松地在分布式系统中共享数据、实现分布式锁、创建分布式对象,并处理各种分布式场景的挑战。 Redisson的设计灵感来…...
初阶C语言-指针
1.指针是什么? 理解指针的两个要点: 1.指针是内存中一个最小单元的编号,也就是地址 2.口头语中说的指针,通常是指指针变量,是用来存放内存地址的变量 总结:指针就是地址,口语中说的指针通常是指…...
论文笔记:微表情欺骗检测
整理了AAAI2018 Deception Detection in Videos 论文的阅读笔记 背景模型实验可视化 背景 欺骗在我们的日常生活中很常见。一些谎言是无害的,而另一些谎言可能会产生严重的后果。例如,在法庭上撒谎可能会影响司法公正,让有罪的被告逍遥法外。…...
智能家居有哪些产品?生活中常见的人工智能有哪些?
智能家居有哪些产品? 1、智能照明设备类:智能开关、智能插座、灯控模块、智能空开、智能灯、无线开关。 2、家庭安防类:智能门锁、智能摄像机、智能猫眼、智能门铃。 3、智能传感器类:烟雾传感器、可燃气体传感器、水浸传感器、声光报警器…...
洗车行软件系统有哪些 佳易王洗车店会员管理系统操作教程#洗车店会员软件试用版下载
一、前言 【试用版软件下载可点击本文章最下方官网卡片】 洗车行软件系统有哪些 佳易王洗车店会员管理系统操作教程#洗车店会员软件试用版下载 洗车管理软件应用是洗车业务的得力助手,实现会员管理及数据统计一体化,助力店铺高效、有序运营。 洗车项…...
【Java】springboot 项目中出现中文乱码
在刚创建的springboot项目中,出现乱码,跟走着解决一下 1、Ctrl Shift S 打开idea设置,根据图片来,将③④这三个地方都修改为UTF-8 2、返回配置查看,解决...
开放式耳机是什么意思?漏音吗?开放式的运动蓝牙耳机推荐
目前运动耳机市场主要分为入耳式、骨传导和开放式三类。入耳式耳机占比30%-40%,虽目前占比较大,但因在运动场景下有闷塞感、出汗不适、屏蔽外界环境音带来安全隐患等缺点,占比会逐渐下降。 骨传导耳机占比也为30%-40%,其不堵塞耳…...
如何优雅的处理NPE问题?
1.什么是NPE? NPE,即NullPointerException,是开发中最常见的问题之一,有必要知道如何正确地处理NPE。 对于 Java 开发者来说,null 是一个令人头疼的类型,一不小心就会发生 NPE (空指针…...
k8s 中存储之 NFS 卷
目录 1 NFS 卷的介绍 2 NFS 卷的实践操作 2.1 部署一台 NFS 共享主机 2.2 在所有k8s节点中安装nfs-utils 2.3 部署nfs卷 2.3.1 生成 pod 清单文件 2.3.2 修改 pod 清单文件增加 实现 NFS卷 挂载的 参数 2.3.3 声明签单文件并查看是否创建成功 2.3.4 在 NFS 服务器 创建默认发布…...
Redis中BitMap实现签到与统计连续签到功能
服务层代码 //签到Overridepublic Result sign() {//1.获取当前登录的用户Long userId UserHolder.getUser().getId();//获取日期LocalDateTime now LocalDateTime.now();//拼接keyString keySuffix now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String …...
【Spring】“请求“ 之传递 JSON 数据
文章目录 JSON 概念JSON 语法JSON 的语法JSON 的两种结构 JSON 字符串和 Java 对象互转JSON 优点传递 JSON 对象 JSON 概念 JSON:JavaScript Object Notation【JavaScript 对象表示法】 JSON 就是一种数据格式,有自己的格式和语法,使用文本…...
文心一言 VS 讯飞星火 VS chatgpt (359)-- 算法导论24.3 1题
一、在图 24-2上运行Dijkstra算法,第一次使用结点 s s s作为源结点,第二次使用结点 z z z作为源结点。以类似于图 24-6 的风格,给出每次while循环后的 d d d值和 π π π值,以及集合 S S S中的所有结点。如果要写代码,…...
Redis-预热雪崩击穿穿透
预热雪崩穿透击穿 缓存预热 缓存雪崩 有这两种原因 redis key 永不过期or过期时间错开redis 缓存集群实现高可用 主从哨兵Redis Cluster开启redis持久化aof,rdb,尽快恢复集群 多缓存结合预防雪崩:本地缓存 ehcache redis 缓存服务降级&…...
jvisualvm学习
系列文章目录 JavaSE基础知识、数据类型学习万年历项目代码逻辑训练习题代码逻辑训练习题方法、数组学习图书管理系统项目面向对象编程:封装、继承、多态学习封装继承多态习题常用类、包装类、异常处理机制学习集合学习IO流、多线程学习仓库管理系统JavaSE项目员工…...
Gazebo环境下开源UAV与USV联合仿真平台
推荐一个ROS2下基于Gazebo环境的开源UAV与USV联合仿真平台。平台是由两个开源项目共同搭建的。首先是UAV仿真平台,是基于PX4官方仿真平台(https://docs.px4.io/main/en/sim_gazebo_gz);其次是USV仿真平台,是基于VRX仿真…...
Linux进程调度和进程切换
并行(Parallel) 含义:并行是指多个任务在同一时刻同时执行。 硬件要求:需要多个处理器(如多核CPU)或者多台计算设备来实现,这些执行单元能够真正地同时处理不同的任务。例如,一个具…...
机器学习基本上就是特征工程——《特征工程训练营》
作为机器学习流程的一部分,特征工程是对数据进行转化以提高机器学习性能的艺术。 当前有关机器学习的讨论主要以模型为中心。更应该关注以数据为中心的机器学习方法。 本书旨在介绍流行的特征工程技术,讨论何时以及如何运用这些技术的框架。我发现&…...
Android Framework AMS(01)AMS启动及相关初始化1-4
该系列文章总纲链接:专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明: 说明:本章节主要涉及systemserver启动AMS及初始化AMS相关操作。同时由于该部分内容分析过多,因此拆成2个章节,本章节是第一章节&…...
基于基于微信小程序的社区订餐系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…...
[单master节点k8s部署]29.Istio流量管理(五)
测试istio熔断管理。 采用httpbin镜像和fortio镜像,其中httpbin作为服务端,fortio是请求端。这两个的配置yaml文件都在istio的samples/httpbin目录下,fortio的配置文件在samples-client目录下。 [rootmaster httpbin]# ls gateway-api ht…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...
【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要: 近期,在使用较新版本的OpenSSH客户端连接老旧SSH服务器时,会遇到 "no matching key exchange method found", "n…...
【Linux】Linux 系统默认的目录及作用说明
博主介绍:✌全网粉丝23W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...
C++_哈希表
本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说,直接开始吧! 一、基础概念 1. 哈希核心思想: 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。理想目标:实现…...
JS的传统写法 vs 简写形式
一、条件判断与逻辑操作 三元运算符简化条件判断 // 传统写法 let result; if (someCondition) {result yes; } else {result no; }// 简写方式 const result someCondition ? yes : no;短路求值 // 传统写法 if (condition) {doSomething(); }// 简写方式 condition &…...
【QT】qtdesigner中将控件提升为自定义控件后,css设置样式不生效(已解决,图文详情)
目录 0.背景 1.解决思路 2.详细代码 0.背景 实际项目中遇到的问题,描述如下: 我在qtdesigner用界面拖了一个QTableView控件,object name为【tableView_electrode】,然后【提升为】了自定义的类【Steer_Electrode_Table】&…...
调试快捷键 pycharm vscode
目录 调试快捷键 pycharm vscode 修改快捷键 方法 1:通过菜单打开 方法 2:用快捷键打开 调试快捷键 pycharm Resume Program F9 Step Over F8 两个离的比较近,比较方便,比vscode的好。 vscode Continue F5 改为F9 S…...
