Go语言实现Redis分布式锁

本文详细介绍了如何使用Go语言和go-redis库实现一个简单的分布式锁,包括连接Redis、基于SETNX的初步锁实现、防误删的唯一标识、以及解锁的原子化处理,确保高可用性和并发控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于go-redis的设计与实现

本文将基于go语言,使用了一个常用的go Redis客户端 go-redis库 , 一步一步探索与实现一个简单的Redis分布式锁。

项目地址https://github.com/liwook/Redislock

连接Redis

​
func NewClient() *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",    //自己的redis实例的ip和port
		Password: "",    //密码,有设置的话,就需要填写
	})
}

func main() {
	client := NewClient()
	defer client.Close()

	val, _ := client.Ping().Result()    //测试ping
	fmt.Println(val)
}

1.基于 SETNX 的锁初步实现

SETNX 命令用于在Redis中设置某个不存在的键的值。如果该键不存在,则设置成功,如果该键存在,则设置失败,不作任何动作。基于此可以实现一种简单的抢占机制。

新建lock.go文件。创建lock结构体,添加加锁解锁方法。

结构体RedisLock有成员key,过期时间expire,连接的redis客户端redisCli。

var (
	defaultExpireTime = 5 * time.Second
)

type RedisLock struct {
	key      string
	expire   time.Duration
	redisCli *redis.Client
}

func NewRedisLock(cli *redis.Client, key string) *RedisLock {
	return &RedisLock{
		key:      key,
		expire:   defaultExpireTime,
		redisCli: cli,
	}
}

加锁

func (lock *RedisLock) Lock() (bool, error) {
	return lock.redisCli.SetNX(lock.key, "111111", lock.expire).Result()
}

上面的加锁是一种简单的方法,非阻塞的,一有结果就直接返回,也不再二次尝试的。lock.redisCli.SetNX(lock.key, "111111", lock.expire) 这行代码本质上执行了如下Redis操作命令:

set key 111111 ex 5 nx

该命令为 my_lock 键以 NX 方式设置了值。

如果持有锁的进程万一挂了,那么该键将永远存在与Redis中,其他竞争者无法进行 SETNX 操作,形成死锁。为了防范这种情况发生,这里设置了过期时间为5s,这样即便持锁者挂了,锁在一定时间后依然后自动释放。这里整个 set 操作是原子性的,并对该操作的返回结果作了判断,如果成功设置,说明抢占锁成功,则函数返回,进入临界区可以继续执行下面的代码。

解锁

func (lock *RedisLock) Unlock() error {
	res, err := lock.redisCli.Del(lock.key).Result()
	if err != nil {
		return err
	}
	if res != 1 {
		return errors.New("can not unlock because del result not is one")
	}
	return nil
}

上述代码中,lock.redisCli.Del(lock.key)对Redis中的lock 键进行了删除操作,当删除后,其他竞争者才有机会对该键进行 SETNX操作。

测试使用

func main() {
	client := NewClient()
	defer client.Close()

	val, _ := client.Ping().Result()
	fmt.Println(val)

	key := "mylock"
	lock1 := redislock.NewRedisLock(client, key)
	lock2 := redislock.NewRedisLock(client, key)

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		//尝试获取锁
		if success, err := lock1.Lock(); success && err == nil {
			fmt.Println("go lock get..")
			time.Sleep(4 * time.Second)
			lock1.Unlock()
		}
		wg.Done()
	}()

	//尝试获取锁
	if success, err := lock2.Lock(); success && err == nil {
		fmt.Println(" main lock get...")
		time.Sleep(7 * time.Second)
		lock2.Unlock()
	}
	wg.Wait()
}

2.锁的防误删实现

上面的就使用Redis实现了一个简单的分布式锁。但会存在个问题,想象一个场景:这个键过期了,但是其持有者线程A仍未完成任务。但这时该键就已经没有,线程B就去获取锁,获取成功了。这时候线程A完成了任务,就去删除键。而这时键是被线程B持有的,而线程A却可以去删除,这就会出了问题。

所以,这里要解决的是只有自己才能删除自己创建的锁。为了解决这种问题,持有者可以给锁添加一个唯一标识,使之只能删除自己的锁。因此需要完善一下加解锁操作:

