Actix 源码解析:从 Mutex 困境到 Actor 模型

你的代码里有多少把 Mutex? 锁是并发编程的「必需品」,却也是 bug 的温床——死锁、活锁、优先级反转,每一个都让人头疼。问题的根源在于:共享可变状态 + 并发 = 复杂度不可控。 1973 年,Carl Hewitt 提出了 Actor 模型,给出了另一种思路:既然共享状态是问题根源,那就不共享。Erlang 把这个思想发扬光大,用 6 个基本函数(spawn、send、receive、register、whereis、self)构建了支撑电信系统「九个九」可用性的并发基础设施。 在 Rust 生态中,Actix 给出了一个有趣的答案:用类型系统来「编码」Actor 模型的约束,把运行时错误转化为编译期错误。 本文不是 Actix 的使用教程,而是一次源码探索。核心问题是:当我们调用 addr.send(msg) 时,背后到底发生了什么? 注意:本文讨论的是 Actix(Actor 框架),而不是 Actix-web(Web 框架)。它们是两个独立的 crate。 第一部分:Actor 模型的本质 传统并发的困境 先看一个「经典」的并发代码: use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } } 表面上没问题,但这段代码藏着几个深层问题: 问题 1:锁的传染性 一旦用了 Mutex,它就会像病毒一样传播。假设你有一个 UserService,它持有 Mutex<HashMap<UserId, User>>。现在你要加一个缓存,于是又多了 Mutex<Cache>。然后你发现需要在更新用户时同时更新缓存——两把锁,需要同时持有。 // 线程 A:更新用户时同步缓存 fn update_user(&self, user: User) { let users = self.users.lock().unwrap(); // 1. 先锁 users let cache = self.cache.lock().unwrap(); // 2. 再锁 cache // ... } // 线程 B:刷新缓存时检查用户 fn refresh_cache(&self) { let cache = self.cache.lock().unwrap(); // 1. 先锁 cache let users = self.users.lock().unwrap(); // 2. 再锁 users ← 死锁! // ... } 如果时序不巧,线程 A 持有 users 等待 cache,而线程 B 恰好持有 cache 等待 users——Loss(死锁)!这种 bug 最阴险:测试时可能跑 1000 次都正常,生产环境高并发时突然卡死。 ...

December 17, 2025 · 11 min · 2279 words · Nanlong

Rust 1.92.0 新 API 深度解析

Rust 1.92.0 于 2025 年 12 月 11 日发布。这个版本没有惊天动地的新特性,但带来了一批等了很久的实用 API。作为一个写了几年 Rust 的开发者,看到这些 API 终于稳定,感觉就像是"终于不用自己造轮子了"。 本文将深入分析这些新 API 的实战价值、生态影响和设计哲学。 ...

December 15, 2025 · 7 min · 1329 words · Nanlong

Elixir 到 Rust Actix(七):状态管理——从 Agent/ETS 到 Rust 的选择

在 Elixir 里,状态管理有很多选择:Agent 简单直接,ETS 高性能共享,GenServer 灵活强大。在 Rust 里呢?Arc<Mutex<T>>?DashMap?Actor 状态?每种方案的适用场景是什么?本篇带你梳理清楚。 ...

December 14, 2025 · 7 min · 1420 words · Nanlong

Elixir 到 Rust Actix(六):模式匹配——熟悉的语法,不同的能力

Rust 的模式匹配看起来和 Elixir 很像——都有 match,都能解构,都能用 guard。但用起来,总觉得差点什么。Elixir 的 pin 操作符呢?Rust 为什么不能匹配任意类型?两者的模式匹配,看似相似,实则不同。 ...

December 14, 2025 · 7 min · 1436 words · Nanlong

Elixir 到 Rust Actix(五):容错机制——Let it crash vs 不让你 crash

Elixir 说"崩就崩,有人管"——监督树会重启你。Rust 说"我不让你崩"——编译器会阻止你。两种截然不同的哲学,一个拥抱失败,一个预防失败。什么时候该用哪种? ...

December 14, 2025 · 7 min · 1374 words · Nanlong

Elixir 到 Rust Actix(四):消息传递——从 send 到 Handler

