Redis分布式锁

2022-08-19 11:11:37 浏览数 (1)

前言

随着分布式系统的普遍运用,分布式锁的重要性也得到了体现

在单机系统中,我们可以运用普通的锁/信号量机制来实现对公共资源的有序访问;但在分布式系统中显然就不行了

因此业界常用的解决方案通常是借助于一个第三方组件,利用它自身的排他性来达到多进程的互斥;如:

  • 基于 DB 的唯一索引
  • 基于 ZK 的临时有序节点
  • 基于 Redis 的 NX EX 参数

本文就主要以Redis分布式锁展开

需要了解的几个词

  • 锁机制:是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足(Wiki百科)
  • 死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
  • ZK:即ZooKeeper,ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等

Redis分布式锁的基本原理

既然是选用了 Redis,那么它就得具有排他性才行;同时它最好也有锁的一些基本特性:

  • 高性能(加、解锁高性能)
  • 可以使用阻塞锁与非阻塞锁
  • 不能出现死锁
  • 可用性(不能出现节点挂掉后加锁失败)

这里利用 Redis set key 的 NX 参数来保证在这个 key 不存在的情况下写入成功,并且再加上 EX 参数设置过期时间

利用以上两个特性可以保证在同一时刻只会有一个进程获得锁,并且不会出现死锁(过期自动删除 key)

加锁

Show me the code:

代码语言:javascript复制
func GetLock(lockName string, acquireTimeout time.Duration, lockTimeOut time.Duration) (string, error) {
	code := uuid.NewV4().String()
	endTime := util.FwTimer.CalcMillis(time.Now().Add(acquireTimeout))
	for util.FwTimer.CalcMillis(time.Now()) <= endTime {
		if success, err := fwRedisClient.SetNX(lockName, code, lockTimeOut).Result(); err != nil && err != redis.Nil {
			return "", err
		} else if success {
			return code, nil
		} else if fwRedisClient.TTL(lockName).Val() == -1 { //-2:失效;-1:无过期;
			fwRedisClient.Expire(lockName, lockTimeOut)
		}
		time.Sleep(time.Millisecond)
	}
	return "", errors.New("timeout")
}

加锁很简单,直接setnx,监控下返回值和error就行啦

解锁

到这里我曾经以为解锁也跟加锁一样简单,直接del就完事了,然而并没有那么简单

如果进程 A 获取了锁设置了超时时间,但是由于执行周期较长导致到了超时时间之后锁就自动释放了。这时进程 B 获取了该锁,然而这时进程 A 执行完了,释放了该锁;这样就会出现进程 A 将进程 B 的锁释放了

所以最好的方式是在每次解锁时都需要判断锁是否是自己

这时就需要结合加锁机制一起实现了

在上面的加锁实现中可以看到设置value为code时同时也返回了这个uuid,这样解锁时我们可以判断get到的值是否与自己的uuid相同,来决定是否解锁,同时 WATCH 锁保证不会错误的释放锁

Show me the code:

代码语言:javascript复制
func ReleaseLock(lockName string, code string) bool {
	txf := func(tx *redis.Tx) error {
		if v, err := tx.Get(lockName).Result(); err != nil && err != redis.Nil {
			return err
		} else if v == code {
			_, err := tx.Pipelined(func(pipe redis.Pipeliner) error {
				//count  
				//fmt.Println(count)
				pipe.Del(lockName)
				return nil
			})
			return err
		}
		return nil
	}

	for {
		if err := fwRedisClient.Watch(txf, lockName); err == nil {
			return true
		} else if err == redis.TxFailedErr {
			fmt.Println("watch key is modified, retry to release lock. err:", err.Error())
		} else {
			fmt.Println("err:", err.Error())
			return false
		}
	}
}

这样也就能满足锁的四个基本特性了:

  • 高性能(加、解锁高性能)
  • 可以使用阻塞锁与非阻塞锁
  • 不能出现死锁
  • 可用性(不能出现节点挂掉后加锁失败)

总结

Redis分布式锁应该是比较简单的分布式锁了,同时本文介绍的也只是redis分布式锁的基本实现,在实际运用场景中还可以使用不同的方式实现 同时基本的实现方法也还存在一些问题,无法保证高可用,如:

  • 超时解锁导致并发业务
  • 客户端无法等待锁释放

0 人点赞