【Redisson 加锁源码解析】

Redisson 源码解析 —— 分布式锁实现过程

在分布式系统中,分布式锁 是非常常见的需求,用来保证多个节点之间的互斥操作。Redisson 是 Redis 的一个 Java 客户端,它提供了对分布式锁的良好封装。本文将从源码角度剖析 Redisson 的分布式锁实现过程。


一、分布式锁的基本需求

一个健壮的分布式锁需要满足以下条件:

  1. 互斥性:同一时间只能有一个客户端持有锁。
  2. 死锁避免:客户端宕机后,锁不会永久被占用。
  3. 可重入性:同一线程可多次获取同一把锁。
  4. 高可用性:在 Redis 集群模式下仍能正常工作。
  5. 超时释放:设置持有锁时间,时间超过锁释放,避免死锁。
  6. 锁时间续约:看门狗机制,避免业务未执行完毕锁释放,导致并发问题。

二、Redisson 分布式锁的核心实现类以及加锁方法

在源码中,Redisson 提供了多种锁的实现,最核心的是:

  • RedissonLock —— 基于 Redis 的可重入锁实现
  • RedissonReadWriteLock —— 读写锁
  • RedissonFairLock —— 公平锁

我们主要关注 RedissonLock 的实现。


 RLock lock = redissonClient.getLock("32r");lock.方法名()

常用加锁方法:
在这里插入图片描述

  1. lock():获取锁,获取不到会一致阻塞直到获取。通过看门狗机制续期,默认持有锁是30s,每隔10s续期一次。
  2. lock(long l, TimeUnit timeUnit):获取锁,获取不到会一致阻塞直到获取。持有锁时间是手动入参的timeUnit,到期释放锁。
  3. tryLock(long waite, long l1, TimeUnit timeUnit) :获取锁失败后,自旋,等待 waite 秒,获取不到返回false,获取到,持有锁时间是 l1,单位 timeUnit。
  4. tryLock():尝试获取一次锁,如果获取不到,立即返回 false,获取锁成功,触发 看门狗续期机制(和 lock() 一样)。
  5. tryLock(long waitTime, TimeUnit unit):在 waitTime 时间窗口内,不断尝试执行,范围内获取锁失败,返回false。获取成功,启动看门狗机制。
 RLock lock = redissonClient.getLock("32r");

我们可以看到 redissonClient 调用这个方法时候,客户端返回的是RedissonLock这个类
在这里插入图片描述

所以对应的我们主要关注 RedissonLock 子类和父类RedissonBaseLock
在这里插入图片描述

这里我主要分析 lock() 方法的调用,其他锁的逻辑都是参考这个去完善的。

三、加锁流程解析

1. 调用入口

当我们执行:

RLock lock = redisson.getLock("myLock");
lock.lock();

进入RedissonLock#lock方法:
在这里插入图片描述
可以看到调用lock方法其实都是调用的另外一个lock(long leaseTime, TimeUnit unit, boolean interruptibly) 方法。
对应真正调用的lock()方法:

