1、在分布式系统中,我们使用锁机制只能保证同一个JVM中一次只有一个线程访问,但是在分布式的系统中锁就不起作用了,这时候就要用到分布式锁(有多种,这里指 redis)
2、在 redis当中可以使用命令 setnx(key, value)来实现分布式锁
setnx:当key不存在的时候设置成功,返回1,若存在的话返回0表示失败。使用这个命令的话要搭配 expire(key, time)来设置过期时间,但是这种组合存在问题,如下:
try { if (redisclient. setnx(key 1) == 1) { //1 redisClient.expire(key, 1000);//2 } } finally { redisClient.del(key); }
如上代码,看上去没什么问题,但是极端情况下如果在1处执行完毕2处还没执行这时候这台机器宕机了,那么这个锁将是无期限的,且不会被删除,也就是说设置setnx和expire是两个命令,不具备原子性。 针对这个问题,可以使用 redis2.6版本之后的命令set(key, value, expirefime,NX)来解决,这个命令跟 setnx一样,但是多了过期时间,可以很好的解决这个问题。
3、解决这个问题之后,还存在着一个问题:如果在过期时间内程序代码没执行完,那么其他其他机器线程获得这个锁,这样会造成同时有两个线程执行一段代码,并且A机器(过期还没执行完)中finally会删除key,导致误删到B机器锁(当前获得锁的机器)的情况
1、此时我们可以在相同的机器上开一个守护线程(如上面例子就在A机器再开一个守护线程),这个线程主要作用是在key快过期的时候进行续命操作,保证代码执行完毕。 2、关于误删,我们可以把value设成线程id,删除前判断一下是否是自己的线程ID,是的话再执行删除,如下面代码
try { .... } finally { if (threadId equals(redisclient get(key)) {//1 redisclient.del(key);//2 } }
这时一般情况都没问题,但是这里的1和2又跟前面的问题类似---不具备原子性,所以还是有出错的可能,但是 redis中没有支持获取删除的原子性命令,该怎么解决呢? 我们可以通过工Lua脚本来解决,例如本例中可以像下面这么写
String luascript = "if redis call('get', KEYS[1])==ARGV[l] then return redis.call('del', KEYS[1]) else return 0 end";
redisClient.eval(luaScript, Collections.singletonList(key), Collections.singletonList(threadId));
4、到这里,基本就没什么问题了,最终的代码如下
try { String luascript = "if redis call('get, KeY[1])==ARGV[l] then return redis.call('del', KEY[1]) else return 0 end"; string threadId = Thread.currentThread() getId(); while (redisclient set(key, threadId, 1000, NX) == 1) { // dosomething() } } finally { redisClient.eval(luaScript, Collections.singletonList(key), Collections.singletonList(threadId)); }