【后端开发】goland分布式锁的几种实现方式(mysql,redis,etcd,zookeeper,mq,s3)

【后端开发】goland分布式锁的几种实现方式(mysql,redis,etcd,zookeeper,mq,s3)

文章目录

1、分布式锁实现方案对比(mysql,redis,etcd,zookeeper,mq,s3)

技术 实现复杂度 性能 可靠性 适用场景 主要特点
Redis 高性能、高并发场景 基于内存,高性能;需要处理锁续期问题;可能出现脑裂问题
Etcd 中高 强一致性要求的场景 基于Raft协议,强一致性;自带租约机制;适合云原生环境
ZooKeeper 中高 复杂协调场景 基于ZAB协议,强一致性;watch机制完善;但运维成本较高
MySQL 低频、简单场景 实现简单;性能瓶颈明显;不适合高并发
MQ 中高 中高 异步、解耦场景 通过消息排他性实现;天然支持分布式;但不如专用锁工具直观
S3 特殊场景(如跨云) 基于对象存储的原子操作;延迟高;适合低频跨地域场景

分布式锁适用场景(排序)

  1. Redis - 最适合大多数场景,性能与功能平衡 (无脑选择)

    • 推荐库: redsync
    • 适用: 电商秒杀、缓存更新等高频场景
  2. Etcd - 强一致性要求的云原生场景首选 (云原生+刚好有etcd)

    • 推荐库: 官方concurrency包
    • 适用: K8s环境、服务注册发现相关锁需求
  3. ZooKeeper - 已有ZK基础设施的复杂协调场景 (刚好有...雾)

    • 推荐库: curator(Java)或go-zookeeper
    • 适用: Hadoop生态、传统分布式系统
  4. MQ - 已使用MQ且需要弱化锁概念的场景 (刚好只有...雾)

    • 推荐: RabbitMQ排他队列/Kafka partition
    • 适用: 异步任务调度、事件驱动架构
  5. MySQL - 简单低频场景的快速实现 (不咋用,刚好只有mysql)

    • 推荐: GET_LOCK函数
    • 适用: 低频管理操作、小型系统
  6. S3 - 特殊跨云/跨地域需求 (刚好只有S3)

    • 推荐: S3原子操作
    • 适用: 多云环境下的协调

特殊场景建议

  • 超高频场景(10万+/秒): Redis集群+Redlock算法
  • 金融级强一致: Etcd或ZooKeeper
  • 云原生环境: 首选Etcd
  • 已有MQ基础设施: 可考虑利用MQ特性实现简化架构
  • 简单低频系统: MySQL实现最方便

参考资料:12

2、锁的常见策略(乐观/悲观,可重入/不可重入,公平/非公平,自旋锁, 读写锁)

CAP定理(一致性Consistency、可用性Availability、分区容错性Partition Tolerance)

用锁的策略考虑

  1. 争用程度:高争用时应减少锁粒度
  2. 持有时间:长时间持有应使用可重入锁
  3. 公平性需求:平衡吞吐量与公平性

悲观锁

  • 特点
    默认认为并发冲突会发生
    访问资源前先加锁
    阻塞其他线程直到锁释放
    实现简单但并发性能较低
  • 适用场景
    冲突频率高的场景
    临界区执行时间较长
    需要强一致性保证

乐观锁

  • 特点
    默认认为冲突不会发生
    不上锁直接操作,提交时检查版本
    非阻塞,冲突时回滚或重试
    高并发性能好
  • 适用场景
    读多写少的环境
    冲突概率低的场景
    需要高吞吐量的系统
sql 复制代码
--- 乐观锁
--- 初始数据
id | name | version
1  | 张三 | 1


场景1:无并发冲突
T1: 服务A读取数据 (version=1)
T2: 服务A更新数据 (version=1 -> 2)
T3: 服务B读取数据 (version=2)
T4: 服务B更新数据 (version=2 -> 3)

场景2:有并发冲突
T1: 服务A读取数据 (version=1)
T2: 服务B读取数据 (version=1)
T3: 服务B更新数据 (version=1 -> 2)
T4: 服务A尝试更新数据 (version=1 -> 2) 失败!

