Crystal + Kemal 实战:构建一个内存级实时应用

追新独立开发者 中级 22小时前 更新于 2026年7月26日 198 浏览 7 点赞 约 1 分钟

14MB 的镜像体积,没有任何数据库,状态全在内存里,这就是我用 Crystal 和 Kemal 搞的一个小工具。很多开发者习惯了无论项目大小都上全家桶框架,但这次我想验证一个观点:框架的选择得跟问题规模匹配,而不是靠习惯。

这个应用的核心逻辑很简单:一个主持人开启会话,参与者加入并私密排序,直到主持人点击“揭晓”之前,任何参与者的排序数据绝对不能出现在服务器发出的 Payload 中。这不是靠前端隐藏,而是从服务器端强制执行的隐私不变性。

为什么选 Kemal?因为它就是一个类 Sinatra 的微框架,只有路由、请求响应、WebSocket 和静态文件。对于一个不需要 ORM、不需要数据库迁移、也不需要复杂资产构建的项目来说,Kemal 刚好覆盖了所有需求,没有任何冗余。

我把业务逻辑全部抽离在纯 Crystal 对象中,完全不依赖 Kemal,这样在测试领域逻辑时根本不需要启动服务器。

一、定义不可变领域对象

为了保证数据的绝对正确,我定义了一个 Ranking 结构体。它在初始化时就强制校验:必须包含所有卡片,且不能重复。这样在后续调用中,我可以放心地使用 index! 而不需要写任何 nil 检查。

struct Ranking
 getter order : Array(Motivator)

 def initialize(@order : Array(Motivator))
 validate!
 end

 # 1 为最重要,10 为最不重要
 def position(motivator : Motivator) : Int32
 @order.index!(motivator) + 1
 end

 private def validate!
 unless @order.size == Motivator.values.size
 raise ArgumentError.new("A ranking must order all ten cards")
 end

 if @order.uniq.size != @order.size
 raise ArgumentError.new("A ranking must not contain a duplicate card")
 end
 end
end

二、状态机控制权限

会话被设计成一个简单的状态机:Lobby(大厅) → Revealed(揭晓)。一旦进入揭晓状态就是终局,不可逆转。通过这种方式,我可以非常简单地在服务器端拦截非法操作。

def reveal(token : String) : Nil
 raise Forbidden.new("Only the facilitator can reveal") unless token == @facilitator_token
 guard_lobby!
 @phase = Phase::Revealed
end

private def guard_lobby! : Nil
 raise WrongPhase.new("Session is already revealed") if revealed?
end

在这种架构下,服务器成为了隐私的最后一道防线。只要状态机没跳转到 Revealed,后端逻辑就绝不会把其他人的排序数据打包发给客户端。

完整源码和详细步骤可以参考这个路径:

github.com/JadeKharats/motivators/blob/main/TUTORIAL.md
AI编程AI编程实战webdevTutorialcrystal

全部回复 (4)

折腾党小雨 中级 12小时前
这种纯内存方案配合 Docker 限制内存配额,能有效防止内存泄漏搞崩宿主机。
0 回复
独立开发者Leo 专家 12小时前
那要是重启服务,内存里的数据是不是全没了?得手动持久化吗
0 回复
产品经理阿强 中级 12小时前
得配个快照机制才行,不然重启真的会心碎,你打算怎么存?
0 回复
杭漂码农 专家 12小时前
之前用 Go 搞过类似的小工具,确实没必要非得挂个数据库,快得离谱。
0 回复

发表回复

支持 Markdown 格式