Reids实现简单分布式锁
什么是分布式锁
分布式锁,简单来说就是在分布式环境下(也称为云环境)不同实例之间抢一把锁。

和普通的锁比起来,也就是抢锁的从线程 (协程)变成了实例。
分布式锁之所以难,基本上都和网络有关。
- 请求可能会超时
怎么设计分布式锁
使用redis设计分布式锁可以很容易地想到使用SETNX,设置一个不可更改的键值对。基本过程分为加锁和解锁主要两个操作。在某些需要原子操作的情况下,可以配合lua脚本一起。
加解锁流程
- 加锁通过 - SETNX,存入一个key,对应的值是一个随机的- uuid,并且需要设置过期时间,最后返回一个- Lock结构体。
- 解锁需要先验证uuid,如果验证成功,则释放锁。 
问题
- 为什么需要设置过期时间? - 如果没有过期时间,那么会出现「实例1」抢到锁之后,不小心奔溃了,终止了运行,后续的实例将永远无法获取到锁。 
- 为什么需要设置随机uuid为key的值? - 本质上,我们就是需要一个唯一的值,当解锁的时候,需要验证该把锁是不是自己的,防止把其他实例加的锁解锁了。 
- 什么情况下可能会出现错误? - 加锁解锁都可能遇到诸如网络错误,redis服务端错误等错误的情况,这种都会将erro返回。 - 加锁可能会遇到该锁已经被他人持有,而无法加锁
- 解锁可能会遇到1. 不是该锁的持有人 2. 锁过期了 3. 锁被人删了 等情况,而无法解锁
 
- 为什么要使用lua脚本来解锁? - 当我们发送查询操作时,返回结果后,再发送删除操作,这两个操作中间可能会出现延迟(因为网络条件),导致出现误删他人的锁的情况。 
代码
| 1 | package redisLock | 
优化分布式锁
手动续约
在某些情况下,用户端可能会执行超时,这时只能眼巴巴地看着锁过期,于是为了优化分布式锁,我们加上了一个续约方法!能让用户可以自行手动续约~
首先在Lock结构体添加一个过期时间属性:
| 1 | type Lock struct { | 
在加锁的时候,将过期时间传入返回的Lock中。
再构造如下的续约方法:
| 1 | func (l *Lock) ManualRefresh(ctx context.Context) error { | 
这个方法使用到了lua脚本:
| 1 | if redis.call("GET", KEYS[1]) == ARGV[1] then | 
这里有两个返回error的地方:
- 第一个地方是意味着可能服务器出错了,或者超时了 
- 第二个地方意味着锁确实存在,但是却不是自己的锁,或者锁没有了 
自动续约
手动续约的方法看起来很简单,但是在用户实现的时候会产生很多困难:
- 间隔多久调一次续约方法? - 这个还比较简单,可以约定一个时间,然后一直进行for循环(提示:用 - ticker)
- 如果续约方法请求出错怎么办? - 可能是出现网络问题,或者Redis服务器奔溃,这个时候一般进行再次重试。但假如一直失败就会一直重试
- 返回其他的error,将如何处理?
 
- 如果真的续约失败,用户将如何处理? 
这时候我们可以自己封装一个AutoRefresh方法给用户:
| 1 | func (l *Lock) AutoRefresh(interval time.Duration, timeout time.Duration) error { | 
自动续约的可控性非常差,因此我们并不是很鼓励用户使用这个API。甚至于如果用户想要万无一失地使用这个布式锁,那么必须要自己手动调用Refresh,并且小心处理各种error。
此外,续约的间隔,应该综合考虑服务可用性。例如如果我们将分布式锁的过期时间设置为10秒,而且预期2秒内绝大慨率续约成功,那么就可以考虑将续约间隔设置为8秒。
加锁重试
加锁可能遇到偶发性的失败,在这种情况下,可以尝试重试。重试逻辑:
- 如果超时了,则触发重试逻辑,判断Key是否存在 
- 若Key不存在,「直接加锁」 
- 若Key存在,则检查一下key对应的值是不是我们刚才超时加锁请求的值 - 如果是,直接返回,前一次「加锁成功」了(这里你可能需要考虑重置一下过期时间) 
- 如果不是,直接返回,「加锁失败」 
 
这里有三种可能:
- key不存在,直接加锁,返回成功
- key存在,且value值相等,返回成功
- key存在,但value值不相等,返回失败
总结
分布式锁是一个比较简单的工具,在Redis加持下响应本来就很快,但是不完全可靠,最重要的还是要优化自己的业务性能。
- 使用分布式锁,你不能指望框架提供万无一失的方案,自己还是要处理各种异常情况(超时) 自己写分布式锁,要考虑过期时间,以及要不要续约。 
- 不管要对锁做什么操作,首先要确认这把锁是我们自己的锁。 
- 大多数时候,与其选择复杂方案,不如直接让业务失败,可能成本还要低一点。(有时候直接赔钱,比你部署一大堆节点,招一大堆开发,搞好几个机房还要便宜,而且便宜很多)也就是选择恰好的方案, 而不是完美的方案。 