分布式锁

  • 特性
    跨进程/跨机器互斥
    高可用性(避免单点故障)
    自动释放(防死锁)
    可重入性(可选)
  • 实现对比
    Redis:AP系统,高性能但不保证强一致
    Zookeeper/Etcd:CP系统,强一致但性能较低

可重入锁(递归锁)

  • 特点
    同一线程可重复获取已持有的锁
    需要记录持有线程和重入次数
    避免自死锁
  • 实现:
    etcd默认 concurrency包

不可重入锁

  • 特点
    严格互斥,即使同一线程也不能重复获取
    实现简单但易引发死锁

死锁

  • 四大条件与预防
    1 互斥条件, 破坏互斥
    2 请求与保持,破坏请求保持,资源有序分配法
    3 不可剥夺,允许资源抢占
    4 循环等待, 破坏循环等待
  • 实现措施
    设置超时时间(Redis锁的expire)
    资源有序申请(按固定顺序获取锁)
    使用tryLock而非lock

公平锁

  • 特点
    按申请顺序分配锁
    避免线程饥饿
    性能较低(需要维护队列)

非公平锁

  • 特点
    允许插队获取锁
    吞吐量更高
    可能导致线程饥饿

自旋锁

  • 适用场景:临界区非常小且CPU资源充足
c++ 复制代码
// C++原子操作实现自旋锁
std::atomic_flag lock = ATOMIC_FLAG_INIT;

void lock() {
    while(lock.test_and_set(std::memory_order_acquire));
}

void unlock() {
    lock.clear(std::memory_order_release);
}

读写锁

  • 特点:读共享,写互斥
java 复制代码
// Java读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读操作
rwLock.readLock().lock();
try {
    // 并发读
} finally {
    rwLock.readLock().unlock();
}

// 写操作
rwLock.writeLock().lock();
try {
    // 独占写
} finally {
    rwLock.writeLock().unlock();
}

3、基于redis,mysql的实现(缓存,AP-高性能)

Redis实现

  • AP系统:高可用和分区容忍优先,牺牲强一致性(如Redis集群可能出现脑裂)
  • 高性能:基于内存操作,TPS可达10万+级别
  • Redis集群脑裂是指在一个Redis集群中,由于网络分区导致集群被分裂成两个或多个部分,每个部分都认为自己是主节点(Master)的情况。
  • redsync/v4(Redsync)
    Redis 官方推荐的分布式锁库,基于 Redlock 算法 实现,支持单节点、多节点 Redis 模式,具备自动续期、容错处理等特性。
  • 特点
    支持单实例和 多实例(Redlock) 模式,多实例模式下通过多数节点加锁提升可靠性。
    自动处理锁的续期(Renewal),避免业务执行时间超过锁有效期导致的误释放。
    提供 Lock 和 RLock(可重入锁)接口,支持上下文取消(context.Context)。 Redlock参考:1, 2, 3
go 复制代码
// Redlock算法示例(多节点Redis)
err := redsync.New(redisPools).NewMutex("resource_lock").Lock()

// 使用 SET NX 命令
lockKey := "sync_lock"
lockValue := "server_id"
success, err := redis.SetNX(ctx, lockKey, lockValue, 5*time.Minute).Result()

手动实现

go 复制代码
package main

import (
	"context"
	"log"
	"math/rand"
	"time"

	"github.com/go-redis/redis/v9"
)

type DistLock struct {
	rdb        *redis.Client
	key        string
	value      string // 唯一标识(如 UUID)
	expiration time.Duration
	ctx        context.Context
}

// 新建分布式锁
func NewDistLock(rdb *redis.Client, key string, expiration time.Duration) *DistLock {
	return &DistLock{
		rdb:        rdb,
		key:        key,
		value:      rand.String(16), // 生成随机 UUID(示例简化)
		expiration: expiration,
		ctx:        context.Background(),
	}
}

// 加锁
func (l *DistLock) Lock() (bool, error) {
	ok, err := l.rdb.SetNX(l.ctx, l.key, l.value, l.expiration).Result()
	if err != nil {
		return false, err
	}
	return ok, nil
}

