跳到主要内容

悲观锁和乐观锁的区别

· 阅读需 3 分钟
素明诚
Full stack development

悲观锁和乐观锁是数据库管理和并发编程中用来控制数据一致性防止数据冲突的两种策略

悲观锁(Pessimistic Locking)

  • 核心 悲观锁假设最坏的情况,认为数据在并发修改时一定会发生冲突,因此在数据处理前就锁定数据,直到事务完成才释放锁
  • 应用场景 适用于写操作多、冲突概率高的环境,如银行账户余额更新

乐观锁(Optimistic Locking)

  • 核心 乐观锁假设冲突发生的可能性较低,因此不会立即锁定数据,而是在数据提交更新时检查数据在读取后是否被修改过(通常通过版本号或时间戳实现)
  • 应用场景 适用于读操作多、冲突概率低的环境,如一些只偶尔更新的数据查询系统

为什么会有这两种锁

这两种锁的存在是为了处理并发情况下的数据一致性和系统性能的平衡 悲观锁提供较强的数据一致性保证,但可能降低系统的并发性能;乐观锁在并发性能上表现更好,但在高冲突环境下可能导致更多的重试和失败 选择哪种锁策略取决于系统的具体需求和预期的并发冲突情况

两种锁在 Go 中的实现方式

悲观锁

在 Go 中,悲观锁通常可以通过使用 sync.Mutexsync.RWMutex 来实现 这些锁在访问共享资源前必须先获得锁,并在访问完成后释放锁 这样可以确保同一时间内只有一个 goroutine 能修改该资源

package main

import (
"fmt"
"sync"
)

var (
balance int
mutex sync.Mutex
)

func Deposit(amount int) {
mutex.Lock() // 获取锁
balance += amount // 修改共享资源
mutex.Unlock() // 释放锁
}

func main() {
balance = 100
fmt.Println("Initial balance:", balance)
Deposit(50)
fmt.Println("New balance:", balance)
}

使用 sync.Mutex 来确保在修改账户余额时不会发生数据竞争 这是一种悲观锁的实现,因为它假设必然会有冲突

乐观锁

乐观锁在 Go 中可以通过原子操作或自定义逻辑来实现

package main

import (
"fmt"
"sync"
"sync/atomic"
)

var (
balance int64
)

func Deposit(amount int64) {
for {
oldBalance := atomic.LoadInt64(&balance) // 读取当前余额
newBalance := oldBalance + amount // 计算新余额
// 尝试更新余额,如果期间余额未被其他 goroutine 修改,则更新成功
if atomic.CompareAndSwapInt64(&balance, oldBalance, newBalance) {
return
}
// 如果数据被修改过,循环重试
}
}

func main() {
atomic.StoreInt64(&balance, 100) // 初始余额
fmt.Println("Initial balance:", balance)
Deposit(50)
fmt.Println("New balance:", balance)
}

使用 atomic.CompareAndSwapInt64 函数来确保在更新余额时,如果没有其他 goroutine 修改过余额,则更新操作会成功 如果余额在更新前被其他 goroutine 修改了,则重试操作,直到成功 这是乐观锁的一种实现方式,它假设冲突发生的可能性较低