当前位置: 代码网 > it编程>数据库>Redis > Go语言实现Redis分布式锁

Go语言实现Redis分布式锁

2024年07月28日 Redis 我要评论
本文将基于go语言,使用了一个常用的go Redis客户端一步一步探索与实现一个简单的Redis分布式锁。SETNX 命令用于在Redis中设置某个不存在的键的值。如果该键不存在,则设置成功,如果该键存在,则设置失败,不作任何动作。基于此可以实现一种简单的抢占机制。先连接redis。新建lock.go文件。创建lock结构体,加锁和解锁方法。

基于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集群可以保证高可用和高并发特性
(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com