AICon全球人工智能与机器学习技术大会周四开幕,点击查看完整日程>> 了解详情
写点什么

Realm Java 原理介绍以及常见问题

  • 2016 年 11 月 17 日
  • 本文字数:3937 字

    阅读完需:约 13 分钟

Realm 简介

Realm 与 MVCC

Realm 是一个 MVCC 数据库 ,底层用 C++ 编写。MVCC 指的是多版本并发控制。

MVCC 解决了一个重要的并发问题:在所有的数据库中都有这样的时候,当有人正在写数据库的时候有人又想读取数据库了(例如,不同的线程可以同时读取或者写入同一个数据库)。这会导致数据的不一致性 - 可能当你读取记录的时候一个写操作才部分结束。如果数据库允许这种事情发生,你就会得到和最终数据库里的数据不一致的数据。

有很多的办法可以解决读、写并发的问题,最常见的就是给数据库加锁。在之前的情况下,我们在写数据的时候就会加上一个锁。在写操作完成之前,所有的读操作都会被阻塞。这就是众所周知的读 - 写锁。这常常都会很慢。

类似 Realm 的 MVCC 的数据库采用了另外的一个方法:每一个连接的线程都会有数据在一个特定时刻的快照。

如上图所示:假设线程 1 正在读取 Realm 数据库的 V1 版本,与此同时,线程 2 需要写入数据库,创建一个新的 R1 节点以修改 V1 版本中的 R 节点;R1 节点的右子树仍然指向原 B 节点,左子树指向新建的 A1 节点;A1 节点的右子树仍然指向原 D 节点,左子树指向新创建的 C1 节点。

在线程 2 写入的过程中,线程 1 的读取操作并不会被阻塞,其仍然能够正常访问数据库版本 V1 的所有节点。

请看上图中的第三部分,当线程 2 写入完成,线程 1 之前的读取操作也完成,于是线程 1 决定刷新以得到最新的数据库更改。这时线程 1 也同步到了数据库的 V2 版本,所有在第二部中线程 2 对数据库的更改都对线程 1 可见。R 和其他相应的节点都替换成了线程 2 写入的新信息,同时原节点 C、A 和 R 不再被任何线程需要,变成了垃圾节点,将会在之后的写操作中被回收。

Realm 的懒加载

大部分的时候,你都把数据存在磁盘上的数据库文件中。开发者发起一个从持久化机制(比如 ORM 或者 Core Data)中获取数据的请求,数据格式会是和本地平台密切相关的(比如安卓或者苹果)。这个时候,持久化机制会把请求转换成一系列的 SQL 语句,创建一个数据库连接(如果没有创建的话),发送到磁盘上,执行查询,读取命中查询的每一行的数据,然后存到内存里(这里有内存消耗)。之后你需要把数据序列化成可在内存里面存储的格式,这意味着比特对齐,这样 CPU 才能处理它们。

最后,数据需要转换成语言层面的类型,然后它会以对象的形式返回,这样平台才能用(POJO, NSManagedObject 等等)来处理它。如果你在你的持续化机制中有子引用或者列表引用的话,这个过程会更复杂。这个过程会一遍一遍的执行(取决于你的持续化机制和配置)。如果你使用自产自销的机制,情况也大致相同。

Realm 的方法不一样。这就是我们零拷贝架构起作用的地方。

Realm 跳过了整个拷贝过程,因为数据库文件是 memory-mapped。Realm 在访问文件偏移的时候就好像文件已经在内存中一样,实际上不是,而是虚拟内存。这是个 Realm 核心文件格式的重要设计决定。它允许文件能在没有做任何反序列化的情况下可以在内存中读取。

Realm 跳过了所有这些开销很大的步骤,而这些步骤在传统的持久化机制中必须执行。Realm 只需要简单地计算偏移来找到文件中的数据,然后从原始访问点返回数据结构 (POJO/NSManagedObject/ 等等) 的值 。这更有效而且更快。

Realm Java 介绍

上文中所提到的 Realm 与 MVCC 相关的概念在所有的 Realm 产品中都适用,接下来我们介绍一下在 Realm Java 中这些概念是怎么与 Java 语言和 安卓框架相结合并实现的。

线程

在 Realm Java 中你可以使用 Realm.getInstance()(或者 Realm.getDefaultInstance())来在当前线程中获得一个 Realm 实例。Realm 使用引用计数管理每个线程中的 Realm 实例。多次针对同一个 RealmConfiguration 在同一线程中调用会返回同一个 Realm 实例。Realm 实现了 Closeable 接口,这意味这每一次的 getInstance() 调用都应该对应一个 close() 调用以释放相应的资源。

