Go并发编程核心:Mutex和RWMutex的用法

文章目录

在 Go 语言并发编程中, 处理共享资源竞争 是绕不开的话题。想象一下: 10 个 goroutine 同时给一个计数器加 1,最后结果却不是预期的 10 万 ------ 这就是典型的并发安全问题。针对这个问题,Go语言中有两类锁: Mutex(互斥锁) RWMutex(读写锁),这两类锁到底有什么区别,以及具体的应用场景是什么样的?

在Go语言中,当多个 goroutine 同时操作共享资源 (比如全局变量、数据库连接、缓存)时,由于 CPU 调度的随机性,可能导致操作 "交错执行"。比如count++看似简单,实际包含 "读取 - 修改 - 写入" 三个步骤,一旦被打断就会出现数据错误。这里面有几个关键定义:

  • 临界区 :需要被保护的共享资源操作代码(比如count++
  • 互斥锁(Mutex):保证临界区同一时间只有一个 goroutine 执行
  • 读写锁(RWMutex):区分读操作和写操作,支持多读单写,提升读多写少场景的性能

Mutex和RWMutex

Mutex 是 Go 标准库sync包中的核心原语,是最常用的基础锁,核心就两个方法:Lock()(加锁)和Unlock()(解锁)。

Mutex基础用法:解决计数器问题

先看一个不加锁的例子,实现一个计数器:

go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	var count = 0
	var wg sync.WaitGroup
	wg.Add(10)

	// 10个goroutine各加1万次
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 10000; j++ {
				count++ // 非原子操作,存在并发安全问题
			}
		}()
	}

	wg.Wait()
	fmt.Println("结果:", count) // 大概率小于100000
}

运行后结果往往小于 10 万,这就是数据竞争导致的问题。用 Mutex 修复只需 3 步:

  1. 声明 Mutex 变量
  2. 临界区前加Lock()
  3. 临界区后加Unlock()(建议用defer保证释放)

修复后的核心代码:

go 复制代码
var count = 0
	var wg sync.WaitGroup
	var mu sync.Mutex // 声明互斥锁
	wg.Add(10)

	// 10个goroutine各加1万次
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 10000; j++ {
				// 加锁保护临界区
				mu.Lock()
				count++
				mu.Unlock()
			}
		}()
	}

	wg.Wait()
	fmt.Println("加锁实现一个计数器的结果:", count) //稳定输出 100000

再次运行,结果稳定为 100000,完美解决并发安全问题。

Mutex 的进阶用法

(1)嵌入结构体

实际开发中,常把 Mutex 嵌入自定义结构体,让锁和资源绑定。

如下示例会启动 100 个 goroutine,每个 goroutine 对计数器进行 1000 次递增操作。通过 sync.WaitGroup 等待所有 goroutine 完成后,打印最终的计数值。

go 复制代码
// Counter 是一个线程安全的计数器
type Counter struct {
	mu    sync.Mutex
	count uint64
}

// Incr 对计数器进行加1操作
func (c *Counter) Incr() {
	c.mu.Lock() //使用了 `sync.Mutex` 进行保护,所以程序是线程安全的
	defer c.mu.Unlock()
	c.count++
}

// Get 获取当前计数器的值
func (c *Counter) Get() uint64 {
	c.mu.Lock() //使用了 `sync.Mutex` 进行保护,所以程序是线程安全的
	defer c.mu.Unlock()
	return c.count
}