在结构体RedisLock中添加字段Id,这是唯一标识符,用uuid表示。

在创建锁时候,需要创建出uuid,并赋值给字段Id。

type RedisLock struct {
	key      string
	expire   time.Duration
	Id       string //锁的标识,新添加的,也即是键的value
	redisCli *redis.Client
}

func NewRedisLock(cli *redis.Client, key string) *RedisLock {
	id := strings.Join(strings.Split(uuid.New().String(), "-"), "")
	return &RedisLock{
		key:      key,
		expire:   defaultExpireTime,
		Id:       id,
		redisCli: cli,
	}
}

 那么在加锁的时候,把lock.Id给value赋值。

func (lock *RedisLock) Lock() (bool, error) {
	return lock.redisCli.SetNX(lock.key, lock.Id, lock.expire).Result()
}

//对比之前的
//func (lock *RedisLock) Lock() (bool, error) {
//	return lock.redisCli.SetNX(lock.key, "111111", lock.expire).Result()
//}

解锁的时候,需要先判断锁的唯一标识值是否是与当前拥有者相匹配,若匹配再进行删除。

// 锁的误删除实现
func (lock *RedisLock) Unlock() error {
	//获取锁并进行判断该锁是否是自己的
	val, err := lock.redisCli.Get(lock.key).Result()
	if err != nil {
		fmt.Println("lock not exit")
		return err
	}
	if val == "" || val != lock.Id {
		return errors.New("lock not belong to myself")
	}

	//进行删除锁
	res, err := lock.redisCli.Del(lock.key).Result()
	if err != nil {
		return err
	}
	if res != 1 {
		return errors.New("can not unlock because del result not is one")
	}
	return nil
}

3.解锁的原子化实现

上面的解锁操作中,仍然是存在一个问题的:在确认当前锁是自己的锁后,和删除锁之前,这个时间段,中途可能会进行阻塞,这个过程中,锁恰巧过期释放,且被其他竞争者抢占。那就有可能会删除了其他竞争者的锁。这是不妥的。

我们要把这两个操作变成原子操作,将整个解锁过程原子化,使得在解锁期间,其他竞争者的任何操作不能被Redis执行。

Redis中可以使用Lua脚本把一系列操作变成原子操作。

func (lock *RedisLock) Unlock() error {
	script := redis.NewScript(LauCheckAndDelete)
	res, err := script.Run(lock.redisCli, []string{lock.key}, lock.Id).Int64()
	if err != nil {
		return err
	}
	if res != 1 {
		return errors.New("can not unlock because del result not is one")
	}
	return nil
}


//lua.go
const (
	LauCheckAndDelete = `
		if(redis.call('get',KEYS[1])==ARGV[1]) then
		return redis.call('del',KEYS[1])
		else
		return 0
		end
	`
)

确认锁与删除锁的整体操作进行了原子化,便可以防止上述存在的误删问题。

4.小结

基于Redis的分布式锁的实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存锁的唯一标识
  • 释放锁时先判断唯一标识是否与自己一致,一致则删除锁
  • 删除锁时候用lua脚本把判断锁唯一标识和删除锁进行原子化

其特性:

  • 利用set nx满足互斥性
  • 利用set ex来保证故障时锁依然能释放,避免死锁
  • 利用Redis集群可以保证高可用和高并发特性