如果 getInstance() 是第一次在当前线程调用,那么它会在当前最新的数据版本之上打开一个新的 Realm 实例。

对于一个拥有安卓 Looper 的线程,Realm 通过安卓的 Handler 系统来通知各个线程中的 Realm 实例有写入操作发生。举例来说,假设线程 1 是安卓 UI 线程,当线程 2 中对 Realm 进行了写入操作后,线程 1 的 Realm 会在下一次 Looper 事件中更新到线程 2 写入后的数据版本。

对于一个非 Looper 线程来说,Realm 的数据版本更新依赖于 Realm.waitForChange() 调用。该调用会阻塞当前线程直到其他线程有写入操作完成。

Realm 对象代理和字节码替换

Realm 通过使用注解处理和字节码变换来联系 RealmObject 和 Realm 数据存储。我们通过下面这个简单的例子来了解一下这个过程。例如我们有如下类定义:

当工程编译完成后,Realm 的注解处理器会生成如下 DogRealmProxy.java:

请注意这里的 realmGet$xxx() 和 realmSet$xxx() 函数。RealmObject 正是通过这些函数来与 Realm 数据库打交道的。当然这还不是 Realm 全部的秘密,如果反编译 build/intermediates/transforms/xxx/xxx/Dog.class 文件,你会发现它与你之前定义的 Dog.java 并不完全一样:

首先,我们注意到了有四个与 DogRealmProxy 类一一对应新的方法(realmGet$xxx/realmSet$xxx)被插入到了 Dog 类中,这四个新方法只是简单的 setter 和 getter;其次,在函数 getAge()、setAget() 以及 printName 中所有对 Dog 属性的直接访问都被替换成了相应生成的方法 realmGet$xxx 和 realmSet$xxx。

这就是全部的秘密所在了。在从 Realm 实例中获取任何 Realm 对象的时候(比如调用 Realm.createObject() 或者 RealmQuery.findFirst()),你实际上是获取了这个对象相应的 Realm 代理对象。对其属性的访问实际上都是通过相应生成的方法来访问底层的 Realm 数据库来实现的。

同时这也解释了我们之前提到的 Realm 的懒加载特性。在查询返回一个或者多个 Realm 对象的时候,这些对象的属性并没有被拷贝到 Java 堆中,这使得 Realm 的查询非常得快。这些属性只在需要被访问的时候,才经由生成的 getter 方法加载。

Realm Java 常见问题

在了解了 Realm 的这些关键实现之后,如下这些常见问题也就不难解释了。

跨线程 Realm 访问

Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.

这是一个初次使用 Realm 时常见的异常。请注意,RealmObject、RealmResults 等相关对象都是与其线程中的 Realm 实例绑定的。因为两个线程中的 Realm 实例可能锁定了不同的 Realm 版本,这些对象也可能处于不同的数据版本,跨线程访问会引起数据的不一致性。所以,在另一个线程中访问同一个对象的时候,请在该线程中进行查询以获得这个对象绑定该线程 Realm 的实例。或者使用 Realm 提供的相应的异步查询接口,具体请参考相关文档。

托管 Realm 对象与非托管 Realm 对象

在 Realm 文档里这两个概念(managed Realm object/unmanaged Realm Object)尝尝被提及。通过以上的介绍,我们不难想象这里的 托管 Realm 对象(managed Realm object)指的是 Realm 的代理对象实例,例如 DogRealmProxy 的实例;而非托管 Realm 对象指的是 (unmanaged Relam object)原始对象的实例,例如 Dog 的实例。

我们也不难想象,对于非托管 Realm 对象来说,他可以经由类似 new Dog() 的方式创建,而且对它本身属性的访问并不会引起任何对 Realm 数据库的访问。

当然,非托管 Realm 对象仍然可以被保存到 Realm 数据库中并且相应地返回一个托管 Realm 对象,例如:

如上代码中的 Realm.copyToRealm() 会将传入的非托管对象保存到 Realm 中并且返回一个托管 Realm 对象。

另外,显而易见,非托管 Realm 对象不具备 Realm 托管对象的一切高级特性,比如自动更新特性。

重复主键异常

在调用 Realm.createObject(Class clazz) 或类似函数时,下列异常有可能被抛出:

Primary key constraint broken. Value already exists: 0

这是因为 Realm.createObject(Class clazz) 实际上隐式调用了原始对象的默认无参数构造器,然后通过 Realm.copyToRealm() 方法将其存入 Realm 中。隐式构造器会给其主键属性赋一个默认值,而当第二次调用时,主键仍会是这个默认值。这就导致了 Realm 存储的对象出现了重复主键,从而异常被抛出。解决方法有很多种,譬如调用 Realm.createObject(Class clazz, Object primaryKeyValue) 方法在对象创建时指定一个不重复的主键。