/*** 获取分布式锁的核心方法* @param leaseTime 锁的租约时间* @param unit 时间单位* @param interruptibly 是否允许中断* @throws InterruptedException 当线程被中断时抛出*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 获取当前线程ID,用于标识锁的持有者long threadId = Thread.currentThread().getId();// 尝试获取锁,返回剩余的TTL(生存时间)// 如果返回null表示获取锁成功,否则返回锁的剩余过期时间Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);// 如果ttl不为null,说明锁获取失败,需要等待if (ttl != null) {// 订阅锁释放的通知,返回一个Future对象CompletableFuture<RedissonLockEntry> future = this.subscribe(threadId);// 设置订阅操作的超时时间this.pubSub.timeout(future);// 根据是否允许中断来获取订阅结果RedissonLockEntry entry;if (interruptibly) {// 允许中断的方式获取结果entry = (RedissonLockEntry)this.commandExecutor.getInterrupted(future);} else {// 不允许中断的方式获取结果entry = (RedissonLockEntry)this.commandExecutor.get(future);}try {// 自旋等待锁释放while(true) {// 再次尝试获取锁ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);// 如果获取锁成功(ttl为null),则退出循环if (ttl == null) {return;}// 如果ttl大于等于0,说明锁还存在,需要等待指定的时间if (ttl >= 0L) {try {// 使用信号量等待指定的ttl时间entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {// 如果允许中断,直接抛出异常if (interruptibly) {throw e;}// 如果不允许中断,继续等待entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {// 如果ttl小于0,表示需要无限等待if (interruptibly) {// 允许中断的无限等待entry.getLatch().acquire();} else {// 不允许中断的无限等待entry.getLatch().acquireUninterruptibly();}}}} finally {// 无论成功与否,都要取消订阅,释放资源this.unsubscribe(entry, threadId);}}
}

这时候我们只需要重点关注对应的this.tryAcquire(-1L, leaseTime, unit, threadId);这个方法。
源码图如下:
在这里插入图片描述
对应的Java代码解释:

/*** 异步尝试获取锁* @param waitTime 等待时间* @param leaseTime 锁的租约时间* @param unit 时间单位* @param threadId 线程ID* @return 返回锁的剩余TTL时间,null表示获取锁成功*/
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {// 声明TTL剩余时间的Future对象RFuture<Long> ttlRemainingFuture;// 判断是否指定了租约时间if (leaseTime > 0L) {// 使用指定的租约时间尝试获取锁ttlRemainingFuture = this.<Long>tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 使用默认的内部锁租约时间尝试获取锁ttlRemainingFuture = this.<Long>tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 对获取锁的结果进行后续处理CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {// 如果ttlRemaining为null,说明成功获取到锁if (ttlRemaining == null) {// 判断是否指定了租约时间if (leaseTime > 0L) {// 将指定的租约时间转换为毫秒并存储到内部锁租约时间this.internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 如果没有指定租约时间,启动锁的自动续期机制// 防止锁因过期而被误释放this.scheduleExpirationRenewal(threadId);}}// 返回TTL剩余时间(null表示获取锁成功,非null表示需要等待的时间)return ttlRemaining;});// 将CompletionStage包装成RFuture并返回return new CompletableFutureWrapper(f);
}

这里最重要的是调用对应的tryAcquire里面的tryLockInnerAsync方法,方法详解如下:

 <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});}

这个tryLockInnerAsync方法主要是执行对应的脚本,然后返回剩余的时间,如果获取锁成功返回 nil ,获取锁失败会返回 持有锁的锁过期时间

核心 Lua 脚本详解如下:

Redisson 并不是简单地 SETNX,而是使用 Lua 脚本 来保证操作的原子性
加锁脚本大致逻辑如下:

if (redis.call('exists', KEYS[1]) == 0) then-- 锁不存在,设置锁并绑定到线程redis.call('hset', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;-- 锁已存在,判断是否是当前线程重入
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;return redis.call('pttl', KEYS[1]);

解释:

  • KEYS[1]: 锁的 key (如 myLock)
  • ARGV[1]: 锁的过期时间(默认 30s)
  • ARGV[2]: 当前线程标识(由 UUID + 线程 ID 组成)

执行流程:

  1. 如果锁不存在,设置 hash,key = 线程标识,value = 1。
  2. 如果锁存在且是自己线程,则递增重入次数。
  3. 否则返回锁的剩余过期时间。

问题延伸:
Redis不是单线程吗,高并发线程下不是线程安全吗?为什么还需要使用Lua脚本保证原子性
想想为什么使用lua脚本,你可以想象一下高并发场景下,Redis执行命令是单线程的,Redis只能保证对应的单条命令是原子性的,不能保证多条命令的原子性,假设线程A执行:redis.call('exists', KEYS[1]) == 0结束后,线程B抢到执行权,然后线程B也执行:redis.call('exists', KEYS[1]) == 0,然后后续大家都会进行对应的锁设置,导致线程A上锁可能会被覆盖,不过可以用hsetnx解决,但是后续可能判断还是会有并发问题。使用 lua 脚本可以将多条命令整合成类似一条命令,redis执行,从而保证原子性

WatchDog 自动续期机制

Redisson 的一大亮点是 锁续期机制

  • 当线程获取锁后,会启动一个 看门狗定时任务,默认每隔 lockWatchdogTimeout / 3 秒续期一次(默认 30s → 10s)。
  • 如果业务逻辑执行很久,不用担心锁被提前释放。
  • 如果线程宕机,定时任务不再执行,锁会在超时后自动释放。

判断对应的leasetime有没有指定,然后执行对应的续期或不续期的方法
源码关键点在:scheduleExpirationRenewal() 方法。
关键代码

   CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {if (ttlRemaining == null) {if (leaseTime > 0L) {this.internalLockLeaseTime = unit.toMillis(leaseTime);} else {this.scheduleExpirationRenewal(threadId);}}return ttlRemaining;});