func main() {
	// 1. 初始化一个 Counter 实例
	var counter Counter

	// 2. 使用 sync.WaitGroup 来等待所有 goroutine 完成
	var wg sync.WaitGroup

	// 3. 定义要启动的 goroutine 数量和每个 goroutine 的迭代次数
	const numGoroutines = 100
	const iterationsPerGoroutine = 1000

	// 4. 启动多个 goroutine 并发地增加计数器
	for i := 0; i < numGoroutines; i++ {
		// 为每个 goroutine 增加 WaitGroup 的计数
		wg.Add(1)

		// 启动 goroutine
		go func(goroutineID int) {
			// 在 goroutine 退出时,将 WaitGroup 的计数减 1
			defer wg.Done()

			// 每个 goroutine 执行 1000 次递增
			for j := 0; j < iterationsPerGoroutine; j++ {
				counter.Incr()
			}

			// 可选:打印每个 goroutine 完成的信息
			// fmt.Printf("Goroutine %d finished. Counter is now: %d\n", goroutineID, counter.Get())
		}(i) // 注意:这里将循环变量 i 作为参数传入,避免闭包引用问题
	}

	// 5. 等待所有 goroutine 完成它们的工作
	wg.Wait()

	// 6. 获取并打印最终的计数器值
	finalCount := counter.Get()
	fmt.Printf("\n最终的计数器值: %d\n", finalCount)

	// 7. 验证结果是否正确
	expectedCount := uint64(numGoroutines * iterationsPerGoroutine)
	fmt.Printf("验证结果是否正确: %v\n", expectedCount == finalCount)
}

由于 CounterIncrGet 方法都使用了 sync.Mutex 进行保护,所以程序是线程安全的。无论你运行多少次,输出结果都应该是:

最终的计数器值: 100000

验证结果是否正确: true

如果你注释掉 IncrGet 方法中的 c.mu.Lock()c.mu.Unlock() 代码,程序就会存在数据竞争 。此时,多个 goroutine 会同时读写 count 变量,导致最终的计数值总是小于预期的 100000,并且每次运行的结果都可能不同。

最终的计数器值: 51517

验证结果是否正确: false

(2)零值可用

Mutex 的零值就是未加锁状态,无需额外初始化,直接声明即可使用:

go 复制代码
var mu sync.Mutex // 直接使用,无需New
mu.Lock()
// 业务逻辑
mu.Unlock()

在 Go 语言中,当你声明一个变量但没有为它赋初始值时,它会被赋予该类型的零值

  • 对于 int,零值是 0
  • 对于 string,零值是 ""
  • 对于 bool,零值是 false

对于 sync.Mutex 这种结构体类型,它的零值是一个合法的、可用的、处于未加锁状态的互斥锁 。这意味着你可以直接声明变量并立即使用它的方法,无需像其他语言或某些 Go 类型那样,需要调用一个 New 函数或进行显式初始化。

sync.Mutex 的结构体定义如下:

go 复制代码
type Mutex struct {
    state int32 //这是一个 32 位整数,用来表示锁的当前状态,比如是否已被锁定、是否有 goroutine 在等待等
    sema  uint32 //这是一个信号量,用于 goroutine 之间的同步
}

当你声明 var mu sync.Mutex 时,mu 的 state 字段会被初始化为 0,sema 字段也会被初始化为 0。

Mutex的TryLock

有时候我们不想阻塞等待锁(也就是 非阻塞获取锁),获取不到就直接放弃,这时候可以使用Mutex的TryLock:

定义如下:

go 复制代码
func (m *Mutex) TryLock() bool {
	old := m.state
	if old&(mutexLocked|mutexStarving) != 0 {
		return false
	}

	// There may be a goroutine waiting for the mutex, but we are
	// running now and can try to grab the mutex before that
	// goroutine wakes up.
	if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
		return false
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
	return true
}

以下案例展示 TryLock 的核心用法:非阻塞地尝试获取锁,获取不到就立即返回。