// 释放锁
func (l *DistLock) Unlock() (bool, error) {
	script := `if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end`
	result, err := l.rdb.Eval(l.ctx, script, []string{l.key}, l.value).Result()
	if err != nil {
		return false, err
	}
	return result.(int64) == 1, nil
}

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	lock := NewDistLock(rdb, "my-lock", 5*time.Second)

	// 尝试加锁
	ok, err := lock.Lock()
	if err != nil || !ok {
		log.Fatal("获取锁失败")
		return
	}
	defer lock.Unlock() // 确保释放锁

	// 模拟业务执行(可能超过锁过期时间,需手动处理续期)
	log.Println("执行业务...")
	time.Sleep(6 * time.Second) // 超过 5s 锁过期时间,可能导致其他进程获取锁
}

MySQL实现

  • 基于唯一索引或乐观锁(CAS)
  • 简单但性能低(TPS通常<1K)
  • 适用场景:低频管理操作、传统单体架构过渡期。
sql 复制代码
-- 悲观锁
SELECT * FROM table WHERE id=1 FOR UPDATE;

-- 乐观锁
UPDATE table SET stock=stock-1, version=version+1 
WHERE id=1 AND version=5;

4、基于etcd,zk的实现(分布式协调系统,CP-强一致性)

基于etcd的分布式锁实现

  • etcd是一个分布式、高可用的键值存储系统,主要用于共享配置和服务发现。由CoreOS开发,现为CNCF(云原生计算基金会)项目。
    etcd采用Raft一致性算法 保证数据一致性,支持分布式锁、观察者模式、TTL(生存时间)等特性,广泛应用于Kubernetes等云原生系统中。
  • etcd实现锁的核心是通过Create操作在KV存储中创建临时键值对。如果创建成功即获取锁,否则监听该键变化。锁释放时自动删除键值对。 1 2
  • 优点:
    强一致性
    自动过期
    适合分布式系统
go 复制代码
// etcd 手动实现
// etcd客户端初始化
cli, err := clientv3.New(clientv3.Config{
    Endpoints:   []string{"localhost:2379"},
    DialTimeout: 5 * time.Second,
})

// 尝试获取锁
lease := clientv3.NewLease(cli)
leaseGrantResp, err := lease.Grant(context.TODO(), 10) // 10秒租约
_, err = cli.Put(context.TODO(), "/lock/key", "", clientv3.WithLease(leaseGrantResp.ID))

// 锁续约
keepAliveCh, err := lease.KeepAlive(context.TODO(), leaseGrantResp.ID)

// 释放锁
lease.Revoke(context.TODO(), leaseGrantResp.ID)
go 复制代码
// etcd的concurrency包
func main() {
    // 创建一个etcd客户端
    client, err := clientv3.New(clientv3.Config{
       Endpoints:   []string{"localhost:2379"},
       DialTimeout: 5 * time.Second,
    })
    if err != nil {
       panic(err)
    }
    defer client.Close()

    // 创建一个分布式锁
    mutex := concurrency.NewMutex(client, "/lock/key")

    // 尝试获取锁
    err = mutex.Lock(context.Background())
    if err != nil {
       panic(err)
    }
    defer mutex.Unlock(context.Background())

    // 执行需要互斥访问的代码
    // ...
}

etcdctl工具 1

shell 复制代码
brew install etcd
etcdctl get --prefix foo

# 创建租约(TTL为10秒)
etcdctl lease grant 10
# 输出示例:lease 32695410dcc0ca06 granted with TTL(10s)

# 使用租约持有锁(写入一个键并绑定租约)
etcdctl put --lease=32695410dcc0ca06 /lock/resource "holder1"

# 续租
etcdctl lease keep-alive 32695410dcc0ca06
# 释放锁(删除键或撤销租约)
etcdctl lease revoke 32695410dcc0ca06

基于Zookeeper的分布式锁实现

java 复制代码
// 创建连接
ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, null);

// 创建临时有序节点
String lockPath = zk.create("/lock/key-", 
    new byte[0], 
    ZooDefs.Ids.OPEN_ACL_UNSAFE,
    CreateMode.EPHEMERAL_SEQUENTIAL);

// 检查是否为最小节点
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);

// 监听前一个节点
String watchNode = "/lock/" + children.get(Collections.binarySearch(children, lockPath)-1);
zk.exists(watchNode, new Watcher() {
    public void process(WatchedEvent event) {
        // 节点删除时重新检查锁状态
    }
});

