惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
O
OpenAI News
TaoSecurity Blog
TaoSecurity Blog
Cloudbric
Cloudbric
Know Your Adversary
Know Your Adversary
I
Intezer
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Privacy & Cybersecurity Law Blog
T
Threatpost
AWS News Blog
AWS News Blog
Google Online Security Blog
Google Online Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
aimingoo的专栏
aimingoo的专栏
Cyberwarzone
Cyberwarzone
GbyAI
GbyAI
P
Proofpoint News Feed
S
Security @ Cisco Blogs
D
Docker
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
Help Net Security
Help Net Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
博客园 - 【当耐特】
月光博客
月光博客
罗磊的独立博客
G
Google Developers Blog
M
MIT News - Artificial intelligence
小众软件
小众软件
C
Check Point Blog
P
Proofpoint News Feed
H
Heimdal Security Blog
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
W
WeLiveSecurity
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security Affairs
Attack and Defense Labs
Attack and Defense Labs
S
Schneier on Security
H
Hacker News: Front Page
The Last Watchdog
The Last Watchdog
H
Help Net Security
T
Threat Research - Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
Hacker News: Ask HN
Hacker News: Ask HN
Project Zero
Project Zero

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
读:Immutability 不是万能药,它是一种权衡 - 暗无天日
2026-04-29 · via 暗无天日

原文:Immutability: Not a Universal Law, But a Trade-off

不可变性(immutability)是指数据一旦创建就不再修改,每次"修改"都产生新版本,这通常被认为是函数式编程的金科玉律。但原文作者 Ivan Gavlik 认为:不可变性不是一个普适法则,而是一种需要根据场景做取舍的权衡。他从代码层、数据层、API 层三个层面分析了不可变性的适用边界。

作为一个 Clojure 程序员,我想从 Clojure 的视角重新审视这篇文章的论点。Clojure 的核心数据结构(list、vector、map、set)全部默认不可变——你没法修改一个 Clojure map 里的某个键值对,只能通过 assocdissoc 生成一个新 map。这不像 Java 那样需要程序员主动选择用 final 或不可变集合,在 Clojure 里不可变性就是唯一的选择。但即便如此,Clojure 也在很多地方做了务实的妥协(比如 =atom=、=transient=)。这些妥协本身就能帮我们理解"什么时候该用不可变性,什么时候不该用"。

代码层:不可变性基本是稳赚的

在单个函数的粒度上,选择不可变性几乎总是对的。原文的论点很简单: 不可变 = 没有隐藏的副作用 = 更容易理解和测试

Clojure 天然就是这样的:

(defn add-balance [user amount]
  (update user :balance + amount))

(def user {:id 1 :balance 100})
(def updated-user (add-balance user 20))

;; user 不变,updated-user 是新的 map
updated-user
;; => {:id 1 :balance 120}
user 保持不变,updated-user 是一个全新的 map。这就是 Clojure 的默认行为,所有数据结构都是不可变的。

并发问题不是线程造成的,是共享可变状态造成的

原文用 Java 展示了一个经典的竞态条件:多个线程同时给 balance+=1 ,期望结果是 1000000,实际得到的数字远小于此且不可预测。问题出在 this.balance += amount 这一行——它不是原子操作,两个线程可以交错执行。

public class User {
    private int balance;

    public void addBalance(int amount) {
        this.balance += amount;     }

    public int getBalance() {
        return balance;
    }
}

User user = new User(); Thread[] threads = new Thread[10];
for (int t = 0; t < 10; t++) {
    threads[t] = new Thread(() -> {
        for (int i = 0; i < 100000; i++) {
            user.addBalance(1);
        }
    });
    threads[t].start();
}
for (Thread t : threads) t.join();
System.out.println("Final balance: " + user.getBalance());

Java 的解决这一问题的方法有好几种: synchronized (加锁,有效但代价高)、 AtomicInteger (Java 提供的一种整数类型,内部用 CPU 的 CAS 指令保证 +=1 这类操作不会被线程交错打断,但值本身仍然是可变的)、完全不可变对象(每次返回新实例)。原文最终推荐的是"不可变 + 协调"的组合方案。