go 复制代码
func main() {
    var mu sync.Mutex
    var wg sync.WaitGroup

    // 定义一个尝试获取锁并执行任务的函数
    tryTask := func(id int) {
        defer wg.Done()
        fmt.Printf("Goroutine %d: 尝试获取锁...\n", id)
        
        // 尝试非阻塞地获取锁
        if mu.TryLock() {
            // 成功获取到锁
            fmt.Printf("Goroutine %d: 成功获取锁,开始执行任务...\n", id)
            time.Sleep(2 * time.Second) // 模拟任务执行
            fmt.Printf("Goroutine %d: 任务执行完毕,释放锁。\n", id)
            mu.Unlock() // 释放锁
        } else {
            // 未能获取到锁
            fmt.Printf("Goroutine %d: 获取锁失败,任务被跳过。\n", id)
        }
    }

    wg.Add(2)
    go tryTask(1)
    go tryTask(2)

    wg.Wait()
    fmt.Println("所有任务处理完毕。")
}

运行效果示例:

Goroutine 2: 尝试获取锁...

Goroutine 2: 成功获取锁,开始执行任务...

Goroutine 1: 尝试获取锁...

Goroutine 1: 获取锁失败,任务被跳过。

Goroutine 2: 任务执行完毕,释放锁。

所有任务处理完毕。

用 Mutex 实现线程安全队列

结合 Mutex 和切片,可以实现简单的线程安全队列:

go 复制代码
// SafeQueue 一个使用 sync.Mutex 实现的线程安全队列
type SafeQueue struct {
	mu    sync.Mutex
	data  []interface{}
}

// 入队: 将一个元素添加到队列尾部
func (q *SafeQueue) Enqueue(item interface{}) {
	q.mu.Lock()
	defer q.mu.Unlock()
	q.data = append(q.data, item)
}

// 出队: 从队列头部移除并返回一个元素; 如果队列为空,返回一个错误
func (q *SafeQueue) Dequeue() interface{} {
	q.mu.Lock()
	defer q.mu.Unlock()
	if len(q.data) == 0 {
		return nil
	}
	item := q.data[0]
	q.data = q.data[1:]
	return item
}

RWMutex的用法

Mutex 不管在读的场景还是写的场景都属于排它锁,在 读多写少的 场景中(比如缓存查询、配置读取)中会浪费性能,因为多个读操作其实可以同时进行。这时候就需要用到 RWMutex 了。

RWMutex 基于 Mutex 实现,它的设计原则是写优先:当有写者等待时,新的读者会被阻塞。

核心字段包括:

  • w:保护写者竞争的 Mutex
  • readerCount:读者数量(负值表示有写者等待)
  • readerWait:写者等待的读者数量
  • 两个信号量:用于阻塞和唤醒

核心方法分为读锁和写锁两组:

  • 写锁:Lock() / Unlock()(排他锁,同一时间只能有一个写者)
  • 读锁:RLock() / RUnlock()(共享锁,同一时间可以有多个读者)

实战示例:我们来模拟一个简单的电商场景:

  1. 创建/修改/删除 商品 :这是写操作,需要用写锁保护,因为它会修改共享数据(商品列表)。
  2. 查询商品信息 :这是读操作,可以用读锁保护,因为它只是读取数据,不会修改。在高并发下,多个用户可以同时查询,不会相互阻塞。
go 复制代码
// Product 代表一个商品
type Product struct {
	ID    int
	Name  string
	Price float64
}

// ProductStore 是一个线程安全的商品存储
type ProductStore struct {
	rwmu     sync.RWMutex
	products map[int]Product
}

func NewProductStore() *ProductStore {
	return &ProductStore{
		products: make(map[int]Product),
	}
}

// CreateProduct 创建商品 (写操作)
func (ps *ProductStore) CreateProduct(p Product) {
	ps.rwmu.Lock()
	defer ps.rwmu.Unlock()
	time.Sleep(10 * time.Millisecond) // 模拟数据库延迟
	ps.products[p.ID] = p
	fmt.Printf("[CREATE] 商品 '%s' (ID: %d) 已上架。\n", p.Name, p.ID)
}

