背景:比如我有100张点卡,有两台服务器同时进行卖这个点卡,但是今天我就想卖10张,超出10张我就不卖了,在多线程的额情况下很容易出现卖出了11张甚至更多,这也是超卖的问题,从实现来说也可能出现两个人获取到的是同一张点卡,那么也是不可取的 

出于一个涉世未深,对那些高大上的东西充满好奇的我希望可以使用自己没用过的东西来解决上面的问题,所以我选择了使用分布式锁来解决分布式情况下超卖的问题 

如果程序只在一台服务器里跑,那么我们可以很简单的在需要控制同步的地方使用锁lock,或者使用sychronized,但是在多台服务器的情况下如此使用是不可以的,但是可以选择在数据库层面通过悲观锁、乐观锁解决,亦或redis的消息队列..还有其他的一些方案

言归正传,本人将阐述分布式锁从无到有,遇到了哪些问题,网上有两种redis分布式锁的实现方案

1.采用setNX配合getSet超时方案

 

步骤如下:
1.使用setNX(lock,时间戳)方式,进行获取锁,如果获取到锁,返回为1 dosomething(),没有获取到返回为0并执行第2步
2.使用get(lock)方法去获取lock对应的时间戳,并判断时间是否已经超时,如果超时了进入第3步
3.使用getset方法将新锁添加到redis中,并判断getset的返回值,是否和2获取到的value值相同,如果相同说明获取到了锁 dosomething(),所以不同则说明锁已经被别人获取到了
4.执行成功释放锁del

dosomething()为自己操作的内容

以上的实现是有问题的,情况在于,如果1号线程在执行第3步并且也获取到锁了的时候2号线程执行了第4步,那么就会出现3号线程执行第1步的时候也获取到锁了,有点绕口,其实就是说,如果一个线程通过getset方法发现占用锁的线程超时了而且我也获取到新锁了,那个超时的线程正好也执行完了做了释放锁的操作,那么肯定会出现第三个锁通过 setNX方法又获取到了新锁。

如果你真正的去尝试写了这么一段代码,你也会发现这里会出现一个问题,经常报空指针异常,问题就在于,有线程释放锁了之后存在一个线程过了get操作,然后将获取的null做超时判断。

2.循环采用setNX方案

步骤如下:
1.使用setNX(lock,时间戳)方式,进行获取锁,如果获取到锁,返回为1并执行第二步,没有获取到返回为0重新执行第1步
2.通过expire设置超时时间,dosomething();
3.执行成功释放锁del;

以上的方法实现获取觉得没有任何的问题,但是依然存在很大的隐患,隐患在于,如果某个线程在第1步获取到锁了但是在执行第2步的时候出现了异常导致设置超时时间失败,那么该锁就一直不会失效,最后导致下面的所有的线程都无法再获取到锁

 

 

就在我觉得redis无法实现分布式锁的时候,我在公司的关于redis方法的封装中发现了一句代码

String result = jedis.set(key, value, "NX", "EX", seconds);

于是我上网查阅了一下资料,了解到这一句的代码的意思是:设置键key,超时时间为seconds,并且如果该键存在则设置失败,该键不存在则设置成功

于是我将第二个方案重新改变了一下

步骤如下:
1.set(key, value, "NX", "EX", seconds)方式获取锁,如果获取到锁,执行2,没有继续执行1
2.dosomething
3.执行成功释放锁del

这个时候我觉得下面的写法已经很正确了

//获取锁
    public Boolean requireLock(String key, String value ,int seconds) {
        String result = jedis.set(key, value, "NX", "EX", seconds);
        return (result != null) && ("OK".equals(result) || "+OK".equals(result));
    }
    //释放锁
    public Boolean releaseLock(String key) {
        return jedis.del(key) == 1;
    }

就在那么一天我看到了一个redis的文档,里面有那么几段内容

分布式锁应该参考the Redlock algorithm的实现,应该这个方法只是复杂一点,但是却能保证更好的使用

避开上面说的不提,下面的一段内容吸引了我的注意

 

可以通过如下优化使得上面的锁系统变得更加棒:
不要设置固定的字符串,而是设置为随机的大字符串,可以称为token。
通过脚步删除指定锁的key,而不是DEL命令。
上述优化方法会避免下述场景:a客户端获得的锁(键key)已经由于过期时间到了被redis服务器删除,但是这个时候a客户端还去执行DEL命令。而b客户端已经在a设置的过期时间之后重新获取了这个同样key的锁,那么a执行DEL就会释放了b客户端加好的锁。

 

 

突然觉得好像是那么一回事哦

所以我给代码又做了修改

//获取锁
    public Boolean requireLock(String key, String value ,int seconds) {
        String result = jedis.set(key, value, "NX", "EX", seconds);
        return (result != null) && ("OK".equals(result) || "+OK".equals(result));
    }
    //释放锁
    public Boolean releaseLock(String key, String value) {
		//避免释放其他线程的锁
		if (value.equals(jedis.get(key))) {
			return jedis.del(key) == 1;
		}
        return false;
    }

上面的代码要保证value是一个随机字符串。如果不用时间戳的更好

 

 

附言:

可能很多人觉得使用最后的方案应该就成功了,但是笔者真真实实的遇到了一个问题——重排序,就是dosometing()方法的在releaseLock方法后面执行了,导致我在测试的时候,发现存在两个线程同时获取到了锁的情况,当我加了同步代码块之后,这个问题也就不再存在了。