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

深入理解Redis锁与Backoff重试机制在Go中的实现

文章目录

    • 流程图
    • Redis锁的深入实现
    • Backoff重试策略的深入探讨
    • 结合Redis锁与Backoff策略的高级应用
    • 具体实现
    • 结论

在构建分布式系统时,确保数据的一致性和操作的原子性是至关重要的。Redis锁作为一种高效且广泛使用的分布式锁机制,能够帮助我们在多进程或分布式环境中同步访问共享资源。本文将深入探讨如何在Go语言中实现Redis锁,并结合Backoff重试策略来优化锁的获取过程,确保系统的健壮性和可靠性。

流程图

获取成功
获取失败
开始
尝试获取锁
执行操作
操作成功?
释放锁
重试操作
应用Backoff策略
重试成功?
结束

Redis锁的深入实现

在Go语言中,我们使用github.com/gomodule/redigo/redis包来操作Redis。Redis锁的实现依赖于Redis的SET命令,该命令支持设置键值对,并且可以带有过期时间(EX选项)和仅当键不存在时才设置(NX选项)。以下是一个更详细的Redis锁实现示例:

func SetWithContext(ctx context.Context, redisPool *redis.Pool, key string, expireSecond uint32) (bool, string, error) {// ...省略部分代码...conn, err := redisPool.GetContext(ctx)if err != nil {return false, "", err}defer conn.Close()randVal := generateRandVal() // 生成随机值_, err = conn.Do("SET", key, randVal, "NX", "EX", int(expireSecond))if err != nil {return false, "", err}return true, randVal, nil
}

在上述代码中,generateRandVal()函数用于生成一个唯一的随机值,这个值在释放锁时用来验证是否是锁的持有者。expireSecond参数确保了即使客户端崩溃或网络问题发生,锁也会在一定时间后自动释放,避免死锁。

释放锁时,我们使用Lua脚本来确保只有持有锁的客户端才能删除键:

func ReleaseWithContext(ctx context.Context, redisPool *redis.Pool, key string, randVal string) error {// ...省略部分代码...conn, err := redisPool.GetContext(ctx)if err != nil {return err}defer conn.Close()script := `if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])elsereturn 0end`_, err = conn.Do("EVAL", script, 1, key, randVal)return err
}

Backoff重试策略的深入探讨

在分布式系统中,获取锁可能会因为网络延迟、高负载或其他原因而失败。Backoff重试策略通过在重试之间引入等待时间来减轻这些问题的影响。在提供的代码中,我们定义了多种Backoff策略,每种策略都有其特定的使用场景和优势。

例如,指数退避策略ExponentialBackoff的实现如下:

func (b *ExponentialBackoff) Next(retry int) (time.Duration, bool) {// ...省略部分代码...m := math.Min(r*b.t*math.Pow(b.f, float64(retry)), b.m)if m >= b.m {return 0, false}d := time.Duration(int64(m)) * time.Millisecondreturn d, true
}

在这个策略中,重试间隔随重试次数的增加而指数级增长,但有一个最大值限制。这有助于在遇到连续失败时,逐步增加等待时间,避免立即重载系统。

结合Redis锁与Backoff策略的高级应用

将Redis锁与Backoff策略结合起来,可以创建一个健壮的锁获取机制。例如,我们可以定义一个MustSetRetry方法,该方法会不断尝试获取锁,直到成功为止:

