如何在Linq-to-Sql中完成插入操作之前将表锁定,使其不能进行写操作
如何在Linq-to-Sql中完成插入操作之前将表锁定,使其不能进行写操作
我正在开发一个拍卖系统,我正在努力确保自己不受到两个人在同一时间为同一物品竞标的情况的影响。
为了做到这一点,我需要在表上加锁,获取当前物品的最高竞价,确保输入的竞价高于该竞价,将一个新的竞价条目添加到表中,然后解锁该表。
我需要锁定这个操作,以防止第二个web服务器在我检查最高竞价和插入新竞价之间触发竞价插入,因为这会导致数据问题。
我该如何使用Linq-to-sql来实现这个功能?
请注意,我不知道事务范围是否能够实现这一点,但我不能使用它们,因为由于我们的webfarm设置,它们往往会触发分布式事务,而我不能使用分布式事务。
在使用Linq-to-Sql进行插入操作时,有时需要阻止其他写操作对表进行修改,直到插入操作完成。为了解决这个问题,可以手动开始一个事务,并将该事务传递给DataContext进行操作。
具体操作可以参考以下链接:http://geekswithblogs.net/robp/archive/2009/04/02/your-own-transactions-with-linq-to-sql.aspx
另外,为了避免不必要的分布式事务升级,还需要手动控制连接的打开和关闭。因为DataContext有时会尝试打开两个连接,从而导致升级为分布式事务。
下面是实现的示例代码:
using (var connection = new SqlConnection(connectionString)) { connection.Open(); using (var transaction = connection.BeginTransaction()) { try { using (var dataContext = new DataContext(connection)) { // 设置事务 dataContext.Transaction = transaction; // 执行插入操作 // ... // 提交事务 transaction.Commit(); } } catch (Exception) { // 回滚事务 transaction.Rollback(); throw; } } }
通过以上的操作,可以在插入操作完成之前锁定表,避免其他写操作对表进行修改。
问题的原因是在使用Linq-to-Sql时需要确保在插入操作完成之前锁定表,以防止其他写操作干扰。解决方法是使用一个单独的表来存储处理信息,并通过插入记录到该表中来锁定表。
具体的解决方法是创建一个名为ItemProcessing的表,该表包含两个列:ItemId(整型)和ProcessingToken(GUID)。当一个进程想要检查当前的出价时,它将项目的ID和一个令牌/GUID写入ItemProcessing表。这告诉其他进程该项目当前正在被检查。如果ItemProcessing表中已经存在该项目的记录,则其他进程必须等待或中止。当原始进程完成后,它会移除令牌(将其设置为null)或完全删除ItemProcessing表中的记录。然后其他进程就知道它们可以处理该项目了。
当然,你需要确保两个进程不会同时写入该处理表。你可以通过在ProcessingToken为null时插入到该表中来实现。如果另一个进程在之前插入了记录,第二个进程将无法插入,因为ProcessingToken已经存在。
虽然不是一个完整的解决方案,但这是基本思路。
问题的原因:在使用Linq-to-sql时,实现一个完全的插入操作需要解决一些障碍:
1. 必须避免表锁,因为表锁会导致在处理一个竞标时无法进行其他商品的竞标,严重影响性能;
2. Linq to SQL似乎不支持悲观锁定。
解决方法:
如果代码中不能使用事务,建议按照以下步骤进行操作:
1. 为操作生成一个GUID;
2. 使用GUID伪锁定商品记录:
UPDATE Items SET LockingGuid = <生成的GUID> WHERE ItemId = <要插入的商品ID> and LockingGuid IS NULL
SELECT @@ROWCOUNT;
3. 如果返回的结果为1,则锁定成功;
4. 执行竞标操作;
5. 将记录的LockingGuid更新为NULL;
6. 如果锁定失败,则将失败信息返回给.NET客户端,或者使用WAITFOR进行忙等待;
7. 应该实现适当的异常处理,以防止由于进程崩溃或失败导致的商品记录被无限期地锁定,可能是通过添加一个存储锁定发生时间戳的datetime列,并清除孤立的锁定。
如果架构允许使用单独的后端操作,可以考虑使用CQRS和Event Sourcing来处理此类竞标操作。
我决定使用此方法,但是使用datetime而不是GUID,这样我就知道如果锁定超过一分钟,99.9%的可能是一个过时的锁定,然后可以清除该锁定。