Go + Redis 实现轻量级分布式锁:实战指南

# Go + Redis 实现轻量级分布式锁:实战指南

分布式系统中,多个服务节点同时访问共享资源,怎么保证数据一致性?这是一个老生常谈的问题。单机环境下,语言自带的并发原语就能搞定,但到了分布式场景,得靠分布式锁出马。

## 背景介绍

分布式锁是分布式系统中保证资源互斥访问的机制。说白了,就是同一时刻只允许一个客户端操作某个资源。

举两个常见的例子。电商秒杀场景:某商品只剩一件库存,两个用户同时下单,如果不加锁,很可能卖出两件,超卖了。定时任务场景:多台机器跑同一个定时任务,不做协调的话,任务会被执行两次,数据全乱套。

目前主流的分布式锁方案有三种:Redis、ZooKeeper、数据库。Redis 方案最受欢迎,原因很简单:性能高、部署容易、成本低。Redis 的 SETNX 命令(SET if Not eXists)可以原子性地设置一个键,如果键不存在则设置成功,返回 true;如果键已存在,则设置失败。这个特性天然适合做分布式锁。

很多项目直接用 Redisson,这是 Redis 官方推荐的客户端,封装得很完善。但我觉得,理解底层原理同样重要。Redisson 底层也是用 SETNX 命令实现的,只是加了看门狗自动续期、公平锁这些功能。下面我们从零实现一个轻量级的分布式锁。

## 问题描述

开始写代码前,先说清楚我们要解决什么问题。分布式锁有几个基本要求:互斥性、可用性、释放安全性。

先看常见的错误实现。很多新手会这样写:

“`
if (redis.get(key) == null) {
redis.set(key, value);
// 处理业务
redis.del(key);
}
“`

先 GET 再 SET,这两个操作不是原子的。两个客户端同时执行这段代码,可能都判断键不存在,然后都设置了值,都去执行业务——这就完蛋了。

还有一个问题:锁的过期时间。设太长了吧,客户端崩溃后其他客户端要等很久;设太短了吧,任务还没跑完锁就放了,其他客户端进来又是重复执行。

释放锁也有讲究。直接用 DEL 命令删键,会把别人持有的锁也删掉。正确的做法是:先查一下这个锁是不是自己设置的,确认是自己设置的再删。查和删必须是原子操作。

## 详细步骤

一步一步来实现。

第一步:装依赖。使用 go-redis/redis v9,这是目前最流行的 Go Redis 客户端。

“`bash
go get github.com/redis/go-redis/v9
“`

第二步:定义分布式锁的结构体。

“`go
type DistributedLock struct {
client *redis.Client
lockKey string
lockValue string
expiration time.Duration
}
“`

lockKey 是锁的名字,lockValue 是锁的值,这个值用来判断锁是不是自己持有的。

第三步:获取锁。核心命令是 SET key value NX PX 30000。

– NX:只在键不存在时设置
– PX:设置过期时间,单位毫秒

“`
result := client.SetNX(ctx, lockKey, lockValue, expiration).Result()
“`

返回值是 true 表示获取锁成功,false 表示获取失败。

第四步:释放锁。用 Lua 脚本保证原子性:

“`lua
if redis.call(“get”, KEYS[1]) == ARGV[1] then
return redis.call(“del”, KEYS[1])
else
return 0
end
“`

只有锁的值等于自己设置的值时才删除,防止误删别人的锁。

第五步(可选):锁续期。任务执行时间超过锁的过期时间怎么办?可以用看门狗机制:持有锁的客户端每隔一段时间延长锁的过期时间。Redisson 默认每 10 秒检查一次,如果还在持有锁,就重置为 30 秒。

## 完整代码示例

直接上完整代码,基于 go-redis/redis v9。