// UpdateProduct 修改商品价格 (写操作)
func (ps *ProductStore) UpdateProduct(id int, newPrice float64) bool {
	ps.rwmu.Lock()
	defer ps.rwmu.Unlock()
	time.Sleep(10 * time.Millisecond) // 模拟数据库延迟

	p, exists := ps.products[id]
	if !exists {
		fmt.Printf("[UPDATE] 商品 ID: %d 不存在,更新失败。\n", id)
		return false
	}

	p.Price = newPrice
	ps.products[id] = p
	fmt.Printf("[UPDATE] 商品 %s (ID: %d) 价格已更新为: %.2f。\n", p.Name, id, newPrice)
	return true
}

// DeleteProduct 删除商品 (写操作)
func (ps *ProductStore) DeleteProduct(id int) bool {
	ps.rwmu.Lock()
	defer ps.rwmu.Unlock()
	time.Sleep(10 * time.Millisecond) // 模拟数据库延迟

	if _, exists := ps.products[id]; !exists {
		fmt.Printf("[DELETE] 商品 ID: %d 不存在,删除失败。\n", id)
		return false
	}

	delete(ps.products, id)
	fmt.Printf("[DELETE] 商品 ID: %d 已下架。\n", id)
	return true
}

// GetProduct 查询单个商品 (读操作)
func (ps *ProductStore) GetProduct(id int) (Product, bool) {
	ps.rwmu.RLock()
	defer ps.rwmu.RUnlock()
	time.Sleep(5 * time.Millisecond) // 模拟数据库延迟

	p, exists := ps.products[id]
	if exists {
		fmt.Printf("[QUERY] 用户查询到商品: %s (ID: %d), 价格: %.2f。\n", p.Name, p.ID, p.Price)
	} else {
		fmt.Printf("[QUERY] 用户查询商品 ID: %d 不存在。\n", id)
	}
	return p, exists
}

