问题 如何将文件写入磁盘并在单个事务中插入数据库记录?


我试图将文件写入磁盘以及通过原子事务中的存储过程将数据插入数据库。即如果这两个操作中的任何一个失败(文件无法写入磁盘或存储过程失败),我想什么也不做,只是将一个异常抛回给调用者。

关于如何最好地解决文件写入和数据库插入的原子事务的任何建议?

附加信息:我正在使用带有存储过程的C#.NET进入MS SQL Server,但不一定适合这些技术的通用解决方案也很好。

更新: 在写完下面的所有答案并研究其他人之后,我写道 这个帖子 关于如何使用3种不同的方法解决这个问题。


7867
2018-02-24 21:07


起源

当你的意思是'交易'时,你的意思是在1例程中,还是你实际上意味着你有回滚的SQL数据库事务? - Robbie Tapping
我只是说一个例程 - Andrew Thompson


答案:


您需要使用新的TxF,Vista,Windows 7和Windows Server 2008中引入的Transacted NTFS。这是一篇很好的介绍性文章: 使用文件系统事务增强应用程序。它包含一个将文件操作注册到系统事务中的小型托管示例:

// IKernelTransaction COM Interface
[Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IKernelTransaction
{
    int GetHandle(out IntPtr pHandle);
}

[DllImport(KERNEL32, 
   EntryPoint = "CreateFileTransacted",
   CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern SafeFileHandle CreateFileTransacted(
   [In] string lpFileName,
   [In] NativeMethods.FileAccess dwDesiredAccess,
   [In] NativeMethods.FileShare dwShareMode,
   [In] IntPtr lpSecurityAttributes,
   [In] NativeMethods.FileMode dwCreationDisposition,
   [In] int dwFlagsAndAttributes,
   [In] IntPtr hTemplateFile,
   [In] KtmTransactionHandle hTransaction,
   [In] IntPtr pusMiniVersion,
   [In] IntPtr pExtendedParameter);

....

using (TransactionScope scope = new TransactionScope())
{
   // Grab Kernel level transaction handle
   IDtcTransaction dtcTransaction = 
      TransactionInterop.GetDtcTransaction(managedTransaction);
   IKernelTransaction ktmInterface = (IKernelTransaction)dtcTransaction;

   IntPtr ktmTxHandle;
   ktmInterface.GetHandle(out ktmTxHandle);

   // Grab transacted file handle
   SafeFileHandle hFile = NativeMethods.CreateFileTransacted(
      path, internalAccess, internalShare, IntPtr.Zero,
      internalMode, 0, IntPtr.Zero, ktmTxHandle,
      IntPtr.Zero, IntPtr.Zero);

   ... // Work with file (e.g. passing hFile to StreamWriter constructor)

   // Close handles
}

您需要在同一事务中注册SQL操作,这将在TransactionScope下自动进行。但我强烈建议您覆盖默认的TransactionScope 选项 使用ReadCommitted隔离级别:

using (TransactionScope scope = new TransactionScope(
     TransactionScope.Required, 
     new TransactionOptions 
         { IsolationLevel = IsolationLEvel.ReadCommitted}))
{
...
}

如果没有这个,你将获得默认的Serializable隔离级别,这对大多数情况来说都是过度杀伤。


8
2018-02-24 21:30



请注意这种方法,因为MS不保证它将在未来的Windows版本中可用: msdn.microsoft.com/en-us/library/windows/desktop/... - Dave


这个问答 似乎是答案的一部分。它涉及Transactional NTFS。 SLaks链接到 在MSDN上托管的Transactional NTFS的.NET托管包装器。 

你可以尝试使用 TransactionScope


5
2018-02-24 21:13



即使事务失败,文件是否仍然存在(如果WriteToDisk是第一个)?从来没有使用过它(它看起来很酷)但它似乎没有任何关于块中可能发生的非数据库事物的魔力,只是如果发生异常就不会提交事务。 - Jamie Treworgy
事务性NTFS将回滚文件;)它具有魔力,因为它根据需要使用DTC来存储回滚信息。 - TomTom
真棒。我将不得不检查出来。我们只是去了一大堆现代服务器,所以我实际上可以使用它! - Jamie Treworgy
如果没有事务性NTFS,您可以使用CRM(补偿资源管理器)在重新启动后清理文件。 - TomTom


你可以利用 System.Transactions 命名空间

System.Transactions命名空间包含允许您编写自己的事务应用程序和资源管理器的类。具体而言,您可以创建并参与具有一个或多个参与者的事务(本地或分布式)。

有关更多详细信息,请参阅MSDN文档 http://msdn.microsoft.com/en-us/library/system.transactions.aspx


1
2018-02-24 21:13





对于这个简单的事情,我会(psudocode)

try
{
//write file

//commit to DB

}
catch(IOException ioe)
{
// no need to worry about sql as it hasn't happened yet
// throw new exception
}
catch(SqlException sqle)
{
// delete file
// throw exception
}

1
2018-02-24 21:21



但是可能有问题,如果删除由于某种原因不能发生(例如,另一个进程锁定了文件,或者您不希望应用程序具有删除权限)。不太可能,但不完全安全。你为什么不先做数据库部分,这可以绝对回滚而不用大惊小怪? - Jamie Treworgy
-1。显示正常程序员对交易的无知。 - TomTom
-1 @TomTom说得很完美...... - Remus Rusanu
@TomTom你的建议是什么? - Eugene Yarmash
使用System.Transaction命名空间,实现CRM(补偿资源管理器进行文件更新。分布式事务101 - 适用于初学者;)所有.NET框架都支持。 - TomTom


也许我在这里没有遇到困难,但看起来很简单......伪代码:

public void DoStuff()
{
      bool itWorked=false;
      StartTransaction();
      itWorked = RunStoredProcedure();
      itWorked = itWorked && WriteFile();
      if (!itWorked) {
          RollbackTransaction();
          throw new Exception("It didn't work");
      } else {
          CommitTransaction();
      }
}

你可以反过来做,但之后你必须删除文件,首先放置数据库尝试最容易撤消第一个操作。

编辑...我刚刚意识到这可以通过几行缩短,bool是没有必要的......为了清晰起见留下原件:

public void DoStuff()
{
      StartTransaction();
      if (!(RunStoredProcedure() && WriteFile())) {
          RollbackTransaction();
          throw new Exception("It didn't work");
      } else {
          CommitTransaction();
      }
}

喜欢短路。


1
2018-02-24 21:13



这里有很多可能出错的地方。您无法确定该文件是否真正被写入(并且在几个月前在Linux中发生了类似的错误,其中配置文件已写入但未正确刷新),提交可能会失败并且您必须删除该文件(删除可能会失败)... - xanatos
假设是 WriteFile() 仅在文件实际写入时才返回true。我不知道任何其他方式可以操作,我的意思是,如果它没有真正写出来并且你没有办法找到它,那么你在哪里?我想如果你不能依赖于其中任何一个的适当水平的信任,那么唯一的解决方案是硬编码这个的“撤消”部分,但这些步骤也可能失败,你真的不是更好的。如果对这两者都有不合适的信任程度,那么你必须尝试所有的东西,如果它没有成功,请给某人发电子邮件。 - Jamie Treworgy