// 释放锁
zk.delete(lockPath, -1);

etcd和zk对比
1

应用场景 etcd Zookeeper 细节差异
发布与订阅(配置中心) etcd API 简洁轻量;Zookeeper 需处理会话超时等细节,Java 生态适配好
软负载均衡 etcd 客户端主动拉取负载;Zookeeper 靠监听节点变化实现,传统架构适配性强
命名服务 etcd 查询高效,适配灵活场景;Zookeeper 适合强层级结构,如传统中间件集成
服务发现 etcd 天然适配云原生(如 K8s );Zookeeper 是 Dubbo 等传统框架默认依赖
分布式通知 / 协调 etcd 多 Key 监听、原子性优;Zookeeper 适合复杂协调逻辑,如集群状态同步
集群管理与 Master 选举 etcd 基于 Raft 算法,选主高效自动;Zookeeper 靠临时 / 有序节点,Java 生态成熟
分布式锁 etcd 轻量级、无锁竞争性能优;Zookeeper 阻塞锁易实现,高并发下节点创建开销略大
分布式队列 etcd 可扩展延时队列(结合 Lease );Zookeeper 天然支持阻塞队列,客户端完善

etcd和redis的对比 1

对比维度 etcd Redis
定位 分布式系统元数据存储、配置中心 内存数据库、高性能缓存、实时数据处理
数据存储 仅磁盘持久化(WAL + 快照) 内存为主(支持 RDB/AOF 持久化)
数据一致性 强一致性(Raft 共识算法) 最终一致性(可配置强一致性)
数据类型 仅简单键值对(String-Byte) 丰富数据类型(String/Hash/List 等)
分布式能力 原生支持(Raft 集群) 需手动配置(Cluster/Sentinel)
典型场景 Kubernetes 集群、分布式锁、配置管理 缓存、排行榜、消息队列、实时分析
读写性能 低频操作(数百次 / 秒,受磁盘限制) 高频内存操作(数万次 / 秒)
扩展性 节点数建议 3-5 个(Raft 限制) 支持动态扩缩容(数千节点)
生态集成 深度集成 Kubernetes 生态 通用型,适配多种框架(如 Spring、Node.js)

5、基于mq,s3等其他中间件的实现(特殊CAP)

MQ实现

  • 原理
    利用消息队列的排他性消费特性
    通过创建唯一队列或独占消费者实现互斥
    基于消息的ACK机制实现锁释放
  • 示例
    RabbitMQ : 使用排他队列(exclusive queue)或单活跃消费者
    Kafka : 使用partition的单一消费者机制
    RocketMQ: 使用MessageGroup保证同一分组消息被同一消费者处理
  • 优点:
    天然分布式特性
    与业务消息系统集成方便
    自带重试和持久化机制
  • 缺点:
    没有专门为锁设计,功能不直观
    锁释放依赖于消费者生命周期
    性能通常低于专用锁服务

S3实现:

  • 方案1:去s3持久化文件,在文件中记录当前锁的状态
  • 方案2:
    通过 PutObject 尝试创建一个唯一的锁文件(如以锁名称为键的对象),利用 IfNoneMatch 条件确保只有当文件不存在时才能创建成功(即获取锁)
    通过 DeleteObject 删除锁文件,利用 IfMatch 条件确保只有持有锁的客户端(通过存储在元数据中的唯一标识)才能删除,避免误删其他客户端的锁
  • 适用场景
    轻量级分布式锁需求:如简单的任务调度、资源编排等。
    跨云或多云环境:S3 兼容接口(如 MinIO)可统一不同云厂商的实现
    低成本场景:避免引入 Redis、ZooKeeper 等复杂中间件
优点 缺点
1. 利用 S3 原生接口,无需额外组件 1. 依赖 S3 服务的可用性和延迟
2. 支持跨区域分布式场景 2. 高频操作时 API 调用成本较高(需付费)
3. 天然支持持久化(锁状态存储在 S3 中) 3. 强一致性依赖 S3 的读一致性模型
4. 实现简单,适合轻量级场景 4. 不支持可重入锁