Восстановление нескольких резервных копий базы данных за транзакцию
-
06-07-2019 - |
Вопрос
Я написал хранимую процедуру, которая восстанавливает набор резервных копий базы данных.Он принимает два параметра — исходный каталог и каталог восстановления.Процедура ищет все файлы .bak в исходном каталоге (рекурсивно) и восстанавливает все базы данных.
Хранимая процедура работает так, как и ожидалось, но у нее есть одна проблема: если я раскомментирую операторы try-catch, процедура завершится со следующей ошибкой:
error_number = 3013
error_severity = 16
error_state = 1
error_message = DATABASE is terminating abnormally.
Самое странное, что иногда (это непоследовательно) восстановление выполняется даже в случае возникновения ошибки.Процедура:
create proc usp_restore_databases
(
@source_directory varchar(1000),
@restore_directory varchar(1000)
)
as
begin
declare @number_of_backup_files int
-- begin transaction
-- begin try
-- step 0: Initial validation
if(right(@source_directory, 1) <> '\') set @source_directory = @source_directory + '\'
if(right(@restore_directory, 1) <> '\') set @restore_directory = @restore_directory + '\'
-- step 1: Put all the backup files in the specified directory in a table --
declare @backup_files table ( file_path varchar(1000))
declare @dos_command varchar(1000)
set @dos_command = 'dir ' + '"' + @source_directory + '*.bak" /s/b'
/* DEBUG */ print @dos_command
insert into @backup_files(file_path) exec xp_cmdshell @dos_command
delete from @backup_files where file_path IS NULL
select @number_of_backup_files = count(1) from @backup_files
/* DEBUG */ select * from @backup_files
/* DEBUG */ print @number_of_backup_files
-- step 2: restore each backup file --
declare backup_file_cursor cursor for select file_path from @backup_files
open backup_file_cursor
declare @index int; set @index = 0
while(@index < @number_of_backup_files)
begin
declare @backup_file_path varchar(1000)
fetch next from backup_file_cursor into @backup_file_path
/* DEBUG */ print @backup_file_path
-- step 2a: parse the full backup file name to get the DB file name.
declare @db_name varchar(100)
set @db_name = right(@backup_file_path, charindex('\', reverse(@backup_file_path)) -1) -- still has the .bak extension
/* DEBUG */ print @db_name
set @db_name = left(@db_name, charindex('.', @db_name) -1)
/* DEBUG */ print @db_name
set @db_name = lower(@db_name)
/* DEBUG */ print @db_name
-- step 2b: find out the logical names of the mdf and ldf files
declare @mdf_logical_name varchar(100),
@ldf_logical_name varchar(100)
declare @backup_file_contents table
(
LogicalName nvarchar(128),
PhysicalName nvarchar(260),
[Type] char(1),
FileGroupName nvarchar(128),
[Size] numeric(20,0),
[MaxSize] numeric(20,0),
FileID bigint,
CreateLSN numeric(25,0),
DropLSN numeric(25,0) NULL,
UniqueID uniqueidentifier,
ReadOnlyLSN numeric(25,0) NULL,
ReadWriteLSN numeric(25,0) NULL,
BackupSizeInBytes bigint,
SourceBlockSize int,
FileGroupID int,
LogGroupGUID uniqueidentifier NULL,
DifferentialBaseLSN numeric(25,0) NULL,
DifferentialBaseGUID uniqueidentifier,
IsReadOnly bit,
IsPresent bit
)
insert into @backup_file_contents
exec ('restore filelistonly from disk=' + '''' + @backup_file_path + '''')
select @mdf_logical_name = LogicalName from @backup_file_contents where [Type] = 'D'
select @ldf_logical_name = LogicalName from @backup_file_contents where [Type] = 'L'
/* DEBUG */ print @mdf_logical_name + ', ' + @ldf_logical_name
-- step 2c: restore
declare @mdf_file_name varchar(1000),
@ldf_file_name varchar(1000)
set @mdf_file_name = @restore_directory + @db_name + '.mdf'
set @ldf_file_name = @restore_directory + @db_name + '.ldf'
/* DEBUG */ print 'mdf_logical_name = ' + @mdf_logical_name + '|' +
'ldf_logical_name = ' + @ldf_logical_name + '|' +
'db_name = ' + @db_name + '|' +
'backup_file_path = ' + @backup_file_path + '|' +
'restore_directory = ' + @restore_directory + '|' +
'mdf_file_name = ' + @mdf_file_name + '|' +
'ldf_file_name = ' + @ldf_file_name
restore database @db_name from disk = @backup_file_path
with
move @mdf_logical_name to @mdf_file_name,
move @ldf_logical_name to @ldf_file_name
-- step 2d: iterate
set @index = @index + 1
end
close backup_file_cursor
deallocate backup_file_cursor
-- end try
-- begin catch
-- print error_message()
-- rollback transaction
-- return
-- end catch
--
-- commit transaction
end
Есть ли у кого-нибудь идеи, почему это может происходить?
Другой вопрос:полезен ли код транзакции?т. е. если нужно восстановить две базы данных, отменит ли SQL Server восстановление одной базы данных, если второе восстановление завершится неудачно?
Решение
По сути, происходило то, что у одного из файлов, которые нужно было восстановить, была проблема, и процесс восстановления выдавал ошибку, но ошибка не настолько серьезна, чтобы прервать процесс. Вот почему нет проблем без try-catch. Однако добавление try-catch перехватывает любую ошибку со степенью серьезности более 10, и поэтому поток управления переключается на блок catch, который отображает сообщения об ошибках и прерывает процедуру.
Другие советы
Кроме того, если вы не удаляете записи NULL из списка файлов (так как он закомментирован), тогда, когда ваш цикл начинается с 0, он завершает обработку несуществующего файла для своей последней итерации.
Вместо @ index = 0
вместо этого должно быть @ index = 1
или раскомментируйте
удалить из @backup_files, где file_path IS NULL
Проблемы, которые я заметил:
- Комплексная транзакция должна быть внутри начала попробуйте .... End Try Block
- Курсор не будет закрыт или сделок, если возникнет ошибка, а управление начинается для начинания ... блок End Catch
Попробуйте этот модифицированный код.Это покажет, что ваш код работает нормально.
ALTER proc usp_restore_databases
(
@source_directory varchar(1000),
@restore_directory varchar(1000)
)
as
begin
declare @number_of_backup_files int
begin transaction
begin try
print 'Entering TRY...'
-- step 0: Initial validation
if(right(@source_directory, 1) <> '\') set @source_directory = @source_directory + '\'
if(right(@restore_directory, 1) <> '\') set @restore_directory = @restore_directory + '\'
-- step 1: Put all the backup files in the specified directory in a table --
declare @backup_files table ( file_path varchar(1000))
declare @dos_command varchar(1000)
set @dos_command = 'dir ' + '"' + @source_directory + '*.bak" /s/b'
/* DEBUG */ print @dos_command
insert into @backup_files(file_path) exec xp_cmdshell @dos_command
--delete from @backup_files where file_path IS NULL
select @number_of_backup_files = count(1) from @backup_files
/* DEBUG */ select * from @backup_files
/* DEBUG */ print @number_of_backup_files
-- step 2: restore each backup file --
declare backup_file_cursor cursor for select file_path from @backup_files
open backup_file_cursor
declare @index int; set @index = 0
while(@index < @number_of_backup_files)
begin
declare @backup_file_path varchar(1000)
fetch next from backup_file_cursor into @backup_file_path
/* DEBUG */ print @backup_file_path
-- step 2a: parse the full backup file name to get the DB file name.
declare @db_name varchar(100)
set @db_name = right(@backup_file_path, charindex('\', reverse(@backup_file_path)) -1) -- still has the .bak extension
/* DEBUG */ print @db_name
set @db_name = left(@db_name, charindex('.', @db_name) -1)
/* DEBUG */ print @db_name
set @db_name = lower(@db_name)
/* DEBUG */ print @db_name
-- step 2b: find out the logical names of the mdf and ldf files
declare @mdf_logical_name varchar(100),
@ldf_logical_name varchar(100)
declare @backup_file_contents table
(
LogicalName nvarchar(128),
PhysicalName nvarchar(260),
[Type] char(1),
FileGroupName nvarchar(128),
[Size] numeric(20,0),
[MaxSize] numeric(20,0),
FileID bigint,
CreateLSN numeric(25,0),
DropLSN numeric(25,0) NULL,
UniqueID uniqueidentifier,
ReadOnlyLSN numeric(25,0) NULL,
ReadWriteLSN numeric(25,0) NULL,
BackupSizeInBytes bigint,
SourceBlockSize int,
FileGroupID int,
LogGroupGUID uniqueidentifier NULL,
DifferentialBaseLSN numeric(25,0) NULL,
DifferentialBaseGUID uniqueidentifier,
IsReadOnly bit,
IsPresent bit
)
insert into @backup_file_contents
exec ('restore filelistonly from disk=' + '''' + @backup_file_path + '''')
select @mdf_logical_name = LogicalName from @backup_file_contents where [Type] = 'D'
select @ldf_logical_name = LogicalName from @backup_file_contents where [Type] = 'L'
/* DEBUG */ print @mdf_logical_name + ', ' + @ldf_logical_name
-- step 2c: restore
declare @mdf_file_name varchar(1000),
@ldf_file_name varchar(1000)
set @mdf_file_name = @restore_directory + @db_name + '.mdf'
set @ldf_file_name = @restore_directory + @db_name + '.ldf'
/* DEBUG */ print 'mdf_logical_name = ' + @mdf_logical_name + '|' +
'ldf_logical_name = ' + @ldf_logical_name + '|' +
'db_name = ' + @db_name + '|' +
'backup_file_path = ' + @backup_file_path + '|' +
'restore_directory = ' + @restore_directory + '|' +
'mdf_file_name = ' + @mdf_file_name + '|' +
'ldf_file_name = ' + @ldf_file_name
print @index
-- restore database @db_name from disk = @backup_file_path
-- with
-- move @mdf_logical_name to @mdf_file_name,
-- move @ldf_logical_name to @ldf_file_name
-- step 2d: iterate
set @index = @index + 1
end
close backup_file_cursor
deallocate backup_file_cursor
end try
begin catch
print 'Entering Catch...'
print error_message()
rollback transaction
return
end catch
commit transaction
end
Радж
Фактическая проблема здесь заключается в том, что попытка и получение только выдает вам последнее сообщение об ошибке 3013 «резервное копирование прекращается ненормально», но не дает ошибку более низкого уровня по причине ошибки 3013. Р>
Если вы выполните команду резервного копирования, например, с неверным именем базы данных, вы получите 2 ошибки. резервное копирование базы данных неправильного_данных_данных на диск = 'диск: \ путь \ имя_файла.bak'
Msg 911, Level 16, State 11, Line 1
Could not locate entry in sysdatabases for database 'incorrect_database_name'. No entry found with that name. Make sure that the name is entered correctly.
Msg 3013, Level 16, State 1, Line 1
BACKUP DATABASE is terminating abnormally.
Если вы хотите узнать фактическую ошибку, объясняющую, почему резервное копирование завершается с ошибкой при попытке перехвата, хранимая процедура маскирует ее.
Теперь, на ваш вопрос ... что бы я сделал, если восстановление выполнится успешно, я бы немедленно удалил или переместил .bak в новое место, удалив его из каталога, который вы указали в параметре. В случае сбоя ваш оператор catch может содержать GOTO, который возвращает вас до начала BEGIN TRY и начинает выполнять с того места, где он остановился, поскольку он не будет рекурсивно определять файлы, которые вы переместили из каталога.
RUN_AGAIN:
BEGIN TRY
RECURSIVE DIR FOR FILENAMES
RESTORE DATABASE...
ON SUCCEED, DELETE .BAK FILE
END TRY
BEGIN CATCH
ON FAILURE, MOVE .BAK to A SAFE LOCATION FOR LATER ANALYSIS
GOTO RUN_AGAIN
END CATCH
Я не говорю, что это красиво, но это сработает. Вы не можете поместить ссылку GOTO в блок TRY / CATCH, поэтому она должна быть вне его. Р>
В любом случае, я просто подумала, что добавлю свои мысли к этому, даже если вопрос старый, чтобы помочь другим в такой же ситуации.