Python中的原子状态存储?
我正在做一个项目,这个系统不太可靠,我觉得它随时可能出问题。我想确保的是,如果我在写状态的时候机器突然崩溃,之后再读取状态时,要么能读取到一个有效的状态,要么根本读取不到任何状态。我实现了一些我认为可行的东西,下面是我的想法——如果有人有不同的看法或者其他解决方案,我很想听听。
我的想法:
import hashlib, cPickle, os
def write_state(logname, state):
state_string = cPickle.dumps(state, cPickle.HIGHEST_PROTOCOL)
state_string += hashlib.sha224(state_string).hexdigest()
handle = open('%s.1' % logname, 'wb')
handle.write(state_string)
handle.close()
handle = open('%s.2' % logname, 'wb')
handle.write(state_string)
handle.close()
def get_state(logname):
def read_file(name):
try:
f = open(name,'rb')
data = f.read()
f.close()
return data
except IOError:
return ''
def parse(data):
if len(data) < 56:
return (None, '', False)
hash = data[-56:]
data = data[:-56]
valid = hashlib.sha224(data).hexdigest() == hash
try:
parsed = cPickle.loads(data)
except cPickle.UnpicklingError:
parsed = None
return (parsed, valid)
data1,valid1 = parse(read_file('%s.1'%logname))
data2,valid2 = parse(read_file('%s.2'%logname))
if valid1 and valid2:
return data1
elif valid1 and not valid2:
return data1
elif valid2 and not valid1:
return data2
elif not valid1 and not valid2:
raise Exception('Theoretically, this never happens...')
例如:
write_state('test_log', {'x': 5})
print get_state('test_log')
5 个回答
我对数据库工作的模糊记忆是这样的。它涉及三个文件:一个控制文件、一个目标数据库文件和一个待处理事务日志。
控制文件里有一个全局事务计数器和一个哈希值或其他校验和。这是一个小文件,大小为一个物理块。也就是说,它只需要一次操作系统级别的写入。
在你的目标文件中,也要有一个全局事务计数器,里面存放真实的数据,还有一个哈希值或其他校验和。
待处理事务日志可以不断增长,或者是一个有限大小的循环队列,或者可能会回滚,这些都没太大关系。
把所有待处理的事务记录到简单的日志中。每条记录都有一个序列号和变更的内容。
更新事务计数器,更新控制文件中的哈希值。只需一次写入,并确保数据已刷新。如果这一步失败了,那么就什么都没变。如果成功了,控制文件和目标文件就不匹配了,这表示一个事务开始了但没有完成。
在目标文件上进行预期的更新。先定位到开头,更新计数器和校验和。如果这一步失败,控制文件的计数器比目标文件多了一次,说明目标文件损坏了。当这一步成功时,最后记录的事务、控制文件和目标文件都在序列号上达成一致。
你可以通过重放日志来恢复数据,因为你知道最后一个有效的序列号。
我来给个不同的建议:那用sqlite怎么样?或者,也可以考虑bsddb,不过这个好像已经不再更新了,你可能需要用一个第三方的模块。
你那两个文件是不能一起用的。文件系统可能会把事情重新排序,这样在任何一个文件写入磁盘之前,两个文件都有可能被截断。
有一些文件系统操作是保证是原子的,比如把一个文件重命名为另一个文件,这样文件要么在一个地方,要么在另一个地方。不过,根据POSIX的规定,它并不保证在文件内容写入磁盘之前就完成了移动,这意味着它只给你提供了锁定的功能。
Linux的文件系统确保文件内容在原子移动之前会写入磁盘(但不是同步的),所以这能满足你的需求。ext4文件系统在一段时间内打破了这个假设,导致这些文件实际上更容易变成空的。这被认为是个很糟糕的做法,后来也进行了修复。
总之,正确的做法是:在同一个目录下创建一个临时文件(这样它就在同一个文件系统上);写入新数据;对临时文件进行fsync操作;然后把它重命名为之前的版本。这是操作系统能保证的最原子的方式。这样做也能保证数据的持久性,但会让磁盘转动,这就是为什么应用开发者通常不喜欢使用fsync,并且会屏蔽那些有问题的ext4版本。