第五章:Swift 觉醒
这是用于 Swift 的 FoundationDB 客户端。它相当底层,(几乎)无 Foundation
依赖,并且可以集成到 async
/await
中(不包含且不需要 Swift-NIO)。
显然,您需要先安装 FoundationDB
。从 官方网站 下载。接下来的部分比较棘手,因为 CFDB 模块(C 绑定)不会自行链接 libfdb_c
库,并且 FoundationDB 安装过程中尚未提供 pkg-config
。因此,您必须自行安装。运行
chmod +x ./scripts/install_pkgconfig.sh
./scripts/install_pkgconfig.sh
或复制 scripts/libfdb.pc
(选择您的平台)到 macOS 上的 /usr/local/lib/pkgconfig/
或 Linux 上的 /usr/lib/pkgconfig/libfdb.pc
。
v5 在内部结构和 API 方面都是一个巨大的更新。最重要的更新当然是 async
/await
的采用。因此,Swift-NIO 依赖项已被删除,因为它变得冗余。自然地,许多 API 因为过时而消失了。但是,剩余的 API 看起来几乎相同,只是它不再使用事件循环和 futures。
在 v4(和更早版本)中,有一个阻塞式 API。当然,几乎没有人使用它,因为,嗯,为什么呢?然而,在 v5 开发过程中,结果表明此 API 是采用 async
/await
的完美基础,只需在签名中添加 async
即可。就像它一直在等待它一样。我一直维护这个 API 以及 NIO API,这真是太好了。对我来说是件好事。
所以现在只有两种使用 FDBSwift 的方式
AnyFDB
(因此,FDB
,默认实现)中使用自动提交的 Oneshot API,例如 get(key:)
、set(key:value:)
、atomic(_ op:key:value)
等。fdb.begin()
,一些操作和 transaction.commit()
(基本上您可以完全控制事务,并且必须手动捕获错误并自行处理重试错误,请参阅下面的详细信息和相关部分)。fdb.withTransaction { transaction in /* 一些操作 */ }
,它为您管理重试,但您仍然需要注意其他错误。另一个不太小的变化是,错误情况 FDB.Error.transactionRetry
不再具有关联值 AnyFDBTransaction
。在 v4 中,当您可以开始一个 NIO 事务,并且在任何时候(包括 flatMapError
/recover
等)都没有对其的引用时,它非常方便(我个人有很多这样的情况)。现在这个问题已经彻底消失,并且完全不需要这个关联值。
默认情况下(并且在核心中),此包装器以及 C API 都使用字节键和值(不是指针,而是 Array<UInt8>
)。有关更多详细信息,请参阅 键、元组和子空间 部分。
值始终是字节 (typealias Bytes = [UInt8]
)(如果未找到键,则为 nil
)。您可能会问为什么不是 Data
?我想尽可能长时间地保持无 Foundation
依赖(说真的,仅仅为了 Data
对象而导入半个世界,而 Data
对象只是 NSData
的一个花哨的包装器,而 NSData
又是 [UInt8]
的一个花哨的包装器?)(您是否忘记了您需要用 autoreleasepool
包装所有 Data
对象,否则您会得到花哨的内存泄漏?)(除了 Linux 之外,是的),您可以始终使用 Data(bytes: myBytes)
初始化器将字节转换为 Data
(您为什么要这样做?哦,是的,JSON...好的,但请您自己完成,扩展来救援)。
// Default cluster file path depending on your OS
let fdb = FDB()
// OR
let fdb = FDB(clusterFile: "/usr/local/etc/foundationdb/fdb.cluster")
您可以选择性地传递网络停止超时。
请记住,此时连接尚未建立,它在第一次实际数据库操作时自动建立。如果您想显式连接到数据库并捕获可能的错误,只需调用
try fdb.connect()
断开连接是自动的,在 deinit
时进行。但您也可以直接调用 disconnect()
方法。请注意,如果在断开连接期间出现任何问题,您将收到无法捕获的致命错误。这没什么大不了的,因为断开连接应该只发生一次,当您的应用程序关闭时(并且您不应该真正关心那时的致命错误)。此外,您非常应该确保 FDB 在实际关闭之前真正断开连接(捕获 SIGTERM
信号并等待 disconnect
完成),否则您可能会遇到未定义的行为(我个人还没有真正遇到过这种情况,但这并非危言耸听;当您不遵循 FoundationDB 建议时,事情确实会变得非常混乱)。
在连接到 FDB 集群之前,您还可以设置网络选项
try fdb.setOption(.TLSCertPath(path: "/opt/fdb/tls/chain.pem"))
try fdb.setOption(.TLSPassword(password: "changeme"))
try fdb.setOption(.buggifyEnable)
有关完整的网络选项集,请参阅 FDB+NetworkOptions.swift
文件。
所有键都是 AnyFDBKey
协议
public protocol AnyFDBKey {
func asFDBKey() -> Bytes
}
此协议被 String
、StaticString
、Tuple
(不是 Swift 中的 Tuple 类型)、Subspace
和 Bytes
(又名 Array<UInt8>
)采用,因此您可以自由使用这些类型中的任何一种,或在您的自定义类型中采用此协议。
由于您可能希望在应用程序中拥有某种键命名空间,因此您应该坚持使用 Subspace
,它是一个用于创建命名空间的极其有用的工具。在底层,它利用了 Tuple 概念。您真的不必费心深入研究它(简而言之:基本上是打折的 MsgPack,一种棘手的二进制协议),只需记住,目前子空间接受 String
、Int
、Float
(又名 Float32
)、Double
、Bool
、UUID
、Tuple
(因此 FDBTuplePackable
)、FDB.Null
(您为什么要这样做?)和 Bytes
作为参数。
// dump subspace if you would like to see how it looks from the inside
let rootSubspace = FDB.Subspace("root")
// also check Subspace.swift for more details and usecases
let childSubspace = rootSubspace["child"]["subspace"]
// OR
let childSubspace = rootSubspace["child", "subspace"]
// Talking about tuples:
let tuple = FDB.Tuple(
Bytes([0, 1, 2]),
322,
-322,
FDB.Null(),
"foo",
FDB.Tuple("bar", 1337, "baz"),
FDB.Tuple(),
FDB.Null()
)
let packed: Bytes = tuple.pack()
let unpacked: FDB.Tuple = try FDB.Tuple(from: packed)
let tupleBytes: Bytes? = unpacked.tuple[0] as? Bytes
let tupleInt: Int? = unpacked.tuple[1] as? Int
// ...
let tupleEmptyTuple: FDB.Tuple? = unpacked.tuple[6] as? FDB.Tuple
let tupleNull: FDB.Null? = unpacked.tuple[7] as? FDB.Null
if tupleNull is FDB.Null || unpacked.tuple[7] is FDB.Null {}
// you get the idea
就这么简单
try await fdb.set(key: "somekey", value: someBytes)
// OR
try await fdb.set(key: Bytes([0, 1, 2, 3]), value: someBytes)
// OR
try await fdb.set(key: FDB.Tuple("foo", FDB.Null(), "bar", FDB.Tuple("baz", "sas"), "lul"), value: someBytes)
// OR
try await fdb.set(key: Subspace("foo", "bar"), value: someBytes)
值始终为 Bytes?
(如果未找到键,则为 nil
),您应该在使用前解包它。键当然仍然是 AnyFDBKey
。
let value = try await fdb.get(key: "someKey")
由于在 FoundationDB 中,键在底层字节上按字典顺序排序,因此您可以通过查询从键 somekey\x00
到键 somekey\xFF
(从字节 0 到字节 255)的范围来获取所有子空间值(甚至来自整个数据库)。不过,您不应该手动执行此操作,因为 Subspace
对象有一个为您执行此操作的快捷方式。
此外,get(range:)
(及其版本)方法不返回 Bytes
,而是返回一个特殊的盒子结构 FDB.KeyValuesResult
,其中包含一个 FDB.KeyValue
结构数组和一个标志,指示数据库是否可以提供更多结果(分页,有点像)。
public extension FDB {
/// A holder for key-value pair
public struct KeyValue {
public let key: Bytes
public let value: Bytes
}
/// A holder for key-value pairs result returned from range get
public struct KeyValuesResult {
/// Records returned from range get
public let records: [FDB.KeyValue]
/// Indicates whether there are more results in FDB
public let hasMore: Bool
}
}
如果范围调用返回零记录,则它将导致一个空的 FDB.KeyValuesResult
结构(而不是 nil
)。
let subspace = FDB.Subspace("root")
let range = subspace.range
/*
these three calls are completely equal (can't really come up with case when you need second form,
but whatever, I've seen worse whims)
*/
let result: FDB.KeyValuesResult = try await fdb.get(range: range)
let result: FDB.KeyValuesResult = try await fdb.get(begin: range.begin, end: range.end)
let result: FDB.KeyValuesResult = try await fdb.get(subspace: subspace)
// although call below is not equal to above one because `key(subspace:)` overload implicitly loads range
// this one will load bare subspace key
let result: FDB.KeyValuesResult = try await fdb.get(key: subspace)
result.records.forEach {
dump("\($0.key) - \($0.value)")
}
清除(删除、删除,随便你怎么称呼)记录也很简单。
try await fdb.clear(key: childSubspace["concrete_record"])
// OR
try await fdb.clear(key: rootSubspace["child"]["subspace"]["concrete_record"])
// OR EVEN
try await fdb.clear(key: rootSubspace["child", "subspace", "concrete_record"])
// OR EVEN (this is not OK, but still possible :)
try await fdb.clear(key: rootSubspace["child", FDB.Null, FDB.Tuple("foo", "bar"), "concrete_record"])
// clears whole subspace, including "concrete_record" key
try await fdb.clear(range: childSubspace.range)
FoundationDB 还支持原子操作,如 ADD
、AND
、OR
、XOR
等等(请参阅 文档)。您可以使用 atomic(_ op:key:value:)
方法执行这些操作中的任何一个
try await fdb.atomic(.add, key: key, value: 1)
考虑到最流行的原子操作是递增(或递减),我添加了方便的语法糖
try await fdb.increment(key: key)
// OR returning incremented value, which is always Int64
let result: Int64 = try await fdb.increment(key: key)
// OR
let result = try await fdb.increment(key: key, value: 2)
但是,请记住,上面的示例不再是原子的。
以及递减,它只是 increment(key:value:)
的代理,只是反转了 value
let result = try await fdb.decrement(key: key)
// OR
let result = try await fdb.decrement(key: key, value: 2)
之前的所有示例都利用了 FDB
对象方法,这些方法是隐式事务性的。如果您想在一个事务中执行多个操作(并体验 ACID 的所有乐趣),您应该首先在 FDB
对象上下文中使用 begin()
方法开始事务,然后执行您的操作(只是不要忘记在最后 commit()
它,默认情况下,如果未显式提交,事务将回滚,或者在 5 秒超时后回滚)。
let transaction = try fdb.begin()
transaction.set(key: "someKey", value: someBytes)
try await transaction.commit()
// OR
transaction.reset()
// OR
transaction.cancel()
或者您可以将事务对象留在原处,它会在 deinit
时重置并销毁自身。将其视为自动回滚。请参阅有关重置和取消行为的官方文档:https://apple.github.io/foundationdb/api-c.html#c.fdb_transaction_reset
不过,实际上没有必要提交只读事务 :)
此外,您可以使用 transaction.setOption(_:)
方法设置事务选项
let transaction: AnyFDBTransaction = ...
try transaction.setOption(.transactionLoggingEnable(identifier: "debuggable_transaction"))
try transaction.setOption(.snapshotRywDisable)
有关选项的完整列表,请参阅 Transaction+Options.swift
。
由于 FoundationDB 是一个相当事务性的数据库,因此有时由于序列化失败(更常见且错误地称为死锁),commit
可能不会成功。当两个或多个事务创建重叠的冲突范围时,可能会发生这种情况。或者,简单来说,当它们尝试同时访问或修改相同的键(除非它们处于 snapshot
读取模式)时。这是预期的(并且在某种程度上是受欢迎的)行为,因为这就是 ACID 的工作方式。
在这些[并非罕见]的情况下,允许再次重放事务。您如何知道您的事务是否可以重放?它失败并出现特殊的错误情况 FDB.Error.transactionRetry
。如果您的事务因这个特定的错误而失败,则意味着事务已经回滚到其初始状态,并准备好再次执行。
您可以手动实现此重试逻辑,或者您可以只使用 FDB
实例方法 withTransaction
。以下示例应该是不言自明的
let maybeString: String? = try await fdb.withTransaction { transaction in
guard let bytes: Bytes = try await transaction.get(key: key) else {
return nil
}
try await transaction.commit()
return String(bytes: bytes, encoding: .ascii)
}
因此,您的代码块将被温和地重试,直到事务成功提交(或者直到数据库确定它已经被重试足够多次并且是时候放手了)。
作为一种特殊的原子操作,值可以写入保证唯一的特殊键。这些键在其元组中使用了不完整的版本戳,这些版本戳将在写入数据时由底层集群完成。然后可以检索事务中使用的 Versionstamp,以便在其他地方引用它。
可以使用 FDB.Versionstamp()
初始化器创建不完整的版本戳并将其添加到元组中。userData
字段是可选的,用于进一步排序同一事务中写入的多个键。
在事务块中,可以使用 set(versionstampedKey:value:)
方法写入具有不完整版本戳的键。此方法将搜索键中不完整的版本戳,如果找到,则会将其标记为在写入集群后替换为完整的版本戳。如果未找到不完整的版本戳,则将抛出 FDB.Error.missingIncompleteVersionstamp
错误。
如果您需要键中使用的完整版本戳,您可以在提交事务之前调用 getVersionstamp()
。请注意,此方法必须在写入版本戳键的同一事务中调用,否则它将不知道要返回哪个版本戳。另请注意,此版本戳不包含与其关联的任何用户数据,因为它将是相同的版本戳,无论写入了多少个版本戳键。
let keyWithVersionstampPlaceholder = self.subspace[FDB.Versionstamp(userData: 42)]["anotherKey"]
let valueToWrite: String = "Hello, World!"
var versionstamp: FDB.Versionstamp = try await fdb.withTransaction { transaction in
try transaction.set(versionstampedKey: keyWithVersionstampPlaceholder, value: Bytes(valueToWrite.utf8))
try await transaction.commit()
return try await transaction.getVersionstamp()
}
versionstamp.userData = 42
let actualKey = self.subspace[versionstamp]["anotherKey"]
// ... return it to user, save it as a reference to another entry, etc…
let key = FDB.Subspace("1337")["322"]
let resultString: String = try await fdb.withTransaction { transaction in
try transaction.setOption(.timeout(milliseconds: 5000))
try transaction.setOption(.snapshotRywEnable)
transaction.set(key: key, value: Bytes([1, 2, 3]))
guard let bytes = try await transaction.get(key: key, snapshot: true) else {
throw MyApplicationError.Something("Bytes are not bytes")
}
guard let string = String(bytes: bytes, encoding: .ascii) else {
throw MyApplicationError.Something("String is not string")
}
try await transaction.commit()
return string
}
print("My string is '\(resultString)'")
FDBSwift 支持官方社区 Swift-Log 库,并带有自定义后端 LGNLog,因此 FDBSwift 将使用 Logger.current
日志记录器(可以通过 Logger.$current.withValue(contextLogger) { ... }
绑定到 TaskLocal 日志记录器),并遵守日志级别和其他 LGNLogger 配置条目,请参阅相关文档。
您没有正确安装 FoundationDB 的 pkg-config
,请参阅 安装部分。
The bundle “FDBTests” couldn’t be loaded because it is damaged or missing necessary resources. Try reinstalling the bundle
。怎么办?在控制台中执行此神奇命令:install_name_tool -id /usr/local/lib/libfdb_c.dylib /usr/local/lib/libfdb_c.dylib
。
感谢 @dimitribouniol 和他的 精彩调查。
(反思并分析事物总是好的)您尝试创建多个 FDB 类的实例,这是 a) 禁止的 b) 完全不需要的,因为一个实例对于任何应用程序都足够了(如果不够,请考虑横向扩展,FDB 绝对不应该是您应用程序的瓶颈)。严格来说,这不是很好,应该有一种在运行时创建多个 FDB 连接的方法,我肯定会尝试使其成为可能。尽管如此,我认为 FDB 连接池不是一个好主意,它已经为您完成了一切。
虽然我的目标是实现 Tuple 层的完全跨语言兼容性,但我不能保证这一点。在开发过程中,我参考了 Python 实现,但可能存在细微差异(例如 Unicode 字符串和字节字符串打包,请参阅关于字符串的 设计文档 和关于此的 我的评论)。总的来说,它应该已经相当兼容了。也许有一天我会花一些时间来确保打包兼容性,但对我来说这不是高优先级。
async/await
,耶,棒呆了