Golang 基于 MySQL 实现分布式读写锁

小岛来信
小岛来信
发布时间:2026-10-15| 6人浏览过
原创

本文介绍如何在 Golang 项目中利用 MySQL 实现分布式读写锁。通过结合 GORM 框架与原子更新的 SQL 语句,在不引入 Redis 或 Zookeeper 等额外组件的情况下,达成读操作共享、写操作互斥的并发控制目标。

业务场景与核心需求

对于刚上线且暂不引入其他中间件的项目,可以直接复用现有的 MySQL 数据库来实现分布式读写锁。该方案适用于以下分布式环境:

  • 分布式的多节点运行:不同的服务实例运行在不同的机器或容器节点中。
  • 读操作的共享性:允许同时存在多个 Goroutine 对同一资源进行读操作,无需阻塞。
  • 写操作的互斥性:同一时刻仅允许一个 Goroutine 对资源执行写操作。
  • 读写互斥:当资源处于写锁状态时,禁止任何读操作;反之亦然(读锁不影响读,但会阻塞写)。

什么是分布式读写锁

在单节点环境中,Golang 原生的 sync.Mutex 足以处理并发安全问题。然而,当应用部署在分布式架构下时,单机锁无法跨进程通信,此时需要依赖数据库或外部缓存构建分布式锁。分布式读写锁相比单纯的排他锁,粒度更细,提供了更灵活的状态管理:

  • 读模式(ReadLock):资源共享,多个客户端可并行获取。
  • 写模式(WriteLock):资源互斥,独占访问。
  • 无锁模式(Unlock):空闲状态,接受任何请求。

访问原则与控制逻辑

分布式读写锁遵循以下严格的访问控制原则:

当前锁状态申请读锁 (RLock)申请写锁 (WLock)
无锁 (Unlock)允许允许
读锁 (ReadLock)允许(共享读取)禁止(需等待所有读释放)
写锁 (WriteLock)禁止(等待写完成)禁止(已被写入者占用)

读锁机制详解

  • 获取条件:只要当前状态不是“写锁”,即可尝试获取读锁。
  • 共享策略:在“读锁”状态下,后续请求仍可获取读锁,系统会自动累加计数器。
  • 阻塞逻辑:只要有读锁存在,写锁请求必须等待所有读锁解锁后,才能转为写锁状态。

写锁机制详解

  • 获取条件:只有当前状态严格为“无锁”时,才允许获取写锁。
  • 独占策略:一旦获取写锁,将阻塞后续所有的读锁和写锁请求。

GORM 连接与数据表设计

第1步:配置数据库连接与结构体

首先需要通过 GORM 建立连接并定义模型。核心在于设计包含锁状态标记和计数器字段的结构体。

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

var (
	db *gorm.DB

	// 配置信息
	dbUsername = "kele"
	dbPassword = "baishi2020"
	dbHost     = "127.0.0.1:7306"
	dbDatabase = "lingmo"

	// 状态常量
	stateReadLock  = "ReadLock"
	stateWriteLock = "WriteLock"
	stateUnlock    = "Unlock"
)

// RWLock 定义分布式读写锁的核心字段
type RWLock struct {
	LockMark      string `gorm:"default:'Unlock'"` // 锁状态标识
	ReadLockCount uint32 `gorm:"default:0"`        // 活跃读锁计数器
	LockReason    string                        // 加锁原因记录
}

// Stock 模拟库存业务数据表
type Stock struct {
	gorm.Model
	RWLock
	Count int64
}

func (Stock) TableName() string {
	return "stocks"
}

// 初始化数据库连接与迁移
func init() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUsername, dbPassword, dbHost, dbDatabase)

	mysqlConfig := mysql.Config{DSN: dsn}
	gormConfig := &gorm.Config{Logger: logger.Default.LogMode(logger.Info)}

	var err error
	if db, err = gorm.Open(mysql.New(mysqlConfig), gormConfig); err != nil {
		panic(err)
	}

	// 设置引擎为 InnoDB 以支持事务与行锁
	db.Set("db:table_options", "ENGINE = InnoDB DEFAULT CHARSET = utf8")

	// 自动创建/迁移表结构
	if err = db.AutoMigrate(&Stock{}); err != nil {
		panic(err)
	}
}

func main() {
	// 初始化测试数据
	if result := db.Model(&Stock{}).Save(&Stock{Model: gorm.Model{}, RWLock: RWLock{}, Count: 10}); result.Error != nil {
		panic(result.Error)
	}
}

