我是个写 Rust 的独立开发者。在我的交易模拟系统里,我引以为傲地设计了一套纯内存的领域事件驱动架构。
没有 Kafka ,没有 RabbitMQ ,就靠 Rust 强大的内存通道,实现了微秒级的模块解耦。交易服务只管平仓,根本不需要关心是谁在发通知、是谁在算奖励。
直到有一天,我发现了一个幽灵般的 Bug 。
第一章:消失的奖励( The Race Condition )
场景很简单:用户注册成功 -> 后端发放“新手大礼包” -> 推送弹窗通知。
逻辑看起来天衣无缝:后端提交注册事务,紧接着发送一个“注册成功”的内部事件。
然而测试发现,用户注册进去了,奖励也发了,但前端死活收不到弹窗。
排查日志后,我发现这是一个经典的分布式竞态条件( Race Condition )。虽然我是单体应用,但前后端在时间维度上是割裂的:
后端(光速):事务提交、触发事件、计算奖励、尝试 Websocket 推送。此时发现用户尚未建立连接(因为是刚注册完),于是只能两手一摊,丢弃消息。
前端(龟速):收到注册成功 200 OK ,保存 Token ,初始化 SDK ,请求个人信息,最后才建立 Websocket 连接。
这就好比:后端这边的发令枪响了,奖杯也扔出去了,前端的运动员还在系鞋带。等他抬起头,奖杯早就摔碎在底上了。
第二章:教科书式的“正确”方案( The Inbox Pattern )
遇到这种“发太快、连太慢”的问题,资深架构师(其实就是我自己)的第一反应是:上持久化,用信箱模式( Inbox Pattern )!
方案极其正统:
建表:搞个“用户通知表”。
落库:不管用户在不在,消息先插进数据库,标记为“未读”。
推拉结合:后端试着推一下;前端连上 WS 后,立马调一个 HTTP 接口拉取未读消息。
这个方案完美吗?完美。可靠性高吗?极高。 但我看着手里的键盘,犹豫了。
作为一个独立开发者,为了这就加一张表?写一套 CRUD ?前端还得改逻辑去轮询?如果未来我有 10 种消息都要这么搞,数据库会不会爆炸?我只有一双手,我要的是功能上线,不是在代码里建一座大教堂。
“复杂性是独立开发者的墓志铭。” 我把这个方案否了。
第三章:来自内存的暴力美学( The Spin-Wait )
我开始反思:通过数据库中转,本质上是用空间(磁盘)换时间(等待)。 那我能不能直接用时间换时间?
Rust 的协程( Task )极其廉价,开几万个跟玩一样。我为什么不直接在内存里“等”前端上线呢?
于是,诞生了这个**“异步自旋重试”**方案。
我并没有修改复杂的架构,而是写了一个简单的“蹲守”逻辑:
当需要发送奖励而用户不在线时,后端并不会直接放弃,而是派出一个轻量级的后台协程(相当于一个临时工)。
这个协程的任务非常简单粗暴:它拿着信站在门口,每隔 0.5 秒看一眼“用户连上 Websocket 了没?”
如果连上了,立马把信塞进去,任务结束。
如果没连上,就睡一会儿再看。
如果等了 60 秒(超时阈值)还没人影,那就算了,毁灭吧。
第四章:取舍与哲学
这个方案在“架构师”眼里可能是离经叛道的:
不可靠:万一这 60 秒内服务器重启,内存里的消息就丢了。
不优雅:居然用轮询( Polling )这种原始手段?
但对于我的场景,它是最合适的:
零基础设施:不需要 Redis ,不需要新表,不需要改前端 Pull 逻辑。
极致简单:就在通知服务里加个小函数,其他地方完全无感。
用户体验:前端注册完,卡顿了 3 秒才连上,第 3.5 秒后端那个“蹲守协程”刚好醒来,把奖励推过去。用户看来就是:注册 -> 自动登录 -> 砰!奖励弹窗。丝般顺滑。
关于丢消息:如果服务器真的在那几十秒重启了,用户仅仅是少看了一个“获得奖励”的弹窗,但他账户里的钱( DB 事务保证)是一分不少的。这个 SLA ,我可以接受。
结语
我们在做架构设计时,往往容易陷入“大厂思维”的陷阱,追求理论上的完美和绝对的可靠。
但在独立开发的世界里,代码量越少,Bug 越少;依赖越少,睡得越香。
用 Rust 的高性能去弥补架构的“土”,用内存的易失性去换取开发的“快”。这就是我作为一个 CRUD Boy 的生存之道。