<think>好的,用户想了解如何用Go语言基于Redis实现分布式锁的最佳实践和示例代码。首先,我需要回忆一下分布式锁的基本概念和Redis的相关命令。记得Redis的SET命令加上NX和PX选项可以实现原子性的加锁操作,这样可以避免竞态条件。然后,解锁时需要确保只有锁的持有者才能释放锁,通常用Lua脚本来保证操作的原子性。 接下来,我需要考虑Go语言中的具体实现。用户可能希望看到具体的代码示例,比如使用github.com/go-redis/redis库来连接Redis,并编写加锁和解锁的函数。同时,还要处理锁的超时和自动续期问题,防止因为业务执行时间过长导致锁自动释放。此外,可重入锁可能也是一个需要考虑的高级特性,但根据引用内容,用户提供的引用中有提到可重入锁的实现,但可能用户更关注基础实现,所以可能需要先介绍基本方法,再提及可重入的扩展。 然后,引用中提到了redsync这个库,根据引用3,官方推荐使用redsync,所以可能需要介绍这个库的使用方法,并给出示例代码。同时,要比较手动实现和使用库的优缺点,比如redsync支持多Redis实例的RedLock算法,提高可靠性,而单实例实现更简单但可能有单点故障风险。 另外,需要确保回答覆盖最佳实践,比如设置合理的超时时间、避免长时间阻塞、处理网络分区等。还要注意错误处理,比如加锁失败后的重试策略,以及解锁时可能遇到的错误。 最后,根据用户提供的引用,特别是引用1、2、3,需要正确引用相关内容,比如引用1提到使用SET命令,引用2提到Lua脚本和可重入锁,引用3提到redsync库。因此,在回答中要自然地插入这些引用,确保信息的准确性和来源的可信度。 可能还需要生成相关问题,比如如何实现锁的可重入性,如何处理锁的自动续期,RedLock算法的原理等。这些问题可以帮助用户进一步深入学习。 总结步骤:介绍基本实现方法,给出代码示例;介绍redsync库的使用;讨论最佳实践和注意事项;引用相关资料;生成相关问题。</think>### Go语言基于Redis实现分布式锁的最佳实践 #### 一、基础实现方法(单Redis实例) 1. **加锁** 使用`SET`命令的`NX`(不存在时设置)和`PX`(过期时间)参数: ```go func AcquireLock(client *redis.Client, key string, value string, ttl time.Duration) (bool, error) { result, err := client.SetNX(context.Background(), key, value, ttl).Result() return result, err } ``` - `key`为锁的唯一标识,`value`通常为随机字符串(如UUID),用于安全释放锁[^1]。 2. **解锁** 使用Lua脚本保证原子性操作: ```lua if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end ``` Go代码实现: ```go func ReleaseLock(client *redis.Client, key string, value string) (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(context.Background(), client, []string{key}, value).Int64() return result == 1, err } ``` **关键点**:验证`value`匹配后才删除锁,避免误删其他客户端的锁[^2]。 --- #### 二、使用Redsync库(推荐生产环境) Redis官方推荐的Go实现库`redsync`支持多节点容错(RedLock算法): 1. **安装依赖** ```bash go get github.com/go-redsync/redsync/v4 go get github.com/gomodule/redigo/redis ``` 2. **示例代码** ```go package main import ( "github.com/gomodule/redigo/redis" "github.com/go-redsync/redsync/v4" "github.com/go-redsync/redsync/v4/redis/redigo" ) func main() { // 创建Redis连接池 pool := redigo.NewRedisPool(&redis.Pool{ Dial: func() (redis.Conn, error) { return redis.Dial("tcp", "localhost:6379") }, }) // 创建Redsync实例 rs := redsync.New(pool) // 获取锁 mutex := rs.NewMutex("my-distributed-lock", redsync.WithExpiry(10*time.Second)) if err := mutex.Lock(); err != nil { panic(err) } // 执行业务逻辑 // ... // 释放锁 if ok, err := mutex.Unlock(); !ok || err != nil { panic("unlock failed") } } ``` **优势**: - 自动处理锁续期(需配合`WithTries`配置重试) - 支持多Redis实例的RedLock算法(需传递多个连接池)[^3] --- #### 三、最佳实践 1. **设置合理的超时时间** 根据业务逻辑耗时设置`ttl`,避免锁长期未释放导致死锁。 2. **避免单点故障** 生产环境建议使用Redis集群或RedLock算法(多实例部署)。 3. **错误处理** - 加锁失败时实现重试机制(如指数退避) - 解锁失败需记录日志并人工介入检查 4. **锁续期** 对于长耗时任务,需后台定期续期: ```go func ExtendLock(mutex *redsync.Mutex) { go func() { for { select { case <-time.After(5 * time.Second): ok, err := mutex.Extend() if !ok || err != nil { return } } } }() } ``` --- #### 四、注意事项 - **网络分区问题**:Redis主从切换可能导致锁状态不一致,RedLock可缓解此问题 - **时钟同步**:多实例部署时需确保机器时钟同步 - **性能影响**:频繁加锁可能增加Redis负载,需监控QPS ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值