Redisson分布式锁原理解析
前言
首先Redis执行命令是单线程的,所以可以利用Redis实现分布式锁,而对于Redis单线程的问题,是其线程模型的问题,本篇重点是对目前流行的工具Redisson
怎么去实现的分布式锁进行深入理解;开始之前,我们可以下你思考一个问题,Redisson
的实现方式有何不同?为什么?
使用
引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.25.0</version>
</dependency>
添加配置
@Autowiredprivate RedisProperties redisProperties;@Beanpublic RedissonClient redisClient() {Config config = new Config();config.setTransportMode(TransportMode.NIO);SingleServerConfig singleServerConfig = config.useSingleServer();singleServerConfig.setAddress(String.format("redis://%s:%s", redisProperties.getHost(), redisProperties.getPort()));singleServerConfig.setPassword(redisProperties.getPassword());return Redisson.create(config);}
加锁
private final RedissonClient redissonClient;public void lock() throws Exception {RLock lock = redissonClient.getLock(PRODUCT_LOCK_KEY + id);if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {// 业务} else {// 业务}
}
如上使用是最简单的方式,Redission它底层封装了很多逻辑,但如果说要redis客户端实现,你要怎么实现?
Redis中本就有支持分布式锁的命令:setnx
,对应RedisTemplate
中,使用如下:
redisTemplate.opsForValue().setIfAbsent("lockkey", "", 1, TimeUnit.SECONDS);
使用Redis实现分布式锁需要注意的是不要产生死锁,所以使用Redis实现分布式锁有两种方式:
- setnx命令
- lua脚本
两种方式都是一个操作完成key,value的设置以及过期时间的设置,你是否有相关,他们的实现是否都一样,或者说,这个实现可以有其他实现方式?
源码
原理
- lua脚本保证多个命令的原子性;
- 采用
hash
数据结构,key为锁的名称,field是线程对应的名称,也因为这个数据结构,也支持可重入锁; - 定时延时的操作避免死锁(看门狗);
lock()
我们先看lock()
方法,tryLock()
和lock()
底层是一样的,所以我们只看lock方法;
@Overridepublic void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}}
位置:org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)
传参是:(-1, null, false)
/**
leaseTime: 过期时间
unit:过期时间单位
interruptibly: 信号量,是否打断线程
*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 取线程ID,后面作为锁的一个标志long threadId = Thread.currentThread().getId();// 加锁// 两个步骤:// 1. 利用lua脚本设置锁与过期时间(原子操作),过期时间lockWatchdogTimeout = 30 * 1000 = 30秒// 2. 执行过期时间刷新(这里的刷新时利用回调的方式 + 延迟执行实现的定时任务),lockWatchdogTimeout/3 = 10秒Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// lua脚本中,加锁成功返回nil,对应redis中是null,加锁失败,则返回的是已存在的锁的过期时间// 所以这里返回null,就是加锁成功了,就不再往下走了if (ttl == null) {return;}// 加锁成功的在上面就已经结束了,所以下面的都是加锁失败时走的// 这里它订阅了一个channel,参数threadId无用,别被误导了,它订阅的名称时固定的// 为什么这里要订阅?可以思考一下CompletableFuture<RedissonLockEntry> future = subscribe(threadId);// 检查是否超时pubSub.timeout(future);RedissonLockEntry entry;// 这里的interruptibly应该时程序一次时,是否结束,而不是一直在执行中if (interruptibly) {entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {// 这里就是一个自旋 + 加锁while (true) {// 每次循环都进行加锁操作ttl = tryAcquire(-1, leaseTime, unit, threadId);// 同样,如果这里时null,那么就是加锁成功了if (ttl == null) {break;}// 加锁失败:返回了锁的过期时间if (ttl >= 0) {try {// 信号量 + unsafe.park // 信号量:本地更改线程共享变量状态达到加锁的目的// unsafe.park:利用park方法将加锁失败(信号量更改失败)的线程进行挂起// 为什么要挂起,这个问题应该不用多说entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {// 这个分支时ttl < 0,也就是过期时间为负数,也就是锁失效了// 对本地加锁,之后在下一次循环redis加锁if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {unsubscribe(future, threadId);}
// get(lockAsync(leaseTime, unit));}
上面的步骤就是Redisson大致逻辑:
- lua脚本加锁,记录线程id,防止非本线程解锁
- 成功则退出
- 添加订阅对应的channel(这里的订阅是异步的)
- 唤醒:当收到channel的通知后,也就是上一个锁解锁了
- 从第6步醒来
- 自旋
- lua脚本加锁(与第一步一样),存在订阅后,上一个锁解锁了,就不用再挂起线程
- 失败挂起:unsafe.park
- 加锁成功后,取消订阅
lua脚本加锁
简单说一下这个脚本做了什么(lua脚本类似js):
redis.call
是调用redis命令,第一个参数是redis的命令,第二个参数是参数;
它先是exists
判断了key
,以及hash数据结构了的field
是否存在,
如果存在,对field
递增,为什么要递增?思考一下;并设置过期时间,返回返回nil
;
如果不存在,调用redis命令pttl
,获取key的过期时间,并返回;
那如果我们自己写lua脚本呢?
redis 2.6之后支持lua脚本,一个脚本,执行这个脚本是原子性的,所以脚本里的多个命令是原子性的。
如果说要执行批量的命令,可以使用piple
,但是管道的话,它并不是原子性的,他只是一次性把批量的命令发给了redis。
LUA脚本格式:
eval "脚本 KEYS[1...N] ARGV[1...N]" count key[1...N] argv[1...N]
KEYS[1…N]:key的展位符,多个时,序号递增,从1开始
ARGV[1…N]:value的展位符,多个时,序号递增,从1开始
count:是对应key输入的个数
key[1…N]:对应KEYS[0…N]的key
argv[1…N]:对应ARGV[0…N]的value
这里就大概简单的说明了一下,使得看本篇的朋友能够理解,详细的还请百度;
看门狗
使用过Redisson的朋友应该都听过“看门狗”,为什么Redisson加锁要看门狗呢?
如果我们使用lock()
方法,不设置过期时间,那么应该是永不过期;
好,如果说,加锁成功,在解锁时出了意外,如服务异常退出,或宕机,导致没有解锁,那么这个锁就需要人工干预了,这是有问题的。
所以,在Redisson中,它并不是永不过期,当我们使用lock()
方法时,它的参数时-1
,但在最终执行lua脚本时传入了默认参数:
位置:org.redisson.RedissonLock#tryAcquireAsync
过期时间时:internalLockLeaseTime;
可以看到在初始化时,它便赋予了该值一个默认的30秒;
它并没有将锁设置为无限时间,而是30秒,又怎么保证锁的有效?
所以它还有一个续约的操作,对未使用unlock
的锁进行时间延迟,这一操作是为了保证未来某一时刻如果出现服务或其他问题导致解锁失败,产生死锁这样的一个情况。
来看代码:
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;// leaseTime=-1表示永不过期if (leaseTime > 0) {// 对应方法:tryLock(过期时间, 时间单位)ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 对应方法:lock()// 注意它达到参数是:internalLockLeaseTime,上面说了他是30秒// internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000;ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 这里是上面ttlRemainingFuture回调结果处理,// 如果出现异常,就unlock解锁CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);
// 这里是定时刷新任务CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 执行成功返回的时null,所以这里以null为加锁成功的标志if (ttlRemaining == null) {if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}
handleNoSync
它只是针对异常做了处理,正常情况下只是进行了封装,而unlocakInnerAysnc
也是在异常时的一个回调;
return
是结果封装;
再回到刷新的部分:
它是在加锁完后的一个回调方法,ttlRemaining
它就是上面执行的结果,null或者是过期时间,
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;// leaseTime=-1表示永不过期if (leaseTime > 0) {// 对应方法:tryLock(过期时间, 时间单位)ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 对应方法:lock()// 注意它达到参数是:internalLockLeaseTime,上面说了他是30秒// internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000;ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 这里是上面ttlRemainingFuture回调结果处理,// 如果出现异常,就unlock解锁CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);
// 这里是定时刷新任务CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 执行成功返回的时null,所以这里以null为加锁成功的标志if (ttlRemaining == null) {if (leaseTime > 0) {// leaseTime > 0 表示加锁时设置了过期时间// 而ternalLockLeaseTime存在默认值,这里就是取消了默认值internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 那,这里执行 过期时间的定时刷新scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}
过期时间刷新的步骤:
- 创建ExpirationEntry,可以看作时一个上下文对象,他是延时的标识
- 原子操作,添加上下文ExpirationEntry
- 设置当前线程id到上下文
- 创建Timeout延时任务,延时10秒
- 延时任务执行(异步):
- 根据客户端id和锁获取上下文
- 通过上下文获取到线程id
- 异步执行延时lua脚本
- 执行回调:成功,回调本身,回调第四步,失败,移除上下文,取消延时
protected void scheduleExpirationRenewal(long threadId) {// 1. 创建ExpirationEntry,可以看作时一个上下文对象,他是延时的标识ExpirationEntry entry = new ExpirationEntry();// 2. 原子操作,添加上下文ExpirationEntry// 要进行过期时间刷新,就要先添加这个对象,可以理解为一个刷新的标志ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 3. 设置当前线程id到上下文if (oldEntry != null) {// 不为空,说明这个对象已经存在,已经有其他线程执行了过期刷新oldEntry.addThreadId(threadId);} else {// 为空,则是不存在该映射对象(锁不存在)entry.addThreadId(threadId);try {// 执行过期时间刷新// 4. 创建Timeout延时任务,延时10秒// 5. 延时任务执行(异步):// 5.1 根据客户端id和锁获取上下文// 5.2 通过上下文获取到线程id// 5.3 异步执行延时lua脚本// 5.4 执行回调:成功,回调本身,回调第四步,失败,移除上下文,取消延时renewExpiration();} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}}
下面我们进入renewExpiration
方法:
private void renewExpiration() {// EXPIRATION_RENEWAL_MAP 在上一层方法中添加了刷新的上下文标志ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {// 如果这里为null,就说明已经有其他线程移除了,那么就是不需要刷新,锁不存在了return;}// 4. 创建Timeout延时任务,延时10秒// 注意这里的时间: internalLockLeaseTime / 3// 之前说过:internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000;// 所以这里是延迟10秒Timeout task = getServiceManager().newTimeout(new TimerTask() {// 5. 延时任务执行(异步)@Overridepublic void run(Timeout timeout) throws Exception {// 5.1 根据客户端id和锁获取上下文// entryName是客户端id+线程id// 每次执行都要检查,刷新的标志存在,就说明锁还在,需要执行ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}// 5.2 通过上下文获取到线程idLong threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 5.3 异步执行延时lua脚本CompletionStage<Boolean> future = renewExpirationAsync(threadId);// 当延时完成后,执行这个方法,也是个回调future.whenComplete((res, e) -> {if (e != null) {log.error("Can't update lock {} expiration", getRawName(), e);// 异常时,移除刷新的标志// 也是避免死锁的一个方式EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}// 5.4 成功:回调本身,回调第四步,失败:移除上下文,取消延时if (res) {// 延时成功,回调当前这个方法,以实现定时任务renewExpiration();} else {// 没有成功,也是移除刷新的标志// 这里没有成功的情况,是锁已经不存在了,对应的定时任务就应该停止;// 两个步骤:// 1. task.cancel():通过上下文获取到任务,然后取消// 2. EXPIRATION_RENEWAL_MAP.remove(getEntryName());cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);// 将定时任务放到entry中,也就是放到了定时任务的上下文中// 在下一次时,通过上下文获取到这个定时任务ee.setTimeout(task);}
其实他延时的逻辑也是一个lua脚本:
漏了一点:
由Redisson实现JDK的Timeout
类,加回调完成的一个定时任务;当当前任务执行完后,又执行本身方法,创建一个延迟任务,也就实现了一个定时任务
相关文章:

Redisson分布式锁原理解析
前言 首先Redis执行命令是单线程的,所以可以利用Redis实现分布式锁,而对于Redis单线程的问题,是其线程模型的问题,本篇重点是对目前流行的工具Redisson怎么去实现的分布式锁进行深入理解;开始之前,我们可以…...

Linux RS232
一、确认硬件信息 RS232: 引脚信息: 二、软件配置 1、pinctrl信息: 2、设备树节点: 3、修改串口支持的模式 三、驱动 bsp/drivers/uart/sunxi-uart.c 四、烧录测试 查看串口参数: stty -F /dev/ttyAS3 -a stty -F…...

英伟达Docker 安装与GPu镜像拉取
获取nvidia_docker压缩包nvidia_docker.tgz将压缩包上传至服务器指定目录解压nvidia_docker.tgz压缩包 tar -zxvf 压缩包执行rpm安装命令: #查看指定rpm包安装情况 rpm -qa | grep libstdc #查看指定rpm包下的依赖包的版本情况 strings /lib64/libstdc |grep GLI…...

智慧交通的神经中枢:利用ARMxy进行实时交通流数据采集
气候变化和水资源日益紧张,精准农业成为了提高农业生产效率、节约资源的关键。在这一变革中,ARMxy工业计算机扮演了核心角色,特别是在智能灌溉系统的实施中。 背景介绍: 某大型农场面临着灌溉效率低、水资源浪费严重的问题。传统的…...
文心一言使用技巧
前言 文心一言是一款基于人工智能技术的自然语言处理工具,它可以帮助用户生成、编辑和优化各种类型的文本。无论是写作、翻译、总结,还是进行信息提取和数据分析,文心一言都能提供强大的支持。本文将详细介绍文心一言的使用技巧,…...
技术人如何打造研发团队
技术人作为写代码一路走上来,其实不像销售岗位,售后交付岗位与人的打交道那么多。主要是很简单的技术沟通,在慢慢走上管理岗位后,也是依据自己的经验,自己的感觉来管理团队,很多时候自己的事情不但没少&…...

月薪6万,想离职...
大家好,我是无界生长,国内最大AI付费社群“AI破局俱乐部”初创合伙人。这是我的第 39 篇原创文章——《月薪6万,想离职...》 是的,你没有看错,我月薪6万,却想离职,很不可思议吧?周围…...

ReentrantLock底层原理
ReentrantLock public ReentrantLock() {sync new NonfairSync(); }public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync(); }ReentrantLock 的默认实现是非公平锁,实际上 ReentrantLock 中的方法,几乎都让 sync 实现…...

基于JSP的医院远程诊断系统
开头语: 你好呀,我是计算机学长猫哥!如果有相关需求,文末可以找到我的联系方式。 开发语言: Java 数据库: MySQL 技术: JSP Servlet JSPBean 工具: IDEA/Eclipse、Navica…...

项目:基于httplib/消息队列负载均衡式在线OJ
文章目录 写在前面关于组件开源仓库和项目上线其他文档说明项目亮点 使用技术和环境项目宏观结构模块实现compiler模块runner模块compile_run模块compile_server模块 基于MVC结构的OJ服务什么是MVC?用户请求服务路由功能Model模块view模块Control模块 写在前面 关于…...

详解python中的pandas.read_csv()函数
😎 作者介绍:我是程序员洲洲,一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。 🤓 同时欢迎大家关注其他专栏,我将分享Web前后端开发、人工智能、机器学习、深…...
速盾:DDoS高防IP上设置转发规则
DDoS攻击是一种网络攻击方式,攻击者通过大量请求使目标服务器或网络资源超负荷运行,导致服务不可用。为了保护网络安全,减少DDoS攻击对网络的影响,使用DDoS高防IP可以是一种解决方案。而在DDoS高防IP上设置转发规则可以提高网络的…...
京东一面测开(KPI)
京东一面测开凉经(笔试ak) 3.8 面试官:你很优秀啊,你不用谦虚 没问技术相关,问了如何设计测试用例步骤一些理论: 什么是软件测试?其目的是什么? 软件测试有哪些类型?请列…...

Django框架中级
Django框架中级 – 潘登同学的WEB框架 文章目录 Django框架中级 -- 潘登同学的WEB框架 中间件自定义中间件常用中间件process_view() 使用中间件进行URL过滤 Django生命周期生命周期分析 Django日志日志配置filter过滤器自定义filter 日志格式化formatter Django信号内置信号定…...
cordova-plugin-inappbrowser内置浏览器插件
一、InAppBrowser(内置浏览器) 允许在在单独的窗口中加载网页。例如要向应用用户展示其他网页。当然可以很容易地在应用中加载网页内容并管理,但有时候需要不同的用户体验,InAppBrowser加载网页内容,应用用户可以更方便的直接返回到主应用。 二、安装命令: cordova pl…...

打造智慧工厂核心:ARMxy工业PC与Linux系统
智能制造正以前所未有的速度重塑全球工业格局,而位于这场革命核心的,正是那些能够精准响应复杂生产需求、高效驱动自动化流程的先进设备。钡铼技术ARMxy工业计算机,以其独特的设计哲学与卓越的技术性能,正成为众多现代化生产线背后…...
Java File IO
Java File IO ~主要介绍四个类 InputStream OutputStream FileReader FileWriter~ InputStream (字节流读取File) public static void main(String[] args) throws IOException {String filePath "D:\\Javaideaporject\\JavaBaseSolid8\\File\\t…...

MySQL 函数与约束
MySQL 函数与约束 文章目录 MySQL 函数与约束1 函数1.1 字符串函数1.2 数值函数1.3 日期函数1.4 流程函数 2 约束2.1 概述2.2 约束演示2.3 外键约束2.4 删除/更新行为 1 函数 函数是指一段可以直接被另一程序调用的程序或代码。 1.1 字符串函数 MySQL中内置了很多字符串函数&…...
12_1 Linux Yum进阶与DNS服务
12_1 Linux Yum进阶与DNS服务 文章目录 12_1 Linux Yum进阶与DNS服务[toc]1. Yum进阶1.1 自定义yum仓库1.2 网络Yum仓库 2. DNS服务2.1 为什么要使用DNS系统2.2 DNS服务器的功能2.3 DNS服务器分类2.4 DNS服务使用的软件及配置2.5 搭建DNS服务示例2.6 DNS特殊解析 1. Yum进阶 1…...
Spring Boot集成geodesy实现距离计算
1.什么是geodesy? 浩瀚的宇宙中,地球是我们赖以生存的家园。自古以来,人类一直对星球上的位置和彼此的距离着迷。无论是航海探险、贸易往来还是科学研究,精确计算两个地点之间的距离都是至关重要的。 Geodesy:大地测量…...

龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

elementUI点击浏览table所选行数据查看文档
项目场景: table按照要求特定的数据变成按钮可以点击 解决方案: <el-table-columnprop"mlname"label"名称"align"center"width"180"><template slot-scope"scope"><el-buttonv-if&qu…...

算术操作符与类型转换:从基础到精通
目录 前言:从基础到实践——探索运算符与类型转换的奥秘 算术操作符超级详解 算术操作符:、-、*、/、% 赋值操作符:和复合赋值 单⽬操作符:、--、、- 前言:从基础到实践——探索运算符与类型转换的奥秘 在先前的文…...
基于Uniapp的HarmonyOS 5.0体育应用开发攻略
一、技术架构设计 1.混合开发框架选型 (1)使用Uniapp 3.8版本支持ArkTS编译 (2)通过uni-harmony插件调用原生能力 (3)分层架构设计: graph TDA[UI层] -->|Vue语法| B(Uniapp框架)B --&g…...
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not explicitly defined.
这个警告表明您在使用Vue的esm-bundler构建版本时,未明确定义编译时特性标志。以下是详细解释和解决方案: 问题原因: 该标志是Vue 3.4引入的编译时特性标志,用于控制生产环境下SSR水合不匹配错误的详细报告1使用esm-bundler…...
生成对抗网络(GAN)损失函数解读
GAN损失函数的形式: 以下是对每个部分的解读: 1. , :这个部分表示生成器(Generator)G的目标是最小化损失函数。 :判别器(Discriminator)D的目标是最大化损失函数。 GAN的训…...