使用 SQLFileStream 并发读取?
Concurrent reads using SQLFileStream?
使用 .Net 4.52 和 SQL Server 2014 with FILESTREAM
我们有一个网络服务在与 "System.InvalidOperationException: The process cannot access the file specified because it has been opened in another transaction."
并发读取时失败
我已经隔离了代码以重现测试程序中的故障,该程序生成 10 个并发任务,这些任务使用 IsolationLevel.ReadCommitted 和 IO.FileAccess.Read 读取相同的数据。我的理解是数据库和文件系统都会有共享锁,应该没有"blocking".
对于单个任务,代码始终如一地工作。有时它适用于 2-3 个任务。对于 10 个任务,代码几乎总是失败——每隔一段时间它就可以工作。我考虑过其他程序员可能正在访问数据,因为数据库位于我们的其中一台开发服务器上,但这并不能解释 10 个任务几乎一致的失败。
任何有关可能导致失败的建议,我们将不胜感激!
驱动测试的代码:
Dim profileKey As Guid = New Guid("DC3F1949-37DB-4D47-B204-0170FA4A40CD")
Dim taskList As List(Of Task) = New List(Of Task)
For x = 1 To 10
Dim tsa As New TestSqlFileStream
taskList.Add(Task.Run(Function() tsa.GetProfiles(profileKey, True)))
Next
Task.WaitAll(taskList.ToArray)
正在测试的class:
Public Class TestSqlFileStream
Public Function GetProfiles(profileKey As Guid, getSmallVersionOfImage As Boolean) As List(Of Profile)
Dim retProfiles = New List(Of Profile)
Using conn As New SqlConnection("server=blah,1435; initial catalog=blah;Trusted_Connection=Yes;")
conn.Open()
Dim cmd = conn.CreateCommand()
Dim iso As IsolationLevel = IsolationLevel.ReadCommitted
cmd.Transaction = conn.BeginTransaction(iso)
Try
cmd.CommandText = "GetProfiles"
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.Add(New SqlParameter("@profileKey", SqlDbType.UniqueIdentifier)).Value = profileKey
Using reader As SqlDataReader = cmd.ExecuteReader
retProfiles = MapGetProfiles(reader, getSmallVersionOfImage)
End Using
cmd.Transaction.Commit()
Catch ex As Exception
cmd.Transaction.Rollback()
Throw
Finally
conn.Close()
End Try
End Using
Return retProfiles
End Function
Public Function MapGetProfiles(reader As SqlDataReader, getSmallVersionOfImage As Boolean) _
As List(Of Profile)
Dim profiles As New List(Of Profile)
Dim transactionToken As Byte()
Try
While reader.Read()
Dim profile As New ServiceTypes.SubTypes.Profile
profile.ParentKey = reader("ParentKey")
profile.ProfileKey = reader("ProfileKey")
profile.ProfileType = ConvertToProfileType(reader("ProfileType"))
If reader("Active") Is Nothing Then profile.Active = False Else profile.Active = reader("Active")
If IsDBNull(reader("Data")) Then profile.Data = Nothing Else profile.Data = reader("Data")
Dim imagePath
If getSmallVersionOfImage Then imagePath = reader("ImageThumbnailPath") Else imagePath = reader("ImagePath")
transactionToken = DirectCast(reader("transactionContext"), Byte())
If Not IsDBNull(imagePath) Then
If Not transactionToken.Equals(DBNull.Value) Then
LoadImage(profile, imagePath, transactionToken)
End If
End If
profiles.Add(profile)
End While
Catch ex As Exception
Throw
Finally
reader.Close()
End Try
Return profiles
End Function
Public Sub LoadImage(ByRef profile As Profile, image As String, transactionContext As Byte())
Using sqlFileStream = New SqlFileStream(image, transactionContext, IO.FileAccess.Read, FileOptions.SequentialScan, 0)
Dim retrievedImage = New Byte(sqlFileStream.Length - 1) {}
sqlFileStream.Read(retrievedImage, 0, sqlFileStream.Length)
profile.Image = retrievedImage
sqlFileStream.Close()
End Using
End Sub
Private Function ConvertToProfileType(profileType As String) As ProfileType
Dim type = ServiceTypes.SubTypes.ProfileType.None
Select Case profileType
Case Nothing
type = ServiceTypes.SubTypes.ProfileType.None
End Select
Return type
End Function
结束Class
更新: 我看过这个问题,但问题不同,因为它们在事务中拆分为并行:Threading and SqlFileStream. The process cannot access the file specified because it has been opened in another transaction
在我的示例中,每个任务都启动自己的事务。
Update2 当我在事务中的断点处停止并且 运行 查询中的 DBCC OPENTRAN window 结果是 "No active open transactions" 它似乎 SqlConnection.BeginTransaction 实际上并未在数据库中打开事务。
Update3 还从事务日志中读取(命名事务后):
Use myDB
GO
select top 1000 [Current LSN],
[Operation],
[Transaction Name],
[Transaction ID],
[Transaction SID],
[SPID],
[Begin Time]
FROM fn_dblog(null,null)
order by [Begin Time] desc
日志中没有显示我提供的名字的交易。
注意:这仅适用于检索集的原子性不重要的解决方案。我建议使用 TransactionScope 以获得更好的原子性。
当在事务(While Reader.Read 循环)下检索许多文件时,事务 queue/locking 机制似乎变得混乱。我将文件检索分解为使用新事务或每个文件检索,并且可以 运行 100 个并行任务针对单个父级的同一层次配置文件集。
使用 .Net 4.52 和 SQL Server 2014 with FILESTREAM
我们有一个网络服务在与 "System.InvalidOperationException: The process cannot access the file specified because it has been opened in another transaction."
并发读取时失败我已经隔离了代码以重现测试程序中的故障,该程序生成 10 个并发任务,这些任务使用 IsolationLevel.ReadCommitted 和 IO.FileAccess.Read 读取相同的数据。我的理解是数据库和文件系统都会有共享锁,应该没有"blocking".
对于单个任务,代码始终如一地工作。有时它适用于 2-3 个任务。对于 10 个任务,代码几乎总是失败——每隔一段时间它就可以工作。我考虑过其他程序员可能正在访问数据,因为数据库位于我们的其中一台开发服务器上,但这并不能解释 10 个任务几乎一致的失败。
任何有关可能导致失败的建议,我们将不胜感激!
驱动测试的代码:
Dim profileKey As Guid = New Guid("DC3F1949-37DB-4D47-B204-0170FA4A40CD")
Dim taskList As List(Of Task) = New List(Of Task)
For x = 1 To 10
Dim tsa As New TestSqlFileStream
taskList.Add(Task.Run(Function() tsa.GetProfiles(profileKey, True)))
Next
Task.WaitAll(taskList.ToArray)
正在测试的class:
Public Class TestSqlFileStream
Public Function GetProfiles(profileKey As Guid, getSmallVersionOfImage As Boolean) As List(Of Profile)
Dim retProfiles = New List(Of Profile)
Using conn As New SqlConnection("server=blah,1435; initial catalog=blah;Trusted_Connection=Yes;")
conn.Open()
Dim cmd = conn.CreateCommand()
Dim iso As IsolationLevel = IsolationLevel.ReadCommitted
cmd.Transaction = conn.BeginTransaction(iso)
Try
cmd.CommandText = "GetProfiles"
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.Add(New SqlParameter("@profileKey", SqlDbType.UniqueIdentifier)).Value = profileKey
Using reader As SqlDataReader = cmd.ExecuteReader
retProfiles = MapGetProfiles(reader, getSmallVersionOfImage)
End Using
cmd.Transaction.Commit()
Catch ex As Exception
cmd.Transaction.Rollback()
Throw
Finally
conn.Close()
End Try
End Using
Return retProfiles
End Function
Public Function MapGetProfiles(reader As SqlDataReader, getSmallVersionOfImage As Boolean) _
As List(Of Profile)
Dim profiles As New List(Of Profile)
Dim transactionToken As Byte()
Try
While reader.Read()
Dim profile As New ServiceTypes.SubTypes.Profile
profile.ParentKey = reader("ParentKey")
profile.ProfileKey = reader("ProfileKey")
profile.ProfileType = ConvertToProfileType(reader("ProfileType"))
If reader("Active") Is Nothing Then profile.Active = False Else profile.Active = reader("Active")
If IsDBNull(reader("Data")) Then profile.Data = Nothing Else profile.Data = reader("Data")
Dim imagePath
If getSmallVersionOfImage Then imagePath = reader("ImageThumbnailPath") Else imagePath = reader("ImagePath")
transactionToken = DirectCast(reader("transactionContext"), Byte())
If Not IsDBNull(imagePath) Then
If Not transactionToken.Equals(DBNull.Value) Then
LoadImage(profile, imagePath, transactionToken)
End If
End If
profiles.Add(profile)
End While
Catch ex As Exception
Throw
Finally
reader.Close()
End Try
Return profiles
End Function
Public Sub LoadImage(ByRef profile As Profile, image As String, transactionContext As Byte())
Using sqlFileStream = New SqlFileStream(image, transactionContext, IO.FileAccess.Read, FileOptions.SequentialScan, 0)
Dim retrievedImage = New Byte(sqlFileStream.Length - 1) {}
sqlFileStream.Read(retrievedImage, 0, sqlFileStream.Length)
profile.Image = retrievedImage
sqlFileStream.Close()
End Using
End Sub
Private Function ConvertToProfileType(profileType As String) As ProfileType
Dim type = ServiceTypes.SubTypes.ProfileType.None
Select Case profileType
Case Nothing
type = ServiceTypes.SubTypes.ProfileType.None
End Select
Return type
End Function
结束Class
更新: 我看过这个问题,但问题不同,因为它们在事务中拆分为并行:Threading and SqlFileStream. The process cannot access the file specified because it has been opened in another transaction 在我的示例中,每个任务都启动自己的事务。
Update2 当我在事务中的断点处停止并且 运行 查询中的 DBCC OPENTRAN window 结果是 "No active open transactions" 它似乎 SqlConnection.BeginTransaction 实际上并未在数据库中打开事务。
Update3 还从事务日志中读取(命名事务后):
Use myDB
GO
select top 1000 [Current LSN],
[Operation],
[Transaction Name],
[Transaction ID],
[Transaction SID],
[SPID],
[Begin Time]
FROM fn_dblog(null,null)
order by [Begin Time] desc
日志中没有显示我提供的名字的交易。
注意:这仅适用于检索集的原子性不重要的解决方案。我建议使用 TransactionScope 以获得更好的原子性。
当在事务(While Reader.Read 循环)下检索许多文件时,事务 queue/locking 机制似乎变得混乱。我将文件检索分解为使用新事务或每个文件检索,并且可以 运行 100 个并行任务针对单个父级的同一层次配置文件集。