Clojure 在这个问题上走得更远——它用 atom 提供了一种既不可变又高效的方案:

(def balance (atom 0))

;; 2 个线程各执行 1000 次 +1,用 swap! 保证原子更新
;; swap! 内部用 CAS(Compare-And-Swap),不阻塞线程
(dotimes [_ 2]
  (future
    (dotimes [_ 1000]
      (swap! balance inc))))

;; 等所有 future 完成
(Thread/sleep 100)
@balance
;; => 2000
2000

关键点: swap! 接受一个纯函数 inc ,atom 内部保证将这个函数应用到当前值的过程是原子的。你写的是纯函数式变风格的代码,Clojure 的运行时会帮你处理协调问题。这正是原文说的"不可变状态迫使你显式地表达变化如何发生"的意思:在 Clojure 里,你必须用 swap!reset! 这样的操作来表达"我要改变状态",而不是随手写一个 + 然后祈祷不会出事。

Clojure 还有 ref (配合 dosync 实现事务性更新)和 agent (异步更新),分别对应不同的协调需求。但核心思路都一样:值是不可变的,状态的变化必须明确表达出来。

代码层结论

在代码层面,不可变性基本没有争议——代价小,收益大。原文也认同这一点。真正的权衡从数据建模开始。

数据层:从"改一个字段"到"记一笔历史"

到了数据建模层面,问题从"要不要修改这个对象"变成了更根本的设计选择:你的数据模型是记录"当前状态"还是记录"发生过什么"?

状态模型 vs 事件模型

;; 状态模型:只存当前值
(def user-state (atom {:name "Alice" :balance 100}))
(swap! user-state update :balance + 20)
(swap! user-state update :balance - 10)
;; 余额是 110,但你不知道中间发生了什么
@user-state
;; => {:name "Alice" :balance 110}
{:name "Alice" :balance 110}
;; 事件模型:存每一笔变更
(def events
  [{:type :balance-increased :amount 20 :at "2026-04-29T10:00:00Z"}
   {:type :balance-decreased :amount 10 :at "2026-04-29T10:05:00Z"}])

;; 当前状态是推导出来的
(reduce (fn [balance event]
          (case (:type event)
            :balance-increased (+ balance (:amount event))
            :balance-decreased (- balance (:amount event))
            balance))
        100 events)
;; => 110
110

两种模型都能得到余额 110,但事件模型额外保留了"怎么到的 110"这个信息。这就是原文说的核心区别:状态模型回答"现在是什么",事件模型回答"怎么到的这里"。

事件模型的代价

但事件模型不是免费的:

  1. 存储成本 :每次变更都存一条记录,数据量远大于只存当前状态。一个用户改了 100 次名字,状态模型只有 1 条记录,事件模型有 100 条。
  2. 设计复杂度 :你得设计事件的结构、状态转换的规则、不变量(invariant,即"在任何时候都必须成立的条件")。比简单的 CRUD 难得多。
  3. 不一定需要 :用户的昵称改了 100 次,你有必要知道中间 99 次发生了什么吗?

什么时候该用事件模型

原文的判断标准很实用:

  • 跨系统共享状态 时——多个服务需要知道同一份数据的变化历史
  • 历史有业务价值 的领域——金融交易、审计日志、工作流状态
  • 需要回溯查询 时——我想知道"上周三下午 3 点这个订单是什么状态?"

什么时候不该用:

  • 大对象频繁小更新(比如一个巨大的 JSON 每次改一个字段)
  • 历史没有业务价值(比如用户头像 URL 改了,你不在乎它之前是什么)
  • 简单的 CRUD 系统(表单→数据库→保存,没有复杂的业务流程)

Clojure 中的 reduce 和事件模型

