本帖最后由 妙笔生花 于 2023-9-1 22:20 编辑
实现 悲观锁 的方式有哪些?
悲观并发控制一般采用行锁、表锁等 排它性对资源进行锁定,确保一个时间点只有一个用户在操作被锁定的资源。
数据库提供的事务
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id) return BadRequest();
// 使用悲观锁
var existingTodoItem = await _context.TodoItems
.FirstOrDefaultAsync(t => t.Id == id,
new QueryOptions<TodoItem>().WithLock(LockType.Update));
if (existingTodoItem == null) return NotFound();
// 更新数据
existingTodoItem.Name = todoItem.Name;
existingTodoItem.Description = todoItem.Description;
try await _context.SaveChangesAsync();
catch (DbUpdateConcurrencyException)
{
// 处理并发冲突
return Conflict();
}
return NoContent();
}
使用Monitor类
Monitor类是.NET中用于实现互斥锁的类,可以使用它来实现悲观锁。通过调用Monitor.Enter方法获取锁,在临界区内执行操作,然后调用Monitor.Exit方法释放锁。这样可以确保同一时间只有一个线程可以进入临界区。
private static readonly object _lockObject = new object();
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id) return BadRequest();
lock (_lockObject)
{
_context.Entry(todoItem).State = EntityState.Modified;
_context.SaveChanges();
}
return NoContent();
}
使用了一个静态的lockObject作为锁对象。在执行更新操作之前,使用lock关键字来获取锁,确保同一时间只有一个线程可以进入临界区。在临界区内执行更新操作,并调用SaveChanges方法来保存更改。然后,释放锁,让其他线程可以继续访问。
注意:避免死锁的情况,确保在获取锁后能够正常释放锁。避免过度使用锁,以免影响性能。
使用Mutex类
Mutex类是.NET中用于实现互斥锁的类,可以使用它来实现悲观锁。通过创建一个命名的Mutex实例,在临界区内调用WaitOne方法获取锁,执行操作,然后调用ReleaseMutex方法释放锁。这样可以确保同一时间只有一个线程可以获取到命名的Mutex实例。
private static readonly Mutex _mutex = new Mutex();
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id) return BadRequest();
_mutex.WaitOne();
try
{
_context.Entry(todoItem).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
finally
{
_mutex.ReleaseMutex();
}
return NoContent();
}
使用了一个静态的Mutex对象来实现锁。在执行更新操作之前,调用WaitOne方法获取锁,确保同一时间只有一个线程可以进入临界区。在临界区内执行更新操作,并调用SaveChangesAsync方法来保存更改。然后,使用ReleaseMutex方法释放锁,让其他线程可以继续访问。
注意:避免死锁的情况,确保在获取锁后能够正常释放锁。避免过度使用锁,以免影响性能。
使用Semaphore类
Semaphore类是.NET中用于实现信号量的类,可以使用它来实现悲观锁。通过创建一个Semaphore实例,设置初始计数器值为1,在临界区内调用WaitOne方法获取锁,执行操作,然后调用Release方法释放锁。这样可以确保同一时间只有一个线程可以获取到信号量。
在类的顶部声明一个Semaphore对象:
private static SemaphoreSlim semaphore = new SemaphoreSlim(1);
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id) return BadRequest();
await semaphore.WaitAsync();
try
{
_context.Entry(todoItem).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
finally
{
semaphore.Release();
}
}
在方法开始时,调用semaphore.WaitAsync()来获取信号量,如果没有其他线程正在访问该方法,则会立即获取到信号量。如果有其他线程正在访问该方法,则会被阻塞,直到有一个线程释放了信号量。
在方法结束时,使用semaphore.Release()释放信号量,以便其他线程可以获取到信号量并访问该方法。
这样就可以确保在同一时间只有一个线程可以访问PutTodoItem方法,从而解决并发冲突的问题。
使用分布式锁
如果应用程序部署在多台服务器上,并且需要对共享资源进行并发控制,可以使用分布式锁来解决并发冲突。可以使用第三方库如RedLock.NET或DistributedLock等来实现分布式锁。
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
// 使用Redis分布式锁
using (var distributedLock = await _distributedLockProvider.AcquireLockAsync($"todoitem:{id}"))
{
if (!distributedLock.IsAcquired)
{
// 无法获取锁,返回冲突错误
return Conflict();
}
if (id != todoItem.Id) return BadRequest();
_context.Entry(todoItem).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
}
使用了一个名为_distributedLockProvider的分布式锁提供程序来获取一个名为 todoitem:{id} 的锁。如果无法获取锁,则返回冲突错误。如果成功获取锁,则继续执行更新操作,并在完成后释放锁。
注意,_distributedLockProvider和AcquireLockAsync是示例中的占位符。需要根据使用的分布式锁库和实际情况进行适当的更改。
注意
悲观并发控制的使用比较简单,仅对要进行并发控制的资源加上锁即可,但是这种锁是独占排它的,如果系统并发量很大,锁会严重影响性能,如果使用不当,甚至会导致死锁。因此,对于高并发系统,要尽量避免使用悲观策略,改为NoSQL(Redis)。如果必须使用数据库控制并发,尽量采用乐观并发控制,因为乐观并发控制不会阻塞其他线程的访问,并且可以更好地处理并发冲突。
|