Clojure/Java的高效二进制序列化

6
我希望能够以高效的方式将Clojure对象序列化为二进制格式——即不仅仅是经典的打印和读取文本序列化。
也就是说,我想要做类似以下操作的事情:
(def orig-data {:name "Data Object" 
                :data (get-big-java-array) 
                :other (get-clojure-data-stuff)})

(def binary (serialize orig-data))

;; here "binary" is a raw binary form, e.g. a Java byte array
;; so it can be persisted in key/value store or sent over network etc.

;; now check it works!

(def new-data (deserialize binary))

(= new-data orig-data)
=> true

动机是我有一些大型数据结构,其中包含大量的二进制数据(在Java数组中),我想避免将所有这些数据转换为文本再进行转换的开销。此外,我试图保持格式紧凑,以最小化网络带宽使用。
我想要具有以下具体特点:
- 轻量级、纯Java实现 - 支持Clojure的所有标准数据结构以及所有Java基元、数组等。 - 不需要额外的构建步骤/配置文件 - 我更希望它“开箱即用”。 - 无论是处理所需的处理时间还是二进制编码表示的紧凑性,都具有良好的性能。
在Clojure中实现这个功能的最佳/标准方法是什么?

根据您所编码的内容以及编码方式的不同,文本可能比某些二进制格式更快且更紧凑。通常,在序列化中的开销是反射的使用而不是转换,因此需要支持任意数据结构才是真正的问题。这比罐头与Java序列化进行了比较(http://vanillajava.blogspot.com/2011/08/avoiding-java-serialization-to-increase.html)。 - Peter Lawrey
它是否需要能够处理对运行时对象(如原子和引用)的引用? - bmillare
我在使用Kryo与Cascalog、ElephantDB和Storm时取得了巨大的成功。carbonite是由revelytix开发的,它可以直接序列化大多数Clojure数据结构。 - Sam Ritchie
4个回答

11

可能我理解有误,但是标准的Java序列化有什么问题吗?速度太慢、文件太大,还是其他什么原因?

一个用于普通Java序列化的Clojure包装器可以像这样:

(defn serializable? [v]
  (instance? java.io.Serializable v))

(defn serialize 
  "Serializes value, returns a byte array"
  [v]
  (let [buff (java.io.ByteArrayOutputStream. 1024)]
    (with-open [dos (java.io.ObjectOutputStream. buff)]
      (.writeObject dos v))
    (.toByteArray buff)))

(defn deserialize 
  "Accepts a byte array, returns deserialized value"
  [bytes]
  (with-open [dis (java.io.ObjectInputStream.
                   (java.io.ByteArrayInputStream. bytes))]
    (.readObject dis)))

 user> (= (range 10) (deserialize (serialize (range 10))))
 true

有些值不能被序列化,例如Java流和Clojure原子/代理/未来,但对于大多数普通的值,包括Java基本类型和数组以及Clojure函数、集合和记录,它应该是有效的。

实际上是否节省任何内容取决于具体情况。在我进行的小数据集测试中,文本和二进制序列化所需的时间和空间大致相同。

但对于主要数据为Java基本类型数组的特殊情况,Java序列化可以比文本序列化快几个数量级,并节省大量空间。(在笔记本电脑上进行快速测试,100k随机字节:序列化需要0.9毫秒,占用100kB; 文本需要490毫秒,占用700kB。)

请注意,(= new-data orig-data) 测试对于数组无效(它委托给Java的 equals,而对于数组只检查它们是否为相同的对象),因此您可能需要编写自己的等式函数来测试序列化。

user> (def a (range 10))
user> (= a (range 10))
true
user> (= (into-array a) (into-array a))
false
user> (.equals (into-array a) (into-array a))
false
user> (java.util.Arrays/equals (into-array a) (into-array a))
true

有趣 - 我以前在Java序列化方面遇到过糟糕的经历(速度太慢,消息大小太大),但从您的测试中听起来它可能确实非常适合大型数组。 - mikera
2
@mikera 我认为Java序列化的主要问题是意外依赖性,因此您可能最终会序列化JVM的一半。但是,如果您坚持使用简单值,它就非常好。这篇博客发现对于一个简单的POJO,Java序列化比Google protobufs(略微)更快、更小。 - j-g-faustus

5

4

你有没有考虑过使用谷歌的protobuf?你可能想要查看适用于Clojure的GitHub存储库中的接口。


有趣...很高兴看到有一个Clojure的包装器! 但是,除非我弄错了,这并不允许序列化任意Clojure对象,即你必须在.proto文件中预先指定结构? - mikera
很高兴你觉得有帮助!不过,我不太明白你所说的“任意”是什么意思,但无论如何,就我而言,.proto文件只是定义数据结构(类似于模式)的地方,API会为你完成其他所有工作。 - Nano Taboada
我的意思是:我希望能够在不预先定义数据结构的情况下序列化和反序列化Clojure结构。似乎这应该是可能的,因为所有Clojure数据结构都只是映射、集合、列表等,再加上一些基本的Java对象。由于许多数据结构在Clojure中是在运行时动态创建的,因此几乎不可能提前指定它们全部。 - mikera

3
如果您没有提前定义模式,将数据序列化为文本可能是最好的选择。要序列化一般的任意数据,您需要做大量的工作来保留对象图,并进行反射以查看如何序列化所有内容……至少Clojure的打印机可以对每个项执行静态、无反射查找print-method。
相反,如果您真的想要一个优化的线路格式,您需要定义一个模式。我曾经从Java使用过Thrift和从Clojure使用过protobuf:两者都不是非常有趣,但如果您提前计划,它并不是极其繁琐的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接