# 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 这个库。生产环境坑太多,成熟方案经过大量验证,比自己写的靠谱。但理解底层原理对排查问题、优化性能都很有帮助。
希望这篇文章对你有启发。如果你在项目中遇到分布式锁的问题,欢迎评论区交流。