|
|
@@ -6,13 +6,10 @@
|
|
|
- **安全第一**:清理前必须完成数据备份
|
|
|
- **选择性备份**:只备份将要删除的数据,不是全量备份
|
|
|
- **快速恢复**:支持快速的数据恢复操作
|
|
|
-- **空间优化**:采用压缩和增量备份减少存储空间
|
|
|
+- **数据库存储**:将备份数据直接存储到数据库中,确保可靠性
|
|
|
|
|
|
### 1.2 备份类型
|
|
|
-- **SQL备份**:生成INSERT语句的SQL文件
|
|
|
-- **JSON备份**:将数据导出为JSON格式
|
|
|
-- **CSV备份**:将数据导出为CSV格式(适合大数据量)
|
|
|
-- **压缩备份**:自动压缩备份文件节省空间
|
|
|
+- **数据库备份**:将INSERT语句直接存储到数据库表中(唯一支持的备份方式)
|
|
|
|
|
|
## 2. 备份架构设计
|
|
|
|
|
|
@@ -22,8 +19,6 @@
|
|
|
├── BackupLogic # 备份核心逻辑
|
|
|
├── BackupService # 备份服务接口
|
|
|
├── BackupExecutor # 备份执行器
|
|
|
-├── BackupStorage # 备份存储管理
|
|
|
-├── BackupCompressor # 备份压缩器
|
|
|
└── BackupRestorer # 备份恢复器
|
|
|
```
|
|
|
|
|
|
@@ -31,31 +26,48 @@
|
|
|
```
|
|
|
1. 分析清理任务 → 确定需要备份的数据
|
|
|
2. 创建备份计划 → 生成备份任务列表
|
|
|
-3. 执行数据备份 → 按表分别备份数据
|
|
|
-4. 压缩备份文件 → 减少存储空间占用
|
|
|
-5. 验证备份完整性 → 确保备份文件可用
|
|
|
-6. 记录备份信息 → 保存备份元数据
|
|
|
-7. 执行清理操作 → 在备份完成后执行清理
|
|
|
+3. 执行数据备份 → 将数据以INSERT语句形式存储到数据库
|
|
|
+4. 验证备份完整性 → 确保备份数据完整
|
|
|
+5. 记录备份信息 → 保存备份元数据
|
|
|
+6. 执行清理操作 → 在备份完成后执行清理
|
|
|
```
|
|
|
|
|
|
## 3. 数据库设计扩展
|
|
|
|
|
|
-### 3.1 备份记录表 (cleanup_backups)
|
|
|
+### 3.1 SQL备份记录表 (cleanup_sql_backups)
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE `kku_cleanup_sql_backups` (
|
|
|
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
|
+ `backup_id` bigint(20) unsigned NOT NULL COMMENT '关联备份记录ID',
|
|
|
+ `table_name` varchar(100) NOT NULL COMMENT '表名',
|
|
|
+ `sql_content` longtext NOT NULL COMMENT 'INSERT语句内容',
|
|
|
+ `records_count` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '记录数量',
|
|
|
+ `content_size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '内容大小(字节)',
|
|
|
+ `content_hash` varchar(64) DEFAULT NULL COMMENT '内容SHA256哈希',
|
|
|
+ `backup_conditions` json DEFAULT NULL COMMENT '备份条件',
|
|
|
+ `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
+ `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ KEY `idx_backup_id` (`backup_id`),
|
|
|
+ KEY `idx_table_name` (`table_name`),
|
|
|
+ KEY `idx_records_count` (`records_count`),
|
|
|
+ KEY `idx_created_at` (`created_at`),
|
|
|
+ FOREIGN KEY (`backup_id`) REFERENCES `kku_cleanup_backups` (`id`) ON DELETE CASCADE
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SQL备份记录表';
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 备份记录表 (cleanup_backups)
|
|
|
|
|
|
```sql
|
|
|
CREATE TABLE `kku_cleanup_backups` (
|
|
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
|
`task_id` bigint(20) unsigned NOT NULL COMMENT '清理任务ID',
|
|
|
`backup_name` varchar(100) NOT NULL COMMENT '备份名称',
|
|
|
- `backup_type` tinyint(3) unsigned NOT NULL COMMENT '备份类型:1SQL,2JSON,3CSV',
|
|
|
- `compression_type` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '压缩类型:1gzip,2zip,3none',
|
|
|
- `backup_path` varchar(500) NOT NULL COMMENT '备份文件路径',
|
|
|
- `backup_size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '备份文件大小(字节)',
|
|
|
- `original_size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '原始数据大小(字节)',
|
|
|
+ `backup_type` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '备份类型:1数据库备份',
|
|
|
`tables_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '备份表数量',
|
|
|
`records_count` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '备份记录数量',
|
|
|
`backup_status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '备份状态:1进行中,2已完成,3已失败',
|
|
|
- `backup_hash` varchar(64) DEFAULT NULL COMMENT '备份文件MD5哈希',
|
|
|
`started_at` timestamp NULL DEFAULT NULL COMMENT '备份开始时间',
|
|
|
`completed_at` timestamp NULL DEFAULT NULL COMMENT '备份完成时间',
|
|
|
`expires_at` timestamp NULL DEFAULT NULL COMMENT '备份过期时间',
|
|
|
@@ -70,421 +82,300 @@ CREATE TABLE `kku_cleanup_backups` (
|
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='清理备份记录表';
|
|
|
```
|
|
|
|
|
|
-### 3.2 备份文件表 (cleanup_backup_files)
|
|
|
-
|
|
|
-```sql
|
|
|
-CREATE TABLE `kku_cleanup_backup_files` (
|
|
|
- `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
|
- `backup_id` bigint(20) unsigned NOT NULL COMMENT '备份记录ID',
|
|
|
- `table_name` varchar(100) NOT NULL COMMENT '表名',
|
|
|
- `file_path` varchar(500) NOT NULL COMMENT '文件路径',
|
|
|
- `file_size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小(字节)',
|
|
|
- `records_count` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '记录数量',
|
|
|
- `file_hash` varchar(64) DEFAULT NULL COMMENT '文件MD5哈希',
|
|
|
- `backup_conditions` json DEFAULT NULL COMMENT '备份条件',
|
|
|
- `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
|
- PRIMARY KEY (`id`),
|
|
|
- KEY `idx_backup_id` (`backup_id`),
|
|
|
- KEY `idx_table_name` (`table_name`),
|
|
|
- FOREIGN KEY (`backup_id`) REFERENCES `kku_cleanup_backups` (`id`) ON DELETE CASCADE
|
|
|
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='清理备份文件表';
|
|
|
-```
|
|
|
|
|
|
-## 4. 备份实现方案
|
|
|
|
|
|
-### 4.1 SQL备份实现
|
|
|
+## 4. 数据库备份实现
|
|
|
|
|
|
-#### 4.1.1 生成INSERT语句
|
|
|
+### 4.1 存储INSERT语句到数据库
|
|
|
```php
|
|
|
/**
|
|
|
- * 生成表的SQL备份
|
|
|
+ * 备份表到数据库
|
|
|
*
|
|
|
+ * @param CleanupBackup $backup 备份记录
|
|
|
* @param string $tableName 表名
|
|
|
- * @param array $conditions 备份条件
|
|
|
- * @return string SQL文件路径
|
|
|
+ * @return array 备份结果
|
|
|
*/
|
|
|
-public function createSqlBackup(string $tableName, array $conditions = []): string
|
|
|
+private static function backupTableToDatabase(CleanupBackup $backup, string $tableName): array
|
|
|
{
|
|
|
- $backupPath = $this->generateBackupPath($tableName, 'sql');
|
|
|
- $whereClause = $this->buildWhereClause($conditions);
|
|
|
-
|
|
|
- // 获取表结构
|
|
|
- $createTableSql = $this->getCreateTableSql($tableName);
|
|
|
-
|
|
|
- // 分批查询数据并生成INSERT语句
|
|
|
- $batchSize = 1000;
|
|
|
- $offset = 0;
|
|
|
-
|
|
|
- $file = fopen($backupPath, 'w');
|
|
|
- fwrite($file, "-- 备份文件: {$tableName}\n");
|
|
|
- fwrite($file, "-- 创建时间: " . date('Y-m-d H:i:s') . "\n\n");
|
|
|
- fwrite($file, $createTableSql . "\n\n");
|
|
|
-
|
|
|
- do {
|
|
|
- $records = DB::table($tableName)
|
|
|
- ->whereRaw($whereClause)
|
|
|
- ->offset($offset)
|
|
|
- ->limit($batchSize)
|
|
|
- ->get();
|
|
|
-
|
|
|
- if ($records->isNotEmpty()) {
|
|
|
- $insertSql = $this->generateInsertSql($tableName, $records);
|
|
|
- fwrite($file, $insertSql . "\n");
|
|
|
+ try {
|
|
|
+ // 获取表数据
|
|
|
+ $records = DB::table($tableName)->get();
|
|
|
+
|
|
|
+ if ($records->isEmpty()) {
|
|
|
+ // 即使没有数据也创建记录,记录表结构
|
|
|
+ $sqlContent = "-- 表 {$tableName} 的数据备份\n";
|
|
|
+ $sqlContent .= "-- 备份时间: " . now()->toDateTimeString() . "\n";
|
|
|
+ $sqlContent .= "-- 该表无数据记录\n\n";
|
|
|
+
|
|
|
+ // 获取表结构
|
|
|
+ $createTable = DB::select("SHOW CREATE TABLE `{$tableName}`")[0];
|
|
|
+ $sqlContent .= $createTable->{'Create Table'} . ";\n";
|
|
|
+
|
|
|
+ $recordsCount = 0;
|
|
|
+ } else {
|
|
|
+ // 生成INSERT语句
|
|
|
+ $sqlContent = static::generateInsertStatements($tableName, $records);
|
|
|
+ $recordsCount = $records->count();
|
|
|
}
|
|
|
-
|
|
|
- $offset += $batchSize;
|
|
|
- } while ($records->count() === $batchSize);
|
|
|
-
|
|
|
- fclose($file);
|
|
|
-
|
|
|
- return $backupPath;
|
|
|
-}
|
|
|
-```
|
|
|
|
|
|
-#### 4.1.2 INSERT语句生成
|
|
|
-```php
|
|
|
-/**
|
|
|
- * 生成INSERT语句
|
|
|
- */
|
|
|
-private function generateInsertSql(string $tableName, $records): string
|
|
|
-{
|
|
|
- if ($records->isEmpty()) {
|
|
|
- return '';
|
|
|
- }
|
|
|
-
|
|
|
- $columns = array_keys((array)$records->first());
|
|
|
- $columnList = '`' . implode('`, `', $columns) . '`';
|
|
|
-
|
|
|
- $values = [];
|
|
|
- foreach ($records as $record) {
|
|
|
- $recordArray = (array)$record;
|
|
|
- $valueList = [];
|
|
|
- foreach ($recordArray as $value) {
|
|
|
- if (is_null($value)) {
|
|
|
- $valueList[] = 'NULL';
|
|
|
- } else {
|
|
|
- $valueList[] = "'" . addslashes($value) . "'";
|
|
|
- }
|
|
|
- }
|
|
|
- $values[] = '(' . implode(', ', $valueList) . ')';
|
|
|
+ $contentSize = strlen($sqlContent);
|
|
|
+ $contentHash = hash('sha256', $sqlContent);
|
|
|
+
|
|
|
+ // 保存到数据库
|
|
|
+ CleanupSqlBackup::create([
|
|
|
+ 'backup_id' => $backup->id,
|
|
|
+ 'table_name' => $tableName,
|
|
|
+ 'sql_content' => $sqlContent,
|
|
|
+ 'records_count' => $recordsCount,
|
|
|
+ 'content_size' => $contentSize,
|
|
|
+ 'content_hash' => $contentHash,
|
|
|
+ 'backup_conditions' => null,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => "表 {$tableName} 数据库备份成功",
|
|
|
+ 'file_size' => $contentSize,
|
|
|
+ 'records_count' => $recordsCount,
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => $e->getMessage(),
|
|
|
+ 'file_size' => 0,
|
|
|
+ 'records_count' => 0,
|
|
|
+ ];
|
|
|
}
|
|
|
-
|
|
|
- return "INSERT INTO `{$tableName}` ({$columnList}) VALUES\n" .
|
|
|
- implode(",\n", $values) . ';';
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-### 4.2 JSON备份实现
|
|
|
+### 4.2 数据库备份的优势
|
|
|
+- **查询快速**:直接存储在数据库中,查询和检索非常快速
|
|
|
+- **无文件管理**:不需要管理文件系统,避免文件丢失问题
|
|
|
+- **事务安全**:利用数据库事务保证数据一致性
|
|
|
+- **压缩存储**:数据库自动优化存储,无需额外压缩
|
|
|
+- **备份可靠**:随数据库一起备份,不会单独丢失
|
|
|
+
|
|
|
|
|
|
-```php
|
|
|
-/**
|
|
|
- * 创建JSON格式备份
|
|
|
- */
|
|
|
-public function createJsonBackup(string $tableName, array $conditions = []): string
|
|
|
-{
|
|
|
- $backupPath = $this->generateBackupPath($tableName, 'json');
|
|
|
- $whereClause = $this->buildWhereClause($conditions);
|
|
|
-
|
|
|
- $metadata = [
|
|
|
- 'table_name' => $tableName,
|
|
|
- 'backup_time' => date('Y-m-d H:i:s'),
|
|
|
- 'conditions' => $conditions,
|
|
|
- 'records' => []
|
|
|
- ];
|
|
|
-
|
|
|
- // 分批查询数据
|
|
|
- $batchSize = 1000;
|
|
|
- $offset = 0;
|
|
|
-
|
|
|
- do {
|
|
|
- $records = DB::table($tableName)
|
|
|
- ->whereRaw($whereClause)
|
|
|
- ->offset($offset)
|
|
|
- ->limit($batchSize)
|
|
|
- ->get();
|
|
|
-
|
|
|
- foreach ($records as $record) {
|
|
|
- $metadata['records'][] = (array)$record;
|
|
|
- }
|
|
|
-
|
|
|
- $offset += $batchSize;
|
|
|
- } while ($records->count() === $batchSize);
|
|
|
-
|
|
|
- file_put_contents($backupPath, json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
-
|
|
|
- return $backupPath;
|
|
|
-}
|
|
|
-```
|
|
|
|
|
|
-### 4.3 CSV备份实现
|
|
|
|
|
|
-```php
|
|
|
-/**
|
|
|
- * 创建CSV格式备份
|
|
|
- */
|
|
|
-public function createCsvBackup(string $tableName, array $conditions = []): string
|
|
|
-{
|
|
|
- $backupPath = $this->generateBackupPath($tableName, 'csv');
|
|
|
- $whereClause = $this->buildWhereClause($conditions);
|
|
|
-
|
|
|
- $file = fopen($backupPath, 'w');
|
|
|
-
|
|
|
- // 写入BOM以支持中文
|
|
|
- fwrite($file, "\xEF\xBB\xBF");
|
|
|
-
|
|
|
- $headerWritten = false;
|
|
|
- $batchSize = 1000;
|
|
|
- $offset = 0;
|
|
|
-
|
|
|
- do {
|
|
|
- $records = DB::table($tableName)
|
|
|
- ->whereRaw($whereClause)
|
|
|
- ->offset($offset)
|
|
|
- ->limit($batchSize)
|
|
|
- ->get();
|
|
|
-
|
|
|
- if ($records->isNotEmpty() && !$headerWritten) {
|
|
|
- // 写入表头
|
|
|
- $headers = array_keys((array)$records->first());
|
|
|
- fputcsv($file, $headers);
|
|
|
- $headerWritten = true;
|
|
|
- }
|
|
|
-
|
|
|
- foreach ($records as $record) {
|
|
|
- fputcsv($file, (array)$record);
|
|
|
- }
|
|
|
-
|
|
|
- $offset += $batchSize;
|
|
|
- } while ($records->count() === $batchSize);
|
|
|
-
|
|
|
- fclose($file);
|
|
|
-
|
|
|
- return $backupPath;
|
|
|
-}
|
|
|
-```
|
|
|
|
|
|
-## 5. 备份压缩和存储
|
|
|
|
|
|
-### 5.1 文件压缩
|
|
|
-```php
|
|
|
-/**
|
|
|
- * 压缩备份文件
|
|
|
- */
|
|
|
-public function compressBackupFile(string $filePath, string $compressionType = 'gzip'): string
|
|
|
-{
|
|
|
- switch ($compressionType) {
|
|
|
- case 'gzip':
|
|
|
- return $this->gzipCompress($filePath);
|
|
|
- case 'zip':
|
|
|
- return $this->zipCompress($filePath);
|
|
|
- default:
|
|
|
- return $filePath; // 不压缩
|
|
|
- }
|
|
|
-}
|
|
|
|
|
|
-/**
|
|
|
- * Gzip压缩
|
|
|
- */
|
|
|
-private function gzipCompress(string $filePath): string
|
|
|
-{
|
|
|
- $compressedPath = $filePath . '.gz';
|
|
|
-
|
|
|
- $input = fopen($filePath, 'rb');
|
|
|
- $output = gzopen($compressedPath, 'wb9');
|
|
|
-
|
|
|
- while (!feof($input)) {
|
|
|
- gzwrite($output, fread($input, 8192));
|
|
|
- }
|
|
|
-
|
|
|
- fclose($input);
|
|
|
- gzclose($output);
|
|
|
-
|
|
|
- // 删除原文件
|
|
|
- unlink($filePath);
|
|
|
-
|
|
|
- return $compressedPath;
|
|
|
-}
|
|
|
-```
|
|
|
|
|
|
-### 5.2 存储路径管理
|
|
|
-```php
|
|
|
-/**
|
|
|
- * 生成备份文件路径
|
|
|
- */
|
|
|
-private function generateBackupPath(string $tableName, string $format): string
|
|
|
-{
|
|
|
- $date = date('Y/m/d');
|
|
|
- $timestamp = date('His');
|
|
|
- $random = substr(md5(uniqid()), 0, 8);
|
|
|
-
|
|
|
- $backupDir = storage_path("app/cleanup/backups/{$date}");
|
|
|
-
|
|
|
- if (!is_dir($backupDir)) {
|
|
|
- mkdir($backupDir, 0755, true);
|
|
|
- }
|
|
|
-
|
|
|
- return "{$backupDir}/{$tableName}_{$timestamp}_{$random}.{$format}";
|
|
|
-}
|
|
|
-```
|
|
|
|
|
|
-## 6. 备份恢复功能
|
|
|
+## 5. 数据库备份恢复功能
|
|
|
|
|
|
-### 6.1 SQL备份恢复
|
|
|
+### 5.1 从数据库备份恢复数据
|
|
|
```php
|
|
|
/**
|
|
|
- * 从SQL备份恢复数据
|
|
|
+ * 从数据库备份恢复数据
|
|
|
+ *
|
|
|
+ * @param int $backupId 备份记录ID
|
|
|
+ * @return bool 恢复是否成功
|
|
|
*/
|
|
|
-public function restoreFromSqlBackup(string $backupPath): bool
|
|
|
+public function restoreFromDatabaseBackup(int $backupId): bool
|
|
|
{
|
|
|
try {
|
|
|
DB::beginTransaction();
|
|
|
-
|
|
|
- $sql = file_get_contents($backupPath);
|
|
|
-
|
|
|
- // 分割SQL语句并执行
|
|
|
- $statements = $this->splitSqlStatements($sql);
|
|
|
-
|
|
|
- foreach ($statements as $statement) {
|
|
|
- if (trim($statement)) {
|
|
|
- DB::statement($statement);
|
|
|
+
|
|
|
+ // 获取备份记录
|
|
|
+ $backup = CleanupBackup::find($backupId);
|
|
|
+ if (!$backup) {
|
|
|
+ throw new \Exception("备份记录不存在: {$backupId}");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取所有SQL备份内容
|
|
|
+ $sqlBackups = CleanupSqlBackup::where('backup_id', $backupId)->get();
|
|
|
+
|
|
|
+ foreach ($sqlBackups as $sqlBackup) {
|
|
|
+ // 执行SQL语句恢复数据
|
|
|
+ $statements = $this->splitSqlStatements($sqlBackup->sql_content);
|
|
|
+
|
|
|
+ foreach ($statements as $statement) {
|
|
|
+ $statement = trim($statement);
|
|
|
+ if (!empty($statement) && !str_starts_with($statement, '--')) {
|
|
|
+ DB::statement($statement);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
DB::commit();
|
|
|
+
|
|
|
+ Log::info('数据库备份恢复成功', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'tables_count' => $sqlBackups->count(),
|
|
|
+ 'total_records' => $sqlBackups->sum('records_count')
|
|
|
+ ]);
|
|
|
+
|
|
|
return true;
|
|
|
-
|
|
|
+
|
|
|
} catch (\Exception $e) {
|
|
|
DB::rollBack();
|
|
|
- Log::error('备份恢复失败', [
|
|
|
- 'backup_path' => $backupPath,
|
|
|
+ Log::error('数据库备份恢复失败', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
'error' => $e->getMessage()
|
|
|
]);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
-```
|
|
|
|
|
|
-### 6.2 JSON备份恢复
|
|
|
-```php
|
|
|
/**
|
|
|
- * 从JSON备份恢复数据
|
|
|
+ * 分割SQL语句
|
|
|
+ *
|
|
|
+ * @param string $sql SQL内容
|
|
|
+ * @return array SQL语句数组
|
|
|
*/
|
|
|
-public function restoreFromJsonBackup(string $backupPath): bool
|
|
|
+private function splitSqlStatements(string $sql): array
|
|
|
{
|
|
|
- try {
|
|
|
- $backupData = json_decode(file_get_contents($backupPath), true);
|
|
|
- $tableName = $backupData['table_name'];
|
|
|
- $records = $backupData['records'];
|
|
|
-
|
|
|
- DB::beginTransaction();
|
|
|
-
|
|
|
- // 分批插入数据
|
|
|
- $chunks = array_chunk($records, 1000);
|
|
|
- foreach ($chunks as $chunk) {
|
|
|
- DB::table($tableName)->insert($chunk);
|
|
|
- }
|
|
|
-
|
|
|
- DB::commit();
|
|
|
- return true;
|
|
|
-
|
|
|
- } catch (\Exception $e) {
|
|
|
- DB::rollBack();
|
|
|
- return false;
|
|
|
- }
|
|
|
+ // 简单的SQL语句分割,按分号分割
|
|
|
+ $statements = explode(';', $sql);
|
|
|
+
|
|
|
+ return array_filter(array_map('trim', $statements));
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-## 7. 备份管理功能
|
|
|
+## 6. 备份管理功能
|
|
|
|
|
|
-### 7.1 备份清理策略
|
|
|
+### 6.1 备份清理策略
|
|
|
```php
|
|
|
/**
|
|
|
* 清理过期备份
|
|
|
+ *
|
|
|
+ * @return int 删除的备份数量
|
|
|
*/
|
|
|
public function cleanExpiredBackups(): int
|
|
|
{
|
|
|
$expiredBackups = CleanupBackup::where('expires_at', '<', now())
|
|
|
->where('backup_status', 2) // 已完成的备份
|
|
|
->get();
|
|
|
-
|
|
|
+
|
|
|
$deletedCount = 0;
|
|
|
-
|
|
|
+
|
|
|
foreach ($expiredBackups as $backup) {
|
|
|
try {
|
|
|
- // 删除备份文件
|
|
|
- if (file_exists($backup->backup_path)) {
|
|
|
- unlink($backup->backup_path);
|
|
|
- }
|
|
|
-
|
|
|
- // 删除相关的备份文件记录
|
|
|
- CleanupBackupFile::where('backup_id', $backup->id)->delete();
|
|
|
-
|
|
|
+ DB::beginTransaction();
|
|
|
+
|
|
|
+ // 删除相关的SQL备份记录
|
|
|
+ CleanupSqlBackup::where('backup_id', $backup->id)->delete();
|
|
|
+
|
|
|
// 删除备份记录
|
|
|
$backup->delete();
|
|
|
-
|
|
|
+
|
|
|
+ DB::commit();
|
|
|
$deletedCount++;
|
|
|
-
|
|
|
+
|
|
|
+ Log::info('删除过期备份成功', [
|
|
|
+ 'backup_id' => $backup->id,
|
|
|
+ 'backup_name' => $backup->backup_name
|
|
|
+ ]);
|
|
|
+
|
|
|
} catch (\Exception $e) {
|
|
|
+ DB::rollBack();
|
|
|
Log::error('删除过期备份失败', [
|
|
|
'backup_id' => $backup->id,
|
|
|
'error' => $e->getMessage()
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return $deletedCount;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-### 7.2 备份验证
|
|
|
+### 6.2 备份验证
|
|
|
```php
|
|
|
/**
|
|
|
- * 验证备份文件完整性
|
|
|
+ * 验证数据库备份完整性
|
|
|
+ *
|
|
|
+ * @param int $backupId 备份ID
|
|
|
+ * @return bool 验证结果
|
|
|
*/
|
|
|
public function verifyBackupIntegrity(int $backupId): bool
|
|
|
{
|
|
|
- $backup = CleanupBackup::find($backupId);
|
|
|
-
|
|
|
- if (!$backup || !file_exists($backup->backup_path)) {
|
|
|
+ try {
|
|
|
+ $backup = CleanupBackup::find($backupId);
|
|
|
+
|
|
|
+ if (!$backup) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查SQL备份记录是否存在
|
|
|
+ $sqlBackupsCount = CleanupSqlBackup::where('backup_id', $backupId)->count();
|
|
|
+
|
|
|
+ if ($sqlBackupsCount === 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证每个SQL备份的内容哈希
|
|
|
+ $sqlBackups = CleanupSqlBackup::where('backup_id', $backupId)->get();
|
|
|
+
|
|
|
+ foreach ($sqlBackups as $sqlBackup) {
|
|
|
+ $currentHash = hash('sha256', $sqlBackup->sql_content);
|
|
|
+ if ($currentHash !== $sqlBackup->content_hash) {
|
|
|
+ Log::warning('SQL备份内容哈希验证失败', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'sql_backup_id' => $sqlBackup->id,
|
|
|
+ 'table_name' => $sqlBackup->table_name
|
|
|
+ ]);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('备份验证失败', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
return false;
|
|
|
}
|
|
|
-
|
|
|
- // 验证文件哈希
|
|
|
- $currentHash = md5_file($backup->backup_path);
|
|
|
-
|
|
|
- return $currentHash === $backup->backup_hash;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-## 8. 配置选项
|
|
|
+## 7. 配置选项
|
|
|
|
|
|
-### 8.1 备份配置
|
|
|
+### 7.1 备份配置
|
|
|
```php
|
|
|
// config/cleanup.php
|
|
|
return [
|
|
|
'backup' => [
|
|
|
- // 默认备份类型
|
|
|
- 'default_type' => 'sql', // sql, json, csv
|
|
|
-
|
|
|
- // 默认压缩类型
|
|
|
- 'compression' => 'gzip', // gzip, zip, none
|
|
|
-
|
|
|
+ // 备份类型(固定为数据库备份)
|
|
|
+ 'type' => 'database',
|
|
|
+
|
|
|
// 备份保留天数
|
|
|
'retention_days' => 30,
|
|
|
-
|
|
|
+
|
|
|
// 分批处理大小
|
|
|
'batch_size' => 1000,
|
|
|
-
|
|
|
- // 最大备份文件大小(MB)
|
|
|
- 'max_file_size' => 500,
|
|
|
-
|
|
|
- // 备份存储路径
|
|
|
- 'storage_path' => 'cleanup/backups',
|
|
|
-
|
|
|
+
|
|
|
// 是否自动清理过期备份
|
|
|
'auto_cleanup' => true,
|
|
|
+
|
|
|
+ // 生成INSERT语句时的最大记录数
|
|
|
+ 'max_records_per_statement' => 100,
|
|
|
]
|
|
|
];
|
|
|
```
|
|
|
|
|
|
-这个备份设计提供了完整的数据保护机制,确保在清理数据前能够安全地备份将要删除的数据,并支持快速恢复。您觉得这个备份方案如何?是否需要调整某些部分?
|
|
|
+## 8. 总结
|
|
|
+
|
|
|
+这个数据库备份设计专注于将备份数据直接存储到数据库中,具有以下优势:
|
|
|
+
|
|
|
+### 8.1 设计优势
|
|
|
+- **简化架构**:只支持数据库备份,架构简单清晰
|
|
|
+- **查询快速**:备份数据存储在数据库中,查询和检索非常快速
|
|
|
+- **无文件管理**:不需要管理文件系统,避免文件丢失问题
|
|
|
+- **事务安全**:利用数据库事务保证数据一致性
|
|
|
+- **备份可靠**:随数据库一起备份,不会单独丢失
|
|
|
+- **空间优化**:数据库自动优化存储,无需额外压缩
|
|
|
+
|
|
|
+### 8.2 适用场景
|
|
|
+- 中小型数据量的清理备份
|
|
|
+- 需要快速查询和恢复的场景
|
|
|
+- 对数据安全性要求较高的环境
|
|
|
+- 不希望管理复杂文件系统的场景
|
|
|
+
|
|
|
+这个备份方案专注于数据库备份,提供了完整的数据保护机制,确保在清理数据前能够安全地备份将要删除的数据,并支持快速恢复。
|