混合持久化策略
学习目标
- 深入理解 RDB+AOF 混合持久化原理
- 掌握启动时的数据加载策略
- 实现完整的混合持久化管理器
- 理解数据一致性保证机制
- 掌握灾难恢复方案和性能调优
混合持久化概述
1. 混合持久化原理
Redis 4.0 引入的混合持久化结合了 RDB 和 AOF 的优势,既保证了数据完整性,又提供了快速的恢复能力:
┌─────────────────────────────────────────────────────────────┐
│ 混合持久化架构 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ RDB 快照 │ │ AOF 日志 │ │ 混合文件 │ │
│ │ │ │ │ │ │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ 数据 │ │ │ │ 命令 │ │ │ │ RDB │ │ │
│ │ │ 快照 │ │ │ │ 日志 │ │ │ │ 头部 │ │ │
│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │
│ └─────────────┘ └─────────────┘ │ ┌───────┐ │ │
│ │ │ │ │ AOF │ │ │
│ └───────────┬───────┘ │ │ 尾部 │ │ │
│ │ │ └───────┘ │ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 混合持久化流程 │ │
│ │ 1. 定期创建 RDB 快照 │ │
│ │ 2. 记录 AOF 命令日志 │ │
│ │ 3. 重写时合并为混合文件 │ │
│ │ 4. 启动时优先加载混合文件 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2. 混合文件格式
混合文件包含 RDB 头部和 AOF 尾部:
[RDB 数据] + [AOF 命令1] + [AOF 命令2] + ... + [AOF 命令N]
格式说明:
- 文件开头是完整的 RDB 快照
- 快照后面是 AOF 格式的命令日志
- 恢复时先加载 RDB,再重放 AOF 命令
️ Go 语言混合持久化实现
1. 混合持久化管理器
// mixed/mixed_persistence.go
package mixed
import (
"fmt"
"io"
"os"
"path/filepath"
"sync"
"time"
)
// 持久化策略
type PersistenceStrategy int
const (
RDB_ONLY PersistenceStrategy = iota
AOF_ONLY
MIXED
)
// 混合持久化管理器
type MixedPersistenceManager struct {
rdbManager *RDBManager
aofManager *AOFManager
strategy PersistenceStrategy
rdbFilename string
aofFilename string
mixedFilename string
mu sync.RWMutex
isEnabled bool
lastRDBTime time.Time
rdbInterval time.Duration
}
// 创建混合持久化管理器
func NewMixedPersistenceManager(rdbFile, aofFile, mixedFile string, strategy PersistenceStrategy) (*MixedPersistenceManager, error) {
// 创建 RDB 管理器
rdbManager, err := NewRDBManager(rdbFile)
if err != nil {
return nil, fmt.Errorf("failed to create RDB manager: %v", err)
}
// 创建 AOF 管理器
aofManager, err := NewAOFManager(aofFile, AOF_EVERYSEC, 1024*1024)
if err != nil {
return nil, fmt.Errorf("failed to create AOF manager: %v", err)
}
return &MixedPersistenceManager{
rdbManager: rdbManager,
aofManager: aofManager,
strategy: strategy,
rdbFilename: rdbFile,
aofFilename: aofFile,
mixedFilename: mixedFile,
isEnabled: true,
lastRDBTime: time.Now(),
rdbInterval: time.Hour, // 默认 1 小时
}, nil
}
// 写入命令
func (mpm *MixedPersistenceManager) WriteCommand(args []string) error {
mpm.mu.RLock()
defer mpm.mu.RUnlock()
if !mpm.isEnabled {
return nil
}
// 根据策略写入
switch mpm.strategy {
case RDB_ONLY:
// 只写入 RDB(定期快照)
return nil
case AOF_ONLY:
// 只写入 AOF
return mpm.aofManager.WriteCommand(args)
case MIXED:
// 写入 AOF,定期创建混合文件
if err := mpm.aofManager.WriteCommand(args); err != nil {
return err
}
// 检查是否需要创建混合文件
if mpm.shouldCreateMixed() {
go mpm.createMixedFile()
}
return nil
}
return nil
}
// 检查是否需要创建混合文件
func (mpm *MixedPersistenceManager) shouldCreateMixed() bool {
// 检查时间间隔
if time.Since(mpm.lastRDBTime) < mpm.rdbInterval {
return false
}
// 检查 AOF 文件大小
aofSize, err := mpm.aofManager.GetStatus()
if err != nil {
return false
}
// 如果 AOF 文件超过 100MB,创建混合文件
if aofSize["size"].(int64) > 100*1024*1024 {
return true
}
return false
}
// 创建混合文件
func (mpm *MixedPersistenceManager) createMixedFile() error {
mpm.mu.Lock()
defer mpm.mu.Unlock()
// 创建临时混合文件
tempFile := mpm.mixedFilename + ".tmp"
// 创建 RDB 快照
if err := mpm.createRDBSnapshot(tempFile); err != nil {
return fmt.Errorf("failed to create RDB snapshot: %v", err)
}
// 追加 AOF 命令
if err := mpm.appendAOFCommands(tempFile); err != nil {
return fmt.Errorf("failed to append AOF commands: %v", err)
}
// 原子性替换文件
if err := mpm.atomicReplace(tempFile); err != nil {
return fmt.Errorf("failed to replace mixed file: %v", err)
}
// 更新 RDB 时间
mpm.lastRDBTime = time.Now()
return nil
}
// 创建 RDB 快照
func (mpm *MixedPersistenceManager) createRDBSnapshot(filename string) error {
// 这里应该调用实际的 RDB 创建逻辑
// 简化实现,实际应该遍历所有数据
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
// 写入 RDB 头部标识
rdbHeader := []byte("REDIS0001")
if _, err := file.Write(rdbHeader); err != nil {
return err
}
// 这里应该写入实际的 RDB 数据
// 简化实现,写入一些示例数据
sampleData := []byte("sample_rdb_data")
if _, err := file.Write(sampleData); err != nil {
return err
}
return nil
}
// 追加 AOF 命令
func (mpm *MixedPersistenceManager) appendAOFCommands(filename string) error {
// 打开文件追加模式
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer file.Close()
// 读取 AOF 文件
aofFile, err := os.Open(mpm.aofFilename)
if err != nil {
return err
}
defer aofFile.Close()
// 复制 AOF 内容
if _, err := io.Copy(file, aofFile); err != nil {
return err
}
return nil
}
// 原子性替换文件
func (mpm *MixedPersistenceManager) atomicReplace(tempFile string) error {
// 备份原文件
backupFile := mpm.mixedFilename + ".bak"
if err := os.Rename(mpm.mixedFilename, backupFile); err != nil {
// 如果原文件不存在,忽略错误
if !os.IsNotExist(err) {
return err
}
}
// 重命名临时文件
if err := os.Rename(tempFile, mpm.mixedFilename); err != nil {
// 恢复原文件
if err := os.Rename(backupFile, mpm.mixedFilename); err != nil {
// 忽略恢复错误
}
return err
}
// 删除备份文件
os.Remove(backupFile)
return nil
}
2. 数据加载策略
// mixed/data_loading.go
package mixed
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
// 数据加载器
type DataLoader struct {
rdbManager *RDBManager
aofManager *AOFManager
mixedFilename string
rdbFilename string
aofFilename string
}
// 创建数据加载器
func NewDataLoader(rdbFile, aofFile, mixedFile string) *DataLoader {
return &DataLoader{
rdbManager: NewRDBManager(rdbFile),
aofManager: NewAOFManager(aofFile, AOF_EVERYSEC, 1024*1024),
mixedFilename: mixedFile,
rdbFilename: rdbFile,
aofFilename: aofFile,
}
}
// 加载数据
func (dl *DataLoader) LoadData() error {
// 检查文件存在性
files := dl.checkFiles()
// 根据文件存在性决定加载策略
switch files {
case "mixed":
return dl.loadMixedFile()
case "rdb":
return dl.loadRDBFile()
case "aof":
return dl.loadAOFFile()
case "none":
return fmt.Errorf("no persistence files found")
default:
return fmt.Errorf("unexpected file combination: %s", files)
}
}
// 检查文件存在性
func (dl *DataLoader) checkFiles() string {
mixedExists := dl.fileExists(dl.mixedFilename)
rdbExists := dl.fileExists(dl.rdbFilename)
aofExists := dl.fileExists(dl.aofFilename)
if mixedExists {
return "mixed"
} else if rdbExists && aofExists {
return "rdb" // 优先加载 RDB
} else if rdbExists {
return "rdb"
} else if aofExists {
return "aof"
} else {
return "none"
}
}
// 检查文件是否存在
func (dl *DataLoader) fileExists(filename string) bool {
_, err := os.Stat(filename)
return !os.IsNotExist(err)
}
// 加载混合文件
func (dl *DataLoader) loadMixedFile() error {
fmt.Println("Loading mixed persistence file...")
// 打开混合文件
file, err := os.Open(dl.mixedFilename)
if err != nil {
return err
}
defer file.Close()
// 读取 RDB 部分
if err := dl.loadRDBPart(file); err != nil {
return fmt.Errorf("failed to load RDB part: %v", err)
}
// 读取 AOF 部分
if err := dl.loadAOFPart(file); err != nil {
return fmt.Errorf("failed to load AOF part: %v", err)
}
fmt.Println("Mixed persistence file loaded successfully")
return nil
}
// 加载 RDB 部分
func (dl *DataLoader) loadRDBPart(file *os.File) error {
// 查找 RDB 结束位置
rdbEndPos, err := dl.findRDBEndPosition(file)
if err != nil {
return err
}
// 重置文件位置
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
// 读取 RDB 数据
rdbData := make([]byte, rdbEndPos)
if _, err := io.ReadFull(file, rdbData); err != nil {
return err
}
// 解析 RDB 数据
if err := dl.parseRDBData(rdbData); err != nil {
return err
}
return nil
}
// 查找 RDB 结束位置
func (dl *DataLoader) findRDBEndPosition(file *os.File) (int64, error) {
// 简化实现,实际应该解析 RDB 格式
// 这里假设 RDB 部分以特定标识结束
buffer := make([]byte, 1024)
var totalRead int64
for {
n, err := file.Read(buffer)
if err != nil {
if err == io.EOF {
break
}
return 0, err
}
// 查找 RDB 结束标识
for i := 0; i < n-1; i++ {
if buffer[i] == 0xFF && buffer[i+1] == 0xFF {
return totalRead + int64(i) + 2, nil
}
}
totalRead += int64(n)
}
return totalRead, nil
}
// 解析 RDB 数据
func (dl *DataLoader) parseRDBData(data []byte) error {
// 简化实现,实际应该解析 RDB 格式
fmt.Printf("Parsing RDB data: %d bytes\n", len(data))
// 这里应该调用实际的 RDB 解析逻辑
// 例如:dl.rdbManager.Parse(data)
return nil
}
// 加载 AOF 部分
func (dl *DataLoader) loadAOFPart(file *os.File) error {
// 从当前位置读取 AOF 数据
aofData, err := io.ReadAll(file)
if err != nil {
return err
}
// 解析 AOF 数据
if err := dl.parseAOFData(aofData); err != nil {
return err
}
return nil
}
// 解析 AOF 数据
func (dl *DataLoader) parseAOFData(data []byte) error {
// 简化实现,实际应该解析 AOF 格式
fmt.Printf("Parsing AOF data: %d bytes\n", len(data))
// 这里应该调用实际的 AOF 解析逻辑
// 例如:dl.aofManager.Parse(data)
return nil
}
// 加载 RDB 文件
func (dl *DataLoader) loadRDBFile() error {
fmt.Println("Loading RDB file...")
// 调用 RDB 管理器加载
if err := dl.rdbManager.Load(); err != nil {
return fmt.Errorf("failed to load RDB file: %v", err)
}
fmt.Println("RDB file loaded successfully")
return nil
}
// 加载 AOF 文件
func (dl *DataLoader) loadAOFFile() error {
fmt.Println("Loading AOF file...")
// 调用 AOF 管理器加载
if err := dl.aofManager.Load(); err != nil {
return fmt.Errorf("failed to load AOF file: %v", err)
}
fmt.Println("AOF file loaded successfully")
return nil
}
3. 数据一致性保证
// mixed/consistency.go
package mixed
import (
"crypto/md5"
"fmt"
"io"
"os"
"sync"
"time"
)
// 一致性检查器
type ConsistencyChecker struct {
rdbManager *RDBManager
aofManager *AOFManager
mixedFilename string
mu sync.RWMutex
lastCheck time.Time
checkInterval time.Duration
}
// 创建一致性检查器
func NewConsistencyChecker(rdbManager *RDBManager, aofManager *AOFManager, mixedFile string) *ConsistencyChecker {
return &ConsistencyChecker{
rdbManager: rdbManager,
aofManager: aofManager,
mixedFilename: mixedFile,
lastCheck: time.Now(),
checkInterval: time.Hour, // 默认 1 小时检查一次
}
}
// 检查数据一致性
func (cc *ConsistencyChecker) CheckConsistency() error {
cc.mu.Lock()
defer cc.mu.Unlock()
// 检查时间间隔
if time.Since(cc.lastCheck) < cc.checkInterval {
return nil
}
// 检查 RDB 和 AOF 的一致性
if err := cc.checkRDBAndAOFConsistency(); err != nil {
return fmt.Errorf("RDB and AOF consistency check failed: %v", err)
}
// 检查混合文件的一致性
if err := cc.checkMixedFileConsistency(); err != nil {
return fmt.Errorf("mixed file consistency check failed: %v", err)
}
// 更新检查时间
cc.lastCheck = time.Now()
return nil
}
// 检查 RDB 和 AOF 的一致性
func (cc *ConsistencyChecker) checkRDBAndAOFConsistency() error {
// 获取 RDB 数据哈希
rdbHash, err := cc.getRDBHash()
if err != nil {
return err
}
// 获取 AOF 数据哈希
aofHash, err := cc.getAOFHash()
if err != nil {
return err
}
// 比较哈希值
if rdbHash != aofHash {
return fmt.Errorf("RDB and AOF data mismatch")
}
return nil
}
// 检查混合文件的一致性
func (cc *ConsistencyChecker) checkMixedFileConsistency() error {
// 检查混合文件是否存在
if _, err := os.Stat(cc.mixedFilename); os.IsNotExist(err) {
return nil // 文件不存在,跳过检查
}
// 获取混合文件哈希
mixedHash, err := cc.getMixedFileHash()
if err != nil {
return err
}
// 获取 RDB 和 AOF 的期望哈希
expectedHash, err := cc.getExpectedMixedHash()
if err != nil {
return err
}
// 比较哈希值
if mixedHash != expectedHash {
return fmt.Errorf("mixed file data mismatch")
}
return nil
}
// 获取 RDB 数据哈希
func (cc *ConsistencyChecker) getRDBHash() (string, error) {
// 简化实现,实际应该计算 RDB 数据的哈希
return "rdb_hash", nil
}
// 获取 AOF 数据哈希
func (cc *ConsistencyChecker) getAOFHash() (string, error) {
// 简化实现,实际应该计算 AOF 数据的哈希
return "aof_hash", nil
}
// 获取混合文件哈希
func (cc *ConsistencyChecker) getMixedFileHash() (string, error) {
file, err := os.Open(cc.mixedFilename)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}
// 获取期望的混合文件哈希
func (cc *ConsistencyChecker) getExpectedMixedHash() (string, error) {
// 简化实现,实际应该计算期望的哈希
return "expected_hash", nil
}
// 修复数据不一致
func (cc *ConsistencyChecker) FixInconsistency() error {
cc.mu.Lock()
defer cc.mu.Unlock()
// 检查 RDB 和 AOF 的一致性
if err := cc.checkRDBAndAOFConsistency(); err != nil {
// 如果 RDB 和 AOF 不一致,优先使用 AOF
fmt.Println("RDB and AOF inconsistent, using AOF data")
return cc.rebuildFromAOF()
}
// 检查混合文件的一致性
if err := cc.checkMixedFileConsistency(); err != nil {
// 如果混合文件不一致,重新创建
fmt.Println("Mixed file inconsistent, recreating...")
return cc.recreateMixedFile()
}
return nil
}
// 从 AOF 重建数据
func (cc *ConsistencyChecker) rebuildFromAOF() error {
// 这里应该调用 AOF 管理器重建数据
fmt.Println("Rebuilding data from AOF...")
return nil
}
// 重新创建混合文件
func (cc *ConsistencyChecker) recreateMixedFile() error {
// 这里应该调用混合持久化管理器重新创建文件
fmt.Println("Recreating mixed file...")
return nil
}
4. 灾难恢复方案
// mixed/disaster_recovery.go
package mixed
import (
"fmt"
"os"
"path/filepath"
"time"
)
// 灾难恢复管理器
type DisasterRecoveryManager struct {
rdbManager *RDBManager
aofManager *AOFManager
mixedManager *MixedPersistenceManager
backupDir string
retentionDays int
}
// 创建灾难恢复管理器
func NewDisasterRecoveryManager(rdbManager *RDBManager, aofManager *AOFManager, mixedManager *MixedPersistenceManager, backupDir string, retentionDays int) *DisasterRecoveryManager {
return &DisasterRecoveryManager{
rdbManager: rdbManager,
aofManager: aofManager,
mixedManager: mixedManager,
backupDir: backupDir,
retentionDays: retentionDays,
}
}
// 创建备份
func (drm *DisasterRecoveryManager) CreateBackup() error {
// 创建备份目录
if err := os.MkdirAll(drm.backupDir, 0755); err != nil {
return fmt.Errorf("failed to create backup directory: %v", err)
}
// 生成备份文件名
timestamp := time.Now().Format("20060102_150405")
rdbBackup := filepath.Join(drm.backupDir, fmt.Sprintf("dump_%s.rdb", timestamp))
aofBackup := filepath.Join(drm.backupDir, fmt.Sprintf("appendonly_%s.aof", timestamp))
mixedBackup := filepath.Join(drm.backupDir, fmt.Sprintf("mixed_%s.rdb", timestamp))
// 备份 RDB 文件
if err := drm.backupFile(drm.rdbManager.GetFilename(), rdbBackup); err != nil {
return fmt.Errorf("failed to backup RDB file: %v", err)
}
// 备份 AOF 文件
if err := drm.backupFile(drm.aofManager.GetFilename(), aofBackup); err != nil {
return fmt.Errorf("failed to backup AOF file: %v", err)
}
// 备份混合文件
if err := drm.backupFile(drm.mixedManager.GetMixedFilename(), mixedBackup); err != nil {
return fmt.Errorf("failed to backup mixed file: %v", err)
}
// 清理过期备份
if err := drm.cleanupExpiredBackups(); err != nil {
return fmt.Errorf("failed to cleanup expired backups: %v", err)
}
return nil
}
// 备份文件
func (drm *DisasterRecoveryManager) backupFile(src, dst string) error {
// 检查源文件是否存在
if _, err := os.Stat(src); os.IsNotExist(err) {
return nil // 文件不存在,跳过备份
}
// 复制文件
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
return nil
}
// 清理过期备份
func (drm *DisasterRecoveryManager) cleanupExpiredBackups() error {
// 获取备份目录中的所有文件
files, err := os.ReadDir(drm.backupDir)
if err != nil {
return err
}
// 计算过期时间
cutoffTime := time.Now().AddDate(0, 0, -drm.retentionDays)
// 删除过期文件
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoffTime) {
filePath := filepath.Join(drm.backupDir, file.Name())
if err := os.Remove(filePath); err != nil {
fmt.Printf("Failed to remove expired backup file %s: %v\n", filePath, err)
}
}
}
return nil
}
// 恢复数据
func (drm *DisasterRecoveryManager) RestoreData(backupType string, timestamp string) error {
var filename string
switch backupType {
case "rdb":
filename = filepath.Join(drm.backupDir, fmt.Sprintf("dump_%s.rdb", timestamp))
case "aof":
filename = filepath.Join(drm.backupDir, fmt.Sprintf("appendonly_%s.aof", timestamp))
case "mixed":
filename = filepath.Join(drm.backupDir, fmt.Sprintf("mixed_%s.rdb", timestamp))
default:
return fmt.Errorf("unsupported backup type: %s", backupType)
}
// 检查备份文件是否存在
if _, err := os.Stat(filename); os.IsNotExist(err) {
return fmt.Errorf("backup file not found: %s", filename)
}
// 恢复文件
if err := drm.restoreFile(filename, backupType); err != nil {
return fmt.Errorf("failed to restore file: %v", err)
}
return nil
}
// 恢复文件
func (drm *DisasterRecoveryManager) restoreFile(src, backupType string) error {
var dst string
switch backupType {
case "rdb":
dst = drm.rdbManager.GetFilename()
case "aof":
dst = drm.aofManager.GetFilename()
case "mixed":
dst = drm.mixedManager.GetMixedFilename()
default:
return fmt.Errorf("unsupported backup type: %s", backupType)
}
// 复制文件
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
return nil
}
// 列出可用备份
func (drm *DisasterRecoveryManager) ListBackups() ([]BackupInfo, error) {
files, err := os.ReadDir(drm.backupDir)
if err != nil {
return nil, err
}
var backups []BackupInfo
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
// 解析文件名获取类型和时间戳
backupType, timestamp := drm.parseBackupFilename(file.Name())
if backupType != "" {
backups = append(backups, BackupInfo{
Type: backupType,
Timestamp: timestamp,
Size: info.Size(),
ModTime: info.ModTime(),
})
}
}
return backups, nil
}
// 解析备份文件名
func (drm *DisasterRecoveryManager) parseBackupFilename(filename string) (string, string) {
// 简化实现,实际应该使用正则表达式
if len(filename) < 4 {
return "", ""
}
if filename[:4] == "dump" {
return "rdb", filename[5:15] // 假设时间戳格式为 YYYYMMDD_HH
} else if filename[:10] == "appendonly" {
return "aof", filename[11:21]
} else if filename[:6] == "mixed" {
return "mixed", filename[7:17]
}
return "", ""
}
// 备份信息
type BackupInfo struct {
Type string
Timestamp string
Size int64
ModTime time.Time
}
测试验证
1. 单元测试
// mixed/mixed_persistence_test.go
package mixed
import (
"os"
"testing"
"time"
)
func TestMixedPersistenceManager(t *testing.T) {
rdbFile := "test.rdb"
aofFile := "test.aof"
mixedFile := "test_mixed.rdb"
defer os.Remove(rdbFile)
defer os.Remove(aofFile)
defer os.Remove(mixedFile)
manager, err := NewMixedPersistenceManager(rdbFile, aofFile, mixedFile, MIXED)
if err != nil {
t.Fatalf("Failed to create mixed persistence manager: %v", err)
}
defer manager.Close()
// 写入测试命令
commands := [][]string{
{"SET", "key1", "value1"},
{"SET", "key2", "value2"},
{"HSET", "hash1", "field1", "value1"},
}
for _, cmd := range commands {
if err := manager.WriteCommand(cmd); err != nil {
t.Fatalf("Failed to write command: %v", err)
}
}
// 等待一段时间
time.Sleep(2 * time.Second)
// 获取状态
status := manager.GetStatus()
if !status["enabled"].(bool) {
t.Error("Mixed persistence should be enabled")
}
}
func TestDataLoader(t *testing.T) {
rdbFile := "test.rdb"
aofFile := "test.aof"
mixedFile := "test_mixed.rdb"
defer os.Remove(rdbFile)
defer os.Remove(aofFile)
defer os.Remove(mixedFile)
loader := NewDataLoader(rdbFile, aofFile, mixedFile)
// 测试加载数据
if err := loader.LoadData(); err != nil {
// 如果没有文件,这是预期的
if err.Error() != "no persistence files found" {
t.Fatalf("Unexpected error: %v", err)
}
}
}
func TestConsistencyChecker(t *testing.T) {
rdbManager := NewRDBManager("test.rdb")
aofManager := NewAOFManager("test.aof", AOF_EVERYSEC, 1024*1024)
checker := NewConsistencyChecker(rdbManager, aofManager, "test_mixed.rdb")
// 测试一致性检查
if err := checker.CheckConsistency(); err != nil {
// 如果没有文件,这是预期的
if err.Error() != "RDB and AOF data mismatch" {
t.Fatalf("Unexpected error: %v", err)
}
}
}
func TestDisasterRecoveryManager(t *testing.T) {
backupDir := "test_backup"
defer os.RemoveAll(backupDir)
rdbManager := NewRDBManager("test.rdb")
aofManager := NewAOFManager("test.aof", AOF_EVERYSEC, 1024*1024)
mixedManager := NewMixedPersistenceManager("test.rdb", "test.aof", "test_mixed.rdb", MIXED)
drm := NewDisasterRecoveryManager(rdbManager, aofManager, mixedManager, backupDir, 7)
// 测试创建备份
if err := drm.CreateBackup(); err != nil {
t.Fatalf("Failed to create backup: %v", err)
}
// 测试列出备份
backups, err := drm.ListBackups()
if err != nil {
t.Fatalf("Failed to list backups: %v", err)
}
if len(backups) == 0 {
t.Error("Expected at least one backup")
}
}
2. 性能基准测试
// mixed/benchmark_test.go
package mixed
import (
"os"
"testing"
"time"
)
func BenchmarkMixedPersistenceWrite(b *testing.B) {
rdbFile := "benchmark.rdb"
aofFile := "benchmark.aof"
mixedFile := "benchmark_mixed.rdb"
defer os.Remove(rdbFile)
defer os.Remove(aofFile)
defer os.Remove(mixedFile)
manager, err := NewMixedPersistenceManager(rdbFile, aofFile, mixedFile, MIXED)
if err != nil {
b.Fatalf("Failed to create mixed persistence manager: %v", err)
}
defer manager.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
command := []string{"SET", "key", "value"}
manager.WriteCommand(command)
}
}
func BenchmarkMixedPersistenceLoad(b *testing.B) {
rdbFile := "benchmark.rdb"
aofFile := "benchmark.aof"
mixedFile := "benchmark_mixed.rdb"
defer os.Remove(rdbFile)
defer os.Remove(aofFile)
defer os.Remove(mixedFile)
// 预填充文件
manager, err := NewMixedPersistenceManager(rdbFile, aofFile, mixedFile, MIXED)
if err != nil {
b.Fatalf("Failed to create mixed persistence manager: %v", err)
}
for i := 0; i < 1000; i++ {
command := []string{"SET", "key", "value"}
manager.WriteCommand(command)
}
manager.Close()
// 加载测试
loader := NewDataLoader(rdbFile, aofFile, mixedFile)
b.ResetTimer()
for i := 0; i < b.N; i++ {
loader.LoadData()
}
}
func BenchmarkConsistencyCheck(b *testing.B) {
rdbManager := NewRDBManager("benchmark.rdb")
aofManager := NewAOFManager("benchmark.aof", AOF_EVERYSEC, 1024*1024)
checker := NewConsistencyChecker(rdbManager, aofManager, "benchmark_mixed.rdb")
b.ResetTimer()
for i := 0; i < b.N; i++ {
checker.CheckConsistency()
}
}
func BenchmarkDisasterRecovery(b *testing.B) {
backupDir := "benchmark_backup"
defer os.RemoveAll(backupDir)
rdbManager := NewRDBManager("benchmark.rdb")
aofManager := NewAOFManager("benchmark.aof", AOF_EVERYSEC, 1024*1024)
mixedManager := NewMixedPersistenceManager("benchmark.rdb", "benchmark.aof", "benchmark_mixed.rdb", MIXED)
drm := NewDisasterRecoveryManager(rdbManager, aofManager, mixedManager, backupDir, 7)
b.ResetTimer()
for i := 0; i < b.N; i++ {
drm.CreateBackup()
}
}
3. 集成测试
// mixed/integration_test.go
package mixed
import (
"os"
"testing"
"time"
)
func TestMixedPersistenceIntegration(t *testing.T) {
rdbFile := "integration.rdb"
aofFile := "integration.aof"
mixedFile := "integration_mixed.rdb"
defer os.Remove(rdbFile)
defer os.Remove(aofFile)
defer os.Remove(mixedFile)
// 创建混合持久化管理器
manager, err := NewMixedPersistenceManager(rdbFile, aofFile, mixedFile, MIXED)
if err != nil {
t.Fatalf("Failed to create mixed persistence manager: %v", err)
}
defer manager.Close()
// 写入测试数据
testCommands := [][]string{
{"SET", "key1", "value1"},
{"SET", "key2", "value2"},
{"HSET", "hash1", "field1", "value1"},
{"LPUSH", "list1", "item1", "item2"},
{"ZADD", "zset1", "1", "member1"},
}
for _, cmd := range testCommands {
if err := manager.WriteCommand(cmd); err != nil {
t.Fatalf("Failed to write command: %v", err)
}
}
// 等待同步
time.Sleep(2 * time.Second)
// 创建数据加载器
loader := NewDataLoader(rdbFile, aofFile, mixedFile)
// 测试数据加载
if err := loader.LoadData(); err != nil {
t.Fatalf("Failed to load data: %v", err)
}
// 创建一致性检查器
rdbManager := NewRDBManager(rdbFile)
aofManager := NewAOFManager(aofFile, AOF_EVERYSEC, 1024*1024)
checker := NewConsistencyChecker(rdbManager, aofManager, mixedFile)
// 测试一致性检查
if err := checker.CheckConsistency(); err != nil {
t.Fatalf("Consistency check failed: %v", err)
}
// 创建灾难恢复管理器
backupDir := "integration_backup"
defer os.RemoveAll(backupDir)
drm := NewDisasterRecoveryManager(rdbManager, aofManager, manager, backupDir, 7)
// 测试备份
if err := drm.CreateBackup(); err != nil {
t.Fatalf("Failed to create backup: %v", err)
}
// 测试恢复
backups, err := drm.ListBackups()
if err != nil {
t.Fatalf("Failed to list backups: %v", err)
}
if len(backups) == 0 {
t.Error("Expected at least one backup")
}
}
性能对比分析
1. 持久化策略对比
策略 | 数据完整性 | 恢复速度 | 文件大小 | 性能影响 | 适用场景 |
---|---|---|---|---|---|
RDB | 中等 | 快 | 小 | 低 | 定期备份 |
AOF | 高 | 慢 | 大 | 中等 | 实时持久化 |
混合 | 高 | 快 | 中等 | 中等 | 生产环境 |
2. 恢复策略对比
策略 | 恢复时间 | 数据丢失 | 实现复杂度 | 适用场景 |
---|---|---|---|---|
RDB 优先 | 快 | 中等 | 低 | 性能优先 |
AOF 优先 | 慢 | 少 | 低 | 数据优先 |
混合优先 | 快 | 少 | 高 | 平衡 |
3. 性能测试结果
// 基准测试结果示例
func BenchmarkComparison(b *testing.B) {
// 混合持久化性能
b.Run("MixedPersistence", func(b *testing.B) {
manager, _ := NewMixedPersistenceManager("test.rdb", "test.aof", "test_mixed.rdb", MIXED)
defer manager.Close()
for i := 0; i < b.N; i++ {
command := []string{"SET", "key", "value"}
manager.WriteCommand(command)
}
})
// RDB 持久化性能
b.Run("RDBPersistence", func(b *testing.B) {
manager, _ := NewMixedPersistenceManager("test.rdb", "test.aof", "test_mixed.rdb", RDB_ONLY)
defer manager.Close()
for i := 0; i < b.N; i++ {
command := []string{"SET", "key", "value"}
manager.WriteCommand(command)
}
})
// AOF 持久化性能
b.Run("AOFPersistence", func(b *testing.B) {
manager, _ := NewMixedPersistenceManager("test.rdb", "test.aof", "test_mixed.rdb", AOF_ONLY)
defer manager.Close()
for i := 0; i < b.N; i++ {
command := []string{"SET", "key", "value"}
manager.WriteCommand(command)
}
})
}
面试要点
1. 混合持久化的优势
答案要点:
- 数据完整性:结合 RDB 和 AOF 的优势
- 恢复速度:RDB 提供快速恢复,AOF 提供数据完整性
- 存储效率:混合文件比纯 AOF 更小
- 性能平衡:在性能和安全性之间取得平衡
2. 数据加载策略
答案要点:
- 优先级:混合文件 > RDB 文件 > AOF 文件
- 完整性:优先选择数据最完整的文件
- 性能:优先选择恢复速度最快的文件
- 一致性:确保加载的数据是一致的
3. 数据一致性保证
答案要点:
- 定期检查:定期检查文件一致性
- 哈希验证:使用哈希值验证数据完整性
- 自动修复:发现不一致时自动修复
- 监控告警:不一致时发送告警
4. 灾难恢复方案
答案要点:
- 定期备份:定期创建数据备份
- 多地点存储:备份存储在不同地点
- 快速恢复:提供快速恢复机制
- 数据验证:恢复后验证数据完整性
总结
通过本章学习,我们深入理解了:
- 混合持久化的基本原理和架构设计
- 数据加载策略的实现和优化
- 数据一致性保证机制和检查方法
- 灾难恢复方案的完整实现
- 性能调优技巧和最佳实践
混合持久化为 Redis 提供了最佳的数据持久化方案,既保证了数据完整性,又提供了快速的恢复能力。在下一章中,我们将学习缓存一致性策略,了解如何保证缓存和数据库的数据一致性。