根据对应的没指定leaseTime ,然后执行对应的RedissonBaseLock#scheduleExpirationRenewal对应的方法逻辑如下:

  /*** 调度锁的过期时间续期任务* 为指定线程启动自动续期机制,防止锁因过期而被误释放* @param threadId 需要续期的线程ID*/
protected void scheduleExpirationRenewal(long threadId) {// 创建新的过期时间管理条目ExpirationEntry entry = new ExpirationEntry();// 尝试将新条目放入续期映射表中,如果已存在则返回旧条目// 使用putIfAbsent确保原子性操作,避免并发问题ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);// 判断是否已经存在续期任务if (oldEntry != null) {// 如果已存在续期任务,只需将当前线程ID添加到现有条目中// 这种情况发生在同一个锁被多个线程(可重入锁)或同一线程多次获取时oldEntry.addThreadId(threadId);} else {// 如果是首次为这个锁创建续期任务// 将当前线程ID添加到新创建的条目中entry.addThreadId(threadId);try {// 启动实际的续期任务// 这会创建定时任务,定期延长锁的过期时间this.renewExpiration();} finally {// 检查当前线程是否被中断if (Thread.currentThread().isInterrupted()) {// 如果线程被中断,取消刚刚启动的续期任务// 防止资源泄漏和无效的续期操作this.cancelExpirationRenewal(threadId);}}}
}

这个通过一个创建一个ExpirationEntry 然后通过EXPIRATION_RENEWAL_MAP判断是否存在,如果条目不存在就启动对应的自动续期机制任务 renewExpiration()

RedissonBaseLock#renewExpiration()方法如下:

/*** 启动锁的自动续期机制* 创建定时任务,定期延长锁的过期时间,防止锁因超时而被释放*/
private void renewExpiration() {// 从续期映射表中获取当前锁的过期时间管理条目ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());// 如果条目存在,说明需要为这个锁设置续期任务if (ee != null) {// 创建定时任务,在锁租约时间的1/3处执行续期操作// 选择1/3时间点是为了在锁过期前有足够的时间进行续期Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {public void run(Timeout timeout) throws Exception {// 定时任务执行时,重新获取续期条目(防止在延迟期间被移除)ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());// 双重检查:确保续期条目仍然存在if (ent != null) {// 获取需要续期的第一个线程ID// 对于可重入锁,可能有多个线程ID,取第一个进行续期Long threadId = ent.getFirstThreadId();// 如果线程ID有效,执行续期操作if (threadId != null) {// 异步执行锁的续期操作CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);// 处理续期结果future.whenComplete((res, e) -> {// 如果续期过程中发生异常if (e != null) {// 记录错误日志RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);// 从续期映射表中移除条目,停止续期RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());} else {// 续期操作成功完成if (res) {// 如果续期成功(返回true),递归调用继续下一轮续期// 这样就形成了持续的自动续期循环RedissonBaseLock.this.renewExpiration();} else {// 如果续期失败(返回false),说明锁已经不存在或不属于当前线程// 取消续期任务,清理资源RedissonBaseLock.this.cancelExpirationRenewal((Long)null);}}});}}}}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 在租约时间的1/3处执行续期// 将定时任务保存到条目中,用于后续的取消操作ee.setTimeout(task);}
}

最后完美结束对应的获取锁的过程,返回一个对应的时间值 ttl
在这里插入图片描述
如果返回的是null代表加锁成功,否则是加锁失败,此时会进行订阅持有锁者this.subscribe(threadId),如果释放锁会通知这个获取锁失败的线程,会将这个线程唤醒。

四、解锁流程解析

解锁的流程

解锁时同样使用 Lua 脚本,保证原子性:

if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) thenreturn nil;
end;local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);if (counter > 0) thenreturn 0;
elseredis.call('del', KEYS[1]);return 1;
end;

解释:

  1. 检查当前线程是否持有锁。
  2. 如果是可重入锁,计数 -1。
  3. 如果计数为 0,则删除锁。

六、源码设计亮点

  1. Lua 脚本保证原子性,避免分布式并发问题。
  2. 可重入性设计:使用 hash 结构存储线程标识和重入次数。
  3. 锁超时释放设计:避免死锁问题。
  4. 看门狗机制:保证长时间任务也能安全持有锁。
  5. 异步化设计:Redisson 提供 lockAsync() 等方法,方便高并发场景。