Clojure 的 reduce 天然适合事件模型——"从初始状态出发,依次应用每个事件,得到最终状态"。上文的例子已经展示了这一点。更实际的做法是定义一个 apply-event 函数,把状态转换逻辑集中管理:

(defn apply-event [state event]
  (case (:type event)
    :user-registered (assoc state :status :active)
    :user-suspended  (assoc state :status :suspended)
    :user-activated  (assoc state :status :active)
    state))

(reduce apply-event
        {:status :pending}
        [{:type :user-registered}
         {:type :user-suspended}
         {:type :user-activated}])
;; => {:status :active}
{:status :active}

不可变模型的几种变体

原文还列举了几种不同的不可变数据建模方式:

模式 存什么 适用场景
事件溯源(Event Sourcing) 领域事件(发生了什么) 金融、审计、工作流
版本化状态(Versioned State) 完整状态的每次版本 需要回溯但不想重放事件
追加日志(Append-Only Log) 字段变更(什么字段从什么值变成了什么值) 轻量级变更追踪
不可变值对象(Immutable Value Objects) 不可变的领域对象,但持久化当前状态 大多数 CRUD 系统

API 层:Command 和 Event 不是一回事

原文最后讨论了 API 层面。传统的 REST API 是面向状态的,即客户端发送最终期望的状态:

PUT /orders/123
{"status": "SHIPPED"}

这告诉服务端"把这个订单的状态设为 SHIPPED",但不表达"为什么"要这么做。

Command 风格的 API 改为发送意图:

POST /orders/123/ship

这个 API 表达的是"发货"这个业务动作,而不是"把某个字段设为某个值"。服务端负责决定发货之后状态应该怎么变。

Event 风格的 API 则记录发生了什么:

POST /orders/123/events
{"type": "OrderShipped", "timestamp": "2026-04-29T10:00:00Z"}

原文特别强调了一点:Command 是请求("请发货"),Event 是事实("已经发货了")。通常客户端发 Command,服务端处理完之后生成 Event。不应该让客户端直接发 Event——因为只有服务端才有资格确认"这件事确实发生了"。

什么时候用 Command/Event 风格

  • 工作流重要、业务逻辑复杂
  • 多个系统需要对同一个动作做出反应
  • 审计追溯有要求

什么时候该避开:

  • 简单的 CRUD 应用,由于没有复杂的业务流程,状态就能直接表示动作,这时加 /ship/cancel/refund 这些端点反而是过度设计

Clojure 的 multimethod 视角

Clojure 的 defmulti / defmethod 天然适合 Command 风格的路由——根据 Command 的类型分发到不同的处理函数:

(defmulti handle-command :action)

(defmethod handle-command :ship [_ order]
  ;; 发货逻辑:更新状态、触发通知等
  (assoc order :status :shipped))

(defmethod handle-command :cancel [_ order]
  (assoc order :status :cancelled))

(handle-command {:action :ship} {:id 123 :status :pending})
;; => {:id 123, :status :shipped}
{:id 123, :status :shipped}

这种写法的好处是新增一个 Command 只需要加一个 defmethod ,不需要修改已有的处理逻辑。每个 Command 的处理逻辑是独立的,互不干扰。

三个层面的权衡总结

层面 不可变性推荐度 关键权衡
代码层 强烈推荐 代价小(多用一点内存),收益大(并发安全、容易推理、容易测试)
数据层 看场景 事件模型提供完整历史但增加存储和设计复杂度;状态模型简单但丢失历史
API 层 看场景 Command/Event 风格适合复杂业务;CRUD 风格适合简单应用

Clojure 的设计哲学很好地体现了这个递进的权衡:在代码层面全面拥抱不可变性(所有数据结构默认不可变),在数据层面提供灵活的选择(你可以用 atom 存状态,也可以用 reduce 重放事件),在 API 层面不强制(你可以用 defmulti 做 Command 风格,也可以直接用 map 存状态)。

不可变性不是一个开关,而是一个旋钮。根据场景调到合适的位置,才是务实的做法。