Elixir 的 send 和 receive 简洁优雅——pid ! message,收到就处理,没收到就等。Actix 的消息系统?先定义消息类型,再声明返回类型,然后实现 Handler……为什么同样是发消息,一个三行代码,一个要写一堆 struct 和 impl? ...

December 14, 2025 · 7 min · 1283 words · Nanlong

Elixir 到 Rust Actix(三):进程与并发——spawn 的两种人生

在 Elixir 里,spawn 一个进程就像呼吸一样自然——300 字节,微秒级创建,随便造。在 Rust 里,你要考虑 Actor 还是 tokio::spawn,要不要 Send + 'static,生命周期够不够长……同样是"创建并发执行单元",为什么体验差这么多? ...

December 14, 2025 · 7 min · 1291 words · Nanlong

Elixir 到 Rust Actix(二):Actor 模型——GenServer vs Actix Actor

GenServer 是 Elixir 开发者的老朋友,三个回调函数就能搞定一切。Actix Actor 是它在 Rust 世界的"表亲",但这个表亲要求你先定义消息类型、实现 Handler trait、处理 Result……为什么同样是 Actor,一个简洁优雅,一个"仪式感"满满? ...

December 14, 2025 · 8 min · 1562 words · Nanlong

Elixir 到 Rust Actix(一):同源不同路——两种 Actor 哲学的碰撞

你用 Elixir 写了三年 GenServer,信手拈来。转到 Rust Actix,同样是 Actor 模型,却和编译器吵了三天架。明明是"表亲"技术,为什么感觉像两个世界? ...

December 14, 2025 · 6 min · 1099 words · Nanlong

当单机 PostgreSQL 撑不住时:Citus 分布式实战

你的数据库快爆了 凌晨 3 点,报警短信把你吵醒:数据库 CPU 100%,响应超时。 你揉着眼睛打开监控,订单表已经 2 亿行,每次查询都在全表扫描。加索引?早加过了。分表?业务代码要大改。 DBA 说:“该分库分表了。” 你打开《一致性实战(四)》,看到分片路由、跨分片查询聚合、Saga 事务补偿… 头皮发麻。 有没有更简单的方案? 有。让我介绍 Citus——PostgreSQL 的分布式扩展。它让分片对应用几乎透明,大部分 SQL 不用改,ORM 和 sqlx 可以直接用。 Citus 是什么 想象你有一个图书馆,书越来越多,一个管理员忙不过来了。 传统方案(手动分片): 你把书分到 4 个房间,每个房间一个管理员。但你得记住每本书在哪个房间,跨房间借书要自己协调。 Citus 方案: 还是 4 个房间 4 个管理员,但前台有一个总调度。你只跟总调度说话,它自动知道书在哪,跨房间的事它帮你协调。 这就是 Citus 的架构: ┌─────────────────────────────────────────────────────────┐ │ 应用程序 │ │ (sqlx, Diesel, 任何 PG 客户端) │ └─────────────────────┬───────────────────────────────────┘ │ 普通 PostgreSQL 协议 ▼ ┌─────────────────────────────────────────────────────────┐ │ Coordinator (协调器) │ │ 看起来就是一个普通 PostgreSQL │ │ • 解析 SQL │ │ • 路由到正确的分片 │ │ • 聚合跨分片结果 │ └───────┬─────────────┬─────────────┬─────────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Worker1 │ │ Worker2 │ │ Worker3 │ │ (分片0-3)│ │ (分片4-7)│ │(分片8-11)│ └─────────┘ └─────────┘ └─────────┘ 关键点:应用只连接 Coordinator,它看起来、用起来就是一个普通 PostgreSQL。 五分钟上手 1. 启动 Citus 集群 用 Docker Compose 快速启动一个本地集群: # docker-compose.yml services: coordinator: image: citusdata/citus:12.1 container_name: citus_coordinator ports: - "5432:5432" environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: mydb healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - citus-network worker1: image: citusdata/citus:12.1 container_name: citus_worker1 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - citus-network worker2: image: citusdata/citus:12.1 container_name: citus_worker2 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - citus-network networks: citus-network: driver: bridge 启动集群并等待健康检查通过: ...

December 11, 2025 · 9 min · 1819 words · Nanlong