“`go
package main

import (
“context”
“fmt”
“time”

“github.com/redis/go-redis/v9”
)

// DistributedLock 分布式锁结构体
type DistributedLock struct {
client *redis.Client
lockKey string
lockValue string
expiration time.Duration
}

// NewDistributedLock 创建分布式锁实例
func NewDistributedLock(client *redis.Client, lockKey string, lockValue string, expiration time.Duration) *DistributedLock {
return &DistributedLock{
client: client,
lockKey: lockKey,
lockValue: lockValue,
expiration: expiration,
}
}

// TryLock 尝试获取锁
func (dl *DistributedLock) TryLock(ctx context.Context) (bool, error) {
result, err := dl.client.SetNX(ctx, dl.lockKey, dl.lockValue, dl.expiration).Result()
if err != nil {
return false, fmt.Errorf(“failed to acquire lock: %w”, err)
}
return result, nil
}

// Unlock 释放锁
func (dl *DistributedLock) Unlock(ctx context.Context) (bool, error) {
script := redis.NewScript(`
if redis.call(“get”, KEYS[1]) == ARGV[1] then
return redis.call(“del”, KEYS[1])
else
return 0
end
`)

result, err := script.Run(ctx, dl.client, []string{dl.lockKey}, dl.lockValue).Int()
if err != nil {
return false, fmt.Errorf(“failed to release lock: %w”, err)
}
return result > 0, nil
}

// Extend 延长锁的过期时间
func (dl *DistributedLock) Extend(ctx context.Context, newExpiration time.Duration) (bool, error) {
script := redis.NewScript(`
if redis.call(“get”, KEYS[1]) == ARGV[1] then
return redis.call(“pexpire”, KEYS[1], ARGV[2])
else
return 0
end
`)

result, err := script.Run(ctx, dl.client, []string{dl.lockKey}, dl.lockValue, newExpiration.Milliseconds()).Int()
if err != nil {
return false, fmt.Errorf(“failed to extend lock: %w”, err)
}
return result > 0, nil
}

// AutoRenewalLock 带自动续期的分布式锁
type AutoRenewalLock struct {
*DistributedLock
renewalInterval time.Duration
ctx context.Context
cancel context.CancelFunc
}

// NewAutoRenewalLock 创建带自动续期的锁
func NewAutoRenewalLock(client *redis.Client, lockKey string, lockValue string, expiration time.Duration) *AutoRenewalLock {
ctx, cancel := context.WithCancel(context.Background())
return &AutoRenewalLock{
DistributedLock: NewDistributedLock(client, lockKey, lockValue, expiration),
renewalInterval: expiration / 3,
ctx: ctx,
cancel: cancel,
}
}

// TryLockWithAutoRenewal 获取锁并启动自动续期
func (arl *AutoRenewalLock) TryLockWithAutoRenewal(ctx context.Context) (bool, error) {
success, err := arl.TryLock(ctx)
if !success || err != nil {
return success, err
}

go arl.keepAlive()
return true, nil
}

// keepAlive 自动续期逻辑
func (arl *AutoRenewalLock) keepAlive() {
ticker := time.NewTicker(arl.renewalInterval)
defer ticker.Stop()

for {
select {
case <-ticker.C: success, err := arl.Extend(arl.ctx, arl.expiration) if err != nil || !success { return } case <-arl.ctx.Done(): return } } } // UnlockWithAutoRenewal 释放锁并停止自动续期 func (arl *AutoRenewalLock) UnlockWithAutoRenewal() error { arl.cancel() return nil } // 示例:模拟秒杀场景 func seckillExample() { client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) ctx := context.Background() productKey := "product:stock:1001" lockKey := "lock:seckill:1001" client.Set(ctx, productKey, 1, 0) for i := 0; i < 5; i++ { go func(id int) { lockValue := fmt.Sprintf("client-%d", id) lock := NewDistributedLock(client, lockKey, lockValue, 10*time.Second) success, err := lock.TryLock(ctx) if err != nil { fmt.Printf("Client %d: Error acquiring lock: %v\n", id, err) return } if !success { fmt.Printf("Client %d: Failed to acquire lock\n", id) return } fmt.Printf("Client %d: Lock acquired\n", id) stock, err := client.Get(ctx, productKey).Int() if err != nil { fmt.Printf("Client %d: Error getting stock: %v\n", id, err) return } if stock > 0 {
client.Decr(ctx, productKey)
fmt.Printf(“Client %d: Successfully purchased! Remaining stock: %d\n”, id, stock-1)
} else {
fmt.Printf(“Client %d: Out of stock\n”, id)
}

_, err = lock.Unlock(ctx)
if err != nil {
fmt.Printf(“Client %d: Error releasing lock: %v\n”, id, err)
} else {
fmt.Printf(“Client %d: Lock released\n”, id)
}
}(i)
}

time.Sleep(2 * time.Second)
}
“`

## 运行结果

运行上面的代码,你会看到类似这样的输出:

“`
Client 1: Lock acquired
Client 1: Successfully purchased! Remaining stock: 0
Client 1: Lock released
Client 0: Failed to acquire lock
Client 2: Failed to acquire lock
Client 3: Failed to acquire lock
Client 4: Failed to acquire lock
“`

Client 1 成功抢到了锁,买到了商品。其他客户端获取锁失败,只能等着。库存不会超卖,这就是分布式锁的作用。

如果业务逻辑执行时间长,用 AutoRenewalLock 可以自动续期,不用担心锁提前过期。

## 总结

这篇文章从需求出发,手把手教你用 Go 和 Redis 实现分布式锁。核心就三个操作:用 SETNX 获取锁、用 Lua 脚本释放锁、用看门狗自动续期。

实际项目中,建议直接上 Redisson 这个库。生产环境坑太多,成熟方案经过大量验证,比自己写的靠谱。但理解底层原理对排查问题、优化性能都很有帮助。

希望这篇文章对你有启发。如果你在项目中遇到分布式锁的问题,欢迎评论区交流。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