Realm 数据库文件不断增大

让我们来看看如下代码:

这里声明的 AsyncTask 会在每次执行的时候打开一个 Realm 实例,但是并没有在使用结束后关闭。通过我们对 Realm 线程相关的介绍,不难想象这会导致某一 Realm 数据版本被该线程中的 Realm 实例锁定,因为 Realm 实例没有被正确关闭,Realm 无法得知其对应的数据已经不需要再被访问。假设这个 AsyncTask 在后台被反复执行,同时又有另一个线程在不断更新着 Realm 的数据,那么每一个 AyncTask 都会锁定一个不同的 Realm 数据版本,从而导致 Realm 文件的体积不断变化。所以,请在后台线程结束时关闭相应的 Realm 实例。

Realm 库与 apk 大小

Realm 几乎发布了针对所有 ABI 的 so 文件。如果你的应用在 google play 市场发布,那么你可以很方便的通过 google 官方提供的 apk Split 将各种 ABI 分开打包。但假设你的应用是在国内市场发布,ABI Split 可能无法正常工作,你可以考虑只包含部分 so 文件(例如 arm64 设备兼容 armeabi 和 armeabi-v7a,而只支持 armeabi 的设备几乎没有人使用了)。具体信息可以查看 Realm 的文档。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 11 月 17 日 16:574355

评论

发布
暂无评论
发现更多内容

JavaScript 学习(八)

空城机

JavaScript 前端 6月日更

在线URL转sitemap工具

入门小站

Linux

我看JAVA 之 JVM

awen

Java JVM

ES6 中的 Symbol 是什么?

编程三昧

JavaScript 前端 ES6 数据类型 symbol

Linux之cp命令

入门小站

Linux

ECMAScript 2021 (ES12)通过,正式写入 ECMAScript 标准

清秋

ecmascript 新闻资讯 ES2021 ES12 标准

蜜雪冰城主题曲血洗B站:企业自媒体运营如何接地气

石头IT视角

【国际禁毒日】和TcaplusDB一起向毒品say NO!

tcaplus

数据库 TcaplusDB

这是一场按下播放键就停不下来的冒险

脑极体

数据仓库常见建模方法与大数据领域建模实例综述

云祁

数据仓库 数据建模 维度建模

真的了解 HDFS 的 SecondaryNameNode 是干什么的?

云祁

Visual Studio 2010下ASPX页面的TreeView控件循环遍历

DisonTangor

C#

【国际奥林匹克日】和TcaplusDB君一起动起来!

tcaplus

数据库 游戏 TcaplusDB

WasmEdge (曾用名 SSVM) 成为 CNCF 沙箱项目

WasmEdge

云计算 云原生 webassembly cncf

Kubernetes手记(21)- 新一代监控架构

雪雷

k8s 6月日更

好忙

IT蜗壳-Tango

6月日更

看完阿里开源笔记,我终于敢说精通“网络协议”了

Java架构师迁哥

你遇到过哪些质量很高的 Java 面试题?

Java架构师迁哥

自动驾驶产业进入“两条腿”时代:车路协同的中国式飞跃

脑极体

深入了解JAVA线程篇

邱学喆

线程 线程池 线程间通信 线程回调

.NET Core HttpClient源码探究

yi念之间

.net core HttpClient

大白话彻底搞懂 HBase Rowkey 设计和实现方式

云祁

大数据 HBase

阿里云中间件首席架构师李小平:企业为什么需要云原生?

阿里巴巴云原生

与8090创业者、投资人共话“初心”!2021中国新青年创业投资峰会举办

创业邦

七牛云 霍锴:SDK 是一款技术服务的门面,如何方便用户高效接入是前提|Meetup 讲师专访

七牛云

音视频 sdk Meetup

Java学到什么程度才能叫精通?

Java架构师迁哥

我用来阻止你摸鱼看直播、知乎和微博的Chrome插件

OBKoro1

chrome 效率工具 前端 工作效率 chrome扩展

Github上星标85k的,图解操作系统、网络、计算机 PDF,竟是阿里的?

Java架构师迁哥

5分钟速读之Rust权威指南(二十九)循环引用

码生笔谈

rust

这是一场按下播放键就停不下来的冒险

白洞计划

基于朴素ML思想的协同过滤算法(十七)

数据与智能

推荐算法

数据cool谈(第2期)寻找下一代企业级数据库

数据cool谈(第2期)寻找下一代企业级数据库

Realm Java 原理介绍以及常见问题-InfoQ