Redis分布式锁深度解析:从SETNX到Redlock的演进与实战
你有没有遇到过这种情况:凌晨两点某个定时任务准时触发,早上来公司一看,负责同一业务的两台服务器都把这个任务跑了一遍。订单重复退款、短信重复发送、报表数据翻倍,线上事故就这么出来了。
这背后就是典型的分布式锁问题。单机时代我们用 synchronized 或者 ReentrantLock 就能解决并发竞争,可一旦服务拆成多节点部署,进程之间的锁就没法互相感知了。这时候 Redis 凭借单线程命令执行、高性能、易接入的特点,成了实现分布式锁最常用的载体。从最基础的 SET NX 到社区讨论激烈的 Redlock 算法,这条路我踩过不少坑,也沉淀了一些自己的理解,这篇就系统梳理一下。
这篇文章适合正在用 Redis 做分布式锁、准备在集群环境中防重跑任务的开发者,也适合准备分布式锁面试题的同学。我会从最原始的 SETNX 命令讲起,把为什么会出现 SET key value NX EX、为什么还需要 Lua 脚本、为什么最后会扯出 Redlock 这一整条演进逻辑线说清楚,最后给出一套能直接落地的方案。
1. 从单机锁到分布式锁:问题本质是什么
很多人一上来就写 SETNX,但没想明白分布式锁到底在解决什么问题。搞不清楚本质,后续的坑基本躲不掉。
1.1 单机锁为什么管不住多节点
Java 里的 synchronized 和 ReentrantLock,本质上依赖的是 JVM 内存中的 Monitor 对象。线程在进入临界区之前,需要先获取到这个锁对象,而这个锁对象只存在于当前进程的内存空间里。
当你的服务部署成两个节点,每个节点都是独立的 JVM 进程。节点 A 上的线程持有锁,节点 B 上的线程根本无法感知这个状态,它去获取的是自己 JVM 里的另一个锁对象。两个节点上的线程同时冲进临界区,锁的互斥效果就完全失效了。
这里关键点在于:分布式锁必须有一个所有节点都能访问到的公共状态存储。所有节点在这个公共存储上做竞争、做标记,才能实现跨进程的互斥。
1.2 为什么选 Redis 而不是数据库或 ZooKeeper
能做公共状态存储的不止 Redis 一个,市面上常见的方案有三类:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 数据库唯一索引 | 实现简单,无需额外组件 | 性能差,每次加锁都是 DB 写操作,且依赖事务,高并发下容易成为瓶颈 |
| ZooKeeper 临时顺序节点 | 可靠性高,有 Watch 机制自动通知 | 需要维护 ZK 集群,客户端较重,加锁解锁的 RTT 相对较高 |
| Redis SET NX | 性能极高,单线程模型天然保证原子性,接入成本低 | 极端场景下有锁丢失风险,需要自己处理续期和容错 |
实际开发中,大部分业务场景对锁的要求是「高性能 + 基本可靠」,而不是「绝对可靠」。Redis 在绝大多数场景下是性价比最高的选择。我见过很多团队用数据库唯一索引做分布式锁,并发一上来,数据库连接池先被打爆。而 Redis 单机就能扛住每秒几万甚至十几万的加锁请求,不在一个量级。
1.3 分布式锁的三个核心要求
不管是哪种实现,一个合格的分布式锁必须满足三个条件:
互斥性:任意时刻,只有一个客户端持有锁。
安全性:持有锁的客户端崩溃后,锁能被自动释放,不会造成死锁。这要求锁必须设置过期时间,或者有类似的兜底机制。
可用性:加锁和解锁的过程必须原子性,不能出现"设置锁成功了,但设置过期时间失败了"这种情况。
注意第三点。很多人写分布式锁出 bug,都是因为把加锁和设置过期时间拆成了两步。我后面会详细讲这个坑。
2. SET NX 的演变:从两条命令到一条原子指令
Redis 实现分布式锁的思路很简单:利用 SET 命令的 NX 参数,只有 key 不存在时才能写入成功。写入成功代表加锁成功,删除 key 代表释放锁。
但这中间经历了好几个版本的演进,每个版本都是在修复上一个版本的漏洞。
2.1 最初的 SETNX 加 EXPIRE 两步走
早期的 Redis 版本里,加锁的经典写法是:
第一条命令尝试写入 lock_key,如果 key 不存在则写入成功返回 1,代表加锁成功;如果 key 已存在则返回 0,说明锁已被别人持有。第二条命令给锁设置 30 秒的过期时间,防止持有者崩溃后锁永远不释放。
这段逻辑看起来没问题,但有个致命缺陷:SETNX 和 EXPIRE 不是原子的。如果客户端在 SETNX 执行成功后、EXPIRE 执行前突然崩溃,锁就没有过期时间,会一直被占着。其他客户端永远拿不到锁,整个系统直接瘫痪。
有人会说,那我先 EXPIRE 再 SETNX 行不行?也不行,因为 key 可能本来就不存在,EXPIRE 直接作用于一个不存在的 key,没有任何效果。而且先设置过期时间再写入 key,中间还是有窗口期。
这个问题的根源是两条命令无法保证原子性,解决方案有两个方向:用 Lua 脚本把两步封装成原子操作,或者等 Redis 官方提供原子指令。
2.2 Redis 2.6.12 带来的 SET 原子指令
Redis 从 2.6.12 版本开始,扩展了 SET 命令的能力,支持同时设置 NX 和 EX 参数:
这条命令的语义是:当 lock_key 不存在时,设置 lock_key 的值为 unique_value,并同时设置 30 秒过期时间。整个过程是原子的,由 Redis 单线程模型保证执行期间不会被其他命令插入。
这才是现代分布式锁的起点。这条命令解决了两步操作的非原子性问题,也是后面所有围绕 SET NX 讨论的基础。现在互联网公司里大部分老旧代码,其实还停留在这条命令或者更早的 SETNX 写法上,能做对第一步的都不多。
2.3 value 的设计:为什么不能随便传一个字符串
很多人在写 SET lock_key value NX EX 30 的时候,value 随便传了个值,比如 "1"、当前时间戳、甚至是业务名称。这个细节看着不起眼,但直接关系到锁释放的安全性。
锁释放的正确流程是:获取锁时保存 value,释放锁时先判断当前锁的 value 是否等于自己保存的 value,相等才能删除。这个过程通常用 Lua 脚本实现原子性:
为什么要判断 value?因为存在一种情况:线程 A 持有锁后,处理时间超过了过期时间,锁被 Redis 自动释放了。这时线程 B 成功加锁。接着线程 A 处理完毕,直接执行 DEL lock_key,这把 B 的锁删掉了。B 的临界区还在执行,另一个线程 C 又成功加锁,三个人同时进入临界区,分布式锁形同虚设。
如果 value 是每个线程唯一的随机值(比如 UUID + 线程ID),A 释放锁时发现当前 value 不是自己的,就不会执行删除,B 的锁得以保留。
我看到热搜词里有"Value为当前日期"这种用法。说实话,用当前日期做 value 有参考意义吗?如果只是为了排查问题方便,看锁是什么时候创建的,可以理解。但从锁安全性的角度,当前日期不具备唯一性,同一秒内两个线程的值完全一样,无法区分持有者。真正做校验时,value 必须能唯一标识一个线程或一次请求。
2.4 过期时间设多少才合理
过期时间的长短,本质是在两个风险之间做权衡:设得太短,业务还没处理完锁就失效了;设得太长,持有者崩溃后锁长时间占着不放,影响系统可用性。
一个常用的估算方法是:先压测出业务临界区的最大执行时间,再乘上 2 到 3 倍的冗余系数。比如某个任务最大执行时间是 8 秒,过期时间设为 20 到 30 秒比较稳妥。
但这种静态设置依然有问题。线上环境的 GC 停顿、网络抖动、IO 阻塞都可能让任务执行时间超过预期。Java 服务一次 Full GC 停顿几十秒都不稀奇,锁早就过期了。这就是后面要说的看门狗续期机制要解决的问题。
3. 锁续期与可重入:SET NX 之后的第一层纵深
前面说的方案,假设业务执行时间一定小于过期时间。但真实世界不给你这个保证,所以成熟方案必须在 "锁过期了但业务没跑完" 这件事上有应对策略。
3.1 看门狗机制的原理
Redisson 里的分布式锁有一个默认行为:拿到锁之后,会启动一个后台定时任务,每隔 10 秒检查一次锁是否还被当前线程持有。如果持有,就把过期时间重置为 30 秒。这样只要客户端一直持有锁,锁的过期时间就会被不断延长,业务跑多久都不怕锁提前失效。
这个后台任务就是"看门狗"。它的工作逻辑是:
- 加锁成功后,记录当前线程标识和锁的过期时间(默认 30 秒)。
- 启动一个定时任务,每过 10 秒执行一次。10 秒是 30 秒的三分之一,这个间隔确保在锁过期之前至少能续期一次。
- 续期操作通过 Lua 脚本完成,先校验锁的 value 是否还属于当前线程,是才更新过期时间。
- 释放锁时,取消定时任务。
这套机制解决了"业务执行时间不可控"的问题。但注意,看门狗只在客户端正常运行时才能续期。如果客户端进程直接宕机,定时任务跟着消失,锁会在 30 秒后自动过期,其他节点可以重新获取锁,这恰好是期望的行为。
3.2 可重入锁:同线程为何需要重复获取锁
分布式锁还有一个容易被忽略的需求:可重入性。同一个线程在持有锁的情况下,再次调用加锁逻辑,应该能成功,而不是把自己锁死。
考虑一个递归函数,或者一个服务 A 调用服务 B、B 内部又要调用同一个加锁方法的方法的场景。如果没有可重入性,第二次加锁会因为锁已存在而失败,导致业务异常。
Redisson 的实现方式是在 value 中记录持有者信息,同时维护一个加锁次数计数器。同一个线程重复加锁时,判断持有者标识一致,计数器加一。释放锁时计数器减一,减到零才真正删除锁。
3.3 主从复制架构下的锁丢失问题
现在很多 Redis 部署是主从架构,主节点负责写,从节点负责读。这里有一个非常隐蔽的风险:
客户端在主节点上 SET 加锁成功,数据还没来得及同步到从节点,主节点突然宕机。哨兵检测到主节点故障,把从节点提升为新的主节点。此时新主节点上并没有锁的数据,另一个客户端成功加锁。两个客户端同时持有锁,互斥被打破。
这不是理论上的漏洞,而是真实发生过的事故。Redis 主从复制的异步特性决定了这种丢失无法完全避免。要彻底解决这个问题,需要引入多节点的确认机制,也就是 Redlock 算法的出发点。
4. Redlock 算法的完整拆解:为什么需要、怎么实现、是否有用
Redlock 是 Redis 作者 antirez 提出的一种分布式锁算法,全称是 Redlock Algorithm。它的设计目标很明确:在主从切换等极端场景下,依然能保证分布式锁的互斥性。
4.1 多节点独立加锁的核心思路
Redlock 解决锁丢失问题的思路是:不在单个 Redis 节点上加锁,而是在多个相互独立的 Redis 节点上都加锁,只有大多数节点加锁成功,才算真正成功。
这就好比你有一个重要的决定要做,不是问一个顾问,而是同时问五个互不通信的顾问,至少三个都同意,才执行。单个顾问出问题,不影响整体决策。
具体步骤是:
- 获取当前时间戳,记为 T1。
- 依次向 N 个独立的 Redis 节点发起加锁请求(使用
SET key value NX EX指令)。加锁的超时时间设置得很短,比如 50 毫秒。目的是避免某个节点不可用时,客户端一直在等它。 - 计算整个加锁过程消耗的时间:
T_elapsed = T2 - T1。 - 判断加锁是否成功:至少在
N/2 + 1个节点上加锁成功,且T_elapsed小于锁的过期时间。 - 如果加锁成功,锁的有效时间 = 锁的原始过期时间 - 已消耗时间。
- 如果加锁失败(不足大多数节点成功,或者消耗时间超过了过期时间),向所有节点发送释放锁请求,清理可能已经成功加锁的部分节点。
注意第 5 步,这是 Redlock 中经常被忽略但很关键的细节。如果加锁过程消耗了 5 秒,而锁的过期时间是 10 秒,那么锁的实际有效时间只有 5 秒。这段时间内必须完成业务操作,否则锁可能提前失效。
4.2 Redlock 的时间估算和重试策略
Redlock 的部署节点数,官方推荐是 5 个。2 个节点不满足大多数原则,3 个节点中只要挂 1 个就无法加锁,5 个节点能容忍 2 个节点故障。
这里有一个数学上的考量。在 5 个节点中,需要至少 3 个节点加锁成功。这意味着系统能容忍最多 2 个节点宕机或不可用。同时,加锁的超时时间需要非常短,比如总锁的有效期是 10 秒,单个节点加锁超时 50 毫秒就放弃,避免在故障节点上浪费时间。
重试策略上,Redlock 建议在加锁失败后随机退避一段时间再重试,避免多个客户端同时重试造成活锁。随机退避的公式可以是 Math.random() * baseDelay + minDelay,具体数值需要根据业务对延迟的容忍度来调整。
4.3 围绕 Redlock 的争论与我的实践判断
Redlock 从提出那天起就有争议。最有名的是分布式系统专家 Martin Kleppmann 的质疑和 antirez 的反驳。
Martin 的核心论点有两个:一是分布式锁的健壮性依赖于时钟的准确性,但 Redis 节点之间没有共享同一套时钟,进程暂停(GC)期间锁可能已经过期;二是锁的意义在于保护共享资源,如果持有锁的客户端发生 GC 停顿,超过锁过期时间,其他客户端也能拿到锁,那锁就失效了。
antirez 的回应是:这些理论问题在大多数实际场景下并不会触发。对于非绝对安全的业务场景(比如防重复执行定时任务、库存扣减),Redlock 已经足够。他还强调,Redlock 明确要求所有节点使用物理时钟,且每个节点的时钟偏差要控制在很小范围内。
我的实践判断是这样:如果你的业务是金融级别的强一致场景,直接用 Redlock 不如引入一个共识组件(比如 ZooKeeper 或 etcd)来得稳。如果你的业务是互联网常见的"允许少量重复、但绝不允许大规模并发执行"场景,在主从架构的 Redis 上用 SET NX + 看门狗方案完全够用。不要为了追求理论上的绝对安全,给系统引入不必要的复杂度。这是我做过很多分布式锁优化后的真实体会。
5. 从理论到实战:一套可落地的分布式锁方案
前面讲了不少原理和演进,现在进入实操环节。我给出两种基于实际业务场景的方案,一种是纯 RedisTemplate 手写,一种是基于 Redisson 的成熟方案,最后聊一下定时任务防重复执行这个高频需求。
5.1 基于 RedisTemplate 的最小可用实现
大部分 Spring Boot 项目里都有 RedisTemplate,用它手写一个分布式锁并不复杂。下面这个实现包含了唯一 value 校验和 Lua 脚本释放锁,我看过很多团队最终稳定运行的版本,核心逻辑基本就是这个。
这里有几个细节值得说明。
lockValue 使用 UUID + 线程 ID,确保每个线程每次加锁的值都是唯一的。检测锁是否属于自己的时候,只有 value 完全匹配才执行删除,防止误删其他线程的锁。
释放锁必须用 Lua 脚本把"检查 value"和"删除 key"两步合在一起,保证原子性。如果用 Java 代码先 GET 再 DEL,中间有窗口期,其他线程可能在这期间替换了锁,导致误删。
加锁也用了 Redis 原子指令 SET key value NX EX,而不是 SETNX 加 EXPIRE 两步操作。这个细节我之前讲过了,是所有正确实现的基础。
5.2 Redisson 方案:看门狗续期和可重入不用自己写
手写方案能做到正确,但缺少两个重要能力:锁自动续期和可重入。Redisson 这两点都帮你实现了。
注意上面的写法中,tryLock(5, 30, TimeUnit.SECONDS) 的语义是等待最多 5 秒、leaseTime 为 30 秒。这里有个很多人踩过的坑:一旦手动指定了 leaseTime,Redisson 就不会启动看门狗续期。只有不传 leaseTime 的 tryLock() 或 lock(),才会走默认的 30 秒过期时间加上看门狗自动续期机制。
完整调用链加锁、执行业务、释放锁,全程不需要我们手动续期。Redisson 的 watch dog 会保证锁不会因为业务执行时间过长而提前失效。这在手写方案里需要额外维护一个定时任务线程,复杂度高很多。
5.3 定时任务防重复执行的完整方案
现在回到开头那个事故场景:两台服务器上的定时任务重复执行。
要彻底解决,需要在定时任务内部加分布式锁。这里的"定时任务"泛指所有通过 XXL-Job、ElasticJob、Spring Scheduler 等框架调度的周期性任务。这些框架本身有分片功能,但如果你用的是简单的 @Scheduled,或者框架配置里没开分片,双重执行的概率就很高。
我给出一个标准的执行模板:
这个模板里有两个容易被忽视的细节。
第一个:任务执行完后必须在 finally 中释放锁,不能因为业务异常导致锁泄漏。很多线上事故的起因就是忘了 finally,业务抛异常,锁一直占着,后续任务全部跳过。
第二个:使用 UUID 作为 value,释放锁时校验。这确保如果本节点因为某些原因持锁时间超过过期时间,锁被自动释放后,本节点的 finally 不会误删其他节点后来获取的锁。
5.4 见过最多的误用场景和典型错误
我在Code Review里见过很多分布式锁的错误用法,列几个高频的。
错误一:用 RedisTemplate 的 setIfAbsent 方法,但没用 Lua 脚本释放锁。
从 GET 到 DELETE 之间,锁可能已经被另一个线程抢占,然后被本线程误删。正确做法是用 Lua 脚本保证获取和删除的原子性。
错误二:加锁和 try-catch-finally 位置不对。
正确做法是把加锁结果用一个变量保存,在 finally 里先判断再加锁是否成功,才决定是否解锁。
错误三:锁粒度太大或太小。
把整个接口包在分布式锁里是常见错误。锁粒度太大,所有请求串行化,系统的吞吐量直线下降。锁粒度太小,比如只锁住了某一行数据的修改,但冲突集中在同一个 key 上,等于没保护。锁的粒度应该精确作用于共享资源的写操作窗口,而不是整个业务流程。
5.5 锁冲突检测与监控:你需要知道锁是否正常工作
分布式锁不是写完就能一直正常运转的,需要在生产环境里加监控。我的做法是监控三个核心指标:
第一,加锁失败率。正常情况下,业务错峰运行时加锁失败率很低。如果某个时间点失败率突然升高,说明多个节点同时竞争某个锁,可能存在流量倾斜或任务编排问题。
第二,锁等待耗时。用 Redisson 的 tryLock(waitTime, ...) 时,如果大部分请求都在等待锁,说明锁的持有时间过长,临界区代码需要优化,或者锁粒度设置不当。
第三,持有锁的时间分布。正常情况下,业务持有锁的时间应该是稳定在某个范围内。如果出现尾部耗时特别长的请求,可能是网络抖动或者 GC 停顿导致持锁时间异常,也可能是看门狗续期失效。
这几个指标可以通过定时统计 Redis 的命令执行耗时和客户端日志来观察,也可以在加锁逻辑里埋点打日志。不要等到线上出事故才回头看,那时候已经晚了。
6. 极端场景下的兜底方案:锁失效后怎么办
分布式锁再怎么设计,依然存在失效的可能。真正健壮的系统,不会把所有的宝押在锁上。锁是重要的一环,但不是全部。
6.1 业务幂等是最后的防线
我见过有些人把分布式锁当成银弹,锁一失效,系统就崩。但实际上,即使锁完全正常工作,也无法覆盖所有故障场景。比如消息队列重复推送、网络超时导致的重试,都可能让同一个请求被处理两次。
所以,在核心链路上设计幂等逻辑才是最终的兜底。分布式锁解决的是并发冲突,幂等解决的是重复请求。两者互补,组合使用效果才最好。
订单支付回调场景下,即使没有分布式锁,也要在数据库里做幂等校验:一个订单只能成功支付一次,重复回调直接返回成功。这就是经典的最强兜底方案。
6.2 锁与本地事务的边界
另外一个容易踩的坑:分布式锁和数据库事务的边界。很多人习惯在一个事务方法里加锁,或者在一个加锁方法里开启事务,结果事务还没提交,锁已经释放了。
正确做法是:先加锁,再开启事务,执行操作,提交事务,最后释放锁。如果顺序反过来,锁释放后,其他线程进入临界区,读到的是还没提交的旧数据,锁就等于白加了。
这个细节在分布式锁的实战中非常关键,但经常被忽略。锁和事务的作用对象不同,锁保护的是临界区的互斥访问,事务保护的是数据库操作的一致性,两者必须协调好生命周期。
6.3 锁的降级与熔断
最后一个思考:如果 Redis 本身就挂了,分布式锁怎么办?很多系统的做法是一旦 Redis 不可用,直接放弃加锁,让请求全部通过。这在某些场景下是合理的降级策略——比起系统完全不可用,接受少量的重复执行更划算。
但如果你的业务是绝对不能并发的(比如资金清算),那 Redis 挂了就只能拒绝服务,也不能降级为无锁模式。这里需要根据业务对一致性的要求来决策,没有统一答案。
我个人的经验是:给锁加上熔断开关。通过配置中心动态控制"强制加锁""降级放行""熔断拒绝"三种模式,线上出问题时能快速调整策略,而不是改代码重新发布。这个开关意味着你提前为最深的坑留了一个逃生通道。
写到最后的一点体会
从最初的一条 SETNX,到今天的 Redlock 和看门狗机制,Redis 分布式锁的演进本质上是不断对抗不确定性:命令的非原子性、时钟的不可靠性、网络的不可预知性、进程的不可控停顿。每一步演进都在消除一个已知的确定性漏洞,但新的不确定性又会冒出来。
在实际项目中,我越来越倾向于一个原则:根据业务的风险承受能力,选择合适复杂度的锁方案。大多数业务用 SET key value NX EX 加上 Lua 脚本释放锁就够了,最多引入 Redisson 的看门狗续期。只有极少数对一致性要求很高的场景,才值得引入 Redlock 或者 ZooKeeper。不要因为面试题里讲了 Redlock,就认为生产环境必须上 Redlock,那是把理论当成了教条。
最后分享一个实战小技巧:上线分布式锁之前,最好写一个并发压测脚本,模拟多节点并发执行同一条任务,验证锁在竞争激烈、线程数远超锁槽数量时也能正常工作。我遇到过一次隐藏很深的死锁问题,就是只有压测才触发,测试环境的低流量根本跑不出来。分布式锁这种东西,看着简单,出问题的时候往往要查很久,前置的压测和监控投入非常值得。
相关推荐
基于Python+Open CV的手势识别算法设计
伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。
ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像
ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)
计算机视觉图像处理-CV2与图像插值算法
用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int
基于水平集CV模型的图像分割(Matlab代码)
基于水平集CV模型的图像分割(Matlab代码),全套代码
导向滤波(guide_filter)的python实现
导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

