最近被问到这个东西,所以查找一下资料然后自己归纳总结一下这两个东西。
乐观锁
概念:乐观锁假设并发冲突不常发生,允许多个事务同时访问数据,但在提交时检查数据是否被修改过(个人认为就是最后检查一下是否某个值比如id或版本,来判定是否有人修改了资料,有就不允许其他事务操作了 )
一般通过某个标识来判定资料是否被异动
- 版本号机制
// 实体类
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Version { get; set; } // 版本号字段
}
// 更新时检查版本
UPDATE Products
SET Name = @NewName, Price = @NewPrice, Version = Version + 1
WHERE Id = @Id AND Version = @OriginalVersion
- 时间戳机制
public class Order
{
public int Id { get; set; }
public DateTime Timestamp { get; set; } // 时间戳字段
}
// SQL
UPDATE Orders
SET Status = @NewStatus, Timestamp = GETDATE()
WHERE Id = @Id AND Timestamp = @OriginalTimestamp
事例:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[Timestamp] // 使用时间戳作为乐观锁
public byte[] RowVersion { get; set; }
}
// 更新时处理并发冲突
try
{
var product = context.Products.Find(id);
product.Name = "New Name";
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// 处理冲突
var entry = ex.Entries[0];
var databaseValues = entry.GetDatabaseValues();
// 决定如何处理(重试/合并/取消)
}
最后UPDATE时判定是否通过刚开始查询出来的标识带入到UPDATE条件看是否会有影响行,来判定资料是有有给人异动( 更新失败后考虑是否要加入重试机制或人员手动触发按钮,捞取最新资料再次进行资料异动)
悲观锁
概念:悲观锁假设并发冲突经常发生,在访问数据前先加锁,阻止其他事务访问(个人认为就是最开始就认定他会被改,只允许一个人改,加了个锁后,其他人就不能直接访问,有点像同步锁的概念 )
实现方式
- 数据库行锁(注意加了行锁之后,需要事务commit,rockback才会释放)
-- SQL Server
BEGIN TRANSACTION
SELECT * FROM Products WITH (UPDLOCK) WHERE Id = 1
-- 执行更新操作
UPDATE Products SET Stock = Stock - 1 WHERE Id = 1
COMMIT TRANSACTION
- C# 代码实现
// 使用锁对象
private static readonly object _lockObj = new object();
public void UpdateProduct(int productId)
{
lock (_lockObj)
{
// 执行更新操作
}
}
事例
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// 获取悲观锁
var cmd = connection.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = "SELECT * FROM Accounts WITH (UPDLOCK) WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", accountId);
var reader = cmd.ExecuteReader();
// 执行更新操作
if (reader.Read())
{
var updateCmd = connection.CreateCommand();
updateCmd.Transaction = transaction;
updateCmd.CommandText = "UPDATE Accounts SET Balance = Balance - @amount WHERE Id = @id";
updateCmd.Parameters.AddWithValue("@id", accountId);
updateCmd.Parameters.AddWithValue("@amount", amount);
updateCmd.ExecuteNonQuery();
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
悲观锁保证了事务的强一致性,但是有可能会导致死锁的现象,需要注意
在实际应用中,可以结合两种锁策略:
// 先尝试乐观锁
try
{
OptimisticUpdate();
}
catch (ConcurrencyException)
{
// 如果乐观锁失败,使用悲观锁重试
PessimisticUpdate();
}
总结:
乐观锁与悲观锁核心区别对比表(C#实现示例)
对比维度 | 悲观锁 (Pessimistic Lock) | 乐观锁 (Optimistic Lock) |
---|---|---|
核心思想 | 认为数据并发冲突概率高,先加锁再操作,确保独占性 | 认为数据并发冲突概率低,先操作后校验,冲突时重试 |
实现机制 | 依赖锁机制强制独占访问 | 不依赖锁,通过版本号机制或CAS算法实现 |
锁粒度 | 粒度较粗,锁开销较大 | 无实际锁,仅通过版本标识控制,开销极小 |
并发性能 | 并发量高时,锁竞争激烈,容易导致线程阻塞,性能下降 | 并发量高时无锁竞争,线程无需阻塞,性能更优 |
阻塞情况 | 会阻塞其他线程的读写操作 | 不会阻塞线程,冲突时仅通过重试机制处理,无阻塞等待 |
适用场景 | 写操作频繁、并发冲突概率高的场景(如库存扣减、订单支付) | 读操作频繁、并发冲突概率低的场景(如数据查询、信息展示) |
数据一致性 | 强一致性,全程锁定数据,避免脏读、不可重复读、幻读 | 最终一致性,仅在提交时校验冲突,可能出现中间态不一致 |
实现示例 | - C#:lock 关键字- Monitor.Enter/Exit - Mutex 互斥体- 数据库: SELECT ... FOR UPDATE (排他锁) | - C#:Interlocked 类(基于CAS算法)- 自定义版本号机制(如实体类添加 Version 属性,更新时校验)- Entity Framework的 [Timestamp] 特性 |
冲突处理方式 | 冲突时阻塞等待,直到锁释放后重新竞争 | 冲突时直接放弃操作或触发重试(需业务层处理重试逻辑,避免死循环) |
死锁风险 | 存在死锁风险(如多个线程交叉持有锁并等待对方释放) | 无死锁风险(无实际锁竞争,仅依赖版本校验) |
补充说明:
-
悲观锁C#示例:使用
lock
关键字确保代码块原子性private readonly object _lockObj = new object(); public void UpdateData() { lock (_lockObj) // 加锁 { // 执行数据更新操作 } }
-
乐观锁C#示例:使用
Interlocked
类(CAS操作)private int _counter = 0; public void IncrementCounter() { int original, newValue; do { original = _counter; newValue = original + 1; // 比较并交换:如果当前值等于original,则更新为newValue } while (Interlocked.CompareExchange(ref _counter, newValue, original) != original); } ``` |