func (r *RedisLock) MustSetRetry(ctx context.Context, key string) (string, error) {op := func() (string, error) {return r.MustSet(ctx, key)}notifyFunc := func(err error) {// ...错误处理逻辑...}return mustSetRetryNotify(op, r.backoff, notifyFunc)
}

在这个方法中,mustSetRetryNotify函数负责执行重试逻辑,直到MustSet方法成功获取锁或达到最大重试次数。通过这种方式,我们能够确保即使在高竞争环境下,也能以一种可控和安全的方式获取锁。

具体实现

  1. backoff
package lockimport ("math""math/rand""sync""time"
)// BackoffFunc specifies the signature of a function that returns the
// time to wait before the next call to a resource. To stop retrying
// return false in the 2nd return value.
type BackoffFunc func(retry int) (time.Duration, bool)// Backoff allows callers to implement their own Backoff strategy.
type Backoff interface {// Next implements a BackoffFunc.Next(retry int) (time.Duration, bool)
}// -- ZeroBackoff --// ZeroBackoff is a fixed backoff policy whose backoff time is always zero,
// meaning that the operation is retried immediately without waiting,
// indefinitely.
type ZeroBackoff struct{}// Next implements BackoffFunc for ZeroBackoff.
func (b ZeroBackoff) Next(retry int) (time.Duration, bool) {return 0, true
}// -- StopBackoff --// StopBackoff is a fixed backoff policy that always returns false for
// Next(), meaning that the operation should never be retried.
type StopBackoff struct{}// Next implements BackoffFunc for StopBackoff.
func (b StopBackoff) Next(retry int) (time.Duration, bool) {return 0, false
}// -- ConstantBackoff --// ConstantBackoff is a backoff policy that always returns the same delay.
type ConstantBackoff struct {interval time.Duration
}// NewConstantBackoff returns a new ConstantBackoff.
func NewConstantBackoff(interval time.Duration) *ConstantBackoff {return &ConstantBackoff{interval: interval}
}// Next implements BackoffFunc for ConstantBackoff.
func (b *ConstantBackoff) Next(retry int) (time.Duration, bool) {return b.interval, true
}// -- Exponential --// ExponentialBackoff implements the simple exponential backoff described by
// Douglas Thain at http://dthain.blogspot.de/2009/02/exponential-backoff-in-distributed.html.
type ExponentialBackoff struct {t float64 // initial timeout (in msec)f float64 // exponential factor (e.g. 2)m float64 // maximum timeout (in msec)
}// NewExponentialBackoff returns a ExponentialBackoff backoff policy.
// Use initialTimeout to set the first/minimal interval
// and maxTimeout to set the maximum wait interval.
func NewExponentialBackoff(initialTimeout, maxTimeout time.Duration) *ExponentialBackoff {return &ExponentialBackoff{t: float64(int64(initialTimeout / time.Millisecond)),f: 2.0,m: float64(int64(maxTimeout / time.Millisecond)),}
}// Next implements BackoffFunc for ExponentialBackoff.
func (b *ExponentialBackoff) Next(retry int) (time.Duration, bool) {r := 1.0 + rand.Float64() // random number in [1..2]m := math.Min(r*b.t*math.Pow(b.f, float64(retry)), b.m)if m >= b.m {return 0, false}d := time.Duration(int64(m)) * time.Millisecondreturn d, true
}// -- Simple Backoff --// SimpleBackoff takes a list of fixed values for backoff intervals.
// Each call to Next returns the next value from that fixed list.
// After each value is returned, subsequent calls to Next will only return
// the last element. The values are optionally "jittered" (off by default).
type SimpleBackoff struct {sync.Mutexticks  []intjitter bool
}// NewSimpleBackoff creates a SimpleBackoff algorithm with the specified
// list of fixed intervals in milliseconds.
func NewSimpleBackoff(ticks ...int) *SimpleBackoff {return &SimpleBackoff{ticks:  ticks,jitter: false,}
}// Jitter enables or disables jittering values.
func (b *SimpleBackoff) Jitter(flag bool) *SimpleBackoff {b.Lock()b.jitter = flagb.Unlock()return b
}// jitter randomizes the interval to return a value of [0.5*millis .. 1.5*millis].
func jitter(millis int) int {if millis <= 0 {return 0}return millis/2 + rand.Intn(millis)
}// Next implements BackoffFunc for SimpleBackoff.
func (b *SimpleBackoff) Next(retry int) (time.Duration, bool) {b.Lock()defer b.Unlock()if retry >= len(b.ticks) {return 0, false}ms := b.ticks[retry]if b.jitter {ms = jitter(ms)}return time.Duration(ms) * time.Millisecond, true
}

关键Backoff策略:

  • ZeroBackoff: 不等待,立即重试。
  • StopBackoff: 从不重试。
  • ConstantBackoff: 固定等待时间。
  • ExponentialBackoff: 指数增长的等待时间。
  • SimpleBackoff: 提供一组固定的等待时间,可选择是否添加随机抖动。
package lockimport ("context""errors""fmt""time""github.com/gomodule/redigo/redis"
)var (// 防止孤儿lock没release// 目前expire过期时间的敏感度是考虑为一致的敏感度defaultExpireSecond uint32 = 30
)var (ErrLockSet     = errors.New("lock set err")ErrLockRelease = errors.New("lock release err")ErrLockFail    = errors.New("lock fail")
)// RedisLockIFace 在common redis上封一层浅封装
// 将redis pool 与expire second作为redis lock已知数据
type RedisLockIFace interface {MustSet(ctx context.Context, k string) (string, error)MustSetRetry(ctx context.Context, k string) (string, error) // 必须设置成功并有重试机制Release(ctx context.Context, k string, randVal string) error
}// RedisLock nil的实现默认为true
type RedisLock struct {redisPool    *redis.PoolexpireSecond uint32backoff      Backoff
}// An Option configures a RedisLock.
type Option interface {apply(*RedisLock)
}// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*RedisLock)func (f optionFunc) apply(log *RedisLock) {f(log)
}// WithBackoff backoff set
func WithBackoff(b Backoff) Option {return optionFunc(func(r *RedisLock) {r.backoff = b})
}func NewRedisLock(redisPool *redis.Pool, opts ...Option) *RedisLock {r := &RedisLock{redisPool:    redisPool,expireSecond: defaultExpireSecond,backoff:      NewExponentialBackoff(30*time.Millisecond, 500*time.Millisecond), // default backoff}for _, opt := range opts {opt.apply(r)}return r
}func (r *RedisLock) Set(ctx context.Context, key string) (bool, string, error) {if r == nil {return true, "", nil}isLock, randVal, err := SetWithContext(ctx, r.redisPool, key, r.expireSecond)if err != nil {return isLock, randVal, ErrLockSet}return isLock, randVal, err
}// MustSetRetry 必须设置成功并带有重试功能
func (r *RedisLock) MustSetRetry(ctx context.Context, key string) (string, error) {op := func() (string, error) {return r.MustSet(ctx, key)}notifyFunc := func(err error) {if err == ErrLockFail {fmt.Printf("RedisLock.MustSetRetry redis must set err: %v", err)} else {fmt.Printf("RedisLock.MustSetRetry redis must set err: %v", err)}}return mustSetRetryNotify(op, r.backoff, notifyFunc)
}func (r *RedisLock) MustSet(ctx context.Context, key string) (string, error) {isLock, randVal, err := r.Set(ctx, key)if err != nil {return "", err}if !isLock {return "", ErrLockFail}return randVal, nil
}func (r *RedisLock) Release(ctx context.Context, key string, randVal string) error {if r == nil {fmt.Printf("that the implementation of redis lock is nil")return nil}err := ReleaseWithContext(ctx, r.redisPool, key, randVal)if err != nil {fmt.Printf("s.RedisLock.ReleaseWithContext fail, err: %v", err)return ErrLockRelease}return nil
}func SetWithContext(ctx context.Context, redisPool *redis.Pool, key string, expireSecond uint32) (bool, string, error) {if expireSecond == 0 {return false, "", fmt.Errorf("expireSecond参数必须大于0")}conn, _ := redisPool.GetContext(ctx)defer conn.Close()randVal := time.Now().Format("2006-01-02 15:04:05.000")reply, err := conn.Do("SET", key, randVal, "NX", "PX", expireSecond*1000)if err != nil {return false, "", err}if reply == nil {return false, "", nil}return true, randVal, nil
}func ReleaseWithContext(ctx context.Context, redisPool *redis.Pool, key string, randVal string) error {conn, _ := redisPool.GetContext(ctx)defer conn.Close()luaScript := `if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])elsereturn 0end;`script := redis.NewScript(1, luaScript)_, err := script.Do(conn, key, randVal)return err
}
  1. 重试
package lockimport "time"type mustSetOperation func() (string, error)type ErrNotify func(error)func mustSetRetryNotify1(operation mustSetOperation, b Backoff, notify ErrNotify) (string, error) {var err errorvar randVal stringvar wait time.Durationvar retry boolvar n intfor {if randVal, err = operation(); err == nil {return randVal, nil}if b == nil {return "", err}n++wait, retry = b.Next(n)if !retry {return "", err}if notify != nil {notify(err)}time.Sleep(wait)}}
  1. 使用

func main() {backoff := lock.NewExponentialBackoff(time.Duration(20)*time.Millisecond,time.Duration(1000)*time.Millisecond,)redisPool := &redis.Pool{MaxIdle:     3,IdleTimeout: 240 * time.Second,// Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial.Dial: func() (redis.Conn, error) {return redis.Dial("tcp","redis host",redis.DialPassword("redis password"),)},}redisLock := lock.NewRedisLock(redisPool, lock.WithBackoff(backoff))ctx := context.Background()s, err := redisLock.MustSetRetry(ctx, "lock_user")if err != nil && err == lock.ErrLockFail {fmt.Println(err)return}time.Sleep(20 * time.Second)defer func() {_ = redisLock.Release(ctx, "lock_user", s)}()return
}

结论

通过深入理解Redis锁和Backoff重试策略的实现,我们可以构建出既能够保证资源访问的原子性,又能在面对网络波动或系统负载时保持稳定性的分布式锁机制。这不仅提高了系统的可用性,也增强了系统的容错能力。在实际开发中,合理选择和调整这些策略对于确保系统的高性能和高可靠性至关重要。通过精心设计的锁机制和重试策略,我们可以为分布式系统提供一个坚实的基础,以应对各种挑战和压力。

关注我

相关文章:

深入理解Redis锁与Backoff重试机制在Go中的实现

文章目录 流程图Redis锁的深入实现Backoff重试策略的深入探讨结合Redis锁与Backoff策略的高级应用具体实现结论 在构建分布式系统时&#xff0c;确保数据的一致性和操作的原子性是至关重要的。Redis锁作为一种高效且广泛使用的分布式锁机制&#xff0c;能够帮助我们在多进程或分…...

uniapp-小程序开发0-1笔记大全

uniapp官网&#xff1a; https://uniapp.dcloud.net.cn/tutorial/syntax-js.html uniapp插件市场&#xff1a; https://ext.dcloud.net.cn/ uviewui类库&#xff1a; https://www.uviewui.com/ 柱状、扇形、仪表盘库&#xff1a; https://www.ucharts.cn/v2/#/ CSS样式&…...

Go语言数据库操作深入讲解

go操作MySQL 使用第三方开源的mysql库: github.com/go-sql-driver/mysql (mysql驱动)github.com/jmoiron/sqlx (基于mysql驱动的封装) 命令行输入 &#xff1a; go get github.com/go-sql-driver/mysqlgo get github.com/jmoiron/sqlx Insert操作 登录后复制 // 连接Mysql data…...

搜维尔科技:SenseGlove Nova 2触觉反馈手套开箱测评

SenseGlove Nova 2触觉反馈手套开箱测评 搜维尔科技&#xff1a;SenseGlove Nova 2触觉反馈手套开箱测评...

步步精科技诚邀您参加2024慕尼黑华南电子展

尊敬的客户&#xff1a; 我们诚挚地邀请您参加即将于2024年10月14日至10月16日在深圳国际会展中心 &#xff08;宝安新馆&#xff09;举办的慕尼黑华南电子展(electronica South China)。本届将聚焦人工智能、数据中心、新型储能、无线通信、硬件安全、新能源汽车、第三代半导…...

OPC UA与PostgreSQL如何实现无缝连接?

随着工业4.0的推进&#xff0c;数据交换和集成在智能制造中扮演着越来越重要的角色。OPC UA能够实现设备与设备、设备与系统之间的高效数据交换。而PostgreSQL则是一种强大的开源关系型数据库管理系统&#xff0c;广泛应用于数据存储和管理。如何将OPC UA与PostgreSQL结合起来&…...

C语言[斐波那契数列2]

本篇文章讲述前一篇文章的细节&#xff0c;方便大家进行代码的运算。 本次代码题为: 输出斐波那契数列的前20位数&#xff0c;每行4位数。 详细解释: 在 main 函数中&#xff0c;首先定义了循环变量 i 和用于存储斐波那契数列项的三个长整型变量 f1 、 f2 和 temp 。其…...

八、Linux之实用指令

1、指定运行级别 1.1 基本介绍 运行级别说明 0 &#xff1a;关机 1 &#xff1a;单用户【找回丢失密码】 2&#xff1a;多用户状态没有网络服务&#xff08;用的非常少&#xff09; 3&#xff1a;多用户状态有网络服务&#xff08;用的最多&#xff09; 4&#xff1a;系统未使…...

2024_E_100_连续字母长度

连续字母长度 题目描述 给定一个字符串&#xff0c;只包含大写字母&#xff0c;求在包含同一字母的子串中&#xff0c;长度第 k 长的子串的长度&#xff0c;相同字母只取最长的那个子串。 输入描述 第一行有一个子串(1<长度<100)&#xff0c;只包含大写字母。 第二行为…...

清空redo导致oracle故障恢复---惜分飞

客户由于空间不足,使用> redo命令清空了oracle的redo文件 数据库挂掉之后,启动报错 Fri Oct 04 10:32:57 2024 alter database open Beginning crash recovery of 1 threads parallel recovery started with 31 processes Started redo scan Errors in file /home/oracle…...

VAE(与GAN)

VAE 1. VAE 模型概述 变分自编码器&#xff08;Variational Autoencoder, VAE&#xff09;是一种生成模型&#xff0c;主要用于学习数据的潜在表示并生成新样本。它由两个主要部分组成&#xff1a;编码器和解码器。 编码器&#xff1a;将输入数据映射到潜在空间&#xff0c;…...

【高等数学】多元微分学(二)

隐函数的偏导数 二元方程的隐函数 F ( x , y ) 0 F(x,y)0 F(x,y)0 推出隐函数形式 y y ( x ) yy(x) yy(x). 欲求 d y d x \frac{d y}{d x} dxdy​ 需要对 F 0 F0 F0 两边同时对 x x x 求全导 0 d d x F ( x , y ( x ) ) ∂ F ∂ x d x d x ∂ F ∂ y d y d x ∂ F…...

.NET 中的 Web服务(Web Services)和WCF(Windows Communication Foundation)

一、引言 在当今数字化时代&#xff0c;不同的软件系统和应用程序之间需要进行高效、可靠的通信与数据交换。.NET 框架中的 Web 服务和 WCF&#xff08;Windows Communication Foundation&#xff09;为此提供了强大的技术支持。它们在构建分布式应用程序、实现跨平台通信以及…...

Linux小知识2 系统的启动

我们在上文中介绍了文件系统&#xff0c;提到了Linux的文件系统存在一个块的概念&#xff0c;其中有一个特殊的块&#xff1a;引导块。这和我们这里要讲的系统启动有关。 BIOS 基本输入输出系统&#xff0c;基本上是一个操作系统最早实现也是最早运行的第一个程序。是一个比较…...

Oracle-19g数据库的安装

简介 Oracle是一家全球领先的数据库和云解决方案提供商。他们提供了一套完整的技术和产品&#xff0c;包括数据库管理系统、企业级应用程序、人工智能和机器学习工具等。Oracle的数据库管理系统是业界最受欢迎和广泛使用的数据库之一&#xff0c;它可以管理和存储大量结构化和…...

Dubbo快速入门(二):第一个Dubbo程序(附源码)

文章目录 一、生产者工程0.目录结构1.依赖2.配置文件3.启动类4.生产者服务 二、消费者工程0.目录结构1.依赖2.配置文件3.启动类4.服务接口5.controller接口 三、测试代码 本博客配套源码&#xff1a;gitlab仓库 首先&#xff0c;在服务器上部署zookeeper并运行&#xff0c;可以…...

不同数据类型转换与转义的对比差异

(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 在C和C语言中&#xff0c;类型转换与转义是有点像的&#xff0c;有时可能被误解&#xff0c;这块需要仔细辨别。 类型转换形如&#xff0c;把不同字节数或相同字节数的类型值进行转换&#xff0c;强调的是数值转换过去&…...

Kylin系统安装VMwareTools工具

如下图所示&#xff0c;安装好Kylin系统之后&#xff0c;还未安装VMwareTools工具&#xff0c;导致系统画面无法填充虚拟机 正常安装了VMwareTools工具后的系统画面 所以&#xff0c;接下来我们介绍一下如何在Kylin系统下安装VMwareTools工具 首先&#xff0c;点击VMware工具栏…...

uni-app 拍照图片添加水印

获取图片信息 uni.chooseImage({count: 6, //默认9sizeType: ["original", "compressed"], //可以指定是原图还是压缩图&#xff0c;默认二者都有sourceType: ["camera"], //从相册选择success: async function (result: any) {if (!props.isMar…...

Docker-registry私有镜像仓库的安装

Docker-registry私有镜像仓库的安装 我在这里的镜像仓库搭建在ip为192.168.3.23的虚机中。 安装docker-registry 1.拉取镜像 # docker pull registry 2.查看镜像 # docker images REPOSITORY TAG IMAGE ID CREATE…...

【Linux】C语言执行shell指令

在C语言中执行Shell指令 在C语言中&#xff0c;有几种方法可以执行Shell指令&#xff1a; 1. 使用system()函数 这是最简单的方法&#xff0c;包含在stdlib.h头文件中&#xff1a; #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

java 实现excel文件转pdf | 无水印 | 无限制

文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...

FFmpeg:Windows系统小白安装及其使用

一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】&#xff0c;注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录&#xff08;即exe所在文件夹&#xff09;加入系统变量…...

深入理解Optional:处理空指针异常

1. 使用Optional处理可能为空的集合 在Java开发中&#xff0c;集合判空是一个常见但容易出错的场景。传统方式虽然可行&#xff0c;但存在一些潜在问题&#xff1a; // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

LangFlow技术架构分析

&#x1f527; LangFlow 的可视化技术栈 前端节点编辑器 底层框架&#xff1a;基于 &#xff08;一个现代化的 React 节点绘图库&#xff09; 功能&#xff1a; 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...

【HarmonyOS 5】鸿蒙中Stage模型与FA模型详解

一、前言 在HarmonyOS 5的应用开发模型中&#xff0c;featureAbility是旧版FA模型&#xff08;Feature Ability&#xff09;的用法&#xff0c;Stage模型已采用全新的应用架构&#xff0c;推荐使用组件化的上下文获取方式&#xff0c;而非依赖featureAbility。 FA大概是API7之…...