func main() {
	store := NewProductStore()
	var wg sync.WaitGroup

	// 1. 初始创建几个商品
	fmt.Println("--- 阶段 1: 初始化商品 ---")
	initialProducts := []Product{
		{ID: 1, Name: "iPhone17", Price: 7999.0},
		{ID: 2, Name: "苹果原装充电器", Price: 825.88},
	}
	for _, p := range initialProducts {
		wg.Add(1)
		go func(product Product) {
			defer wg.Done()
			store.CreateProduct(product)
		}(p)
	}
	wg.Wait()

	// 2. 模拟并发操作:查询、更新、删除
	fmt.Println("\n--- 阶段 2: 并发操作 ---")
	// 模拟多个用户并发查询
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			store.GetProduct(common.GenerateRandomNumber(1, 2)) // 随机查询商品
		}()
	}

	// 后台管理员更新商品价格
	wg.Add(1)
	go func() {
		defer wg.Done()
		time.Sleep(100 * time.Millisecond) // 稍微延迟,确保有查询在进行
		store.UpdateProduct(1, 6999.0)     // 给 iPhone17 降价
	}()

	// 另一个管理员删除商品
	wg.Add(1)
	go func() {
		defer wg.Done()
		time.Sleep(200 * time.Millisecond) // 再延迟一些
		store.DeleteProduct(2)             // 删除商品 苹果原装充电器
	}()

	// 更新和删除后,再进行一些查询,验证结果
	wg.Add(2)
	go func() {
		defer wg.Done()
		time.Sleep(300 * time.Millisecond)
		store.GetProduct(1) // 应该能查到更新后的价格
	}()
	go func() {
		defer wg.Done()
		time.Sleep(300 * time.Millisecond)
		store.GetProduct(2) // 应该查询不到,因为已被删除
	}()

	wg.Wait()
}
  • 写操作(CreateProduct :使用 Lock()。当管理员创建商品时,其他所有试图创建或查询商品的操作都会被阻塞,确保了商品数据在写入时的一致性。

  • 读操作(GetProduct :使用 RLock()。多个用户可以同时查询到商品信息,他们的查询操作是并发执行的,不会互相等待。这大大提高了系统在高并发读场景下的吞吐量和响应速度。

运行结果示例:

--- 阶段 1: 初始化商品 ---

CREATE\] 商品 '苹果原装充电器' (ID: 2) 已上架。 \[CREATE\] 商品 'iPhone17' (ID: 1) 已上架。 --- 阶段 2: 并发操作 --- \[QUERY\] 用户查询到商品: 苹果原装充电器 (ID: 2), 价格: 825.88。 \[QUERY\] 用户查询到商品: iPhone17 (ID: 1), 价格: 7999.00。 \[QUERY\] 用户查询到商品: iPhone17 (ID: 1), 价格: 7999.00。 \[QUERY\] 用户查询到商品: 苹果原装充电器 (ID: 2), 价格: 825.88。 \[QUERY\] 用户查询到商品: 苹果原装充电器 (ID: 2), 价格: 825.88。 \[UPDATE\] 商品 iPhone17 (ID: 1) 价格已更新为: 6999.00。 \[DELETE\] 商品 ID: 2 已下架。 \[QUERY\] 用户查询商品 ID: 2 不存在。 \[QUERY\] 用户查询到商品: iPhone17 (ID: 1), 价格: 6999.00。

细节和注意事项

在使用 Mutex和RWMutex 的时候,需要注意以下细节问题:

1. Lock/Unlock 未成对出现

  • 常见场景:if-else 分支中漏写 Unlock,重构时误删 Lock
  • 后果:死锁(漏 Unlock)或 panic(漏 Lock 却调用 Unlock)
  • 解决方案:始终用defer mu.Unlock(),紧跟在 Lock 之后
go 复制代码
// 正确写法
mu.Lock()
defer mu.Unlock() // 确保无论如何都会释放锁

if err != nil {
	return err // 无需手动解锁
}
// 业务逻辑

2. 复制已使用的锁

Mutex 和 RWMutex 都是有状态的,复制已加锁的实例会导致状态错乱,引发死锁。

错误示例:函数参数按值传递锁。当你将一个包含 Mutex 的结构体(如 Counter)按值传递给函数时,会创建该结构体的一个副本,其中也包括了 Mutex 的状态。这会导致锁的状态错乱,引发潜在的死锁或数据竞争。

go 复制代码
package main

import (
    "sync"
)

// Counter 一个包含互斥锁的计数器结构体
type Counter struct {
    mu    sync.Mutex
    count int
}

// foo 函数按值接收一个 Counter
// 错误:这会复制整个 Counter,包括内部的 mu
func foo(c Counter) {
    c.mu.Lock()   // 操作的是副本的锁
    c.count++
    c.mu.Unlock() // 释放的是副本的锁
}

func main() {
	var c Counter

	// 调用 foo 函数,会发生值拷贝
	for i := 0; i < 1000; i++ {
		foo(c)
	}

	// main 函数中的 c 和 foo 函数中的 c 是两个完全独立的实例
	// 对 foo 中副本的操作不会影响到 main 中的实例
	println(c.count) // 输出:0
}

解决方案:通过传递结构体的指针,所有函数调用都将操作同一个结构体实例及其内部的锁,从而保证了并发安全。

go 复制代码
// foo 函数接收一个 Counter 的指针
// 正确:所有操作都针对同一个 Counter 实例
func foo(c *Counter) {
    c.mu.Lock()   // 操作的是原始实例的锁
    c.count++
    c.mu.Unlock() // 释放的是原始实例的锁
}

func main() {
	var c Counter

	// 调用 foo 函数,传递 c 的地址
	for i := 0; i < 1000; i++ {
		foo(&c)
	}

	// main 函数中的 c 和 foo 函数中操作的 c 是同一个实例
	// foo 中的操作会正确地修改 main 中 c 的 count 值
	println(c.count) // 输出:1000
}

3. 重复加锁导致死锁

同一 goroutine 对同一个 sync.Mutex 多次调用 Lock(),会立即导致死锁。因为 Mutex 是互斥锁,当前 goroutine 已经持有锁,再次请求会阻塞自己,且没有其他 goroutine 会释放锁。

go 复制代码
// 错误示例:重入锁导致死锁
func foo(m *sync.Mutex) {
	m.Lock()
	bar(m) // 再次加锁,死锁!
	m.Unlock()
}

func bar(m *sync.Mutex) {
	m.Lock()
	// ...
	m.Unlock()
}

报错信息:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [sync.Mutex.Lock]:

4. 释放未加锁的锁

多余的 Unlock 或 RUnlock 会直接 panic,生产环境中一定要避免:

go 复制代码
// 错误示例:释放未加锁的锁
var mu sync.Mutex
mu.Unlock() // panic: sync: unlock of unlocked mutex

Mutex 和 RWMutex如何选择

特性 Mutex RWMutex
并发控制 单读单写 多读单写
性能(读多写少) 较低 较高
性能(写多) 较高 较低
复杂度 简单 复杂
适用场景 读写均衡、写操作多 读操作远多于写操作

实际项目中的使用建议:

  • 优先使用Mutex:读多写少用 RWMutex,读写均衡用 Mutex。在不确定的场景下,先使用Mutex,后期再根据性能需求考虑RWMutex。
  • 锁要精简:尽量减少临界区的代码,只保护必要的部分。
  • 使用defer:确保锁一定会被释放,避免忘记Unlock。始终遵循 "谁加锁谁释放",用 defer 保证解锁。
  • 避免锁嵌套:复杂的锁依赖关系容易导致死锁,避免复制已使用的锁、重入锁、释放未加锁的锁。
  • 监控锁竞争:使用pprof等工具监控锁的竞争情况,及时发现性能瓶颈。

以上示例代码参考:https://gitee.com/rxbook/go-demo-2025/tree/master/demo/mutex

对比MySQL和Redis的锁

我在之前的文章中简单描述过MySQL中锁的基本用法,可以参考以下文章:

这里说到Go语言内置的锁机制了,就再啰嗦两句,对比下。

MySQL 的锁与 Go 锁本质一致,只是面向的共享资源不同:Go 的锁保护内存中的变量 ,解决进程内 goroutine 的并发冲突;MySQL 的锁保护磁盘上的数据行/表,解决多客户端事务的并发冲突;在分布式系统中,MySQL 锁和 Redis 锁又常常用于解决跨服务的数据竞争问题。

那么,既然 Go 已经内置了高效的锁机制,为什么实际项目中还需要依赖 MySQL 或 Redis 锁?

对比维度 Go Mutex/RWMutex MySQL 锁(表锁/行锁/间隙锁) Redis 锁(SET NX/Redlock)
作用范围 单进程内的 goroutine 间 单 MySQL 实例内的会话 / 事务间 分布式系统的跨服务 / 跨机器节点间
性能开销 极低(内存操作,无网络延迟) 中低(依赖存储引擎,磁盘 IO 影响) 中(需网络通信,缓存操作高效)
适用场景 进程内共享资源(如内存缓存、计数器) 数据库数据的增删改查(事务安全) 分布式任务调度、跨服务库存扣减等
核心优势 简单高效、无额外依赖 天然支持事务、数据持久化 高可用、跨节点、性能优越
常见问题 无法跨进程、无超时自动释放 锁等待超时、死锁风险、性能瓶颈 网络分区风险、锁超时处理复杂
解锁保障 需手动解锁(defer 必用) 事务结束自动释放(Commit/Rollback) 需手动释放或设置过期时间

实际项目中的用法差异

1. Go内置的锁:进程内goroutine安全

代码示例:

go 复制代码
// 进程内线程安全的计数器(用 Mutex 保护临界区)
type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Incr() {
    c.mu.Lock()
    defer c.mu.Unlock() // 确保解锁
    c.count++ // 临界区仅保留必要操作
}

核心场景:单服务内的内存共享数据(如本地缓存更新、全局计数器),无需跨进程通信。

2. MySQL的锁:数据库事务安全

sql 复制代码
-- 行锁:更新商品库存(事务内自动加锁,提交后释放)
BEGIN;
SELECT stock FROM product WHERE id=1 FOR UPDATE; -- 悲观行锁,防止并发修改
UPDATE product SET stock=stock-1 WHERE id=1;
COMMIT; -- 事务结束自动解锁

核心场景:数据库层面的数据一致性(如订单创建、库存扣减),依赖事务 ACID 特性。

3. Redis的锁:分布式跨服务安全

go 复制代码
// Redis 分布式锁(基于 SET NX + EX 实现,Go 客户端示例)
func AcquireRedisLock(redisCli *redis.Client, key string, expire int) (bool, error) {
    // NX:仅当 key 不存在时设置,EX:自动过期(避免死锁)
    return redisCli.SetNX(context.Background(), key, "locked", time.Duration(expire)*time.Second).Result()
}

func ReleaseRedisLock(redisCli *redis.Client, key string) error {
    return redisCli.Del(context.Background(), key).Err()
}

核心场景:跨服务的资源竞争(如分布式定时任务、多服务共享库存)。

Go已有锁,为何还需MySQL/Redis 锁?

答案的核心是 "作用范围不同" ------ Go 锁解决的是 "进程内" 的并发安全,而 MySQL/Redis 锁解决的是 "跨进程 / 跨服务" 的并发安全,二者无法相互替代,具体分两种情况:

1. 单服务架构:优先用 Go 锁,MySQL 锁按需补充

  • 若仅需保护内存中的共享数据 (如本地缓存、全局变量),直接用 Mutex/RWMutex,性能最优且无额外依赖;
  • 若操作数据库数据(如更新用户余额),需用 MySQL 锁(或事务隔离级别)保障数据一致性,此时 Go 锁无法替代 ------ 因为数据库操作是跨会话的,进程内的 Go 锁无法控制其他进程(或同一进程的不同数据库连接)对数据的修改。

2. 分布式架构:必须用 MySQL/Redis 锁

当系统部署为多服务实例(如微服务集群、多机部署)时,Go 锁完全失效 ------ 因为不同服务实例运行在不同进程(甚至不同机器),进程内的锁无法跨节点同步。此时必须依赖分布式锁:

  • 例 1:电商秒杀场景,多服务实例同时扣减同一商品库存,需用 Redis 锁保证不会超卖;
  • 例 2:分布式定时任务,需用 Redis 锁避免多个服务实例重复执行同一任务;
  • 例 3:跨服务转账,需用 MySQL 锁(或分布式事务)保证资金一致性。

3. 补充:特殊场景的混合使用

  • 场景:单服务实例中,既需要操作本地缓存,又需要更新数据库;
  • 用法:用 Go 锁保护本地缓存的读写,用 MySQL 锁(或事务)保护数据库操作,二者各司其职。
相关推荐
散峰而望2 小时前
C++数组(一)(算法竞赛)
c语言·开发语言·c++·算法·github
疯狂的程序猴2 小时前
混淆 iOS 类名变量名,从符号隐藏到成品 IPA 混淆的工程化方案
后端
爱吃的小肥羊2 小时前
GPT-5.1-Codex-Max正式发布,超越Gemini 3,编程能力第一!(附使用方法)
后端·aigc·openai
郡杰2 小时前
Spring(2-IOC/DI管理第三方)
后端
洗澡水加冰2 小时前
MCP与Skills的辨析
后端·aigc·mcp
wjs20242 小时前
C++ 指针
开发语言
该用户已不存在2 小时前
Python正在死去,2026年Python还值得学吗?
后端·python
20岁30年经验的码农2 小时前
Java Sentinel流量控制与熔断降级框架详解
java·开发语言·sentinel
程序员西西2 小时前
SpringBoot轻松整合Sentinel限流
java·spring boot·后端·计算机·程序员