分布式锁

顾名思义,分布式锁就是应用于分布式系统中的一种锁。在讨论如何使用分布式锁之前,先要搞明白为什么需要分布式锁。也即要弄明白,是什么,为什么,干什么。在一般的多线程编程中,编程语言本身提供了锁这个功能。以Go语言为例,官方标准库中提供了sync包用于实现互斥锁。但这个功能也是有限制的,它只能在单台服务器中使用,如果系统只在一台服务器中部署,那么分布式锁就用不上了。在服务器集群中,两台服务器是不能直接交流的,在有资源竟争的场景中,锁又是必需要使用的。为了解决这一冲突,于是就有了分布式锁这个概念。

以我们日常抢高铁票的例子来说:票的数量是固定的,但抢票的人数大部份情况下是多于票数的。也就是说,100张票可能会有200个人抢。如果这两百个人都在同一个服务器上购票,那么就可以使用go语言提供的互斥锁去防止100张票同时卖给了200个人的情况。但是现在的服务器基本不会只部署在一台机器上,那么就编程语言本身提供的锁这个功能就用不上了。这种情况下,就需要引入一个第三方,用于控制车票这个共享资源的在效分配。而这个第三方,就是我们常用的redis。

Redis实现分布式锁

如果把整个分布式系统理解为单台服务器,那么每台服务器就是一个单独的线程,redis就充当编程语言自带的锁。所以本质上,分布式锁和编程语言本身提供的锁是一样的,只是应用场景有点区别,本质是同一个东西:用于控制资源的有序分配。

redis实现分布式锁需要使用两个命令setnx和expire。setnx用于设置一个key值,如果key值存在则设置失败,不存在则设置成功。expire用于设置key值的过期时间,在某一线程执行过程中如果服务宕机或其他原因导致一直没有响应,则让redis将这个key值清理。具体流程应该是这样的:

由上面的图可以看出,有三台服务器,可以把这三台服器理解为三个线程,这三个线程同时往redis服务器中设置一个相同的key值,由redis控制谁能设置成功。三个线程分别判断是否设置成功,设置成功的线程自动进入下一步操作,设置失败的线程则终止后续操作。设置成功的线程在处理完业务后再返回redis删除自已设置的key值。在设置成功的线程处理业务逻辑时,其后进来的线程也会同时设置同样的key值,但redis会告诉他设置失败。所以就实了一个最简单的分布式锁。为什么说是最简单呢,因为还有许多漏洞没有处理。

Redis分布式锁存在的问题分析

从上面的例子可以看出,如果ServerA在设置key成功后,在处理业务的过程中宕机了,那么是不是意味着redis中的key会永远留在那里,后面来的线程是不是全部进不来。对于这个问题,解决办法也很简单粗暴,就是对key值写加一个过期时间。如果ServerA在处理业务的过程中宕机了,那么redis会有指定时间清除该key,以保证服务的可用性。但即使是这样,还是会有问题的。

如果ServerA在设置key完成后,在设置key失效时间这一过程中宕机了,是不是还是一样?再比如说,ServerA设置key值和过期时间都成功,假如key的过期时间为10秒,如果ServerA处理业务的时间超过了10秒,redis就清除了ServerA设置的key其他线程是不是就进来了。

是不是有种不论你怎么努力都无法让别人满意的感觉。如果使用Go语言操作时,可以有一个比较讨巧的方法,就是把key的过期时间精确算到毫秒,每执行一步就起一个协程去更新redis中的key值过期时间,不过不建议这样操作,毕竟这个时间不好控制。

后语

使用上面的方法实现分布式锁,基本可以解决大部份应用场景,但是在更高并发的场景中,还是会显得有点力不从心。