在上述结构中:LockMark 用于标识当前是读锁、写锁还是无锁;ReadLockCount 用于统计当前并发的读请求数量,是实现共享读的关键;LockReason 记录触发锁的原因,便于调试与审计。

第2步:实现读锁 (RLock/RUnlock)

实现读锁时,关键在于利用 SQL 表达式原子性地增加计数器,并通过条件过滤排除写锁状态。

func (s Stock) RLock(db *gorm.DB, lockReason string) error {
	// 条件:指定ID且当前不是写锁状态
	condition := "(id = ?) AND (lock_mark != ?)"
	fields := map[string]interface{}{
		"lock_mark":       stateReadLock,
		"read_lock_count": gorm.Expr("read_lock_count + ?", 1), // 原子自增
		"lock_reason":     lockReason,
	}

	result := db.Model(&Stock{}).Where(condition, s.ID, stateWriteLock).Updates(fields)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return fmt.Errorf("failed to rlock Stock, RowsAffected=0 (likely blocked by write lock)")
	}

	return nil
}

func (s Stock) RUnlock(db *gorm.DB, unlockReason string) error {
	// 使用原生 SQL 处理复杂的 IF 逻辑:判断计数归零后再清除状态
	sql := fmt.Sprintf(`
	UPDATE stocks 
	SET read_lock_count=IF(read_lock_count>0,read_lock_count-1,0), 
	    lock_mark=IF(read_lock_count<1, 'Unlock', 'ReadLock'),
	    lock_reason = '%s' 
	WHERE id = %d AND lock_mark='%s'`, unlockReason, s.ID, stateReadLock)
	
	result := db.Exec(sql)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return fmt.Errorf("failed to RUnlock Stock, RowsAffected=0")
	}

	return nil
}

// 调用示例
func testRUnlock(s Stock) {
	_ = s.RLock(db, "readLock_reason_1")
	_ = s.RLock(db, "readLock_reason_2")
	// 并发读成功...
	_ = s.RUnlock(db, "readLock_unlock_1")
	_ = s.RUnlock(db, "readLock_unlock_2")
}

生成的 SQL 分析:

  • 加读锁:如果当前状态非 WriteLock,则直接更新为 ReadLock 并将计数+1。
  • 解除读锁:若计数减1后仍大于0,状态维持 ReadLock;若计数归零,则状态转为 Unlock。

第3步:实现写锁 (WLock/WUnlock)

写锁具有排他性,要求当前必须是完全的无锁状态(Unlock),且成功后需强制重置读锁计数。

func (s Stock) WLock(db *gorm.DB, lockReason string) error {
	// 只有完全无锁状态才能获取写锁
	condition := "(id = ?) AND (lock_mark = ?)"
	fields := map[string]interface{}{
		"lock_mark":       stateWriteLock,
		"read_lock_count": 0,             // 写锁介入,清除读计数
		"lock_reason":     lockReason,
	}

	result := db.Model(&Stock{}).Where(condition, s.ID, stateUnlock).Updates(fields)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return fmt.Errorf("failed to WLock Stock, likely blocked by existing locks")
	}

	return nil
}

func (s Stock) WUnlock(db *gorm.DB, unlockReason string) error {
	condition := "(id = ?) AND (lock_mark = ?)"
	fields := map[string]interface{}{
		"lock_mark":       stateUnlock,
		"read_lock_count": 0,
		"lock_reason":     unlockReason,
	}

	result := db.Model(&Stock{}).Where(condition, s.ID, stateWriteLock).Updates(fields)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return fmt.Errorf("failed to WUnlock Stock, RowsAffected=0")
	}

	return nil
}

// 调用示例
func testWLock(s Stock) {
	_ = s.WLock(db, "writeLock_reason_1")
	// 进行独占写操作...
	_ = s.WUnlock(db, "unlockWrite_reason_1")
}

生成的 SQL 分析:

  • 加写锁:严格匹配 LockMark='Unlock',成功后将 LockMark 改为 WriteLock,Count 清零。
  • 解除写锁:仅在 LockMark='WriteLock' 时执行回退。

总结

虽然市面上存在 Etcd、Redis 等成熟的分布式协调组件,但在轻量级或初期场景中,利用 Golang 配合 MySQL 实现简单的分布式读写锁是一种务实的选择。该方法避免了额外的基础设施维护成本,借助数据库的行级锁特性保障了跨节点并发下的数据一致性与正确性。

以上就是基于 Golang 和 MySQL 实现分布式读写锁的详细内容,更多关于 Golang 高并发控制与数据库锁设计的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。