七、总结

  • Redisson 的分布式锁实现基于 Redis + Lua 脚本,解决了互斥、可重入和死锁问题。
  • 看门狗续期机制 是 Redisson 的亮点,保证了业务执行时间不可预测的情况下的安全性。
  • 在生产环境中,Redisson 的分布式锁相较于 SETNX + EXPIRE 的手写版本,更加健壮和可靠。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/920850.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/920850.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uni-app支持单多选、搜索、查询、限制能否点击组件

<template><view class="multi-select-container" :class="{ single-select: !multiple, no-search: !searchable }"><!-- 当组件被禁用时,直接显示选中的内容 --><view class="disabled-display" v-if="disabled &a…

TFT屏幕:STM32硬件SPI+DMA+队列自动传输

看了网上的很多的SPIDMA的代码&#xff0c;感觉都有一些缺陷&#xff0c;就是基本都是需要有手动等待DMA完成的这个操作&#xff0c;我感觉这种等待操作在很大程度上浪费了时间&#xff0c;那么我加入的“队列”就是一种将等待时间利用起来的方法。原本的SPIDMA的操作逻辑如下图…

AI操作系统语言模型设计 之1 基于意识的Face-Gate-Window的共轭路径的思维-认知-情感嵌套模型

摘要&#xff08;AI生成&#xff09;本文提出了一种创新的AI操作系统语言模型设计框架&#xff0c;将人类意识活动的分层结构映射到人工智能系统中。该模型包含三个嵌套层次&#xff1a;理性思维层&#xff08;Face层&#xff09;&#xff1a;采用双面胶隐喻&#xff08;A/B面&…

疯狂星期四文案网第57天运营日记

网站运营第57天&#xff0c;点击观站&#xff1a; 疯狂星期四 crazy-thursday.com 全网最全的疯狂星期四文案网站 运营报告 今日访问量 今日搜索引擎收录情况

SQLark:一款面向信创应用开发者的数据库开发和管理工具

SQLark 是一款面向信创应用开发者的数据库开发和管理工具&#xff0c;用于快速查询、创建和管理不同类型的数据库系统&#xff0c;现已支持达梦、Oracle、MySQL、PostgreSQL 数据库。 SQLark 提供了对多种数据库的连接支持&#xff0c;实现跨平台数据库管理的无缝切换&#xff…

BigDecimal——解决Java浮点数值精度问题:快速入门与使用

在Java开发中&#xff0c;涉及金额计算、科学计数或需要高精度数值处理时&#xff0c;你是否遇到过这样的困惑&#xff1f;用double计算0.1加0.2&#xff0c;结果竟不是0.3&#xff1b;用float存储商品价格&#xff0c;小数点后两位莫名多出几位乱码&#xff1b;甚至在金融系统…

wpf之WrapPanel

前言 WrapPanel类似winform中的FlowLayoutPanel&#xff0c;采用流式布局。 1、Orientation 该属性指定WrapPanel中子空间布局的方向&#xff0c;有水平和垂直方向两种 1&#xff09;Horizontal 水平方向 子元素Button按照水平方向排列&#xff0c;如果一行排满了自动换下一…

Woody:开源Java应用性能诊断分析工具

核心价值 Woody是一款专注于Java应用性能问题诊断的工具&#xff0c;旨在帮助开发者 定位高GC频率问题&#xff0c;识别内存分配热点分析CPU使用率过高的代码路径追踪接口耗时瓶颈&#xff0c;定位内部操作耗时占比诊断锁竞争问题&#xff0c;支持精准优化针对特定业务接口/请…

《山东棒球》板球比赛规则·棒球1号位

⚾ Baseball vs Cricket 终极科普&#xff5c;规则异同发展史全解&#xff01;Hey sports babes&#xff01;别再傻傻分不清棒球⚾和板球&#xff01;全网最清晰双运动对照指南来啦&#xff5e;⚾ 棒球 Baseball&#xff5c;美式激情风暴Core Goal核心目标击球员&#xff08;Ba…

【游戏开发】Houdini相较于Blender在游戏开发上有什么优劣势?我该怎么选择开发工具?

