Perl 的 CGI/多进程情况下,如果多个进程(或 CGI 实例)同时写入同一个文件,直接 save 会导致数据丢失或损坏(比如 A、B 进程各自读、写,后写的覆盖前写的所有更改)。
最常用可靠方案是文件锁(flock)+ 读-合并-写,确保每次 save 的是最新的、所有其它进程已持久化的数据的叠加。
use Fcntl qw(:flock);
sub save {
my ($self, $filename) = @_;
die "No filename provided" unless $filename;
# 打开文件并加写锁
open my $fh, "+<", $filename or open $fh, ">", $filename or die "Can't open $filename: $!";
flock($fh, LOCK_EX) or die "Cannot lock $filename: $!";
# 读取磁盘上的最新数据(如果有)
my $disk_data;
eval {
seek($fh, 0, 0); # 回到文件头
local $/; # slurp mode
$disk_data = -s $filename ? Storable::fd_retrieve($fh) : {};
};
$disk_data ||= {};
# 合并内存和磁盘数据
foreach my $key (keys %{ $self->{store} }) {
if (!exists $disk_data->{$key}) {
$disk_data->{$key} = $self->{store}->{$key};
} else {
# 合并所有版本
my $disk_versions = $disk_data->{$key}->{versions} || {};
my $memory_versions = $self->{store}->{$key}->{versions} || {};
foreach my $ver (keys %$memory_versions) {
$disk_versions->{$ver} = $memory_versions->{$ver};
}
$disk_data->{$key}->{versions} = $disk_versions;
# 更新 latest_version
$disk_data->{$key}->{latest_version} = $self->{store}->{$key}->{latest_version}
if $self->{store}->{$key}->{latest_version} > $disk_data->{$key}->{latest_version};
}
}
# 截断并写回
seek($fh, 0, 0);
truncate($fh, 0) or die "Truncate failed: $!";
Storable::store_fd($disk_data, $fh) or die "Unable to save database to $filename: $!";
close $fh;
# 更新内存最新数据
$self->{store} = $disk_data;
return 1;
}
请将以下完整代码替换你的 KVDatabase.pm:
package KVDatabase;
use strict;
use warnings;
use Storable qw(store retrieve store_fd fd_retrieve);
use Fcntl qw(:flock);
sub new {
my $class = shift;
my $self = {
store => {},
};
bless $self, $class;
return $self;
}
sub exist {
my ($self, $key) = @_;
return exists $self->{store}->{$key};
}
sub set {
my ($self, $key, $value) = @_;
if (exists $self->{store}->{$key}) {
my $ver = ++$self->{store}->{$key}->{latest_version};
$self->{store}->{$key}->{versions}->{$ver} = $value;
} else {
$self->{store}->{$key} = {
versions => { 1 => $value },
latest_version => 1,
};
}
return 1;
}
sub get {
my ($self, $key, $version) = @_;
return undef unless exists $self->{store}->{$key};
my $versions = $self->{store}->{$key}->{versions};
if (defined $version) {
return exists $versions->{$version} ? $versions->{$version} : undef;
} else {
my $latest = $self->{store}->{$key}->{latest_version};
return exists $versions->{$latest} ? $versions->{$latest} : undef;
}
}
sub delete {
my ($self, $key) = @_;
return 0 unless exists $self->{store}->{$key};
delete $self->{store}->{$key};
return 1;
}
sub version {
my ($self, $key) = @_;
return undef unless exists $self->{store}->{$key};
return $self->{store}->{$key}->{latest_version};
}
# 多进程安全持久化
sub save {
my ($self, $filename) = @_;
die "No filename provided" unless $filename;
open my $fh, "+<", $filename or open $fh, ">", $filename or die "Can't open $filename: $!";
flock($fh, LOCK_EX) or die "Cannot lock $filename: $!";
# 读取磁盘上的最新数据(如果有)
my $disk_data;
eval {
seek($fh, 0, 0);
local $/;
$disk_data = -s $filename ? fd_retrieve($fh) : {};
};
$disk_data ||= {};
# 合并内存和磁盘数据
foreach my $key (keys %{ $self->{store} }) {
if (!exists $disk_data->{$key}) {
$disk_data->{$key} = $self->{store}->{$key};
} else {
my $disk_versions = $disk_data->{$key}->{versions} || {};
my $memory_versions = $self->{store}->{$key}->{versions} || {};
foreach my $ver (keys %$memory_versions) {
$disk_versions->{$ver} = $memory_versions->{$ver};
}
$disk_data->{$key}->{versions} = $disk_versions;
if ($self->{store}->{$key}->{latest_version} > $disk_data->{$key}->{latest_version}) {
$disk_data->{$key}->{latest_version} = $self->{store}->{$key}->{latest_version};
}
}
}
seek($fh, 0, 0);
truncate($fh, 0) or die "Truncate failed: $!";
store_fd($disk_data, $fh) or die "Unable to save database to $filename: $!";
close $fh;
$self->{store} = $disk_data;
return 1;
}
sub load {
my ($class, $filename) = @_;
die "No filename provided" unless $filename;
my $restored = retrieve($filename) or die "Unable to load database from $filename: $!";
my $self = {
store => $restored,
};
bless $self, $class;
return $self;
}
1;