多用户客户端应用中如何防止同一ID的dosomething Sub并发执行?
解决多客户端并发执行同一ID的dosomething方法的方案
针对多客户端(多个应用实例)场景下,防止同一ID的dosomething方法被并发执行,你提到的SyncLock确实无法解决问题——它只能控制单个应用进程内的同步,跨进程的并发必须依赖数据库层面的同步机制。下面提供两种可行的方案:
方案1:使用SQL Server应用锁(sp_getapplock)
这是SQL Server内置的应用级锁机制,无需额外创建表,直接针对特定ID生成唯一锁键,确保同一时间只有一个客户端能持有该锁。
实现代码
Public Sub dosomething(ByVal ID As Integer) Dim connString As String = "你的数据库连接字符串" Using conn As New SqlConnection(connString) conn.Open() Using tran As SqlTransaction = conn.BeginTransaction() Try ' 获取针对当前ID的排他锁,超时时间设为5000毫秒(可根据需求调整) Dim lockCmd As New SqlCommand("EXEC sp_getapplock @Resource = @LockKey, @LockMode = 'Exclusive', @LockTimeout = 5000", conn, tran) lockCmd.Parameters.AddWithValue("@LockKey", "ID_Processing_Lock_" & ID) Dim lockResult As Integer = CInt(lockCmd.ExecuteScalar()) ' 锁获取成功(返回值为0或1表示成功) If lockResult >= 0 Then ' -------------------------- ' 执行你的核心业务逻辑:删除+插入表B ' -------------------------- ' 示例:删除表B中关联当前ID的记录 Dim deleteCmd As New SqlCommand("DELETE FROM B WHERE ID_A = @ID", conn, tran) deleteCmd.Parameters.AddWithValue("@ID", ID) deleteCmd.ExecuteNonQuery() ' 示例:插入表B的逻辑(根据表A数据生成) ' Dim insertCmd As New SqlCommand("INSERT INTO B (ID_A, ...) VALUES (@ID, ...)", conn, tran) ' insertCmd.Parameters.AddWithValue("@ID", ID) ' insertCmd.ExecuteNonQuery() ' 释放锁 Dim releaseCmd As New SqlCommand("EXEC sp_releaseapplock @Resource = @LockKey", conn, tran) releaseCmd.Parameters.AddWithValue("@LockKey", "ID_Processing_Lock_" & ID) releaseCmd.ExecuteNonQuery() tran.Commit() Else Throw New InvalidOperationException($"无法获取ID {ID} 的执行锁,当前有其他客户端正在处理该ID") End If Catch ex As Exception tran.Rollback() Throw ex End Try End Using End Using End Sub
注意事项
@LockTimeout:设置等待锁的超时时间,避免客户端无限等待- 锁必须在事务内使用,确保异常时能自动释放锁
- 锁键要足够唯一,避免和其他业务的应用锁冲突
方案2:优化你的临时表锁方案
你提出的“用临时表存ID作为主键”思路是可行的,这里做一些优化,确保逻辑更严谨:
步骤1:创建锁表
CREATE TABLE ID_Processing_Locks ( ID INT PRIMARY KEY, LockedAt DATETIME DEFAULT GETDATE() )
实现代码
Public Sub dosomething(ByVal ID As Integer) Dim connString As String = "你的数据库连接字符串" Using conn As New SqlConnection(connString) conn.Open() ' 使用Serializable隔离级别,确保锁的排他性 Using tran As SqlTransaction = conn.BeginTransaction(IsolationLevel.Serializable) Try ' 尝试插入锁记录,主键重复则触发异常 Dim lockCmd As New SqlCommand("INSERT INTO ID_Processing_Locks (ID) VALUES (@ID)", conn, tran) lockCmd.Parameters.AddWithValue("@ID", ID) lockCmd.ExecuteNonQuery() ' -------------------------- ' 执行你的核心业务逻辑:删除+插入表B ' -------------------------- ' 示例:删除表B中关联当前ID的记录 Dim deleteCmd As New SqlCommand("DELETE FROM B WHERE ID_A = @ID", conn, tran) deleteCmd.Parameters.AddWithValue("@ID", ID) deleteCmd.ExecuteNonQuery() ' 示例:插入表B的逻辑(根据表A数据生成) ' Dim insertCmd As New SqlCommand("INSERT INTO B (ID_A, ...) VALUES (@ID, ...)", conn, tran) ' insertCmd.Parameters.AddWithValue("@ID", ID) ' insertCmd.ExecuteNonQuery() ' 释放锁:删除锁记录 Dim unlockCmd As New SqlCommand("DELETE FROM ID_Processing_Locks WHERE ID = @ID", conn, tran) unlockCmd.Parameters.AddWithValue("@ID", ID) unlockCmd.ExecuteNonQuery() tran.Commit() Catch ex As SqlException ' 捕获主键重复异常(SQL错误号2627) If ex.Number = 2627 Then Throw New InvalidOperationException($"ID {ID} 正在被其他客户端处理,请稍后重试") Else tran.Rollback() Throw ex End If Catch ex As Exception tran.Rollback() Throw ex End Try End Using End Using End Sub
优化点
- 使用
Serializable隔离级别,避免并发插入时的幻读问题 - 捕获特定的SQL异常(错误号2627)来判断是否是并发冲突
- 事务包裹整个流程,确保异常时锁记录能被回滚
方案对比
- 应用锁方案:轻量,无需额外维护锁表,适合大多数场景
- 临时表方案:直观,可通过查询锁表排查当前正在处理的ID,适合需要监控锁状态的场景
内容的提问来源于stack exchange,提问作者bautista
相关产品推荐
相关产品推荐