在游戏开发中&#xff0c;Houdini与Blender的选择需结合项目规模、技术需求和团队资源综合考量。以下是两者的核心优劣势对比及决策建议&#xff1a; 一、核心优劣势对比 Houdini的优势与局限 优势&#xff1a;程序化内容生成的统治力 Houdini的节点系统&#xff08;如VEX语言、…

基于开源AI智能名片链动2+1模式S2B2C商城小程序的用户活跃度提升与价值挖掘策略研究

摘要&#xff1a;本文聚焦于在开源AI智能名片链动21模式S2B2C商城小程序环境下&#xff0c;探讨如何提高用户活跃度并挖掘用户价值。在用户留存的基础上&#xff0c;通过分析该特定模式与小程序的特点&#xff0c;提出一系列针对性的策略&#xff0c;旨在借助开源AI智能名片以及…

《投资-41》- 自然=》生物=》人类社会=》商业=》金融=》股市=》投资,其层层叠加构建中内在的相似的规律和规则

从自然到投资的层层递进中&#xff0c;尽管各领域看似差异巨大&#xff0c;但内在遵循着相似的规律和规则。这些规律体现了“底层逻辑的普适性”&#xff0c;即不同系统在动态平衡、资源分配、信息传递和反馈调节等方面具有共性。以下是关键规律的解析&#xff1a;1. 能量流动与…

VSCode中调试python脚本

VSCode中安装以下插件 ms-python.python&#xff1a;python调试ms-python.vscode-pylance&#xff1a;代码跳转&#xff08;非必要&#xff09; 配置launch.json 在当前工作区&#xff0c;按此路径.vscode\launch.json新建launch.json文件&#xff0c;并配置以下参数&#x…

动作指令活体检测通过动态交互验证真实活人,保障安全

在当今社会&#xff0c;人脸识别技术已深入日常生活的方方面面&#xff0c;从手机解锁、移动支付到远程开户、门禁考勤&#xff0c;人脸识别技术已无处不在。然而&#xff0c;这项技术也面临着严峻的安全挑战&#xff1a;打印照片、播放视频、制作3D面具等简单的“欺骗手段”都…

KingbaseES数据库:开发基础教程,从部署到安全的全方位实践

KingbaseES数据库&#xff1a;开发基础教程&#xff0c;从部署到安全的全方位实践 KingbaseES数据库&#xff1a;开发基础教程&#xff0c;从部署到安全的全方位实践&#xff0c;本文围绕 KingbaseES 数据库开发核心基础展开。先介绍三种部署模式&#xff0c;即单机、双机热备、…

安装nodejs安装node.js安装教程(Windows Linux)

文章目录Linux**一、下载 Node.js**1. **访问官网**&#xff1a;2. **选择版本**&#xff1a;**二、安装 Node.js****方法 1&#xff1a;使用包管理器&#xff08;推荐&#xff09;****Ubuntu/Debian 系统**1. **更新包列表**&#xff1a;2. **安装 Node.js**&#xff1a;3. **…

shell脚本函数介绍

1. 函数 (Functions)定义与优势函数是可重复使用的功能模块优势&#xff1a;代码复用&#xff0c;直接调用解决问题分类内置函数&#xff1a;编程语言自带的函数&#xff08;如 print&#xff09;自定义函数&#xff1a;程序员自己编写的函数定义语法# 方式一 function 函数名(…

DAY 20 奇异值SVD分解-2025.9.1

奇异值SVD分解 知识点回顾&#xff1a; 线性代数概念回顾奇异值推导奇异值的应用 a. 特征降维&#xff1a;对高维数据减小计算量、可视化 b. 数据重构&#xff1a;比如重构信号、重构图像&#xff08;可以实现有损压缩&#xff0c;k 越小压缩率越高&#xff0c;但图像质量损失…

《C++——定长内存池》

一、为什么需要内存池&#xff1f; 常规的new/delete操作存在两个主要问题&#xff1a; 性能开销大&#xff1a;每次new都需要向操作系统申请内存&#xff0c;delete需要归还给系统&#xff0c;这涉及内核态与用户态的切换&#xff0c;在高频次调用时性能损耗明显。 内存碎片&a…

【跨境电商】上中下游解释,以宠物行业为例

上中下游概念及其在宠物行业的应用 在产业链分析中&#xff0c;“上中下游”指的是一个产品或服务的不同环节&#xff1a;上游涉及原材料供应和基础资源&#xff0c;中游负责生产加工和制造&#xff0c;下游则包括销售、分销和服务。这种划分有助于理解整个价值链的运作。下面&…