10.1 分析持久化对象用例
第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”中介绍的持久化机制主要针对基于压缩文件读写一个已序列化的对象。如果想要更新文件的任何一个部分,必须替换整个文件。这是使用数据的简洁表示法的结果,即很难在文件中定位一个对象,同时如果对象的大小改变了,替换对象也很困难。比起用更聪明、更复杂的算法来解决这些难点,我们希望可以简单地序列化和存储对象。当我们的域中包含大量的持久化操作和可变对象时,会对使用带来一些额外的难点。下面是一些需要额外考虑的部分。
- 不希望一次把所有的对象都加载到内存中。对于许多大数据应用,一次把所有的对象都加载到内存中可能根本做不到。
- 只想更新我们域中对象的一个小子集或者单独实例。为了更新一个对象而加载并且更新所有的对象是一个相对低效的处理方法。
- 不会一次更新所有的对象,可能会逐渐累加对象。有些格式,例如YAML和CSV,它们允许我们简单地将它们追加到一个文件中。而一些其他有终止符的格式,例如JSON和XML,却让我们很难把它们追加到文件中。
还有一些我们可能也想要的功能。将序列化、持久化、并行更新和写入操作整合成单一的数据库概念是很常见的做法。shelve模块自身不是一个完整的数据库解决方案。shelve内部使用的dbm模块没有直接处理并行写操作,也没有处理多操作事务。可以利用操作系统底层的文件锁完成更新操作,但是这种方法太过于依赖操作系统。对于并行写操作,最好能使用一个适当的数据库或者一个RESTful的数据服务器。可以参考第11章“用SQLite保存和获取对象”和第12章“传输和共享对象”。
ACID属性
我们的设计必须考虑到ACID属性(ACID properties)是如何应用于shelve数据库的。应用程序往往会用一系列相关的操作改变状态,这些操作会将数据库从一个常态转变为下一个常态。改变数据库状态的操作集合可以被称为事务(transaction)。
多操作事务的一个例子是更新两个对象从而让总和保持不变。比如,从一个账户中取钱,然后存入另外一个账户。全局的余额必须保持不变,这样数据库才会处于一个一致并且正确的状态。ACID属性描述了我们期望的数据库事务的行为。我们用4个规则定义我们的预期。
- 原子性(Atomicity):事务必须是原子的。如果事务中包括多个操作,那么所有的操作都必须全部完成或者全部取消。永远都不应该存在部分完成的事务。
- 一致性(Consistency):事务必须保证一致。它会将数据库从某个状态改变为另外一个状态。事务不应该破坏数据库或者导致同时在线的不同用户看到不一致的视图。对于已完成的事务,所有的用户都应该看到相同的结果。
- 隔离性(Isolation):每个事务都应该正常运行,就好像他们是完全隔离的。不存在两个用户可以互相干扰对方更新的并发用户。我们必须能够将并发的访问转变为顺序访问(有可能更慢),这样对数据库的更新就可以得到一致的结果。
- 持久性(Durability):对数据库的改变必须是持久的,他们应该被正确地存储在文件系统中。
当操作内存中的Python对象时,很明显,我们只有ACI而没有D。根据定义,内存中的对象不是持久的。如果试图从多个并发的进程中使用shelve模块,但是没有使用锁(locking)和版本化(versioning)的话,就可能只获得D却丢了ACI属性。
shelve模块没有直接支持原子性,它没有提供处理包含多个操作的事务的方法。如果有包含多个操作的事务并且需要原子性,那么必须保证它们这些操作全部正常工作或者全部失败,这可能会用到更复杂的try:语句。当操作失败时,我们必须恢复数据库的前一个状态。
shelve模块不保证所有类型的改变都可以持久化。如果将一个可更改对象存在shelf上,然后在内存中改变了对象的状态,持久化在shelf上的版本不会自动改变。如果希望改变已经存在shelf上的对象,应用程序必须显式地更新shelf。我们可以通过使用回写模式(writeback mode)让shelf对象追踪所有的变更,但是使用这个功能会影响性能。
