在C#中,乐观锁的实现通常基于版本号(Version Number)或时间戳(Timestamp)来检测数据是否被其他线程修改。以下是几种常见的实现方式:
1. 使用版本号实现乐观锁
版本号是最常见的乐观锁实现方式。每次对数据进行修改时,版本号会递增。在提交操作时,会检查版本号是否发生变化,如果发生变化,则说明数据被其他线程修改过,需要处理冲突。
示例代码
假设有一个Counter
类,使用版本号来实现乐观锁:
using System;
using System.Threading;
public class Counter
{
private int count = 0; // 要保护的共享资源
private int version = 0; // 版本号
public void Increment()
{
int currentVersion;
int newVersion;
do
{
currentVersion = version; // 获取当前版本号
newVersion = currentVersion + 1; // 计算新版本号
} while (Interlocked.CompareExchange(ref version, newVersion, currentVersion) != currentVersion);
// 如果版本号更新成功,说明没有冲突
count++;
}
public int GetCount()
{
return count;
}
}
代码解析
- 版本号字段:
version
字段用于记录当前版本号。 Interlocked.CompareExchange
方法:Interlocked.CompareExchange(ref version, newVersion, currentVersion)
方法会原子性地比较version
和currentVersion
:- 如果它们相等,说明没有其他线程修改过
version
,此时将version
更新为newVersion
。 - 如果它们不相等,说明其他线程已经修改了
version
,当前线程需要重新获取version
并重试。
- 如果它们相等,说明没有其他线程修改过
- 循环逻辑:
do-while
循环用于处理冲突。如果版本号更新失败(即CompareExchange
返回的值不等于currentVersion
),则重新获取当前版本号并重试。
2. 使用时间戳实现乐观锁
时间戳也可以用于实现乐观锁。每次修改数据时,更新时间戳字段。在提交操作时,检查时间戳是否发生变化,从而判断数据是否被其他线程修改过。
示例代码
using System;
using System.Threading;
public class Counter
{
private int count = 0; // 要保护的共享资源
private long timestamp = 0; // 时间戳,用于记录版本
public void Increment()
{
long currentTimestamp;
long newTimestamp;
do
{
currentTimestamp = timestamp; // 获取当前时间戳
newTimestamp = currentTimestamp + 1; // 计算新时间戳
} while (Interlocked.CompareExchange(ref timestamp, newTimestamp, currentTimestamp) != currentTimestamp);
// 如果时间戳更新成功,说明没有冲突
count++;
}
public int GetCount()
{
return count;
}
}
代码解析
- 时间戳字段:
timestamp
字段用于记录当前时间戳。 Interlocked.CompareExchange
方法:与版本号实现类似,通过原子性比较和交换操作来检测冲突。- 循环逻辑:如果时间戳更新失败,则重新获取当前时间戳并重试。
3. 使用ConcurrentDictionary
实现乐观锁
ConcurrentDictionary
是一个线程安全的字典,它内部使用了乐观锁机制。虽然它本身是一个线程安全的集合,但也可以通过它的AddOrUpdate
方法实现类似乐观锁的逻辑。
示例代码
using System.Collections.Concurrent;
public class Counter
{
private ConcurrentDictionary<int, int> counterDict = new ConcurrentDictionary<int, int>();
public void Increment()
{
counterDict.AddOrUpdate(1, 1, (key, oldValue) => oldValue + 1);
}
public int GetCount()
{
counterDict.TryGetValue(1, out int count);
return count;
}
}
代码解析
AddOrUpdate
方法:- 如果键不存在,则添加键值对(初始值为1)。
- 如果键已存在,则更新值(将旧值加1)。
- 线程安全:
ConcurrentDictionary
内部使用了乐观锁机制,确保了线程安全。
4. 使用数据库的乐观锁
在C#中,乐观锁也可以通过数据库实现。例如,可以在数据库表中添加一个版本号字段或时间戳字段,每次更新数据时检查版本号或时间戳是否发生变化。
示例代码
假设有一个数据库表Products
,包含字段Id
、Stock
和Version
:
CREATE TABLE Products (
Id INT PRIMARY KEY,
Stock INT,
Version INT
);
在C#代码中,可以通过以下方式实现乐观锁:
using System;
using System.Data.SqlClient;
public class ProductService
{
private string connectionString = "your_connection_string";
public void DecrementStock(int productId)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
// 获取当前版本号和库存
string selectQuery = "SELECT Stock, Version FROM Products WHERE Id = @Id";
using (SqlCommand selectCommand = new SqlCommand(selectQuery, connection))
{
selectCommand.Parameters.AddWithValue("@Id", productId);
using (SqlDataReader reader = selectCommand.ExecuteReader())
{
if (reader.Read())
{
int currentStock = reader.GetInt32(0);
int currentVersion = reader.GetInt32(1);
// 更新库存和版本号
string updateQuery = @"
UPDATE Products
SET Stock = Stock - 1, Version = Version + 1
WHERE Id = @Id AND Version = @Version";
using (SqlCommand updateCommand = new SqlCommand(updateQuery, connection))
{
updateCommand.Parameters.AddWithValue("@Id", productId);
updateCommand.Parameters.AddWithValue("@Version", currentVersion);
int rowsAffected = updateCommand.ExecuteNonQuery();
if (rowsAffected == 0)
{
throw new InvalidOperationException("Update failed due to concurrent modification.");
}
}
}
}
}
}
}
}
代码解析
- 获取当前版本号和库存:通过
SELECT
语句获取当前的库存和版本号。 - 更新库存和版本号:通过
UPDATE
语句更新库存和版本号,同时检查版本号是否匹配。 - 处理冲突:如果
UPDATE
语句没有影响任何行(rowsAffected == 0
),说明版本号不匹配,即数据被其他线程修改过,此时可以抛出异常或重试。
总结
在C#中,乐观锁可以通过以下方式实现:
- 版本号:通过
Interlocked.CompareExchange
方法实现原子性比较和交换操作。 - 时间戳:类似于版本号,但使用时间戳字段。
ConcurrentDictionary
:利用线程安全的集合实现类似乐观锁的逻辑。- 数据库:通过版本号或时间戳字段在数据库层面实现乐观锁。
选择哪种方式取决于具体的应用场景和需求。